DBGridのPopupMenuで・・・。

Delphi2007で作ったDBGridから派生させた自作コンポーネントをDelphi XE2で動作確認していたところ、セルの編集時、設定したPopupMenuが表示されないことが確認されました。Delphi2007の時は、セル編集時、設定されたPopupMenuが表示されていましたが、Delphi XE2では、デフォルトのものが表示されてしまいます。


次のようなシンプルな構成でDBGridとStringGridにPopupMenuを割り当てて、Delphi2007、Delphi2009、Delphi XE2で試してみました。

Design2007




[Delphi2007] 
DBGrid、InplaceEditor共割り当てたPopupMenuが表示されています。

Delphi2007runtime


Delphi2007runtime2




[Delphi2009] 
セル編集時、デフォルトのPopupMenuが表示されてしまいます。

Delphi2009runtime


Delphi2009runtime2




[Delphi XE2] 
Delphi2009と同じく、デフォルトのPopupMenuが表示されています。

Delphixe2runtime




ということで、この動作は、Delphi2009から変わっていたようです。いつもながらスマートな方法とは言えないんですけど、下記の処理でInplaceEditorにもPopupMenuを割り当てることができました。
type
   TDummyDBG = class(TCustomDBGrid);
   TDummyCG  = class(TCustomGrid);
   TDummyCtl = class(TControl);

procedure TForm1.FormShow(Sender: TObject);
var
  Options1: TDBGridOptions;
  Options2: TGridOptions;
begin
  // DBGridの場合
  Options1 := DBGrid1.Options;
  try
    DBGrid1.Options := DBGrid1.Options + [dgEditing, dgAlwaysShowEditor];
    TDummyDBG(DBGrid1).ShowEditor;
    if Assigned(TDummyDBG(DBGrid1).InplaceEditor) then
      TDummyCtl(TDummyDBG(DBGrid1).InplaceEditor).PopupMenu := DBGrid1.PopupMenu;
  finally
    DBGrid1.Options := Options1;
  end;

  // StringGridの場合
  Options2 := StringGrid1.Options;
  try
    StringGrid1.Options := StringGrid1.Options + [goEditing, goAlwaysShowEditor];
    TDummyCG(StringGrid1).ShowEditor;
    if Assigned(TDummyCG(StringGrid1).InplaceEditor) then
      TDummyCtl(TDummyCG(StringGrid1).InplaceEditor).PopupMenu := StringGrid1.PopupMenu;
  finally
    StringGrid1.Options := Options2;
  end;
end;



Delphixe2runtimeok

|

DBGridを同期させる。

この時期、なぜ、XE2の新機能やFireMonkeyの試行錯誤をやらないの?と突っ込まれそうですが、古いプログラムの修正用にいろいろ調べて直しているからです(^-^)

図のように上下に配置したDBGridのタイトルの幅変更、移動、セルの移動そして横スクロールを同期させます。Excelのように上段のDBGridは横スクロールバー無し、下段のDBGridはタイトル無しとします。若干、問題が残っていますが、とりあえずは良しとしましょう。

設計時

Design

実行時

Runtime


問題点
1.上段DBGridの縦スクロールバーがタイトルの幅を変えたり、移動させたりすると
  消えてしまいます。マウスでスクロールさせると再び表示されるのですけど、
  見た目に統一感がないですね。

2.タイトル幅の変更時のラインを下段のDBGridでも描画しています。しかしタイト
  ル間のクリック位置によって微妙にラインがずれることがあります。

3.カラム移動のラインまでは対応していません(^-^)

DBGridのOnMouseDownでは、タイトル間のクリックが拾えません。そのため、下段のDBGridへのタイトル幅変更時のライン描画用フラグがややこしくなっています。又、サブクラスには、いつものようにMr.XRAYさんところのコンポーネントを利用させて頂きました。
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB, Bde.DBTables, Vcl.ExtCtrls,
  Vcl.Grids, Vcl.DBGrids, SubClassUnit, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    DBGrid1: TDBGrid;
    DBGrid2: TDBGrid;
    Splitter1: TSplitter;
    Table1: TTable;
    Table2: TTable;
    DataSource1: TDataSource;
    DataSource2: TDataSource;
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure DBGrid1ColEnter(Sender: TObject);
    procedure DBGrid2ColEnter(Sender: TObject);
    procedure DBGrid1ColumnMoved(Sender: TObject; FromIndex, ToIndex: Integer);
    procedure DBGrid1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure DBGrid1MouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure DBGrid1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure FormDestroy(Sender: TObject);
  private
    RowHeight: Integer;
    SubClass1: TSubClass;
    SubClass2: TSubClass;
    procedure SubClass1MessageAfter(Sender: TObject;
      var message: TMessage);
    procedure SubClass2MessageAfter(Sender: TObject;
      var message: TMessage);
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

