谈谈VCL程序标准化 (0分)

  • 主题发起人 主题发起人 laozhongcheng
  • 开始时间 开始时间
L

laozhongcheng

Unregistered / Unconfirmed
GUEST, unregistred user!
众所周知,Delphi所制作的VCL程序和其他Win32程序在界面上有些许不同,具体表现在程序的标题可以于不同主窗体的标题,还有任务栏按钮上点击右键仅存还原、最小化和关闭三个菜单项。出现这个问题的原因在于Delphi中有一个类TApplication和一个TApplication的实例——全局变量Application,这个全局变量掌管着Delphi VCL程序的基本行为。通过这个全局变量,我们才能以非常方便的方式来对程序进行操控。但是有时候这有点烦人,比如可能会使我们的用户困扰,特别是对于初学者,“怎么没法(通过右键点击任务栏按钮)最大化程序”。但这并不是最重要的,当我们要建立一些比较特异的程序风格时,就有可能碰上难题。因为每一个程序只允许拥有一份Application的实例,而窗体又没有自己的任务栏按钮。在某些特定环境下,譬如在制作Word 2002风格的程序或者DirectX的游戏时,程序的表现可能会非常奇怪。这就是为什么我们要让VCL程序标准化(VAN,VCL Application Normalization),而标准化后的VCL程序则称之为NAV程序(Normalized Architecture VCL application)。本文目的是探讨如何VAN一个NAV程序。

1. 删除Application对象的任务栏按钮
如果要获得和其他Win32程序相同的外观,首先我们就必须删除Application对象的任务栏按钮。要实现这个操作,只需要简单地往Application的隐藏窗口附加WS_EX_TOOLWINDOW风格就可以了。
向程序的主窗体添加OnCreate事件处理过程,假设主窗体的类被命名为TMainForm,过程会被自动命名为TMainForm.FormCreate。添加如下代码:

procedure TMainForm.FormCreate(Sender: TObject);
begin
SetWindowLong ( Application.Handle, GWL_EXSTYLE,
GetWindowLong (Application.Handle, GWL_EXSTYLE) or WS_EX_TOOLWINDOW);
end;

代码1.1 删除Application对象的任务栏按钮

现在,测试一下,可以看到,Application对象的任务栏按钮已经被删除。下一步,我们将考虑使用主窗体的按钮来替代。

2. 一个比较通用的方法
在很多组件中(比如DelphiX,一个用于Delphi的DirectX组件),都使用了这样一种方法。下面我们来展示这种方法。

1) 覆盖TMainForm.CreateParams
procedure CreateParams(var Params: TCreateParams)
override;

2) 添加代码
procedure TMainForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;

代码2.1 非常通用的显示窗体任务栏按钮的方法

这段代码是存在很多潜在的问题的。第一,当一个模式对话框在显示的时候,如果我们用鼠标键去点击任务栏按钮,将会使整个程序的输入焦点消失,并且无法通过Alt+Tab或者其他方式来切换到该程序,只有使用Ctrl+Alt+Del来结束任务。这是一个非常难以解决的问题,文章第五部分我们将讨论以另外的方式解决这个问题。
第二,当程序运行时,如果将程序最小化,程序会像第一个问题一样消失掉。还好这个问题并不难以解决,下面我们将首先来解决这个问题。

3. 解决最小化后消失
产生这个问题的原因就是我们没有正确处理响应的消息。在传统的Delphi程序(非NAV程序)里面,最小化是由Application对象完成的,而Application对象的窗口是一个隐藏窗口,所以在我们完全取消了Application对象的任务栏按钮后,最小化主窗体仍然由Application对象完成,结果就是主窗体消失。所以我们修改消息处理代码,在TForm拦截响应最小化消息(在Win32中,该消息是WM_SYSCOMMAND的一部分)。

1) 处理WM_SYSCOMMAND消息
procedure WMSysCommand(var Message: TWMSysCommand)
message WM_SYSCOMMAND;

