色変更が可能なボタン


オーナードローを使って色変更が可能なボタンを作ってみる.

オーナードロー(owner-drawn)とは,コンポーネントの描画をそのコンポーネント自身が 行うのではなく,コンポーネントのオーナーに行わせるものである. オーナードローを使えば通常とは外観の異なるコンポーネントを作成することができるなど ちょっとした高等テクニックである.

Windows標準のListBoxやComboBoxのオーナードローはDelphiの旧バージョンからサポートされており, OnDrawItemイベントの中で項目を自由に描画できる. ここに書いたComboBoxもオーナードローを使っている. また,Delphi 5からはMenuのオーナードローもサポートされ,Menuの色を変えたり ビットマップを描画したりということが簡単に行えるようになった. Menuのオーナードローについてはここに書いた. その他,TabContorlやListViewなどのWin32コンポーネントでもオーナードローが サポートされている.

しかしながら,Delphi 5になってもなぜかボタン(TButton)のオーナードローはサポートされていない. BitBtnではオーナードローが使われており,フォントの変更はできるが,色の変更はできないし, オーナードローに対応した適当なイベントも用意されていない.

オーナードローをサポートし,色変更が可能なButtonを作ってみようと思い立った. インターネットでサンプルコードを探してみたところ,MenuやListBoxの オーナードローのサンプルはたくさんあったが,肝心のButtonについてはほとんど 情報が見つからなかった(Visual C++でのやり方を書いたページはいくつかあった).

色変更が可能なボタンコンポーネント自体はあちこちに見つかった. ただし,Graphic Controlから派生したものであったり(したがってウィンドウハンドルを持たない), TButtonから派生していてもなぜかバイナリ配布だったりして参考にならない. Buttonのオーナードローのやり方を知っている人はあまり他人に教えたがらないのだろうか. Graphic Controlから派生したボタンを作るのは比較的簡単だし,以前やったことがある.

実は,「Delphi 3 Q&A集」に付属のCD-ROMには オーナードローをサポートしたボタンコンポーネントが収録されていたが, あろうことか,私はそのCD-ROMを紛失してしまった.

ここでは,BitBtnのコードを参考にしてTButtonから 派生した色変更可能なボタンTOwnerDrawButtonを作ってみる. このボタンはColorプロパティで色を変更できるうえ,OnDrawButtonイベントが 用意されており,このイベントハンドラの中で自由にボタン表面に描画できる(Canvasプロパティ をサポートする).


オーナードローボタンの作成

オーナードローボタンを作成するには,TButtonのCreateParamsメソッドをオーバーライドし, Params.StyleにBS_OWNERDRAWを追加すればよい. すなわち,

procedure TOwnerDrawButton.CreateParams( var Params: TCreateParams );
begin
   inherited CreateParams( Params );
   with Params do Style := Style or BS_OWNERDRAW;
end;

とする.

これで,ボタンの再描画が必要になったときに, ボタンのオーナーにWM_DRAWITEMメッセージが送られてくるようになる.

ただし,VCLのTButtonではSetButtonStyleメソッドが呼ばれると ボタンのスタイルを通常の(オーナードローでない)ボタンに変更してしまう. したがって,このメソッドをオーバーライドし, ウィンドウのスタイルが再設定されるのを抑制しなければならない(どのようにオーバーライドするかは ソースコード参照). BitBtnでも同じようにオーバーライドされている.


CN_DRAWITEMに対するメッセージハンドラ

VCLでは,WM_DRAWITEMを受け取ったオーナーがコンポーネントを描画するのではなく, 描画対象となるコンポーネントにCN_DRAWITEMメッセージを送り, コンポーネントに描画を行わせるようになっている. したがって,コンポーネント側にCN_DRAWITEMのメッセージハンドラを用意しておいて, このハンドラの中にコンポーネントの外観を描画するコードを書けばよい.

オーナー側にコンポーネントの描画ルーチンを書くことも可能だが(こちらの方が本来の意味の 「オーナードロー」であるが),コンポーネント側のコードに描画ルーチンを含めることによって コードの独立性が高まり,より汎用的になる.

さて,今回はCN_DRAWITEMのメッセージハンドラを次のように書いた.

procedure TOwnerDrawButton.CNDrawItem( var Message: TWMDrawItem );
var
  SaveIndex: Integer;