type
  TDummyDBGrid = class(TCustomDBGrid);

// 初期設定
procedure TForm1.FormCreate(Sender: TObject);
begin
  // Tableの設定
  Table1.DatabaseName := 'DBDEMOS';
  Table1.TableName := 'country.db';
  Table2.DatabaseName := 'DBDEMOS';
  Table2.TableName := 'country.db';

  Table1.Open;
  Table2.Open;

  // 常に編集モード
  DBGrid1.Options := DBGrid1.Options + [dgAlwaysShowEditor];
  DBGrid2.Options := DBGrid2.Options + [dgAlwaysShowEditor] - [dgTitles];

  // 縦スクロールバーのみ表示させます。
  TDummyDBGrid(DBGrid1).ScrollBars := ssVertical;

  // サブクラスの設定
  SubClass1:= TSubClass.Create(Self);
  SubClass1.TargetControl := DBGrid2;
  SubClass1.OnMessageAfter := SubClass1MessageAfter;

  // サブクラスの設定 - MouseDownの取得だけ
  SubClass2:= TSubClass.Create(Self);
  SubClass2.TargetControl := DBGrid1;
  SubClass2.OnMessageAfter := SubClass2MessageAfter;

  // 行の高さ
  RowHeight := TDummyDBGrid(DBGrid1).DefaultRowHeight;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Table1.Close;
  Table2.Close;
end;

// サイズ変更時の処理
procedure TForm1.FormResize(Sender: TObject);
begin
  DBGrid1ColEnter(Self);
end;

// 起動時に強引に縦スクロールを表示させます。
procedure TForm1.FormShow(Sender: TObject);
begin
  // 縦スクロールバーを表示させるための処理
  // これをしないとマウスでスクロールさせるまで
  // スクロールバーが表示されません。
  SendMessage(DBGrid1.Handle, WM_VSCROLL, SB_LINEDOWN, 0);
  SendMessage(DBGrid1.Handle, WM_VSCROLL, SB_LINEUP, 0);
end;

// カラムの同期
procedure TForm1.DBGrid1ColEnter(Sender: TObject);
begin
  if (TDummyDBGrid(DBGrid2).LeftCol<> TDummyDBGrid(DBGrid1).LeftCol) then
    TDummyDBGrid(DBGrid2).LeftCol := TDummyDBGrid(DBGrid1).LeftCol;
end;

procedure TForm1.DBGrid2ColEnter(Sender: TObject);
begin
  if (TDummyDBGrid(DBGrid1).LeftCol<> TDummyDBGrid(DBGrid2).LeftCol) then
    TDummyDBGrid(DBGrid1).LeftCol := TDummyDBGrid(DBGrid2).LeftCol;
end;

procedure TForm1.DBGrid1ColumnMoved(Sender: TObject; FromIndex,
  ToIndex: Integer);
begin
  TDummyDBGrid(DBGrid2).ColumnMoved(FromIndex+1, ToIndex+1);
  {
  DBGrid2.Columns.BeginUpdate;
  try
    DBGrid2.Columns.Assign(DBGrid1.Columns);
  finally
    DBGrid2.Columns.EndUpdate;
  end;
  }
end;

var
  AX: Integer =0;
  Drawing: Boolean = False;
  IsFirstDrawing: Boolean = False;
  IsColumnMoving: Boolean = False;

procedure TForm1.DBGrid1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  // タイトル間のクリックは、ここでは取得できません。
  // タイトルのクリックは、取得できます。
  IsColumnMoving := True;
end;

// タイトル幅変更時のライン描画
procedure TForm1.DBGrid1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  OldPen: TPen;
begin
  if Drawing then
  begin
    OldPen := TPen.Create;
    try
      with DBGrid2.Canvas do
      begin
        OldPen.Assign(Pen);
        try
          Pen.Style := psDot;
          Pen.Mode := pmXor;
          Pen.Width := 1;

          if not IsFirstDrawing then
          begin
            // 一度目はこの処理をさせません。
            MoveTo(AX, 0);
            LineTo(AX, 0 + DBGrid2.Height);
          end
          else if IsFirstDrawing then
            IsFirstDrawing := False;

          MoveTo(X, 0);
          LineTo(X, 0 + DBGrid2.Height);
          AX := X;
        finally
          Pen := OldPen;
        end;
      end;
    finally
      OldPen.Free;
    end;
  end;
