« ■TEAD for CodeGear RAD Studio | トップページ | ☆DBGridに複数行選択のオペレーション機能を追加する。 »

☆DBGridにインデント機能を付ける。

前回、☆TEditにインデント機能をつける。 で作成したインデント機能をDBGridのInplaceEditorに実装してみます。

当初、インデント処理をクラスにしてInplaceEditorをプロパティで与えて処理させようと したのですが、そこはやはりDBGrid・・・データベースに対しての処理が必要になり、 結局カスタムコンポーネントとしました。

今回一番悩んだのは、加工した文字列をInplaceEditorに設定する部分です。
SelectedFieldに設定してPostしてしまえば、簡単に実現できたのですが、 それではDBGridの動作とかけ離れてしまいます。

処理のポイントとしては、
1.インデント処理以前に Key := #0 でキーを無効にしておく。
2.SelectedFieldのDataSetを編集モードにしておく。
3.InplaceEditor.TextではなくSelectedFieldに加工した文字列を設定する。
4.次の編集処理のため、Postしない。
です。

インデント処理によるデータ落ちを防ぐため、フィールド長さが超えるものは処理させません。 ですから、テストするときにフィールド長さが短いものやデータの文字列自身が長い場合には 動作していないように見えますのでご注意して下さい。 又、要素間の移動は、前回より拡張して、要素の前後に移動するようにしました。
unit HRIndentDBGrid;

interface

uses
  Windows, Messages, SysUtils, Classes, DB, Grids, DBGrids, AdjustedEdit;

type
  TIndentPosList = array of Integer;

  THRIndentDBGrid = class(TDBGrid)
  private
    FIndentPosList: TIndentPosList;
    function GetIndentPos: String;
    procedure SetIndentPos(Value: String);
    procedure MakeIndentPosList(Indent: String);
    procedure GetElementPosList(S: String; var List: TIndentPosList);
    procedure PriorIndent;
    procedure NextIndent;
    procedure PriorElement;
    procedure NextElement;
    function IndentKeyPress(Key: Char): Boolean;
  protected
    procedure KeyPress(var Key: Char); override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property IndentPosition: String read GetIndentPos write SetIndentPos;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Hiderin', [THRIndentDBGrid]);
end;

{ THRIndentDBGrid }

constructor THRIndentDBGrid.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  IndentPosition := '1';
end;

procedure THRIndentDBGrid.KeyPress(var Key: Char);
begin
  // TStringFieldの場合のみインデント処理を可能します。
  if (InplaceEditor <> nil) and (SelectedField.DataType = ftString) and
  IndentKeyPress(Key) then
  begin
    key := #0; // 2度設定していることになるが、
               // これを入れないと要素間移動時にポンと音が鳴ります。
    Exit;
  end;
 inherited KeyPress(Key);
end;

function THRIndentDBGrid.IndentKeyPress(Key: Char): Boolean;

  procedure DefaultResult;
  begin
    Result := True;
    Key := #0;
  end;

