代码改变世界

[资料收集]动态链接库DLL

2013-08-06 14:56  lilooo  阅读(267)  评论(0编辑  收藏  举报

一、Delphi中DLL的消息处理

[来源]http://hi.baidu.com/game_base/item/d6157b61c28d722268105bed

以前用到DLL的时候, 从来没有想过这个问题. 一直觉得这完全是没必要考虑的问题. 可前不久的一个开发完全打破了我的这种想法. 看来, 很多东西并不是想像的完美.

事情的导火线是GIF图片的显示. 在应用程序中, 利用三方的GIFImage.pas可以很好的显示GIF图片.
这次, 要在一个DLL中显示一个GIF图片. 还是像往常一样拖个TImage放到窗体上, 打开一个动态GIF图片. 编译, 运行. 怪了: GIF图片显示是静态的. 还以为Delphi又出Bug了. 于是又把图片放到程序主窗体上一运行, 动的. 这下头可大了!

找相关的资料, 没有.

看来事情还得自己解决: 于是专心研究起GIFImage.pas, 首先看的当然是重画部分的代码了( 呵呵, 这是我一贯的风格: 觉得是哪里出问题就先看哪里, 不管是谁的代码 ). 经过一番搜索. 把目标定位在线程上. GIFImage.pas的重画其实就是调用一个线程, 在线程内读取文件中相应的图像数据画到目标位置. 在线程内重画是调用线程的Synchronize过程. 以前知道这个过程是为了避免多个线程同时访问同一个数据或对象的. 现在得对它的执行方法做一番了解才行.

经过一翻摸索, 找到了解决方法. 在DLL的窗体上放一个TTimer控件. Interval尽量小. OnTimer只添加一行代码: CheckSynchronize;

运行. OK. 图片动起来了......( 这种方法所存的问题就不用再多说了吧. )

但接下来的一个问题却很恼火的: 在DLL的窗体上放一个TSpeedButton控件, Flat属性设置为True. 运行. 当鼠标从TSpeedButton上移过时, TSpeedButton怎么也还原不了. 试着调用它的重画等功能. 全部没用. 好几天的时间一直在思考这个问题.

后来在处理应用程序的消息的时候, 突然想到: DLL虽然有自己Application, 但它并没有自己的消息循环, 而线程的Synchronize不能执行, TSpeedButton不能还原都是因为有些消息没有得到相应的处理而导致的.

也就是说, 只要给DLL加上一个消息循环, 上面的这些问题都会全部解决.

刚开始的时候想从主程序发送消息给DLL. 可消息截取的结果是: 很多DLL里产生的消息并没有发送给主程序. 看来这个方法是行不通的. 只得另寻方法.

在看到以下几行大家很熟悉的代码后想到.

  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;

可不可以给DLL也加上这们的代码呢?

动手实验, 创建一个DLL, DLL里包含一个窗体DLLForm. 从DLL里导出一个函数. 加上上面的代码. 如下:

procedure InitDLL; stdcall;
begin
  Application.Initialize;
  Application.CreateForm(TDLLForm, DLLForm);
  Application.Run;
end;

再到主程序窗体的创建事件代码如下:

procedure TForm1.FormCreate(Sender: TObject);
begin
  InitDLL;
end;

运行, 结果不对. 它是先打开主窗体了... :( 郁闷. 并且 InitDLL; 也不是立即返回, 而是当DLL里主窗体关闭后才返回. 其实早就应该想到了.

把OnCreate的代码放到一个TTimer控件里. Interval为1. OnTimer的代码如下.

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  TTimer(Sender).Enabled := False;
  InitDLL;
end;

这下可以了. 但不能让DLL里的窗体一开始就显示出来吧. 得. 再改改InitDLL. 如下:

procedure InitDLL; stdcall;
begin
  Application.Initialize;
  Application.ShowMainForm := False;
  Application.CreateForm(TDLLForm, DLLForm);
  Application.Run;
end;

主窗体不显示了, 得加上一个, 看看效果:)

再到DLL里加上一个Form( 命名为 DLLChildForm ), 在窗体上放一个TSpeedButton控件.
再给DLL导出一个函数, 如下:

procedure CreateChildForm; stdcall;
begin
  with TDLLChildForm.Create(Application) do
  begin
    Show;
  end;
end;

再到主窗体中添加一个按钮. 点击事件代码如下.

procedure TForm1.Button1Click(Sender: TObject);
begin
  CreateChildForm;
end;

运行. 结果理想: TSpeedButton在鼠标移过后能还原了. 呵呵...... 真爽!

不过, 问题又来了. 程序退出时报异常了. 想一下, 哦. DLL里的窗体资源还没有释放呢. 得, 再从DLL里导出一个过程, 代码如下:

procedure DestoryDLL; stdcall;
var
  i: Integer;
begin
  for i := Application.ComponentCount - 1 downto 0 do
  begin
    if Application.Components[i].ClassNameIs('TDLLChildForm') then
    begin
      TDLLChildForm(Application.Components[i]).Release;
    end;
  end;

  if DLLForm = nil then
  begin
    Exit;
  end;

  DLLForm.Release;
  DLLForm := nil;
end;

再给主程序主窗体的OnCloseQuery添加代码如下:

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  DestoryDLL;
end;

运行. 靠, 虽然DLL里的窗体全关闭了, 可主程序还是退不出啊. 换换方法, 把 DLLForm.Release; 这里改成Application.Terminate; 试试. 还是不行. 咋回事?

反复调试, 发现虽然Terminate了, 可Run仍在循环. 并没有结束.
再研究Run的代码. 呵呵. 有了.
把Application.Terminate;换成PostMessage(Application.Handle, WM_QUIT, 0, 0);
运行, 还是不行. 但Run是循环是退出了. 那哪里还会有问题呢? 该不会是窗体没有释放吧. 好, 在PostMessage前加上DLLForm.Release;这时, DestoryDLL过程的代码如下:

procedure DestoryDLL; stdcall;
var
  i: Integer;
begin
  for i := Application.ComponentCount - 1 downto 0 do
  begin
    if Application.Components[i].ClassNameIs('TDLLChildForm') then
    begin
      TDLLChildForm(Application.Components[i]).Release;
    end;
  end;

  if DLLForm = nil then
  begin
    Exit;
  end;

  DLLForm.Release;
//  Application.Terminate;
  PostMessage(Application.Handle, WM_QUIT, 0, 0);
  DLLForm := nil;
end;

运行. OK. 完美解决...

再加上线程试试( 这时InitDLL过程要改成如下, 这样才能真正的处理所有的消息 ) . 真爽. 与想像的一样.

procedure InitDLL(AHandle: Thandle); stdcall;
begin
  Application.Initialize;
  Application.ShowMainForm := False;
  Application.CreateForm(TDLLForm, DLLForm);

  // 保存原来的句柄
  DLLForm.Tag := Application.Handle;
  // DLL 从属的句柄 ( 如果没有此行, 线程的执行不能达到理想效果 )
  // 并且这样才能真正的让消息循环处理它应处理的所有消息
  Application.Handle := AHandle;
  Application.Run;

  Application.Handle := DLLForm.Tag;
end;

 

二、delphi 开发中遇到的DLL问题思考及解决方法

[来源]http://www.delphitop.com/html/Dll/2010.html

最近在公司写一个外壳程序,调用DLL插件把FORM嵌入到EXE中的一个PANEL中,其中遇到了不少的问题,大部分已经解决,还有几个至今没有找到解决方法,有待研究,也希望知道解决方法的富翁共享一下研究成果.

  以下列出的问题及解决方法仅针对我写的程序(DLL插件把FORM嵌入到EXE中的一个PANEL中),和自己的解决方法.

  从遇到的问题看出,DELPHI封装了太多的东西,有时候直接使用API会有意想不到的效果.
  经验:DLL与EXE之间的通讯应该全部使用消息. 


第一个问题:Tab键和Enter键在DLL的FORM中无效

原始程序:
//frmDll为DLL中的FORM,frmEXE为EXE主窗体,下同
//下面的代码为什么直接引用Exe中的Form又引用Dll中的Form?只是为了方便阅读,实际只是传递一个句柄,下同
//panWorkSpace为Exe中的一个TPanel,DLL中的窗体要嵌入其中
frmDll.WindowState := wsMaximized;
frmDll.BorderStyle := bsNone;
windows.SetParent(frmDll.Handle,frmExe.panWorkSpace.Handle);

发现Tab及Enter键在嵌入的FORM中无效,去掉

frmDll.BorderStyle := bsNone;

后正常,但我不需要标题,就用API解决

frmDll.WindowState := wsMaximized;
SetWindowLong(frmDll.Handle,GWL_STYLE,GetWindowLong(frmDll.Handle,GWL_STYLE) and not (WS_CAPTION or WS_THICKFRAME));
windows.SetParent(frmDll.Handle,frmExe.panWorkSpace.Handle);

其中WS_CAPTION和WS_THICKFRAME分别表示标题栏和边框,问题解决.

 
第二个问题:DLL窗体的Resize
EXE主窗体改变尺寸时,窗体中的Panel也会跟着变(Panel.Align设为了alClient),但其中嵌入的DLL窗体不会跟着变,解决方法:
//exe窗口接收消息并改变子窗体大小
//FChildWindowList为TList,子窗体的结构信息列表
Type
//子窗体一些信息的结构体
PFormInfo = ^TFormInfo;
TFormInfo = record
  Handle : HWND;
  Parent : HWND;
  Style : HWND;
end;

TfrmExe = class(TForm)
private
  procedure WMSize(var Message:TWMSize);message WM_Size;
end;

procedure TfrmExe.WMSize(var Message: TWMSize);
//ReSize消息
var
  i : Integer;
  rc : TRect;
begin
  inherited;
  if GetWindowRect(panWorkSpace.Handle,rc) then
    if Assigned(FChildWindowList) then
    for i := 0 to FChildWindowList.Count - 1 do
      SetWindowPos(PFormInfo(FChildWindowList[i]).Handle, 0,
      0, 0, rc.Right - rc.Left, rc.Bottom - rc.Top, SWP_NOACTIVATE);
end;

 

第三个问题:焦点在DLL中的窗体时,切换到其它应用程序,再点击任务栏上Application对象的按钮,不能切换过来

焦点在DLL中的窗体时,切换到其它应用程序,再点击任务栏上Application对象的按钮,不能切换过来,EXE主窗体不最小化,切换到其它程序,直接点嵌入的DLL窗体,DLL窗体获得焦点,发现Application对象在任务栏上的按钮是被按下去了,但是EXE窗体并没有被提到最前,还有,DLL窗体得到焦点时,EXE窗体的标题栏变为灰色,这些都是不符合使用习惯的,虽然不影响使用,但我觉得还是要解决.

1.DLL窗体得到焦点时,EXE窗体的标题栏变为灰色的解决方法.
DLL窗体
TfrmDll=class(TForm)
private
procedure WMActivate(var Message : TMessage);message WM_ACTIVATE;
end;

procedure TfrmDll.WMActivate(var Message: TMessage);
begin
inherited;
SendMessage(frmEXE.Handle, WM_NCACTIVATE, Integer(True), 0);
end;

2.焦点的问题解决方法

把下面这个单元加入工程
//==============================================================================
// Unit Name: AppHandler
// Author : ysai
// Date : 2003-06-05
// Purpose : 处理焦点问题
// History :
//==============================================================================

unit AppHandler;

interface

uses
  Windows, Messages, SysUtils,Forms;

implementation

var
  OldWProc : TFNWndProc;

function NewWndProc(
  Handle : HWND;
  Msg : Integer;
  wParam : Longint;
  lParam : Longint
):Longint; stdcall;
begin
  Result := 0;
  case Msg of
    WM_ACTIVATEAPP : //嵌入到主窗口的DLL中的窗口得到焦点不会把程序提前
    begin
      case wParam of
        0 : //应用程序失去焦点
        begin
          if Assigned(Application.MainForm)
           and (GetWindowLong(Application.Handle, GWL_EXSTYLE)
              and WS_EX_TOOLWINDOW = 0) then
            SendMessage(Application.MainForm.Handle, WM_NCACTIVATE,
Integer(False),
0);//失去焦点把标题栏变灰 end; 1 : //应用程序得到焦点 begin if Assigned(Application.MainForm) and (GetWindowLong(Application.Handle, GWL_EXSTYLE) and WS_EX_TOOLWINDOW = 0) then SendMessage(Application.MainForm.Handle, WM_ACTIVATE, WA_ACTIVE, 1);//注意,这里设为1,后面会用到 end; //case wParam end; Result := CallWindowProc(OldWProc, Handle, Msg, wParam, lParam); end; //msg : WM_ACTIVATEAPP else Result := CallWindowProc(OldWProc, Handle, Msg, wParam, lParam); end; //case msg end; initialization //取代应用程序的消息处理 OldWProc := TFNWndProc(SetWindowLong(Application.Handle, GWL_WNDPROC, Longint(@NewWndProc))); finalization //还原消息处理过程 if OldWProc <> nil then SetWindowLong(Application.Handle, GWL_WNDPROC, LongInt(OldWProc)); end.
//单元结束
//EXE程序主窗口
TfrmEXE = class(TForm)
private
  procedure WMActivate(var Message : TMessage);message WM_ACTIVATE;
end;

procedure TfrmExe.WMActivate(var Message: TMessage);
//激话消息,Message.lParam=1时是OAAppHandler单元发来的,激活子窗口
var
  hWindow : HWND;
begin
  inherited;
  if Message.lParam = 1 then //如果是1就是AppHander发出的消息,将焦点设到活动子窗体
  begin
    hWindow := GetActiveChildWindowHandle;//这个函数得到活动子窗体
    //如果有子窗口而且不存在模态显示的窗体则把焦点移到子窗体上
    if (hWindow > 0) and IsWindowEnabled(Application.Handle) then
    windows.SetForegroundWindow(hWindow);
  end;
end;

 

第四个问题:SpeedButton在DLL中鼠标离开不会恢复平面(ShowModal时不会出现)(未解决)
SpeedButton.Flat设为真时,在DLL中鼠标离开不会恢复平面状态,而ShowModal时不会出现,不知道原因,应该是消息处理得不好,不知道有没有人解决过
 
又一个焦点问题:焦点在DLL窗体时,按Alt+Tab,对话框里出来的程序中竟然没有EXE程序!
焦点在EXE窗体上时没问题,焦点在DLL窗体上时,用Alt+Tab不会出现EXE应用程序的图标,切换到其它任务后,也不能用Alt+Tab切换回来!这是个比较大的BUG,还未找到原因
用spy++看了一下,按下Alt+Tab键,窗体收到了一个WM_CANCELMODE消息,我想,既然焦点在exe窗体上时可以看到图标,而在dll上看不到,那么我在收到这个消息时把焦点给设到exe上不就可以了?
  事实证明这点是可行的,代码如下:
TDllForm = class(TForm)
private
  procedure WMCancelMode(var Message : TMessage);message WM_CANCELMODE;
end;

procedure TDllForm.WMCancelMode(var Message: TMessage);
//处理Alt+Tab键弹出的对话框中没有应用程序图标问题
begin
  SetForegroundWindow(exeForm.Handle); //把exe窗体设为当前有焦点的窗体
end;

  现在不论焦点在exe的窗体上还是dll的窗体上,按Alt+Tab出现的对话框中都有应用程序的图标,但不同的是,焦点在exe的窗体上时按Alt+Tab,默认激活的是下一个应用程序,而焦点在dll窗体上时按Alt+Tab,默认激活的是第一个,也就是应用程序本身,实际激活的是exe窗体.

  虽然还是不怎么习惯,但总算把它给弄出来了,以后有好的解决方法再贴上来.
 
Hint的问题(未解决)
焦点在Dll中的窗体时,鼠标移动到控件上不会显示控件的Hint,而且Application.OnHint事件也不会发生,但是焦点在Exe窗体上时,把鼠标放在Dll窗体中的控件上却能显示Hint.原因还未找到:(
 
ALT+TAB解决了,但是那是键盘,鼠标操作还是有问题
焦点在DLL中时,用鼠标点其它应用程序,失去焦点了,再按ALT+TAB,那个该死的应用程序图标又没了,焦虑中....