end;


// カラム幅の同期 - DBGrid1のカラム幅の変更をDBGrid2に反映させます。
procedure TForm1.DBGrid1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  I: Integer;
  Cell: TGridCoord;
begin
  IsColumnMoving := False; // MouseDownでなければ常にOK
  Drawing := False;
  IsFirstDrawing := False;

  // カラム幅の同期
  // カラムをクリックされた場合のみチェックします。
  // 本来、Cell.Yは0が返されるべきですが、時々1が返ってきます。
  // そのためCell.Y in [0,1]としています。
  Cell := DBGrid1.MouseCoord(X, Y);
  if Cell.Y in [0,1] then
  begin
    for I := 0 to DBGrid1.Columns.Count -1 do
    begin
      if DBGrid2.Columns[I].Width <> DBGrid1.Columns[I].Width then
        DBGrid2.Columns[I].Width := DBGrid1.Columns[I].Width;
    end;
  end;
end;

// 横スクロールバーの同期 - DBGrid2の変更をDBGrid1に反映します。
procedure TForm1.SubClass1MessageAfter(Sender: TObject; var message: TMessage);
begin
  case Message.Msg of
    WM_HSCROLL: DBGrid1.Perform(Message.Msg, Message.WParam, Message.LParam);
  end;
end;

// こちらは単純にDBGrid1のMouseDownを取得しているだけです。
// カラム間のクリックは、DBGridのOnMouseDownでは取得できなかった。
procedure TForm1.SubClass2MessageAfter(Sender: TObject; var message: TMessage);
begin
  case Message.Msg of
    WM_LBUTTONDOWN:
      begin
        if (Message.LParamHi < RowHeight) and (not IsColumnMoving)  then
        begin
          Drawing := True;
          IsFirstDrawing:=True;
        end;
    end;
  end;
end;

end.


サブクラス用コンポーネント
Delphi Library [Mr.XRAY]
SubClassUnit
http://homepage2.nifty.com/Mr_XRAY/Halbow/Notes/N004.html



20111005追記

DBGrid1のMouseUp、FormResizeの処理を下記に入れ替えると、そこそこ希望の動作になりました。スクロールバーは表示されるもののUpdateScrollBarを通していないので、レコード数が反映されたものではないですけどね。まあ、見た目的には問題ないです。
// カラム幅の同期 - DBGrid1のカラム幅の変更をDBGrid2に反映させます。
procedure TForm1.DBGrid1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  I: Integer;
  Cell: TGridCoord;
begin
  IsFirstDrawing := False;

  // カラム移動の終了
  if IsColumnMoving then
  begin
    ShowScrollBar(DBGrid1.Handle, SB_VERT, True);
    IsColumnMoving := False;
    DBGrid1ColEnter(Self); // DBGrid2の横スクロールバーが動くから
  end;

  // カラム幅の同期
  if Drawing then
  begin
    Drawing := False; // カラム幅を変更-終了
    DBGrid2.Columns.BeginUpdate;
    try
      for I := 0 to DBGrid1.Columns.Count -1 do
      begin
        if DBGrid2.Columns[I].Width <> DBGrid1.Columns[I].Width then
          DBGrid2.Columns[I].Width := DBGrid1.Columns[I].Width;
      end;
    finally
      DBGrid2.Columns.EndUpdate;
      DBGrid1ColEnter(Self); // DBGrid2の横スクロールバーが動くから
    end;
    ShowScrollBar(DBGrid1.Handle, SB_VERT, True);
  end;
end;

// サイズ変更時の処理
procedure TForm1.FormResize(Sender: TObject);
begin
  DBGrid2ColEnter(Self);
  ShowScrollBar(DBGrid1.Handle, SB_VERT, True);
end;

|

TDBGridの描画が変!?(MyBase接続時)

TCustomDBGridから派生した自作コンポーネントの修正をしていたのですが、時々描画がおかしくなるときがあり、いろいろ調べてみるとTDBGrid自体の描画が変な時がありました。

その1
TDBGridは、標準機能としてMouseOverでColumnsが強調表示されます。標準のテーマではわかりにくかったので、VCL StyleのGolden Graphiteを使っています。

Dbgrid1


