动态链接库 |
|
动态链接库(也称为DLL)是Microsoft Windows最重要的组成要素之一。大多数与Windows相关的磁盘文件如果不是程序模块,就是动态链接程序。迄今为止,我们都是在开发Windows应用程序;现在是尝试编写动态链接库的时候了。许多您已经学会的编写应用程序的规则同样适用于编写这些动态链接库模块,但也有一些重要的不同。
动态链接库的基本知识
正如前面所看到的,Windows应用程序是一个可执行文件,它通常建立一个或几个窗口,并使用消息循环接收使用者输入。通常,动态链接库并不能直接执行,也不接收消息。它们是一些独立的文件,其中包含能被程序或其它DLL呼叫来完成一定作业的函数。只有在其它模块呼叫动态链接库中的函数时,它才发挥作用。
所谓「动态链接」,是指Windows把一个模块中的函数呼叫连结到动态链接库模块中的实际函数上的程序。在程序开发中,您将各种目标模块(.OBJ)、执行时期链接库(.LIB)文件,以及经常是已编译的资源(.RES)文件连结在一起,以便建立Windows的.EXE文件,这时的连结是「静态连结」。动态链接与此不同,它发生在执行时期。
KERNEL32.DLL、USER32.DLL和GDI32.DLL、各种驱动程序文件如KEYBOARD.DRV、SYSTEM.DRV和MOUSE.DRV和视讯及打印机驱动程序都是动态链接库。这些动态链接库能被所有Windows应用程序使用。
有些动态链接库(如字体文件等)被称为「纯资源」。它们只包含数据(通常是资源的形式)而不包含程序代码。由此可见,动态链接库的目的之一就是提供能被许多不同的应用程序所使用的函数和资源。在一般的操作系统中,只有操作系统本身才包含其它应用程序能够呼叫来完成某一作业的例程。在Windows中,一个模块呼叫另一个模块函数的程序被推广了。结果使得编写一个动态链接库,也就是在扩充Windows。当然,也可认为动态链接库(包括构成Windows的那些动态链接库例程)是对使用者程序的扩充。
尽管一个动态链接库模块可能有其它扩展名(如.EXE或.FON),但标准扩展名是.DLL。只有带.DLL扩展名的动态链接库才能被Windows自动加载。如果文件有其它扩展名,则程序必须另外使用LoadLibrary或者LoadLibraryEx函数加载该模块。
您通常会发现,动态链接库在大型应用程序中最有意义。例如,假设要为Windows编写一个由几个不同的程序组成的大型财务软件包,就会发现这些应用程序会使用许多共同的例程。可以把这些公共例程放入一个一般性的目的码链接库(带.LIB扩展名)中,并在使用LINK静态连结时把它们加入各程序模块中。但这种方法是很浪费的,因为软件包中的每个程序都包含与公共例程相同的程序代码。而且,如果修改了链接库中的某个例程,就要重新连结使用此例程的所有程序。然而,如果把这些公共例程放到称为ACCOUNT.DLL的动态链接库中,就可解决这两个问题。只有动态链接库模块才包含所有程序都要用到的例程。这样能为储存文件节省磁盘空间,并且在同时执行多个应用程序时节省内存,而且,可以修改动态链接库模块而不用重新连结各个程序。
动态链接库实际上是可以独立存在的。例如,假设您编写了一系列3D绘图例程,并把它们放入名为GDI3.DLL的DLL中。如果其它软件开发者对此链接库很感兴趣,您就可以授权他们将其加入他们的图形程序中。使用多个这样的图形程序的使用者只需要一个GDI3.DLL文件。
链接库:一词多义
动态链接库有着令人困惑的印象,部分原因是由于「链接库」这个词被放在几种不同的用语之后。除了动态链接库之外,我们也用它来称呼「目的码链接库」或「引用链接库」。
目的码链接库是带.LIB扩展名的文件。在使用连结程序进行静态连结时,它的程序代码就会加到程序的.EXE文件中。例如,在Microsoft Visual C++中,连同程序连结的一般C执行目的码链接库被称为LIBC.LIB。
引用链接库是目的码链接库文件的一种特殊形式。像目的码链接库一样,引用链接库有.LIB扩展名,并且被连结器用来确定程序代码中的函数呼叫来源。但引用链接库不含程序代码,而是为连结程序提供信息,以便在.EXE文件中建立动态链接时要用到的复位位表。包含在Microsoft编译器中的KERNEL32.LIB、USER32.LIB和GDI32.LIB文件是Windows函数的引用链接库。如果一个程序呼叫Rectangle函数,Rectangle将告诉LINK,该函数在GDI32.DLL动态链接库中。该信息被记录在.EXE文件中,使得程序执行时,Windows能够和GDI32.DLL动态链接库进行动态连结。
目的码链接库和引用链接库只用在程序开发期间使用,而动态链接库在执行期间使用。当一个使用动态链接库的程序执行时,该动态链接库必须在磁盘上。当Windows要执行一个使用了动态链接库的程序而需要加载该链接库时,动态链接库文件必须储存在含有该.EXE程序的目录下、目前的目录下、Windows系统目录下、Windows目录下,或者是在通过MS-DOS环境中的PATH可以存取到的目录下(Windows会按顺序搜索这些目录)。
一个简单的DLL
虽然动态链接库的整体概念是它们可以被多个应用程序所使用,但您通常最初设计的动态链接库只与一个应用程序相联系,可能是一个「测试」程序在使用DLL。
下面就是我们要做的。我们建立一个名为EDRLIB.DLL的DLL。文件名中的「EDR」代表「简便的绘图例程(easy drawing routines)」。这里的EDRLIB只含有一个函数(名称为EdrCenterText),但是您还可以将应用程序中其它简单的绘图函数添加进去。应用程序EDRTEST.EXE将通过呼叫EDRLIB.DLL中的函数来利用它。
要做到这一点,需要与我们以前所做的略有不同的方法,也包括Visual C++ 中我们没有看过的特性。在Visual C++ 中「工作空间(workspaces)」和「项目(projects)」不同。项目通常与建立的应用程序(.EXE)或者动态链接库(.DLL)相联系。一个工作空间可以包含一个或多个项目。迄今为止,我们所有的工作空间都只包含一个项目。我们现在就建立一个包含两个项目的工作空间EDRTEST-一个用于建立EDRTEST.EXE,而另一个用于建立EDRLIB.DLL,即EDRTEST使用的动态链接库。
现在就开始。在Visual C++中,从「File」菜单选择「New」,然后选择「Workspaces」页面标签。(我们以前从来没有选择过。)在「Location」栏选择工作空间要储存的目录,然后在「Workspace Name」栏输入「EDRTEST」,按Enter键。
这样就建立了一个空的工作空间。Developer Studio还建立了一个名为EDRTEST的子目录,以及工作空间文件EDRTEST.DSW(就像两个其它文件)。
现在让我们在此工作空间里建立一个项目。从「File」菜单选择「New」,然后选择「Projects」页面标签。尽管过去您选择「Win32 Application」,但现在「Win32 Dynamic-Link Library」。另外,单击单选按钮「Add To Current Workspace」,这使得此项目是「EDRTEST」工作空间的一部分。在「Project Name栏输入EDRLIB,但先不要按「OK」按钮。当您在Project Name栏输入EDRLIB时,Visual C++将改变「Location」栏,以显示EDRLIB作为EDRTEST的一个子目录。这不是我们要的,所以接着在「Location」栏删除EDRLIB子目录以便项目建立在EDRTEST目录。现在按「OK」。屏幕将显示一个对话框,询问您建立什么型态的DLL。选择「An Empty DLL Project」,然后按「Finish」。Visual C++将建立一个项目文件EDRLIB.DSP和一个构造文件EDRLIB.MAK(如果「Tools Options」对话框的B「uild页面卷标中选择了「Export Makefile」选项」。
现在您已经在此项目中添加了一对文件。从「File」菜单选择「New」,然后选择「Files」页面标签。选择「C/C++ Header File」,然后输入文件名EDRLIB.H。输入程序21-1所示的文件(或者从本书光盘中复制)。再次从「File」菜单中选择「New」,然后选择「Files」页面标签。这次选择「C++ Source File」,然后输入文件名EDRLIB.C。继续输入程序21-1所示的程序。
EDRLIB.H /*-------------------------------------------------------------------------- EDRLIB.H header file ----------------------------------------------------------------------------*/ #ifdef __cplusplus #define EXPORT extern "C" __declspec (dllexport) #else #define EXPORT __declspec (dllexport) #endif EXPORT BOOL CALLBACK EdrCenterTextA (HDC, PRECT, PCSTR) ; EXPORT BOOL CALLBACK EdrCenterTextW (HDC, PRECT, PCWSTR) ; #ifdef UNICODE #define EdrCenterText EdrCenterTextW #else #define EdrCenterText EdrCenterTextA #endif EDRLIB.C /*--------------------------------------------------------------------------- EDRLIB.C -- Easy Drawing Routine Library module (c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include windows.h> #include "edrlib.h" int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { return TRUE ; } EXPORT BOOL CALLBACK EdrCenterTextA ( HDC hdc, PRECT prc, PCSTR pString) { int iLength ; SIZE size ; iLength = lstrlenA (pString) ; GetTextExtentPoint32A (hdc, pString, iLength, &size) ; return TextOutA (hdc,(prc->right - prc->left - size.cx) / 2, ( prc->bottom - prc->top - size.cy) / 2, pString, iLength) ; } EXPORT BOOL CALLBACK EdrCenterTextW (HDC hdc, PRECT prc, PCWSTR pString) { int iLength ; SIZE size ; iLength = lstrlenW (pString) ; GetTextExtentPoint32W (hdc, pString, iLength, &size) ; return TextOutW (hdc, ( prc->right - prc->left - size.cx) / 2, ( prc->bottom - prc->top - size.cy) / 2, pString, iLength) ; }
这里您可以按Release设定,或者也可以按Debug设定来建立EDRLIB.DLL。之后,RELEASE和DEBUG目录将包含EDRLIB.LIB(即动态链接库的引用链接库)和EDRLIB.DLL(动态链接库本身)。
纵观全书,我们建立的所有程序都可以根据UNICODE标识符来编译成使用Unicode或非Unicode字符串的程序代码。当您建立一个DLL时,它应该包括处理字符和字符串的Unicode和非Unicode版的所有函数。因此,EDRLIB.C就包含函数EdrCenterTextA(ANSI版)和EdrCenterTextW(宽字符版)。EdrCenterTextA定义为带有参数PCSTR(指向const字符串的指针),而EdrCenterTextW则定义为带有参数PCWSTR(指向const宽字符串的指针)。EdrCenterTextA函数将呼叫lstrlenA、GetTextExtentPoint32A和TextOutA。EdrCenterTextW将呼叫lstrlenW、GetTextExtentPoint32W和TextOutW。如果定义了UNICODE标识符,则EDRLIB.H将EdrCenterText定义为EdrCenterTextW,否则定义为EdrCenterTextA。这样的做法很像Windows表头文件。
EDRLIB.H也包含函数DllMain,取代了DLL中的WinMain。此函数用于执行初始化和未初始化(deinitialization),我将在下一节讨论。我们现在所需要的就是从DllMain传回TRUE。
在这两个文件中,最后一点神秘之处就是定义了EXPORT标识符。DLL中应用程序使用的函数必须是「输出(exported)」的。这跟税务或者商业制度无关,只是确保函数名添加到EDRLIB.LIB的一个关键词(以便连结程序在连结使用此函数的应用程序时,能够解析出函数名称),而且该函数在EDRLIB.DLL中也是看得到的。EXPORT标识符包括储存方式限定词__declspec(dllexport)以及在表头文件按C++模式编译时附加的「C」。这将防止编译器使用C++的名称轧压规则(name mangling)来处理函数名称,使C和C++程序都能使用这个DLL。
链接库入口/出口点
当动态链接库首次启动和结束时,我们呼叫了DllMain函数。DllMain的第一个参数是链接库的执行实体句柄。如果您的链接库使用需要执行实体句柄(诸如DialogBox)的资源,那么您应该将hInstance储存为一个整体变量。DllMain的最后一个参数由系统保留。
fdwReason参数可以是四个值之一,说明为什么Windows要呼叫DllMain函数。在下面的讨论中,请记住一个程序可以被加载多次,并在Windows下一起执行。每当一个程序加载时,它都被认为是一个独立的程序(process)。
fdwReason的一个值DLL_PROCESS_ATTACH表示动态链接库被映像到一个程序的地址空间。链接库可以根据这个线索进行初始化,为以后来自该程序的请求提供服务。例如,这类初始化可能包括内存配置。在一个程序的生命周期内,只有一次对DllMain的呼叫以DLL_PROCESS_ATTACH为参数。使用同一DLL的其它任何程序都将导致另一个使用DLL_PROCESS_ATTACH参数的DllMain呼叫,但这是对新程序的呼叫。
如果初始化成功,DllMain应该传回一个非0值。传回0将导致Windows不执行该程序。
当fdwReason的值为DLL_PROCESS_DETACH时,意味着程序不再需要DLL了,从而提供给链接库自己清除自己的机会。在32位的Windows下,这种处理并不是严格必须的,但这是一种良好的程序写作习惯。
类似地,当以DLL_THREAD_ATTACH为fdwReason参数呼叫DllMain时,意味着某个程序建立了一个新的线程。当线程中止时,Windows以DLL_THREAD_DETACH为fdwReason参数呼叫DllMain。请注意,如果动态链接库是在线程被建立之后和一个程序连结的,那么可能会得到一个没有事先对应一个DLL_THREAD_ATTACH呼叫的DLL_THREAD_DETACH呼叫。
当使用一个DLL_THREAD_DETACH参数呼叫DllMain时,线程仍然存在。动态链接库甚至可以在这个程序期间发送线程消息。但是它不应该使用PostMessage,因为线程可能在此消息被处理到之前就已经退出执行了。
测试程序
现在让我们在EDRTEST工作空间里建立第二个项目,程序名称为EDRTEST,而且使用EDRLIB.DLL。在Visual C++中加载EDRTEST工作空间时,请从「File」菜单选择「New」,然后在「New」对话框中选择「Projects」页面标签。这次选择「Win32 Application」,并确保选中了「Add To Current Workspace」按钮。输入项目名称EDRTEST。再在「Locations」栏删除第二个EDRTEST子目录。按下「OK」,然后在下一个对话框选择「An Empty Project」,按「Finish」。
从「File」菜单再次选择「New」。选择「Files」页面标签然后选择「C++ Source File」。确保「Add To Project」清单方块显示「EDRTEST」而不是「EDRLIB」。输入文件名称EDRTEST.C,然后输入程序21-2所示的程序。此程序用EdrCenterText函数将显示区域中的字符串居中对齐。
EDRTEST.C /*--------------------------------------------------------------------------- EDRTEST.C -- Program using EDRLIB dynamic-link library (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "edrlib.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("StrProg") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("DLL Demonstration Program"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; EdrCenterText (hdc, &rect, TEXT ("This string was displayed by a DLL")) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
注意,为了定义EdrCenterText函数,EDRTEST.C包括EDRLIB.H表头文件,此函数将在WM_PAINT消息处理期间呼叫。
在编译此程序之前,您可能希望做以下几件事。首先,在「Project」菜单选择「Select Active Project」。这时您将看到「EDRLIB」和「EDRTEST」,选择「EDRTEST」。在重新编译此工作空间时,您真正要重新编译的是程序。另外,在「Project」菜单中,选择「Dependencies」,在「Select Project To Modify」清单方块中选择「EDRTEST」。在「Dependent On The Following Project(s)」列表选中「EDRLIB」。此操作的意思是:EDRTEST需要EDRLIB动态链接库。以后每次重新编译EDRTEST时,如果必要的话,都将在编译和连结EDRTEST之前重新重新编译EDRLIB。
从「Project」菜单选择「Settings」,单击「General」标签。当您在左边的窗格中选择「EDRLIB」或者「EDRTEST」项目时,如果设定为「Win32 Release」,则显示在右边窗格中的「Intermediate Files」和「Output Files」将位于RELEASE目录;如果设定为「Win32 Debug」,则位于DEBUG目录。如果不是,请按此修改。这样可确保EDRLIB.DLL与EDRTEST.EXE在同一个目录中,而且程序在使用DLL时也不会产生问题。
在「Project Setting」对话框中依然选中「EDRTEST」,单击「C/C++」页面标签。按本书的惯例,在「Preprocessor Definitions」中,将「UNICODE」添加到Debug设定。
现在您就可以在「Debug」或「Release」设定中重新编译EDRTEST.EXE了。必要时,Visual C++将首先编译和连结EDRLIB。RELEASE和DEBUG目录都包含EDRLIB.LIB(引用链接库)和EDRLIB.DLL。当Developer Studio连结EDRTEST时,将自动包含引用链接库。
了解EDRTEST.EXE文件中不包含EdrCenterText程序代码很重要。事实上,要证明执行了EDRLIB.DLL文件和EdrCenterText函数很简单:执行EDRTEST.EXE需要EDRLIB.DLL。
执行EDRTEST.EXE时,Windows按外部链接库模块执行固定的函数。其中许多函数都在一般Windows动态链接库中。但Windows也看到程序从EDRLIB呼叫了函数,因此Windows将EDRLIB.DLL文件加载到内存,然后呼叫EDRLIB的初始化例程。EDRTEST呼叫EdrCenterText函数是动态链接到EDRLIB中函数的。
在EDRTEST.C原始码文件中包含EDRLIB.H与包含WINDOWS.H类似。连结EDRLIB.LIB与连结Windows引用链接库(例如USER32.LIB)类似。当您的程序执行时,它连结EDLIB.DLL的方式与连结USER32.DLL的方式相同。恭喜您!您已经扩展了Windows的功能!
在继续之前,我还要对动态链接库多说明一些:
首先,虽然我们将DLL作为Windows的延伸,但它也是您的应用程序的延伸。DLL所完成的每件工作对于应用程序来说都是应用程序所交代要完成的。例如,应用程序拥有DLL配置的全部内存、DLL建立的全部窗口以及DLL打开的所有文件。多个应用程序可以同时使用同一个DLL,但在Windows下,这些应用程序不会相互影响。
多个程序能够共享一个动态链接库中相同的程序代码。但是,DLL为每个程序所储存的数据都不同。每个程序都为DLL所使用的全部数据配置了自己的地址空间。我们将在下以节看到,共享内存需要额外的工作。
在DLL中共享内存
令人兴奋的是,Windows能够将同时使用同一个动态链接库的应用程序分开。不过,有时却不太令人满意。您可能希望写一个DLL,其中包含能够被不同应用程序或者同一个程序的不同例程所共享的内存。这包括使用共享内存。共享内存实际上是一种内存映像文件。
让我们测试一下,这项工作是如何在程序STRPROG(「字符串程序(string program)」)和动态链接库STRLIB(「字符串链接库(string library)」)中完成的。STRLIB有三个输出函数被STRPROG呼叫,我们只对此感兴趣,STRLIB中的一个函数使用了在STRPROG定义的callback函数。
STRLIB是一个动态链接库模块,它储存并排序了最多256个字符串。在STRLIB中,这些字符串均为大写,并由共享内存维护。利用STRLIB的三个函数,STRPROG能够添加字符串、删除字符串以及从STRLIB获得目前的所有字符串。STRPROG测试程序有两个菜单项(「Enter」和「Delete」),这两个菜单项将启动不同的对话框来添加或删除字符串。STRPROG在其显示区域列出目前储存在STRLIB中的所有字符串。
下面这个函数在STRLIB定义,它将一个字符串添加到STRLIB的共享内存。
EXPORT BOOL CALLBACK AddString (pStringIn)
参数pStringIn是字符串的指针。字符串在AddString函数中变成大写。如果在STRLIB的列表中有一个相同的字符串,那么此函数将添加一个字符串的复本。如果成功,AddString传回「TRUE」(非0),否则传回「FALSE」(0)。如果字符串的长度为0,或者不能配置储存字符串的内存,或者已经储存了256个字符串,则传回值将都是FALSE。
STRLIB函数从STRLIB的共享内存中删除一个字符串。
EXPORT BOOL CALLBACK DeleteString (pStringIn)
另外,参数pStringIn是一个字符串指针。如果有多个相同内容字符串,则删除第一个。如果成功,那么DeleteString传回「TRUE」(非0),否则传回「FALSE」(0)。传回「FALSE」表示字符串长度为0,或者找不到相同内容的字符串。
STRLIB函数使用了呼叫程序中的一个callback函数,以便列出目前储存在STRLIB共享内存中的字符串:
EXPORT int CALLBACK GetStrings (pfnGetStrCallBack, pParam)
在呼叫程序中,callback函数必须像下面这样定义:
EXPORT BOOL CALLBACK GetStrCallBack (PSTR pString, PVOID pParam)
GetStrings的参数pfnGetStrCallBack指向callback函数。直到callback函数传回「FALSE」(0),GetStrings将为每个字符串都呼叫一次GetStrCallBack。GetStrings传回传递给callback函数的字符串数。pParam参数是一个远程指针,指向程序写作者定义的数据。
当然,此程序可以编译成Unicode程序,或者在STRLIB的支持下,编译成Unicode和非Unicode应用程序。与EDRLIB一样,所有的函数都有「A」和「W」两种版本。在内部,STRLIB以Unicode储存所有的字符串。如果非Unicode程序使用了STRLIB(也就是说,程序将呼叫AddStringA、DeleteStringA和GetStringsA),字符串将在Unicode和非Unicode之间转换。
与STRPROG和STRLIB项目相关的工作空间名为STRPROG。此文件按EDRTEST工作空间的方式组合。程序21-3显示了建立STRLIB.DLL动态链接库所必须的两个文件。
STRLIB.H /*---------------------------------------------------------------------------- STRLIB.H header file -----------------------------------------------------------------------------*/ #ifdef __cplusplus #define EXPORT extern "C" __declspec (dllexport) #else #define EXPORT __declspec (dllexport) #endif // The maximum number of strings STRLIB will store and their lengths #define MAX_STRINGS 256 #define MAX_LENGTH 63 // The callback function type definition uses generic strings typedef BOOL (CALLBACK * GETSTRCB) (PCTSTR, PVOID) ; // Each function has ANSI and Unicode versions EXPORT BOOL CALLBACK AddStringA (PCSTR) ; EXPORT BOOL CALLBACK AddStringW (PCWSTR) ; EXPORT BOOL CALLBACK DeleteStringA (PCSTR) ; EXPORT BOOL CALLBACK DeleteStringW (PCWSTR) ; EXPORT int CALLBACK GetStringsA (GETSTRCB, PVOID) ; EXPORT int CALLBACK GetStringsW (GETSTRCB, PVOID) ; // Use the correct version depending on the UNICODE identifier #ifdef UNICODE #define AddString AddStringW #define DeleteString DeleteStringW #define GetStrings GetStringsW #else #define AddString AddStringA #define DeleteString DeleteStringA #define GetStrings GetStringsA #endif STRLIB.C /*--------------------------------------------------------------------------- STRLIB.C - Library module for STRPROG program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <wchar.h> // for wide-character string functions #include "strlib.h" // shared memory section (requires /SECTION:shared,RWS in link options) #pragma data_seg ("shared") int iTotal = 0 ; WCHAR szStrings [MAX_STRINGS][MAX_LENGTH + 1] = { '\0' } ; #pragma data_seg () #pragma comment(linker,"/SECTION:shared,RWS") int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { return TRUE ; } EXPORT BOOL CALLBACK AddStringA (PCSTR pStringIn) { BOOL bReturn ; int iLength ; PWSTR pWideStr ; // Convert string to Unicode and call AddStringW iLength = MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, NULL, 0) ; pWideStr = malloc (iLength) ; MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, pWideStr, iLength) ; bReturn = AddStringW (pWideStr) ; free (pWideStr) ; return bReturn ; } EXPORT BOOL CALLBACK AddStringW (PCWSTR pStringIn) { PWSTR pString ; int i, iLength ; if (iTotal == MAX_STRINGS - 1) return FALSE ; if ((iLength = wcslen (pStringIn)) == 0) return FALSE ; // Allocate memory for storing string, copy it, convert to uppercase pString = malloc (sizeof (WCHAR) * (1 + iLength)) ; wcscpy (pString, pStringIn) ; _wcsupr (pString) ; // Alphabetize the strings for (i = iTotal ; i > 0 ; i-) { if (wcscmp (pString, szStrings[i - 1]) >= 0) break ; wcscpy (szStrings[i], szStrings[i - 1]) ; } wcscpy (szStrings[i], pString) ; iTotal++ ; free (pString) ; return TRUE ; } EXPORT BOOL CALLBACK DeleteStringA (PCSTR pStringIn) { BOOL bReturn ; int iLength ; PWSTR pWideStr ; // Convert string to Unicode and call DeleteStringW iLength = MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, NULL, 0) ; pWideStr = malloc (iLength) ; MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, pWideStr, iLength) ; bReturn = DeleteStringW (pWideStr) ; free (pWideStr) ; return bReturn ; } EXPORT BOOL CALLBACK DeleteStringW (PCWSTR pStringIn) { int i, j ; if (0 == wcslen (pStringIn)) return FALSE ; for (i = 0 ; i < iTotal ; i++) { if (_wcsicmp (szStrings[i], pStringIn) == 0) break ; } // If given string not in list, return without taking action if (i == iTotal) return FALSE ; // Else adjust list downward for (j = i ; j < iTotal ; j++) wcscpy (szStrings[j], szStrings[j + 1]) ; szStrings[iTotal-][0] = '\0' ; return TRUE ; } EXPORT int CALLBACK GetStringsA (GETSTRCB pfnGetStrCallBack, PVOID pParam) { BOOL bReturn ; int i, iLength ; PSTR pAnsiStr ; for (i = 0 ; i < iTotal ; i++) { // Convert string from Unicode iLength = WideCharToMultiByte ( CP_ACP, 0, szStrings[i], -1, NULL, 0, NULL, NULL) ; pAnsiStr = malloc (iLength) ; WideCharToMultiByte ( CP_ACP, 0, szStrings[i], -1, pAnsiStr, iLength, NULL, NULL) ; // Call callback function bReturn = pfnGetStrCallBack (pAnsiStr, pParam) ; if (bReturn == FALSE) return i + 1 ; free (pAnsiStr) ; } return iTotal ; } EXPORT int CALLBACK GetStringsW (GETSTRCB pfnGetStrCallBack, PVOID pParam) { BOOL bReturn ; int i ; for (i = 0 ; i < iTotal ; i++) { bReturn = pfnGetStrCallBack (szStrings[i], pParam) ; if (bReturn == FALSE) return i + 1 ; } return iTotal ; }
除了DllMain函数以外,STRLIB中只有六个函数供其它函数输出用。所有这些函数都按EXPORT定义。这会使LINK在STRLIB.LIB引用链接库中列出它们。
STRPROG程序
STRPROG程序如程序21-4所示,其内容相当浅显易懂。两个菜单选项(Enter和Delete)启动一个对话框,让您输入一个字符串,然后STRPROG呼叫AddString或者DeleteString。当程序需要更新它的显示区域时,呼叫GetStrings并使用函数GetStrCallBack来列出所列举的字符串。
STRPROG.C /*---------------------------------------------------------------------------- STRPROG.C - Program using STRLIB dynamic-link library (c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #include "strlib.h" #include "resource.h" typedef struct { HDC hdc ; int xText ; int yText ; int xStart ; int yStart ; int xIncr ; int yIncr ; int xMax ; int yMax ; } CBPARAM ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("StrProg") ; TCHAR szString [MAX_LENGTH + 1] ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("DLL Demonstration Program"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } BOOL CALLBACK DlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: SendDlgItemMessage (hDlg, IDC_STRING, EM_LIMITTEXT, MAX_LENGTH, 0) ; return TRUE ; case WM_COMMAND: switch (wParam) { case IDOK: GetDlgItemText (hDlg, IDC_STRING, szString, MAX_LENGTH) ; EndDialog (hDlg, TRUE) ; return TRUE ; case IDCANCEL: EndDialog (hDlg, FALSE) ; return TRUE ; } } return FALSE ; } BOOL CALLBACK GetStrCallBack (PTSTR pString, CBPARAM * pcbp) { TextOut ( pcbp->hdc, pcbp->xText, pcbp->yText, pString, lstrlen (pString)) ; if ((pcbp->yText += pcbp->yIncr) > pcbp->yMax) { pcbp->yText = pcbp->yStart ; if ((pcbp->xText += pcbp->xIncr) > pcbp->xMax) return FALSE ; } return TRUE ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HINSTANCE hInst ; static int cxChar, cyChar, cxClient, cyClient ; static UINT iDataChangeMsg ; CBPARAM cbparam ; HDC hdc ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hInst = ((LPCREATESTRUCT) lParam)->hInstance ; hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = (int) tm.tmAveCharWidth ; cyChar = (int) (tm.tmHeight + tm.tmExternalLeading) ; ReleaseDC (hwnd, hdc) ; // Register message for notifying instances of data changes iDataChangeMsg = RegisterWindowMessage (TEXT ("StrProgDataChange")) ; return 0 ; case WM_COMMAND: switch (wParam) { case IDM_ENTER: if (DialogBox (hInst, TEXT ("EnterDlg"), hwnd, &DlgProc)) { if (AddString (szString)) PostMessage (HWND_BROADCAST, iDataChangeMsg, 0, 0) ; else MessageBeep (0) ; } break ; case IDM_DELETE: if (DialogBox (hInst, TEXT ("DeleteDlg"), hwnd, &DlgProc)) { if (DeleteString (szString)) PostMessage (HWND_BROADCAST, iDataChangeMsg, 0, 0) ; else MessageBeep (0) ; } break ; } return 0 ; case WM_SIZE: cxClient = (int) LOWORD (lParam) ; cyClient = (int) HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; cbparam.hdc = hdc ; cbparam.xText= cbparam.xStart = cxChar ; cbparam.yText= cbparam.yStart = cyChar ; cbparam.xIncr= cxChar * MAX_LENGTH ; cbparam.yIncr= cyChar ; cbparam.xMax = cbparam.xIncr * (1 + cxClient / cbparam.xIncr) ; cbparam.yMax = cyChar * (cyClient / cyChar - 1) ; GetStrings ((GETSTRCB) GetStrCallBack, (PVOID) &cbparam) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; default: if (message == iDataChangeMsg) InvalidateRect (hwnd, NULL, TRUE) ; break ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
STRPROG.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog ENTERDLG DIALOG DISCARDABLE 20, 20, 186, 47 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Enter" FONT 8, "MS Sans Serif" BEGIN LTEXT "&Enter:",IDC_STATIC,7,7,26,9 EDITTEXT IDC_STRING,31,7,148,12,ES_AUTOHSCROLL DEFPUSHBUTTON "OK",IDOK,32,26,50,14 PUSHBUTTON "Cancel",IDCANCEL,104,26,50,14 END DELETEDLG DIALOG DISCARDABLE 20, 20, 186, 47 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Delete" FONT 8, "MS Sans Serif" BEGIN LTEXT "&Delete:",IDC_STATIC,7,7,26,9 EDITTEXT IDC_STRING,31,7,148,12,ES_AUTOHSCROLL DEFPUSHBUTTON "OK",IDOK,32,26,50,14 PUSHBUTTON "Cancel",IDCANCEL,104,26,50,14 END ///////////////////////////////////////////////////////////////////////////// // Menu STRPROG MENU DISCARDABLE BEGIN MENUITEM "&Enter!", IDM_ENTER MENUITEM "&Delete!", IDM_DELETE END
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by StrProg.rc #define IDC_STRING 1000 #define IDM_ENTER 40001 #define IDM_DELETE 40002 #define IDC_STATIC -1
STRPROG.C包含STRLIB.H表头文件,其中定义了STRPROG将使用的STRLIB中的三个函数。
当您执行STRPROG的多个执行实体的时候,本程序的奥妙之处就会显露出来。STRLIB将在共享内存中储存字符串及其指针,并允许STRPROG中的所有执行实体共享此数据。让我们看一下它是如何执行的吧。
在STRPROG执行实体之间共享数据
Windows在一个Win32程序的地址空间周围筑了一道墙。通常,一个程序的地址空间中的数据是私有的,对别的程序而言是不可见的。但是执行STRPROG的多个执行实体表示了STRLIB在程序的所有执行实体之间共享数据是毫无问题的。当您在一个STRPROG窗口中增加或者删除一个字符串时,这种改变将立即反映在其它的窗口中。
在全部例程之间,STRLIB共享两个变量:一个字符数组和一个整数(记录已储存的有效字符串的个数)。STRLIB将这两个变量储存在共享的一个特殊内存区段中:
#pragma data_seg ("shared") int iTotal = 0 ; WCHAR szStrings [MAX_STRINGS][MAX_LENGTH + 1] = { '\0' } ; #pragma data_seg ()
第一个#pragma叙述建立数据段,这里命名为shared。您可以将这段命名为任何一个您喜欢的名字。在这里的#pragma叙述之后的所有初始化了的变量都放在shared数据段中。第二个#pragma叙述标示段的结束。对变量进行专门的初始化是很重要的,否则编译器将把它们放在普通的未初始化数据段中而不是放在shared中。
连结器必须知道有一个「shared」共享数据段。在「Project Settings」对话框选择「Link」页面卷标。选中「STRLIB」时在「Project Options」字段(在Release和Debug设定中均可),包含下面的连结叙述:
/SECTION:shared,RWS
字母RWS表示段具有读、写和共享属性。或者,您也可以直接用DLL原始码指定连结选项,就像我们在STRLIB.C那样:
#pragma comment(linker,"/SECTION:shared,RWS")
共享的内存段允许iTotal变量和szStrings字符串数组在STRLIB的所有例程之间共享。因为MAX_STRINGS等于256,而MAX_LENGTH等于63,所以,共享内存段的长度为32,772字节-iTotal变量需要4字节,256个指针中的每一个都需要128字节。
使用共享内存段可能是在多个应用程序间共享数据的最简单的方法。如果需要动态配置共享内存空间,您应该查看内存映像文件对象的用法,文件在/Platform SDK/Windows Base Services/Interprocess Communication/File Mapping。
如前所述,动态链接库模块不接收消息,但是,动态链接库模块可呼叫GetMessage和PeekMessage。实际上,从消息队列中得到的消息是发给呼叫链接库函数的程序的。一般来说,链接库是替呼叫它的程序工作的,这是一项对链接库所呼叫的大多数Windows函数都适用的规则。
动态链接库可以从链接库文件或者从呼叫链接库的程序文件中加载资源(如图标、字符串和位图)。加载资源的函数需要执行实体句柄。如果链接库使用它自己的执行实体句柄(初始化期间传给链接库的),则链接库能从它自己的文件中获得资源。为了从呼叫程序的.EXE文件中得到资源,程序链接库函数需要呼叫该函数的程序的执行实体句柄。
在链接库中登录窗口类别和建立窗口需要一点技巧。窗口类别结构和CreateWindow呼叫都需要执行实体句柄。尽管在建立窗口类别和窗口时可使用动态链接库模块的执行实体句柄,但在链接库建立窗口时,窗口消息仍会发送到呼叫链接库中程序的消息队列。如果使用者必须在链接库中建立窗口类别和窗口,最好的方法可能是使用呼叫程序的执行实体句柄。
因为模态对话框的消息是在程序的消息循环之外接收到的,因此使用者可以在链接库中呼叫DialogBox来建立模态对话框。执行实体句柄可以是链接库句柄,并且DialogBox的hwndParent参数可以为NULL。
不用输入引用信息的动态链接
除了在第一次把使用者程序加载内存时,由Windows执行动态链接外,程序执行时也可以把程序同动态链接库模块连结到一起。例如,您通常会这样呼叫Rectangle函数:
Rectangle (hdc, xLeft, yTop, xRight, yBottom) ;
因为程序和GDI32.LIB引用链接库连结,该链接库提供了Rectangle的地址,因此这种方法有效。
您也可以用更迂回的方法呼叫Rectangle。首先用typedef为Rectangle定义一个函数型态:
typedef BOOL (WINAPI * PFNRECT) (HDC, int, int, int, int) ;
然后定义两个变量:
HANDLE hLibrary ; PFNRECT pfnRectangle ;
现在将hLibrary设定为链接库句柄,将lpfnRectangle设定为Rectangle函数的地址:
hLibrary = LoadLibrary (TEXT ("GDI32.DLL")) pfnRectangle = (PFNPRECT) GetProcAddress (hLibrary, TEXT ("Rectangle"))
如果找不到链接库文件或者发生其它一些错误,LoadLibrary函数传回NULL。现在您可以呼叫函数然后释放链接库:
pfnRectangle (hdc, xLeft, yTop, xRight, yBottom) ; FreeLibrary (hLibrary) ;
尽管这项执行时期动态链接的技术并没有为Rectangle函数增加多大好处,但它肯定是有用的,如果直到执行时还不知道程序动态链接库模块的名称,这时就需要使用它。
上面的程序代码使用了LoadLibrary和FreeLibrary函数。Windows为所有的动态链接库模块提供「引用计数」,LoadLibrary使引用计数递增。当Windows加载任何使用了链接库的程序时,引用计数也会递增。FreeLibrary使引用计数递减,在使用了链接库的程序执行实体结束时也是如此。当引用计数为零时,Windows将从内存中把链接库删除掉,因为不再需要它了。
纯资源链接库
可由Windows程序或其它链接库使用的动态链接库中的任何函数都必须被输出。然而,DLL也可以不包含任何输出函数。那么,DLL到底包含什么呢?答案是资源。
假设使用者正在使用需要几幅位图的Windows应用程序进行工作。通常要在程序的资源描述文件中列出资源,并用LoadBitmap函数把它们加载内存。但使用者可能希望建立若干套位图,每一套均适用于Windows所使用的不同显示卡。将不同套的位图存放到不同文件中可能是明智的,因为只需要在硬盘上保留一套位图。这些文件就是纯资源文件。
程序21-5说明如何建立包含9幅位图的名为BITLIB.DLL的纯资源链接库文件。BITLIB.RC文件列出了所有独立的位图文件并为每个文件赋予一个序号。为了建立BITLIB.DLL,需要9幅名为BITMAP1.BMP、BITMAP2.BMP等等的位图。您可以使用附带的光盘上提供的位图或者在Visual C++中建立这些位图。它们与ID从1到9相对应。
BITLIB.C /*-------------------------------------------------------------- BITLIB.C -- Code entry point for BITLIB dynamic-link library (c) Charles Petzold, 1998 ------------------------------------------------------------------*/ #include <windows.h> int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { return TRUE ; }
BITLIB.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Bitmap 1 BITMAP DISCARDABLE "bitmap1.bmp" 2 BITMAP DISCARDABLE "bitmap2.bmp" 3 BITMAP DISCARDABLE "bitmap3.bmp" 4 BITMAP DISCARDABLE "bitmap4.bmp" 5 BITMAP DISCARDABLE "bitmap5.bmp" 6 BITMAP DISCARDABLE "bitmap6.bmp" 7 BITMAP DISCARDABLE "bitmap7.bmp" 8 BITMAP DISCARDABLE "bitmap8.bmp" 9 BITMAP DISCARDABLE "bitmap9.bmp"
在名为SHOWBIT的工作空间中建立BITLIB项目。在名为SHOWBIT的另一个项目中,建立程序21-6所示的SHOWBIT程序,这与前面的一样。不过,不要使BITLIB依赖于SHOWBIT;否则,连结程序中将需要BITLIB.LIB文件,并且因为BITLIB没有任何输出函数,它也不会建立BITLIB.LIB。事实上,要分别重新编译BITLIB和SHOWBIT,可以交替设定其中一个为「Active Project」然后再重新编译。
SHOWBIT.C从BITLIB读取位图资源,然后在其显示区域显示。按键盘上的任意键可以循环显示。
SHOWBIT.C /*-------------------------------------------------------------------------- SHOWBIT.C -- Shows bitmaps in BITLIB dynamic-link library (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("ShowBit") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Show Bitmaps from BITLIB (Press Key)"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBitmap (HDC hdc, int xStart, int yStart, HBITMAP hBitmap) { BITMAP bm ; HDC hMemDC ; POINT pt ; hMemDC = CreateCompatibleDC (hdc) ; SelectObject (hMemDC, hBitmap) ; GetObject (hBitmap, sizeof (BITMAP), &bm) ; pt.x = bm.bmWidth ; pt.y = bm.bmHeight ; BitBlt (hdc, xStart, yStart, pt.x, pt.y, hMemDC, 0, 0, SRCCOPY) ; DeleteDC (hMemDC) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HINSTANCE hLibrary ; static int iCurrent = 1 ; HBITMAP hBitmap ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_CREATE: if ((hLibrary = LoadLibrary (TEXT ("BITLIB.DLL"))) == NULL) { MessageBox ( hwnd, TEXT ("Can't load BITLIB.DLL."), szAppName, 0) ; return -1 ; } return 0 ; case WM_CHAR: if (hLibrary) { iCurrent ++ ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hLibrary) { hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ; if (!hBitmap) { iCurrent = 1 ; hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ; } if (hBitmap) { DrawBitmap (hdc, 0, 0, hBitmap) ; DeleteObject (hBitmap) ; } } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: if (hLibrary) FreeLibrary (hLibrary) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
在处理WM_CREATE消息处理期间,SHOWBIT获得了BITLIB.DLL的句柄:
if ((hLibrary = LoadLibrary (TEXT ("BITLIB.DLL"))) == NULL)
如果BITLIB.DLL与SHOWBIT.EXE不在同一个目录,Windows将按本章前面讨论的方法搜索。如果LoadLibrary传回NULL,SHOWBIT显示一个消息框来报告错误,并从WM_CREATE消息传回-1。这将导致WinMain中的CreateWindow呼叫传回NULL,而且程序终止程序。
SHOWBIT透过链接库句柄和位图号码来呼叫LoadBitmap,从而得到一个位图句柄:
hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ;
如果号码iCurrent对应的位图无效或者没有足够的内存加载位图,则传回一个错误。
在处理WM_DESTROY消息时,SHOWBIT释放链接库:
FreeLibrary (hLibrary) ;
当SHOWBIT的最后一个执行实体终止时,BITLIB.DLL的引用计数变为0,并且释放所占用的内存。这就是实作「图片剪辑」程序的一种简单方法,所谓的「图片剪辑」程序就是能够将预先建立的位图(或者metafile、增强型metafile)加载到剪贴簿,以供其它程序使用的程序。
动态链接库 |
|
动态链接库(也称为DLL)是Microsoft Windows最重要的组成要素之一。大多数与Windows相关的磁盘文件如果不是程序模块,就是动态链接程序。迄今为止,我们都是在开发Windows应用程序;现在是尝试编写动态链接库的时候了。许多您已经学会的编写应用程序的规则同样适用于编写这些动态链接库模块,但也有一些重要的不同。
动态链接库的基本知识
正如前面所看到的,Windows应用程序是一个可执行文件,它通常建立一个或几个窗口,并使用消息循环接收使用者输入。通常,动态链接库并不能直接执行,也不接收消息。它们是一些独立的文件,其中包含能被程序或其它DLL呼叫来完成一定作业的函数。只有在其它模块呼叫动态链接库中的函数时,它才发挥作用。
所谓「动态链接」,是指Windows把一个模块中的函数呼叫连结到动态链接库模块中的实际函数上的程序。在程序开发中,您将各种目标模块(.OBJ)、执行时期链接库(.LIB)文件,以及经常是已编译的资源(.RES)文件连结在一起,以便建立Windows的.EXE文件,这时的连结是「静态连结」。动态链接与此不同,它发生在执行时期。
KERNEL32.DLL、USER32.DLL和GDI32.DLL、各种驱动程序文件如KEYBOARD.DRV、SYSTEM.DRV和MOUSE.DRV和视讯及打印机驱动程序都是动态链接库。这些动态链接库能被所有Windows应用程序使用。
有些动态链接库(如字体文件等)被称为「纯资源」。它们只包含数据(通常是资源的形式)而不包含程序代码。由此可见,动态链接库的目的之一就是提供能被许多不同的应用程序所使用的函数和资源。在一般的操作系统中,只有操作系统本身才包含其它应用程序能够呼叫来完成某一作业的例程。在Windows中,一个模块呼叫另一个模块函数的程序被推广了。结果使得编写一个动态链接库,也就是在扩充Windows。当然,也可认为动态链接库(包括构成Windows的那些动态链接库例程)是对使用者程序的扩充。
尽管一个动态链接库模块可能有其它扩展名(如.EXE或.FON),但标准扩展名是.DLL。只有带.DLL扩展名的动态链接库才能被Windows自动加载。如果文件有其它扩展名,则程序必须另外使用LoadLibrary或者LoadLibraryEx函数加载该模块。
您通常会发现,动态链接库在大型应用程序中最有意义。例如,假设要为Windows编写一个由几个不同的程序组成的大型财务软件包,就会发现这些应用程序会使用许多共同的例程。可以把这些公共例程放入一个一般性的目的码链接库(带.LIB扩展名)中,并在使用LINK静态连结时把它们加入各程序模块中。但这种方法是很浪费的,因为软件包中的每个程序都包含与公共例程相同的程序代码。而且,如果修改了链接库中的某个例程,就要重新连结使用此例程的所有程序。然而,如果把这些公共例程放到称为ACCOUNT.DLL的动态链接库中,就可解决这两个问题。只有动态链接库模块才包含所有程序都要用到的例程。这样能为储存文件节省磁盘空间,并且在同时执行多个应用程序时节省内存,而且,可以修改动态链接库模块而不用重新连结各个程序。
动态链接库实际上是可以独立存在的。例如,假设您编写了一系列3D绘图例程,并把它们放入名为GDI3.DLL的DLL中。如果其它软件开发者对此链接库很感兴趣,您就可以授权他们将其加入他们的图形程序中。使用多个这样的图形程序的使用者只需要一个GDI3.DLL文件。
链接库:一词多义
动态链接库有着令人困惑的印象,部分原因是由于「链接库」这个词被放在几种不同的用语之后。除了动态链接库之外,我们也用它来称呼「目的码链接库」或「引用链接库」。
目的码链接库是带.LIB扩展名的文件。在使用连结程序进行静态连结时,它的程序代码就会加到程序的.EXE文件中。例如,在Microsoft Visual C++中,连同程序连结的一般C执行目的码链接库被称为LIBC.LIB。
引用链接库是目的码链接库文件的一种特殊形式。像目的码链接库一样,引用链接库有.LIB扩展名,并且被连结器用来确定程序代码中的函数呼叫来源。但引用链接库不含程序代码,而是为连结程序提供信息,以便在.EXE文件中建立动态链接时要用到的复位位表。包含在Microsoft编译器中的KERNEL32.LIB、USER32.LIB和GDI32.LIB文件是Windows函数的引用链接库。如果一个程序呼叫Rectangle函数,Rectangle将告诉LINK,该函数在GDI32.DLL动态链接库中。该信息被记录在.EXE文件中,使得程序执行时,Windows能够和GDI32.DLL动态链接库进行动态连结。
目的码链接库和引用链接库只用在程序开发期间使用,而动态链接库在执行期间使用。当一个使用动态链接库的程序执行时,该动态链接库必须在磁盘上。当Windows要执行一个使用了动态链接库的程序而需要加载该链接库时,动态链接库文件必须储存在含有该.EXE程序的目录下、目前的目录下、Windows系统目录下、Windows目录下,或者是在通过MS-DOS环境中的PATH可以存取到的目录下(Windows会按顺序搜索这些目录)。
一个简单的DLL
虽然动态链接库的整体概念是它们可以被多个应用程序所使用,但您通常最初设计的动态链接库只与一个应用程序相联系,可能是一个「测试」程序在使用DLL。
下面就是我们要做的。我们建立一个名为EDRLIB.DLL的DLL。文件名中的「EDR」代表「简便的绘图例程(easy drawing routines)」。这里的EDRLIB只含有一个函数(名称为EdrCenterText),但是您还可以将应用程序中其它简单的绘图函数添加进去。应用程序EDRTEST.EXE将通过呼叫EDRLIB.DLL中的函数来利用它。
要做到这一点,需要与我们以前所做的略有不同的方法,也包括Visual C++ 中我们没有看过的特性。在Visual C++ 中「工作空间(workspaces)」和「项目(projects)」不同。项目通常与建立的应用程序(.EXE)或者动态链接库(.DLL)相联系。一个工作空间可以包含一个或多个项目。迄今为止,我们所有的工作空间都只包含一个项目。我们现在就建立一个包含两个项目的工作空间EDRTEST-一个用于建立EDRTEST.EXE,而另一个用于建立EDRLIB.DLL,即EDRTEST使用的动态链接库。
现在就开始。在Visual C++中,从「File」菜单选择「New」,然后选择「Workspaces」页面标签。(我们以前从来没有选择过。)在「Location」栏选择工作空间要储存的目录,然后在「Workspace Name」栏输入「EDRTEST」,按Enter键。
这样就建立了一个空的工作空间。Developer Studio还建立了一个名为EDRTEST的子目录,以及工作空间文件EDRTEST.DSW(就像两个其它文件)。
现在让我们在此工作空间里建立一个项目。从「File」菜单选择「New」,然后选择「Projects」页面标签。尽管过去您选择「Win32 Application」,但现在「Win32 Dynamic-Link Library」。另外,单击单选按钮「Add To Current Workspace」,这使得此项目是「EDRTEST」工作空间的一部分。在「Project Name栏输入EDRLIB,但先不要按「OK」按钮。当您在Project Name栏输入EDRLIB时,Visual C++将改变「Location」栏,以显示EDRLIB作为EDRTEST的一个子目录。这不是我们要的,所以接着在「Location」栏删除EDRLIB子目录以便项目建立在EDRTEST目录。现在按「OK」。屏幕将显示一个对话框,询问您建立什么型态的DLL。选择「An Empty DLL Project」,然后按「Finish」。Visual C++将建立一个项目文件EDRLIB.DSP和一个构造文件EDRLIB.MAK(如果「Tools Options」对话框的B「uild页面卷标中选择了「Export Makefile」选项」。
现在您已经在此项目中添加了一对文件。从「File」菜单选择「New」,然后选择「Files」页面标签。选择「C/C++ Header File」,然后输入文件名EDRLIB.H。输入程序21-1所示的文件(或者从本书光盘中复制)。再次从「File」菜单中选择「New」,然后选择「Files」页面标签。这次选择「C++ Source File」,然后输入文件名EDRLIB.C。继续输入程序21-1所示的程序。
EDRLIB.H /*-------------------------------------------------------------------------- EDRLIB.H header file ----------------------------------------------------------------------------*/ #ifdef __cplusplus #define EXPORT extern "C" __declspec (dllexport) #else #define EXPORT __declspec (dllexport) #endif EXPORT BOOL CALLBACK EdrCenterTextA (HDC, PRECT, PCSTR) ; EXPORT BOOL CALLBACK EdrCenterTextW (HDC, PRECT, PCWSTR) ; #ifdef UNICODE #define EdrCenterText EdrCenterTextW #else #define EdrCenterText EdrCenterTextA #endif EDRLIB.C /*--------------------------------------------------------------------------- EDRLIB.C -- Easy Drawing Routine Library module (c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include windows.h> #include "edrlib.h" int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { return TRUE ; } EXPORT BOOL CALLBACK EdrCenterTextA ( HDC hdc, PRECT prc, PCSTR pString) { int iLength ; SIZE size ; iLength = lstrlenA (pString) ; GetTextExtentPoint32A (hdc, pString, iLength, &size) ; return TextOutA (hdc,(prc->right - prc->left - size.cx) / 2, ( prc->bottom - prc->top - size.cy) / 2, pString, iLength) ; } EXPORT BOOL CALLBACK EdrCenterTextW (HDC hdc, PRECT prc, PCWSTR pString) { int iLength ; SIZE size ; iLength = lstrlenW (pString) ; GetTextExtentPoint32W (hdc, pString, iLength, &size) ; return TextOutW (hdc, ( prc->right - prc->left - size.cx) / 2, ( prc->bottom - prc->top - size.cy) / 2, pString, iLength) ; }
这里您可以按Release设定,或者也可以按Debug设定来建立EDRLIB.DLL。之后,RELEASE和DEBUG目录将包含EDRLIB.LIB(即动态链接库的引用链接库)和EDRLIB.DLL(动态链接库本身)。
纵观全书,我们建立的所有程序都可以根据UNICODE标识符来编译成使用Unicode或非Unicode字符串的程序代码。当您建立一个DLL时,它应该包括处理字符和字符串的Unicode和非Unicode版的所有函数。因此,EDRLIB.C就包含函数EdrCenterTextA(ANSI版)和EdrCenterTextW(宽字符版)。EdrCenterTextA定义为带有参数PCSTR(指向const字符串的指针),而EdrCenterTextW则定义为带有参数PCWSTR(指向const宽字符串的指针)。EdrCenterTextA函数将呼叫lstrlenA、GetTextExtentPoint32A和TextOutA。EdrCenterTextW将呼叫lstrlenW、GetTextExtentPoint32W和TextOutW。如果定义了UNICODE标识符,则EDRLIB.H将EdrCenterText定义为EdrCenterTextW,否则定义为EdrCenterTextA。这样的做法很像Windows表头文件。
EDRLIB.H也包含函数DllMain,取代了DLL中的WinMain。此函数用于执行初始化和未初始化(deinitialization),我将在下一节讨论。我们现在所需要的就是从DllMain传回TRUE。
在这两个文件中,最后一点神秘之处就是定义了EXPORT标识符。DLL中应用程序使用的函数必须是「输出(exported)」的。这跟税务或者商业制度无关,只是确保函数名添加到EDRLIB.LIB的一个关键词(以便连结程序在连结使用此函数的应用程序时,能够解析出函数名称),而且该函数在EDRLIB.DLL中也是看得到的。EXPORT标识符包括储存方式限定词__declspec(dllexport)以及在表头文件按C++模式编译时附加的「C」。这将防止编译器使用C++的名称轧压规则(name mangling)来处理函数名称,使C和C++程序都能使用这个DLL。
链接库入口/出口点
当动态链接库首次启动和结束时,我们呼叫了DllMain函数。DllMain的第一个参数是链接库的执行实体句柄。如果您的链接库使用需要执行实体句柄(诸如DialogBox)的资源,那么您应该将hInstance储存为一个整体变量。DllMain的最后一个参数由系统保留。
fdwReason参数可以是四个值之一,说明为什么Windows要呼叫DllMain函数。在下面的讨论中,请记住一个程序可以被加载多次,并在Windows下一起执行。每当一个程序加载时,它都被认为是一个独立的程序(process)。
fdwReason的一个值DLL_PROCESS_ATTACH表示动态链接库被映像到一个程序的地址空间。链接库可以根据这个线索进行初始化,为以后来自该程序的请求提供服务。例如,这类初始化可能包括内存配置。在一个程序的生命周期内,只有一次对DllMain的呼叫以DLL_PROCESS_ATTACH为参数。使用同一DLL的其它任何程序都将导致另一个使用DLL_PROCESS_ATTACH参数的DllMain呼叫,但这是对新程序的呼叫。
如果初始化成功,DllMain应该传回一个非0值。传回0将导致Windows不执行该程序。
当fdwReason的值为DLL_PROCESS_DETACH时,意味着程序不再需要DLL了,从而提供给链接库自己清除自己的机会。在32位的Windows下,这种处理并不是严格必须的,但这是一种良好的程序写作习惯。
类似地,当以DLL_THREAD_ATTACH为fdwReason参数呼叫DllMain时,意味着某个程序建立了一个新的线程。当线程中止时,Windows以DLL_THREAD_DETACH为fdwReason参数呼叫DllMain。请注意,如果动态链接库是在线程被建立之后和一个程序连结的,那么可能会得到一个没有事先对应一个DLL_THREAD_ATTACH呼叫的DLL_THREAD_DETACH呼叫。
当使用一个DLL_THREAD_DETACH参数呼叫DllMain时,线程仍然存在。动态链接库甚至可以在这个程序期间发送线程消息。但是它不应该使用PostMessage,因为线程可能在此消息被处理到之前就已经退出执行了。
测试程序
现在让我们在EDRTEST工作空间里建立第二个项目,程序名称为EDRTEST,而且使用EDRLIB.DLL。在Visual C++中加载EDRTEST工作空间时,请从「File」菜单选择「New」,然后在「New」对话框中选择「Projects」页面标签。这次选择「Win32 Application」,并确保选中了「Add To Current Workspace」按钮。输入项目名称EDRTEST。再在「Locations」栏删除第二个EDRTEST子目录。按下「OK」,然后在下一个对话框选择「An Empty Project」,按「Finish」。
从「File」菜单再次选择「New」。选择「Files」页面标签然后选择「C++ Source File」。确保「Add To Project」清单方块显示「EDRTEST」而不是「EDRLIB」。输入文件名称EDRTEST.C,然后输入程序21-2所示的程序。此程序用EdrCenterText函数将显示区域中的字符串居中对齐。
EDRTEST.C /*--------------------------------------------------------------------------- EDRTEST.C -- Program using EDRLIB dynamic-link library (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "edrlib.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("StrProg") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("DLL Demonstration Program"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; EdrCenterText (hdc, &rect, TEXT ("This string was displayed by a DLL")) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
注意,为了定义EdrCenterText函数,EDRTEST.C包括EDRLIB.H表头文件,此函数将在WM_PAINT消息处理期间呼叫。
在编译此程序之前,您可能希望做以下几件事。首先,在「Project」菜单选择「Select Active Project」。这时您将看到「EDRLIB」和「EDRTEST」,选择「EDRTEST」。在重新编译此工作空间时,您真正要重新编译的是程序。另外,在「Project」菜单中,选择「Dependencies」,在「Select Project To Modify」清单方块中选择「EDRTEST」。在「Dependent On The Following Project(s)」列表选中「EDRLIB」。此操作的意思是:EDRTEST需要EDRLIB动态链接库。以后每次重新编译EDRTEST时,如果必要的话,都将在编译和连结EDRTEST之前重新重新编译EDRLIB。
从「Project」菜单选择「Settings」,单击「General」标签。当您在左边的窗格中选择「EDRLIB」或者「EDRTEST」项目时,如果设定为「Win32 Release」,则显示在右边窗格中的「Intermediate Files」和「Output Files」将位于RELEASE目录;如果设定为「Win32 Debug」,则位于DEBUG目录。如果不是,请按此修改。这样可确保EDRLIB.DLL与EDRTEST.EXE在同一个目录中,而且程序在使用DLL时也不会产生问题。
在「Project Setting」对话框中依然选中「EDRTEST」,单击「C/C++」页面标签。按本书的惯例,在「Preprocessor Definitions」中,将「UNICODE」添加到Debug设定。
现在您就可以在「Debug」或「Release」设定中重新编译EDRTEST.EXE了。必要时,Visual C++将首先编译和连结EDRLIB。RELEASE和DEBUG目录都包含EDRLIB.LIB(引用链接库)和EDRLIB.DLL。当Developer Studio连结EDRTEST时,将自动包含引用链接库。
了解EDRTEST.EXE文件中不包含EdrCenterText程序代码很重要。事实上,要证明执行了EDRLIB.DLL文件和EdrCenterText函数很简单:执行EDRTEST.EXE需要EDRLIB.DLL。
执行EDRTEST.EXE时,Windows按外部链接库模块执行固定的函数。其中许多函数都在一般Windows动态链接库中。但Windows也看到程序从EDRLIB呼叫了函数,因此Windows将EDRLIB.DLL文件加载到内存,然后呼叫EDRLIB的初始化例程。EDRTEST呼叫EdrCenterText函数是动态链接到EDRLIB中函数的。
在EDRTEST.C原始码文件中包含EDRLIB.H与包含WINDOWS.H类似。连结EDRLIB.LIB与连结Windows引用链接库(例如USER32.LIB)类似。当您的程序执行时,它连结EDLIB.DLL的方式与连结USER32.DLL的方式相同。恭喜您!您已经扩展了Windows的功能!
在继续之前,我还要对动态链接库多说明一些:
首先,虽然我们将DLL作为Windows的延伸,但它也是您的应用程序的延伸。DLL所完成的每件工作对于应用程序来说都是应用程序所交代要完成的。例如,应用程序拥有DLL配置的全部内存、DLL建立的全部窗口以及DLL打开的所有文件。多个应用程序可以同时使用同一个DLL,但在Windows下,这些应用程序不会相互影响。
多个程序能够共享一个动态链接库中相同的程序代码。但是,DLL为每个程序所储存的数据都不同。每个程序都为DLL所使用的全部数据配置了自己的地址空间。我们将在下以节看到,共享内存需要额外的工作。
在DLL中共享内存
令人兴奋的是,Windows能够将同时使用同一个动态链接库的应用程序分开。不过,有时却不太令人满意。您可能希望写一个DLL,其中包含能够被不同应用程序或者同一个程序的不同例程所共享的内存。这包括使用共享内存。共享内存实际上是一种内存映像文件。
让我们测试一下,这项工作是如何在程序STRPROG(「字符串程序(string program)」)和动态链接库STRLIB(「字符串链接库(string library)」)中完成的。STRLIB有三个输出函数被STRPROG呼叫,我们只对此感兴趣,STRLIB中的一个函数使用了在STRPROG定义的callback函数。
STRLIB是一个动态链接库模块,它储存并排序了最多256个字符串。在STRLIB中,这些字符串均为大写,并由共享内存维护。利用STRLIB的三个函数,STRPROG能够添加字符串、删除字符串以及从STRLIB获得目前的所有字符串。STRPROG测试程序有两个菜单项(「Enter」和「Delete」),这两个菜单项将启动不同的对话框来添加或删除字符串。STRPROG在其显示区域列出目前储存在STRLIB中的所有字符串。
下面这个函数在STRLIB定义,它将一个字符串添加到STRLIB的共享内存。
EXPORT BOOL CALLBACK AddString (pStringIn)
参数pStringIn是字符串的指针。字符串在AddString函数中变成大写。如果在STRLIB的列表中有一个相同的字符串,那么此函数将添加一个字符串的复本。如果成功,AddString传回「TRUE」(非0),否则传回「FALSE」(0)。如果字符串的长度为0,或者不能配置储存字符串的内存,或者已经储存了256个字符串,则传回值将都是FALSE。
STRLIB函数从STRLIB的共享内存中删除一个字符串。
EXPORT BOOL CALLBACK DeleteString (pStringIn)
另外,参数pStringIn是一个字符串指针。如果有多个相同内容字符串,则删除第一个。如果成功,那么DeleteString传回「TRUE」(非0),否则传回「FALSE」(0)。传回「FALSE」表示字符串长度为0,或者找不到相同内容的字符串。
STRLIB函数使用了呼叫程序中的一个callback函数,以便列出目前储存在STRLIB共享内存中的字符串:
EXPORT int CALLBACK GetStrings (pfnGetStrCallBack, pParam)
在呼叫程序中,callback函数必须像下面这样定义:
EXPORT BOOL CALLBACK GetStrCallBack (PSTR pString, PVOID pParam)
GetStrings的参数pfnGetStrCallBack指向callback函数。直到callback函数传回「FALSE」(0),GetStrings将为每个字符串都呼叫一次GetStrCallBack。GetStrings传回传递给callback函数的字符串数。pParam参数是一个远程指针,指向程序写作者定义的数据。
当然,此程序可以编译成Unicode程序,或者在STRLIB的支持下,编译成Unicode和非Unicode应用程序。与EDRLIB一样,所有的函数都有「A」和「W」两种版本。在内部,STRLIB以Unicode储存所有的字符串。如果非Unicode程序使用了STRLIB(也就是说,程序将呼叫AddStringA、DeleteStringA和GetStringsA),字符串将在Unicode和非Unicode之间转换。
与STRPROG和STRLIB项目相关的工作空间名为STRPROG。此文件按EDRTEST工作空间的方式组合。程序21-3显示了建立STRLIB.DLL动态链接库所必须的两个文件。
STRLIB.H /*---------------------------------------------------------------------------- STRLIB.H header file -----------------------------------------------------------------------------*/ #ifdef __cplusplus #define EXPORT extern "C" __declspec (dllexport) #else #define EXPORT __declspec (dllexport) #endif // The maximum number of strings STRLIB will store and their lengths #define MAX_STRINGS 256 #define MAX_LENGTH 63 // The callback function type definition uses generic strings typedef BOOL (CALLBACK * GETSTRCB) (PCTSTR, PVOID) ; // Each function has ANSI and Unicode versions EXPORT BOOL CALLBACK AddStringA (PCSTR) ; EXPORT BOOL CALLBACK AddStringW (PCWSTR) ; EXPORT BOOL CALLBACK DeleteStringA (PCSTR) ; EXPORT BOOL CALLBACK DeleteStringW (PCWSTR) ; EXPORT int CALLBACK GetStringsA (GETSTRCB, PVOID) ; EXPORT int CALLBACK GetStringsW (GETSTRCB, PVOID) ; // Use the correct version depending on the UNICODE identifier #ifdef UNICODE #define AddString AddStringW #define DeleteString DeleteStringW #define GetStrings GetStringsW #else #define AddString AddStringA #define DeleteString DeleteStringA #define GetStrings GetStringsA #endif STRLIB.C /*--------------------------------------------------------------------------- STRLIB.C - Library module for STRPROG program (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <wchar.h> // for wide-character string functions #include "strlib.h" // shared memory section (requires /SECTION:shared,RWS in link options) #pragma data_seg ("shared") int iTotal = 0 ; WCHAR szStrings [MAX_STRINGS][MAX_LENGTH + 1] = { '\0' } ; #pragma data_seg () #pragma comment(linker,"/SECTION:shared,RWS") int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { return TRUE ; } EXPORT BOOL CALLBACK AddStringA (PCSTR pStringIn) { BOOL bReturn ; int iLength ; PWSTR pWideStr ; // Convert string to Unicode and call AddStringW iLength = MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, NULL, 0) ; pWideStr = malloc (iLength) ; MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, pWideStr, iLength) ; bReturn = AddStringW (pWideStr) ; free (pWideStr) ; return bReturn ; } EXPORT BOOL CALLBACK AddStringW (PCWSTR pStringIn) { PWSTR pString ; int i, iLength ; if (iTotal == MAX_STRINGS - 1) return FALSE ; if ((iLength = wcslen (pStringIn)) == 0) return FALSE ; // Allocate memory for storing string, copy it, convert to uppercase pString = malloc (sizeof (WCHAR) * (1 + iLength)) ; wcscpy (pString, pStringIn) ; _wcsupr (pString) ; // Alphabetize the strings for (i = iTotal ; i > 0 ; i-) { if (wcscmp (pString, szStrings[i - 1]) >= 0) break ; wcscpy (szStrings[i], szStrings[i - 1]) ; } wcscpy (szStrings[i], pString) ; iTotal++ ; free (pString) ; return TRUE ; } EXPORT BOOL CALLBACK DeleteStringA (PCSTR pStringIn) { BOOL bReturn ; int iLength ; PWSTR pWideStr ; // Convert string to Unicode and call DeleteStringW iLength = MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, NULL, 0) ; pWideStr = malloc (iLength) ; MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, pWideStr, iLength) ; bReturn = DeleteStringW (pWideStr) ; free (pWideStr) ; return bReturn ; } EXPORT BOOL CALLBACK DeleteStringW (PCWSTR pStringIn) { int i, j ; if (0 == wcslen (pStringIn)) return FALSE ; for (i = 0 ; i < iTotal ; i++) { if (_wcsicmp (szStrings[i], pStringIn) == 0) break ; } // If given string not in list, return without taking action if (i == iTotal) return FALSE ; // Else adjust list downward for (j = i ; j < iTotal ; j++) wcscpy (szStrings[j], szStrings[j + 1]) ; szStrings[iTotal-][0] = '\0' ; return TRUE ; } EXPORT int CALLBACK GetStringsA (GETSTRCB pfnGetStrCallBack, PVOID pParam) { BOOL bReturn ; int i, iLength ; PSTR pAnsiStr ; for (i = 0 ; i < iTotal ; i++) { // Convert string from Unicode iLength = WideCharToMultiByte ( CP_ACP, 0, szStrings[i], -1, NULL, 0, NULL, NULL) ; pAnsiStr = malloc (iLength) ; WideCharToMultiByte ( CP_ACP, 0, szStrings[i], -1, pAnsiStr, iLength, NULL, NULL) ; // Call callback function bReturn = pfnGetStrCallBack (pAnsiStr, pParam) ; if (bReturn == FALSE) return i + 1 ; free (pAnsiStr) ; } return iTotal ; } EXPORT int CALLBACK GetStringsW (GETSTRCB pfnGetStrCallBack, PVOID pParam) { BOOL bReturn ; int i ; for (i = 0 ; i < iTotal ; i++) { bReturn = pfnGetStrCallBack (szStrings[i], pParam) ; if (bReturn == FALSE) return i + 1 ; } return iTotal ; }
除了DllMain函数以外,STRLIB中只有六个函数供其它函数输出用。所有这些函数都按EXPORT定义。这会使LINK在STRLIB.LIB引用链接库中列出它们。
STRPROG程序
STRPROG程序如程序21-4所示,其内容相当浅显易懂。两个菜单选项(Enter和Delete)启动一个对话框,让您输入一个字符串,然后STRPROG呼叫AddString或者DeleteString。当程序需要更新它的显示区域时,呼叫GetStrings并使用函数GetStrCallBack来列出所列举的字符串。
STRPROG.C /*---------------------------------------------------------------------------- STRPROG.C - Program using STRLIB dynamic-link library (c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #include "strlib.h" #include "resource.h" typedef struct { HDC hdc ; int xText ; int yText ; int xStart ; int yStart ; int xIncr ; int yIncr ; int xMax ; int yMax ; } CBPARAM ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("StrProg") ; TCHAR szString [MAX_LENGTH + 1] ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("DLL Demonstration Program"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } BOOL CALLBACK DlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: SendDlgItemMessage (hDlg, IDC_STRING, EM_LIMITTEXT, MAX_LENGTH, 0) ; return TRUE ; case WM_COMMAND: switch (wParam) { case IDOK: GetDlgItemText (hDlg, IDC_STRING, szString, MAX_LENGTH) ; EndDialog (hDlg, TRUE) ; return TRUE ; case IDCANCEL: EndDialog (hDlg, FALSE) ; return TRUE ; } } return FALSE ; } BOOL CALLBACK GetStrCallBack (PTSTR pString, CBPARAM * pcbp) { TextOut ( pcbp->hdc, pcbp->xText, pcbp->yText, pString, lstrlen (pString)) ; if ((pcbp->yText += pcbp->yIncr) > pcbp->yMax) { pcbp->yText = pcbp->yStart ; if ((pcbp->xText += pcbp->xIncr) > pcbp->xMax) return FALSE ; } return TRUE ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HINSTANCE hInst ; static int cxChar, cyChar, cxClient, cyClient ; static UINT iDataChangeMsg ; CBPARAM cbparam ; HDC hdc ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hInst = ((LPCREATESTRUCT) lParam)->hInstance ; hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = (int) tm.tmAveCharWidth ; cyChar = (int) (tm.tmHeight + tm.tmExternalLeading) ; ReleaseDC (hwnd, hdc) ; // Register message for notifying instances of data changes iDataChangeMsg = RegisterWindowMessage (TEXT ("StrProgDataChange")) ; return 0 ; case WM_COMMAND: switch (wParam) { case IDM_ENTER: if (DialogBox (hInst, TEXT ("EnterDlg"), hwnd, &DlgProc)) { if (AddString (szString)) PostMessage (HWND_BROADCAST, iDataChangeMsg, 0, 0) ; else MessageBeep (0) ; } break ; case IDM_DELETE: if (DialogBox (hInst, TEXT ("DeleteDlg"), hwnd, &DlgProc)) { if (DeleteString (szString)) PostMessage (HWND_BROADCAST, iDataChangeMsg, 0, 0) ; else MessageBeep (0) ; } break ; } return 0 ; case WM_SIZE: cxClient = (int) LOWORD (lParam) ; cyClient = (int) HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; cbparam.hdc = hdc ; cbparam.xText= cbparam.xStart = cxChar ; cbparam.yText= cbparam.yStart = cyChar ; cbparam.xIncr= cxChar * MAX_LENGTH ; cbparam.yIncr= cyChar ; cbparam.xMax = cbparam.xIncr * (1 + cxClient / cbparam.xIncr) ; cbparam.yMax = cyChar * (cyClient / cyChar - 1) ; GetStrings ((GETSTRCB) GetStrCallBack, (PVOID) &cbparam) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; default: if (message == iDataChangeMsg) InvalidateRect (hwnd, NULL, TRUE) ; break ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
STRPROG.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog ENTERDLG DIALOG DISCARDABLE 20, 20, 186, 47 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Enter" FONT 8, "MS Sans Serif" BEGIN LTEXT "&Enter:",IDC_STATIC,7,7,26,9 EDITTEXT IDC_STRING,31,7,148,12,ES_AUTOHSCROLL DEFPUSHBUTTON "OK",IDOK,32,26,50,14 PUSHBUTTON "Cancel",IDCANCEL,104,26,50,14 END DELETEDLG DIALOG DISCARDABLE 20, 20, 186, 47 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Delete" FONT 8, "MS Sans Serif" BEGIN LTEXT "&Delete:",IDC_STATIC,7,7,26,9 EDITTEXT IDC_STRING,31,7,148,12,ES_AUTOHSCROLL DEFPUSHBUTTON "OK",IDOK,32,26,50,14 PUSHBUTTON "Cancel",IDCANCEL,104,26,50,14 END ///////////////////////////////////////////////////////////////////////////// // Menu STRPROG MENU DISCARDABLE BEGIN MENUITEM "&Enter!", IDM_ENTER MENUITEM "&Delete!", IDM_DELETE END
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by StrProg.rc #define IDC_STRING 1000 #define IDM_ENTER 40001 #define IDM_DELETE 40002 #define IDC_STATIC -1
STRPROG.C包含STRLIB.H表头文件,其中定义了STRPROG将使用的STRLIB中的三个函数。
当您执行STRPROG的多个执行实体的时候,本程序的奥妙之处就会显露出来。STRLIB将在共享内存中储存字符串及其指针,并允许STRPROG中的所有执行实体共享此数据。让我们看一下它是如何执行的吧。
在STRPROG执行实体之间共享数据
Windows在一个Win32程序的地址空间周围筑了一道墙。通常,一个程序的地址空间中的数据是私有的,对别的程序而言是不可见的。但是执行STRPROG的多个执行实体表示了STRLIB在程序的所有执行实体之间共享数据是毫无问题的。当您在一个STRPROG窗口中增加或者删除一个字符串时,这种改变将立即反映在其它的窗口中。
在全部例程之间,STRLIB共享两个变量:一个字符数组和一个整数(记录已储存的有效字符串的个数)。STRLIB将这两个变量储存在共享的一个特殊内存区段中:
#pragma data_seg ("shared") int iTotal = 0 ; WCHAR szStrings [MAX_STRINGS][MAX_LENGTH + 1] = { '\0' } ; #pragma data_seg ()
第一个#pragma叙述建立数据段,这里命名为shared。您可以将这段命名为任何一个您喜欢的名字。在这里的#pragma叙述之后的所有初始化了的变量都放在shared数据段中。第二个#pragma叙述标示段的结束。对变量进行专门的初始化是很重要的,否则编译器将把它们放在普通的未初始化数据段中而不是放在shared中。
连结器必须知道有一个「shared」共享数据段。在「Project Settings」对话框选择「Link」页面卷标。选中「STRLIB」时在「Project Options」字段(在Release和Debug设定中均可),包含下面的连结叙述:
/SECTION:shared,RWS
字母RWS表示段具有读、写和共享属性。或者,您也可以直接用DLL原始码指定连结选项,就像我们在STRLIB.C那样:
#pragma comment(linker,"/SECTION:shared,RWS")
共享的内存段允许iTotal变量和szStrings字符串数组在STRLIB的所有例程之间共享。因为MAX_STRINGS等于256,而MAX_LENGTH等于63,所以,共享内存段的长度为32,772字节-iTotal变量需要4字节,256个指针中的每一个都需要128字节。
使用共享内存段可能是在多个应用程序间共享数据的最简单的方法。如果需要动态配置共享内存空间,您应该查看内存映像文件对象的用法,文件在/Platform SDK/Windows Base Services/Interprocess Communication/File Mapping。
如前所述,动态链接库模块不接收消息,但是,动态链接库模块可呼叫GetMessage和PeekMessage。实际上,从消息队列中得到的消息是发给呼叫链接库函数的程序的。一般来说,链接库是替呼叫它的程序工作的,这是一项对链接库所呼叫的大多数Windows函数都适用的规则。
动态链接库可以从链接库文件或者从呼叫链接库的程序文件中加载资源(如图标、字符串和位图)。加载资源的函数需要执行实体句柄。如果链接库使用它自己的执行实体句柄(初始化期间传给链接库的),则链接库能从它自己的文件中获得资源。为了从呼叫程序的.EXE文件中得到资源,程序链接库函数需要呼叫该函数的程序的执行实体句柄。
在链接库中登录窗口类别和建立窗口需要一点技巧。窗口类别结构和CreateWindow呼叫都需要执行实体句柄。尽管在建立窗口类别和窗口时可使用动态链接库模块的执行实体句柄,但在链接库建立窗口时,窗口消息仍会发送到呼叫链接库中程序的消息队列。如果使用者必须在链接库中建立窗口类别和窗口,最好的方法可能是使用呼叫程序的执行实体句柄。
因为模态对话框的消息是在程序的消息循环之外接收到的,因此使用者可以在链接库中呼叫DialogBox来建立模态对话框。执行实体句柄可以是链接库句柄,并且DialogBox的hwndParent参数可以为NULL。
不用输入引用信息的动态链接
除了在第一次把使用者程序加载内存时,由Windows执行动态链接外,程序执行时也可以把程序同动态链接库模块连结到一起。例如,您通常会这样呼叫Rectangle函数:
Rectangle (hdc, xLeft, yTop, xRight, yBottom) ;
因为程序和GDI32.LIB引用链接库连结,该链接库提供了Rectangle的地址,因此这种方法有效。
您也可以用更迂回的方法呼叫Rectangle。首先用typedef为Rectangle定义一个函数型态:
typedef BOOL (WINAPI * PFNRECT) (HDC, int, int, int, int) ;
然后定义两个变量:
HANDLE hLibrary ; PFNRECT pfnRectangle ;
现在将hLibrary设定为链接库句柄,将lpfnRectangle设定为Rectangle函数的地址:
hLibrary = LoadLibrary (TEXT ("GDI32.DLL")) pfnRectangle = (PFNPRECT) GetProcAddress (hLibrary, TEXT ("Rectangle"))
如果找不到链接库文件或者发生其它一些错误,LoadLibrary函数传回NULL。现在您可以呼叫函数然后释放链接库:
pfnRectangle (hdc, xLeft, yTop, xRight, yBottom) ; FreeLibrary (hLibrary) ;
尽管这项执行时期动态链接的技术并没有为Rectangle函数增加多大好处,但它肯定是有用的,如果直到执行时还不知道程序动态链接库模块的名称,这时就需要使用它。
上面的程序代码使用了LoadLibrary和FreeLibrary函数。Windows为所有的动态链接库模块提供「引用计数」,LoadLibrary使引用计数递增。当Windows加载任何使用了链接库的程序时,引用计数也会递增。FreeLibrary使引用计数递减,在使用了链接库的程序执行实体结束时也是如此。当引用计数为零时,Windows将从内存中把链接库删除掉,因为不再需要它了。
纯资源链接库
可由Windows程序或其它链接库使用的动态链接库中的任何函数都必须被输出。然而,DLL也可以不包含任何输出函数。那么,DLL到底包含什么呢?答案是资源。
假设使用者正在使用需要几幅位图的Windows应用程序进行工作。通常要在程序的资源描述文件中列出资源,并用LoadBitmap函数把它们加载内存。但使用者可能希望建立若干套位图,每一套均适用于Windows所使用的不同显示卡。将不同套的位图存放到不同文件中可能是明智的,因为只需要在硬盘上保留一套位图。这些文件就是纯资源文件。
程序21-5说明如何建立包含9幅位图的名为BITLIB.DLL的纯资源链接库文件。BITLIB.RC文件列出了所有独立的位图文件并为每个文件赋予一个序号。为了建立BITLIB.DLL,需要9幅名为BITMAP1.BMP、BITMAP2.BMP等等的位图。您可以使用附带的光盘上提供的位图或者在Visual C++中建立这些位图。它们与ID从1到9相对应。
BITLIB.C /*-------------------------------------------------------------- BITLIB.C -- Code entry point for BITLIB dynamic-link library (c) Charles Petzold, 1998 ------------------------------------------------------------------*/ #include <windows.h> int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { return TRUE ; }
BITLIB.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Bitmap 1 BITMAP DISCARDABLE "bitmap1.bmp" 2 BITMAP DISCARDABLE "bitmap2.bmp" 3 BITMAP DISCARDABLE "bitmap3.bmp" 4 BITMAP DISCARDABLE "bitmap4.bmp" 5 BITMAP DISCARDABLE "bitmap5.bmp" 6 BITMAP DISCARDABLE "bitmap6.bmp" 7 BITMAP DISCARDABLE "bitmap7.bmp" 8 BITMAP DISCARDABLE "bitmap8.bmp" 9 BITMAP DISCARDABLE "bitmap9.bmp"
在名为SHOWBIT的工作空间中建立BITLIB项目。在名为SHOWBIT的另一个项目中,建立程序21-6所示的SHOWBIT程序,这与前面的一样。不过,不要使BITLIB依赖于SHOWBIT;否则,连结程序中将需要BITLIB.LIB文件,并且因为BITLIB没有任何输出函数,它也不会建立BITLIB.LIB。事实上,要分别重新编译BITLIB和SHOWBIT,可以交替设定其中一个为「Active Project」然后再重新编译。
SHOWBIT.C从BITLIB读取位图资源,然后在其显示区域显示。按键盘上的任意键可以循环显示。
SHOWBIT.C /*-------------------------------------------------------------------------- SHOWBIT.C -- Shows bitmaps in BITLIB dynamic-link library (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("ShowBit") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Show Bitmaps from BITLIB (Press Key)"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBitmap (HDC hdc, int xStart, int yStart, HBITMAP hBitmap) { BITMAP bm ; HDC hMemDC ; POINT pt ; hMemDC = CreateCompatibleDC (hdc) ; SelectObject (hMemDC, hBitmap) ; GetObject (hBitmap, sizeof (BITMAP), &bm) ; pt.x = bm.bmWidth ; pt.y = bm.bmHeight ; BitBlt (hdc, xStart, yStart, pt.x, pt.y, hMemDC, 0, 0, SRCCOPY) ; DeleteDC (hMemDC) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HINSTANCE hLibrary ; static int iCurrent = 1 ; HBITMAP hBitmap ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_CREATE: if ((hLibrary = LoadLibrary (TEXT ("BITLIB.DLL"))) == NULL) { MessageBox ( hwnd, TEXT ("Can't load BITLIB.DLL."), szAppName, 0) ; return -1 ; } return 0 ; case WM_CHAR: if (hLibrary) { iCurrent ++ ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hLibrary) { hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ; if (!hBitmap) { iCurrent = 1 ; hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ; } if (hBitmap) { DrawBitmap (hdc, 0, 0, hBitmap) ; DeleteObject (hBitmap) ; } } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: if (hLibrary) FreeLibrary (hLibrary) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
在处理WM_CREATE消息处理期间,SHOWBIT获得了BITLIB.DLL的句柄:
if ((hLibrary = LoadLibrary (TEXT ("BITLIB.DLL"))) == NULL)
如果BITLIB.DLL与SHOWBIT.EXE不在同一个目录,Windows将按本章前面讨论的方法搜索。如果LoadLibrary传回NULL,SHOWBIT显示一个消息框来报告错误,并从WM_CREATE消息传回-1。这将导致WinMain中的CreateWindow呼叫传回NULL,而且程序终止程序。
SHOWBIT透过链接库句柄和位图号码来呼叫LoadBitmap,从而得到一个位图句柄:
hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ;
如果号码iCurrent对应的位图无效或者没有足够的内存加载位图,则传回一个错误。
在处理WM_DESTROY消息时,SHOWBIT释放链接库:
FreeLibrary (hLibrary) ;
当SHOWBIT的最后一个执行实体终止时,BITLIB.DLL的引用计数变为0,并且释放所占用的内存。这就是实作「图片剪辑」程序的一种简单方法,所谓的「图片剪辑」程序就是能够将预先建立的位图(或者metafile、增强型metafile)加载到剪贴簿,以供其它程序使用的程序。