VC++动态链接库(DLL)编程深入浅出(VS2015)

本文转载于太平洋电脑网,原文由宋宝华于 2005 年发表。原文示例由 VC++ 6.0 开发,本文使用 VC++ 2015 重新编写。
本文版权仍归原作者所有。

原文链接如下:


1. 概论

先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。
  
静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
  
对动态链接库,我们还需建立如下概念:

(1)DLL 的编制与具体的编程语言及编译器无关
只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是Visual Basic、Visual C++还是Delphi。

(2)动态链接库随处可见
我们在Windows目录下的system32文件夹中会看到kernel32.dll、user32.dll和gdi32.dll,windows的大多数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面;gdi32.dll中的函数则负责图形方面的操作。
一般的程序员都用过类似MessageBox的函数,其实它就包含在user32.dll这个动态链接库中。由此可见DLL对我们来说其实并不陌生。

(3)VC动态链接库的分类
Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。
非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL 包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。

由于本文篇幅较长,内容较多,势必需要先对阅读本文的有关事项进行说明,下面以问答形式给出。

问:本文主要讲解什么内容?
答:本文详细介绍了DLL编程的方方面面,努力学完本文应可以对DLL有较全面的掌握,并能编写大多数DLL程序。

问:如何看本文?
答:本文每一个主题的讲解都附带了源代码例程,可以随文下载(每个工程都经WINRAR压缩)。所有这些例程都由笔者编写并在VC++6.0中调试通过。
当然看懂本文不是读者的最终目的,读者应亲自动手实践才能真正掌握DLL的奥妙。

问:学习本文需要什么样的基础知识?
答:如果你掌握了C,并大致掌握了C++,了解一点MFC的知识,就可以轻松地看懂本文。

2. 静态链接库

对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。
在这里插入图片描述

图1 建立一个静态链接库

如图1,在 VC++ 2015 中new一个名称为 libTest 的 Win32 静态库项目(单击此处下载本项目附件),并新建 lib.h 和 lib.cpp 两个文件,lib.h 和 lib.cpp 的源代码如下:

//lib.h
#pragma once

extern "C" int add(int x, int y);
//lib.cpp
#include "stdafx.h"
#include "lib.h"
int add(int x, int y)
{
	return x + y;
}

编译这个工程就得到了一个.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件和.lib文件提交给用户后,用户就可以直接使用其中的add函数了。

标准Turbo C2.0中的C库函数(我们用来的scanf、printf、memcpy、strcpy等)就来自这种静态库。

下面来看看怎么使用这个库,在 libTest 项目所在的解决方案内new一个 libCall 项目。libCall 项目仅包含一个 main.cpp 文件,它演示了静态链接库的调用方法,其源代码如下:

#include "stdafx.h"

#include <stdio.h>
#include "../libTest/lib.h"
#pragma comment( lib, "../Debug/libTest.lib")  //指定与静态库一起连接

int main()
{
	printf("2 + 3 = %d", add(2, 3));
    return 0;
}

静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中 #pragma comment( lib , "../Debug/libTest.lib" ) 的意思是指本文件生成的.obj文件应与 libTest.lib 一起连接。

如果不用#pragma comment指定,则可以直接在VC++中设置,如图2,在 libCall 项目名称上点击鼠标右键,在弹出菜单上选择“属性”,打开 libCall 属性页,选择 配置属性 -> 链接器 -> 输入,在 附加依赖项 中,填入库文件路径。
在这里插入图片描述

图2 在VC中设置库文件路径

这个静态链接库的例子至少让我们明白了库函数是怎么回事,它们是哪来的。我们现在有下列模糊认识了:

(1) 库不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行;
(2) 库提供一些可以给别的程序调用的东东,别的程序要调用它必须以某种方式指明它要调用之。

以上从静态链接库分析而得到的对库的懵懂概念可以直接引申到动态链接库中,动态链接库与静态链接库在编写和调用上的不同体现在库的外部接口定义及调用方式略有差异。

3. 库的调试与查看

在具体进入各类DLL的详细阐述之前,有必要对库文件的调试与查看方法进行一下介绍,因为从下一节开始我们将面对大量的例子工程。

由于库文件不能单独执行,因而在按下F5(开始debug模式执行)或CTRL+F5(运行)执行时,其弹出如图3所示的对话框。这个时候,我们可以在 DLL 项目属性的“调试 -> 命令”栏输入要调用该库的EXE文件的路径就可以对库进行调试了(如图3.1),其调试技巧与一般应用工程的调试一样。
在这里插入图片描述

图3 库的调试与“运行”

在这里插入图片描述

图3.1 DLL 调试命令

通常有比上述做法更好的调试途径,那就是将库项目和应用项目(调用库的项目)放置在同一解决方案,只对应用项目进行调试,在应用项目调用库中函数的语句处设置断点,执行后按下F11,这样就单步进入了库中的函数。第2节中的libTest和libCall工程就放在了同一解决方案,其工程结构如图4所示。
在这里插入图片描述

图4 把库项目和调用库的项目放入同一解决方案进行调试

上述调试方法对静态链接库和动态链接库而言是一致的。所以本文提供下载的所有源代码中都包含了库项目和调用库的项目,这二者都被包含在一个解决方案内,这是笔者提供这种打包下载的用意所在。

动态链接库中的导出接口可以使用Visual C++的Depends工具进行查看,让我们用Depends打开系统目录中的user32.dll,看到了吧?红圈内的就是几个版本的MessageBox了!原来它真的在这里啊,原来它就在这里啊!
在这里插入图片描述

图5 用Depends查看DLL

当然Depends工具也可以显示DLL的层次结构,若用它打开一个可执行文件则可以看出这个可执行文件调用了哪些DLL。

好,让我们正式进入动态链接库的世界,先来看看最一般的DLL,即非MFC DLL(待续…)


上节给大家介绍了静态链接库与库的调试与查看(动态链接库(DLL)编程深入浅出(一)),本节主要介绍非MFC DLL。

4. 非 MFC DLL

4.1 一个简单的 DLL

第2节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。

如图6,在VC++中new一个 Win32 DLL 项目 dllTest(单击此处下载本工程附件)。注意不要选择 MFC,因为用 MFC 建立的将是第5、6节要讲述的 MFC 动态链接库。
在这里插入图片描述

图6 建立一个非MFC DLL

在建立的工程中添加lib.h及lib.cpp文件,源代码如下:

/* 文件名:lib.h */
#pragma once