begin
   with Message.DrawItemStruct^ do
   begin
      SaveIndex := SaveDC( hDC );
      FCanvas.Lock;
      try
        FCanvas.Handle := hDC;
        FCanvas.Font := Font;
        FCanvas.Brush := Brush;
        DrawButton( rcItem, itemState );
      finally
        FCanvas.Handle := 0;
        FCanvas.Unlock;
        RestoreDC( hDC, SaveIndex );
      end;
   end;
   Message.Result := 1;
end;

DrawButtonメソッドの中で実際の描画を行っている. メッセージに付随して送られてくる情報はWindowsからオーナーに送られたものと同じである. 付随情報の型はTWMDrawItem型として次のように宣言されている.

  TWMDrawItem = packed record
    Msg: Cardinal;
    Ctl: HWND;
    DrawItemStruct: PDrawItemStruct;
    Result: Longint;
  end;

MsgはメッセージのIDで,Ctlは描画対象となる コントロールのウィンドウハンドルである. 今の場合,Ctlはメッセージを受け取ったコントロール自身のハンドルとなっている(はずである). DrawItemStructは描画時に必要となる情報を格納したTDrawItemStruct型構造体への ポインタである. この構造体は以下のような構造になっている.

  tagDRAWITEMSTRUCT = packed record
    CtlType: UINT;
    CtlID: UINT;
    itemID: UINT;
    itemAction: UINT;
    itemState: UINT;
    hwndItem: HWND;
    hDC: HDC;
    rcItem: TRect;
    itemData: DWORD;
  end;
  TDrawItemStruct = tagDRAWITEMSTRUCT;

各メンバーの意味はAPIのヘルプ等に譲るが,ここで重要なのはitemState,hDCおよびrcItemの三つである. itemStateはコントロールの状態を表している. ボタンの場合は,ボタンが押された状態か否か,あるいはフォーカスを持っている否かを このメンバーを見て判断しなければならない. hDCはコンポーネントのデバイスコンテキストである. コンポーネントの外観を描画するときは,WM_DRAWITEMメッセージと一緒に送られてくるこの デバイスコンテキストを使えばよい. rcItemは描画対象となるコンポーネントの境界を表すRect値である.

CN_DRAWITEMのメッセージハンドラではFCanvas.HandleにhDCを割り当てているが, FCanvasはコンポーネントのprivateなフィールドとして宣言しておき, コンストラクタの中で,

constructor TOwnerDrawButton.Create( AOwner: TComponent );
begin
   inherited Create( AOwner );
   FCanvas := TCanvas.Create;
end;

のようにインスタンスを作成しておく.当然,デストラクタでは

destructor TOwnerDrawButton.Destroy;
begin
   inherited Destroy;
   FCanvas.Free;
end;

としてFCanvasのインスタンスを破棄しなければならない. なぜinherited Destroyが先に来るかは不明である(BitBtnのソースがそうなっていたので何か意味があると思う).

FCanvasはCanvasプロパティとして参照可能とするが,

property Canvas: TCanvas read FCanvas;

のように読み出し専用で宣言する必要がある.

と,ここまで書いて面倒くさくなったので,とりあえず実際のTOwerDrawButtonのソースコードを示す. あとで気が向いたら他の部分の説明を付け加えることにする.


DrawFrameControlの使い方

気が向いたので説明を追加する. ボタンの外形の描画にはDrawFrameControlを用いる. これはその名の通りコントロール(Windowsではコンポーネントと呼ばずにコントロールと呼ぶのが一般的である) の外形を描画してくれる便利なAPIであり, プロトタイプは以下のように宣言されている.

function DrawFrameControl( DC: HDC; const Rect: TRect; uType, uState: UINT ): BOOL; stdcall;

DCは描画対象となるデバイスコンテキストであり,Rectは 描画領域を表すRect値である. 描画領域はCN_DRAWITEMと一緒に送られてきたTDrawItemStruct構造体のrcItemメンバーをそのまま渡せばよいだろう. uTypeは描画するコントロールの種類であり, 標準のボタンの場合はDFC_BUTTONを指定する. uStateはコントロールの状態を指定するためのフラグで,通常のボタンの状態(押されていない状態)を 描画するときは,

Flags := DFCS_BUTTONPUSH or DFCS_ADJUSTRECT;

とし,押されている状態を描画したいときは,さらに,

Flags :=  Flags or DFCS_PUSHED;

とする.また,無効状態のボタンを描画したいときは,

Flags :=  Flags or DFCS_INACTIVE;

とする.


ボタンの状態の調べ方

