几何尺寸与公差论坛

 找回密码
 注册
查看: 3311|回复: 0

【转帖】dll学习(资料收集)

[复制链接]
发表于 2008-5-17 21:24:59 | 显示全部楼层 |阅读模式
DLL学习(资料收集) 首先需要一个声明
type
  TDll = procedure(ID:string;AHandle:THandle;ADOConnection:TADOConnection);stdcall;
//该定义的作用相当于C++中的函数指针声明

通过某个事件来调用
procedure TForm1.Button1Click(Sender: TObject);
var
  Dll:TDll;
  DllName:THandle;
begin
  DllName:=LoadLibrary('..\Dll\Test_Dll.dll');// Dll的路径
  try
    if DllName<32 then
    begin
      messagebox(handle,'没有找到附带DLL文件,请确认程序是否完整!','加载DLL失败',MB_OK+MB_ICONEXCLAMATION);
      exit;
    end;
    @Dll:=GetProcAddress(DllName,'CreateTest_DLLForm');
    if @dll <> nil then
    try
      Dll('000001',Application.Handle,form1.ADOConnection1);
    except
      raise Exception.Create('不存在!');
    end;
  finally
    FreeLibrary(DllName);//注意释放
  end;
end;

在被调用的Dll里面写个函数,Dll被调用的时候用(有点绕口,呵呵)
当然,首先要声明了
procedure CreateTest_DllForm(ID:string;AHandle:THandle;ADOConnection:TADOConnection);stdcall;

函数具体内容
procedure CreateTest_DllForm(ID:string;AHandle:THandle;ADOConnection:TADOConnection);
var
  OldHandle:THandle;
begin
  Oldhandle:=Application.Handle;
  Application.Handle:=AHandle;
  MainFrm:=TMainFrm.Create(nil);
  conn:=ADOConnection;   // conn为已定义的全局变量
  Try
    MainFrm.ShowModal;
  finally
    Application.Handle:=OldHandle;
    MainFrm.Free;
    MainFrm:=nil;
  end;
end;

最后不要忘了在ViewSource里写上
exports  CreateTest_DllForm;

以上是动态调用,很简单的。

一 Dll的制作一般步骤
  二 参数传递
  三 DLL的初始化和退出清理[如果需要初始化和退出清理]
  四 全局变量的使用
  五 调用静态载入
  六 调用动态载入
  七 在DLL建立一个TForM
  八 在DLL中建立一个TMDIChildForM
  九 示例:
  十 Delphi制作的Dll与其他语言的混合编程中常遇问题:
  十一 相关资料

  一 Dll的制作一般分为以下几步:

  1 .在一个DLL工程里写一个过程或函数
  2 .写一个Exports关键字,在其下写过程的名称。不用写参数和调用后缀。

  二 参数传递

  1 .参数类型最好与window C++的参数类型一致。不要用DELPHI的数据类型。
  2 .最好有返回值[即使是一个过程],来报出调用成功或失败,或状态。成功或失败的返回值最好为1[成功]或0[失败].一句话,与windows c++兼容。
  3 .用stdcall声明后缀。
  4 .最好大小写敏感。
  5 .无须用far调用后缀,那只是为了与windows 16位程序兼容。

  三 DLL的初始化和退出清理[如果需要初始化和退出清理]

  1 .DLLProc[SysUtils单元的一个Pointer]是DLL的入口。在此你可用你的函数替换了它的入口。但你的函数必须符合以下要求[其实就是一个回调函数]。如下:

  procedure DllEnterPoint(dwReason: DWORD);far;stdcall;


  dwReason参数有四种类型:
  DLL_PROCESS_ATTACH:进程进入时
  DLL_PROCESS_DETACH进程退出时
  DLL_THREAD_ATTACH 线程进入时
  DLL_THREAD_DETACH 线程退出时
  在初始化部分写:

  DLLProc := @DLLEnterPoint;
  DllEnterPoint(DLL_PROCESS_ATTACH);


  2 .如Form上有TdcomConnection组件,就Uses Activex,在初始化时写一句CoInitialize (nil);
  3 .在退出时一定保证DcomConnection.Connected := False,并且数据集已关闭。否则报地址错。

  四 全局变量的使用

  在widnows 32位程序中,两个应用程序的地址空间是相互没有联系的。虽然DLL在内存中是一份,但变量是在各进程的地址空间中,因此你不能借助dll的全局变量来达到两个应用程序间的数据传递,除非你用内存映像文件。

  五 调用静态载入

  1 客户端函数声名:

  1)大小写敏感。

2)与DLL中的声明一样。
  如: showform(form:Tform);Far;external'yproject_dll.dll';
  3)调用时传过去的参数类型最好也与windows c++一样。
  4)调用时DLL必须在windows搜索路径中,顺序是:当前目录;Path路径;windows;widows\system;windows\ssystem32;

  六 调用动态载入

  1 .建立一种过程类型[如果你对过程类型的变量只是一个指针的本质清楚的话,你就知道是怎么回事了]。如:

  type
  mypointer=procedure(form:Tform);Far;external;
  var
  Hinst:Thandle;
  showform:mypointer;
  begin
  Hinst:=loadlibrary('yproject_dll');//Load一个Dll,按文件名找。
  showform:=getprocaddress(Hinst,'showform');//按函数名找,大小写敏感。如果你知道自动化对象的本质就清楚了。
  showform(application.mainform);//找到函数入口指针就调用。
  Freelibrary(Hinst);
  end;

  七 .在DLL建立一个TForM

  1 把你的Form Uses到Dll中,你的Form用到的关联的单元也要Uses进来[这是最麻烦的一点,因为你的Form或许Uses了许多特殊的单元或函数]
  2 传递一个Application参数,用它建立Form.

  八 .在DLL中建立一个TMDIChildForM

  1 Dll中的MDIForm.FormStyle不用为fmMDIChild.
  2 在CreateForm后写以下两句:

  function ShowForm(mainForm:TForm):integer;stdcall
  var
  Form1: TForm1;
  ptrLongInt;
  begin
  ptr:=@(Application.MainForm);//先把dll的MainForm句柄保存起来,也无须释放,只不过是替换一下
  ptr^:=LongInt(mainForm);//用主调程序的mainForm替换DLL的MainForm。MainForm是特殊的WINDOW,它专门管理Application中的Forms资源.
  //为什么不直接Application.MainForm := mainForm,因为Application.MainForm是只读属性
  Form1:=TForm1.Create(mainForm);//用参数建立
  end;


  备注:参数是主调程序的Application.MainForm

九 .示例:

  DLL源代码:

  library Project2;

  uses
  SysUtils,
  Classes,
  Dialogs,
  Forms,
  Unit2 in 'Unit2.pas' {Form2};

  {$R *.RES}
  var
  ccc: Pchar;

  procedure OpenForm(mainForm:TForm);stdcall;
  var
  Form1: TForm1;
  ptrLongInt;
  begin
  ptr:=@(Application.MainForm);
  ptr^:=LongInt(mainForm);
  Form1:=TForm1.Create(mainForm);
  end;

  procedure InputCCC(Text: Pchar);stdcall;
  begin
  ccc := Text;
  end;

  procedure ShowCCC;stdcall;
  begin
  ShowMessage(String(ccc));
  end;

  exports
  OpenForm;
  InputCCC,
  ShowCCC;
  begin
  end.

  调用方源代码:


  unit Unit1;

  interface

  uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TForm1 = class(TForm)
  Button1: TButton;
  Button2: TButton;
  Edit1: TEdit;
  procedure Button1Click(Sender: TObject);
  procedure Button2Click(Sender: TObject);
  private
  { Private declarations }
  public
  { Public declarations }
  end;

  var
  Form1: TForm1;

  implementation

  {$R *.DFM}
  procedure OpenForm(mainForm:TForm);stdcall;External'project2.dll';
  procedure ShowCCC;stdcall;External'project2.dll';
  procedure InputCCC(Text: Pchar);stdcall;External'project2.dll';

  procedure TForm1.Button1Click(Sender: TObject);
  var
  Text: Pchar;
  begin
  Text := Pchar(Edit1.Text);
  // OpenForm(Application.MainForm);//为了调MDICHILD
  InputCCC(Text);//为了实验DLL中的全局变量是否在各个应用程序间共享
  end;

  procedure TForm1.Button2Click(Sender: TObject);
  begin
  ShowCCC;//这里表明WINDOWS 32位应用程序DLL中的全局变量也是在应用程序地址空间中,16位应用程序或许不同,没有做实验。
  end;



  十 Delphi制作的Dll与其他语言的混合编程中常遇问题:

  1 .与PowerBuilder混合编程

  在定义不定长动态数组方面在函数退出清理堆栈时老出现不可重现的地址错,原因未明,大概与PB的编译器原理有关,即使PB编译成二进制代码也如此。

  


