1 引言
在开发一个大型通用控制系统时曾遇到这么一个问题:该系统软件包由若干个可执行文件和动态链接库组成,因为扩展性和兼容性的要求,需要将系统划分为若干个可执行文件和动态链接库,并且在大部分DLL中封装各自的操作界面,在调用DLL时将其中包含的部分界面嵌入地显示在主界面的某个区域或某个窗口内,与主界面的其他部分浑然一体。这样主程序与DLL在功能操作上各司其职,在外部界面上又彼此交融,使用户可以通过增加和修改DLL来实现对系统内部、外部的扩展和升级;同时因为DLL的跨语言特性,内部包含操作界面的DLL可以更为方便地在以后的不同工作、不同语言环境中更好地重复使用。
这一问题的应用较为广泛,但没有充分的资料来帮助解决,经过不断的试验,笔者将初步体会总结出来,用以抛砖引玉。本文中涉及的主程序和DLL都是在Delphi5.0下实现的,但因为其中所依赖的基础还是Windows本身的窗口机制,所以对于其他的语言平台也有实际意义。
在Delphi中如何创建DLL及输出DLL中的函数有较多资料进行过介绍,在本文中不再赘述,本文只针对DLL中的窗口部分做重点介绍。
2 DLL中自带窗口的创建和显示
DLL和普通EXE一样,可以自带窗口,用Delphi设 计包含窗口的DLL较其他语言更为方便。在Delphi的DLL工程中,窗口的生成和编程与普通的EXE工程基本相同,但与EXE文件不同的是:在Delphi的EXE工程中所包含的窗口是自动创建的,而DLL工程中所包含的窗口需要显示创建。
在通常的应用中,DLL将所包含的窗口的创建和显示函数(或过程)输出,由宿主程序根据情况调用将DLL中的窗口显示出来(如点击宿主程序中的某个按钮时显示DLL中的窗口),其窗口创建和显示的过程如下:
首先,创建一个DLL工程,并新建一个名为DllForm的Form,可以在该Form上放置任何控件。
窗体设计完成后,在该窗口的Unit中添加如下代码:
procedure ShowDllForm:stdcall;export;
begin
if DllForm = nil then
DllForm := TDllForm.Create(Application);
DllForm.Show;
end;
在上面的代码片断中,ShowDllForm即为DLL输出的窗口创建和显示过程,宿主程序通过调用该过程来创建并显示DLL中包含的窗口。TDllForm为DLL中窗口类的类型,DllForm为该窗口类型的变量,Delphi虽然不会为DLL自动创建窗口,但会为DLL中包含的窗口类型自动建立默认的变量(如:var DllForm:TDllForm)。
在宿主程序的某个窗口中放置一个按钮,在按钮的OnClick事件中添加ShowDllForm调用(有关方法请参阅口被显示出来。
至此,DLL中所包含的窗口已经可以正常显示,但该窗口与宿主程序的窗口互相独立和游离,还没有融合为一体。
注:上面的代码中用Show来显示DLL中的窗口,该函数显示的窗口为非模态形式;实际上DLL中的窗口也可以使用ShowModal函数来显示成模态的,本文阐述的是如何将DLL中的窗口与宿主程序的窗口融为一体,模态窗口显然不符合这一要求。
3 DLL中自带窗口与主程序中窗口的融合
DLL中的窗口与宿主窗口的融合是通过Windows的子窗口机制来实现的,即把DLL中的窗口设置为宿主窗口或宿主窗口中的某一部分的子窗口,DLL中的窗口即能和宿主程序中的窗口融为一体了。
在Windows的窗口机制中对窗口的操作依靠窗口句柄(Handle)来进行;Delphi中的可视化控件分为两种,其中一种TwinControl继承而来的控件,包括TForm,TPanel,TGroup等都是标准的Windows窗口,其Handle属性即代表其窗口句柄。这些控件都可以作为DLL中窗口融合于宿主窗口的容器,即DLL窗口可以在这些控件所在的范围内显示,宛如通过这些控件“嵌入”宿主窗口一样。
这时DLL必须知道这些容器的窗口句柄,并将自身窗口作为容器的子窗口,ShowDllForm必须改写如下:
procedure ShowDllForm:stdcall(Parent:THandle);stdcall;export;
begin
Application.handle:=parent;
//将容器设为应用程序句柄
//以非模态创建并显示窗口
if DllForm = nil then
DllForm := TDllForm.Create(Application);
DllForm.PParentWindow:=Parent;//将容器设置为父窗口
DllForm.Show;
end;
为了实现与宿主窗口的融合,在ShowDllForm过程中增加了以下内容:
(1)增加了Parent入口参数,该参数为Thandle类型,是宿主程序传来的容器句柄。
(2)增加了DllForm.ParentWindow:=Parent,该代码将DLL中窗口设为宿主窗口中容器的子窗口。
同样,在宿主窗口中可以添加一个Panel作为DLL窗口显示的容器,并将按钮的OnClick时间中的代码修改为ShowDllForm(Panel1.Handle),将容器Panel的句柄传到DLL。
此时运行宿主程序并点击该按钮,可以发现DLL中 的窗口显示在宿主窗口的Panel中,但仍然具有标题条,可以在Panel中进行移动;如果要解决这一问题,只需将DLL中窗口的BorderStyle属性设为bsNone即可。
此时,DLL中窗口已经与宿主程序的窗口融为一体,只是还有一个问题,DLL窗口不能获得输入焦点,按Tab和光标键时输入焦点不会进入到DLL窗口中去,即使用鼠标将焦点强制切换到DLL窗口中,一使用Tab键焦点又会回到主程序窗口。
4 DLL中自带窗口的焦点控制
DLL没有自己的消息循环,焦点问题的出现可能与此有关。经笔者反复试验,焦点问题可以这么解决:
(1)宿主程序转发DLL窗口的消息
首先,需要把ShowDllForm由procedure改为Functiion
function ShowDllForm:stdcall(Parent:THandle);stdcall;export;
begin
Application.handle:=parent;
//将容器设为应用程序句柄
//以非模态创建并显示窗口
if DllForm = nil then
DllForm := TDllForm.Create(Application);
DllForm.PParentWindow:=Parent;//将容器设置为父窗口
DllForm.Show;
Result := DllForm.Handle; //返回DllForm的句柄
end;
其次,在宿主程序中设置储存DLL窗口句柄的变量DllFormHandle,并在按钮的OnClick事件中利用ShowDllForm返回的句柄为其赋值。即:
DllFormHandle:=ShowDllForm(Panel1.Handle);
最后,在宿主程序中添加AppEvent控件,并在其OnMessage事件中增加如下代码:
if IsDialogMessage(ExternMonitorHandle,Msg)then
Handled:=True;
通过该代码转发DLL窗口的消息。
在做了上面的改动后,可以用鼠标将输入焦点送给DLL窗口,并用Tab和光标键在DLL窗口内移动焦点,但是还不能用Tab键将焦点从宿主窗口中移到DLL窗口中。
(2)在焦点进入容器时转换焦点到DLL窗口 为了能用Tab键将焦点从宿主窗口中移到DLL窗口中,可以在焦点进入DLL窗口容器(如Panel)时强制将焦点传送给DLL窗口;
如,在Panel的OnEnter事件中添加如下代码:
SetForegroundWindow(DllFormHandle);
//将DLL窗口设置为前景窗口
Windows.SetFocus(DllFormHandle);
//将焦点交给DLL窗口
现在,可以通过Tab键将焦点从宿主窗口中移到DLL窗口中,但是一旦焦点进入DLL窗口中后无法再回到宿主窗口。
(3)焦点从DLL窗口的返回
为了能使焦点从DLL窗口返回到宿主窗口,需要利 用一个“Wrap”手段,即在DLL窗口放置一个无用的WinControl控件来实现Wrap,例如一个Width,Height都
为0的Button,当焦点传递到该Button时强制返回焦点到宿主窗口;即,在Button的OnEnter事件中添加如下代码:
SetForegroundWindow(Application.Handle);
Windows.SetFocus(Application.Handle);
因为Application.Handle中存贮着宿主窗口的句柄,这两句调用可以强制把焦点返回给宿主窗口。至此,DLL中的窗口已经可以与宿主程序的窗口完全融合到一起,并
且可以和宿主程序一起使用输入焦点。经笔者试验,可以将多个DLL中的窗口在宿主程序窗口、其他DLL窗口中嵌入显示,可以和Panel,GroupBoxM,TabControl等控
件结合起来进行DLL窗口嵌入和嵌套,实现灵活多变的、浑然一体的窗口组合。