extern "C"  int __declspec(dllexport) add(int x, int y);
/* 文件名:lib.cpp */
#include "stdafx.h"
#include "lib.h"
int add(int x, int y)
{
	return x + y;
}

与第2节对静态链接库的调用相似,我们也建立一个与DLL项目处于同一解决方案的应用项目dllCall,它调用DLL中的函数add,其源代码如下:

// dllCall.cpp : 以显式方式调用DLL

#include "stdafx.h"
#include "windows.h"

typedef int(*lpAddFun)(int, int);//宏定义函数指针类型

int main()
{
	HINSTANCE hDll;   //DLL句柄
	lpAddFun addFun;  //函数指针
	hDll = LoadLibrary(L"..\\Debug\\dllTest.dll");
	if (hDll != NULL)
	{
		addFun = (lpAddFun)GetProcAddress(hDll, "add");
		if (addFun != NULL)
		{
			int result = addFun(2, 3);
			printf("%d", result);
		}
		FreeLibrary(hDll);
	}
    return 0;
}

分析上述代码,dllTest项目中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数。DLL内的函数分为两种:

(1) DLL导出函数,可供应用程序调用;
(2) DLL内部函数,只能在DLL程序使用,应用程序无法调用它们。

而应用程序对本DLL的调用和对第2节静态链接库的调用却有较大差异,下面我们来逐一分析。

首先,语句 typedef int ( * lpAddFun)(int,int) 定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpAddFun的实例 addFun

其次,在函数main中定义了一个DLL HINSTANCE句柄实例hDll,通过Win32 Api函数 LoadLibrary 动态加载了DLL模块并将DLL模块句柄赋给了hDll;

再次,在函数main中通过Win32 Api函数 GetProcAddress 得到了所加载DLL模块中函数add的地址并赋给了addFun。经由函数指针addFun进行了对DLL中add函数的调用;

最后,应用工程使用完DLL后,在函数main中通过Win32 Api函数 FreeLibrary 释放了已经加载的DLL模块。

通过这个简单的例子,我们获知DLL定义和调用的一般概念:

(1) DLL中需以某种特定的方式声明导出函数(或变量、类);
(2) 应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。

下面我们来对“特定的方式进行”阐述。

4.2 声明导出函数

DLL中导出函数的声明有两种方式:一种为4.1节例子中给出的在函数声明中加上 __declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。

下面的代码演示了怎样用.def文件将函数add声明为DLL导出函数(需在dllTest工程中添加lib.def文件):

; lib.def : 导出DLL函数

LIBRARY dllTest

EXPORTS

add @ 1

.def文件的规则为:

(1) LIBRARY语句说明.def文件相应的DLL;
(2) EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
(3) .def 文件中的注释由每个注释行开始处的分号 ( ; ) 指定,且注释不能与语句共享一行。

由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。

注意:编写 .def 文件后,必须在项目属性中明确指定为模块定义文件,才能生效。否则即使编写了 .def 文件,也无法导出函数,同时也不能生成 .lib 文件。如图 4.2 所示:
在这里插入图片描述

图 4.2 模块定义文件

4.3 DLL的调用方式

在4.1节的例子中我们看到了由“LoadLibrary-GetProcAddress-FreeLibrary”系统Api提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式称为DLL的动态调用
动态调用方式的特点是完全由编程者用 API 函数加载和卸载 DLL,程序员可以决定 DLL 文件何时加载或不加载,显式链接在运行时决定加载哪个 DLL 文件。

与动态调用方式相对应的就是静态调用方式,“有动必有静”,这来源于物质世界的对立统一。“动与静”,其对立与统一竟无数次在技术领域里得到验证,譬如静态IP与DHCP、静态路由与动态路由等。从前文我们已经知道,库也分为静态库与动态库DLL,而想不到,深入到DLL内部,其调用方式也分为静态与动态。“动与静”,无处不在。《周易》已认识到有动必有静的动静平衡观,《易.系辞》曰:“动静有常,刚柔断矣”。哲学意味着一种普遍的真理,因此,我们经常可以在枯燥的技术领域看到哲学的影子。

静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。

下面我们来看看静态调用的例子(单击此处下载本工程附件),将编译dllTest工程所生成的.lib和.dll文件拷入dllCall工程所在的路径,dllCall执行下列代码:

// dllCall.cpp : 隐式调用DLL

#include "stdafx.h"
#include "windows.h"

#pragma comment(lib,"dllTest.lib")
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息

extern "C" int __declspec(dllimport) add(int x, int y);

int main()
{
	int result = add(2, 3);
	printf("%d", result);

    return 0;
}

由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:

(1) 告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib") 就是起这个作用。
程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。

(2) 声明导入函数,extern "C" int __declspec(dllimport) add(int x,int y) 语句中的 __declspec(dllimport) 发挥这个作用。

静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。

4.4 DllMain函数

Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。在前面的例子中,DLL并没有提供DllMain函数,应用工程也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着DLL可以放弃DllMain函数。

根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。

我们来看一个DllMain函数的例子(单击此处下载本工程附件)。

#include "stdafx.h"
#include "stdio.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		printf("\nprocess attach of dll");
		break;
	case DLL_THREAD_ATTACH:
		printf("\nthread attach of dll");
		break;
	case DLL_THREAD_DETACH:
		printf("\nthread detach of dll");
		break;
	case DLL_PROCESS_DETACH:
		printf("\nprocess detach of dll");
		break;
	}
	return TRUE;
}

DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了被调用的原因。原因共有4种,即 PROCESS_ATTACHPROCESS_DETACHTHREAD_ATTACHTHREAD_DETACH,以switch语句列出。

来仔细解读一下DllMain的函数头BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )。

APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;

进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。

执行下列代码:

	hDll = LoadLibrary(L"..\\Debug\\dllTest.dll");
	if (hDll != NULL)
	{
		addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCEA(1));
		//MAKEINTRESOURCE直接使用导出文件中的序号
		if (addFun != NULL)
		{
			int result = addFun(2, 3);
			printf("\ncall add in dll:%d", result);
		}
		FreeLibrary(hDll);
	}

我们看到输出顺序为:

process attach of dll
call add in dll:5
process detach of dll

这一输出顺序验证了DllMain被调用的时机。

代码中的 GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) ) 值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):

#define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
#define MAKEINTRESOURCEW(i) ((LPWSTR)((ULONG_PTR)((WORD)(i))))
#ifdef UNICODE
#define MAKEINTRESOURCE  MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE  MAKEINTRESOURCEA
#endif // !UNICODE

