windows程序设计 Fifth 笔记(一)
第一章 开始
早期,Windows的主要部分仅通过三个动态连结程式库实作。Kernel、User和GDI。Kernel(日前由16位元的KRNL386.EXE和32位元的KERNEL32.DLL实现)处理所有在传统上由作业系统核心处理的事务-记忆体管理、档案I/O和多工管理。User(由16位的USER.EXE和32位的USER32.DLL实作)指使用者介面,实作所有视窗运作机制。GDI(由16位的GDI.EXE和32位的GDI32.DLL实作)是一个图形装置介面,允许程式在萤幕和印表机上显示文字和图形。
当您执行Windows程式时,它通过一个称作「动态连结」的过程与Windows相接。一个Windows的 .EXE文件拥有使用的不同动态连结库的引用,所使用的函数即在那些动态连结程式库中。当Windows程序被载入到内存中时,程序中的调用被指向DLL函数的入口。如果该DLL不在内存中,就把它载入到记忆体中。
当您连结Windows程序以产生一个可执行文件时,您必须连结编程环境提供的特定「导入库(import library)」。这些导入库包含了动态连结库名称和所有Windows函数调用的引用信息。链接程序使用该信息在.EXE文件中建立一个表,在加载程序时,Windows使用它将调用转换为Windows函数。
第二章 Unicode 简介
DBCS(double-byte character set)宽字节字符集,仍然处理8位值
Unicode是ASCII字符编码的一个扩展,使用全16位字符编码,不处理8位 。windows NT从底层支持Unicode。
宽字符:支持多和字节代表一个字符的字符集
宽字符不需要Unicode,Unicode是一种可能的宽字符编码. 本书把宽字符和Unicode作为同义词
C中的宽字符基于wchar_t,在几个头文件(包括WCHAR.H)中都有定义
typedef unsigned short wchar_t;
wchar_t c = 'A';
不用加L,编译器会对该字符进行零扩充
c是一个双字节值0x0041,因为intel处理器首先从最低位字节开始存储多字节数值,所以该字符实际以0x41、0x00的顺序存放在内存中。
wchar_t *pw = L"Hello!"; iLength = strlen (pw);// from 'unsigned short *' to 'const char *'
继续运行iLength会等于1
字串Hello!:0x0048 0x0065 0x006C 0x006C 0x006F 0x0021
Intel处理器在内存中将其存为:48 00 65 00 6C 00 6C 00 6F 00 21 00,strlen遇字节0返回
strlen的宽字符版本是wcslen,在STRING.H(也声明了strlen)和WCHAR.H中都有声明
size_t __cdecl strlen (const char *) ; size_t __cdecl wcslen (const wchar_t *) ;
由于使用Unicode每个字符都要两倍空间,宽字符运行库中函数也比常规的大。所以想维护既能按ASCII编译又能按Unicode编译的单一源码文件。
一个办法是使用Microsoft Visual C++包含的TCHAR.H头文件。该头文件不是ANSI C标准的一部分,因此那里定义的每个函式和宏定义的前面都有一条下划线。TCHAR.H为需要字串参数的标准运行库函数提供了一系列的替代名称(例如,_tprintf和_tcslen)。有时这些名称也称为「通用」函式名称,因为它们既可以指向函式的Unicode版也可以指向非Unicode版。
TCHAR.H中的部分声明:
#ifdef _UNICODE typedef wchar_t TCHAR ; #define _tcslen wcslen #define __T(x) L##x #else typedef char TCHAR ; #define _tcslen strlen #define __T(x) x #endif #define _T(x) __T(x) #define _TEXT(x) __T(x)
Windows程序都包括WINDOWS.H。该投文件包括许多其他头文件,包括WINDEF.H,该头文件中有许多在Windows中使用的基本类型定义,而且它本身也包括WINNT.H。WINNT.H处理基本的Unicode支持。
WINNT.H的前面包含C的头文件CTYPE.H,这是C的众多头文件之一,包括wchar_t的定义。WINNT.H定义了新的数据类型,称作CHAR和WCHAR:
typedef char CHAR ; typedef wchar_t WCHAR ; // wc
当您需要定义8位字符或者16位字符时,推荐您在Windows程序中使用的数据型态是CHAR和WCHAR。WCHAR定义后面的注释是匈牙利标记法的建议:一个基于WCHAR数据型态的变量可在前面附加上字母wc以说明一个宽字符。
WINNT.H表头文件进而定义了可用做8位字符串指针的六种数据型态和四个可用做const 8位字符串指针的数据型态。
typedef CHAR * PCHAR, * LPCH, * PCH, * NPSTR, * LPSTR, * PSTR; typedef CONST CHAR * LPCCH, * PCCH, * LPCSTR, * PCSTR;
前缀N和L表示「near」和「long」,指的是16位Windows中两种大小不同的指标。在Win32中near和long指标没有区别。
类似地,WINNT.H定义了六种可作为16位字符串指针的数据型态和四种可作为const 16位字符串指针的数据型态:
typedef WCHAR * PWCHAR, * LPWCH, * PWCH, * NWPSTR, * LPWSTR, * PWSTR ; typedef CONST WCHAR * LPCWCH, * PCWCH, * LPCWSTR, * PCWSTR ;
与TCHAR.H一样,WINNT.H将TCHAR定义为一般的字符类型。
#ifdef UNICODE typedef WCHAR TCHAR, * PTCHAR ; typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR ; typedef LPCWSTR LPCTSTR ; #define __TEXT(quote) L##quote #else typedef char TCHAR, * PTCHAR ; typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR ; typedef LPCSTR LPCTSTR ; #define __TEXT(quote) quote #endif #define TEXT(quote) __TEXT(quote)
如果已经在某个表头文件或者其它表头文件中定义了TCHAR数据型态,那么WINNT.H和WCHAR.H表头文件都能防止其重复定义。不过,无论何时在程序中使用其它表头文件时,都应在所有其它表头文件之前包含WINDOWS.H。
建议:如果您希望明确定义8位字符变量和字符串,请使用CHAR、PCHAR(或者其它),以及带引号的字符串。为明确地使用16位字符变量和字符串,请使用WCHAR、PWCHAR,并将L添加到引号前面。对于是8位还是16位取决于UNICODE标识符的定义的变量或字符串,要使用TCHAR、PTCHAR和TEXT宏。
从Windows 1.0到Windows 3.1的16位Windows中,MessageBox函数位于动态链接库USER.EXE。在Windows 3.1软件开发套件的WINDOWS.H中,MessageBox函数定义如下:
int WINAPI MessageBox (HWND, LPCSTR, LPCSTR, UINT) ;
在USER32.DLL中,没有32位MessageBox函数的进入点。实际上,有两个进入点,一个名为MessageBoxA(ASCII版),另一个名为MessageBoxW(宽字符版)。
在WINUSER.H头文件中:
WINUSERAPI int WINAPI MessageBoxA (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) ; WINUSERAPI int WINAPI MessageBoxW (HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) ; #ifdef UNICODE #define MessageBox MessageBoxW #else #define MessageBox MessageBoxA #endif
Windows的字符串函数
下面是Windows定义的一组字符串函数,这些函数用来计算字符串长度、复制字符串、连接字符串和比较字符串:
ILength = lstrlen (pString) ; pString = lstrcpy (pString1, pString2) ; pString = lstrcpyn (pString1, pString2, iCount) ; pString = lstrcat (pString1, pString2) ; iComp = lstrcmp (pString1, pString2) ; iComp = lstrcmpi (pString1, pString2) ;//不区分大小写
这些函数与C链接库中对应的函数功能相同。如果定义了UNICODE标识符,那么这些函数将接受宽字符串,否则只接受常规字符串。宽字符串版的lstrlenW函数可在Windows 98中执行。
当格式字符串与被格式化的变量不合时,可能使printf执行错误并可能造成程序当掉。使用sprintf时,您不但要担心这些,而且还有一个新的负担:您定义的字符串缓冲区必须足够大以存放结果。Microsoft专用函数_snprintf解决了这一问题,此函数引进了另一个参数,表示以字符计算的缓冲区大小。
vsprintf是sprintf的一个变形,它只有三个参数。vsprintf用于执行有多个参数的定义函数,类似printf格式。vsprintf的前两个参数与sprintf相同:一个用于保存结果的字符缓冲区和一个格式字符串。第三个参数是指向格式化参数数组的指针。实际上,该指针指向在堆栈中供函数呼叫的变量。va_list、va_start和va_end宏(在STDARG.H中定义)帮助我们处理堆栈指针。使用vsprintf函数,sprintf函数可以这样编写:
typedef char * va_list; #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) //获取类型占用的空间长度,最小占用长度为int的整数倍 #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )//获取可变参数列表的第一个参数的地址 #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )//获取可变参数的当前参数,将指针指向下一参数 #define va_end(ap) ( ap = (va_list)0 ) //清空va_list可变参数列表 int __cdecl sprintf (char * szBuffer, const char * szFormat, ...) { int iReturn ; va_list pArgs ; va_start (pArgs, szFormat) ; iReturn = vsprintf (szBuffer, szFormat, pArgs) ; va_end (pArgs) ; return iReturn ; }
_cdecl调用约定: 1.参数从右到左依次入栈 2.调用者负责清理堆栈 3.参数的数量类型不会导致编译阶段的错误
这章最后,自己写个printf
int myprintf (const char *format, ...) { va_list pArgs; va_start (pArgs, format); while (*format != '\0') { if(*format != '%') putchar(*format++); else { switch(*++format) { case 'c': putchar(va_arg(pArgs, char)); break; case 'd': { char cbuf[100]; itoa(va_arg(pArgs, int), cbuf, 10); char* p = cbuf; while(*p != '\0') putchar(*p++); break; } case 's': { const char *pstr = va_arg(pArgs,const char*); while (*pstr != '\0') putchar(*pstr++); break; } //other case... default: putchar(*format); } ++format; } } va_end(pArgs); return 0; }
第三章 窗口和消息
#include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("HelloWin") ; HWND hwnd ; MSG msg ; WNDCLAS 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.lpszMenuNam = NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow( szAppName, // window class name TEXT ("The Hello Program"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT,// initial x position CW_USEDEFAULT,// initial y position CW_USEDEFAULT,// initial x size CW_USEDEFAULT,// initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL) ; // creation parameters ShowWindow (hwnd, iCmdShow) ;//如果ShowWindow的第二个参数是SW_SHOWNORMAL,则窗口的显示区域就会被窗口类别中定义的背景画刷所覆盖 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_CREATE: PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
WINUSER.H表头文件中定义了两种不同WNDCLASS,ASCII版WNDCLASSA和Unicode版的WNDCLASSW。
本书后面列出结构时,将只列出功用相同的结构定义,对WNDCLASS就像这样:
typedef struct { UINT style ; //cs, CS_HREDRAW | CS_VREDRAW WNDPROC lpfnWndProc ;//lpfn int cbClsExtra ; //count of bytes 类附加内存 int cbWndExtra ; //窗口附加内存 HINSTANCE hInstance ; //指定包含窗口过程函数的程序的实例句柄,WinMain参数 HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ;//hbrush, HGDIOBJ GetStockObject(int fnObject)可获得系统标准画刷画笔字体调色板的句柄 LPCTSTR lpszMenuName ; //菜单资源名字,如果用资源号,要用MAKEINTRESOURCE宏转换,null为无菜单 LPCTSTR lpszClassName ; //指定窗口类名字 } WNDCLASS, * PWNDCLASS ;
typedef struct tagMSG { HWND hwnd ; UINT message ; //消息标识符
WPARAM wParam ; LPARAM lParam ;//两个附加消息随消息不同而不同 DWORD time ;//消息投递时间 POINT pt ; //鼠标当前位置 }MSG, * PMSG ;
while (GetMessage (&msg, NULL, 0, 0)){ //只要从消息队列中取出消息的message字段不为WM_QUIT(其值为0x0012),GetMessage就传回一个非零值。 TranslateMessage (&msg) ;//将msg结构传给Windows,进行一些键盘转换?。(在第六章中深入讨论) DispatchMessage (&msg) ;// 又将msg结构回传给Windows。然后,Windows将该消息发送给适当的窗口消息处理程序.处理完消息之后,WndProc传回到Windows。 } //此时,Windows还停留在DispatchMessage呼叫中。在结束DispatchMessage呼叫的处理之后,Windows回到HELLOWIN,并从下一个GetMessage开始消息循环。 BOOL GetMessage( LPMSG lpMsg, // message information HWND hWnd, // handle to window UINT wMsgFilterMin, // first message 获取的消息的最小值 UINT wMsgFilterMax // last message 获取的消息的最大值。都为0则接受全部消息 );
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
第一个参数hwnd是接收消息的窗口的句柄,它与CreateWindow函数的传回值相同。
第二个参数与MSG结构中的message字段相同,它是标识消息的数值。最后两个参数都是32位的消息参数,提供关于消息的更多信息。
窗口消息处理程序通常由Windows本身呼叫。通过呼叫SendMessage函数,程序能够直接呼叫它自己的窗口消息处理程序。
窗口消息处理程序在处理消息时,必须传回0.
窗口消息处理程序不予处理的所有消息应该被传给名为DefWindowProc的Windows函数。从DefWindowProc传回的值必须由窗口消息处理程序传回
对WM_PAINT的处理几乎总是从一个BeginPaint呼叫开始:
hdc = BeginPaint (hwnd, &ps) ;而以一个EndPaint呼叫结束:
EndPaint (hwnd, &ps) ;
在BeginPaint呼叫中,如果显示区域的背景还未被删除,则由Windows来删除。它使用注册窗口类别的WNDCLASS结构的hbrBackground字段中指定的画刷来删除背景
BeginPaint呼叫令整个显示区域有效,并传回一个「设备内容句柄」。设备内容是指实体输出设备(如视讯显示器)及其设备驱动程序。在窗口的显示区域显示文字和图形需要设备内容句柄。但是从BeginPaint传回的设备内容句柄不能在显示区域之外绘图。EndPaint释放设备内容句柄,使之不再有效。
如果窗口消息处理程序不处理WM_PAINT消息(这是很罕见的),它们必须被传送给DefWindowProc。DefWindowProc只是依次呼叫BeginPaint和EndPaint,以使显示区域有效。
HELLOWIN通过呼叫PostQuitMessage以标准方式响应WM_DESTROY消息:
PostQuitMessage (0) ;
该函数在程序的消息队列中插入一个WM_QUIT消息。
最好WinMain返回:
return msg.wParam ;
结构的wParam字段是传递给PostQuitMessage函数的值(通常是0)。然后return叙述将退出WinMain并终止程序。
前缀 类别
CS 窗口类别样式
CW 建立窗口
DT 绘制文字
IDI 图示ID
IDC 游标ID
MB 消息框
SND 声音
WM 窗口消息
WS 窗口样式
by BYTE (无正负号字符)
n short
w WORD (无正负号短整数)
sz 以字节值0结尾的字符串
消息WM_SIZE的参数wParam的值是SIZE_RESTORED、SIZE_MINIMIZED、SIZE_MAXIMIZED、SIZE_MAXSHOW或SIZE_MAXHIDE (在WINUSER.H表头文件中分别定义为数字0到4)。也就是说,参数wParam表明窗口是非最小化还是非最大化,是最小化、最大化,还是隐藏。
lParam参数包含了新窗口的大小,新宽度和新高度均为16位值,合在一起成为32位的lParam。WINDEF.H中提供了帮助程序写作者从lParam中取出这两个值的宏。
有时候,DefWindowProc处理完消息后会产生其它的消息。例如,使用者最终单击了 Close按钮,或者假设用键盘或鼠标从系统菜单中选择了 Close, DefWindowProc处理这一键盘或者鼠标输入,在检测到使用者选择了Close选项之后,它给窗口消息处理程序发送一条WM_SYSCOMMAND消息。WndProc将这个消息传给DefWindowProc。DefWindowProc给窗口消息处理程序发送一条WM_CLOSE消息来响应之。WndProc再次将它传给DefWindowProc。DestroyWindow呼叫DestroyWindow来响应这条WM_CLOSE消息。DestroyWindow导致Windows给窗口消息处理程序发送一条WM_DESTROY消息。WndProc再呼叫PostQuitMessage,将一条WM_QUIT消息放入消息队列中,以此来响应此消息。这个消息导致WinMain中的消息循环终止,然后程序结束。
总结windows消息机制:
Windows为当前执行的每个Windows程序维护一个「消息队列」。在发生输入事件之后,Windows将事件转换为一个「消息」,消息分为入队消息和不入队消息。入队消息被放入程序的消息队列中。程序通过执行一块称之为「消息循环」的程序代码从消息队列中取出消息。TranslateMessage(&msg); 将msg结构传给Windows,进行一些键盘转换。DispatchMessage(&msg);又将msg结构回传给Windows。然后,Windows将该消息发送给适当的窗口消息处理程序,让它进行处理。处理完消息之后,WndProc传回到Windows。结束DispatchMessage呼叫的处理之后,Windows回到程序中,并且接着从下一个GetMessage呼叫开始消息循环。
非队列化消息则是其它消息。在许多情况下,非队列化消息来自呼叫特定的Windows函数。例如,当WinMain呼叫CreateWindow时的WM_CREATE消息。ShowWindow时的WM_SIZE和WM_SHOWWINDOW消息。UpdateWindow时的WM_PAINT消息。键盘或鼠标输入时发出的队列化消息信号,也能在非队列化消息中出现。例如,用键盘或鼠标选择了一个菜单项时,键盘或鼠标消息就是队列化的,而说明菜单项已选中的WM_COMMAND消息则可能就是非队列化的。