さて,ボタンの状態を取得する方法であるが,上でも述べたように, TDrawItemStruct構造体のitemStateメンバーを調べればよい. ボタンが押されているか否かはODS_SELECTEDとのandを取ればわかる. 例えば,

IsDown := State and ODS_SELECTED <> 0;

のように書けば(StateはitemStateと同じ),ボタンが押されているならばIsDownがTrueとなる. 同様に,フォーカスがあるか否かを調べるにはODS_FOCUSとのandを取り, 無効状態か否かを調べるにはODS_DISABLEDとのandを取る.


ボタン内部の描画

DrawFrameControlでボタンの外形を描画した後,ボタン内部の描画を行う. まず,Colorプロパティで指定された色で内部を塗りつぶす.

OldColor := FCanvas.Brush.Color;
FCanvas.Brush.Color := Color;
FCanvas.FillRect( Rect );
FCanvas.Brush.Color := OldColor;

という感じである. 塗りつぶす範囲はメッセージと一緒に送られてきたTDrawItemStruct構造体のrcItemメンバーを 若干修正して(ほんの少し小さくする.ソース参照)指定すればよいだろう.

次にボタンの中央にCaptionプロパティで指定された文字列を描画する. 通常はDrawTextを使うが,無効状態の場合はDrawStateを使う.


その他の留意点

WM_LBUTTONDBLCLKメッセージのハンドラを以下のように書いておかなければ, クリックを2回続けた場合,2回目のときにボタンが押された状態にならない.

procedure TOwnerDrawButton.WMLButtonDblClk( var Message: TWMLButtonDblClk );
begin
   Perform( WM_LBUTTONDOWN, Message.Keys, Longint( Message.Pos ) );
end;

また,EnabledプロパティおよびFontプロパティが変更されてときには速やかに再描画を行うため, CN_ENABLEDCHANGEDメッセージおよびCN_FONTCHANGEDメッセージを処理しなければならない.

procedure TOwnerDrawButton.CMEnabledChanged( var Message: TMessage );
begin
   inherited;
   Invalidate;
end;

procedure TOwnerDrawButton.CMFontChanged( var Message: TMessage );
begin
   inherited;
   Invalidate;
end;


ソースコード

ソースコード全体を以下に示す.

unit Odbtn;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
   StdCtrls, Buttons, ExtCtrls;

type
  TDrawButtonEvent = procedure( Control: TWinControl;
                       Rect: TRect; State: TOwnerDrawState ) of object;

  TOwnerDrawButton = class( TButton )
  private
    FCanvas: TCanvas;
    IsFocused: Boolean;
    FOnDrawButton: TDrawButtonEvent;
  protected
    procedure CreateParams( var Params: TCreateParams ); override;
    procedure SetButtonStyle( ADefault: Boolean ); override;
    procedure CMEnabledChanged( var Message: TMessage ); message CM_ENABLEDCHANGED;
    procedure CMFontChanged( var Message: TMessage ); message CM_FONTCHANGED;
    procedure CNMeasureItem( var Message: TWMMeasureItem ); message CN_MEASUREITEM;
    procedure CNDrawItem( var Message: TWMDrawItem ); message CN_DRAWITEM;
    procedure WMLButtonDblClk( var Message: TWMLButtonDblClk ); message WM_LBUTTONDBLCLK;
    procedure DrawButton( Rect: TRect; State: UINT );
  public
    constructor Create( AOwner: TComponent ); override;
    destructor Destroy; override;
    property Canvas: TCanvas read FCanvas;
  published
    property OnDrawButton: TDrawButtonEvent read FOnDrawButton write FOnDrawButton;
    property Color;
  end;

procedure Register;

//////////////////////////////////////////////////////////////////////////////
implementation

procedure Register;
begin
  RegisterComponents( 'MyVCL', [TOwnerDrawButton] );
end;

constructor TOwnerDrawButton.Create( AOwner: TComponent );
begin
   inherited Create( AOwner );
   FCanvas := TCanvas.Create;
end;

destructor TOwnerDrawButton.Destroy;
begin
   inherited Destroy;
   FCanvas.Free;
end;

procedure TOwnerDrawButton.SetButtonStyle( ADefault: Boolean );
begin
   if ADefault <> IsFocused then
   begin
      IsFocused := ADefault;
      Refresh;
   end;
end;

procedure TOwnerDrawButton.CreateParams( var Params: TCreateParams );
begin
   inherited CreateParams( Params );
   with Params do Style := Style or BS_OWNERDRAW;
end;