2) 添加代码
procedure TMainForm.WMSysCommand(var Message: TWMSysCommand);
begin
if (Message.CmdType and $FFF0 = SC_MINIMIZE) then
begin
if not IsIconic ( Handle) then
begin
Application.NormalizeTopMosts;
if (Application.ShowMainForm or Visible) and IsWindowEnabled( Handle) then
DefWindowProc( Handle, WM_SYSCOMMAND, SC_MINIMIZE, 0);
end;
end
else if (Message.CmdType and $FFF0 <> SC_MOVE) or (csDesigning in ComponentState) or (Align = alNone) or (WindowState = wsMinimized) then
inherited;

if ((Message.CmdType and $FFF0 = SC_MINIMIZE) or
(Message.CmdType and $FFF0 = SC_RESTORE)) and
not (csDesigning in ComponentState) and (Align <> alNone) then
RequestAlign;
end;

代码3.1 解决最小化后消失

粗体部分的代码就是这段代码的精华。我们把消息经由Windows的缺省消息处理函数DefWindowProc来处理,这样我们的程序就正常了。
但是另外一个问题——关于模式对话框的显示问题仍然没有解决。下一步我们将涉及一些Win32 API的基础知识,然后我们将深入VCL源码。

4. 预备知识
当我们单纯使用Win32 API来编程,我们的操作步骤是先注册一个窗口类,然后通过CreateWindow或者CreateWindowEx来创建程序主窗口,接着显示我们所创建的窗口,然后进入消息循环。但是为什么一个纯Win32 API的应用程序能获得一个任务栏按钮呢?难道也使用了WS_EX_APPWINDOW风格?实际情况并不是这样的。如果我们使用WinSight32、Spy++或者类似程序来查看Win32 API应用程序主窗口的属性的话,可以看到,并没有附加上WS_EX_APPWINDOW风格。
就让我们用Delphi来创建一个纯Win32 API应用程序来看一看一个应用程序如何获得一个任务栏按钮。

program Test;

uses
Windows;

{$R *.RES}
var
Msg : TMsg;
wcex : WNDCLASSEX;
HandleA, HandleB : HWND;

function WndProc(hWnd : HWND
msg : UINT
wParam : WPARAM
lParam : LPARAM): LRESULT stdcall;
begin
if Msg = 2 then PostQuitMessage (0);
Result := DefWindowProc(hWnd, msg, wParam, lParam);
end;

const
ClassName : PChar = 'DelphiWin32API';
WnNAVme : PChar = 'Delphi Win32 API Program';

begin
wcex.cbSize := sizeof(WNDCLASSEX);

wcex.style := CS_HREDRAW or CS_VREDRAW;
wcex.lpfnWndProc := @WndProc;
wcex.cbClsExtra := 0;
wcex.cbWndExtra := 0;
wcex.hInstance := hInstance;
wcex.hIcon := HICON (nil);
wcex.hCursor := HICON (nil);
wcex.hbrBackground := HBRUSH (COLOR_WINDOW + 1);
wcex.lpszMenuName := nil;
wcex.lpszClassName := ClassName;
wcex.hIconSm := HICON (nil);
RegisterClassEx( wcex);

HandleA := CreateWindowEx( 0, ClassName, WnNAVme, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND(nil), HMENU(nil), hInstance, nil);
ShowWindow ( HandleA, SW_SHOW);

HandleB := CreateWindowEx( 0, ClassName, WnNAVme, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HandleA, HMENU(nil), hInstance, nil);
ShowWindow ( HandleB, SW_SHOW);

while GetMessage ( Msg, 0, 0, 0) do
begin
TranslateMessage (Msg);
DispatchMessage (Msg);
end;
end.

代码4.1纯Win32 API的Delphi程序

运行程序就会发现,两个出现的窗口中一个有任务栏按钮,另外一个却没有。那让我们来修改一下其中的代码,做另外一个测试。