来自:zhousy_2000, 时间:2005-9-14 8:20:59, ID:3204433
tiDLL参数调用约定
----------------------
令      传递顺序        参数删除
stdcall     从左到右        函数方面
cdecl       从右到左        调用方面
pascal      从左到右        函数方面
register    从左到右        函数方面
━━━━━━━━━━━━━━━━━━━━━━
退出过程编译时必须关闭stack_checking,因而需设置编译指示 {$S-} 。 
━━━━━━━━━━━━━━━━━━━━━
//----------dll的创建
library mydll
{$S-}
//--------uses单元
uses
  classes,stdsys, form in 'form.pas'{form};
//---------变量声明
var
  love:string;
  baby:integer;
  SaveExit: Pointer; 
//---------函数和过程
procedure myinnerproc();stdcall;         //内部使用过程
begin
{添入代码}
end;
procedure myproc(var love:string);stdcall;export; //输出可以调用过程
begin
{添入代码}
end;
function  myfunction(baby:integer):integer;stdcall;export;//可调用函数
begin
{添入代码}
end;
procedure LibExit; far;
begin
  if ExitCode = wep_System_Exit then
    begin
     { 系统关闭时的相应处理 }
    end
  else
    begin
    { DLL卸出时的相应处理 }
    end;
  ExitProc := SaveExit; { 恢复原来的退出过程指针 }
end; 
//----------输出说明
exports
myproc name 'myproc' index 1,
myfunction name 'myfuntion' index 2 risdent;//输出信息始终保持在内存中{risdent}
//----------初始化工作
begin
{DLL的初始化工作 }
SaveExit := ExitProc; { 保存原来的退出过程指针 }
ExitProc := @LibExit; { 安装新的退出过程 }
End.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
表10.1 ExitCode的取值与意义 :取 值 意 义
---------------------
WEP_System_Exit                 Windows关闭 
WEP_Free_DLLx                   DLLs被卸出
━━━━━━━━━━━━━━━━━━━━━ 
//--------------调用dll
1。静态调用
  在静态调用一个DLLs中的过程或函数时,external指示增加到过程或函数的声明语句中。
被调用的过程或函数必须采用远调用模式。这可以使用far过程指示或一个{$F +}编译指示。
Delphi全部支持传统Windows动态链接库编程中的三种调用方式,它们是:
  ● 通过过程/函数名
  ● 通过过程/函数的别名
  ● 通过过程/函数的顺序号 
//--------------------静态调用举例
unit windows
interface
function FindWindowsEx(Parent,Child:hwnd;classname,windowsname:pchar):hwnd;stdcall;
const
user32='user32.dll'
implementation
function FindWindowEx; external user32 name 'FindWindowExA'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2. 动态调用 
2.1 动态调用中的API函数 
  动态调用中使用的Windows API函数主要有三个,即:Loadlibrary,GetProcAddress和Freelibrary。
  1.Loadlibrary: 把指定库模块装入内存
  语法为:  function Loadlibrary(LibFileName: PChar): THandle; 
    LibFileName指定了要装载DLLs的文件名,如果LibFileName没有包含一个路径,则Windows按下述顺序进行查找:
 (1)当前目录;
 (2)Windows目录(包含win.com的目录)。函数GetWindowDirectory返回这一目录的路径;
 (3)Windows系统目录(包含系统文件如gdi.exe的目录)。函数GetSystemDirectory返回这一目录的路径;
 (4)包含当前任务可执行文件的目录。利用函数GetModuleFileName可以返回这一目录的路径;
 (5)列在PATH环境变量中的目录;
 (6)网络的映象目录列表。
  如果函数执行成功,则返回装载库模块的实例句柄。否则,返回一个小于HINSTANCE_ERROR的错误代码。错误代码的意义如下表: 
表10.2 Loadlibrary返回错误代码的意义
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
错误代码         意        义
--------------------------------------
0      系统内存不够,可执行文件被破坏或调用非法
2      文件没有被发现
3      路径没有被发现
5      企图动态链接一个任务或者有一个共享或网络保护错
6      库需要为每个任务建立分离的数据段
8      没有足够的内存启动应用程序
10     Windows版本不正确
11     可执行文件非法。或者不是Windows应用程序,或者在.EXE映像中有错误
12     应用程序为一个不同的操作系统设计(如OS/2程序)
13     应用程序为MS DOS4.0设计
14     可执行文件的类型不知道
15     试图装载一个实模式应用程序(为早期Windows版本设计)
16     试图装载包含可写的多个数据段的可执行文件的第二个实例
19     试图装载一个压缩的可执行文件。文件必须被解压后才能被装裁
20     动态链接库文件非法
21     应用程序需要32位扩展
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
假如在应用程序用Loadlibrary调用某一模块前,其它应用程序已把该模块装入内存,则Loadlibrary并不会装载该模块的另一实例,而是使该模块的"引用计数"加1。
2.GetProcAddress:捡取给定模块中函数的地址
 语法为:  function GetProcAddress(Module: THandle; ProcName: PChar): TFarProc; 
  Module包含被调用的函数库模块的句柄,这个值由Loadlibrary返回。如果把Module设置为nil,则表示要引用当前模块。
 ProcName是指向含有函数名的以nil结尾的字符串的指针,或者也可以是函数的次序值。如果ProcName参数是次序值,则如果该次序值的函数在模块中并不存在时,GetProcAddress仍返回一个非nil的值。这将引起混乱。因此大部分情况下用函数名是一种更好的选择。如果用函数名,则函数名的拼写必须与动态链接库文件EXPORTS节中的对应拼写相一致。
如果GetProcAddress执行成功,则返回模块中函数入口处的地址,否则返回nil。
3.Freelibrary:从内存中移出库模块
  语法为: procedure Freelibrary(Module : THandle); 
    Module为库模块的句柄。这个值由Loadlibrary返回。
  由于库模块在内存中只装载一次,因而调用Freelibrary首先使库模块的引用计数减一。如果引用计数减为0,则卸出该模块。
 每调用一次Loadlibrary就应调用一次FreeLibray,以保证不会有多余的库模块在应用程序结束后仍留在内存中。 
//------------ 动态调用举例 
 在利用GetProcAddess返回的函数指针时,必须进行强制类型转换: 
   Order := TInstr(PFunc)(text,Key);
 TInStr是一个定义好了的函数类型: 
type
  TInStr = function(Source: PChar;Check: Char): Integer;