procedure TOwnerDrawButton.CNMeasureItem( var Message: TWMMeasureItem );
begin
   with Message.MeasureItemStruct^ do
   begin
      itemWidth := Width;
      itemHeight := Height;
   end;
end;

procedure TOwnerDrawButton.CNDrawItem( var Message: TWMDrawItem );
var
  SaveIndex: Integer;
begin
   with Message.DrawItemStruct^ do
   begin
      SaveIndex := SaveDC( hDC );
      FCanvas.Lock;
      try
        FCanvas.Handle := hDC;
        FCanvas.Font := Font;
        FCanvas.Brush := Brush;
        DrawButton( rcItem, itemState );
      finally
        FCanvas.Handle := 0;
        FCanvas.Unlock;
        RestoreDC( hDC, SaveIndex );
      end;
   end;
   Message.Result := 1;
end;

procedure TOwnerDrawButton.CMEnabledChanged( var Message: TMessage );
begin
   inherited;
   Invalidate;
end;

procedure TOwnerDrawButton.CMFontChanged( var Message: TMessage );
begin
   inherited;
   Invalidate;
end;

procedure TOwnerDrawButton.WMLButtonDblClk( var Message: TWMLButtonDblClk );
begin
   Perform( WM_LBUTTONDOWN, Message.Keys, Longint( Message.Pos ) );
end;

procedure TOwnerDrawButton.DrawButton( Rect: TRect; State: UINT );
var
  Flags, OldMode: Longint;
  IsDown, IsDefault, IsDisabled: Boolean;
  OldColor: TColor;
  OrgRect: TRect;
begin
   OrgRect := Rect;
   Flags := DFCS_BUTTONPUSH or DFCS_ADJUSTRECT;
   IsDown := State and ODS_SELECTED <> 0;
   IsDefault := State and ODS_FOCUS <> 0;
   IsDisabled := State and ODS_DISABLED <> 0;

   if IsDown then Flags := Flags or DFCS_PUSHED;
   if IsDisabled then Flags := Flags or DFCS_INACTIVE;

   if IsFocused or IsDefault then
   begin
      FCanvas.Pen.Color := clWindowFrame;
      FCanvas.Pen.Width := 1;
      FCanvas.Brush.Style := bsClear;
      FCanvas.Rectangle( Rect.Left, Rect.Top, Rect.Right, Rect.Bottom );
      InflateRect( Rect, -1, -1 );
   end;

   if IsDown then
   begin
      FCanvas.Pen.Color := clBtnShadow;
      FCanvas.Pen.Width := 1;
      FCanvas.Brush.Color := clBtnFace;
      FCanvas.Rectangle( Rect.Left, Rect.Top, Rect.Right, Rect.Bottom );
      InflateRect( Rect, -1, -1 );
   end
   else
      DrawFrameControl( FCanvas.Handle, Rect, DFC_BUTTON, Flags );

   if IsDown then OffsetRect( Rect, 1, 1 );

   OldColor := FCanvas.Brush.Color;
   FCanvas.Brush.Color := Color;
   FCanvas.FillRect( Rect );
   FCanvas.Brush.Color := OldColor;
   OldMode := SetBkMode( FCanvas.Handle, TRANSPARENT );
   FCanvas.Font.Color := clBtnText;
   if IsDisabled then
      DrawState( FCanvas.Handle, FCanvas.Brush.Handle, nil, Integer( Caption ), 0,
         ( ( Rect.Right - Rect.Left ) - FCanvas.TextWidth( Caption ) ) div 2,
         ( ( Rect.Bottom - Rect.Top ) - FCanvas.TextHeight( Caption ) ) div 2,
         0, 0, DST_TEXT or DSS_DISABLED )
   else
      DrawText( FCanvas.Handle, PChar( Caption ), -1, Rect,
                DT_SINGLELINE or DT_CENTER or DT_VCENTER );
   SetBkMode( FCanvas.Handle, OldMode );

   if Assigned( FOnDrawButton ) then
      FOnDrawButton( Self, Rect, TOwnerDrawState( LongRec( State ).Lo ) );

   if IsFocused and IsDefault then
   begin
      Rect := OrgRect;
      InflateRect(Rect, -4, -4 );
      FCanvas.Pen.Color := clWindowFrame;
      FCanvas.Brush.Color := clBtnFace;
      DrawFocusRect( FCanvas.Handle, Rect );
   end;

end;

end.


お問い合わせはメールにて: akasaka@klc.ac.jp

戻る
SEO [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送