DrawGrid/StringGridで拡張クラスを使用してFixedCellをクリックできるようにする方法
16 DrawGridStringGridDrawCell-4 動作確認 Delphi2007 更新日 2008/01/07(月)

『DrawGrid/StringGridでFixedCellをクリックできるようにする方法 』の続きになります。

前回のテクニックではGridのOnMouseDown/MouseUp/MouseLeave/DrawCell
という各イベント部分に実装コードをたくさん書く必要があったので
やや使い勝手が悪い感じがします。
Unit1.pasにFixedCellをクリックするためのソースコードが多くなり
可読性が落ちている感じがしました。

そこで、FixedCellクリック機能を分離して別unitで記載してみましょう。

このような場合、Gridを継承して実装するのも手ですが
DrawGrid/StringGrid、2つのコンポーネント
それぞれについて継承して実装するのも変ですし
たとえば私はTStringGridではなく
スクロールバーつまみサイズが変化してくれるTStrGridを使っていますが

そのTStrGridに対しても
TFixedCellClickableStrGrid、というものを実装するのも
エレガントではありませんので

ここで、拡張クラスをつかってGridを拡張するという手法をとってみます。

コンストラクタで、Gridを指定して生成した場合に
固定セルをクリックする機能が拡張されるように
TFixedGridClickExtensionというクラスを実装してみます。

ソースコードは長いですが、中身を詳細に知る必要はありません。
前回のテクニックではMouseLeaveを使っていますが
若干のバグ対応もしたので今は使っていません。
────────────────────
unit FixedGridClickUnit;

interface

uses
  Grids, Controls, Classes, Types, Graphics, Windows,
  SysUtils,		
  SystemUnit,	//このあたり以下は自作だから
  MathUnit,		//DelFusaLibraryからさがしてください
  RectPointUnit,
end_uses;

type
  TFixedGridClickExtension = class(TObject)
  private
    FGrid: TDrawGrid;
    FMouseDownGridFixedCellPoint: TPoint;
    FOriginalMouseDownEvent: TMouseEvent;
    FOriginalMouseUpEvent: TMouseEvent;
    FOriginalMouseMoveEvent: TMouseMoveEvent;
    FOriginalDrawCellEvent: TDrawCellEvent;
    procedure GridMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure GridMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure GridMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure GridDrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect;
      State: TGridDrawState);
    procedure GridMousePointUpdate(Shift: TShiftState; X, Y: Integer);
  protected

  public
    constructor Create(Grid: TDrawGrid);
    destructor Destroy; override;
  published
  end;

function CheckFixedCell(Grid: TDrawGrid; ACol, ARow: Longint): Boolean;
function CheckNormalCell(Grid: TDrawGrid; ACol, ARow: Longint): Boolean;

implementation

{ TFixedGridClickExtension }

constructor TFixedGridClickExtension.Create(Grid: TDrawGrid);
begin
  FGrid := Grid;
  FGrid.DefaultDrawing := False;
  {↓フラグ初期化}
  FMouseDownGridFixedCellPoint := Point(-1, -1);

  if Assigned(Grid.OnMouseDown) then
  begin
    FOriginalMouseDownEvent := Grid.OnMouseDown;
  end else
  begin
    FOriginalMouseDownEvent := nil;
  end;
  Grid.OnMouseDown := Self.GridMouseDown;

  if Assigned(Grid.OnMouseUp) then
  begin
    FOriginalMouseUpEvent := Grid.OnMouseUp;
  end else
  begin
    FOriginalMouseUpEvent := nil;
  end;
  Grid.OnMouseUp := Self.GridMouseUp;

  if Assigned(Grid.OnMouseMove) then
  begin
    FOriginalMouseMoveEvent := Grid.OnMouseMove;
  end else
  begin
    FOriginalMouseMoveEvent := nil;
  end;
  Grid.OnMouseMove := Self.GridMouseMove;

  if Assigned(Grid.OnDrawCell) then
  begin
    FOriginalDrawCellEvent := Grid.OnDrawCell;
  end else
  begin
    FOriginalDrawCellEvent := nil;
  end;
  Grid.OnDrawCell := Self.GridDrawCell;
end;

destructor TFixedGridClickExtension.Destroy;
begin
  FGrid.OnMouseDown := FOriginalMouseDownEvent;
  FGrid.OnMouseMove := FOriginalMouseMoveEvent;
  FGrid.OnMouseUp := FOriginalMouseUpEvent;
  FGrid.OnDrawCell := FOriginalDrawCellEvent;
  inherited;
end;

//Col/Rowで指定した位置が固定セルかどうかを調べる関数
function CheckFixedCell(Grid: TDrawGrid; ACol, ARow: Longint): Boolean;
begin
  Result := False;
  if CheckRange(0, ACol, Grid.FixedCols-1)
    and CheckRange(0, ARow, Grid.RowCount-1) then
  begin
    Result := True;
    Exit;
  end;

  if CheckRange(0, ARow, Grid.FixedRows-1)
    and CheckRange(0, ACol, Grid.ColCount-1) then
  begin
    Result := True;
    Exit;
  end;