HandleB := CreateWindowEx( 0, ClassName, WnNAVme, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND(nil), HMENU(nil), hInstance, nil);
ShowWindow ( HandleB, SW_SHOW);

代码4.2 修改过后的纯Win32 API的Delphi程序

编译并且运行。现在出现了两个窗口,两个窗口都拥有它自己的任务栏按钮。注意这两段代码的区别,特别是CreateWindowEx。从Win32 API帮助文件中,我们可以知道第9个参数是父窗口或者所有者窗口的句柄。当我们将其设置为HandleA时,这时由于窗口获得了一个已经存在的窗口作为所有者窗口,HandleB就不会获得任务栏按钮。当设置为nil时,这时所有者窗口被设置为Windows桌面的句柄,HandleB就拥有自己的任务栏按钮。
或许你已经明白,VCL源代码中存在类似的错误。下面,让我们深入VCL源代码。

5. 深入VCL

TObject
|
TPersistent
|
TComponent
|
TControl
|
TWinControl
|
TScrollingWinControl
|
TCustomForm
|
TForm
|
TMainForm(我们自己的类)

图表5.1 继承关系

上面列出了类之间的继承关系。从Delphi帮助中,我们知道,在这些类中,有一个叫CreateWindowHandle的过程。我们从Delphi帮助中摘取了这个过程的说明。

TCustomForm.CreateWindowHandle
procedure CreateWindowHandle(const Params: TCreateParams)
override;
说明
The CreateWnd method calls CreateWindowHandle to create the form window once it has been specified in the window-creation parameters. CreateWindowHandle creates the window by calling the CreateWindowEx API function, passing parameters from the record passed in the Params parameter. CreateWindowHandle removes the CS_HREDRAW and CS_VREDRAW class styles from the window class.
Params holds information needed when telling Windows to create a window handle.

TWinControl.CreateWindowHandle
procedure CreateWindowHandle(const Params: TCreateParams)
virtual;
说明
The CreateWnd method calls CreateWindowHandle to create the window for a control. CreateWindowHandle creates the window by calling the CreateWindowEx API function, passing parameters from the record passed in the Params parameter. Once the window is created, its handle is available as the Handle property.

图表5.2 CreateWindowHandle的说明,来自Delphi帮助文件,Borland版权所有

似乎问题就在TWinControl.CreateWindowHandle。打开forms.pas和controls.pas,并找到TWinControl.CreateWindowHandle。让我们来查看一下这个过程的源代码。

procedure TCustomForm.CreateWindowHandle(const Params: TCreateParams);
var
CreateStruct: TMDICreateStruct;
NewParams: TCreateParams;
begin
.
.
.
end;

procedure TWinControl.CreateWindowHandle(const Params: TCreateParams);
begin
with Params do
FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style,
X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);
end;

代码5.1 CreateWindowHandle过程源代码(来自Delphi VCL源代码),Borland版权所有

TWinControl.CreateWindowHandle使用Params.WndParent作为所有者窗口,这好像并没有什么问题。再看一看相关函数。

procedure TCustomForm.CreateParams(var Params: TCreateParams);
var
Icons: TBorderIcons;
CreateStyle: TFormBorderStyle;
begin
inherited CreateParams(Params);
InitAlphaBlending(Params);
with Params do
begin
if (Parent = nil) and (ParentWindow = 0) then
begin
WndParent := Application.Handle;
Style := Style and not (WS_CHILD or WS_GROUP or WS_TABSTOP);
end;
.
.
.
end;
end;
end;

代码5.2 TCustomForm.CreateParams过程源代码(来自Delphi VCL源代码),Borland版权所有

原因就出在粗体部分的代码中!Params.WndParent为窗体的所有者,而窗体的所有者必然为Application对象。这就是为什么我们的主窗体没有获得任务栏按钮的原因所在。