しかし次のように先頭レコードのインジゲーター部分も強調表示されています。(他のレコードでは表示されない)まあ、これは気にするほどのことではないです。

Dbgrid2



その2
編集時にマウスの移動によって、セルの描画がおかしくなります。文字部分は、InplaceEditorが扱っているので、問題ないですけど、場合によっては、インジゲーターのアイコンが表示されたりします。結果として、ADTを使わない場合、ClientDataSet1.ObjectViewをFalseにすると問題なく描画されるようになりました。ADTがある場合は、Falseに設定していたとしても自動的にTrueになるようで、解決できませんでした。

アンダーラインのようなもの?(カラムが描画されてるのかも)

Dbgrid_underline


フォーカス枠の残り?

Dbgrid_error2


描画のタイミングが悪い?

Dbgrid_error3


Delphi2007とDelphi2009でも試してみましたが、こちらでは全く問題なかったです。(まあグリッドの描画があれですけど)

Dbgrid_2009_2




試しにDefaultDrawingをFalseに設定して、試してみました。 マウスを動かすとカラムのタイトルやインジゲーターアイコンが表示されます。 上手くマウスを動かすとこんな描画になります(^-^)こんなものがDefaultDrawingを使わなくても描画されていたら、オーナードローでは解決できなさそうです。(InplaceEditor表示後に描画されているのが厄介です)

Dbgrid_error4


Dbgrid_nodefaultdraw



ならば、DrawCellを全く表示させないとどうなるかを試してみました。 やはりDrawCell以外の部分での描画が影響しているようですね。

Dbgrid_nodrawcell



  TDBGridTest = class(TDBGrid)
  protected
    procedure DrawCell(ACol, ARow: Longint; ARect: TRect;
      AState: TGridDrawState); override;
  end;

procedure TDBGridTest.DrawCell(ACol, ARow: Integer; ARect: TRect;
  AState: TGridDrawState);
begin
  Exit;
  inherited;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  // TStyleManager.TrySetStyle('Golden Graphite');
  with ClientDataSet1 do
  begin
    FieldDefs.Add('ID',ftAutoInc);
    FieldDefs.Add('ITEM',ftWideString,10);
    FieldDefs.Add('QTY',ftFloat);
    FieldDefs.Add('UNIT',ftWideString,4);
    FieldDefs.Add('PRICE',ftCurrency);
    CreateDataSet;

    Append;
    FieldByName('ITEM').AsString := 'PEN';
    FieldByName('QTY').AsInteger := 10;
    FieldByName('UNIT').AsString := '本';
    FieldByName('PRICE').AsString := '1000';
    Append;
    FieldByName('ITEM').AsString := 'Fan';
    FieldByName('QTY').AsInteger := 3;
    FieldByName('UNIT').AsString := '台';
    FieldByName('PRICE').AsString := '30000';
    Append;
    FieldByName('ITEM').AsString := 'PC';
    FieldByName('QTY').AsInteger := 1;
    FieldByName('UNIT').AsString := '台';
    FieldByName('PRICE').AsString := '180000';
    CheckBrowseMode;
  end;

  TestDBGrid:= TDBGridTest.Create(Self);
  TestDBGrid.Parent := self;
  TestDBGrid.align := alClient;
  DataSource1.DataSet := ClientDataSet1;
  TestDBGrid.DataSource := DataSource1;
end;

|

☆DBGridのDrawCell

カスタムコンポーネントでのDrawCellの一例です。
今回は次のような描画をしてみます。
1.Excelのように選択セルの行(インジゲーター)と列(タイトル)部分に色を付ける。
2.InplaceEditorの背景に色を付ける。

Drawcell

※フォームやツールバーは、Delphi標準のコンポーネントではなく、TMS Software製TMS ToolBar Application Wizardから新規に作成したままの状態です。

type
  TDummyEditor = class(TCustomEdit);

  TMyDBGrid = class(TDBGrid)
  private
    procedure WMCommand(var Message: TWMCommand); message WM_COMMAND;
  protected
    procedure DrawCell(ACol, ARow: Longint; ARect: TRect;
      AState: TGridDrawState); override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
      X, Y: Integer); override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
  end;


uses
  GraphUtil;

{ TMyDBGrid }

const
  ActiveColor = $009FEBFD;
  ActiveColorTo = $0056B4FE;
  BackColor = $00FDEADA;
  BackColorTo =  $00E4AE88;
  EditColor = $00D3F8FF;

var
  Old_SelectedField: Integer;