end;

//Col/Rowで指定した位置が固定セルかどうかを調べる関数
function CheckNormalCell(Grid: TDrawGrid; ACol, ARow: Longint): Boolean;
begin
  Result := False;
  if CheckRange(Grid.FixedCols, ACol, Grid.ColCount-1)
    and CheckRange(Grid.FixedRows, ARow, Grid.RowCount-1) then
  begin
    Result := True;
    Exit;
  end;
end;

procedure TFixedGridClickExtension.GridMousePointUpdate(Shift: TShiftState; X, Y: Integer);
var
  ACol, ARow: Longint;
begin
  if ssLeft in Shift then
  begin
    FGrid.MouseToCell(X, Y, ACol, ARow);
    FMouseDownGridFixedCellPoint := Point(ACol, ARow);
    {↓選択した場所がGridの内部ではないなら、フラグをキャンセル}
    if not PointInRect(Point(X, Y), FGrid.ClientRect) then
      FMouseDownGridFixedCellPoint := Point(-1, -1);

    {↓選択した場所が固定セルではないなら、フラグをキャンセル}
    if not CheckFixedCell(FGrid, ACol, ARow) then
      FMouseDownGridFixedCellPoint := Point(-1, -1);

//    DebugPrintNotepad(IntToStr(ACol));
    FGrid.Repaint;
  end;
end;

//マウスDown時にフラグON
procedure TFixedGridClickExtension.GridMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  GridMousePointUpdate(Shift, X, Y);

  if Assigned(FOriginalMouseDownEvent) then
  begin
    FOriginalMouseDownEvent(Sender, Button, Shift, X, Y);
  end;
end;

procedure TFixedGridClickExtension.GridMouseMove(Sender: TObject;
  Shift: TShiftState; X, Y: Integer);
begin
  GridMousePointUpdate(Shift, X, Y);

  if Assigned(FOriginalMouseMoveEvent) then
  begin
    FOriginalMouseMoveEvent(Sender, Shift, X, Y);
  end;
end;

//マウスUpでフラグOFF
procedure TFixedGridClickExtension.GridMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  ACol, ARow: Longint;
begin
//  FGrid.MouseToCell(X, Y, ACol, ARow);

  FMouseDownGridFixedCellPoint := Point(-1, -1);
  FGrid.Repaint;

  if Assigned(FOriginalMouseUpEvent) then
  begin
    FOriginalMouseUpEvent(Sender, Button, Shift, X, Y);
  end;
end;

procedure TFixedGridClickExtension.GridDrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  PenBuffer: TPen;
  BrushBuffer: TBrush;
  FontBuffer: TFont;
  DC: HDC;
  R: TRect;

  {↓FixedCellの枠を描画する}
  procedure DrawFixedFrame(ARect: TRect; Click: Boolean=False);
  begin
    if (Sender as TDrawGrid).Ctl3D then
    with (Sender as TDrawGrid) do
    begin
      Canvas.Pen.Style := psSolid;
      Canvas.Pen.Mode := pmCopy;
      with ARect do begin
        if Click then
          Canvas.Pen.Color := clBtnShadow
        else
          Canvas.Pen.Color := clBtnHighlight;
        Canvas.MoveTo( Right - 1, Top );
        Canvas.LineTo( Left, Top);
        Canvas.LineTo( Left, Bottom - 1 );
        if Click then
          Canvas.Pen.Color := clBtnHighlight
        else
          Canvas.Pen.Color := clBtnShadow;
        Canvas.LineTo( Right - 1, Bottom - 1 );
        Canvas.LineTo( Right - 1, Top );
      end;
    end;
  end;

  {↓パラメータを一時保存}
  procedure BufferingOn;
  begin
    with Sender as TDrawGrid do
    begin
      PenBuffer.Assign(Canvas.Pen);
      BrushBuffer.Assign(Canvas.Brush);
      FontBuffer.Assign(Canvas.Font);
    end;
  end;

  {↓パラメータを復帰}
  procedure BufferingOff;
  begin
    with Sender as TDrawGrid do
    begin
      Canvas.Pen.Assign(PenBuffer);
      Canvas.Brush.Assign(BrushBuffer);
      Canvas.Font.Assign(FontBuffer);
    end;
  end;