//---------------------------------
procedure TForm1.Edit2KeyPress(Sender: TObject; var Key: Char);
var
order: Integer;
txt: PChar;
PFunc: TFarProc;
Moudle: THandle;
begin
  Moudle := Loadlibrary('c:\dlls\example.dll');
  if Moudle > 32 then
     begin
           Edit2.text := '';
           Pfunc := GetProcAddress(Moudle,'Instr');
    txt := StrAlloc(80);
    txt := StrPCopy(txt,Edit1.text);
    Order := TInstr(PFunc)(txt,Key);
    if Order = -1 then
    Label1.Caption := '不包含这个字符 '
  end else
        Label1.Caption := '位于第'+IntToStr(Order+1)+'位';
  Freelibrary(Moudle);
end;
//------------ 用于实现数据传输的DLLs的编写 
用于实现数据传输的DLLs与一般DLLs的编写基本相同,其中特别的地方是:
1. 定义一个全局变量句柄: 
var
   hMem: THandle;
2. 定义一个过程,返回该全局变量的句柄。该过程要包含在exports子句中。如: 
function GetGlobalMem: THandle; export;
begin
    Result := hMem;
end;
3. 在初始化代码中分配全局内存块:
程序清单如下: 
begin
  hMem := GlobalAlloc(gmem_MOVEABLE and gmem_DDEShare,num);
  if hMem = 0 then
  MessageDlg('Could not allocate memory',mtWarning,[mbOK],0);
end.
//--------------------------------
 num是一个预定义的常数。
表10.3 全局内存块的分配标志 :标 志 意 义
---------------------------------
gmem_DDEShare 分配可由应用程序共享的内存
gmem_Discardable 分配可抛弃的内存(只与gmem_Moveable连用)
gmem_Fixed 分配固定内存
gmem_Moveable 分配可移动的内存
gmem_Nocompact 该全局堆中的内存不能被压缩或抛弃
gmem_Nodiscard 该全局堆中的内存不能被抛弃
gmem_NOT_Banked 分配不能被分段的内存
gmem_Notify 通知功能。当该内存被抛弃时调用GlobalNotify函数
gmem_Zeroinit 将所分配内存块的内容初始化为零
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
有两个预定义的常用组合是:
GHND = gmem_Moveable and gmem_Zeroinit
GPTK = gmem_Fixed and gmem_Zeroinit

  


来自:zsy146, 时间:2005-9-14 8:22:41, ID:3204435 | 编辑
一、开使你的第一个DLL专案
  1.File->Close all->File->New﹝DLL﹞
  代码:
  //自动产生Code如下
  library Project2;
  //这有段废话
  uses
  SysUtils,
  Classes;

  {$R *.RES}

  begin
  end.


  2.加个Func进来:
  代码:
  library Project2;
  uses
  SysUtils,
  Classes;

Function MyMax ( X , Y : integer ) : integer ; stdcall ;
begin
if X > Y then
Result := X
else
Result := Y ;
end ;
//切记:Library 的名字大小写没关系,可是DLL-Func的大小写就有关系了。
// 在 DLL-Func-Name写成MyMax与myMAX是不同的。如果写错了,立即
// 的结果是你叫用到此DLL的AP根本开不起来。
//参数的大小写就没关系了。甚至不必同名。如原型中是 (X,Y:integer)但引
// 用时写成(A,B:integer),那是没关系的。
//切记:要再加个stdcall。书上讲,如果你是用Delphi写DLL,且希望不仅给
// Delphi-AP也希望BCB/VC-AP等使用的话,那你最好加个Stdcall ; 的指示
//参数型态:Delphi有很多种它自己的变量型态,这些当然不是DLL所喜欢的
// ,Windows/DLL的母语应该是C。所以如果要传进传出DLL的参数,我们
// 尽可能照规矩来用。这两者写起来,后者会麻烦不少。如果你对C不熟
// 的话,那也没关系。我们以后再讲。

  {$R *.RES}

  begin
  end.


  3.将这些可共享的Func送出DLL,让外界﹝就是你的Delphi-AP啦﹞使用:光如此,你的AP还不能用到这些,你还要加个Exports才行。
  代码:
  {$R *.RES}
  exports
  MyMax ;
  begin
  end.


  4.好了,可以按 Ctrl-F9编译了。此时可不要按F9。DLL不是EXE┌不可单独执行的,如果你按F9,会有ErrorMsg的。这时如果DLL有Error,请修正之。再按Ctrl-F9。此时可能有Warning,不要紧,研究一下,看看就好。再按Ctrl-F9,此时就『Done , Compiled 』。同目录就会有个 *.dll 。恭喜,大功告成了。

二、进行测试:开个新application:
  1.加个TButton
  代码:
  ShowMessage ( IntToStr(MyMax(30,50)) ) ;


  2.告知Exe到那里抓个Func
  代码:
  //在Form,interface,var后加
  Function MyMax ( X , Y : integer ) : integer ; stdcall ; external 'MyTestDLL.dll' ;
  // MyTestDLL.dll为你前时写的DLL项目名字
  // DLL名字大小写没关系。不过记得要加 extension的 .DLL。在Win95或NT,
  // 是不必加 extension,但这两种OS,可能越来越少了吧。要加extension

  可以了,简单吧。


  上面的例子是不是很简单?熟悉Delphi的朋友可以看出以上代码和一般的Delphi程序的编写基本是相同的,只是在TestDll函数后多了一个stdcall参数并且用exports语句声明了TestDll函数。只要编译上面的代码,就可以玫揭桓雒狣elphi.dll的动态链接库。现在,让我们来看看有哪些需要注意的地方:

  1.在DLL中编写的函数或过程都必须加上stdcall调用参数。在Delphi 1或Delphi 2环境下该调用参数是far。从Delphi 3以后将这个参数变为了stdcall,目的是为了使用标准的Win32参数传递技术来代替优化的register参数。忘记使用stdcall参数是常见的错误,这个错误不会影响DLL的编译和生成,但当调用这个DLL时会发生很严重的错误,导致操作系统的死锁。原因是register参数是Delphi的默认参数。

  2.所写的函数和过程应该用exports语句声明为外部函数。
  正如大家看到的,TestDll函数被声明为一个外部函数。这样做可以使该函数在外部就能看到,具体方法是单激鼠标右键用"快速查看(Quick View)"功能查看该DLL文件。(如果没有"快速查看"选项可以从Windows CD上安装。)TestDll函数会出现在Export Table栏中。另一个很充分的理由是,如果不这样声明,我们编写的函数将不能被调用,这是大家都不愿看到的。

  3.当使用了长字符串类型的参数、变量时要引用ShareMem。
  Delphi中的string类型很强大,我们知道普通的字符串长度最大为256个字符,但Delphi中string类型在默认情况下长度可以达到2G。(对,您没有看错,确实是两兆。)这时,如果您坚持要使用string类型的参数、变量甚至是记录信息时,就要引用ShareMem单元,而且必须是第一个引用的。既在uses语句后是第一个引用的单元。如下例:
  uses
  ShareMem,
  SysUtils,
  Classes;

  还有一点,在您的工程文件(*.dpr)中而不是单元文件(*.pas)中也要做同样的工作,这一点Delphi自带的帮助文件没有说清楚,造成了很多误会。不这样做的话,您很有可能付出死机的代价。避免使用string类型的方法是将string类型的参数、变量等声明为Pchar或ShortString(如:s:string[10])类型。同样的问题会出现在当您使用了动态数组时,解决的方法同上所述。

在Delphi中静态调用DLL  

  调用一个DLL比写一个DLL要容易一些。首先给大家介绍的是静态调用方法,稍后将介绍动态调用方法,并就两种方法做一个比较。同样的,我们先举一个静态调用的例子。


unit Unit1;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics,
Controls, Forms, Dialogs, StdCtrls;

type
TForm1 = class(TForm)
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}