4.5 __stdcall约定

如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式,而C/C++缺省的调用方式却为__cdecl。__stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern “C”),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而__cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。

Windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):

#define CALLBACK    __stdcall //这就是传说中的回调函数
#define WINAPI      __stdcall //这就是传说中的WINAPI
#define WINAPIV     __cdecl
#define APIENTRY    WINAPI    //DllMain的入口就在这里
#define APIPRIVATE  __stdcall
#define PASCAL      __stdcall

在lib.h中,应这样声明add函数:

int __stdcall add(int x, int y);

在应用工程中函数指针类型应定义为:

typedef int(__stdcall *lpAddFun)(int, int);

若在lib.h中将函数声明为__stdcall调用,而应用工程中仍使用typedef int (* lpAddFun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图7所示的对话框。
在这里插入图片描述

图7 调用约定不匹配时的运行错误

图7中的那段话实际上已经给出了错误的原因,即“This is usually a result of …”。
单击此处下载__stdcall调用例子工程源代码附件。

4.6 DLL导出变量

DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子(单击此处下载本工程附件)。

/* 文件名:lib.h */
#pragma once
extern int dllGlobalVar;
extern "C" int GetGlobalVar();
// lib.cpp : DLL中的全局变量
#include "lib.h"
int GetGlobalVar()
{
	return dllGlobalVar;
}
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"

int dllGlobalVar;
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		dllGlobalVar = 100;  //在dll被加载时,赋全局变量为100
		break;
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}
;文件名:lib.def
;在DLL中导出变量

LIBRARY "dllTest"

EXPORTS

dllGlobalVar CONSTANT
;或dllGlobalVar DATA

GetGlobalVar

从lib.h和lib.cpp中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加:

变量名 CONSTANT   //过时的方法
  或
变量名 DATA     //VC++提示的新方法

在主函数中引用DLL中定义的全局变量:

#include "stdafx.h"
#include <stdio.h>
#pragma comment(lib,"dllTest.lib")

extern int dllGlobalVar;

int main()
{
	printf("%d ", *(int*)dllGlobalVar);

	*(int *)dllGlobalVar = 1;
	//特别要注意的是这种方法导出的并不是变量本身,而是DLL中导出变量的指针,应用程序必须通过强制指针转换来使用

	printf("%d ", *(int*)dllGlobalVar);
	
    return 0;
}

特别要注意的是用 extern int dllGlobalVar 声明所导入的并不是DLL中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从 *(int*)dllGlobalVar 可以看出。因此在采用这种方式引用DLL全局变量时,千万不要进行这样的赋值操作:

dllGlobalVar = 1;

其结果是 dllGlobalVar 指针的内容发生变化,程序中以后再也引用不到DLL中的全局变量了。

在应用工程中引用DLL中全局变量的一个更好方法是:

#include "stdafx.h"
#include <stdio.h>
#pragma comment(lib,"dllTest.lib")

extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入

int main()
{
	printf("%d ", dllGlobalVar);

	dllGlobalVar = 1; //这里就可以直接使用, 无须进行强制指针转换

	printf("%d ", dllGlobalVar);

    return 0;
}

通过 _declspec(dllimport) 方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。

4.7 DLL导出类

DLL中定义的类可以在应用工程中使用。

下面的例子里,我们在DLL中定义了 point 和 circle 两个类,并在应用工程中引用了它们(单击此处下载本工程附件)。

//文件名:point.h,point类的声明
#pragma once

#ifdef DLL_FILE
class _declspec(dllexport) point //导出类point
#else
class _declspec(dllimport) point //导入类point
#endif
{
public:
	float y;
	float x;
	point();
	point(float x_coordinate, float y_coordinate);
};
//文件名:point.cpp,point类的实现
#ifndef DLL_FILE
#define DLL_FILE
#endif

#include "stdafx.h"
#include "point.h"

//类point的缺省构造函数
point::point()
{
	x = 0.0;
	y = 0.0;
}

//类point的构造函数
point::point(float x_coordinate, float y_coordinate)
{
	x = x_coordinate;
	y = y_coordinate;
}
//文件名:circle.h,circle类的声明
#pragma once
#include "point.h"

#ifdef DLL_FILE
class _declspec(dllexport) circle //导出类circle
#else
class _declspec(dllimport) circle //导入类circle
#endif
{
public:
	circle();

	void SetCentre(const point &centrePoint);
	void SetRadius(float r);
	float GetGirth();
	float GetArea();
private:
	float radius;
	point centre;
};
//文件名:circle.cpp,circle类的实现
#ifndef DLL_FILE
#define DLL_FILE
#endif

#include "stdafx.h"
#include "circle.h"

#define PI 3.1415926

//circle类的构造函数
circle::circle()
{
	centre = point(0, 0);
	radius = 0;
}
//得到圆的面积
float circle::GetArea()
{
	return PI*radius*radius;
}
//得到圆的周长
float circle::GetGirth()
{
	return 2 * PI*radius;
}
//设置圆心座标
void circle::SetCentre(const point &centrePoint)
{
	centre = centrePoint;
}
//设置圆的半径
void circle::SetRadius(float r)
{
	radius = r;
}

类的引用:

// dllCall.cpp : 调用DLL中的类
#include "stdafx.h"
#include "../dllTest/circle.h"

#pragma comment(lib,"dllTest.lib")

int main()
{
	circle c;
	point p(2.0, 2.0);
	c.SetCentre(p);
	c.SetRadius(1.0);
	printf("area:%f girth:%f", c.GetArea(), c.GetGirth());

    return 0;
}

从上述源代码可以看出,由于在DLL的类实现代码中定义了宏 DLL_FILE,故在DLL的实现中所包含的类声明实际上为:

class _declspec(dllexport) point //导出类point
{}

class _declspec(dllexport) circle //导出类circle
{}

而在应用工程中没有定义DLL_FILE,故其包含 point.h 和 circle.h 后引入的类声明为:

class _declspec(dllimport) point //导入类point
{}

class _declspec(dllimport) circle //导入类circle
{}

不错,正是通过DLL中的

class _declspec(dllexport) class_name //导出类
{}

与应用程序中的

class _declspec(dllimport) class_name //导入类
{}

匹对来完成类的导出和导入的!

我们往往通过在类的声明头文件中用一个宏来决定使其编译为 class _declspec(dllexport) class_name 还是 class _declspec(dllimport) class_name 版本,这样就不再需要两个头文件。本程序中使用的是:

