VC++
动态链接库
(DLL)
编程(三)
――
MFC
规则
DLL
作者:宋宝华 e-mail:21cnbao@21cn.com
第
4
节我们对非
MFC DLL
进行了介绍,这一节将详细地讲述
MFC
规则
DLL
的创建与使用技巧。
另外,自从本文开始连载后,收到了一些读者的
e-mail
。有的读者提出了一些问题,笔者将在本文的最后一次连载中选取其中的典型问题进行解答。由于时间的关系,对于读者朋友的来信,笔者暂时不能一一回复,还望海涵!由于笔者的水平有限,文中难免有错误和纰漏,也热诚欢迎读者朋友不吝指正!
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
,依次选择
Visual C++
的
project -> Settings -> General
菜单或选项,在
Microsoft Foundation Classes
中进行设置。
图8 设置动态/静态链接MFC DLL
5.2 MFC
规则
DLL
的创建
我们来一步步讲述使用
MFC
向导创建
MFC
规则
DLL
的过程,首先新建一个
project
,如图
9
,选择
project
的类型为
MFC AppWizard(dll)
。点击
OK
进入如图
10
所示的对话框。
图9 MFC DLL工程的创建
图
10
所示对话框中的
1
区选择
MFC DLL
的类别。
2
区选择是否支持
automation
(自动化)技术,
automation
允许用户在一个应用程序中操纵另外一个应用程序或组件。例如,我们可以在应用程序中利用
Microsoft Word
或
Microsoft Excel
的工具,而这种使用对用户而言是透明的。自动化技术可以大大简化和加快应用程序的开发。
3
区选择是否支持
Windows Sockets
,当选择此项目时,应用程序能在
TCP/IP
网络上进行通信。
CWinApp
派生类的
InitInstance
成员函数会初始化通讯端的支持,同时工程中的
StdAfx.h
文件会自动
include
头文件。
添加
socket
通讯支持后的
InitInstance
成员函数如下:
BOOL CRegularDllSocketApp::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}
return TRUE;
}
4
区选择是否由
MFC
向导自动在源代码中添加注释,一般我们选择“
Yes,please
”。
图10 MFC DLL的创建选项
5.3
一个简单的
MFC
规则
DLL
这个
DLL
的例子(属于静态链接到
MFC
的规则
DLL
)中提供了一个如图
11
所示的对话框。
图11 MFC规则DLL例子
在
DLL
中添加对话框的方式与在
MFC
应用程序中是一样的。
在图
11
所示
DLL
中的对话框的
Hello
按钮上点击时将
MessageBox
一个“
Hello,pconline
的网友”对话框,下面是相关的文件及源代码,其中删除了
MFC
向导自动生成的绝大多数注释(
下载本工程
):
第一组文件:
CWinApp
继承类的声明与实现
// RegularDll.h : main header file for the REGULARDLL DLL
#if !defined(AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_)
#define AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#ifndef __AFXWIN_H__
#error include 'stdafx.h' before including this file for PCH
#endif
#include "resource.h" // main symbols
class CRegularDllApp : public CWinApp
{
public:
CRegularDllApp();
DECLARE_MESSAGE_MAP()
};
#endif
// RegularDll.cpp : Defines the initialization routines for the DLL.
#include "stdafx.h"
#include "RegularDll.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
BEGIN_MESSAGE_MAP(CRegularDllApp, CWinApp)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CRegularDllApp construction
CRegularDllApp::CRegularDllApp()
{
}
/////////////////////////////////////////////////////////////////////////////
// The one and only CRegularDllApp object
CRegularDllApp theApp;
分析:
在这一组文件中定义了一个继承自
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
成员函数中。
第二组文件
自定义对话框类声明及实现
#if !defined(AFX_DLLDIALOG_H__CEA 4C 6AF_245D_ 48A 6_B 11A _A5521EAD 7C 4E__INCLUDED_)
#define AFX_DLLDIALOG_H__CEA 4C 6AF_245D_ 48A 6_B 11A _A5521EAD 7C 4E__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// DllDialog.h : header file
/////////////////////////////////////////////////////////////////////////////
// CDllDialog dialog
class CDllDialog : public CDialog
{
// Construction
public:
CDllDialog(CWnd* pParent = NULL); // standard constructor
enum { IDD = IDD_DLL_DIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Implementation
protected:
afx_msg void OnHelloButton();
DECLARE_MESSAGE_MAP()
};
#endif
// DllDialog.cpp : implementation file
#include "stdafx.h"
#include "RegularDll.h"
#include "DllDialog.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CDllDialog dialog
CDllDialog::CDllDialog(CWnd* pParent /*=NULL*/)
: CDialog(CDllDialog::IDD, pParent)
{
}
void CDllDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CDllDialog, CDialog)
ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CDllDialog message handlers
void CDllDialog::OnHelloButton()
{
MessageBox("Hello,pconline
的网友
","pconline");
}
分析:
这一部分的编程与一般的应用程序根本没有什么不同,我们照样可以利用
MFC
类向导来自动为对话框上的控件添加事件。
MFC
类向导照样会生成类似
ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton)
的消息映射宏。
第三组文件
DLL
中的资源文件
//`NO_DEPENDENCIES`
// Microsoft Developer Studio generated include file.
// Used by RegularDll.rc
//
#define IDD_DLL_DIALOG 1000
#define IDC_HELLO_BUTTON 1000
分析:
在
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::OnCalldllButton()
{
typedef void (*lpFun)(void);
HINSTANCE hDll; //DLL
句柄
hDll = LoadLibrary("RegularDll.dll");
if (NULL==hDll)
{
MessageBox("DLL
加载失败
");
}
lpFun addFun; //
函数指针
lpFun pShowDlg = (lpFun)GetProcAddress(hDll,"ShowDlg");
if (NULL==pShowDlg)
{
MessageBox("DLL
中函数寻找失败
");
}
pShowDlg();
}
上述例子中给出的是显示调用的方式,可以看出,其调用方式与第
4
节中非
MFC DLL
的调用方式没有什么不同。
我们照样可以在
EXE
程序中隐式调用
MFC
规则
DLL
,只需要将
DLL
工程生成的
.lib
文件和
.dll
文件拷入当前工程所在的目录,并在
RegularDllCallDlg.cpp
文件(图
12
所示对话框类的实现文件)的顶部添加:
#pragma comment(lib,"RegularDll.lib")
void ShowDlg(void);
并将
void CRegularDllCallDlg::OnCalldllButton()
改为:
void CRegularDllCallDlg::OnCalldllButton()
{
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::OnCalldllButton()
{
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);
AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,BOOL bSystem);
#else
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
是一个宏,其原型为:
AFX_MANAGE_STATE( AFX_MODULE_STATE* pModuleState )
该宏用于将
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);
}
通过
AfxGetResourceHandle
和
AfxSetResourceHandle
的合理变更,我们能够灵活地设置程序的资源模块句柄,而方法一则只能在
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();
}
而将应用程序的
OnCalldllButton
函数改为:
void CSharedDllCallDlg::OnCalldllButton()
{
//
方法
3
:由应用程序本身进行状态切换
//
获取
EXE
模块句柄
HINSTANCE exe_hInstance = GetModuleHandle(NULL);
//
或者
HINSTANCE exe_hInstance = AfxGetResourceHandle();
//
获取
DLL
模块句柄
HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll");
AfxSetResourceHandle(dll_hInstance); //
切换状态
ShowDlg(); //
此时显示的是
DLL
的对话框
AfxSetResourceHandle(exe_hInstance); //
恢复状态
//
资源模块恢复后再调用
ShowDlg
ShowDlg(); //
此时显示的是
EXE
的对话框
}
方法三中的
Win32
函数
GetModuleHandle
可以根据
DLL
的文件名获取
DLL
的模块句柄。如果需要得到
EXE
模块的句柄,则应调用带有
Null
参数的
GetModuleHandle
。
方法三与方法二的不同在于方法三是在应用程序中利用
AfxGetResourceHandle
和
AfxSetResourceHandle
进行资源模块句柄切换的。同样地,在应用程序主对话框的“调用
DLL
”按钮上点击,也将看到两个对话框,相继为
DLL
中的对话框(图
13
)和
EXE
中的对话框(图
14
)。
在下一节我们将对
MFC
扩展
DLL
进行详细分析和实例讲解,欢迎您继续关注本系列连载。