6. 解决
既然知道问题的原因了,那么我们就动手解决。有两种解决方法。一种就是从TForm中派生一个新的类,然后覆盖CreateWnd或者CreateWindowHandle过程。但是我们不推荐使用这种方法。因为,在TWinControl的派生类中这两个函数调用了很多私有成员过程,派生时,我们也必须考虑这些成员过程,这样花费的时间将会成倍增加。所以我们推荐第二种方法,也就是重写VCL源代码。但是这种方法有一个缺点,就是不能使用VCL的运行时包(关掉Build with runtime packages选项),如果您确实需要使用运行时包,请考虑用第一种方法来实现。
还有一个必须考虑的问题。几乎现有的所有Delphi程序都是在非NAV风格下制作的,如果我们重写源代码,可能会和现有程序不兼容。所以,我们增加一个布尔值变量,用于在两种风格之间切换(NAV风格和非NAV风格)。

请到Borland代码中心下载VAN库
所有代码均在Windows XP Professional + Delphi 6 Update Pack 2或C++Builder 6 Update Pack 1环境下通过测试。

结束
英文版 完成于2002年8月4日
中文版 完成于2002年8月5日
V2.0beta1(注意此版本号,从Borland代码中心下载时注意和此版本号进行对照)
http://codecentral.borland.com/codecentral/ccWeb.exe/listing?id=18693
劳中成(laozhongcheng@163.com)

相关文章
谈谈VCL程序标准化,本文:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=1244894
再谈VCL程序标准化:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=1247595
 
保留所有权利 转载请注明作者和来自www.delphibbs.com
未经许可 不得擅自用于出版目的
代码段可自由使用 但不得出版同类文章

请各位感兴趣的DFW进来讨论。多谢!
 
呵呵!写得好。
我喜欢,希望在这儿能看到越来越多这种经典文章。
 
看得好累,保存了先。
 
修改了一些bug
主要是在处理工具栏窗口作为主窗口那一点
在TCustomForm.CreateParams(var Params: TCreateParams);这一段
原来犯的一个错误就是没考虑使用传统风格的情况[:)]
还有把原来在TCustomForm.Create的代码移到现在的TCustomForm.CreateNew里面
对不起大家了
 
好文章。
 
我的方法还来得简单些。
http://eagleboost.myrice.com/issues/Materials/Articles/DelphiWindow.htm
 
你这种方法的缺点是无法应用到C++Builder
而且我曾经试过 对于文章中所提及的一些问题
这种方法无法解决
简单是简单 但是却不能完全解决问题
我的方法是基于完全解决问题的
 
再谈VCL程序标准化
http://www.delphibbs.com/delphibbs/dispq.asp?lid=1247595
介绍文章所说的两种方法中的另外一种方法
 
我的方法不能解决什么问题,你说说看。
我自以为都解决了。
 
呵呵,可以偷懒一下吗? 经过修改的Forms.pas和Dialogs.pas哪里有下载?
 
楼上 如果你要的话 我可以发给你的
我没有申请主页空间 所以没地方放
如果各位DFW谁有空间的 也可以寄放在他那里
先谢过了
 
嘿嘿,多谢了! 好人会有好报的 :)
我的Delphi是D6+sp2版本,不知是否兼容。
adnilzhou@hotmail.com
 
还有 to CathyEagle
你完全可以通过显示一个模式对话框来验证我的说法
而且 由于你的主窗体并不是你显示出来的窗体
在编程的时候 无法利用程序的Application.MainForm.Handle
对它进行操作
还有 最大的问题就是无法平滑应用到C++Builder
要经过比较大的修改
 
http://codecentral.borland.com/codecentral/ccWeb.exe/listing?id=18693
呵呵,Borland的网站还是很有用的。
 
to laozhongcheng:
Using the two units in my application, I have found one bug as follows:

Establish two forms, place a panel and a button on form1 and set the color of form2 to
clBlack.

type the code in Unit1 as floows,

uses unit2;
procedure TForm1.Button1Click(Sender: TObject);
begin
form2.Align := alclient;
form2.ManualDock(panel1);
form2.Visible := true;
end;