#ifdef DLL_FILE
class _declspec(dllexport) class_name //导出类
#else
class _declspec(dllimport) class_name //导入类
#endif

实际上,在 MFC DLL 的讲解中,您将看到比这更简便的方法,而此处仅仅是为了说明 _declspec(dllexport) 与 _declspec(dllimport) 匹对的问题。

由此可见,应用工程中几乎可以看到DLL中的一切,包括函数、变量以及类,这就是DLL所要提供的强大能力。只要DLL释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!

本章虽以VC++为平台讲解非MFC DLL,但是这些普遍的概念在其它语言及开发环境中也是相同的,其思维方式可以直接过渡。

接下来,我们将要研究MFC规则DLL(待续…)


相关链接:VC++动态链接库(DLL)编程深入浅出(二) (一)

第4节我们对非MFC DLL进行了介绍,这一节将详细地讲述MFC规则DLL的创建与使用技巧。

5. MFC规则DLL

5.1 概述

MFC规则DLL的概念体现在两方面:

(1) 它是MFC的
“是MFC的”意味着可以在这种DLL的内部使用MFC;

(2) 它是规则的
“是规则的”意味着它不同于MFC扩展DLL,在MFC规则DLL的内部虽然可以使用MFC,但是其与应用程序的接口不能是MFC。而MFC扩展DLL与应用程序的接口可以是MFC,可以从MFC扩展DLL中导出一个MFC类的派生类。

Regular DLL能够被所有支持DLL技术的语言所编写的应用程序调用,当然也包括使用MFC的应用程序。在这种动态连接库中,包含一个从CWinApp继承下来的类,DllMain函数则由MFC自动提供。

Regular DLL分为两类:

(1)静态链接到 MFC 的规则DLL
静态链接到MFC的规则DLL与MFC库(包括MFC扩展 DLL)静态链接,将MFC库的代码直接生成在.dll文件中。在调用这种DLL的接口时,MFC使用DLL的资源。因此,在静态链接到MFC 的规则DLL中不需要进行模块状态的切换。
使用这种方法生成的规则DLL其程序较大,也可能包含重复的代码。

(2)动态链接到 MFC 的规则DLL
动态链接到MFC 的规则DLL 可以和使用它的可执行文件同时动态链接到 MFC DLL 和任何MFC扩展 DLL。在使用了MFC共享库的时候,默认情况下,MFC使用主应用程序的资源句柄来加载资源模板。这样,当DLL和应用程序中存在相同ID的资源时(即所谓的资源重复问题),系统可能不能获得正确的资源。因此,对于共享MFC DLL的规则DLL,我们必须进行模块切换以使得MFC能够找到正确的资源模板。

我们可以在Visual C++中设置MFC规则DLL是静态链接到MFC DLL还是动态链接到MFC DLL。如图8,在项目名称上点击鼠标右键,在弹出菜单上选择“属性”,打开项目属性页,选择 配置属性 -> 常规,在 MFC 的使用 栏,选择 在静态库中使用 MFC在共享 DLL 中使用 MFC
在这里插入图片描述

图8 设置动态/静态链接MFC DLL

5.2 MFC规则DLL的创建

我们来一步步讲述使用MFC向导创建MFC规则DLL的过程,首先新建一个项目,如图9,选择项目的类型为 MFC DLL。点击OK进入如图10所示的对话框。
在这里插入图片描述

图9 MFC DLL项目的创建

在这里插入图片描述

图10 MFC DLL的创建选项

图10 所示对话框中的1区选择MFC DLL的类别。

2区选择是否支持自动化(automation)技术, automation 允许用户在一个应用程序中操纵另外一个应用程序或组件。例如,我们可以在应用程序中利用 Microsoft Word 或Microsoft Excel的工具,而这种使用对用户而言是透明的。自动化技术可以大大简化和加快应用程序的开发。

3区选择是否支持Windows Sockets,当选择此项目时,应用程序能在 TCP/IP 网络上进行通信。 CWinApp派生类的InitInstance成员函数会初始化通讯端的支持,同时工程中的StdAfx.h文件会自动include <AfxSock.h>头文件。

添加socket通讯支持后的InitInstance成员函数如下:

BOOL CRegularDllSocketApp::InitInstance()
{
	CWinApp::InitInstance();

	if (!AfxSocketInit())
	{
		AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
		return FALSE;
	}

	return TRUE;
}

5.3 一个简单的MFC规则DLL

这个DLL的例子(属于静态链接到MFC 的规则DLL)中提供了一个如图11所示的对话框。
在这里插入图片描述

图11 MFC规则DLL例子

在DLL中添加对话框的方式与在MFC应用程序中是一样的。

在图11所示DLL中的对话框的Hello按钮上点击时将MessageBox一个“Hello,pconline的网友”对话框,下面是相关的文件及源代码,其中删除了MFC向导自动生成的绝大多数注释(下载本工程附件):

第一组文件:CWinApp继承类的声明与实现

// RegularDll.h : RegularDll DLL 的主头文件

#pragma once

#ifndef __AFXWIN_H__
	#error "在包含此文件之前包含“stdafx.h”以生成 PCH 文件"
#endif

#include "resource.h"		// 主符号

class CRegularDllApp : public CWinApp
{
public:
	CRegularDllApp();

// 重写
public:
	virtual BOOL InitInstance();

	DECLARE_MESSAGE_MAP()
};
// RegularDll.cpp : 定义 DLL 的初始化例程。

#include "stdafx.h"
#include "RegularDll.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

BEGIN_MESSAGE_MAP(CRegularDllApp, CWinApp)
END_MESSAGE_MAP()

// CRegularDllApp 构造
CRegularDllApp::CRegularDllApp()
{
	// TODO:  在此处添加构造代码,
	// 将所有重要的初始化放置在 InitInstance 中
}

// 唯一的一个 CRegularDllApp 对象
CRegularDllApp theApp;

// CRegularDllApp 初始化
BOOL CRegularDllApp::InitInstance()
{
	CWinApp::InitInstance();

	return TRUE;
}

分析:
在这一组文件中定义了一个继承自CWinApp的类CRegularDllApp,并同时定义了其的一个实例 theApp。乍一看,您会以为它是一个MFC应用程序,因为MFC应用程序也包含这样的在工程名后添加“App”组成类名的类(并继承自CWinApp类),也定义了这个类的一个全局实例 theApp。

我们知道,在MFC应用程序中CWinApp取代了SDK程序中WinMain的地位,SDK程序WinMain所完成的工作由CWinApp的三个函数完成:

