第九講 創建用戶模塊
這一講介紹Visual C++的用戶模塊的概念,並介紹靜態庫、動態連接庫、擴展類庫的設計與使用。
9.1 用戶模塊
用戶模塊是由用戶自己開發的、可以加入到最終用戶(包括用戶本人和其他使用該模塊的人)應用程序中提供某一特定功能的函數和類的集合。
爲了完成同樣的工作,也可以向最終用戶提供源程序。但是,使用用戶模塊有許多好處:首先是省去用戶管理源代碼的煩惱,用戶許多情況下往往並不關心模塊的內部實現,他只是想把它作爲一個黑匣子使用。另外,模塊的開發者有時候並不希望模塊使用者看到源代碼。還有,使用模塊而不使用源代碼還可以避免模塊的函數名、變量名與最終用戶的程序上的衝突。
用戶模塊可分爲兩大類:靜態連接庫和動態連接庫。
靜態連接庫提供了函數的完整的目標代碼,如果程序調用靜態連接庫中的函數,則在進行連接時連接程序將靜態連接庫中所包含的該函數的代碼拷貝至運行文件中。
動態連接庫是一個可執行模塊,其包含的函數可以由Windows應用程序調用以執行一些功能。動態連接庫主要爲應用程序模塊提供服務。Windows內核的三個模塊USER.EXE、KENERL.EXE和GDI.EXE實際上都是動態連接庫,分別提供用戶消息服務、進程管理、圖形輸出等服務。
動態連接庫也包含了其所提供的函數的目標代碼,但是在程序連接動態連接庫中的函數時,連接程序並不將包含在動態連接庫中的函數的目標代碼拷貝至運行文件,而只是簡單地記錄了函數的位置信息(即包含於哪個動態連接庫中以及在動態連接庫中的位置)。有了這些信息後,程序在執行時,即可找到該函數的目標代碼。因爲只是在執行時纔得到真正的連接,因此稱爲動態連接。提供函數在動態連接庫中位置的信息存放在一個獨立的文件中,這個文件就是引入庫(IMPORT LIB)。
由於靜態連接庫將目標代碼連接到應用程序中,當程序運行時,如果兩個程序調用了同一靜態庫中的函數,內存中將出現該函數的多份拷貝。而動態連接庫則更適合於多任務環境:當兩個應用程序調用了同一動態連接庫中的同一個函數時,內存中只保留該函數的一份拷貝,這樣內存利用率更高。
利用動態連接庫還可以實現資源共享:像Windows下的串行口、並行口驅動程序都是動態連接庫;另外,Windows下的字體也是動態連接庫。
但是,靜態庫由於將目標代碼連入應用程序中,應用程序可獨立運行。而使用動態連接庫時,隨同應用程序還要提供動態連接庫文件(DLL文件)。比如,發佈Visual C++編寫的程序時,如果使用了動態連接,則在提供可執行文件同時還需要提供Visual C++的動態連接庫。
應用程序和動態連接庫都是完成一定功能的可執行模塊。它們的區別是:應用程序有自己的消息循環,而動態連接庫沒有自己的消息循環(但是它可以發送消息);應用程序一般是主動完成某一功能的,而動態連接庫主要是被動(在中斷驅動程序中也主動完成一些功能)的提供服務。
9.2靜態連接庫
9.2.1創建靜態庫
現在以一個簡單的數學函數庫爲例介紹靜態庫的創建和使用。
要創建靜態庫,選擇File->New菜單,彈出New對話框。選擇Projects標籤,在項目類型列表框中選擇Win32 Static Library,在Name中輸入mymath,表明要創建一個mymath.lib的靜態庫文件。
然後用Project->Add to Project->Files菜單往mymath工程中加入以下兩個文件:
1.頭文件(見清單9.1):定義了Summary和Factorial兩個函數,分別用於完成求和與階乘。注意這裏使用C風格的函數,需要加入extern “C”關鍵字,表明它是C風格的外部函數。
清單9.1 頭文件
#ifndef _MYMATH_H
#define _MYMATH_H
extern “C”
{
int Summary(int n);
int Factorial(int n);
}
#endif
2.源文件:包含了Summary和Factorial函數的定義,見清單9.2。
清單9.2 源文件
int Summary(int n)
{
int sum=0;
int i;
for(i=1;i<=n;i++)
{
sum+=i;
}
return sum;
}
int Factorial(int n)
{
int Fact=1;
int i;
for(i=1;i<=n;i++)
{
Fact=Fact*i;
}
return Fact;
}
在Build菜單下,選擇Build菜單下的Build mymath.lib。Visual C++編譯鏈接工程,在mymath/debug目錄下生成mymath.lib文件。至此,靜態連接庫生成的工作就做完了。下面用一個小程序來測試這個靜態庫。
提示:用戶在交付最終靜態連接庫時,只需要提供.lib文件和頭文件,不需要再提供庫的源代碼。
9.2.2測試靜態庫
用AppWizard生成一個基於對話框的應用程序test。打開test資源文件,修改IDD_TEST_DIALOG對話框資源,加入兩個按鈕。按鈕ID和文字爲:
IDC_SUM “&Summary”
IDC_FACTORIAL “&Factorial”
如圖9-1所示。
圖9-1 修改test對話框
用ClassWizard爲上述兩個按鈕Click事件生成消息處理函數OnSum和OnFactorial,並加入代碼,修改後的OnSum和OnFactorial見清單9.3。
清單9.3 OnSum和OnFactorial函數定義
void CTestDlg::OnSum()
{
// TODO: Add your control notification handler code here
int nSum=Summary(10);
CString sResult;
sResult.Format("Sum(10)=%d",nSum);
AfxMessageBox(sResult);
}
void CTestDlg::OnFactorial()
{
// TODO: Add your control notification handler code here
int nFact=Factorial(10);
CString sResult;
sResult.Format("10!=%d",nFact);
AfxMessageBox(sResult);
}
由於要使用mymath.lib中的函數,首先要將mymath.lib和mymath.h兩個文件拷貝到test目錄下。然後用Project->Add to Project->Files命令,將mymath.lib加入到工程中。
在testdlg.cpp文件頭部,還要加入頭文件mymath.h:
#include "stdafx.h"
#include "Test.h"
#include "TestDlg.h"
#include "mymath.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
編譯運行test程序,點Factorial按鈕,彈出如圖9-2的消息框。
圖9-2 Test程序運行結果
9.3創建動態連接庫
在一些情況下,必須使用動態連接庫:
1.多個應用程序共享代碼和數據:比如Office軟件的各個組成部分有相似的外觀和功能,這就是通過共享動態連接庫實現的。
2.在鉤子程序過濾系統消息時必須使用動態連接庫
3.設備驅動程序必須是動態連接庫
4.如果要在對話框編輯器中使用自己定義的控件,也必須使用動態連接庫
5.動態連接庫以一種自然的方式將一個大的應用程序劃分爲幾個小的模塊,有利於小組內部成員的分工與合作。而且,各個模塊可以獨立升級。如果小組中的一個成員開發了一組實用例程,他就可以把這些例程放在一個動態連接庫中,讓小組的其他成員使用。
6.爲了實現應用程序的國際化,往往需要使用動態連接庫。使用動態連接庫可以將針對某一國家、語言的信息存放在其中。對於不同的版本,使用不同的動態連接庫。在使用AppWizard生成應用程序時,我們可以指定資源文件使用的語言,這就是通過提供不同的動態連接庫實現的。
MFC支持兩類動態連接庫的創建:
用戶動態連接庫
MFC擴展類庫。
9.3.1用戶動態連接庫(_USRDLL)
用戶動態連接庫一般使用C語言接口。要創建一個動態連接庫,選擇File->New菜單,彈出New對話框。在Projects標籤頁下,選擇“Win32 Dynamic-Link Library”。Visual C++就會創建動態連接庫所需的工程文件和MAK文件。
然後把下面兩個文件加入到工程中(Project-Add to Project-Files菜單)。
文件1:mymaths.cpp
////////////////////////////
//mymaths.cpp
//
//a maths API DLL.
//
///////////////////////////
#include<windows.h>
//Declare the DLL functions prototypes
int Summary(int);
int Factorial(int);
//////////////////////////
//DllEntryPoint():The entry point of the DLL
//
/////////////////////////
BOOL WINAPI DLLEntryPoint(HINSTANCE hDLL,DWORD dwReason,
LPVOID Reserved)
{
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
{
//一些初始化代碼
break;
}
case DLL_PROCESS_DETACH:
{
//一些用於清理的代碼
break;
}
}
return TRUE;
}
int Summary(int n)
{
int sum=0;
int i;
for(i=1;i<=n;i++)
{
sum+=i;
}
return sum;
}
int Factorial(int n)
{
int Fact=1;
int i;
for(i=1;i<=n;i++)
{
Fact=Fact*i;
}
return Fact;
}
文件2:mymaths.def
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Mymaths.DEF
;
;The DEF file for the Mymaths.DLL DLL.
;
LIBRARY mymaths
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD SINGLE
EXPORTS
;The names of the DLL functions
Summary
Factorial
在文件mymaths.cpp開頭,聲明瞭動態連接庫所包含的兩個函數:Summary和Factorial。接着是DllEntryPoint()函數的定義。DllEntryPoint()顧名思義是動態連接庫的入口,應用程序通過該入口訪問動態連接庫提供的服務。DllEntryPoint()主體是一個switch/case語句:
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
{
//一些初始化代碼
break;
}
case DLL_PROCESS_DETACH:
{
//一些用於清理的代碼
break;
}
}
其中,在case DLL_PROCESS_ATTACH分支可加入動態連接庫執行時的一些初始化代碼。在case DLL_PROCESS_DETACH加入動態連接庫被卸載時的一些清理代碼,比如釋放動態連接庫運行時申請的內存等。
在DllEntryPoint()函數後,是兩個函數Summary和Factorial函數的定義。它們的定義與前面的靜態庫完全相同。在這裏用戶可以放入任何函數。
另外,我們還需要一個mymaths.def文件。這個文件記錄了可被外部應用程序使用的DLL庫函數名字。這些名字信息和對應的函數位置的信息將被編譯進動態連接庫文件中,然後應用程序根據函數名字和函數位置對照表來找到對應的函數。
按F7編譯工程,Visual C++就在mymaths/debug目錄下生成一個mymaths.dll動態連接庫文件。
現在,我們來使用剛纔生成的動態連接庫。我們並不重新生成一個程序,而是修改前面測試靜態庫時的test程序。首先,把mymaths/debug目錄下的mymaths.dll拷貝到test/debug目錄下。test程序運行時,會在該目錄下搜索動態連接庫文件。然後修改testdlg.h,在其中加入一個函數LoadDLL()的聲明,見清單9.4。LoadDLL用於載入動態連接庫。
清單9.4 修改後的對話框頭文件
class CTestDlg : public CDialog
{
// Construction
public:
CTestDlg(CWnd* pParent = NULL); // standard constructor
protected:
void LoadDLL();
//......
}
然後修改testdlg.cpp,修改後如清單9.5。
清單95. TestDlg.cpp文件
// TestDlg.cpp : implementation file
//
#include "stdafx.h"
#include "Test.h"
#include "TestDlg.h"
//#include "mymath.h" //註釋掉mymath.h頭文件
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
//The instance of the Mymaths.DLL library
HINSTANCE ghMathsDLL=NULL;
//declare the Summary() function from the Mymaths.DLL libray.
typedef int (*SUMMARY)(int);
SUMMARY Summary;
//declare the Factorial() function from
//the Mymaths.DLL library.
typedef int (*FACTORIAL)(int);
FACTORIAL Factorial;
/////////////////////////////////////////////////////////////////////////////
// CAboutDlg dialog used for App About
class CAboutDlg : public CDialog
{
//...
};
//CAboutDlg的一些成員函數定義
//CTestDlg的一些成員函數定義
void CTestDlg::OnSum()
{
// TODO: Add your control notification handler code here
LoadDLL();
int nSum=Summary(10);
CString sResult;
sResult.Format("Sum(10)=%d",nSum);
AfxMessageBox(sResult);
}
void CTestDlg::OnFactorial()
{
// TODO: Add your control notification handler code here
LoadDLL();
int nFact=Factorial(10);
CString sResult;
sResult.Format("10!=%d",nFact);
AfxMessageBox(sResult);
}
void CTestDlg::LoadDLL()
{
//如果DLL已經載入,則返回
if(ghMathsDLL!=NULL)
{
return;
}
//載入Mymaths.DLL文件.
ghMathsDLL=LoadLibrary("mymaths.DLL");
//如果載入DLL失敗,提示用戶
if(ghMathsDLL==NULL)
{
AfxMessageBox("Cannot load DLL file!");
}
//獲得DLL中Summary函數的地址
Summary=(SUMMARY)GetProcAddress(ghMathsDLL,"Summary");
//獲得DLL中Factorial函數的地址
Factorial=(FACTORIAL)GetProcAddress(ghMathsDLL,"Factorial");
}
在testdlg.cpp文件開頭,加入:
//The instance of the Mymaths.DLL library
HINSTANCE ghMathsDLL=NULL;
//declare the Summary() function from the Mymaths.DLL libray.
typedef int (*SUMMARY)(int);
SUMMARY Summary;
//declare the Factorial() function from
//the Mymaths.DLL library.
typedef int (*FACTORIAL)(int);
FACTORIAL Factorial;
首先加入一個ghMathsDLL的全局變量,它是動態連接庫載入後的句柄(同應用程序一樣,每個動態連接庫載入都會有一個句柄和它相對應)。應用程序通過句柄訪問庫中的函數。然後加入Summary和Factorial函數指針的類型定義。
在LoadDLL()函數定義中,檢查動態連接庫句柄是否爲空;若爲空,則用LoadLibrary載入該動態連接庫。然後用GetProcAddress取得Summary和Factorial函數地址。
在OnFactorial和OnSummary函數開頭,調用LoadDLL(),載入動態連接庫。現在編譯運行程序,按Factorial按鈕測試一下程序。
應用程序是如何查找DLL文件的
應用程序test按以下順序查找動態連接庫文件:
- 當前目錄下(因此要將動態連接庫拷貝至DEBUG目錄下,因爲可執行文件在該目錄下)
- Windows目錄
- Windows系統目錄
- PATH環境變量中設置的目錄
- 列入映射網絡的目錄表中的目錄
調用動態連接庫中的函數的方法
有兩種方法可以調用動態連接庫中的函數:
1.通過引入庫:
利用Visual C++提供的IMPLIB工具爲動態連接庫生成引入庫,爲引入庫設計一個頭文件:
#ifndef _MYMATH_H
#define _MYMATH_H
extern “C”
{
int Summary(int n);
int Factorial(int n);
}
#endif
將該頭文件包含在使用動態連接庫的源文件中,連接應用程序時會連接上該引入庫。這樣,應用程序就可以象使用靜態連接庫一樣自由的使用動態連接庫中的函數了。注意要把動態連接庫拷貝到應用程序可執行文件所在的目錄(/TEST/DEBUG)下。
這是一種常用的方法。實際上,應用程序就是通過這種方式訪問Windows的API函數的。Windows爲其內核動態連接庫生成引入庫並提供了頭文件。應用程序在編譯時將引入庫的信息帶入可執行文件中,在運行時通過引入庫信息訪問API函數。
2. 直接指定庫和函數地址
這種方式適合於一些提供文件格式轉換等服務的動態連接庫。比如,一個程序帶有多個動態連接庫,分別用於訪問JPG、BMP、GIF等多種圖像文件格式,這些動態連接庫提供了相同的庫函數接口。此時,無法使用引入庫方式指定庫函數。可以採用下面的方法來解決這個問題。
HANDLE hLibrary;
FARPROC lpFunc;
int nFormat;
if(nFormat==JPEG)//如果是JPEG格式,裝入JPEG動態連接庫
{
hLibrary=LoadLibrary(“JPEG.DLL”);
}
else//是GIF格式
hLibrary= LoadLibrary(“GIF.DLL”);
if(hLibrary>=32)
{
lpFunc=GetProcAddress(hLibrary,”ReadImage”);
if(lpFunc!=(FARPROC)NULL)
(*lpFunc)((LPCTSTR)strFileName);
FreeLibrary(hLibrary);
}
LoadLibrary函數裝入所需的動態連接庫,並返回庫的句柄。如果句柄小於32,則載入庫失敗,錯誤含義參見有關手冊。GetProcAddress函數使用函數名字取得函數的地址。利用該函數地址,就可以訪問動態連接庫的函數了。
FreeLibrary通過檢查動態連接庫的引用計數器,判斷是否還有別的程序在使用這個動態連接庫。如果沒有,就從內存中移去該動態連接庫;如果有,將動態連接庫的使用計數器減1。LoadLibrary則將引用計數加1。
在用戶動態連接庫中,也可以使用MFC類。這時,可以選擇靜態連接和動態連接兩種方式使用MFC庫。
9.3.2 MFC擴展類庫(_AFXDLL)
除了創建具有C語言接口的用戶動態連接庫外,MFC還允許用戶在動態連接庫中創建MFC類的派生類,這些類作爲MFC類的自然延伸出現,可以爲其他MFC應用程序所使用,就象使用普通的MFC類一樣。
創建擴展類庫
要創建擴展類庫,可以選擇File->New菜單,在Projects類型中選擇MFC AppWizard(dll)。彈出MFC AppWizard 1of 1對話框,從中選擇MFC Extension DLL(using shared MFC DLL)。AppWizard就會生成Extension DLL所需的框架。
這裏不再創建動態連接庫,而是用Visual C++的例子DLLHUSK程序(在SAMPLES/MFC/ADVANCED /DLLHUSK目錄下)說明擴展類庫的創建和使用。
在DLLHUSK項目工作區中,包含三個工程:DLLHUSK,TESTDLL1,TESTDLL2。
TESTDLL1和TESTDLL2分別定義了幾個擴展類:CTextDoc、CHelloView和CListOutputFrame,DLLHUSK是使用這些類的示例程序。
在CListOutputFrame聲明中,要加入AFX_EXT_CLASS,表明它是一個MFC擴展類。
class AFX_EXT_CLASS CListOutputFrame:public CMDIChildWnd
{
...
}
在函數定義處,還要包含afxdllx.h頭文件
// Initialization of MFC Extension DLL
#include "afxdllx.h" // standard MFC Extension DLL routines
類的成員函數使用與應用程序中類的使用大致相同。
在CListOutputFrame類定義文件中,還提供了一個C函數。它的函數聲明在類頭文件testdll2.h中:
// Initialize the DLL, register the classes etc
extern "C" AFX_EXT_API void WINAPI InitTestDLL2();
這個函數用於初始化動態連接庫和註冊類:
// Exported DLL initialization is run in context of running application
extern "C" void WINAPI InitTestDLL2()
{
// create a new CDynLinkLibrary for this app
new CDynLinkLibrary(extensionDLL);
// nothing more to do
}
另外,源文件中還需要提供一個DllMain函數:
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
這個函數作用與前面的DllEntryPoint類似。
擴展類庫也需要一個DEF文件,這個文件包含了動態連接庫中可用的函數信息。由於現在動態連接庫包含的是類,因此在函數命名上與用戶動態連接庫有所不同。
EXPORTS
?AddString@CListOutputFrame@@QAAXPBD@Z
??_7CListOutputFrame@@6B@
??_GCListOutputFrame@@UAAPAXI@Z
?OnEditCut@CListOutputFrame@@IAAXXZ
?_messageEntries@CListOutputFrame@@0QBUAFX_MSGMAP_ENTRY@@B
??0CListOutputFrame@@QAA@XZ
??1CListOutputFrame@@UAA@XZ
?Clear@CListOutputFrame@@QAAXXZ
?OnEditClear@CListOutputFrame@@IAAXXZ
?OnEditCopy@CListOutputFrame@@IAAXXZ
InitTestDLL2
......
有關函數名擴展的技術參考Visual C++幫助文檔。
使用擴展類動態連接庫
要使用擴展類庫,要將類庫的頭文件包含在工程中。然後在適當位置初始化類庫,DLLHusk是在InitInstance中完成這一工作的。
BOOL CHuskApp::InitInstance()
{
//...
InitTestDLL1();
InitTestDLL2();
//...
}
然後就可以象使用普通MFC類一樣使用擴展類庫中定義的類了。
m_pListOut=new CListOutputFrame;
訪問DLL中的資源
當應用程序使用資源時,它按以下順序查找資源:首先查找應用程序本身,看有沒有對應的資源;如果沒有,查找MFC400.DLL(或MFC400D.DLL,它包含調試信息)。再查找應用程序所帶的動態連接庫中的資源。如果想在DLL中直接使用資源而不經過以上搜索順序,可以使用AfxGetResouceHandle()和AfxSetResourceHandle()函數。
AfxGetResourceHandle()和AfxSetResouceHandle()函數分別用來保存舊的資源句柄和設置新的資源句柄。比如,要想直接從DLL中載入一個位圖資源,可以這麼調用:
CBitmap mybitmap;
HINSTANCE hInstOld=AfxGetResourceHandle()
AfxSetResouceHandler(extensionDLL.hModule);
if(!mybitmap.LoadBitmap(IDR_BITMAP));
{
//restore the old resouce chain and return error
AfxSetResouceHandle(hInstOld);
return FALSE;
}
AfxSetResouceHandle(hInstOld);
//use this bitmap...
return TRUE;
還可以使用FindResource()搜索資源表,尋找給定的資源。
HRSRC FindResource(
HMODULE hModule,
LPCTSTR lpName,
LPCTSTR lpType
);
FindResource帶三個參數,第一個參數是模塊句柄,第二個是要查找的資源名字,如“MYDIALOG”,第三個是資源類型,可參見Visual C++文檔。如果查找成功,則返回該資源句柄。可以用LoadResouce以該句柄爲參數裝入資源.
小 結
本章介紹了用戶模塊的創建和使用。
用戶模塊是由用戶自己開發的、可以加入到最終用戶應用程序中提供某一特定功能的函數和類的集合。
用戶模塊包括靜態連接庫和動態連接庫兩大類:靜態連接庫將函數的目標代碼直接連入到應用程序中;動態連接庫只是給出函數入口信息,在調用時訪問DLL文件中函數的目標代碼。
創建靜態連接庫:指定工程類型爲Win32 Static Library,加入函數聲明和定義,並編譯和連接。提交函數庫時只需要提供函數的lib文件和頭文件。要使用靜態庫,可以將函數庫和頭文件包含在工程文件中。
創建動態連接庫:提供函數定義、聲明以及包含DLL文件函數信息的DEF文件。使用時需要將DLL文件拷貝至適當目錄下。
兩類動態連接庫的創建:用戶動態連接庫和MFC擴展庫。