在DLL中使用資源
現在最常看見的關於DLL的問題就是如何在DLL中使用對話方塊,這是一個很普遍的關於如何在DLL中使用資源的問題。這裡我們從Win32 DLL和MFC DLL兩個方面來分析並解決這個問題。
1.Win32 DLL
在Win32 DLL中使用對話方塊很簡單,你只需要在你的DLL中添加對話方塊資源,而且可以在對話方塊上面設定你所需要的控制項。然後使用DialogBox或者CreateDialog這兩個函數(或相同作用的其它函數)來建立對話方塊,並定義你自己的對話方塊回呼函數處理對話方塊收到的訊息。下面通過一個具體執行個體來學習如何在Win32 DLL中使用對話方塊,可以按照以下步驟來完成這個例子:
1)在VC菜單中File->New建立一個命名為UseDlg的Win32 Dynamic-Link Library工程,下一步選擇A simple DLL project。
2)在VC菜單中Insert->Resource添加一個ID為IDD_DLG_SHOW的Dialog資源,將此Dialog上的Cancel按鈕去掉,僅保留OK按鈕。再添加一個ID為IDD_ABOUTBOX的對話方塊,其Caption為About。儲存此資源,將資源檔命名為UseDlg.rc。並將resource.h和UseDlg.rc加入到工程裡面。
3)在UseDlg.app中包含resource.h,並添加如下代碼:
HINSTANCE hinst = NULL;
HWND hwndDLG = NULL;
BOOL CALLBACK DlgProc(HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam);
BOOL CALLBACK AboutProc(HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam);
extern "C" __declspec(dllexport) void ShowDlg();
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
hinst = (HINSTANCE)hModule;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
extern "C" __declspec(dllexport) void ShowDlg()
{
hwndDLG = CreateDialog(hinst,MAKEINTRESOURCE(IDD_DLG_SHOW),
NULL,(DLGPROC)DlgProc);
ShowWindow(hwndDLG, SW_SHOW);
}
BOOL CALLBACK DlgProc(HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_INITDIALOG:
return TRUE;
case WM_COMMAND:
if(LOWORD(wParam)==IDOK)
DialogBox(hinst,MAKEINTRESOURCE(IDD_ABOUTBOX),
hDlg,(DLGPROC)AboutProc);
return TRUE;
case WM_CLOSE:
DestroyWindow(hDlg);
hwndDLG = NULL;
return TRUE;
}
return FALSE;
}
BOOL CALLBACK AboutProc(HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_CLOSE:
EndDialog(hDlg,NULL);
hwndDLG = NULL;
return TRUE;
}
return FALSE;
}
4)編譯產生UseDlg.dll和UseDlg.lib。
接下來我們建立調用此DLL的應用程式,其步驟如下:
1)在VC菜單中File->New建立一個命名為Use的MFC AppWizard(exe)工程,下一步選擇Dialog Based之後點擊Finish按鈕。
2)在主對話方塊上面添加一個按鈕,之後雙擊此按鈕,會彈出Add Member Function的對話方塊,直接點擊OK進入void CUseDlg::OnButton1()函數。並在此函數內添加一個函數調用:ShowDlg();。
3)緊跟在#include語句後面加上如下代碼:
extern "C" __declspec(dllexport) void ShowDlg();
#pragma comment(lib,"debug/UseDlg")
4)將上面UseDlg工程中產生的UseDlg.dll和UseDlg.lib兩個檔案複製到Use工程的Debug目錄內。
5)編譯產生Use.exe。
運行Use.exe,點擊Button1按鈕,可以看到一個名稱為Dialog的非模態對話方塊彈出。點擊上面的按鈕,可以彈出模態對話方塊About。運行成功。
讓我們來回顧一下在Win32 DLL中使用對話方塊的過程。
在DLL中,我們定義了兩個對話方塊資源:IDD_DLG_SHOW和IDD_ABOUTBOX,並且匯出了函數ShowDlg。在函數ShowDlg之中使用CreateDialog函數建立了非模態對話方塊IDD_DLG_SHOW,並指定了該對話方塊的回呼函數DlgProc。在DlgProc之中處理了WM_INITDIALOG、WM_COMMAND和WM_CLOSE訊息,以響應使用者對對話方塊所做的動作。在處理按鈕動作的時候,使用DialogBox函數建立IDD_ABOUTBOX這個模態對話方塊,指定其回呼函數為AboutProc,並且在AboutProc中處理其相應訊息。
在EXE中,我們使用隱式連結的方法來調用DLL,並使用DLL中匯出的ShowDlg函數來調用DLL中的對話方塊。
在Win32 DLL中使用對話方塊就是這麼簡單,下面讓我們來看一下在MFC DLL中如何使用對話方塊。
2.MFC DLL
在MFC DLL中使用對話方塊不像Win32 DLL中那麼簡單,主要是因為MFC程式中存在一個模組狀態(Module State)的問題,也就是資源重複的問題。(此處的術語模組是指一個可執行程式,或指其操作不依賴於應用程式的其餘部分但使用MFC運行庫的共用副本的一個DLL(或一組DLL)。我們所建立的MFC DLL就是這種模組的一個典型執行個體。)
在每個模組(EXE或DLL)中,都存在一種全域的狀態資料,MFC依靠這種全域的狀態資料來區分不同的模組,以執行正確的操作。這種資料包括:Windows執行個體控制代碼(用於載入資源),指嚮應用程式當前的CWinApp和CWinThread對象的指標,OLE模組引用計數,以及維護Windows物件控點與相應的MFC對象執行個體之間串連的各種映射等。但當應用程式使用多個模組時,每個模組的狀態資料不是應用程式範圍的。相反,每個模組具有自已的MFC狀態資料的私人副本。這種全域的狀態資料就叫做MFC模組狀態。
模組的狀態資料包含在結構中,並且總是可以通過指向該結構的指標使用。當代碼在執行時進入了某一個模組時,只有此模組的狀態為“當前”或“有效”狀態時,MFC才能正確的區分此模組並執行正確的操作。
例如,MFC應用程式可以使用下面代碼從資源檔中載入字串:
CString str;
str.LoadString(IDS_MYSTRING);
使用這種代碼非常方便,但它掩蓋了這樣一個事實:即此程式中IDS_MYSTRING可能不是唯一的標識符。一個程式可以載入多個DLL,某些DLL可能也用IDS_MYSTRING標識符定義了一個資源。MFC怎樣知道應該載入哪個資源呢?MFC使用當前模組狀態尋找資源控制代碼。如果當前模組不是我們要使用的正確模組,那麼就會產生不正確的調用或者錯誤。
按照MFC庫的連結方法,一個MFC DLL有兩種使用MFC庫的方法:靜態連結到MFC的DLL和動態連結到MFC的DLL。下面我們就按照這兩種類型的MFC DLL來介紹如何切換當前模組狀態以正確的在MFC DLL中使用資源。
1、靜態連結到MFC的DLL
靜態連結到MFC的規則DLL與MFC庫靜態連結,則此時MFC庫不能共用,所以MFC總是使用它所連結的DLL的模組狀態。這樣也就不存在管理模組狀態的問題。但使用這種方法的缺點是DLL程式將會變大,而且會在程式中留下重複代碼。下面給出的例子驗證了這一點。本例可以按照以下步驟來完成:
1)在VC菜單中File->New建立一個命名為DLLStatic的MFC AppWizard的工程,下一步選擇Regular DLL with MFC statically linked。
2)在工程中添加一個對話方塊資源,其ID為:IDD_ABOUTBOX。並在resource.h之中將IDD_ABOUTBOX 的數值改為100。
3)在DLLStatic.cpp中定義如下函數:
void ShowDlg()
{
CDialog dlg(IDD_ABOUTBOX);
dlg.DoModal();
}
4)在DLLStatic.def檔案中的EXPORTS語句中添加一行:ShowDlg,以匯出ShowDlg函數。
5)編譯產生DLLStatic.dll和DLLStatic.lib。
繼續使用上一節中的Use工程,將前面產生的DLLStatic.dll和DLLStatic.lib兩個檔案複製到工程的Debug目錄內,並將
extern "C" __declspec(dllexport) void ShowDlg();
#pragma comment(lib,"debug/UseDlg")
這兩行改為:
void ShowDlg();
#pragma comment(lib,"debug/DLLStatic")
編譯並運行Use.exe。點擊按鈕,可以看到DLLStatic中的模態對話方塊彈出。
本例中,可以注意到DLL中所定義的About對話方塊資源與EXE中所定義的About對話方塊資源ID完全相同,但是當我們點擊Use.exe上面的按鈕時,彈出的是DLL中的模態對話方塊。說明,當使用靜態連結到MFC的規則DLL時,不存在管理模組狀態的問題。
2、動態連結到MFC的DLL
在討論關於動態連結到MFC的DLL的模組狀態問題之前,先來看一個例子。本例可以通過如下步驟來完成:
1)在VC菜單中File->New建立一個命名為DLLShared的MFC AppWizard的工程,下一步選擇Regular DLL using shared MFC DLL。
2)在工程中添加一個對話方塊資源,其ID為:IDD_ABOUTBOX。並在resource.h之中將IDD_ABOUTBOX 的數值改為100。
3)在DLLShared.cpp中定義如下函數:
void ShowDlg()
{
CDialog dlg(IDD_ABOUTBOX);
dlg.DoModal();
}
4)在DLLShared.def檔案中的EXPORTS語句中添加一行:ShowDlg,以匯出ShowDlg函數。
5)編譯產生DLLShared.dll和DLLShared.lib。
繼續使用上面的Use工程,將前面產生的DLLShared.dll和DLLShared.lib兩個檔案複製到工程的Debug目錄內,並將
extern "C" __declspec(dllexport) void ShowDlg();
#pragma comment(lib,"debug/DLLStatic")
這兩行改為:
void ShowDlg();
#pragma comment(lib,"debug/DLLShared")
編譯並運行Use.exe。點擊按鈕,這次你看到了什嗎?對,沒錯,這次彈出的是Use.exe的關於對話方塊。將上述例子的DLL類型換成MFC Extension DLL(using shared MFC DLL)也會出現相同的問題。
為什麼會出現上面的問題?這是因為在使用了MFC共用庫的時候,預設情況下,MFC使用主應用程式的資源控制代碼來載入資源模板。雖然我們調用的是DLL中的函數來顯示DLL中的對話方塊,並且對應的對話方塊模板是儲存在DLL中的,但MFC仍舊在主應用程式也就是Use.exe中尋找相應的對話方塊模板。由於在DLL中所定義的對話方塊資源ID與主應用程式中所定義的關於對話方塊的資源ID相同,所以MFC就把主應用程式中的關於對話方塊顯示了出來。如果二者不同,則MFC就認為DLL中所定義的對話方塊資源不存在,dlg.DoModal會返回0,也就是什麼都不會顯示。
那麼如何解決上述問題呢?解決辦法就是在適當的時候進行模組狀態切換,以保證具有目前狀態的模組是我們所需要的模組從而使用正確的資源。MFC提供了下列函數和宏來完成這些工作:
AfxGetStaticModuleState:這是一個函數,其函數原型為:
AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState( );
此函數在堆棧上構造AFX_MODULE_STATE類的執行個體pModuleState並對其賦值後將其返回。在AFX_MODULE_STATE類的建構函式中,該類擷取指向當前模組狀態的指標並將其儲存在成員變數中,然後將pModuleState設定為新的有效模組狀態。在它的解構函式中,該類將儲存在其成員變數中的指標還原為存貯的前一個模組狀態。
AFX_MANAGE_STATE:這是一個宏,其原型為:
AFX_MANAGE_STATE( AFX_MODULE_STATE* pModuleState )
該宏用於將pModuleState(指向包含模組全域資料也就是模組狀態的AFX_MODULE_STATE結構的指標)設定為當前的即時作用空間中(the remainder of the immediate containing scope)的有效模組狀態。在離開包含該宏的作用空間時,前一個有效模組狀態自動還原。
AfxGetResourceHandle:這個函數的原型為:
HINSTANCE AfxGetResourceHandle( );
該函數返回了一個儲存了HINSTANCE類型的、應用程式預設所載入資源的模組的控制代碼。
AfxSetResourceHandle:這個函數的原型為:
void AfxSetResourceHandle( HINSTANCE hInstResource );
該函數將hInstResource所代表的模組設定為具有目前狀態的模組。
通過使用上述四個函數或宏就可以正確的在動態連結到MFC的DLL中切換模組狀態。接下來我們將通過修改上面出現問題的那個例子來介紹如何使用上述四個函數或宏。先來看看Regular DLL using shared MFC DLL類型:
在上述例子的第三步的ShowDlg函數的第一條語句前加上如下語句(要確保該語句在函數實現的第一行):
AFX_MANAGE_STATE(AfxGetStaticModuleState());
之後重新編譯產生DLLShared.dll和DLLShared.lib,並將這兩個檔案重新拷貝到Use工程的Debug目錄內。這次編譯產生Use.exe並運行,點擊按鈕,可以看到彈出的時我們在DLL中所加入的那個對話方塊,而不再是Use.exe的關於對話方塊了。
通過上面的講解,相信你已經知道該語句的作用了。在函數ShowDlg的第一行加上這麼一句後,每次調用DLL的應用程式使用該函數的時候,MFC庫都會自動切換當前模組狀態,這樣就保證了資源讀取的正確性。
AFX_MANAGE_STATE(AfxGetStaticModuleState());是自動切換當前模組狀態,也可以通過使用AfxGetResourceHandle和AfxSetResourceHandle來手動切換當前模組狀態。具體使用方法如下:
在上述例子的第三步的ShowDlg函數的第一條語句前加上如下語句(要確保該語句在函數實現的第一行):
HINSTANCE save_hInstance = AfxGetResourceHandle();
AfxSetResourceHandle(theApp.m_hInstance);
在調用對話方塊成功之後,也就是dlg.DoModal();之後,添加:
AfxSetResourceHandle(save_hInstance);
這種方法在進入ShowDlg函數之後,通過AfxGetResourceHandle來獲得並儲存目前狀態模組的控制代碼。然後獲得DLL模組的控制代碼theApp.m_hInstance(當然,也可以使用GetModuleHandle函數來獲得DLL模組的控制代碼),並使用AfxSetResourceHandle函數來將其設定為目前狀態狀態。最後在調用對話方塊成功之後再用恢複AfxSetResourceHandle資源控制代碼,將當前模組狀態恢複。
這樣做有些麻煩,但是有一點好處是可以在完成使用資源的任務之後就可以立即恢複資源控制代碼。而AFX_MANAGE_STATE(AfxGetStaticModuleState());的方法只能等函數的作用空間結束之後才恢複資源控制代碼。由於可執行檔必須重畫工具條等原因,因此建議只要有可能就必須恢複資源控制代碼,否則可能會遇到許多問題。比如說,如果使用者移動DLL的對話方塊,而此時資源控制代碼仍然為DLL的資源,那麼程式就會崩潰。最好的恢複控制代碼的時機在對話方塊響應WM_INITDIALOG訊息的時候,因為這時對話方塊的模板等已經讀出了。
對於MFC Extension DLL(using shared MFC DLL)類型的MFC DLL,切換當前模組狀態的方法與Regular DLL using shared MFC DLL類型的MFC DLL所使用的方法很相似,這裡不再舉例實現。二者不同的地方如下:
在MFC擴充DLL中使用AFX_MANAGE_STATE(AfxGetStaticModuleState());時,會產生如下錯誤:
mfcs42d.lib(dllmodul.obj) : error LNK2005: __pRawDllMain already defined in dllextend.obj
mfcs42d.lib(dllmodul.obj) : error LNK2005: _DllMain@12 already defined in dllextend.obj
mfcs42d.lib(dllmodul.obj) : error LNK2005: __pRawDllMain already defined in dllextend.obj
因此在MFC擴充DLL中需要將AFX_MANAGE_STATE(AfxGetStaticModuleState());換成AFX_MANAGE_STATE(AfxGetAppModuleState());才能正確切換當前模組狀態。
在MFC擴充DLL中使用AfxGetResourceHandle和AfxSetResourceHandle的方法與在Regular DLL using shared MFC DLL類型的MFC DLL中所使用的方法相同。並且,DLL模組的控制代碼可以通過MFC提供的DlgextentDLL這個結構的hModule成員來獲得。即使用AfxSetResourceHandle(DlgextentDLL.hModule);語句。
當然,對於動態連結到MFC的DLL,也可以在調用該DLL的MFC應用程式中使用AfxGetResourceHandle和AfxSetResourceHandle兩個函數來切換目前狀態模組。該DLL模組的控制代碼可以用GetModuleHandle函數來獲得。在此不再贅述。