begin
  PenBuffer := TPen.Create;
  BrushBuffer := TBrush.Create;
  FontBuffer := TFont.Create;
  BufferingOn;

  with FGrid do begin

  {↓固定セル}
  if gdFixed in State then
  begin
    if (FMouseDownGridFixedCellPoint.X=ACol)
      and (FMouseDownGridFixedCellPoint.Y=ARow) then
    begin
      Canvas.Brush.Color := FixedColor;
      Canvas.FillRect(Rect);
      {↑セル内を色で塗りつぶす}
      {↓文字描画}
      if FGrid is TStringGrid then
      begin
        Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2,
          TStringGrid(FGrid).Cells[ACol, ARow]);
      end;
      DrawFixedFrame(Rect, True);
      {↑クリック時の枠を描画}
    end else
    begin
      Canvas.Brush.Color := FixedColor;
      Canvas.FillRect(Rect);
      {↑セル内を色で塗りつぶす}
      {↓文字描画}
      if FGrid is TStringGrid then
      begin
        Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2,
          TStringGrid(FGrid).Cells[ACol, ARow]);
      end;
      DrawFixedFrame(Rect);
      {↑枠を描く}
    end;
  end else

  {↓セレクトセル}
  if gdSelected in State then
  begin
    {↓DrawFocusSelectedの場合はFocusedセルの色が違う}
    if (goDrawFocusSelected in Options)
      and (ACol = Col) and (ARow = Row) then
    begin
      {↓選択色}
      Canvas.Brush.Color := clHighlight;
      Canvas.FillRect(Rect);
      if FGrid is TStringGrid then
      begin
        Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2,
          TStringGrid(FGrid).Cells[ACol, ARow]);
      end;
      Canvas.DrawFocusRect(Rect);
      {↑枠を描く}
    end else
    if (ACol = Col) and (ARow = Row) then
    begin
      {↓通常セル色}
      Canvas.Brush.Color := clWindow;
      Canvas.FillRect(Rect);
      if FGrid is TStringGrid then
      begin
        Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2,
          TStringGrid(FGrid).Cells[ACol, ARow]);
      end;
      Canvas.DrawFocusRect(Rect);
      {↑枠を描く}
    end else
    begin
      Canvas.Brush.Color := clHighlight;
      Canvas.FillRect(Rect);
      {↓文字描画}
      if FGrid is TStringGrid then
      begin
        Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2,
          TStringGrid(FGrid).Cells[ACol, ARow]);
      end;
      {枠描画処理は無し}
    end;
  end else

  {↓普通のセル}
  begin
    Canvas.Brush.Color := clWindow;
    Canvas.FillRect(Rect);
    {↓文字描画}
    if FGrid is TStringGrid then
    begin
      Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2,
        TStringGrid(FGrid).Cells[ACol, ARow]);
    end;
  end;

  end;
  BufferingOff;
  PenBuffer.Free;
  BrushBuffer.Free;
  FontBuffer.Free;

  if Assigned(FOriginalDrawCellEvent) then
  begin
    FOriginalDrawCellEvent(Sender, ACol, ARow, Rect, State);
  end;
end;

end.
────────────────────
使い方は、上記ソースをFixedGridClickUnit.pasとして保存
プロジェクトに追加しておき

Formに
FFixedGridClicker: TFixedGridClickExtension
を定義しておいて

FormCreateなどで
FFixedGridClicker := TFixedGridClickExtension.Create(StringGrid1);
こうすることだけで、StringGrid1がクリック可能になります。

※FormDestroyあたりで破棄はしておいてください。

クリックイベントを感知して固定セルがクリックされたときに
Gridの列選択や行選択を行いたい場合は
次のようにイベントを実装しておきます。下記はDrawGridの例になります。
────────────────────
procedure TForm1.DrawGrid1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  ACol, ARow: Longint;
  SelectionSetRect: TGridRect;
begin
  with TDrawGrid(Sender) do begin

  MouseToCell(X, Y, ACol, ARow);
  if (ARow <= FixedRows - 1)
    and (ACol <= FixedCols - 1) then
  begin
    {↑もしFixedRow/Colより小さい領域=左上ボタンを押したなら
     ↓全選択}
    SelectionSetRect.Left := FixedCols;
    SelectionSetRect.Top := FixedRows;
    SelectionSetRect.Right := ColCount-1;
    SelectionSetRect.Bottom := RowCount-1;
    Selection := SelectionSetRect;
  end else
  if (ARow <= FixedRows - 1) then
  begin
    {↑もしFixedRowより小さい領域=上ボタンを押したなら
     ↓列選択}
    SelectionSetRect.Left := ACol;
    SelectionSetRect.Top := FixedRows;
    SelectionSetRect.Right := ACol;
    SelectionSetRect.Bottom := RowCount-1;
    Selection := SelectionSetRect;
  end else
  if (ACol <= FixedCols - 1) then
  begin
    {↑もしFixedColより小さい領域=左ボタンを押したなら
     ↓行選択}
    SelectionSetRect.Left := FixedCols;
    SelectionSetRect.Top := ARow;
    SelectionSetRect.Right := ColCount-1;
    SelectionSetRect.Bottom := ARow;
    Selection := SelectionSetRect;
  end;
  end;
end;
────────────────────

このTFixedGridClickExtensionクラスの実装で
あるコントロールの機能UPをおこなう拡張クラスの実装として参考になるのは

クラスを生成した時にCreateで指定しているGridに対して
OnMouseUpなどのイベントを乗っ取り、
TFixedGridClickExtensionクラスのMouseUpの実装コードが
呼び出されるようにしている部分です。
特にその準備はTFixedGridClickExtension.Create を中心におこなっています。

元のGridのOnMouseUp処理も正しくおこなわれます。

どんなコンポーネントに対してもこのテクニックは使えます。
定型的なイベントハンドラを実装する必要がある時には
拡張クラスを作って別ユニットにしてソースを隠蔽すると
よりスッキリとしたコードで機能を実現できるでしょう。