begin
  Result := False;
  // Ctrl + Iで指定位置にエレメントを移動させます。
  if (GetKeyState(VK_SHIFT) and $80 = 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #9) then
  begin
    DefaultResult;
    NextIndent;
  end;

  // Ctrl + Shift + Iで指定位置にエレメントを移動させます。
  if (GetKeyState(VK_SHIFT) and $80 > 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #9) then
  begin
    DefaultResult;
    PriorIndent;
  end;

  // 要素開始位置へキャレットを移動させます。CTRL+U
  if (GetKeyState(VK_SHIFT) and $80 = 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #21) then
  begin
    DefaultResult;
    NextElement;
  end;

  // 要素開始位置へキャレットを移動させます。CTRL+SHIFT+U
  if (GetKeyState(VK_SHIFT) and $80 > 0) and
     (GetKeyState(VK_CONTROL) and $80 > 0) and
     (Key = #21) then
  begin
    DefaultResult;
    PriorElement;
  end;
end;

function THRIndentDBGrid.GetIndentPos: String;
var
  I: Integer;
begin
  Result := IntToStr(FIndentPosList[Low(FIndentPosList)]);
  for I := Low(FIndentPosList) +1 to High(FIndentPosList) do
    Result := Result + ',' + IntToStr(FIndentPosList[I]);
end;

procedure THRIndentDBGrid.SetIndentPos(Value: String);
begin
  MakeIndentPosList(Value);
end;

procedure SpaceAdd(Count: Integer);
var
  I: Integer;
begin
  keybd_event(VK_CONTROL, 0, 0, 0);
  for I := 0 to Count- 1 do
  begin
    keybd_event(VK_SPACE, 0, 0, 0);
    keybd_event(VK_SPACE, 0, KEYEVENTF_KEYUP, 0);
  end;
  // VK_CONTROLのKeyUpは不要です。
  // ユーザーがCTRL+○と操作しているからです。
end;

procedure THRIndentDBGrid.NextIndent;
var
  P1, P2: Integer;
  I,J: Integer;
  S1, S2, S3: String;
begin
  SendMessage(InplaceEditor.Handle, WM_SETREDRAW, Ord(False), 0);
  try
    P1 := GetSelStart(InplaceEditor);
    P2 := 0;

    for I := 0 to High(FIndentPosList) do
      if FIndentPosList[I]-1 > P1 then
      begin
        P2 := FIndentPosList[I]-1;
        Break;
      end;

    if P2 = 0 then
    begin
      P2 := P1;
      Exit;
    end;

    S1 := Copy(InplaceEditor.Text, 1, P1);
    S2 := Copy(InplaceEditor.Text, P1+1, Length(InplaceEditor.Text)-P1);

    J := P2 - P1;

    S3 := S1 + StringOfChar(' ', J) + S2;

    if Length(S3) < SelectedField.Size then
    begin
      // カーソルより先が空白の場合
      if (J > 0) and (Trim(S2) = '') then
      begin
        SpaceAdd(J);
        Exit;
      end;

      if not (DataLink.DataSet.State in [dsEdit, dsInsert]) then
        SelectedField.DataSet.Edit;
        SelectedField.AsString := S3;
    end
    else
      P2 := P1;

  finally
    SendMessage(InplaceEditor.Handle, WM_SETREDRAW, Ord(True), 0);
    SetSelStart(InplaceEditor, P2);
  end;

end;

procedure THRIndentDBGrid.PriorIndent;
var
  P1, P2, P3: Integer;
  I,J: Integer;
  S1, S2, S3: String;
begin
  SendMessage(InplaceEditor.Handle, WM_SETREDRAW, Ord(False), 0);
  try
    P1 := GetSelStart(InplaceEditor);
    P2 := 0;

    for I := High(FIndentPosList) downto 0 do
      if FIndentPosList[I]-1 < P1 then
      begin
        P2 := FIndentPosList[I]-1;
        Break;
      end;

    if P2 < 0 then
    begin
      P2 := P1;
      Exit;
    end;

    S1 := TrimRight(Copy(InplaceEditor.Text, 1, P1));
    P3 := Length(S1);

    S2 := Copy(InplaceEditor.Text, P1+1, Length(InplaceEditor.Text)-P1);

    if P3 >= P2 then
    begin
      S3 := S1 + S2;
      P2 := P3;
    end
    else
    begin
      J := P2 - Length(S1);
      S3 := S1 + StringOfChar(' ', J) + S2;
    end;

    if Length(S3) < SelectedField.Size then
    begin

      // カーソルより先が空白の場合
      if (J > 0) and (Trim(S2) = '') then
      begin
        SpaceAdd(J);
        Exit;
      end;

      if not (DataLink.DataSet.State in [dsEdit, dsInsert]) then
        SelectedField.DataSet.Edit;
      SelectedField.AsString := S3;
    end
    else
      P2 := P1;
  finally
    SendMessage(InplaceEditor.Handle, WM_SETREDRAW, Ord(True), 0);
    SetSelStart(InplaceEditor, P2);
  end;
end;


procedure THRIndentDBGrid.NextElement;
var
  I: Integer;
  P1: Integer;
  List: TIndentPosList;
begin
  GetElementPosList(InplaceEditor.Text, List);
  P1 := GetSelStart(InplaceEditor);
  for I := 0 to High(List) do
    if P1 < List[I] then
    begin
      P1 := List[I];
      Break;
    end;
  SetSelStart(InplaceEditor, P1);
end;

procedure THRIndentDBGrid.PriorElement;
var
  I: Integer;
  P1: Integer;
  List: TIndentPosList;
begin
  GetElementPosList(InplaceEditor.Text, List);
  P1 := GetSelStart(InplaceEditor);
  for I := High(List) downto 0 do
    if P1 > List[I] then
    begin
      P1 := List[I];
      Break;
    end;
  SetSelStart(InplaceEditor, P1);
end;

// インデント位置リストを作成します。
procedure THRIndentDBGrid.MakeIndentPosList(Indent: String);
var
  I: Integer;
  SL: TStringList;
begin
  SL := TStringList.Create;
  try
    SL.CommaText := Indent;
    SetLength(FIndentPosList, SL.Count);
    for I := 0 to SL.Count - 1 do
      FIndentPosList[I] := StrToIntDef(SL[I],0);
  finally
    SL.Free;
  end;
end;

// 要素の位置を取得します。
procedure THRIndentDBGrid.GetElementPosList(S: String; var List: TIndentPosList);

  procedure Increment(var P: PChar; var I: Integer; Value: Integer);
  begin
    Inc(P, Value);
    Inc(I, Value);
  end;

var
  P: PChar;
  I, J: Integer;
  w: Word;
  F: Boolean;
begin
  SetLength(List, 100); // 100は適当
  P := PChar(S + #0);
  I := 0;
  J := -1;
  F := True;
  while (P^ <> #0) do
  begin
    if IsDBCSLeadByte(Byte(P^)) then
    begin
      w := (Byte(P^) shl 8) or Byte((P+1)^);
      if F and (w <> $8140) then
      begin
        Inc(J);
        List[J] := I;
        Increment(P, I, 2);
        F := False;
      end
      else if (not F) and (w = $8140) then
      begin
        Inc(J);
        List[J] := I;
        Increment(P, I, 2);

        F := True;
      end
      else if F and (w = $8140) then
      begin
        Increment(P, I, 2);
      end
      else
       Increment(P, I, 2);
    end
    else
    begin
      if F and (P^ <> ' ') then
      begin
        Inc(J);
        List[J] := I;
        Increment(P, I, 1);
        F := False;
      end
      else if (not F) and (P^ = ' ') then
      begin
        Inc(J);
        List[J] := I;
        Increment(P, I, 1);
        F := True;
      end
      else if (P^ = ' ') then
      begin
        Increment(P, I, 1);
        F := True;
      end
      else
       Increment(P, I, 1);
    end;
  end;
  // 文字列の最後を付加しておきます。
  Inc(J);
  List[J] := length(S);
  SetLength(List, J+1);
end;

end.



コンポーネントとして登録するのは面倒なので、下記のように実行時に生成して試しています。
type
  TForm1 = class(TForm)
    Table1: TTable;
    DataSource1: TDataSource;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    HRIndentDBGrid: THRIndentDBGrid;
  end;


procedure TForm1.FormCreate(Sender: TObject);
begin
  HRIndentDBGrid := THRIndentDBGrid.Create(Self);
  HRIndentDBGrid.Parent := Self;
  HRIndentDBGrid.Align := alClient;
  HRIndentDBGrid.DataSource := DataSource1;
  HRIndentDBGrid.IndentPosition := '1,3,5,7,9,11';
end;
Undoができないのは、相変わらずなんですが、キーでインデント位置を前後に移動できるため Undoの仕様としては、インデントに対して無効、キー入力部分に対して有効となり、 それはそれでいいんじゃないかと思っています。


[20071009訂正]

空白セルに対して動作しなかったため、次の部分を訂正しました。
SpaceAdd手続きを追加しました。
NextIndent手続きを差し替えました。
PriorIndent手続きを差し替えました。
結局、力技での解決になってしまいました。
DBGridでCTRL+SPACEキーで半角進むことを利用しています。

|

« ■TEAD for CodeGear RAD Studio | トップページ | ☆DBGridに複数行選択のオペレーション機能を追加する。 »