virtual BOOL InitApplication( );
virtual BOOL InitInstance( );
virtual BOOL Run( ); //传说中MFC程序的“活水源头”

但是MFC规则DLL并不是MFC应用程序,它所继承自CWinApp的类不包含消息循环。这是因为,MFC规则DLL不包含CWinApp::Run机制,主消息泵仍然由应用程序拥有。如果 DLL 生成无模式对话框或有自己的主框架窗口,则应用程序的主消息泵必须调用从DLL 导出的函数来调用 PreTranslateMessage 成员函数。

另外,MFC规则DLL与MFC 应用程序中一样,需要将所有 DLL中元素的初始化放到InitInstance 成员函数中。

第二组文件 自定义对话框类声明及实现(点击查看附件)

#pragma once

// CDllDialog 对话框
class CDllDialog : public CDialog
{
	DECLARE_DYNAMIC(CDllDialog)

public:
	CDllDialog(CWnd* pParent = NULL);   // 标准构造函数
	virtual ~CDllDialog();

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_DLLDIALOG };
#endif

protected:
	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持

	DECLARE_MESSAGE_MAP()
public:
	afx_msg void OnBnClickedHelloButton();
};
// DllDialog.cpp : 实现文件

#include "stdafx.h"
#include "RegularDll.h"
#include "DllDialog.h"
#include "afxdialogex.h"

// CDllDialog 对话框

IMPLEMENT_DYNAMIC(CDllDialog, CDialog)

CDllDialog::CDllDialog(CWnd* pParent /*=NULL*/)
	: CDialog(IDD_DLLDIALOG, pParent)
{
}

CDllDialog::~CDllDialog()
{
}

void CDllDialog::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CDllDialog, CDialog)
	ON_BN_CLICKED(IDC_HELLO_BUTTON, &CDllDialog::OnBnClickedHelloButton)
END_MESSAGE_MAP()

// CDllDialog 消息处理程序
void CDllDialog::OnBnClickedHelloButton()
{
	// TODO: 在此添加控件通知处理程序代码
	MessageBox(L"Hello,pconline的网友", L"pconline");
}

分析:
这一部分的编程与一般的应用程序根本没有什么不同,我们照样可以利用MFC类向导来自动为对话框上的控件添加事件。MFC类向导照样会生成类似 ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton) 的消息映射宏。

第三组文件 DLL中的资源文件

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 RegularDll.rc 使用
//
#define IDD_DLLDIALOG                   11000
#define IDC_HELLO_BUTTON                11000

分析:
在MFC规则DLL中使用资源也与在MFC应用程序中使用资源没有什么不同,我们照样可以用Visual C++的资源编辑工具进行资源的添加、删除和属性的更改。

第四组文件 MFC规则DLL接口函数

#include "StdAfx.h"
#include "DllDialog.h"

extern "C" __declspec(dllexport) void ShowDlg(void)
{
	CDllDialog dllDialog;
	dllDialog.DoModal();
}

分析:
这个接口并不使用MFC,但是在其中却可以调用MFC扩展类CDllDialog的函数,这体现了“规则”的概类。
与非MFC DLL完全相同,我们可以使用__declspec(dllexport)声明或在.def中引出的方式导出MFC规则DLL中的接口。

5.4 MFC规则DLL的调用

笔者编写了如图12的对话框MFC程序(下载本工程附件)来调用5.3节的MFC规则DLL,在这个程序的对话框上点击“调用DLL”按钮时弹出5.3节MFC规则DLL中的对话框。
在这里插入图片描述

图12 MFC规则DLL的调用例子

下面是“调用DLL”按钮单击事件的消息处理函数:

void CRegularDllCallDlg::OnBnClickedCalldllButton()
{
	typedef void(*lpFun)(void);

	HINSTANCE hDll;   //DLL句柄
	hDll = LoadLibrary(L"RegularDll.dll");
	if (NULL == hDll)
	{
		MessageBox(L"DLL加载失败");
	}

	lpFun addFun;  //函数指针
	lpFun pShowDlg = (lpFun)GetProcAddress(hDll, "ShowDlg");
	if (NULL == pShowDlg)
	{
		MessageBox(L"DLL中函数寻找失败");
	}
	pShowDlg();
}

上述例子中给出的是显示调用的方式,可以看出,其调用方式与第4节中非MFC DLL的调用方式没有什么不同。

我们照样可以在EXE程序中隐式调用MFC规则DLL,只需要将DLL工程生成的.lib文件和.dll文件拷入当前工程所在的目录,并在RegularDllCallDlg.cpp文件(图12所示对话框类的实现文件)的顶部添加:

#pragma comment(lib,"RegularDll.lib")
void ShowDlg(void);

并将 void CRegularDllCallDlg::OnBnClickedCalldllButton() 改为:

void CRegularDllCallDlg::OnBnClickedCalldllButton()
{
	ShowDlg();
}

5.5 共享MFC DLL的规则DLL的模块切换

应用程序进程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。进程本身的模块句柄一般为0x400000,而DLL模块的缺省句柄为0x10000000。如果程序同时加载了多个DLL,则每个DLL模块都会有不同的HINSTANCE。应用程序在加载DLL时对其进行了重定位。

共享MFC DLL(或MFC扩展DLL)的规则DLL涉及到HINSTANCE句柄问题,HINSTANCE句柄对于加载资源特别重要。EXE和DLL都有其自己的资源,而且这些资源的ID可能重复,应用程序需要通过资源模块的切换来找到正确的资源。如果应用程序需要来自于DLL的资源,就应将资源模块句柄指定为DLL的模块句柄;如果需要EXE文件中包含的资源,就应将资源模块句柄指定为EXE的模块句柄。

这次我们创建一个动态链接到MFC DLL的规则DLL(下载本工程附件),在其中包含如图13的对话框。
在这里插入图片描述

图13 DLL中的对话框

另外,在与这个DLL相同的工作区中生成一个基于对话框的MFC程序,其对话框与图12完全一样。但是在此工程中我们另外添加了一个如图14的对话框。
在这里插入图片描述

图14 EXE中的对话框

图13和图14中的对话框除了caption不同(以示区别)以外,其它的都相同。

尤其值得特别注意,在DLL和EXE中我们对图13和图14的对话框使用了相同的资源ID=2000,在DLL和EXE工程的resource.h中分别有如下的宏:

//DLL中对话框的ID
#define IDD_DLL_DIALOG                  2000
//EXE中对话框的ID
#define IDD_EXE_DIALOG                  2000

