第14回エンバカデロ・デベロッパーキャンプセッションT7自己フォロー(グリッドの特定列をボタンにする)

やることは、グリッドのセルにボタンを表示してそれに応じたダイアログ等を表示すること。コンポーネントを作らずにオーナードローでごまかす。

1.初期化

まずフォームにTDrawGridとTButtonを貼り付ける。TDBGridならばオーナードローにする
行数分だけ動的にTButtonを生成するやり方もあるかもしれないけど、メモリが無駄になるので貼るのは1つだけ。
OnCreateでボタンをグリッドの子供にする。 あと、プロジェクトオプション等でマクロ"NO_STRICT"が無効であることを確認。でないと、TThemeServices::DrawTextでリンクエラーが発生する。

//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
  // ボタンの親をグリッドに
  Button1->Parent = DrawGrid1;

  // ボタンのサイズをセルのサイズに
  TRect BoundsRect = DrawGrid1->CellRect(BUTTON_COL, DrawGrid1->FixedRows);
  Button1->BoundsRect = BoundsRect;
}
//---------------------------------------------------------------------------

2.ボタンのイメージをセルに描画する

今までだとDrawFrameControlですんでいたけど、こいつはテーマに対応していない。
そこで、ThemeAPIとTThemeServicesを使って描画する。

まず、インクルード文

#undef  DrawText  //TThemeServices::DrawTextとかぶるのでundefする 
#include <Themes.hpp>

DrawTextをundefしないとTThemeServices::DrawTextがリンクされないので注意。

続いて、セルにボタンのイメージを描画する。

//---------------------------------------------------------------------------
void __fastcall TForm1::DrawGrid1DrawCell(TObject *Sender, int ACol, int ARow, TRect &Rect, TGridDrawState State)
{
  if (State.Contains(gdFixed)) return;    // 固定領域
  if (ACol != BUTTON_COL) return;         // ボタンの描画列でない

  TRect clientRect = Rect;
  LPCWSTR ButtonLabel = L"...";

  // テーマサービスの取得
  TThemeServices* pTheme = ThemeServices();
  if (pTheme && pTheme->ThemesEnabled) {
    // テーマが有効

    // ボタンを描画
    TThemedElementDetails Details = pTheme->GetElementDetails(tbPushButtonNormal);
    pTheme->DrawElement(DrawGrid1->Canvas->Handle,Details,Rect, NULL);

    // ボタンのラベルを描画 マクロ"NO_STRICT"が定義されているとTThemeServices::DrawTextのリンクエラーになるので注意。
    pTheme->DrawText(DrawGrid1->Canvas->Handle, Details, ButtonLabel, Rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE, 0);
  } else {
    // テーマが無効

    // ボタンを描画
    DrawFrameControl(DrawGrid1->Canvas->Handle, &Rect, DFC_BUTTON, DFCS_BUTTONPUSH| DFCS_ADJUSTRECT);

    // ボタンのラベルを描画
    int LastMode = SetBkMode(DrawGrid1->Canvas->Handle, TRANSPARENT);
    DrawTextW(DrawGrid1->Canvas->Handle, ButtonLabel, lstrlenW(ButtonLabel), &Rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    SetBkMode(DrawGrid1->Canvas->Handle, LastMode);
  }
}
//---------------------------------------------------------------------------

まず、ThemeServices関数でテーマサービスオブジェクトを取得する
こいつは、シングルトンオブジェクトなので生成する必要は無し。
NULLが帰ってきたら、あるいは、ThemesEnabledがfalseならば*1テーマが有効でないので従来通りDrawFrameControlでボタンを描画する。

TThemeServicesの使い方は、GetElementDetailsでUIの詳細情報を取得し、それを元にDrawElementで描画する。
この例では、通常のプッシュボタン(tbPushButtonNormal)を取得して描画している。

文字列の描画はDrawTextというメソッドがあるのだけど、C++Builderだとリンクエラーになる。('A`)*2
おそらく、DrawText APIとかぶるのが理由だけど、"#undef DrawText"とかやっても駄目だった。 仕方がないので、Theme APIを直に呼び出す。 ちなみに、DelphiではOK。
文字列の描画はメソッドDrawTextで行う。Themesユニットの類はTheme APIをラップしているだけなので、詳細な使い方はTheme APIが参考になるかも。

3.マウスイベントの処理

このやり方のキモは、ボタンをマウスに追随させてグリッド上の対応するセルに動かしている。

//---------------------------------------------------------------------------
void __fastcall TForm1::DrawGrid1MouseMove(TObject *Sender, TShiftState Shift, int X, int Y)
{
  int Col, Row;
  DrawGrid1->MouseToCell(X, Y, Col, Row);
  if (Row >= DrawGrid1->FixedRows && Col == BUTTON_COL) {
    TRect BoundsRect = DrawGrid1->CellRect(Col, Row);
    Button1->BoundsRect = BoundsRect;
  }
}
//---------------------------------------------------------------------------

ボタンは常にマウスに追随しているので、ボタン化したセルの押下処理はボタンに任せ、イベント処理は一カ所だけ。
なぜか、ボタンのOnClickイベントが発生しないのでOnMouseUpイベントで代用。

//---------------------------------------------------------------------------
void __fastcall TForm1::Button1MouseUp(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y)
{
  int Col, Row;
  DrawGrid1->MouseToCell(Button1->Left + X, Button1->Top + Y, Col, Row);

  ShowMessage(UnicodeString::Format(_T("Clicked! Col=%d Row=%d"), OPENARRAY(TVarRec, (Col, Row))));
}
//---------------------------------------------------------------------------

副次的な効果として、イベントが発生したマウス座標が取得できるので、MouseToCellメソッドでグリッドの行と列を取得する。
これでセルの特定列にボタンを持つグリッドの処理が完成。

応用として、三相ブール値(True/False/None)を表現するチェックボックスとかも可能。

9/15 追記:
TThemeServices::DrawTextがリンクされない原因が判明したので一部修正。
原因は、マクロ"NO_STRICT"か"DrawText"のどちらかが有効になっていたため。両者は必ず無効にしておくこと。
NO_STRICTはプロジェクトオプションで*3、DrawTextはソースコード中で無効にする。

*1:09/08 追記:山本様の指摘によると、NULLは帰ってこない模様。テーマが有効か否かはThemesEnabledでチェックする。NULLチェックは念のため。ご指摘ありがとうございます。<(_ _)>

*2:マクロNO_STRICTが定義されているとDrawTextをundefしてもリンクエラーになる。プロジェクトオプション等で要確認。

*3:CB2009のリリースノートCB2010のリリースノートを参照