//本行以下代码为我们真正动手写的代码

function TestDll(i:integer):integer;stdcall;
external 'Delphi.dll';

procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text:=IntToStr(TestDll(1));
end;

end.

  上面的例子中我们在窗体上放置了一个编辑框(Edit)和一个按钮(Button),并且书写了很少的代码来测试我们刚刚编写的Delphi.dll。大家可以看到我们唯一做的工作是将TestDll函数的说明部分放在了implementation中,并且用external语句指定了Delphi.dll的位置。(本例中调用程序和Delphi.dll在同一个目录中。)让人兴奋的是,我们自己编写的TestDll函数很快被Delphi认出来了。您可做这样一个实验:输入"TestDll(",很快Delphi就会用fly-by提示条提示您应该输入的参数是什么,就像我们使用Delphi中定义的其他函数一样简单。注意事项有以下一些:
一、调用参数用stdcall
  和前面提到的一样,当引用DLL中的函数和过程时也要使用stdcall参数,原因和前面提到的一样。

二、用external语句指定被调用的DLL文件的路径和名称
  正如大家看到的,我们在external语句中指定了所要调用的DLL文件的名称。没有写路径是因为该DLL文件和调用它的主程序在同一目录下。如果该DLL文件在C:\,则我们可将上面的引用语句写为external 'C:\Delphi.dll'。注意文件的后缀.dll必须写上。

三、不能从DLL中调用全局变量
  如果我们在DLL中声明了某种全局变量,如:var s:byte 。这样在DLL中s这个全局变量是可以正常使用的,但s不能被调用程序使用,既s不能作为全局变量传递给调用程序。不过在调用程序中声明的变量可以作为参数传递给DLL。

四、被调用的DLL必须存在
  这一点很重要,使用静态调用方法时要求所调用的DLL文件以及要调用的函数或过程等等必须存在。如果不存在或指定的路径和文件名不正确的话,运行主程序时系统会提示"启动程序时出错"或"找不到*.dll文件"等运行错误。



在Delphi中动态调用DLL top

  动态调用DLL相对复杂很多,但非常灵活。为了全面的说明该问题,这次我们举一个调用由C++编写的DLL的例子。首先在C++中编译下面的DLL源程序。


#include

extern "C" _declspec(dllexport)
int WINAPI TestC(int i)
{
return i;
}


  编译后生成一个DLL文件,在这里我们称该文件为Cpp.dll,该DLL中只有一个返回整数类型的函数TestC。为了方便说明,我们仍然引用上面的调用程序,只是将原来的Button1Click过程中的语句用下面的代码替换掉了。
procedure TForm1.Button1Click(Sender: TObject);
type
TIntFunc=function(i:integer):integer;stdcall;
var
Th:Thandle;
Tf:TIntFunc;
Tp:TFarProc;
begin
Th:=LoadLibrary('Cpp.dll'); {装载DLL}
if Th>0 then
try
Tp:=GetProcAddress(Th,PChar('TestC'));
if Tp<>nil
then begin
Tf:=TIntFunc(Tp);
Edit1.Text:=IntToStr(Tf(1)); {调用TestC函数}
end
else
ShowMessage('TestC函数没有找到');
finally
FreeLibrary(Th); {释放DLL}
end
else
ShowMessage('Cpp.dll没有找到');
end;

  大家已经看到了,这种动态调用技术很复杂,但只要修改参数,如修改LoadLibrary('Cpp.dll')中的DLL名称为'Delphi.dll'就可动态更改所调用的DLL。

一、定义所要调用的函数或过程的类型
  在上面的代码中我们定义了一个TIntFunc类型,这是对应我们将要调用的函数TestC的。在其他调用情况下也要做同样的定义工作。并且也要加上stdcall调用参数。

二、释放所调用的DLL
  我们用LoadLibrary动态的调用了一个DLL,但要记住必须在使用完后手动地用FreeLibrary将该DLL释放掉,否则该DLL将一直占用内存直到您退出Windows或关机为止。

  现在我们来评价一下两种调用DLL的方法的优缺点。静态方法实现简单,易于掌握并且一般来说稍微快一点,也更加安全可靠一些;但是静态方法不能灵活地在运行时装卸所需的DLL,而是在主程序开始运行时就装载指定的DLL直到程序结束时才释放该DLL,另外只有基于编译器和链接器的系统(如Delphi)才可以使用该方法。动态方法较好地解决了静态方法中存在的不足,可以方便地访问DLL中的函数和过程,甚至一些老版本DLL中新添加的函数或过程;但动态方法难以完全掌握,使用时因为不同的函数或过程要定义很多很复杂的类型和调用方法。对于初学者,笔者建议您使用静态方法,待熟练后再使用动态调用方法。



使用DLL的实用技巧  

一、编写技巧
  1 、为了保证DLL的正确性,可先编写成普通的应用程序的一部分,调试无误后再从主程序中分离出来,编译成DLL。

  2 、为了保证DLL的通用性,应该在自己编写的DLL中杜绝出现可视化控件的名称,如:Edit1.Text中的Edit1名称;或者自定义非Windows定义的类型,如某种记录。

  3 、为便于调试,每个函数和过程应该尽可能短小精悍,并配合具体详细的注释。

  4 、应多利用try-finally来处理可能出现的错误和异常,注意这时要引用SysUtils单元。

  5 、尽可能少引用单元以减小DLL的大小,特别是不要引用可视化单元,如Dialogs单元。例如一般情况下,我们可以不引用Classes单元,这样可使编译后的DLL减小大约16Kb。

二、调用技巧
  1 、在用静态方法时,可以给被调用的函数或过程更名。在前面提到的C++编写的DLL例子中,如果去掉extern "C"语句,C++会编译出一些奇怪的函数名,原来的TestC函数会被命名为@TestC$s等等可笑的怪名字,这是由于C++采用了C++ name mangling技术。这个函数名在Delphi中是非法的,我们可以这样解决这个问题:
改写引用函数为


function TestC(i:integer):integer;stdcall;
external 'Cpp.dll';name '@TestC$s';

其中name的作用就是重命名。

  2 、可把我们编写的DLL放到Windows目录下或者Windows\system目录下。这样做可以在external语句中或LoadLibrary语句中不写路径而只写DLL的名称。但这样做有些不妥,这两个目录下有大量重要的系统DLL,如果您编的DLL与它们重名的话其后果简直不堪设想,况且您的编程技术还不至于达到将自己编写的DLL放到系统目录中的地步吧!

三、调试技巧
  1 、我们知道DLL在编写时是不能运行和单步调试的。有一个办法可以,那就是在Run|parameters菜单中设置一个宿主程序。在Local页的Host Application栏中添上宿主程序的名字就可进行单步调试、断点观察和运行了。

  2 、添加DLL的版本信息。开场白中提到了版本信息对于DLL是很重要的,如果包含了版本信息,DLL的大小会增加2Kb。增加这么一点空间是值得的。很不幸我们如果直接使用Project|options菜单中Version选项是不行的,这一点Delphi的帮助文件中没有提到,经笔者研究发现,只要加一行代码就可以了。如下例:
library Delphi;

uses
SysUtils,
Classes;

{$R *.RES}
//注意,上面这行代码必须加在这个位置

function TestDll(i:integer):integer;stdcall;
begin
Result:=i;
end;

exports
TestDll;

begin
end.


  3 、为了避免与别的DLL重名,在给自己编写的DLL起名字的时候最好采用字符数字和下划线混合的方式。如:jl_try16.dll。

  4 、如果您原来在Delphi 1或Delphi 2中已经编译了某些DLL的话,您原来编译的DLL是16位的。只要将源代码在新的Delphi 3或Delphi 4环境下重新编译,就可以得到32位的DLL了。  