与5.3节静态链接MFC DLL的规则DLL相同,我们还是在规则DLL中定义接口函数ShowDlg,原型如下:

#include "StdAfx.h"
#include "SharedDll.h"

void ShowDlg(void)
{
	CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
	dlg.DoModal();
}

而为应用工程主对话框的“调用DLL”的单击事件添加如下消息处理函数:

void CSharedDllCallDlg::OnBnClickedCalldllButton()
{
	ShowDlg();
}

我们以为单击“调用DLL”会弹出如图13所示DLL中的对话框,可是可怕的事情发生了,我们看到是图14所示EXE中的对话框!

惊讶?

产生这个问题的根源在于应用程序与MFC规则DLL共享MFC DLL(或MFC扩展DLL)的程序总是默认使用EXE的资源,我们必须进行资源模块句柄的切换,其实现方法有三:

  • 方法一 在DLL接口函数中使用:
AFX_MANAGE_STATE(AfxGetStaticModuleState());

我们将DLL中的接口函数ShowDlg改为:

void ShowDlg(void)
{
	//方法1:在函数开始处变更,在函数结束时恢复
	//将AFX_MANAGE_STATE(AfxGetStaticModuleState());作为接口函数的第一条语句进行模块状态切换
	AFX_MANAGE_STATE(AfxGetStaticModuleState());

	CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
	dlg.DoModal();
}

这次我们再点击EXE程序中的“调用DLL”按钮,弹出的是DLL中的如图13的对话框!嘿嘿,弹出了正确的对话框资源。

AfxGetStaticModuleState 是一个函数,其原型为:

AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState();

该函数的功能是在栈上(这意味着其作用域是局部的)创建一个 AFX_MODULE_STATE 类(模块全局数据也就是模块状态)的实例,对其进行设置,并将其指针pModuleState返回。

AFX_MODULE_STATE 类的原型如下:

// AFX_MODULE_STATE (global data for a module)
class AFX_MODULE_STATE : public CNoTrackObject
{
public:
#ifdef _AFXDLL
	AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,
		BOOL bSystem = FALSE);
#else
	explicit AFX_MODULE_STATE(BOOL bDLL);
#endif
	~AFX_MODULE_STATE();

	CWinApp* m_pCurrentWinApp;
	HINSTANCE m_hCurrentInstanceHandle;
	HINSTANCE m_hCurrentResourceHandle;
	LPCTSTR m_lpszCurrentAppName;//省略后面的部分
};

AFX_MODULE_STATE 类利用其构造函数和析构函数进行存储模块状态现场及恢复现场的工作,类似汇编中call指令对pc指针和sp寄存器的保存与恢复、中断服务程序的中断现场压栈与恢复以及操作系统线程调度的任务控制块保存与恢复。

许多看似不着边际的知识点居然有惊人的相似!

AFX_MANAGE_STATE 是一个宏,其原型为:

#define AFX_MANAGE_STATE(p) _AfxInitManaged(); AFX_MANAGE_STATE_NO_INIT_MANAGED(p)

该宏用于将pModuleState设置为当前的有效模块状态。当离开该宏的作用域时(也就离开了pModuleState所指向栈上对象的作用域),先前的模块状态将由AFX_MODULE_STATE的析构函数恢复。

  • 方法二 在DLL接口函数中使用:
AfxGetResourceHandle();
AfxSetResourceHandle(HINSTANCE xxx);

AfxGetResourceHandle 用于获取当前资源模块句柄,而 AfxSetResourceHandle 则用于设置程序目前要使用的资源模块句柄。

我们将DLL中的接口函数ShowDlg改为:

void ShowDlg(void)
{
	//方法2:状态变更
	HINSTANCE save_hInstance = AfxGetResourceHandle();
	AfxSetResourceHandle(theApp.m_hInstance);

	CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
	dlg.DoModal();

	//方法2:状态还原
	AfxSetResourceHandle(save_hInstance);
}

通过 AfxGetResourceHandleAfxSetResourceHandle 的合理变更,我们能够灵活地设置程序的资源模块句柄,而方法一则只能在DLL接口函数退出的时候才会恢复模块句柄。方法二则不同,如果将ShowDlg改为:

extern CSharedDllApp theApp;	//需要声明theApp外部全局变量

void ShowDlg(void)
{
	//方法2:状态变更
	HINSTANCE save_hInstance = AfxGetResourceHandle();
	AfxSetResourceHandle(theApp.m_hInstance);

	CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
	dlg.DoModal();

	//方法2:状态还原
	AfxSetResourceHandle(save_hInstance);

	//使用方法2后在此处再进行操作针对的将是应用程序的资源
	CDialog dlg1(IDD_DLL_DIALOG);//打开ID为2000的对话框
	dlg1.DoModal();
}

在应用程序主对话框的“调用DLL”按钮上点击,将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。

  • 方法三 由应用程序自身切换

资源模块的切换除了可以由DLL接口函数完成以外,由应用程序自身也能完成(下载本工程附件)。

现在我们把DLL中的接口函数改为最简单的:

void ShowDlg(void)
{
	CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
	dlg.DoModal();
}

而将应用程序的 OnBnClickedCalldllButton 函数改为:

void CSharedDllCallDlg::OnBnClickedCalldllButton()
{
	//方法3:由应用程序本身进行状态切换

	//获取EXE模块句柄
	HINSTANCE exe_hInstance = GetModuleHandle(NULL);
	//或者HINSTANCE exe_hInstance = AfxGetResourceHandle();

	//获取DLL模块句柄
	HINSTANCE dll_hInstance = GetModuleHandle(L"SharedDll.dll");

	AfxSetResourceHandle(dll_hInstance); //切换状态
	ShowDlg(); //此时显示的是DLL的对话框
	AfxSetResourceHandle(exe_hInstance); //恢复状态

	//资源模块恢复后再调用ShowDlg
	ShowDlg(); //此时显示的是EXE的对话框
}

方法三中的Win32函数 GetModuleHandle 可以根据DLL的文件名获取DLL的模块句柄。如果需要得到EXE模块的句柄,则应调用带有Null参数的GetModuleHandle。

方法三与方法二的不同在于方法三是在应用程序中利用 AfxGetResourceHandleAfxSetResourceHandle 进行资源模块句柄切换的。同样地,在应用程序主对话框的“调用DLL”按钮上点击,也将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。

在下一节我们将对MFC扩展DLL进行详细分析和实例讲解,欢迎您继续关注本系列连载。