procedure DrawBackground(ACanvas: TCanvas; ARect: TRect;
  Focused: Boolean);
begin
  if Focused then
    GradientFillCanvas(ACanvas, ActiveColor, ActiveColorTo,
      ARect, gdVertical)
  else
    GradientFillCanvas(ACanvas, BackColor, BackColorTo,
      ARect, gdVertical);
end;

procedure TMyDBGrid.DrawCell(ACol, ARow: Integer; ARect: TRect;
  AState: TGridDrawState);
const
  AlignFlags : array [TAlignment] of Integer =
    (DT_LEFT, DT_RIGHT, DT_CENTER);
var
  Text: String;
  Flags: LongInt;
  Check: Boolean;
begin
  inherited DrawCell(ACol,ARow,ARect,AState);

  InflateRect(ARect, 1, 1);
  if (ACol = 0) then
  begin
    Check := (ARow >= 0) and (ARow-1 = Datalink.ActiveRecord);
    DrawBackground(Canvas, ARect, Check);
  end
  else if (ARow = 0) then
  begin
    // 背景の描画
    SetBkMode(Canvas.Handle, TRANSPARENT);
    Check := (SelectedIndex = ACol- 1);
    DrawBackground(Canvas, ARect, Check);

    // カラム間の線
    Canvas.Pen.Color := clSilver;
    Canvas.MoveTo(ARect.Left, ARect.Top+1);
    Canvas.LineTo(ARect.Left, ARect.Bottom-1);
    // タイトルの描画
    Canvas.Font.Assign(TitleFont);
    Text := Columns[ACol-1].Title.Caption;
    Flags := DT_SINGLELINE or DT_VCENTER or
      AlignFlags[Columns[ACol-1].Title.Alignment];
    InflateRect(ARect, -2, -2);
    DrawText(Canvas.Handle, PChar(Text), Length(Text), ARect, Flags);
  end;
end;

procedure TMyDBGrid.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited KeyDown(Key, Shift);
  // 選択フィールドのカラムに色をつける処理
  if SelectedField.Index <> Old_SelectedField then
  begin
    Old_SelectedField := SelectedField.Index;
    Invalidate;
  end;
end;

procedure TMyDBGrid.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
begin
  inherited MouseDown(Button, Shift, X, Y);
  // 選択フィールドのカラムに色をつける処理
  if SelectedField.Index <> Old_SelectedField then
  begin
    Old_SelectedField := SelectedField.Index;
    Invalidate;
  end;
end;

// InplaceEditorの背景色を設定する。
procedure TMyDBGrid.WMCommand(var Message: TWMCommand);
begin
  with Message do
  begin
    if (InplaceEditor <> nil) and (Ctl = InplaceEditor.Handle) then
    begin
      if TDummyEditor(InplaceEditor).Color <> EditColor then
        TDummyEditor(InplaceEditor).Color := EditColor;
    end;
  end;
  inherited;
end;

|

☆DBGridのスクロールバーを非表示にする。

カスタムコンポーネントでの処理です。 縦スクロールバーは、簡単に非表示にできましたが、横スクロールバーは、なかなか思うよう にできなかったです。

TDBGridからの派生コンポーネントで、UpdateScrollBarをoverrideしておきます。
  THRCustomDBGrid = class(TDBGrid)
  protected
    procedure UpdateScrollBar; override;


縦スクロールバーを非表示にする処理です。(inheritedはしない)
procedure THRCustomDBGrid.UpdateScrollBar;
var
  SI: TScrollInfo;
begin
  SI.cbSize := sizeof(SI);
  SI.fMask := SIF_RANGE;
  SI.nMin := 0;
  SI.nMax := 0;
  SetScrollInfo(Handle, SB_VERT, SI, False);
end;

横スクロールバーを消す処理です。
(SetScrollInfoをSB_HORZとしても非表示にはできませんでした。)
constructor THRCustomDBGrid.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ScrollBars := ssVertical; // この処理
  (略)
end;  

上記の両方を設定すると、無事、両方のスクロールバーが非表示になりました。
(ScrollBars := ssNoneにしてもなぜか縦スクロールが表示されました。)

以上のように、なんかScrollBarsやSetScrollInfo APIだけで簡単に設定できそうなのに、なかなか手強かったです。まだ、実際に使っていない処理なので、不具合があるかも知れないです。

[追記]
Delphi Q & A 掲示板のMr.XRAYさんの方法が、簡単でいいですね。
(property ScrollBars;を追記するだけ)

