TFindDialog に TControl 派生コントロールを乗せる必要があり、TFindDialogEx を実装しました。
TFindDialog に限らず TCommonDialog を継承するダイアログは、Windows のコモンダイアログのラッパーになっています。
そのため、VCL のコントロールを乗せるのは、少しテクニックが必要です。
具体的には、下記の様に TPanel などのコンテナに乗せてやる必要があるのです。
// Vcl.ExtDlg の TOpenTextFileDialog から引用constructor TOpenTextFileDialog.Create(AOwner: TComponent);varI: Integer;begin(省略)FPanel := TPanel.Create(Self);with FPanel dobegin(省略)with FLabel dobegin(省略)Parent := FPanel;end;FComboBox := TComboBox.Create(Self);with FComboBox dobegin(省略)Parent := FPanel;end;end;end;
もし、TPanel に乗せないとどうなるかというと、フォーカスを受け取れなくなり、マウス・キーボードの操作が効かなくなります。
ということで、まねして TFindDialogEx のコンストラクタでも TPanel のインスタンスを生成します。
constructor TFindDialogEx.Create(iOwner: TComponent);begininherited;FPanel := TPanel.Create(Self);with FPanel do beginBevelInner := bvNone;BevelOuter := bvNone;StyleElements := [];FullRepaint := False;TabOrder := 0;TabStop := True;Color := clBtnFace;end;end;
ここで重要なのは StyleElements です。
Windows のコモンダイアログに乗るので Style による描画は必要ありません。
もし、StyleElements に何もしていないと……
上図のように周りから浮いてしまいます。
次に、ダイアログに乗せるコントロールを追加するメソッドを定義します。
// iControl を FPanel の子供に設定するprocedure TFindDialogEx.AddControl(const iControl: TControl);beginif (FControl <> nil) thenFControl.Parent := nil;FControl := iControl;if (FControl <> nil) then beginFControl.Parent := FPanel;ListupControl(FControl,procedure (const iControl: TControl)beginiControl.StyleElements := [];end);end;end;// ListUpControl は全てのコントロールを列挙するtypeTListupCallback = reference to procedure (const iControl: TControl);procedure TFindDialogEx.ListupControl(const iControl: TControl;const iCallback: TListupCallback);vari: Integer;beginif (iControl <> nil) then beginif (iControl is TWinControl) thenwith TWinControl(iControl) dofor i := 0 to ControlCount - 1 doListupControl(Controls[i], iCallback);iCallback(iControl);end;end;
このように FPanel を Parent に設定しています。
また、FPanel と同様に StyleElements も [] として設定しています。
ListupControl では reference to を使って、無名メソッドを呼び出すようにしています。
これによって、Listup した Control を簡単に処理できます。
これによって、Listup した Control を簡単に処理できます。
次に、ダイアログに FPanel を乗せる処理です。
コモンダイアログは、WM_INITDIALOG メッセージで、初期化処理をします。
TCommonDialog には WM_INITDIALOG メッセージを受信したときに呼ばれる DoShow メソッドがあります。
そこで、これを override して、初期化処理をします。
typeTFindDialogEx = class(TFindDialog)(略)protectedprocedure DoShow; override;(略)end;implementationprocedure TFindDialogEx.DoShow;varWndRect: TRect;tmpRect: TRect;begininherited;GetWindowRect(Handle, WndRect); // Screen 座標の位置。NoClient エリアも含むGetClientRect(Handle, tmpRect); // Window 座標の位置。Client エリアのみif (FControl <> nil) then beginFPanel.ParentWindow := Handle; // Dialog を親に設定// パネルのサイズを計算FPanel.ClientHeight := FControl.Height;FPanel.SetBounds(0,tmpRect.Height - 10, // 10 pixel 位上だと見栄えが良いtmpRect.Width,FControl.Height);// パネルの高さ分、ダイアログのウィンドウを伸ばすMoveWindow(Handle,WndRect.Left,WndRect.Top,WndRect.Width,WndRect.Height + FPanel.Height, // FPanel の高さ分伸ばすTrue);end;end;
では、ここまでで動かしてみると……
なんと、コントロールが表示されていません。
ちなみに、マウスをオーバーしたり、Alt キーを押すと表示されます。
これは、Windows のコモンダイアログのメッセージ処理が上手く流れてこないためです。
なので、コモンダイアログの WM_ERASEBKGND メッセージで、コントロールを再描画してやる必要があります。
procedure TFindDialogEx.WMEraseBkGnd(var ioMsg: TWMEraseBkGnd);begininherited;ListupControl(FControl,procedure (const iControl: TControl)beginiControl.Repaint; // 再描画させるend);end;
では、これでもう一度表示させてみると……
上手く表示されました。
で、事はこれで終わりかと思いきや、とんでもないバグが潜んでいたのです。
TFindDialog は他のダイアログと違い、モードレスなダイアログです。
つまり、ダイアログを表示している最中に、ユーザーは任意のフォームを操作できます。
そのため、TFindDialog を出したまま、スタイルを変更することもできるのです。
もし、そうするとどうなるかというと、Execute を呼び出してもダイアログは表示されなくなります。
これは、何故かと言うと下記のコードが Execute に記述されているからです。
function TFindDialog.Execute(ParentWnd: HWND): Boolean;varOption: TFindOption;beginif FFindHandle <> 0 then // 0 かどうかで判断しているbegin// 0 じゃなかったら既に表示されていると判断して// ウィンドウを前に持ってくる処理を走らせる// しかし! ウィンドウハンドルが無効だった場合、何も起きない!BringWindowToTop(FFindHandle);Result := True;end elsebegin(略)end;end;
Style が変更されるとウィンドウは RecreateWnd を使って再生成されます。
そのため、FFindHandle は 0 ではありませんが、無効なウィンドウハンドルになっています。
これによって、Execute を呼び出しても何も起きないのです。
そこで、この不具合を取り除くために拡張 RTTI を使って、FFindHandle を 0 にします。
function TFindDialogEx.Execute(ParentWnd: HWND): Boolean;varTId: DWORD;Context: TRttiContext;Field: TRttiField;Obj: TObject;begin// 無効なウィンドウハンドルでは 0 が返るTId := GetWindowThreadProcessId(Handle);if (TId = 0) then beginContext := TRttiContext.Create;trytryField := Context.GetType(Self.ClassType).GetField('FFindHandle');if (Field <> nil) thenField.SetValue(Self, 0); // FFindHandle を 0 にField := Context.GetType(Self.ClassType).GetField('FRedirector');if (Field <> nil) then beginObj := Field.GetValue(Self).AsObject;if (Obj <> nil) thenObj.Free; // FRedirector を解放end;exceptend;finallyContext.Free;end;end;Result := inherited;end;
FFindHandle だけではなく FRedirector というコントロールも破棄する必要があるので、それも破棄しています。
このように拡張 RTTI を使うと private として隠蔽されていた変数やメソッドまで呼べてしまいます。
Java を知っている方にはおなじみ?のリフレクションと同じ機能です。
静的型付けのコンパイル型言語で、同じ事ができるとは驚きです。
静的型付けのコンパイル型言語で、同じ事ができるとは驚きです。
非常に強力なので、注意が必要です。
拡張 RTTI については、第23回デベロッパーキャンプの福士さんの資料が詳しいです。
これで、完全に動作するようになりました。
Delphi でコモンダイアログをカスタマイズする方法があまり存在しないので、書いてみました。
0 件のコメント:
コメントを投稿