这是《VC++动态链接库(DLL)编程深入浅出》的第四部分,阅读本文前,请先阅读前三部分:(一)(二)(三)

6. MFC扩展DLL

MFC扩展DLL的内涵为MFC的扩展,用户使用MFC扩展DLL就像使用MFC本身的DLL一样。除了可以在MFC扩展DLL的内部使用MFC以外,MFC扩展DLL与应用程序的接口部分也可以是MFC。我们一般使用MFC扩展DLL来包含一些MFC的增强功能,譬如扩展MFC的 CStatic、CButton 等类使之具备更强大的能力。

使用Visual C++向导生产MFC扩展DLL时,MFC向导会自动增加DLL的入口函数DllMain:

extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
	// 如果使用 lpReserved,请将此移除
	UNREFERENCED_PARAMETER(lpReserved);

	if (dwReason == DLL_PROCESS_ATTACH)
	{
		TRACE0("MfcExpendDll.DLL 正在初始化!\n");
		
		// 扩展 DLL 一次性初始化
		if (!AfxInitExtensionModule(MfcExpendDllDLL, hInstance))
			return 0;

		// 将此 DLL 插入到资源链中
		// 注意:  如果此扩展 DLL 由
		//  MFC 规则 DLL (如 ActiveX 控件)隐式链接到,
		//  而不是由 MFC 应用程序链接到,则需要
		//  将此行从 DllMain 中移除并将其放置在一个
		//  从此扩展 DLL 导出的单独的函数中。  使用此扩展 DLL 的
		//  规则 DLL 然后应显式
		//  调用该函数以初始化此扩展 DLL。  否则,
		//  CDynLinkLibrary 对象不会附加到
		//  规则 DLL 的资源链,并将导致严重的
		//  问题。

		new CDynLinkLibrary(MfcExpendDllDLL);

	}
	else if (dwReason == DLL_PROCESS_DETACH)
	{
		TRACE0("MfcExpendDll.DLL 正在终止!\n");

		// 在调用析构函数之前终止该库
		AfxTermExtensionModule(MfcExpendDllDLL);
	}
	return 1;   // 确定
}

上述代码完成MFC扩展DLL的初始化和终止处理。

由于MFC扩展DLL导出函数和变量的方式与其它DLL没有什么区别,我们不再细致讲解。下面直接给出一个MFC扩展DLL的创建及在应用程序中调用它的例子。

6.1 MFC扩展DLL的创建

下面我们将在MFC扩展DLL中导出一个按钮类 CSXButton(扩展自MFC的CButton类),类CSXButton是一个用以取代 CButton的类,它使你能在同一个按钮上显示位图和文字,而MFC的按钮仅可显示二者之一。类CSXbutton的源代码在Internet上广泛流传,有很好的“群众基础”,因此用这个类来讲解MFC扩展DLL有其特殊的功效。

MFC中包含一些宏,这些宏在DLL和调用DLL的应用程序中被以不同的方式展开,这使得在DLL和应用程序中,使用统一的一个宏就可以表示出输出和输入的不同意思:

// for data
#ifndef AFX_DATA_EXPORT
	#define AFX_DATA_EXPORT __declspec(dllexport)
#endif
#ifndef AFX_DATA_IMPORT
	#define AFX_DATA_IMPORT __declspec(dllimport)
#endif

// for classes
#ifndef AFX_CLASS_EXPORT
	#define AFX_CLASS_EXPORT __declspec(dllexport)
#endif
#ifndef AFX_CLASS_IMPORT
	#define AFX_CLASS_IMPORT __declspec(dllimport)
#endif

// for global APIs
#ifndef AFX_API_EXPORT
	#define AFX_API_EXPORT __declspec(dllexport)
#endif
#ifndef AFX_API_IMPORT
	#define AFX_API_IMPORT __declspec(dllimport)
#endif
#ifndef AFX_EXT_DATA
	#ifdef _AFXEXT
		#define AFX_EXT_CLASS       AFX_CLASS_EXPORT
		#define AFX_EXT_API         AFX_API_EXPORT
		#define AFX_EXT_DATA        AFX_DATA_EXPORT
		#define AFX_EXT_DATADEF
	#else
		#define AFX_EXT_CLASS       AFX_CLASS_IMPORT
		#define AFX_EXT_API         AFX_API_IMPORT
		#define AFX_EXT_DATA        AFX_DATA_IMPORT
		#define AFX_EXT_DATADEF
	#endif
#endif

导出一个类,直接在类声明头文件中使用AFX_EXT_CLASS即可,以下是导出CSXButton类的例子:

#define	SXBUTTON_CENTER	-1

class AFX_EXT_CLASS CSXButton : public CButton
{
	DECLARE_DYNAMIC(CSXButton)

public:
	CSXButton();
	virtual ~CSXButton();

	// Attributes
private:
	//	Positioning
	BOOL		m_bUseOffset;
	CPoint		m_pointImage;
	CPoint		m_pointText;
	int			m_nImageOffsetFromBorder;
	int			m_nTextOffsetFromImage;

	//	Image
	HICON		m_hIcon;
	HBITMAP		m_hBitmap;
	HBITMAP		m_hBitmapDisabled;
	int			m_nImageWidth, m_nImageHeight;

	//	Color Tab
	char		m_bColorTab;
	COLORREF	m_crColorTab;

	//	State
	BOOL		m_bDefault;
	UINT		m_nOldAction;
	UINT		m_nOldState;


	// Operations
public:
	//	Positioning
	int		SetImageOffset(int nPixels);
	int		SetTextOffset(int nPixels);
	CPoint	SetImagePos(CPoint p);
	CPoint	SetTextPos(CPoint p);

	//	Image
	BOOL	SetIcon(UINT nID, int nWidth, int nHeight);
	BOOL	SetBitmap(UINT nID, int nWidth, int nHeight);
	BOOL	SetMaskedBitmap(UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask);
	BOOL	HasImage() { return (BOOL)(m_hIcon != 0 | m_hBitmap != 0); }

	//	Color Tab
	void	SetColorTab(COLORREF crTab);

	//	State
	BOOL	SetDefaultButton(BOOL bState = TRUE);

private:
	BOOL	SetBitmapCommon(UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask, BOOL bUseMask);
	void	CheckPointForCentering(CPoint &p, int nWidth, int nHeight);
	void	Redraw();

