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 中进行设置。
VC++动态链接库(DLL)编程(三)――MFC规则DLL_第1张图片
8 设置动态/静态链接MFC DLL
5.2 MFC 规则 DLL 的创建
我们来一步步讲述使用 MFC 向导创建 MFC 规则 DLL 的过程,首先新建一个 project ,如图 9 ,选择 project 的类型为 MFC AppWizard(dll) 。点击 OK 进入如图 10 所示的对话框。
VC++动态链接库(DLL)编程(三)――MFC规则DLL_第2张图片
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 ”。
VC++动态链接库(DLL)编程(三)――MFC规则DLL_第3张图片
10  MFC DLL的创建选项
5.3 一个简单的 MFC 规则 DLL
这个 DLL 的例子(属于静态链接到 MFC 的规则 DLL )中提供了一个如图 11 所示的对话框。
VC++动态链接库(DLL)编程(三)――MFC规则DLL_第4张图片
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 中的对话框。
VC++动态链接库(DLL)编程(三)――MFC规则DLL_第5张图片
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 的对话框。
VC++动态链接库(DLL)编程(三)――MFC规则DLL_第6张图片
13  DLL中的对话框
另外,在与这个 DLL 相同的工作区中生成一个基于对话框的 MFC 程序,其对话框与图 12 完全一样。但是在此工程中我们另外添加了一个如图 14 的对话框。
VC++动态链接库(DLL)编程(三)――MFC规则DLL_第7张图片
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 进行详细分析和实例讲解,欢迎您继续关注本系列连载。