2005-9-26 10:15:08   

查看评语&raquo;&raquo;&raquo;   
2005-9-26 10:16:57    DLL(Dynamic Link Libraries)专题:
    比较大的应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序EXE文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是,在编写大的EXE程序时,在每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性,也不利于阶段性的单元测试。
    Windows系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。Windows自己就将一些主要的系统功能以DLL模块的形式实现。
    一般来说,DLL是一种磁盘文件,以.DLL、.DRV、.FON、.SYS和许多以.EXE为扩展名的系统文件都可以是DLL。它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。DLL模块中包含各种导出函数,用于向外界提供服务。DLL可以有自己的数据段,但没有自己的堆栈,使用与调用它的应用程序相同的堆栈模式;一个DLL在内存中只有一个实例;DLL实现了代码封装性;DLL的编制与具体的编程语言及编译器无关。
    在Win32环境中,每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。DLL模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。Windows在加载DLL模块时将进程函数调用与DLL文件的导出函数相匹配。Windows操作系统对DLL的操作仅仅是把DLL映射到需要它的进程的虚拟地址空间里去。DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有.        

一、关于调用方式:

1、静态调用方式:由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码(如还有其它程序使用该DLL,则Windows对DLL的应用记录减1,直到所有相关程序都结束对该DLL的使用时才释放它),简单实用,但不够灵活,只能满足一般要求。

隐式的调用:需要把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须说明一下。隐式调用不需要调用LoadLibrary()和FreeLibrary()。程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。所有被应用程序调用的DLL文件都会在应用程序EXE文件加载时被加载在到内存中。可执行程序链接到一个包含DLL输出函数信息的输入库文件(.LIB文件)。操作系统在加载使用可执行程序时加载DLL。可执行程序直接通过函数名调用DLL的输出函数,调用方法和程序内部其他的函数是一样的。


2、动态调用方式:是由编程者用API函数加载和卸载DLL来达到调用DLL的目的,使用上较复杂,但能更加有效地使用内存,是编制大型应用程序时的重要方式。

显式的调用:是指在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态连接库调进来,动态连接库的文件名即是上面两个函数的参数,再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同本应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态连接库。直接调用Win32 的LoadLibary函数,并指定DLL的路径作为参数。LoadLibary返回HINSTANCE参数,应用程序在调用GetProcAddress函数时使用这一参数。GetProcAddress函数将符号名或标识号转换为DLL内部的地址。程序员可以决定DLL文件何时加载或不加载,显式链接在运行时决定加载哪个DLL文件。使用DLL的程序在使用之前必须加载(LoadLibrary)加载DLL从而得到一个DLL模块的句柄,然后调用GetProcAddress函数得到输出函数的指针,在退出之前必须卸载DLL(FreeLibrary)。

    Windows将遵循下面的搜索顺序来定位DLL:
1.包含EXE文件的目录,
2.进程的当前工作目录,
3.Windows系统目录,
4.Windows目录,
5.列在Path环境变量中的一系列目录。

二、MFC中的dll:

a、Non-MFC DLL:指的是不用MFC的类库结构,直接用C语言写的DLL,其输出的函数一般用的是标准C接口,并能被非MFC或MFC编写的应用程序所调用。

b、Regular DLL:和下述的Extension Dlls一样,是用MFC类库编写的。明显的特点是在源文件里有一个继承CWinApp的类。其又可细分成静态连接到MFC和动态连接到MFC上的。

静态连接到MFC的动态连接库只被VC的专业般和企业版所支持。该类DLL应用程序里头的输出函数可以被任意Win32程序使用,包括使用MFC的应用程序。输入函数有如下形式:
extern "C" EXPORT YourExportedFunction( );
如果没有extern “C”修饰,输出函数仅仅能从C++代码中调用。
DLL应用程序从CWinApp派生,但没有消息循环。

动态链接到MFC的规则DLL应用程序里头的输出函数可以被任意Win32程序使用,包括使用MFC的应用程序。但是,所有从DLL输出的函数应该以如下语句开始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ))
此语句用来正确地切换MFC模块状态。

Regular DLL能够被所有支持DLL技术的语言所编写的应用程序所调用。在这种动态连接库中,它必须有一个从CWinApp继承下来的类,DllMain函数被MFC所提供,不用自己显式的写出来。

c、Extension DLL:用来实现从MFC所继承下来的类的重新利用,也就是说,用这种类型的动态连接库,可以用来输出一个从MFC所继承下来的类。它输出的函数仅可以被使用MFC且动态链接到MFC的应用程序使用。可以从MFC继承你所想要的、更适于你自己用的类,并把它提供给你的应用程序。你也可随意的给你的应用程序提供MFC或MFC继承类的对象指针。Extension DLL使用MFC的动态连接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。Extension DLLs 和Regular DLLs不一样,它没有一个从CWinApp继承而来的类的对象,所以,你必须为自己DllMain函数添加初始化代码和结束代码。

和规则DLL相比,有以下不同:

1、它没有一个从CWinApp派生的对象;
2、它必须有一个DllMain函数;
3、DllMain调用AfxInitExtensionModule函数,必须检查该函数的返回值,如果返回0,DllMmain也返回0;
4、如果它希望输出CRuntimeClass类型的对象或者资源(Resources),则需要提供一个初始化函数来创建一个CDynLinkLibrary对象。并且,有必要把初始化函数输出;
5、使用扩展DLL的MFC应用程序必须有一个从CWinApp派生的类,而且,一般在InitInstance里调用扩展DLL的初始化函数。

三、dll入口函数:

1、每一个DLL必须有一个入口点,DllMain是一个缺省的入口函数。DllMain负责初始化(Initialization)和结束(Termination)工作,每当一个新的进程或者该进程的新的线程访问DLL时,或者访问DLL的每一个进程或者线程不再使用DLL或者结束时,都会调用DllMain。但是,使用TerminateProcess或TerminateThread结束进程或者线程,不会调用DllMain。

DllMain的函数原型:
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
.......
case DLL_THREAD_ATTACH:
.......
case DLL_THREAD_DETACH:
.......
case DLL_PROCESS_DETACH:
.......
return TRUE;
}
}

参数:
hMoudle:是动态库被调用时所传递来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符);
ul_reason_for_call:是一个说明动态库被调原因的标志。当进程或线程装入或卸载动态连接库的时候,操作系统调用入口函数,并说明动态连接库被调用的原因。它所有的可能值为:
DLL_PROCESS_ATTACH: 进程被调用;
DLL_THREAD_ATTACH: 线程被调用;
DLL_PROCESS_DETACH: 进程被停止;
DLL_THREAD_DETACH: 线程被停止;
lpReserved:是一个被系统所保留的参数。

2、_DllMainCRTStartup

为了使用“C”运行库(CRT,C Run time Library)的DLL版本(多线程),一个DLL应用程序必须指定_DllMainCRTStartup为入口函数,DLL的初始化函数必须是DllMain。

_DllMainCRTStartup完成以下任务:当进程或线程捆绑(Attach)到DLL时为“C”运行时的数据(C Runtime Data)分配空间和初始化并且构造全局“C++”对象,当进程或者线程终止使用DLL(Detach)时,清理C Runtime Data并且销毁全局“C++”对象。它还调用DllMain和RawDllMain函数。

RawDllMain在DLL应用程序动态链接到MFC DLL时被需要,但它是静态的链接到DLL应用程序的。在讲述状态管理时解释其原因。

四、关于约定:

动态库输出函数的约定有两种:调用约定和名字修饰约定。

1)调用约定(Calling convention):决定函数参数传送时入栈和出栈的顺序,由调用者还是被调用者把参数弹出栈,以及编译器用来识别函数名字的修饰约定。

