在 Delphi 下自定义通用对话框
--------------自定义打开文件对话框
几次碰到有人在论坛里问如何在 Delphi 下自定义通用对话框,本人对此问题也比较感兴趣,所以抽点时间搞了下,现在把“成果”与大家分享。本文的题目大了点,通用对话框有好多,但这里只以打开文件对话框为例。其实自定义所有通用对话框的原理是一样的。
第一步:建立对话框模版
先来说个概念:对话框模版。对话框模版是一种资源,在 .rc 文件中定义,编译后生成 .res 文件,最终一般存在于资源动态链接库(DLL)中或可执行程序中。在专业的共享软件中一般都大量使用模版来创建对话框。通过模版生成的对话框一般用来采集用户输入,上面可以放标准的 Windows 控件,比如 Button, Label, TextBox, ListBox, ListView, TreeView 等。
通用对话框也是由对话框模版生成的窗体,只不过这些对话框模版由操作系统定义,自定义通用对话框就是通过更改这些模版来实现的(打开和保存文件对话框例外,它们是通过添加新的模版来自定义的)。所以第一步要知道怎样定义对话框模版,可以在 Notpad 里直接敲 .rc 文件(这种方法这里就不使用了),还可以使用现有的工具,我机器上最好的工具是 Visual Studio .Net IDE,只需要点几下鼠标即可。(用它也可以查看、修改可执行文件中的资源,直接点打开->文件,打开可执行文件即可)。现在就先在 VS.Net 中定义一个对话框模版(过程略),该模版就是我们在打开文件对话框上自定义的部分,需要注意的是该模版必须具有 DS_3DLOOK 、DS_CONTROL 、WS_CHILD 、WS_CLIPSIBLINGS 风格且不能有 Border,因为通用对话框是将我们的整个模版当作子窗体SubClass 到原有对话框的(类似 Button 等标准控件与其拥有者的关系)。我将对话框模版的外观和 .rc 文件的内容贴出来:
// .rc 文件内容
131 DIALOGEX 0, 0, 282, 36
STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | DS_CONTROL | WS_CHILD |
WS_CLIPSIBLINGS
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "文件名称:",-1,7,8,40,8
LTEXT "此静态控件用来显示文件名称",1004,51,8,224,8,
SS_PATHELLIPSIS
CONTROL "如果选中文件是图片文件则进行预览",1005,"Button",
BS_AUTOCHECKBOX | WS_TABSTOP,7,18,268,10
END
上面对话框模版中的 CheckBox 没有任何实际意义,只用来说明功能。将此 .rc 文件编译成 .res 文件(可以用 VS.Net 直接编译,也可以用 Delphi 带的 brcc32 工具)以备后面例子使用。
第二步:继承 TOpenDialog 类
调用打开文件对话框只需一个 API:GetOpenFileName,这个 API 需要一个 OPENFILENAME 结构的参数,自定义对话框时将该结构的 lpTemplateName 成员指定为对话框模版的标识(Identifier)并在 flags 成员中包含 OFN_ENABLETEMPLATE 常数即可。
对话框模版及其包含的每个控件都应该有自己唯一的标识,而且这些标识不能与通用对话框上原有控件的标识重复。标识有2种:字符串标识和数字标识,在本例中使用数字标识:对话框模版的标识为 131,第一个静态控件的标识为 -1,第二个静态控件的标识为 1004,CheckBox 的标识为 1005。(标识为 -1 的控件一般为内容固定不变的静态控件。) OPENFILENAME 结构的 lpTemplateName 成员的类型是 null-terminated 字符串指针,如果对话框模版的标识为字符串,则可直接赋值,比如某模版的标识为 IDD_MYDIALOG,那么赋值语句为:lpTemplateName := PChar('IDD_MYDIALOG'),如果为数字,比如本例中的模版的标识为 131,赋值语句则为:lpTemplateName := Windows.MakeIntResource(131)。
Delphi 中所有通用对话框类都继承自 TCommonDialg 抽象类,在这个抽象类中定义了个受保护的(Protected)属性:Template,类型为 PChar。这个属性就是用来存放模版标识的,但遗憾的是我们不能在除它的继承类以外的地方访问到它(因为受保护)。所以我们需要在它的孙子类中重新定义一公有(Public)属性来间接访问它。
在 Delphi 里新建一个 VCL 类 TMyOpenDialog 继承自 TOpenDialog 类,定义一公有属性 TemplateRes ,通过其写入方法为受保护的 Template 属性赋值,下面列出代码:
//********************************************
// MyOpenDialog.pas
// TMyOpenDialog 类实现自定义打开文件对话框
// by: Joe Huang date: 2004-01-05
//********************************************
unit MyOpenDialog;
interface
uses
SysUtils, Classes, Dialogs, Windows, Messages, CommDlg;
type
TCommandEvent = procedure (ControlID: Word) of object;
TMyOpenDialog = class(TOpenDialog)
private
{ Private declarations }
FTemplateRes: PChar;
FOnCommand: TCommandEvent;
procedure SetTemplateRes(const Value: PChar);
protected
{ Protected declarations }
procedure WndProc(var Message: TMessage); override;
public
{ Public declarations }
//该属性用来指定自定义模版的标识
property TemplateRes: PChar read FTemplateRes write SetTemplateRes;
published
{ Published declarations }
property OnCommand: TCommandEvent read FOnCommand write FOnCommand;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TMyOpenDialog]);
end;
{ TMyOpenDialog }
procedure TMyOpenDialog.SetTemplateRes(const Value: PChar);
begin
FTemplateRes := Value;
Self.Template := Value;
end;
procedure TMyOpenDialog.WndProc(var Message: TMessage);
begin
Message.Result := 0;
if (Message.Msg = WM_COMMAND) then
begin
if Assigned(FOnCommand) then
FOnCommand(Message.WParamLo);
end;
inherited WndProc(Message);
end;
end.
TMyOpenDialog 类还定义了一个事件用来捕获对话框模版上控件状态发生的改变(后面说明此事件的用法)。将该类注册到组件面板的 Samples 页中。
第三步:建立工程实现自定义对话框
新建一工程保存至目录,将第一步中的 .res 文件放至该目录中并加入到工程文件中。从 Samples 页中把我们的新控件拖入 Form1 中,名字为 MyOpenDialog1,再在 Form1 上放入 Button1,Button1 的 On_Click 事件代码如下:
procedure TForm1.Button1Click(Sender: TObject);
begin
MyOpenDialog1.TemplateRes := Windows.MakeIntResource(131);
MyOpenDialog1.Execute;
end;
MyOpenDialog1 的 On_Show 事件代码如下:
procedure TForm1.MyOpenDialog1Show(Sender: TObject);
begin
//标识为 1004 的控件为第二个静态控件,用来显示选择的文件全名
//此事件一般用来初始化模版上的控件内容和状态
SetDlgItemText(MyOpenDialog1.Handle, 1004, '初始化...');
end;
MyOpenDialog1 的 On_SelectionChange 事件代码如下:
procedure TForm1.MyOpenDialog1SelectionChange(Sender: TObject);
begin
//在第二个静态控件上显示选择的文件全名
SetDlgItemText(MyOpenDialog1.Handle, 1004,
PChar(MyOpenDialog1.FileName));
end;
MyOpenDialog1 的 On_Command 事件代码如下:
procedure TForm1.MyOpenDialog1Command(ControlID: Word);
var
hCtrl: HWND;
begin
if ControlID = 1005 then //模版上的 CheckBox 控件,当状态发生改变时触发此事件
begin
hCtrl := GetDlgItem(MyOpenDialog1.Handle, ControlID);
if hCtrl <= 0 then Exit;
if SendMessage(hCtrl, BM_GETCHECK, 0, 0) = BST_CHECKED then
ShowMessage('You checked me.')
else
ShowMessage('You unchecked me.');
end;
end;
运行后效果见下图:
小结
本例中的自定义部分默认的放在了对话框原有部分的下面,那么如何把对话框中自定义部分放在其它位置呢?其实这个工作不是在代码里做的,而是在第一步创建模版的时候。方法是在模版上放置一个不显示的静态控件(从 MSDN 带的例子来看可以放多个这样的控件),并且这个静态控件的标识必须为 stc32 ,即 0x045F 。通用对话框通过模版上其他控件相对于这个静态控件的位置来定位自定义部分相对于原有部分的位置,模版中位于 stc32 静态控件上方和左边的控件将被安放在通用对话框原有部分的上方和左边,同理,位于 stc32 静态控件下方和右边的控件将被安放在通用对话框原有部分的下方和右边。( All new controls above and to the left of the stc32 control are positioned the same amount above and to the left of the controls in the default dialog box. New controls below and to the right of the stc32 control are positioned below and to the right of the default controls. In general, each new control is positioned so that it has the same position relative to the default controls as it had to the stc32 control. )
再说说我个人认为 Delphi 封装通用对话框不够完美的地方,一般来说通用对话框都有一个父窗体(Parent Window)或拥有者(Owner),拿打开文件对话框来说:OPENFILENAME 结构的 hwndOwner 成员指定其父窗体或拥有者的句柄,但 Delphi 并没有让我们来指定,而是将它指定为 Application.Handle,其他通用对话框也都是这样。
MSDN 中有个 C++ 的完整例子,也是自定义打开文件对话框,很短也很容易看懂,它的对话框模版涉及到定位,可在下面位置找到该例子:
User Interface Design and Development
└User Interface Design & Usability
└Technicle Articles
└Using the Common Dialogs Under Windows 95
本文参考了 MSDN 以下部分:
User Interface Design and Development
└Windows Management
└User Input
└Common Dialog Box Library
和
User Interface Design and Development
└Windows Management
└Windowing
└Dialog Box
The end. 作者:Joe Huang 欢迎来 Email 讨论:happyjoe@21cn.com 时间:2004年1月5日 6:01 PM