You can see the behavior is not the same as before.

Anyway, thank you for your component.
 
谢谢你这么快就为我们逮住了一个bug。
我试过了。
你的问题可以由http://www.delphibbs.com/delphibbs/dispq.asp?lid=1247595
这里所说的方法解决
我会在这两天研究比较好的解决方法 请耐心等待
如果谁还发现其他bug 请到这里来发表
多谢
 
to Adnil 问题解决了
新的文件已经上传到Borland代码中心
可以下载了
注意,原来的ShowOnTaskBar现在被我改成了ShowTaskBarButton
增加了一个OrdinaryForm

下面我来说一下问题是怎么发生的
原来VAN化之后,由于窗体都直接由Windows管理,TApplication
的停靠功能便不正常(由于TControl的停靠实现太复杂了,我还
没有弄清楚问题根本原因,望各位Delphi高手指教)。
只要把需要停靠的窗体改回仍然将TApplication
作为所有者窗口,就可以了。
注意创建这种停靠窗体之前,OrdinaryForm必须设置为True。
而创建模式对话框窗体之前,OrdinaryForm必须设置为False。
(连续创建同类窗体则不需要更改OrdinaryForm的值)
比如:
OrdinaryForm := True;
Application.CreateForm(TForm2, Form2);
OrdinaryForm := False;
Application.CreateForm(TAboutBox, AboutBox);

OK 问题解决。继续VAN我们的NAV程序。
 
to laozhongcheng:
这样做没有解决问题根本,窗口的Dock是可选的,也就是说,创建窗体之后,
可以选择普通显示,模态显示,停靠显示等等多种显示方式,并且是可以切换
的。
 
to Adnil
你可能没有理解我的做法。
模式对话框的显示问题实际上不是我的代码的问题,
而是VCL设计上的问题(可以说是失误,或者是在这种设计方式下最好的选择)
他所做的实际上并不是真的在显示模式对话框,而只是一种模拟。
按照Win32程序的设计,模式对话框的显示应该是
调用CreateDialog时设置对话框的父窗口
这样所有这个程序中的消息就被传递到当前显示的模式对话框,而由于
主窗口基本上没有接受到消息(实际上还是有,比如WM_SETCURSOR这样的消息)
所有就有了这种表现
在VCL中,模式对话框的父窗口却是Application.Handle,这样一来,我们的
主窗体就会出现问题,由于Win32并不知道应该禁止一些消息被传递到主窗体,
所以就造成了那些文章中所说的不可预料的错误。
所以,按照Win32的设计方式,模式对话框和普通窗口实际上是不同的。
而VCL中在程序一开始就建立了所有窗体(除了MDI子窗口和非自动创建窗体),
而且统一了窗体和对话框(他们都是从TCustomForm派生),虽然方便了编程,
但无疑为我们的标准化增加了难度。
从这个角度上说,在这两种模式下进行切换是不可能的。如果一定要的话,
就必须得新增加一个TDialog的类,来专门进行对话框的处理。

还有,为什么我不在TForm类中来增加这些变量,就是如果这样做的话,要对
整个VCL源代码进行重新编译,否则会报错——版本不匹配。

至于停靠显示,因为我还是Delphi的初学者,不理解Delphi如何实现停靠,
所以只能使用这种方式解决。不过,按照普通Win32程序的表现,应该可以使
窗体完全脱离TApplication。现在的措施就当是应急吧。日后再改进。
(V2.0,哈)
不过关于实时切换,可能停靠显示还有一些可能,另外就不太可能了。道理
前面已经说得很明白了。希望你能满意我的解释。有问题继续探讨。也希望
更多的人进入我们的讨论,发现更多问题,共同解决问题。
我的QQ:29500314 E-MAIL:laozhongcheng@163.com
Hotmail(MSN):laozhongcheng@hotmail.com
 
后退
顶部