函数调用约定有多种,这里简单说一下:

   1、__stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。在32位的VC++5.0中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall。除了__pascal外,__fortran和__syscall也不被支持),取而代之的是__stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。

    _stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

    2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。

    _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。

    3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。

    _fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。   

    4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

    5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。

    关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。

    要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。

2)名字修饰约定

1、修饰名(Decoration name)

“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。

修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。

2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。

    a、C编译时函数名修饰约定规则:

__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。

__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
   
__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。

    它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。

    b、C++编译时函数名修饰约定规则:

__stdcall调用约定:
          1、以“?”标识函数名的开始,后跟函数名;
          2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
          3、参数表以代号表示:
             X--void ,
             D--char,
             E--unsigned char,
             F--short,
             H--int,
             I--unsigned int,
             J--long,
             K--unsigned long,
             M--float,
             N--double,
             _N--bool,
             ....
             PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
          4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
          5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。

    其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
          int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
          void Test2()                       -----“?Test2@@YGXXZ”

__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。

__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。

    VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.
   
五、关于DLL的函数:

    动态链接库中定义有两种函数:导出函数(export function)和内部函数(internal function)。导出函数可以被其它模块调用,内部函数在定义它们的DLL程序内部使用。

输出函数的方法有以下几种:

1、传统的方法

在模块定义文件的EXPORT部分指定要输入的函数或者变量。语法格式如下:
entryname[=internalname] [@ordinal[NONAME]] [DATA] [PRIVATE]

其中:

entryname是输出的函数或者数据被引用的名称;

internalname同entryname;

@ordinal表示在输出表中的顺序号(index);

NONAME仅仅在按顺序号输出时被使用(不使用entryname);

DATA表示输出的是数据项,使用DLL输出数据的程序必须声明该数据项为_declspec(dllimport)。

上述各项中,只有entryname项是必须的,其他可以省略。

对于“C”函数来说,entryname可以等同于函数名;但是对“C++”函数(成员函数、非成员函数)来说,entryname是修饰名。可以从.map映像文件中得到要输出函数的修饰名,或者使用DUMPBIN /SYMBOLS得到,然后把它们写在.def文件的输出模块。DUMPBIN是VC提供的一个工具。

如果要输出一个“C++”类,则把要输出的数据和成员的修饰名都写入.def模块定义文件。

2、在命令行输出

对链接程序LINK指定/EXPORT命令行参数,输出有关函数。

3、使用MFC提供的修饰符号_declspec(dllexport)

在要输出的函数、类、数据的声明前加上_declspec(dllexport)的修饰符,表示输出。__declspec(dllexport)在C调用约定、C编译情况下可以去掉输出函数名的下划线前缀。extern "C"使得在C++中使用C编译方式成为可能。在“C++”下定义“C”函数,需要加extern “C”关键词。用extern "C"来指明该函数使用C编译方式。输出的“C”函数可以从“C”代码里调用。
   
    例如,在一个C++文件中,有如下函数:
    extern "C" {void  __declspec(dllexport) __cdecl Test(int var);}
其输出函数名为:Test

MFC提供了一些宏,就有这样的作用。

AFX_CLASS_IMPORT:__declspec(dllexport)

AFX_API_IMPORT:__declspec(dllexport)

AFX_DATA_IMPORT:__declspec(dllexport)

AFX_CLASS_EXPORT:__declspec(dllexport)

AFX_API_EXPORT:__declspec(dllexport)

AFX_DATA_EXPORT:__declspec(dllexport)

AFX_EXT_CLASS: #ifdef _AFXEXT
    AFX_CLASS_EXPORT
    #else
    AFX_CLASS_IMPORT

AFX_EXT_API:#ifdef _AFXEXT
    AFX_API_EXPORT
    #else
    AFX_API_IMPORT

AFX_EXT_DATA:#ifdef _AFXEXT
     AFX_DATA_EXPORT
     #else
     AFX_DATA_IMPORT

像AFX_EXT_CLASS这样的宏,如果用于DLL应用程序的实现中,则表示输出(因为_AFX_EXT被定义,通常是在编译器的标识参数中指定该选项/D_AFX_EXT);如果用于使用DLL的应用程序中,则表示输入(_AFX_EXT没有定义)。

要输出整个的类,对类使用_declspec(_dllexpot);要输出类的成员函数,则对该函数使用_declspec(_dllexport)。如:

class AFX_EXT_CLASS CTextDoc : public CDocument
{

}

extern "C" AFX_EXT_API void WINAPI InitMYDLL();

这几种方法中,最好采用第三种,方便好用;其次是第一种,如果按顺序号输出,调用效率会高些;最次是第二种。

六、模块定义文件(.DEF)

模块定义文件(.DEF)是一个或多个用于描述DLL属性的模块语句组成的文本文件,每个DEF文件至少必须包含以下模块定义语句:

* 第一个语句必须是LIBRARY语句,指出DLL的名字;
* EXPORTS语句列出被导出函数的名字;将要输出的函数修饰名罗列在EXPORTS之下,这个名字必须与定义函数的名字完全一致,如此就得到一个没有任何修饰的函数名了。
* 可以使用DESCRIPTION语句描述DLL的用途(此句可选);
* ";"对一行进行注释(可选)。

七、DLL程序和调用其输出函数的程序的关系

1、dll与进程、线程之间的关系

DLL模块被映射到调用它的进程的虚拟地址空间。
DLL使用的内存从调用进程的虚拟地址空间分配,只能被该进程的线程所访问。
DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。
DLL使用调用进程的栈。

2、关于共享数据段

DLL定义的全局变量可以被调用进程访问;DLL可以访问调用进程的全局数据。使用同一DLL的每一个进程都有自己的DLL全局变量实例。如果多个线程并发访问同一变量,则需要使用同步机制;对一个DLL的变量,如果希望每个使用DLL的线程都有自己的值,则应该使用线程局部存储(TLS,Thread Local Strorage)。

    在程序里加入预编译指令,或在开发环境的项目设置里也可以达到设置数据段属性的目的.必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。

                                     rivershan原创于2002年9月18日

2005-1-28 22:59:02   


查看评语&raquo;&raquo;&raquo;   


2005-1-28 23:10:29    关于VC中的DLL的编程

来自www.csdn.net  作者aocanghai

      在我们实际用软件时,经常可看到许多动态连接库。动态连接库有其自身的优点如节省内存、支持多语种等功能,而且,当DLL中的函数改变后,只要不是参数的改变调用起的函数并不需要重新编译。这在编程时十分有用。至于其他妙处,各位在电脑杂志、书籍中都能看到,我这里再说就是废话了.这次小弟我所要讲的是如何在VC5.0中如何做自己的Win32 DLLs,各位要做自己的动态连接库,首先要知道DLL在VC5.0中都有哪几种分类。VC支持三种DLL,它们是:

1.Non-MFC Dlls
2.Regular Dlls
3.Extension Dlls Note

翻译措辞不当,故遇到术语是引用原词

      Non-MFC DLL:指的是不用MFC的类库结构,直接用C语言写的DLL,其输出的函数一般用的是标准C接口,并能被非MFC或MFC编写的应用程序所调用。

      Regular DLL:和下述的Extension Dlls一样,是用MFC类库编写的。明显的特点是在源文件里有一个继承CWinApp的类。其又可细分成静态连接到MFC和动态连接到MFC上的。但静态连接到MFC的动态连接库只被VC的专业般和企业版所支持。

      Extension DLL:用来实现从MFC所继承下来的类的重新利用,也就是说,用这种类型的动态连接库,可以用来输出一个从MFC所继承下来的类。Extension DLL使用MFC的动态连接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。

各位看到这里如果眼有点花或头有点晕,请别泄气,再看两遍,然后继续往下看,定有收获。

