第十一章 动态链接库

  动态链接库(也称为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的动态链接库中,就可以解决这两个问题。只有库模块才包含所有程序都要用到的例程。这样能为存储文件节省磁盘空间,在同时运行多个应用程序时节省内存,并且可以修改库模块,而不用重新链接各个程序。
  动态链接实际上是一种可以独立自存的产品。例如,假设您编写了一系列三维绘图例程,并把它们放入名为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能够和GDI32DLL动态链接库进行动态链接。
  目标库和输入库只用在程序开发期间,而动态链接库在运行期间使用。当一个使用动态链接库的程序运行时,该动态链接库必须在磁盘上。当Windows需要运行一个使用了动态链接库的程序而需要如载该时,库文件必须存储在含有该.EXE程序的目录下,或者是在当前的目录下,或Windows系统目录下,或Windows目录下,或者是在通过MS-DOS环境中的PATH可以访问到的目录下(将此顺序搜索这目录)。
一个简单的DIL
  虽然动态链接库的整体概念是它们可以被多个应用程序所使用,但您通常最初设计的动态链接库只与一个应用程序相联系,可能是一个text程序在使用DLL。
  下面就是我们要做的。我们创建一个名为“EDRLIB。DLL”的DLL。文件名中的“EDR”代表“简便的绘图例程(easy drawing routines)”。这里的EDRLIB只含有一个函数(名称为EdrCenterText),但是您还可以将应用程序中其他简单的绘图函数添加进去。应用程序EDRTEST.EXE将通过调用EDRLIB.DLL中的函数来利用它。
  要做到这一点,需要与我们以前所做的略有不同的方法,也包括Visual C++ 中我们没有介绍过的特性。在Visual C++中“工作空间(worksdaces)”和“项目(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按钮。当您在ProjectName域输入EDRLIB时,Visual C++将改变Location域,以显示EDRLIB作为EDRTEST的一子目录。您不希望如此!在Location域,删除EDRLIB子目录以便项目创建在EDRTEST目录。现在按OK。屏幕将显示一个对话框,询问您创建什么类型的DLL。选择An Empty DLL Project,然后按Finish。Visual C++将创建一个项目文件EDRLIB.DSP和一个构造文件EDRLIB.MAK(如果Tools Options 对话框的Build 选项卡中选中了ExportMakefile选项)。
  现在您可以在此项目中加入一些文件。从File菜单选择New,然后选择Files选项卡,选择C/C++Header File,然后输入文件名EDRLIB.H。
  再次从File菜单中选择New,然后选择Files选项卡,这次选择C++ Source File,然后输入文件名EDRLIB.C,继续输入程序
EDRLIB.H
/*-----------------------
 EDRLIB.H header file
-------------------------*/
#ifdef _cplusplus
#define EXPORT extem "C" _declspec (dllexport)
#else
#define EXPORT _declspec (dllexport)
#endif
EXPORT BOOL CALLBACK EdrCenterTextA(HDC.PRECT.PCSTR):
#ifdef UNICODE
#define EdrCenterText EdrCenterTextW
#else
#define EdrCenterText EdrCenterTextA
#endif
EDRLIB.C
/*---------------------------------------------
EDRLIB.C——Easv Drawing Routine Library module
(c)Chades Petzold,1998
-----------------------------------------------*/
#include <windows.h>
#include "edrlib.h"
int WINAPI DIIMain(HINSTANCE hlnstance,DWORD fdwReason,PVOID pvReserved)
{
  return TRUE;
}

EXPORT BOOL CALLBACK EdrCenterTextA(HDC hdc,PRECT prc,PCSTR pString)
{
  int iLenglh;
  SIZE size;
  iLength=IstrlenA(pString);
  GetTextExtentPoint32A(hdc,pString,iLength,&size);
  return TextOutA(hdc,(prc->right-pro->left-size.cx)/2,
          (prc->bottom-prc->top-size.cy)/2,
          pString,iLeogth);
  GetTextExtentPoint32A(hdc,pString,iLength,&size);
  return TextOutA(hdc,(prc->right-pro->left-size.cx)/2,
            (prc->bottom-prc->top-size.cy)/2,
            pString,iLeogth);
}

EXPORT BOOL CALLBACK EdrCenterTextW(HDC hdc,PRECT prc,PCWSTR pString)
{
  int iLength;
  SIZE size;
  iLength=IstrlenW(pString);
  GetTextExtentPoint32W(hdc,pString,iLength,&size);
  return TextOutW(hdc,(prc->right-prc->left-size.cx)/2,
            (prc->bottom-prc->top-size.cy)/2,
            pShing。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串的指针),而EdrCenlerTextW则定义为带有参数PCWSTR(指向conet宽串的指针)。EdrCenter TextA函数将调用lstrlenA、GetTextExtentPoint32A和TextOutA。EdrCenterTextW将调用lstrlenW、GetTextExtentPoint32W和TextOutW。如果定义了UNICODE标识符,则EDRLIB.H将EdrCenterText定义为EdrCenterTextW,否则定义为FdrCenterTextA。这很像Windows头文件。
  EDBLIB.H也包含函数DllMain,取代了 DLL中的 WinMain。此函数用于执行初始化和取消始化,我将在本章的下一节讨论此问题。我们现在所需要的就是从DllMain返回TRUE。
  在这两个文件中,最后一点特殊之处就是定义EXPORT标识符。DLL中应用程序使用的数必须是“导出(exported)”的。这不是任何税务或者商业制度,只是确保函数名添加到EDRLIB.LIB的一个关键词(以便连接器在连接使用此函数的应用程序时,能够解析函数名),而且该函数在EDRLIB.DLL中也是可视的。EXPORT标识符包括保存类说明_declspec(dllexport),以及当头文件按C++模块编译时附加的“C”。这将防止编译程序破坏C++的函数名,而且能名允许C和C++程序都使用这个DLL。
库入口/出口点
  当库首次启动和结束时,我们调用了DllMain函数。DllMain的第一个参数是库的实例旬柄。如果您的库使用需要实例句柄(诸如DialogBox)的资源,那么您应该将hInstance保存为一个全局变量。DllMain的最后一个参数由系统保留。
  fdwReason参数可以是4个值这一,说明为什么Windows要调用DllMain函数。在下面的讨论中,请记住一个程序可以被多次加载,并在Windows下一起运行。每当加载一个程序时,它都被认为是一个独立的进程。
  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调用的DLLTHREAD_DETACH调用。当使用一个DLL_THREAD_DETACH参数调用DllMain时,线程仍然存在。动态链接库甚至可以在这个过程期间发送线程消息。但是它不应该使用PostMessage,因为线程可能在此消息被检索到之前已经退出了。
测试程序
  现在让我们在EDHTEST工作空间里创建第二个项目,程序名你为EDRTEST,而且使用EDRLIB.DLL在Visual C++中加载EDRTEST工作空间时,请从File菜单选择New然后在New对话框中选择Projects选项卡。这次选择Win32 Applieation,并确保选中Add To Current Workspace 按钮,输入项目名称EDRTEST,然后在Locations 域删除第二个EDRTEST子目录。按下OK,然后在下一个对框选择An Empty Project,按Finish。
  从File菜单再次选择New,选择Files选项卡,然后选择C++ Source File。确保AddToProject列表框显示的是EDRTEST而不是EDRLIB,输入文件名EDRTEST.C,然后输入程序。此程序用EdrCenterText函数将客户区中的文本串居中。
EDRTEST.C
/*-------------------------------------------------
EDRTEST.C----Program using EDRLIB dynamic—link library
(c)Chatles Petzold,1998
---------------------------------------------------*/
#include <windows.h>
#include "edrlib.h"
LRESULT CALLBACK WndProc(HWND,UINT,WPPARAM,LPARAM);
int WINAPI WinMain(HINSTANCE hlnstance,HINSTANCE hPrevlnstance,PSTR szCmdLine,int iCmdShow)
{
  static TCHAR szAppName[]=TEXT("StrProg");
  HWND hWnd;
  MSG msg;
  WNDCLASS wndclass;
  wndclass.style=CS_HREDPAW | CS_VREDRAW;
  wndcless.lpfnWndProc =WndPres;
  wndclass cbClsExtra =0;
  wndcless.cbWndExtra =0;
  wndclass.hlnstance =hlnstanes;
  wndclass.hicon =Londicon(NULL,IDI_APPLlCATION);
  wndclass.hCursor =LoadCursor(NULL,IDC_ARROW);
  wndclass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
  wndclass.lpszMenuName=NULL;
  wndcless.lpszClessName=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_OVEPLAPPEDWINDOW,
           CW_USEDEFAULT,CW_USEOEFAULT,
           CW_USEDEFAULT.CW_USEOEFAULT,
           NULL,NULL,hlnstance,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,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时,如果必要的话,都将在编译和连接EDHTEST之前重新连编EDRLIB。
  从Project菜单选择Settings,单击General标签。当您在左边的窗格中选择EDRLIB或者EDRTEST项目时,如果配置为Win32 Release,则显示在右边窗格中的IntermediateFiles和Output Files将位于RELEASE目录;如果配置为Win32 Debug,则位于DEBUG目录。如果不是,请按此修改。这样可确保 EDRILB.DLL与EDRTEST.EXE在同一个目录中,而且程序在使用DLL时也不会产生问题。
  在Project Setting对话框中依然选中EDRTEST,单击C/C++选项卡。按本书的惯例,在Preprocessor Definitions 中,将 UNICODE 添加到 Debug 配置。
  现在您就可以在 Debug 或 Release 配置中连编 EDRTEXT.EXE 了。必要时,VisualC++将首先编译和连接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,但在 Windows 下,这些应用程序不会相互影响。多个进程能够共享一个动态链接库中相同的代码。但是,DLL为每个进程保存的数据都不同。每个进程都为DLL使用的全部数据分配了自已的地址空间。我们将在下一节看到,在进程间共享内存还需要额外的工作。
  在DLL中共享内存令人兴奋的是,Windows能名将同时使用同一个动态链接库的应用程序分开。不过,有时却不太令人满意。您可能希望写一个DLL,其中包含能够被不同应用程序或者同一个程序的不同例程共享的内存。这包括使用共享内存,实际上是一个内存映射文件。
  让我们测试一下,这项工作是如何在程序STRPROC("串程序(string program)")和动志链接库STRLIB(“串库(string library)”)中完成的。STRLIB有三个输出函数被STRPROG调用,我们只对此感兴趣,STRLIB中的一个函数使用了在STRPROC定义的回调函数。
  STRLIB是一个动态链接库模块,它保存并排序最多256个字符串。在STRLIB中,这些串均为大写,并由共享内存维护。利用STRILB的三个函数,STRPROG能够添加串、删除串,以及从STRLIB获得当前的所有串。STRPROC测试程序有两个菜单项(Enter和Delete),这两个菜单项将激活不同的对话框来添加或删除串。STRPROG在其客户 区列出当前保存在STRLIB中的所有串。
  下面这个函数在STRLIB定义,它将一个串添加到STRLIB的共享内存。
EXPORT BOOL CALLBACK AddString(pStringln)
  参数pStringIn是串的指针。串在AddString函数中变成大写。如果在STRLIB的列表中有一个相同的串,那么此函数将加一个串的复本。如果成功AddString返回TRUE(非O),否则返回FALSE(0)。如果串的长度为0,或者不能分配保存串的内存,或者已经保存了 256 个串,则返回值将都是FALSE。
  STRLIB函和从STRLIB的共享内存中删除一个串EXPORT BOOL CALLBACK DeleteString(pString)另外,参数 pStringIn 是一个串指针。如果有多个相匹配的串,则删除第一个。如果成功,那么 DeleteString 返回TRUE(非0),否则返回FALSE(O)。返回FALSE表明串和长度为0,或者找不到相匹配的串。
  STRLIB函数使用了调用程序中的一个回调函数,以便列出目前保在STRLIB共享内存中的串EXPORT int CALLBACK GetStrings(pfnGetStrCallBack,pParam)在调用程序中,回调函数必须像下面那样定义:
EXPORT BOOL CALLBACK GetStrCallBark(PSTR pString,PVOID pParam)
  GetStrings 的参数 pfnGetStrCallBack。指向回调函数。直到回调函数返回FALSE(0),GetStrings将为每个串都调用一次GetStrCallBack。GetStrings 返回传递给回调函数的串数。
pParam参数是一个远指针,指向程序员定义的数据。
  当然,此程序可以编译成Unicode程序,或者在STRLIB的支持下,编译成Unicode和非Unicode应用程序。与EDRLIB一样,所有的函数都有"A"和"W"两种版。在内部,STRLIB按Unicode保存所有的串。如果非Unicode程序使用了STRLIB(也就是说,程序将调用AddStringA、DeleteStringA和GetStringsA)、串将在Unicode和非Unicode之间转换。
  与STRPROCG和STRLIB项目相关的工作空间名为STRPROG。此文件按EDRTEST工作空间的方式组合。程序显示了创建STRLIB.DLL动态链接库所必需的两个文件。
程序STRLIB库
STRLIB.H

/*---------------------------
  STRLIB.H header file
-----------------------------*/
#ifdef_cplusplus
#define EXPORT extern "C" _dectspec(dllexport)
#else
#define EXPORT extern "C" _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 GetStringA(GETSTRCB,PVOID);
EXPORT int CALLBACK GetStringW(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 GetString GetStringsA
#endif
STRLIB.C
/*---------------------------------------------
STRLIB.C----Library module for STRPROG prograrm
     (c)Charles Petzold,1998
-----------------------------------------------*/
#include <windows.h>
#include <wchar.h>
#inclube "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 data_seg ()
int WINAPI DIMain(HINSTANCE hinstance,DWORD fdwReason,PVOID pvReserved)
{
  return TRUE;
}
EXPORT BOOL CALLBACK AddStringA(PCSTR pStringin)
{
  BOOL bBeturn;
  int iLength;
  PWSTR pWideStr;
  //convert string to Unicode and call AddStringW
  iLength=MuitiByteToWideChar(CP_ACP,0,pStringln,-1,NULL,0);
  pWideStr=mailoc(iLength);
  MuitiByteToWideChar(CP_ACP,0,pStringln,-1,pWideStr,iLength);
  bRetum=AddStringW(pWideStr);
  free(pWideStr);
  return bRetum;
}
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,pStringln);
  _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++;
  flee(pString);
  return TRUE;
}
EXPORT BOOL CALLBACK DeleteStringA(PCSTR pStringln)
{
  BOOL bRetum;
  int iLength;
  PWSTR pWideStr;
  //Convert string to Unicode and call DeleteStringln,-1,NULL,0);
  iLength=MuitiByteToWideChar(CP_ACP,0,pStringln,-1,NULL,0);
  pWideStr=malloc(iLength);
  MuitiByteToWideChar(CP_ACP,0,pStringln,-1,pWideStr,iLength);
  bReturn=DeleteStringW(pWideStr);
  free(pWideStr);
  return bRetum;
}
EXPORT BOOL CALLBACK DeleteStringW(PCWSTR pStringln)
{
  int i,j;
  if(0==wcslen(pStringln))
    return FALSE
  for(i=0;i<iTotal;i++)
  {
    if(szStrings[i],pStringln)==0)
      break;
  }
  //if given string not in list,retum without taking action
  if(i==iTotal)
    return FALSE;
  //Else adjust list downward
  for(j=i ; j<iTotal ; j++)
    wcscpy(szStrings[i],szStrings[j+1]);
  szStrings[iTotal--][0]='\0';
  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 fron Unicode
    iLength=WideCharToMuitiByte(CP_ACP,0,szStrings[i],-1,NULL,0,NULL,NULL;
    pAnsiStr=malloc(iLength);
    WideCharToMuitiByte(CP_ACP,0,szString[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=pfnGetdStrCallBack(szStrings[i],pParam);
    if(bRetum==FALSE)
      return i+1;
  } 
  return iTotal
}
  除了DllMain函数以外,STRLIB中只有6个函数其他函数输出用。所有这函数都按EXPORT定义。这会使LINK在STRLIB.LIB输入库中列出它们。
STRPROG程序
STRPROG程序其内容是相当直观的。两个菜单选项(Enler和Delete)激活同一个对话框,让你输入一个串,然后格STRPROG调 用AddString或DeleteString。当程序需要新它的客户区时,请调用GetString,并使用函数GetStrCallBack来列出枚举的串。
STRPROG.C
/*----------------------------------------------------
STPRPOG.C----Program using STRLIB dynamic-link library
        (c)Charles Petzold,1998
------------------------------------------------------*/
#include<windows.h>
#include "strlib.h"
#include "resource.h"
typedet struct
{
  HDC hdc
  int xText;
  int yText;
  int xStart;
  int yStart;
  int xlncr;
  int ylncr;
  int xMax;
  int yMax;
}
CBPARAM;
URSULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
TCHAR szAppName[]=TEXT("StrProg");
TCHAR szString [MAX_LENGTH+1];
int WINAPI WinMain(HINSTANCE hlnstance,HINSTANCE hPrevlnstance,PSZR szCrndLine,int iCmdShow)
{
  HWND hwnd;
  MSG msg;
  WNDCLASS wndclass;
  wndclass.style=CS_HREDRAW | CS_VREDRAW;
  wndclass.lpfnWndProc=WndProc;
  wndclass.cbClsExtra=0;
  wndclass.cbWndExtra=0;
  wndclass.hlnstance=hlnstance;
  wndclass.hlcon=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=CreateWindows(szAppName,TEXT("DLL Demonstration Program"),
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,CW_USEDEFAULT,
            CW_USEDEFAULT,CW_USEDEFAULT,
            NULL,NULL,hlnstance,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 iParam)

  switch{message)
  {
    case WM_INITDIALOG:
    SendDlgItemMessage(hDig,IDC_STRING,EM_LIMITTEXT,MAX_LENGTH,0);
    return TRUE;

    case WM_COMMAND:
    switch(wParam)
    {
      case IDOK:
      GetDigitemText(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->ylncr)>pcbp->yMax)
  {
    pcbp->yText=pcbp->yStart;
    if((pcbp->xText+=pcbp->xlncr)>pcbp->xMax)
      return FALSE;
  } 
  return TRLE;
}
LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM iParam)
{
  static HINSTANCE hlnst;
  static int cxChar,cyChar,cxClient,cyClient;
  static UINT iDataChangeMsg;
  CBPARAM cbparam;
  HDC hdc;
  PAINTSTRUCT ps;
  TEXTMERIC tm;
  switch(message)
  {
    case WM_CREATE:
    hlnst=((LPCREATESTRUCT(iParam)->hlnstance;
    hdc=GetDC(hwnd);
    GetTextMetrics(hdc,&trn);
    cxChar=(int)trn.trnAveCharWidth;
    cyChar=(int)(trn.trnHeight+trn.trnExternalLeading);
    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(hlnst,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)LOWORD(lParam);
    return 0;

    case WM_PAINT:
    hdc=BeginPaint(hwnd,&ps);
    cbparam.hdc=hdc;
    cbparam.xText=cbparam.xStart=cxChar;
    cbparam.yText=cbparam.yStart=cyChar;
    cbparam.xlncr=cxChar * MAX_LENGTH;
    cbparam.ylncr=cyChar;
    cbparam.xMax=cbparam.xlncr * (1 + cxClinet/cbparam.xlncr);
    cbparam.yMax=cyChar * (cyClient/cyChar-1);
    GetString((GETSTRCB)GetStrCallBack,(PVOID)&cbparam);
    EndPaint(hwnd,&ps);
    return 0;

    case WM_DESTROY:
    PostQuitMessage(0);
    return 0;

    defauit:
    if(message==iDataChangeMsg)
      lnvalidateRect(hwnd,NULL,TRUE);
    break;
  }
  return DefWindowProc(hwnd,message,wParam,lparam);
}
STRPROG.RC(摘录)
//Microsoft Developer Strudio 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
LIEXT "&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 DISCARUABLE
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在程序的所有实例这间共享数据是毫无问题的。当您在一个要STPRPOG窗口中添加或删除一个串时,这种改变将立即反映在其他的窗口中。
  在它的所有例程这间,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段。在Proiect Settings对话框选择Link选项卡,选中STRLIB时在Project Options域(在Release和Debug配置中均可),包含下面的连接语句:
/SECTION:shared,HWS
  字母RWS表明段具读、写和共享属性。或者,您也可以直接用DLL源代码指定连接选项,就像我们在STRLIB.C 那样:
#pragma comment(linker,"/SECTION:shared,RWS")
  共享的内存段允许不iTotal变量和szStrings字符串数组在STRLIB的所有例程之间共享。因为MAX_STRINGS字符串数组等于256,而MAX_LENGTH等于63,所以共享内存段的长度应为32772字节----iTotal变量需要4字节,256个指针中的每一个都需要128字节。
  使用共享内存段可能是在多个应用程序间共享数据的最简单的方法。如果需要动态分配共享内存空间,您应该查看对象映射文件的用法,文档在/Platform SDK/Windows Base Services/Interprocess Communication/File Mapping。
各种各样的DLL主题
  如前所述,动态链接库模块不接收消息,但是库模块可以调用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 hLinbrary;
PFNRECT pfnRectangle;
现大将hLibrary设置为库句柄,将lpfnRectangle设置为Rectangle函数的地址:
hLibrary=LoadLibrary(TEXT("GDI32.DLL"))
pfnRectangle=(PRNPRECT)GetProcAddress(hLibrary,TEXT("Rectangle"))
  如果找不到库文件,或者发生其他一些错误,LoadLibrary函数返回NULL。现在您可以调用如下函数,然后释放库:
pfnRectangle(hdc,xLeft,yTop,xRight,yBootom);
Freelibrary(hLiBrary);
  尽管这项运行时动态链接的技术并没有为Rectangle函数增加多大好处,但它肯定是有用的,如果直到运行时还不知道库模块的名称,这时就需要用它。
  上面的代码使用了LoadLibrary和FreeLibrary函数。Windows为所有的库模块提供"引用计数",LoadLibrary使引用计算递增。当Windows加载任何使用库的程序时,引用计数也会递增。FreeLibrary使引用计数递减,在使用库的程序实例结束时也是如此。当引用计数为0时,Windows将从内存中把库删除掉,因为不再需要它了。
纯资源库
  可由Windows程序或其他库使用的动态链接库中的任何函数都 必须被 输出。然而.DLL也可以不包含任何输出函数。那么,DDL到底包含什么呢?答案是资源。
  假设用户正在使用需要几幅位图的Windows应用程序进行工作。通常要在程序的资源描述文件中列出资源,并用LoadBitmap函数把它们加载到内存中。但用户可能希望创建若干套位图,每一套均适用于Windows所使用的不同显示适配器。将不同套的位图存放到不同文件中可能是明智的,因为只需要在硬盘上但留一套位图。这些文件就是纯资源文件。
  程序说明如何创建包含9幅位图的名为BITLIB.DLL的纯资源库文件。BITLIB.RC文件列出了所有独立的位图文件,并为每个文件赋予一个序号。为了创建BITLIB.DLL,需要9幅名为BITMAP1.BMP、BITMAP2.BMP等位图。您可使用随书附带的光盘上提供的位图,或者在Visual C++中创建这些位图。它们与ID号从1到9相对应。
程序BITLIB库
BITLIB.C

/*----------------------------------------------------------
BITLIB.C----Code entry point for BITLIB dynamic-link library
        (c)Charles Petzold,1998
------------------------------------------------------------*/
#include<windows.h>
int WINAPI DllMain(HINSTANCE hlnstance,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的另一个项目中,创建程序所示的SHOWBIT程序,这与前面的一样。不过,不要使BITLIB依靠SHOWBIT:否则,连接过程中将需要BITLIB.LIB文件,并且因为BITLIB没有任何输出函数,它也不会创建。事实上分别连编BITLIB和SHOWBIT,可以交替设置其中一个为"Active Project",然后再连编。
  SHOWBIT.C从BITLIB读取位图资源,然后顺其客户区显示。按键盘上的任意键可以循环显示。
程序SHOWBIT程序
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 hlnstance,HINSTANCE hPrevlnstance,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.hlnstance=hlnstance;
  wndclass.hlconc=Loadlcon(NULL,lDI_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,hlnstance,NULL);
  if(!hwnd)
    return 0;
  ShowWindow(hvmd,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)
{
  BITMAF bm;
  HDC hMemDC;
  POINT pt;
  hMemDC=CreateCompatibleDC(hdc);
  SelectObject(hMemDC,hBitmap);
  GetObject(hBitmap,sizeof(BITMAP),&bm);
  pt.x=bm.bmWitdth;
  pt.y=bm.bmHeight;
  BitBit(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;
  switct(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(hvmd,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);
    PostOuitMessage(0);
    return 0;
  }
  return DefWindowProc(hwnd,message,wParam,lParam);
}
在处理WM_CREATE消息期间,WHOWBIT获得了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,并且释放占用的内存。这就是实现“剪贴画”程序的一种简单方法,所谓的“剪贴画”程序就是能够将预先创建的位图(或者元文件、增强元文件)加载到供其他程序使用的剪贴板。
  动态链接库允许Windows应用程序共离资源和代码,动态链接库DLL(Dynamics Link Library)实际上足一个可执行模块,它包台了Windows应用程序所需要的函数,DLL在程序远行时与应用程序动态他链接,在程序结束时DLL与应用程序动态地失去链接。
静态链接和动态链接
  在程序中大量使用库函数,库函数的作用机制是一种静态链接的机制,当在创建可执行文件时,先将C/C++源代码编译成目标代码,然后用链接程序将目标码与库函数链接起来,最后生成可执行代码。这种用链接程序将目标代码和库函数柜链接的方式叫做静态链接,静态链接能方便地为所有的应用程序担可用的函数集,应用程序无须重写这些函数的源代码,链接程序在链序在链接期间将相应的信息拷贝到可执行文件中。
  在Windows操作系统中,应用程序在多任务环境中共离内存资源,因此,静态链接往往导致资源浪费。例如,两个应用程序使用相同的C/C++库函数时,用静态链接的方式,则函数在链接时拷贝给每个应用程序,结果在内存中每个函数拷贝了两份。如果在程序中采用础奄铸接,两个应用程序共享资源和函数,因而可使内存和系统资源的使用率大大提高。
  为了指示应用程序运行时从何处找到所需要的代码,Windows操作系统提供了一个称为输入库(Input Library)的机制。输入库帮助应用程序在DLL中寻找代码。输入库中包含了应用程序模块和DLL模块相联系的信息。一个应用程序模块是该应用程序的EXE文件,它包含了应用程序的代码;DLL模块是包含库代码的模块。并用往往具有下列的扩展名:.dll、.drv和.fon等,其中包含了可以进行动态链接的可执行模块。应用程序执行时,当需要从动志链接库中调用函数时,链接程序并不拷贝函数的代码,它只是从输入库中拷贝一些信息,这些信息用米指示程序运行时从何处找到所要求的代码。输入库文件的后缀一般为.lib。
一个动态链接库和一个可执行文件之间的别主要体现在以下几个方面
  1、一个DLL要求个DEF文件:
  2、一个DLL有一个DllMain的入口点,而不是传统的WinMain入口点:
  3、一个DLL可以装入,但永远不能直接执行。
DLL的创建过程
  启动Visual C++编译器后,选择File菜单下的New菜单项,这时将弹出如图所示的对话框。

  在图左边的列表框中选择Win32 Dynamic_Link Library项,在Project name文本框中输入新建的动态链接库的项目名称,然后单击OK按钮,这时弹出如图所示的对话框。

Win32 Dynameic-Link Library-Step 1 of 1对话框
在图中.Visual C++编译器提供了如下三种方式来创建动态链接库:
1、An empty DLL project 空的DLL项目;
2、A simple DLL project 一个简单的DLL项目;
3、A DLL that exports some symbols 输出符号的DLL项目。

New Project Information对话框
  图中列出了当前新建项目的特征信息,单击OK按钮,则返回Visual C++编译器的主界面。
DLL的代码结构
  如果想要通过Visual C++6.0设计自已的动态链接库,只需在新建一个项目时选择新建的项目类型为DLL,然后输入相应的源程序,经过编译,调试后就可以得到相应的DLL文件。
  动态链库的源程序结构与普通的Windows应用程序有较大差别,它的一般形式如下:
#include<windows.h>
int WINAPI LibMain(HANDLE hInstance,UINT wDataSeg,UINT wHeapSize,LPSTR lpszCmdLine)
{
  //动态链库初始化部分
  return 1;
}
//输出函数
void WINAPI Routine1 (int iParam1,int iParam2)
{
  //Routine函数体
}
//输出函数
void WINAPI Routine2(int iParam1,int iParam2)
{
  //Routine函数体
}
  DLL程序的源代码一般可以分成两个部分,一是LibMain,它是Windows DLL的主入口点,负责DLL的初始化,二是Routine函数,DLL代码中可以包含一个或多个输出函数,也可不需要Routine函数。
  一个DLL文件一般还需要一个模块定义文件,该文件指定应用程序模块的各种属性。
模块定义文件的后缀为.def,其一般格式如下:
LIBRARY动态链接库名称
DESCRIPTION动态链接库的描述
EXPORTS
  输出函数名1
  输出函数名1
```
  其中EXPORTS部分列出了所有需要在动态链库中输出函数的函数名。只有在其中被列出的函数才能被其他的应用程序所调用。
  下面是一个简单的DLL文件的源程序,通过它可以加深对DLL结构的认识。
#include <windows.h>
#include <string.h>
#define EXPORT
#include "text.h"
//函数:DllMain
//作用:DLL入口函数
int WINAPI DllMain(HINSTANCE hInstance,DWORD fdwRcason,PVOID pvReserved)
{
  return TRUE
}
//函数:OutputText
//作用:输出函数
EXPORT BOOL CALLBACK OutputText(HDC hdc,PRECT prc,PSTR pString)
{
  int iLength;
  SIZE size;
  iLength=strlen(pString);
  GetTextExtentPoint32(hdc,pString,iLength,&size);
  retrun TextOut(hdc,(poc->right-prc->left-size.cx)/2,
         (prc->bottom-prc->top-size.cy)/2,
         pString,iLength);
}
头文件text.h如下所示:
#define EXPORT extern "C" _declspec(dllexport)
EXPORT BOOL CALLBACK EdrCenterText(HDC,PRECT,PSTR);
其对应的模块定义文件如下所示:
LIBRARY mydll
DESCRIPTION "动态链接库实例"
EXETYPE WINDOWS
HEAPSIZE 1024
EXPORTS
OutpytText
其中函数EdrCenterText可以被其他的应用程序所调用,函数的功能是在窗口中输出一段文本。
动态链接库设计
下面通过一个实例来说明动态链接库的设计。
【例】 动态链接库设计实例
下面的程序中创建了一个动态链接库,它包含如下三个输出函数:
  1、EnterString 输入字符串。
  2、DeleteString 删除字符串。
  3、GetStrings 输出所有的字符串。
生成该动态链接库的模块定义文件如下所示:
LIBRARY dlltest
DESCRIFTION "DLL EXAMPLE"
EXETYPE WINDOWS
HEAPSIZE 1024
EXPORTS
EnterString
DeleteString
GetStrings
在动志链接库的源程序中,首先定义了一个用于记录字符串属性的结构体CDPARAM。然后定义了一个记录字符串内容的指针pszStrings,变量iTotal用来记录字符串数目。在源程序的头部还定义了一个用于获取字符串信息的函数GetStr,这个函数不是输出函数,而是在动态链接库内部使用的一个函数,因此没自在模块定义文件的 EXFORTS中列出函数名GetStr。
动态链接库的入口是DllMain,该函数的原型不能改变的,源型定义如下所示:
BOOL WINAPI DllMain(HINSTANCE hinstDll, //DLL模块句柄
          DWORD fcwReason, //调用函数的原因
          LPVOID lpvReserved //系统保留
);
其中,fdwReason参数的取什可以是表中所列值之一。
表fdwReason数的取值

参数
说明
DLL_PROCESS_ATTACH进程调用动态链接库
DLL_THREAD_ATTACH线程调用动态链接库
DLL_THREAD_DETACH线程卸载动态链接库
DLL_PROCESS_DETACH进程卸载动态链接库

  在例中,DllMain函数仅仅简单地返回TRUE值,而忽略了所有其他的响应,当然,也可以在DllMain函数中加入诸如对DLL初始化如何进行操作的语名,但这不是必须的。
  在函数EnterString中,用到了函数CreateFileMapping,这个函数将为指定的文件创建一个文件映像对象,其函数原型如下所示:
HANDLE CreatuFileMapping (HANDLE hFile, //文件句柄
             LPSECUKITY ATTRIBUTES lpFileMappingAttribbutes, //安全属性指针
             DWORD flProtect, //映像对象的保护模式
             DWORD dwMaximumSizeHigh, //映像对象的高32位值
             DWORD dwMaximumSizeLow, //映像对象的低32位值
             LPCTSTR lpName //映像对象指针);
参数lpFileMappingAttributes是一个指向结构体SECURITY_ATTRIBUTES变量的指针,其具体定义如下所示:
typcdef struct_SECURITY_ATTRIBUTES{//sa
  DWORD nLength; //结构体的大小
  LPVOID lpSeeurityDescriptor, //安全描述指针
  BOOL binhcritHandle; //继承标志
}SECURITY_ATTRIBUTES;
如果,blnheritHandie为TRUE,则表示该映像对象可以被新建的其他进程所继承。
参数flProtect用于指定倮护模式,它可以是表中所示值之一。
表参flProtect的取值

取值
说明
PAGE_READONLY只读
PAGE_READWRITE可读和写
PAGE_WRITECOPY可复制和写

函数MapViewOfFile的作用是把文件的一个视图映射到进程的自存空间之中。函数MapViewOfFile的原型定义如下:
LPVOID MapViewOfFile( HANDLE hFileMappingObject, //文件映像对象句柄
           DWORD dwDesiredAccess, //访问模式
           DWORD dwFileOffsetHigh, //文件编移值的高32位
           DWORD dwFileOffsetLow,//文件编移值的低32位
           DWORD dwNumberOfBytesToMap //映像字节数); 
其中,参数dwDesiredAccess指定的访问模式可以是表中所示值之一。
表dwDesiredAccess的访问横式

访问模式
说明
FILE_MAP_WRITE可写的文件视图
FILE_MAP_READ可读的文件视图
FILE_MAP_ALL_ACCESS具有所有访问权限的文件视图
FILE_MAP_COPY可复制的文件视图

  为了便于两个字符串的比较,在EnterString中调用函数CharUpper把一个字符串中的所有字符都转化成大写。函数CharUpper的原型定义如下所示:
LPTSTR CharUpper(LPTSTR lpsz //字符中指针);
  函数DeleteString用于删除一个字符串,为了找到和输入的字符串相同的字符串,在函数DeleteString中也用到了一个for循环,这个循环用于比较已有的字符串中是否有一个和要删除的字符串相同的字符串。如果没有,则返回FALSE值;如果有,则删除这个字符串,并把字符串列表中位于该字符串以后的字符串依次向前移动一位。
  函数GetStrings用于获得整个字符串集合。其核心功能实际上是通过对自数GetStr的调用来实现的。
函数GetStr的具体定义如下所示;
BOOL CALLBACK GetStrCallBack(PSTR pString,CBPARAM *pcbp)
{
  TextOut(pcbp->hdc,pcbp->xText,pcbp->yText,pString,strlen(pString));
  if((pcbp->yText+=pcbp->yIncr)>pcbp->yMax)
  {
    pcbp->yText=pcbp->yStart;
    if((pcbp->xText+=pcbp->xIncr)>pcbp->xMax)
      return FALSE;
  }
  return TRUE;
}
  其中,函数TextOut是字符串输出函数,程序中通过它输出一个字符串,GetStr函数中if语名块用来判断输出位置,如果超出了Y方向的最大值,则返回工作区的开始位置,如果超出了X方向的最大置,则返回FALSE。
  编泽上述文件后,将产生一个名为dlltest.dll的动态链接库文件和一个名为dlltest.lib的导出文件(其中dlltest为项目名)。
例1:
#include <windows.h>
#define MAX_STRINGS 256//最大字符串数
typedef struct
{
  HDC hdc;
  int xText;
  int yText;
  int xStart;
  int yStart;
  int xIncr;
  int yIncr;
  int xMax;
  int yMax;
}CBPARAM;
PSTR pszStrings[MAX_STRINGS];
int iTotal = 0;//当前字符串数目
BOOL CALLBACK GetStr(PSTR pString,CBPARAM *pcbp);
//函数:DllMain
//作用:DLL入口
int WINAPI DllMain (HINSTANCE hInstance,DWORD fdwReason,PVOID pvReserved)
{
  return TRUE;
}
//函数:EnterString
//作用:输入字符串
int WINAPI EnterString (PSTR pStringIn)
{
  HANDLE hString;
  PSTR pString;
  int i, iLength, iCompare;
  if (iTotal == MAX_STRINGS - 1)//达到最大字符串数目
    return FALSE;
  iLength = strlen (pStringIn);//获取字符串长度
  if (iLength == 0)//空字符串
    return FALSE;
  //创建映像文件
  hString=CreateFileMapping((HANDLE)-1,NULL,PAGE_READWRITE, 0, 1 + iLength, NULL);
  if (hString == NULL)
    return FALSE;
  pString = (PSTR) MapViewOfFile (hString, FILE_MAP_WRITE, 0, 0, 0);
  strcpy (pString, pStringIn);
  AnsiUpper (pString);//全部转换成大写
  for (i = iTotal; i > 0; i--)
  {
    iCompare = strcmpi (pStringIn, pszStrings[i - 1]);
    //比较字符串
    if (iCompare >= 0)//字符出不等则调处循环
      break;
    pszStrings[i] = pszStrings[i - 1];
    //字符出相等则删除字符串
  }
  pszStrings[i] = pString;
  iTotal++;
  return TRUE;
}
//函数:DeleteString 
//作用:输入字符串
int WINAPI DeleteString(PSTR pStringIn)
{
  int i, j, iCompare;
  if (0 == strlen (pStringIn))//忽略输入的空字符串
    return FALSE;
  for (i = 0 ; i < iTotal ; i++)//字符串排序
  {
    iCompare = lstrcmpi (pszStrings[i], pStringIn);
    if(iCompare == 0) break;
  }
  if (i == iTotal) return FALSE;
  UnmapViewOfFile (pszStrings[i]);
  for (j = i ; j < iTotal ; j++)
    pszStrings[j] = pszStrings[j + 1];
  pszStrings[iTotal--] = NULL; 
  return TRUE;
}
//函数:GetStrings
//作用:输出所有的字符串
int WINAPI GetStrings(CBPARAM * pParam)
{
  BOOL bReturn;
  int i;
  for (i = 0; i < iTotal; i++)
  {
    bReturn = GetStr(pszStrings[i], pParam);
    if (bReturn == FALSE)
      return i + 1;
  }
  return iTotal;
}
//函数:GetStrCallBack
//作用:输出一个字符串
BOOL CALLBACK GetStr(PSTR pString, CBPARAM *pcbp)
{
  TextOut (pcbp->hdc, pcbp->xText,pcbp->yText,pString, strlen (pString));
  if ((pcbp->yText += pcbp->yIncr) > pcbp->yMax)
  {
    pcbp->yText = pcbp->yStart;
    if ((pcbp->xText += pcbp->xIncr) > pcbp->xMax)
      return FALSE;
  }
  return TRUE ;
}
动态链接库的应用
  上节建立的动态链接库还必须经过严格的测试后才能投入使用。下面是一个在应用程序中使用例创建的动态链接库的例子。
[例]动态链接库的应用
  可以专门针对动态链接库设计一个应用程序,这个应用程序要使用动态链库中定义的输出函数,并把调用这输出函数的结果显示出来。
  应用程序中要使用某个动态链接库,必须先把动态链接库“导入”动应用程序中,这一过程可以通过以下两个步骤来实现。
  首先复制动态链接库(后缀为.dll)和相应的导出文件(后缀为.lib)到可执行文件后在目录下。例如,要在可执行文件中调用例10-1中生成的动态链接库,就必须复制dlltest.dll和dlltest.lib到当前项目所在的debug目录下。
  复制文件完成后,还必须设置文件的链接属性。单击Project菜单下的Settings菜单项,这时将弹出如图所示的对话框。

  在图中的Object/library moduless栏中输入dlltest.lib,然后单击OK按钮。这样应用程序通过这个加入的dlltest.lib的导出文件就能找到动态链接库dlltest.dll中的函数。
例 所用头文件exetest.h如下所示:
#define IDM_ENTER 1
#define IDM_DELETE 2
#define IDD_STRING 0x10
例 所用资源文件如下所示:
#include "exetest.h"
//菜单
EXETEST MENU DISCARDABLE
BEGIN
  MENUITEM "输入", IDM_ENTER
  MENUITEM "删除", IDM_DELETE
END
//输入对话框
ENTERDLG DIALOG DISCARDABLE 24.24.190.44
STYLE DS_MODALFRAME | WS_POPUP
FONT 10,"System"
BEGIN
LTEXT "输入",0,4,8,24,8
EDITTEXT IDD_STRING,32,6,154,12
//删除对话框
DEFPUSHBUTTON ”确认”。IDOK,44,24,32,14
PUSHBUTTON ”关闭”,IDCANCEL,114,24,32,14
DELETEDLG DIALOG DISCARDABLE 24,24,190,44
STYLE DS_MODALFRAME | WS_POPUP
FONT 10,"System"
BEGIN
LTBXT "删除",0,4,8,28,8
EDITTEXT IDD STKING,36,6,150,12
DEFPUSHBUTTON "确认",IDOK,44,24,32,14
PUSHBUTTON "关闭",IDCANCEL,114,24,32,14
END
  在例的源序中,首先对DLL中的函数进行了声明,只有重新声明了的函数才能在程序中被调用。
  在例的源程序中,首先对DLL中的函数进行了声明,只有重新声明了的函数才能在程序中被调用。当用户单击输入莱单项时,应用程序调用函数DialogBox创建一个对话框,对话框外理函数把用户输入的字符串保存到全局变量szString中,然后再调用在动态链接库dlltest.dll中定义的函数E性腺EnterString实现字符串输入。
  当用户单击输出菜单项时,应用程序也会弹出一个对话框,这个对话框要求用户输入要删除的字符串。然后再调用在动态链接库dlltest.dll中定义的函数DeleteString删除该字符串。
  在每次重画窗口时,都调用了在动态链接库dlltest.dll中定义的函数GetStrings来输出所有的享符串。由以上的分析可以手出,调用动态链接库中的函数和调用在本程序中定义的函数并设有什么不同之处,动态链接的概念对程序员来讲是完全透明的。
  执行后,单击主窗口中的输入菜单项,将弹出一个对话框,当输入如图所示的一个字符串并单出"确认"按钮后,相应的字符串将显示在应用程序的工作区中。
  单击删除菜单项,这时也会弹出一个对话框,在其中输入要删除的字符串,如图所示,则相应的字符串将被删除,同时应用程序的工作区中也不会再显示该字符串。
#include<windows.h>
#include<string.h>
#include"exetest.h"
#define MAXLEN 32
#define MAX_STRINGS 256 //最大字符串数
#define WM_DATACHANGE WM_USER
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);
//声明动态链接库中的函数原型
int WINAPI EnterString (PSTR pStringIn);
int WINAPI DeleteString (PSTR pStringIn);
int WINAPI GetStrings(CBPARAM * pParam);
char szAppName[] = "exetest";
char szString[MAXLEN];
//函数:WinMain
//作用:程序入口
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,PSTR szCmdLine,int iCmdShow)
{
  HWND hwnd;
  MSG msg;
  WNDCLASSEX wndclass;
  wndclass.cbSize = sizeof (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;
  wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
  RegisterClassEx (&wndclass); 
  hwnd = CreateWindow (szAppName, 
             "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;
}
//函数:DlgProc
//作用:对话可小型处理
BOOL CALLBACK DlgProc (HWND hDlg, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
  switch (iMsg)
  {
    case WM_INITDIALOG://初始化窗口
    SendDlgItemMessage (hDlg, IDD_STRING,EM_LIMITTEXT,MAXLEN - 1,0);
    return TRUE;

    case WM_COMMAND://响应用户在对话框中的操作
    switch (wParam)
    {
      case IDOK://用户单击确认按钮
      //获取字符串
      GetDlgItemText (hDlg, IDD_STRING, szString, MAXLEN);
      EndDialog (hDlg, TRUE);//关闭对话框
      return TRUE;

      case IDCANCEL://用户单击关闭按钮
      EndDialog (hDlg, FALSE);//关闭对话框
      return TRUE;
    }
  }
  return FALSE ;
}
//函数:EnumCallBack
//作用:通知主窗口中的数据是否发生变化
BOOL CALLBACK EnumCallBack (HWND hwnd, LPARAM lParam)
{
  char szClassName[16] ;
  GetClassName (hwnd, szClassName, 
  sizeof (szClassName));//获取窗口类名
  if (0 == strcmp (szClassName, szAppName))
  SendMessage (hwnd, WM_DATACHANGE, 0, 0) ;
  return TRUE ;
}
//函数:WndProc
//作用:处理主窗口消息
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
  static HINSTANCE hInst;
  static int cxChar, cyChar, cxClient, cyClient;
  CBPARAM cbparam;
  HDC hdc;
  PAINTSTRUCT ps;
  TEXTMETRIC tm;
  switch (iMsg) 
  {
    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);
    return 0 ;

    case WM_COMMAND:
    switch (wParam)
    {
      case IDM_ENTER ://单击输入菜单项
      //生成添加对话框
      if (DialogBox (hInst, "EnterDlg", hwnd, &DlgProc))
      {
        if (EnterString (szString))//调用DLL中定义的函数EnterString
          EnumWindows (&EnumCallBack, 0);
        else
          MessageBeep (0);
      }
      break;

      case IDM_DELETE://单击删除菜单项
      //生成删除对话框
      if (DialogBox (hInst, "DeleteDlg", hwnd, &DlgProc))
      {
        if (DeleteString (szString))
          EnumWindows (&EnumCallBack, 0);
        else
          MessageBeep (0);
      }
      break;
    }
    return 0;

    case WM_SIZE://改变窗口大小
    cxClient = (int) LOWORD (lParam);
    cyClient = (int) HIWORD (lParam);
    return 0;

    case WM_DATACHANGE://工作区中的数据发生变化
    InvalidateRect (hwnd, NULL, TRUE);
    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 * MAXLEN;
    cbparam.yIncr = cyChar;
    cbparam.xMax = cbparam.xIncr * (1 + cxClient / cbparam.xIncr);
    cbparam.yMax = cyChar * (cyClient / cyChar - 1);
    //调用动态链接库定义中的函数GetStrings
    GetStrings (&cbparam);
    EndPaint (hwnd, &ps);
    return 0;

    case WM_DESTROY://应用程序结束
    PostQuitMessage (0);
    return 0;
  }
  return DefWindowProc (hwnd, iMsg, wParam, lParam);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值