Delphi Q & A 掲示板
■ DBGridの横スクロールバーの非表示について

|

☆DBGridのカラム移動が変?

まず、次のように3つのDBGridに同じDataSourceを関連付けたものを3つ用意します。

Column_move1



そして実行します。

Column_move2



それから最上段のDBGridのCapitalというカラムを先頭に移動させます。 2段目、3段目が一緒に移動しています。しかも微妙にカラム幅が変わっています。

Column_move3_2



カラム移動は、WMTimerで制御しているようなので、その影響なのでしょうか? 理由はよくわからないですけど、中途半端に連動して動くという点が使い物にならないですよね。 対応策ですが、DBGridのカラムを設定するとこの問題は発生しませんでした。

|

☆DBGridに複数行選択のオペレーション機能を追加する。

Excelのようにインジゲーター部分をスライドさせて複数行選択できる機能を実装してみます。 DBGridの複数行選択機能としては、CTRL+左クリックとShift+上下キーの 2種類がありますが、今回は、MouseMove時にShift+上下キーをシミュレートします。 スライド選択でのポイントは、端部にスライドした場合の処理です。 当初WMTimerで処理していたのですが、タイトルをクリックしたときにも メッセージが流れてきてたので、Timerコントロールを使いました。

こんなイメージです。

Slide


unit HRSlideSelectDBGrid;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, DB, Grids, DBGrids, ExtCtrls;

type
  THRSlideSelectDBGrid = class(TDBGrid)
  private
    FEditMode: Boolean;
    FTrackingDirection :Integer;
    FTracking: Boolean;
    FIndicatorWidth: Integer;
    FScrollTimer: TTimer;
    FScrollOperation: Word;
    procedure ShiftPlusUp;
    procedure ShiftPlusDn;
    procedure SetIndicatorWidth(Value: Integer);
    procedure ScrollTimer(Sender: TObject);
  protected
    procedure SetColumnAttributes; override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
      X, Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState;
      X, Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property IndicatorWidth: Integer read  FIndicatorWidth
      write SetIndicatorWidth default 30;
  end;

procedure Register;

implementation

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

constructor THRSlideSelectDBGrid.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  // スライドし易いように、Indicator幅を広くする。
  FIndicatorWidth := 30;
  SetColumnAttributes;

  // スクロールタイマー
  FScrollTimer := TTimer.Create(Self);
  with FScrollTimer do
  begin
    Interval := 50;
    OnTimer  := ScrollTimer;
    Enabled := False;
  end;

  // 複数選択できるようにしておく。
  Options :=  Options + [dgMultiSelect];
end;

destructor THRSlideSelectDBGrid.Destroy;
begin
  FScrollTimer.Free;
  inherited Destroy;
end;