[1]

这一节介绍Non-MFC DLLs的编写方法。下面是一个通用的写法:

BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
      switch( ul_reason_for_call )
      {
            case DLL_PROCESS_ATTACH:
            .......
            case DLL_THREAD_ATTACH:
            .......
            case DLL_THREAD_DETACH:
            .......
            case DLL_PROCESS_DETACH:
            .......
      }
      return TRUE;
}

      每一个DLL必须有一个入口点,这就象我们用C编写的应用程序一样,必须有一个WINMAIN函数一样。
在这个示例中,DllMain是一个缺省的入口函数,你不需要编写自己的DLL入口函数,并用linker的命令行的参数开关/ENTRY声明。用这个缺省的入口函数就能使动态连接库被调用时得到正确的初始化,当然了,你
不要在初始化的时候填写使系统崩溃的代码了。

      参数中,hMoudle是动态库被调用时所传递来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符),ul_reason_for_call是一个说明动态库被调原因的标志。当进程或线程装入或卸载动态连接库的时候,操作系统调用入口函数,并说明动态连接库被调用的原因。它所有的可能值为:

DLL_PROCESS_ATTACH: 进程被调用
DLL_THREAD_ATTACH: 线程被调用
DLL_PROCESS_DETACH: 进程被停止
DLL_THREAD_DETACH: 线程被停止

lpReserved是一个被系统所保留的参数。

      入口函数已经写了,盛下的也不难,你可以在文件中加入你所想要输出的函数或变量或c++类或、或、或、?好象差部多了。Look here!现在就要加入一个新的输出函数了:

void _declspec(dllexport) JustSoSo()
{
      MessageBox(NULL,"It's so easy!","Hahaha......",MB_OK);
}

要输出一个类也可以,如下:
class _declspec(dllexport) Easy
{
      //add your class definition...
};

      各位一定注意到在输出函数或类是我用到_declspec(dllexport),这是VC提供的一个关键字,用它可在动态连接库中输出一个数据、一个函数或一个类。用这个关键字可省你不少事,你不用在.DEF文件中说明我要输出这个类、那个函数的。

Ok!各位照着上面的例子试着敲敲看,Just so easy!
先说到这了


[2]

      前面讲到Non-MFC DLL的编法,现在讲讲调用DLL的方法。对DLL的调用分为两种,一种是显式的调用,一种是隐式的调用。

      所谓显式的调用,是指在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态连接库调近来,动态连接库的文件名即是上两函数的参数,再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同本应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxLoadLibrary释放动态连接库。

      隐式的调用则需要把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须说明以下,如下:说明上篇的输出函数void JustSoSo();隐式调用不需要调用LoadLibrary()和FreeLibrary().

      由此看来,隐式说明调用的方法比较简单,但DLL改变后,应用程序须从新编译。并且,所有所调用的DLL在应用程序加载的同时被加载到内存中,但应用程序调用的DLL比较多时,装入的过程十分慢。隐式的调用则在应用程序不知道所要装入的DLL或隐式调用不成功,此时,允许用户指定所要加载的动态连接库,比较灵活.


[3]

      Regular DLL能够被所有支持DLL技术的语言所编写的应用程序所调用。在这种动态连接库中,它必须有一个从CWinApp继承下来的类,DllMain函数被MFC所提供,不用自己显式的写出来。下面是一个例子:

// MyRegularDll.h:main header file for the MYREGULARDLL DLL
#include "resource.h" // main symbols

class CMyRegularDllApp : public CWinApp
{
public:
      CMyRegularDllApp();
      // Overrides

      // ClassWizard generated virtual function overrides
      //{{AFX_VIRTUAL(CMyRegularDllApp)
      //}}AFX_VIRTUAL

      //{{AFX_MSG(CMyRegularDllApp)
            // NOTE - the ClassWizard will add and
            // remove member functions here.
            // DO NOT EDIT what you see in these blocks
            // of generated code !
      //}}AFX_MSG

      DECLARE_MESSAGE_MAP()
};

//MyRegularDll.cppefines the initialization routines for the DLL.
//

#include "stdafx.h"
#include "MyRegularDll.h"
// Note!

// If this DLL is dynamically linked against the MFC DLLs, any functions exported from this DLL which call
//into MFC must have the AFX_MANAGE_STATE macro added at the very beginning of the function.
//
// For example:
//
// extern "C" BOOL PASCAL EXPORT ExportedFunction()
// {
//       AFX_MANAGE_STATE(AfxGetStaticModuleState());
//       normal function body here
// }
//
// It is very important that this macro appear in each function, prior to any calls into MFC. This means
//that it must appear as the first statement within the function, even before any object variable
//declarations as their constructors may generate calls into the MFC DLL.

BEGIN_MESSAGE_MAP(CMyRegularDllApp, CWinApp)
      //{{AFX_MSG_MAP(CMyRegularDllApp)
            // NOTE - the ClassWizard will add
            // and remove mapping macros here.
            // DO NOT EDIT what you see in these blocks
END_MESSAGE_MAP()

////////////////////////////////////////////////////////////
// CMyRegularDllApp construction
CMyRegularDllApp::CMyRegularDllApp()
{
      // TODO: add construction code here,
      // Place all significant initialization in InitInstance
}

      以上是AppWizard产生的含有主要代码的两个文件,各位可从中看出和Non-MFC Dlls的区别。但要注意上面的AppWizard的提醒啊。


[4]

      这次要讲的是最后一种动态连接库:Extension Dlls.再次说明,Extension Dll只被用MFC类库所编写的应用程序所调用.在这种动态连接库中,你可以从MFC继承你所想要的、更适于你自己用的类,并把它提供给你的应用程序。你也可随意的给你的应用程序提供MFC或MFC继承类的对象指针。

      Extension DLLs 和Regular DLLs不一样,它没有一个从CWinApp继承而来的类的对象,所以,你必须为自己DllMain函数添加初始化代码和结束代码.如下:

#include "stdafx.h"
#include

static AFX_EXTENSION_MODULE PROJNAMEDLL = { NULL, NULL };

extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
      if (dwReason == DLL_PROCESS_ATTACH)
      {
            TRACE0("ROJNAME.DLL Initializing!\n");

            // Extension DLL one-time initialization
            AfxInitExtensionModule(PROJNAMEDLL,hInstance);

            // Insert this DLL into the resource chain
            new CDynLinkLibrary(Dll3DLL);
      }

      else if (dwReason == DLL_PROCESS_DETACH)
      {
            TRACE0("ROJNAME.DLL Terminating!\n");
      }
      return 1; // ok
}

      在上面代码中AfxInitExtensionMoudle函数捕捉此动态库模块用.在初始化的时NEW一个CDynLinkLibrary对象的目的在于:它能是Extension DLL想应用程序输出CRuntimeClass对象或资源.如果此动态连接库被显式的调用,还必须在DLL_PROCESS_DETACH选择项的执行代码上调用AfxTermEXtensonModule,这保证了当调
用进程与动态连接库分离是正确清理内存中的动态库模块。

      如果是隐式的被调用,则此步不是必须的了。



2005-1-28 23:12:48    Win32环境下dll编程原理

    比较大应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。其中可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序EXE文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是,在编写大的EXE程序时,在每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性,也不利于阶段性的单元测试。

  Windows系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。Microsoft Windows自己就将一些主要的系统功能以DLL模块的形式实现。例如IE中的一些基本功能就是由DLL文件实现的,它可以被其它应用程序调用和集成。

  一般来说,DLL是一种磁盘文件(通常带有DLL扩展名),它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。DLL模块中包含各种导出函数,用于向外界提供服务。Windows在加载DLL模块时将进程函数调用与DLL文件的导出函数相匹配。
  在Win32环境中,每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。DLL模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。