	// Overrides
	// ClassWizard generated virtual function overrides
	//{{AFX_VIRTUAL(CSXButton)
public:
	virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
	//}}AFX_VIRTUAL

protected:
	DECLARE_MESSAGE_MAP()
};

把SXBUTTON.CPP文件直接添加到工程,编译工程,得到“mfcexpenddll.lib”和“mfcexpenddll.dll”两个文件。我们用Visual Studio自带的Depends工具可以查看这个.dll,发现其导出了众多符号(见图15)。
在这里插入图片描述

图15 导出类时导出的大量符号

这些都是类的构造函数、析构函数及其它成员函数和变量经编译器处理过的符号,我们直接用__declspec(dllexport)语句声明类就导出了这些符号。

如果我们想用.lib文件导出这些符号,是非常困难的,我们需要在工程中生成.map文件,查询.map文件的符号,然后将其一一导出。如图16,在项目名称上点击鼠标右键,在弹出菜单上选择“属性”,打开项目属性页,选择 配置属性 -> 链接器 -> 调试,在 生成映射文件 栏,选择 是(/MAP),就可以产生.map文件了。
在这里插入图片描述

图16 产生.map文件

打开mfcexpenddll工程生成的.map文件,我们发现其中包含了图15中所示的符号(symbol)

 0002:00008ed0       ?HasImage@CSXButton@@QAEHXZ 10019ed0 f i SXButton.obj
 0002:00008f70       ?Height@CRect@@QBEHXZ      10019f70 f i SXButton.obj
 0002:00008fc0       ?Offset@CPoint@@QAEXHH@Z   10019fc0 f i SXButton.obj
 0002:00009020       ?Redraw@CSXButton@@AAEXXZ  1001a020 f i SXButton.obj
 0002:00009080       ?SetBitmap@CSXButton@@QAEHIHH@Z 1001a080 f   SXButton.obj
 0002:000090f0       ?SetBitmapCommon@CSXButton@@AAEHIHHKH@Z 1001a0f0 f   SXButton.obj
 0002:00009330       ?SetColorTab@CSXButton@@QAEXK@Z 1001a330 f   SXButton.obj
 0002:000093a0       ?SetDefaultButton@CSXButton@@QAEHH@Z 1001a3a0 f   SXButton.obj
 0002:00009440       ?SetIcon@CSXButton@@QAEHIHH@Z 1001a440 f   SXButton.obj
 0002:00009520       ?SetImageOffset@CSXButton@@QAEHH@Z 1001a520 f   SXButton.obj
 0002:000095b0       ?SetImagePos@CSXButton@@QAE?AVCPoint@@V2@@Z 1001a5b0 f   SXButton.obj
 0002:000096c0       ?SetMaskedBitmap@CSXButton@@QAEHIHHK@Z 1001a6c0 f   SXButton.obj
 0002:00009730       ?SetTextOffset@CSXButton@@QAEHH@Z 1001a730 f   SXButton.obj
 0002:000097c0       ?SetTextPos@CSXButton@@QAE?AVCPoint@@V2@@Z 1001a7c0 f   SXButton.obj
 ………………………………………..//省略

所以,对于MFC扩展DLL,我们不宜以.lib文件导出类。

6.2 MFC扩展DLL的调用

在DLL所在工作区新增一个dllcall工程,它是一个基于对话框的MFC EXE程序。在其中增加两个按钮 SXBUTTON1、SXBUTTON2,并设置其属性为“Owner draw”,如图17。
在这里插入图片描述

图17 设置按钮属性为“Owner draw”

在工程中添加两个ICON资源:IDI_MSN_ICON(MSN的图标)、IDI_REFBAR_ICON(Windows的系统图标)。

修改工程的“DllCallDlg.h”头文件为:

#include "../MfcExpendDll/SXButton.h"   //包含dll的导出类头文件
#pragma comment(lib,"mfcexpenddll.lib") //隐式链接dll

// CDllCallDlg 对话框
class CDllCallDlg : public CDialogEx
{
// 构造
public:
	CDllCallDlg(CWnd* pParent = NULL);	// 标准构造函数

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_DLLCALL_DIALOG };
#endif

	//增加与两个按钮对应的成员变量
	CSXButton m_button1;
	CSXButton m_button2;
	...
};

同时,修改“DllCallDlg.cpp”文件,使得 m_button1、m_button2 成员变量与对话框上的按钮控件建立关联:

void CDllCallDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_BUTTON2, m_button2);
	DDX_Control(pDX, IDC_BUTTON1, m_button1);
}

修改 BOOL CDllCallDlg::OnInitDialog() 函数,在其中增加对两个按钮设置ICON的代码:

BOOL CDllCallDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 将“关于...”菜单项添加到系统菜单中。

	// IDM_ABOUTBOX 必须在系统命令范围内。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != NULL)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// 设置此对话框的图标。  当应用程序主窗口不是对话框时,框架将自动
	//  执行此操作
	SetIcon(m_hIcon, TRUE);			// 设置大图标
	SetIcon(m_hIcon, FALSE);		// 设置小图标

	// TODO: 在此添加额外的初始化代码
	m_button1.SetIcon(IDI_MSN_ICON, 16, 16);
	m_button2.SetIcon(IDI_REFBAR_ICON, 16, 16);

	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

运行程序,将出现如图18的对话框,图形和文字同时出现在按钮上,这说明我们正确地调用了MFC扩展DLL。
在这里插入图片描述

图18 DLL扩展的按钮被显示

如果我们不修改 void CDllCallDlg::DoDataExchange(CDataExchange* pDX),即不增加下列代码:

	DDX_Control(pDX, IDC_BUTTON2, m_button2);
	DDX_Control(pDX, IDC_BUTTON1, m_button1);

我们也可以在 BOOL CDllCallDlg::OnInitDialog() 函数中添加如下代码实现m_button1、m_button2与IDC_BUTTON1、IDC_BUTTON2的关联:

	m_button1.SubclassDlgItem(IDC_BUTTON1, this);
	m_button2.SubclassDlgItem(IDC_BUTTON2, this);

但是,DDX_Control 与按钮类的 SubclassDlgItem 成员函数不能同时存在,否则程序会出错。

6.3 总结

由以上分析可知,MFC扩展DLL的导出与引用方式与前几节所讲述的方式没有太大的差别,MFC扩展DLL主要强调对MFC进行功能扩展。因此,如果DLL的目标不是增强MFC的功能,其与应用程序的接口也不是MFC,请不要将DLL建立为MFC扩展DLL。

下载本文相关源代码。

补充

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章