procedure THRSlideSelectDBGrid.MouseDown(Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Cell: TGridCoord;
begin
  // セル位置の取得
  Cell := MouseCoord(X, Y);

  // スライド選択開始のための設定
  if (Cell.X = 0) and (Cell.Y > 0) and (not FTracking) and
     (not (ssCtrl in Shift)) and (not (ssShift in Shift)) then
  begin
    FTracking := True;
    FTrackingDirection := Cell.Y;
    // dgEditingがOptionsに含まれていると、スライド選択時に最終行を追加
    // 処理してしまうため、一時無効にしておく。
    if ([dgEditing] * Options = [dgEditing]) then
    begin
      FEditMode := True;
      Options := Options - [dgEditing];
    end;
  end;

  inherited MouseDown(Button, Shift, X, Y);;
end;

procedure THRSlideSelectDBGrid.MouseUp(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
begin
  // スライド選択の終了処理
  FScrollTimer.Enabled := False;
  FTracking := False;
  // 必要ならば無効にしたdgEditingを有効に戻しておく。
  if FEditMode then
  begin
    FEditMode := False;
    Options := Options + [dgEditing];
  end;
  inherited MouseUp(Button, Shift, X, Y);
end;

procedure THRSlideSelectDBGrid.MouseMove(Shift: TShiftState; X, Y: Integer);

  procedure RowSelectByMouseMove;
  const
    Offset = 20; //適当
  var
    Cell: TGridCoord;
  begin
    // セル位置の取得
    Cell := MouseCoord(X, Y);

    // 表示されていない部分をスクロールさせます。
    if (Y < Offset) or (Y >= ClientHeight - Offset) then
      begin
        if (Y < Offset) then
          FScrollOperation := SB_LINEUP
        else
          FScrollOperation := SB_LINEDOWN;
        FScrollTimer.Enabled := True;
      end
    else
      FScrollTimer.Enabled := False;

    // マウスの位置の行を選択させます。
    if (FTrackingDirection <> Cell.Y) then
    begin
      if (FTrackingDirection < Cell.Y) then
        ShiftPlusDn
      else
        ShiftPlusUp;
      FTrackingDirection := Cell.Y;
    end;
  end;

begin
  // スライド選択が開始されていない場合には処理しません。
  if FTracking then
  begin
    RowSelectByMouseMove;
    Exit;
  end;
  inherited MouseMove(Shift, X, Y);
end;


procedure THRSlideSelectDBGrid.ScrollTimer(Sender: TObject);
var
  KeyBoardState: TKeyBoardState;
  DefaultKeyState: Byte;
begin
  if (FScrollOperation = SB_LINEDOWN) then
  begin
    GetKeyBoardState(KeyBoardState);
    DefaultKeyState := KeyBoardState[VK_SHIFT];
    KeyBoardState[VK_SHIFT] := $81;
    SetKeyBoardState(KeyBoardState);
    Perform(WM_KEYDOWN, VK_DOWN, 1);
    Perform(WM_KEYUP, VK_DOWN, 1);
    KeyBoardState[VK_SHIFT] := DefaultKeyState;
    SetKeyBoardState(KeyBoardState);
  end
  else
  begin
    GetKeyBoardState(KeyBoardState);
    DefaultKeyState := KeyBoardState[VK_SHIFT];
    KeyBoardState[VK_SHIFT] := $81;
    SetKeyBoardState(KeyBoardState);
    Perform(WM_KEYDOWN, VK_UP, 1);
    Perform(WM_KEYUP  , VK_UP, 1);
    KeyBoardState[VK_SHIFT] := DefaultKeyState;
    SetKeyBoardState(KeyBoardState);
  end;
end;

procedure THRSlideSelectDBGrid.ShiftPlusDn;
var
  KeyBoardState: TKeyBoardState;
  DefaultKeyState: Byte;
begin
  GetKeyBoardState(KeyBoardState);
  DefaultKeyState := KeyBoardState[VK_SHIFT];
  KeyBoardState[VK_SHIFT] := $81;
  SetKeyBoardState(KeyBoardState);
  Perform(WM_KEYDOWN, VK_DOWN, 1);
  Perform(WM_KEYUP, VK_DOWN, 1);
  KeyBoardState[VK_SHIFT] := DefaultKeyState;
  SetKeyBoardState(KeyBoardState);
end;

procedure THRSlideSelectDBGrid.ShiftPlusUp;
var
  KeyBoardState: TKeyBoardState;
  DefaultKeyState: Byte;
begin
  GetKeyBoardState(KeyBoardState);
  DefaultKeyState := KeyBoardState[VK_SHIFT];
  KeyBoardState[VK_SHIFT] := $81;
  SetKeyBoardState(KeyBoardState);
  Perform(WM_KEYDOWN, VK_UP, 1);
  Perform(WM_KEYUP  , VK_UP, 1);
  KeyBoardState[VK_SHIFT] := DefaultKeyState;
  SetKeyBoardState(KeyBoardState);
end;

procedure THRSlideSelectDBGrid.SetIndicatorWidth(Value: Integer);
begin
  if Value <> FIndicatorWidth then
  begin
    FIndicatorWidth := Value;
    SetColumnAttributes;
  end;
end;

procedure THRSlideSelectDBGrid.SetColumnAttributes;
begin
  inherited;
  if (dgIndicator in Options) then
    ColWidths[0] := IndicatorWidth;
end;

end.

|

☆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キーで半角進むことを利用しています。

|

☆DBGridのColumn描画

ランタイムテーマを有効にしても、頑なに自分のスタイルを守るコンポーネントがあります。 DBGridもその中の一つで、スクロールバーやPickListのボタンは、いい感じなのにIndicatorやColumnは このように最悪です。

Dbgrid_1

※フォームやツールバーは、Delphi標準のコンポーネントではなく、TMS Software製TMS ToolBar Application Wizardから新規に作成したままの状態です。

そこで、Column部分を描画してみます。 Indicator部分は面倒なので、表示させないようにしています。 Indicatorも描画したいという方は、TCustomDBGrid.DrawCellを参考にして下さい。

宣言部です。
  TMyDBGrid = class(TDBGrid)
  protected
    procedure DrawCell(ACol, ARow: Longint; ARect: TRect;
      AState: TGridDrawState); override;
  public
    constructor Create(AOwner: TComponent); override;
  end;

実装部です。
uses
  GraphUtil;

constructor TMyDBGrid.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Options := Options - [dgIndicator];
end;

procedure TMyDBGrid.DrawCell(ACol, ARow: Integer; ARect: TRect;
  AState: TGridDrawState);
const
  AlignFlags : array [TAlignment] of Integer = (DT_LEFT, DT_RIGHT, DT_CENTER);
var
  Text: String;
  Flags: LongInt;
begin
  inherited DrawCell(ACol,ARow,ARect,AState);

  if (ARow = 0) and (Columns.Count > ACol) then
  begin
    // 背景の描画
    SetBkMode(Canvas.Handle, TRANSPARENT);
    InflateRect(ARect, +1, +1);
    GradientFillCanvas(Canvas, clWhite, clGradientInactiveCaption,
      ARect, gdVertical);
    // カラム間の線
    Canvas.Pen.Color := clSilver;
    Canvas.MoveTo(ARect.Left +0, ARect.Top +1);
    Canvas.LineTo(ARect.Left +0, ARect.Bottom -1);
    // タイトルの描画
    Canvas.Font.Assign(TitleFont);
    Text := Columns[ACol].Title.Caption;
    Flags := DT_SINGLELINE or DT_VCENTER or AlignFlags[Columns[ACol].Title.Alignment];
    InflateRect(ARect, -2, -2);
    DrawText(Canvas.Handle, PChar(Text), Length(Text), ARect, Flags);
  end;
end;


こんな風になりました(^^;

Dbgrid_2

|

☆DBGridでのTopRow設定

ClientDataSetを使ってDBGridにデータを表示させます。
私は、このテーブルに対して検索等を行うには、次のようにクローンを作成して処理させています。
その理由は、レコードを移動させる処理を行うとDBGridの表示が変わってしまうからです。例えば、BookMarkStrで元の位置に戻したとしても、BookMarkではDBGridの中央に指定レコードを表示するため、処理前の表示を再現することができません。
function LocateEx(ClientDataSet: TClientDataSet; const KeyFields: String;
  const KeyValues: Variant; Options: TLocateOptions): Boolean;
var
  CloneCDS: TClientDataSet;
begin
  CloneCDS := TClientDataSet.Create(nil);
  try
    CloneCDS.CloneCursor(ClientDataSet, True);
    Result := CloneCDS.Locate(KeyFields, KeyValues , Options);
    // if Result then
    //   ClientDataSet.GotoCurrent(CloneCDS)
  finally
    CloneCDS.Free;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  if not LocateEx(ClientDataSet1,'name', Edit1.Text, []) then
    ShowMessage('見つかりません');
end;

では、クローンを作成しないでレコード移動前の表示に戻せないかという質問がありました。
そこで、元の表示のTopRowを保存しておき、レコードを移動した後、TopRowとカレントレコードを戻すというサンプルを作ってみました。
type
  TDummyDBGrid = class(TCustomDBGrid);

procedure TForm1.Button1Click(Sender: TObject);
var
  RowCount, RN: Integer;
  TopRowRecNo, CurrentRowNo: Integer;
begin
  ClientDataSet1.DisableControls;
  try
    // TopRowのRecNoとCurrentRowのRecNoを保存しておきます。   
    CurrentRowNo := TDummyDBGrid(DBGrid1).Row - 1;
    TopRowRecNo := ClientDataSet1.RecNo - CurrentRowNo;

    //レコードを移動させる処理---好きにして~。
    //ClientDataSet1.Locate('name', Edit1.Text,[]);
    //ClientDataSet1.Last;
    //ClientDataSet1.First;
    
    // DBGridの表示を元に戻す処理

    // 指定レコードをTopRowに移動させる処理
    RowCount := TDummyDBGrid(DBGrid1).VisibleRowCount;
    ClientDataSet1.RecNo := TopRowRecNo;
    ClientDataSet1.MoveBy(RowCount);    
    
    while not ClientDataSet1.BOF do
    begin
      RN := ClientDataSet1.RecNo;
      if RN = TopRowRecNo then
        Break
      else
        ClientDataSet1.Prior;
    end;

    // カーソルを元のカレントレコードに移動させる処理
    ClientDataSet1.MoveBy(CurrentRowNo);
  finally
    ClientDataSet1.EnableControls;
  end;
end;

※Delphi User's Forumでコメントした内容をまとめています。

|