DLL现在越来越容易编写。Win32已经大大简化了其编程模式,并有许多来自AppWizard和MFC类库的支持。

一、导出和导入函数的匹配

  DLL文件中包含一个导出函数表。这些导出函数由它们的符号名和称为标识号的整数与外界联系起来。函数表中还包含了DLL中函数的地址。当应用程序加载DLL模块时时,它并不知道调用函数的实际地址,但它知道函数的符号名和标识号。动态链接过程在加载的DLL模块时动态建立一个函数调用与函数地址的对应表。如果重新编译和重建DLL文件,并不需要修改应用程序,除非你改变了导出函数的符号名和参数序列。
简单的DLL文件只为应用程序提供导出函数,比较复杂的DLL文件除了提供导出函数以外,还调用其它DLL文件中的函数。这样,一个特殊的DLL可以既有导入函数,又有导入函数。这并不是一个问题,因为动态链接过程可以处理交叉相关的情况。
  在DLL代码中,必须像下面这样明确声明导出函数:
__declspec(dllexport) int MyFunction(int n);
但也可以在模块定义(DEF)文件中列出导出函数,不过这样做常常引起更多的麻烦。在应用程序方面,要求像下面这样明确声明相应的输入函数:
__declspec(dllimport) int MyFuncition(int n);
仅有导入和导出声明并不能使应用程序内部的函数调用链接到相应的DLL文件上。应用程序的项目必须为链接程序指定所需的输入库(LIB文件)。而且应用程序事实上必须至少包含一个对DLL函数的调用。

二、与DLL模块建立链接

  应用程序导入函数与DLL文件中的导出函数进行链接有两种方式:隐式链接和显式链接。所谓的隐式链接是指在应用程序中不需指明DLL文件的实际存储路径,程序员不需关心DLL文件的实际装载。而显式链接与此相反。
  采用隐式链接方式,程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。
  显式链接方式对于集成化的开发语言(例如VB)比较适合。有了显式链接,程序员就不必再使用导入文件,而是直接调用Win32 的LoadLibary函数,并指定DLL的路径作为参数。LoadLibary返回HINSTANCE参数,应用程序在调用GetProcAddress函数时使用这一参数。GetProcAddress函数将符号名或标识号转换为DLL内部的地址。假设有一个导出如下函数的DLL文件:
extern "C" __declspec(dllexport) double SquareRoot(double d);
下面是应用程序对该导出函数的显式链接的例子:
typedef double(SQRTPROC)(double);
HINSTANCE hInstance;
SQRTPROC* pFunction;
VERIFY(hInstance=:oadLibrary("c:\\winnt\\system32\\mydll.dll"));
VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
double d=(*pFunction)(81.0);//调用该DLL函数
在隐式链接方式中,所有被应用程序调用的DLL文件都会在应用程序EXE文件加载时被加载在到内存中;但如果采用显式链接方式,程序员可以决定DLL文件何时加载或不加载。显式链接在运行时决定加载哪个DLL文件。例如,可以将一个带有字符串资源的DLL模块以英语加载,而另一个以西班牙语加载。应用程序在用户选择了合适的语种后再加载与之对应的DLL文件。

三、使用符号名链接与标识号链接

  在Win16环境中,符号名链接效率较低,所有那时标识号链接是主要的链接方式。在Win32环境中,符号名链接的效率得到了改善。Microsoft现在推荐使用符号名链接。但在MFC库中的DLL版本仍然采用的是标识号链接。一个典型的MFC程序可能会链接到数百个MFC DLL函数上。采用标识号链接的应用程序的EXE文件体相对较小,因为它不必包含导入函数的长字符串符号名。

四、编写DllMain函数

  DllMain函数是DLL模块的默认入口点。当Windows加载DLL模块时调用这一函数。系统首先调用全局对象的构造函数,然后调用全局函数DLLMain。DLLMain函数不仅在将DLL链接加载到进程时被调用,在DLL模块与进程分离时(以及其它时候)也被调用。下面是一个框架DLLMain函数的例子。
HINSTANCE g_hInstance;
extern "C" int APIENTRY DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
TRACE0("EX22A.DLL Initializing!\n");
//在这里进行初始化
}
else if(dwReason=DLL_PROCESS_DETACH)
{
TRACE0("EX22A.DLL Terminating!\n");
//在这里进行清除工作
}
return 1;//成功
}
  如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其它运行库中引入一个不做任何操作的缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。正如由dwReason参数所表明的那样。

五、模块句柄

  进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识。进程自己还有一个HINSTANCE句柄。所有这些模块句柄都只有在特定的进程内部有效,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这个两种类型可以替换使用。进程模块句柄几乎总是等于0x400000,而DLL模块的加载地址的缺省句柄是0x10000000。如果程序同时使用了几个DLL模块,每一个都会有不同的HINSTANCE值。这是因为在创建DLL文件时指定了不同的基地址,或者是因为加载程序对DLL代码进行了重定位。
模块句柄对于加载资源特别重要。Win32 的FindResource函数中带有一个HINSTANCE参数。EXE和DLL都有其自己的资源。如果应用程序需要来自于DLL的资源,就将此参数指定为DLL的模块句柄。如果需要EXE文件中包含的资源,就指定EXE的模块句柄。
但是在使用这些句柄之前存在一个问题,你怎样得到它们呢?如果需要得到EXE模块句柄,调用带有Null参数的Win32函数GetModuleHandle;如果需要DLL模块句柄,就调用以DLL文件名为参数的Win32函数GetModuleHandle。

六、应用程序怎样找到DLL文件

  如果应用程序使用LoadLibrary显式链接,那么在这个函数的参数中可以指定DLL文件的完整路径。如果不指定路径,或是进行隐式链接,Windows将遵循下面的搜索顺序来定位DLL:
1. 包含EXE文件的目录,
2. 进程的当前工作目录,
3. Windows系统目录,
4. Windows目录,
5. 列在Path环境变量中的一系列目录。
  这里有一个很容易发生错误的陷阱。如果你使用VC++进行项目开发,并且为DLL模块专门创建了一个项目,然后将生成的DLL文件拷贝到系统目录下,从应用程序中调用DLL模块。到目前为止,一切正常。接下来对DLL模块做了一些修改后重新生成了新的DLL文件,但你忘记将新的DLL文件拷贝到系统目录下。下一次当你运行应用程序时,它仍加载了老版本的DLL文件,这可要当心!

七、调试DLL程序

  Microsoft 的VC++是开发和测试DLL的有效工具,只需从DLL项目中运行调试程序即可。当你第一次这样操作时,调试程序会向你询问EXE文件的路径。此后每次在调试程序中运行DLL时,调试程序会自动加载该EXE文件。然后该EXE文件用上面的搜索序列发现DLL文件,这意味着你必须设置Path环境变量让其包含DLL文件的磁盘路径,或者也可以将DLL文件拷贝到搜索序列中的目录路径下。  


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

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

  从遇到的问题看出,DELPHI封装了太多的东西,有时候直接使用API会有意想不到的效果.
  经验LL与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分别表示标题栏和边框,问题解决.

第二个问题LL窗体的ResizeEXE主窗体改变尺寸时,窗体中的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).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
    )ongint; 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,还未找到原因

上一个Alt+Tab问题的非正规解决方法  用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,那个该死的应用程序图标又没了,焦虑中....
您需要登录后才可以回帖 登录 | 注册

本版积分规则

QQ|Archiver|小黑屋|几何尺寸与公差论坛

GMT+8, 2024-5-10 21:24 , Processed in 0.058364 second(s), 19 queries .

Powered by Discuz! X3.4 Licensed

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表