第一章:起步
API(Application Programming Interface):应用程序编程接口
Windows自身带有许多函数,应用程序就是通过调用这些函数来实现它的用户界面和在屏幕上显示文本与图形的。这些函数都是在动态链接库里实现的。主要的动态链接库有3个,分别为内核(kernel),用户(user)以及GDI。
调用Windows函数与调用C语言的库函数最主要的区别就是C语言的库函数的机器代码(为什么是机器代码是因为已经过了编译步骤到了链接步骤了,编译后都是机器代码)会直接链接到你的程序代码中,而Windows函数则是放在你程序之外的DLL里,程序中的函数调用都被解析成动态链接库里的函数入口指针。
当Windows程序运行时,每个Windows的EXE文件需要包含导入库(它所要用的各个动态链接库以及库中的函数的引用地址)。
#include <windows.h>
#include <tchar.h>
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
MessageBox(NULL, _T("Hello ,Windows 98!"), _T("HelloMsg"), 0);
return 0;
}
WinMain函数第一个参数hInstance是实例句柄,这个句柄唯一表示了我们这个程序。第二个参数在32位的Windows中已经不再使用,为NULL,第三个参数是用来运行程序的命令行,第四个参数则指明程序最初如何显示。
MessageBox函数产生一个模态对话框,需要注意一下几点:
1.第二个参数如果需要换行直接‘\n’即可。
2.如果有Cancel按钮,则Esc键也是返回IDCANCEL,若没有Cancel按钮,则Esc键也没有反应。
3.第四个参数可以设置图标,按钮和默认按钮。
4.第四个参数的组合既可以用 | 也可以用 +。
5.函数返回的是一个int值。通常是int button = MessageBox(); switch(button)……。
引用一个MSDN里的例子:
int DisplayResourceNAMessageBox()
{
int msgboxID = MessageBox(
NULL,
(LPCWSTR)L"Resource not available\nDo you want to try again?",
(LPCWSTR)L"Account Details",
MB_ICONWARNING | MB_CANCELTRYCONTINUE | MB_DEFBUTTON2
);
switch (msgboxID)
{
case IDCANCEL:
// TODO: add code
break;
case IDTRYAGAIN:
// TODO: add code
break;
case IDCONTINUE:
// TODO: add code
break;
}
return msgboxID;
}
第二章:Unicode简介
ASCII码为7位,共128个代码。ASCII码是一个真正的美国标准,并不能满足其他英语国家的需求。
Windows自有的字符集为ANSI字符集,8位256个代码。但是并不能处理中文,日文,韩文这样的象形文字。
所以解决方案为Unicode字符集。Unicode字符集采用16位系统,它可以代表65536个字符,这对世界上所有书面语言的所有字符和象形文字来说都已经足够(但是Unicode不足以表示某些语言的所有字符)。Unicode最大的优点在于只有一个字符集,但是缺点也有,Unicode字符的字符串占用内存比ASCII字符串大两倍。
Unicode字符使用的UTF-16编码,UTF-16将每个字符编码为两个字节。此外UTF-8是将一些字符编码为1个字节,一些字符编码为两个字节,一些字符编码为3个字节,一些字符编码为4个字节。UTF-32则将每个字符都编码为4个字节。
美国标准ANSI C定义了宽字符概念,是用多个字节代表一个字符的字符集(Unicode是用两个字节代表一个字符)。宽字符不一定是Unicode,Unicode只是宽字符编码的一种实现。使用宽字符需要头文件wchar.h,C++为cwchar。
C语言中的宽字符是基于wchar_t数据类型的。wchar_t为16位宽。如定义wchar_t c = 'A',则变量c是一个两个字节的值0x0041,由于Intel处理器是小端存储,所以实际存储为:0x41 0x00(一共两个字节,每个字节8位)。此外如果是指向字符串的指针或者数组则还需要在前面加上L。如wchar_t *p = L"Hello!"。单个字符可以不需要加L。
所以如果使用了宽字符,则每一个C库函数都需要重写,如strlen改写成wcslen。(需要头文件string.h,C++为cstring)。
注意:使用宽字符的时候,字符个数并没有改变,只改变了每个字符所占的字节长度。
目前一个很好的维护一个源代码文件的方法是使用tchar.h头文件。这个头文件可以把代码编译成ASCII或者Unicode。
首先,tchar.h提供了很多通用的函数名字。
TCHAR.H提供了很多通用的函数名字,可以指向Unicode或者非Unicode版本的函数。
如strlen和wcslen可以用_tcslen函数代替。即如果Unicode被定义,_tcslen则被定义为wcslen:
#define _tcslen wcslen;
否则_tcslen则被定义为strlen:
#define _tcslen strlen;
其次tchar.h也引入了一种新的名为TCHAR的数据类型解决wchar_t与普通的char。
最后tchar.h采用_T解决宽字符前面那个‘L’的问题。如果Unicode标识符被定义了,字符串就被解释成宽字符,否则被解释为8为的字符串。
(注意:_T()== __T() == TEXT())
windows.h头文件包括了ctype.h头文件,而ctype.h头文件里就对char和wchar_t进行了重定义:
typedef char CHAR; typedef CHAR* PCHAR;
typedef wchar_t WCHAR; typedef WCHAR* PWCHAR;
CHAR和WCHAR是写Windows程序时推荐使用的数据类型。
在USER32.DLL中,并没有MessageBox函数的入口点,实际上它有两个入口点,MessageBoxA(ASCII版本)和MessageBoxW(宽字符版本),像这样用字符串做参数的每个Win32函数,都有两个入口点。但是我们使用的时候不必担心这个问题,只需要调用MessageBox即可。因为在winuser.h里已经实现了这一技巧:
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif
不能在Windows程序中使用printf函数,Windows不存在标准输入和输出。但是在字符模式下我们仍然可以使用sprintf和sprintf系类的其他函数显示文本。
sprintf函数第一个参数是一个字符缓冲区,后面是一个格式字符串,sprintf是将格式化结果存入到缓冲区中,返回字符串的长度。
Windows版本的sptrintf类函数为wsprintf函数,这是一个Windows API,缺陷是:
1.不能处理浮点数格式。
2.最大只支持1024个字节。
#include <windows.h>
#include <tchar.h>
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
TCHAR szBuffer[100];
int count = wsprintf(szBuffer, _T("5 + 3 等于 %d"), 5 + 3); //count = 10 (字符个数并没有改变)
MessageBox(NULL, szBuffer, _T("欢迎大家"), MB_YESNOCANCEL | MB_DEFBUTTON2 | MB_ICONASTERISK);
return 0;
}
如果需要处理浮点数,采用 _stprintf_s
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
TCHAR szBuffer[1200];
int count = _stprintf_s(szBuffer, _T("5.1 + 3.2 等于 %.2f"), 5.1 + 3.2);
MessageBox(NULL, szBuffer, _T("欢迎大家"), MB_YESNOCANCEL | MB_DEFBUTTON2 | MB_ICONASTERISK);
return 0;
}
Windows向应用程序发送消息。实际上是Windows调用了该应用程序内部的一个函数,这个函数是我们自己写的,叫做窗口过程。
窗口类允许多个窗口共享一个窗口过程。窗口过程可以是应用程序的某一个函数,也可以位于动态链接库中。如Windows程序中所有按钮都基于相同的窗口类,与该窗口类相关联的窗口过程位于一个Windows动态链接库中。
创建Win32应用程序:
#include <windows.h>
#include <tchar.h>
// Global variables
// The main window class name.
static TCHAR szWindowClass[] = _T("win32app");
// The string that appears in the application's title bar.
static TCHAR szTitle[] = _T("Win32 Guided Tour Application");
HINSTANCE hInst;
// Forward declarations of functions included in this code module:
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wcex))
{
MessageBox(NULL,
_T("Call to RegisterClassEx failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
hInst = hInstance; // Store instance handle in our global variable
// The parameters to CreateWindow explained:
// szWindowClass: the name of the application
// szTitle: the text that appears in the title bar
// WS_OVERLAPPEDWINDOW: the type of window to create
// CW_USEDEFAULT, CW_USEDEFAULT: initial position (x, y)
// 500, 100: initial size (width, length)
// NULL: the parent of this window
// NULL: this application does not have a menu bar
// hInstance: the first parameter from WinMain
// NULL: not used in this application
HWND hWnd = CreateWindow(
szWindowClass,
szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (!hWnd)
{
MessageBox(NULL,
_T("Call to CreateWindow failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
// The parameters to ShowWindow explained:
// hWnd: the value returned from CreateWindow
// nCmdShow: the fourth parameter from WinMain
ShowWindow(hWnd,
nCmdShow);
UpdateWindow(hWnd);
// Main message loop:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
TCHAR greeting[] = _T("Hello, World!");
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// Here your application is laid out.
// For this introduction, we just print out "Hello, World!"
// in the top left corner.
TextOut(hdc,
5, 5,
greeting, _tcslen(greeting));
// End application-specific layout section.
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
break;
}
return 0;
}
GetMessage函数检索到的消息如果不等于WM_QUIT,返回非0值,否则返回0。
TranslateMessage函数是将击键消息转换成字符消息(如按下键A可以产生很多不同的字符),而Dispatchmessage函数是调用窗口过程函数。
BeginPaint函数将使整个客户区有效,并返回一个设备环境句柄。BeginPaint返回的设备环境句柄,是无法在客户区以外的区域进行绘制的。EndPant函数释放这个设备环境句柄。
PostQuitMessage(0)是将一个WM_QUIT消息插入到程序的消息队列中。WM_QUIT的wParam值为PostQuitMessage中给定的值,所以最后return (int)msg.wParam实际上就是返回0。
点击关闭按钮(右上角的X)---确定是点击的哪个窗口---发送WM_NCHITTEST消息---DefwindowProc返回HTCLOSE--- 发送WM_NCLBUTTONDOWN消息(wParam值为HTCLOSE)---DefWindowProc---发送WM_SYSCOMMAND消息(wParam值为SC_CLOSE)---DefWindowProc---发送WM_CLOSE消息---DefWindowProc---发送WM_DESTROY消息---处理消息,调用PostQuitMessage函数。
队列消息是那些放入消息队列等待消息循环的消息,而非队列消息就是Windows直接调用窗口过程的产生的消息。队列消息主要是由用户的输入产生,而非队列消息通常是调用特定的Windows函数引起的。
在窗口过程处理某一消息的过程中,程序不会被其他消息中断。在许多情况下,窗口过程必须保留其从消息中获取的信息,并在处理其他消息时使用该信息,这种信息必须保存在窗口过程所定义的静态变量中,或保存在全局变量中。
系统默认的回调函数为DefWindowProc,在default中是return DefWindowProc(hwnd,message,wParam,lParam),回调函数返回0。
第四章 文本输出
客户区是指整个应用程序窗口中没有被标题栏,边框,菜单栏,工具栏和滚动条占用的区域,也就是说,客户区就是窗口中程序可以在上边绘制并且想用户传达可视化信息的区域。
windows程序不能指定自己的客户区的大小,也不能指定文本字符的大小,我们只能利用Windows提供的工具来获取程序运行环境的相关信息。
在以下任何一个事件发生时,窗口过程都会收到一条WM_PAINT消息:
1.用户移动一个窗口,导致原来被覆盖的部分窗口暴露出来。(那个原先被覆盖的窗口收到消息)
2.用户调整一个窗口的大小(如果窗口的类型设定为CS_HREDRAW和CS_VREDRAW)
3.程序调用ScrollWindow或ScrollDC函数滚动客户区。
4.程序调用InvalidateRect或者InvalidateRgn函数显式生成WM_PAINT消息。
特别注意的是:不要在WM_PAINT消息中使用MessageBox函数。这可能导致窗口不断的重绘。
尽管窗口过程必须能够在收到WM_PAINT消息时更新整个客户区,但是通常它只更新一部分,最常见的是一个矩形区域。需要重新绘制的部分被称为“无效矩形”或者“更新区域”。在客户区中有一个无效区域将导致Windows在应用程序的消息队列中放置一条WM_PAINT消息。Windows不会在消息队列中放置多条WM_PAINT消息(只更新第一条待处理的WM_PAINT消息的无效区域)。
窗口过程可以通过调用InvalidateRect函数来强制自己的客户区的一个矩形无效。窗口过程在处理WM_PAINT消息时,在调用BeginPaint函数后,整个客户区会变成有效的。程序也可以通过调用ValidateRect函数来使客户区中的任意矩形变得有效。
程序在绘制前必须获得一个设备环境句柄,在获取句柄之后,Windows会在内部环境结构中填入默认的属性值。当完成了对客户区的绘制之后,它必须释放设备环境句柄。有两种方法获取设备环境句柄:
1.这种方法只能在处理WM_PAINT消息时使用。
case WM_PAINT:
hdc = BeginPaint(hwnd,&ps);
EndPaint(hwnd,&ps);
break;
PAINTSTRUCT结构维护了一个绘制信息结构,调用BeginPaint函数,Windows将自动填充这个结构中的字段。
typedef struct tagPAINTSTRUCT
{
HDC hdc;
BOOL fErase; //通常设置为0,表示已经擦除了无效区域的背景
RECT rcPaint; //无效区域
……
} PAINTSTRUCT;
而调用InvalidateRect函数第三个参数如果是FALSE ,则Windows不会擦除背景,在调用BeginPaint函数之后,PAINTSTRUCT结构的fErase字段为TRUE。第二个参数是无效区域,但是一般我们使用NULL来更新整个客户区。BeginPaint返回的设备环境句柄,是无法在客户区以外的区域(具体而言是rcPaint定义的矩形区域之外)进行绘制的。
2.这种方法在处理费WM_PAINT消息时使用:
hdc = GetDC(hwnd);
ReleaseDC(hwnd,hdc);
GetDC与BeginPaint的区别:
1.GetDC返回的句柄的裁剪区域是整个客户区,不仅仅是在无效矩形里。
2.GetDC不会将无效区域有效化。
通常GetDC和ReleaseDC函数用于处理键盘消息或者鼠标消息。
设备环境的属性决定了文本显示的特性,默认文本颜色是黑色,背景颜色是白色。Windows将用这个背景色填充每个字符周围的矩形区域。(这里与是否透明有关),注意文本背景色和窗口背景色是不一样的,窗口背景色是用来擦客户区的刷子。
设备环境同时定义了一个裁剪区域。从GetDC得到的设备环境句柄中,裁剪区域是整个客户区,从BeginPaint得到的设备环境句柄中,裁剪区域则是无效区域。在调用TextOut时,Windows不会显示落在裁剪区域之外的部分。
前面说过Windows不能指定文本字符的大小,因为这和显示器的分辨率有关。可以通过GetTextMetrics函数获取字体尺寸。(使用之前需要定义一个TEXTMETRIC变量),注意:不要再Windows应用程序中猜测文本尺寸,不要使用固定的值,应该调用GetTextMetrics函数来获取这些信息。在WM_CREATE消息里调用GetTextMetrics函数。
文本字符与TEXTMETRIC的关系:
高度 = tmHeight + tmInternalLeading
宽度(小写字符) = tmAveCharWidth;
宽度(大写字符) = tmAveCharWidth * 3 / 2;
(多说几句,在Windows3.0之前,都是等宽字符,小写字符和大写字符宽度一样,但是Windows3.0之后,系统字体变成了变宽字体,研究表明使用变宽字体比使用等宽字体的印刷可读性更强,tmPitchAndFamily低位决定字体是否是等宽字体,如果是1则为变宽字体,0为等宽字体)
当窗口的大小发生改变时,Windows向窗口过程发送一条WM_SIZE消息,相应的lParam参数低位为客户区宽度,高位为客户区高度。(用静态变量存储,原因前面已经说过,窗口过程必须保留从一个消息中获取的信息,并在处理其他消息时使用该信息),此外获得客户区宽度和高度也可以用GetClientRect函数。
在许多Windows程序中,WM_SIZE消息之后会竞猜有一个WM_PAINT消息,因为在定义窗口类时,我们指定了窗口的风格:CS_HREDRAW | CS_VREDRAW。
下面可以计算当前客户区窗口可以显示的字符行数:
cyClient / cyChar
下面可以计算当前客户区窗口每一行可以显示的小写字符数:
cxClient / cxChar
不必担心除数为0的问题,首先窗口过程收到的第一条消息是WM_CREATE,这里会初始化cyChar和cxChar(除数已经不为0了),WM_SIZE消息在WM_PAINT消息之前,WM_SIZE消息会初始化cyClient和cxClient。
当用户使用滚动条向下滚动时,程序需要将文件相对于显示窗口向上移动。
在应用程序中包括滚动条只需在CreateWindow第三个参数中添加WS_VSCROLL(垂直滚动条)或者WS_HSCROLL(水平滚动条),或者全部添加。
在默认情况下,滚动条的范围是0——100,可以通过SetScrollRange(hwnd,iBar,iMin,iMax,bRedraw)函数可以把范围改成更有意义的值。iBar这里可以为SB_VERT(垂直)或者SB_HORZ,bRedraw为是否重画滚动条。
调用SetScrollPos(hwnd,iBar,iPos,bRedraw)函数可以指定滑块在滚动条范围中的位置。GetScrollPos函数可以获得滑块在滚动条范围中的位置。
使用注意:
1.不能用SetScrollRange函数来隐藏滚动条,如果需要隐藏滚动条,调用ShowScrollBar函数。
2.如果SetScrollRange函数后紧接着一个SetScrollPos,则SetScrollRange函数最后一个参数应设为FALSE以免重画两次滚动条。
3.因为在WM_VSCROLL和WM_HSCROLL消息中,只有16位用来指定滑块位置,所以iMax不要超过65535。
4.学会使用ScrollInfo函数,这个函数可以处理32位。
当用户单击滚动条或者拖动滑块时,Windows向窗口过程发送WM_VSCROLL消息或者WM_HSCROLL消息。wParam低位字代表了鼠标在滚动条上的操作(SB开头的标识符,如SB_LINEUP等)。
鼠标拖动滑块会产生SB_THUMBTRACK和SB_THUMBPOSITION的通知码消息。SB_THUMBTRACK和SB_THUMBPOSITION的区别在于前者是用户拖动滑块的同时移动客户区的内容,而后者是停止拖动滑块时才更新客户区内容。一般而言我们只处理其中一。
如果WM_VSCROLL和WM_HSCROLL消息wParam的低位为SB_THUMBTRACK和SB_THUMBPOSITION时,wParam高位则为用户松开鼠标键时滑块的最终位置。对于其他滚动条操作,wParam高位应该被忽略。我们更倾向于处理SB_THUMBTRACK。
特别注意的是:如果用鼠标使用了滑块滚动了窗口,但是如果不调用SetScrollPos函数,滑块会在用户松开鼠标键的时候回到原来位置!!!
前面说了如果客户区有无效区域会在消息队列中放置一条WM_PAINT消息。但是放入消息队列的WM_PAINT消息优先级比较低, 如果想立即更新无效区域,可以调用UpdateWindow函数。这个函数会使窗口过程立即收到一条WM_PAINT消息。(很多情况是InvalidateRect函数后直接调用UpdateWindow函数)
SetScrollInfo和GetScrollInfo这两个函数的优点在于:
1.设定滑块大小
2.改进了以前滚动条范围最大只有65535的限制。现在可以指定32位的位置。
3.自动把滚动条的最大值限定在si.nMax - si.nPage + 1的位置。
4.如果一页就可以显示完所有的内容,自动隐藏滚动条。
SetScrollInfo(hwnd,iBar,&si,bRedraw)
GetScrollInfo(hwnd,iBar,&si)
最重要的是第三个参数,这是一个SCROLLINFO结构。
typedef struct tagSCROLLINFO {
UINT cbSize; //设为sizeof(SCROLLINFO)
UINT fMask; //要设置或获取的值
int nMin; //范围的最小值
int nMax; //范围的最大值
UINT nPage; //页面大小
int nPos; //当前位置
int nTrackPos; //当前追踪位置
} SCROLLINFO, **LPCSCROLLINFO;
fmask是一个或者多个以SIF为前缀的标志,使用SIF_ALL肯定不会有错。
在WM_SIZE消息中对滚动条进行初始化。
这里再介绍一下ScrollWindow函数,该函数滚动所指定的窗口客户区域内容。注意的是,内容向下向右为正。调用ScrollWindow函数会产生无效区域,有无效区域会自动产生WM_PAINT函数入消息队列。无效区域范围的计算,这里举一个例子,如果有一个窗口长和宽都为100,假如内容向上滚动50个单位,则无效区域就是下半部分。
#include <windows.h>
#include <tchar.h>
#include "main.h"
// Global variables
// The main window class name.
static TCHAR szWindowClass[] = _T("win32app");
// The string that appears in the application's title bar.
static TCHAR szTitle[] = _T("Win32 Guided Tour Application");
HINSTANCE hInst;
// Forward declarations of functions included in this code module:
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wcex))
{
MessageBox(NULL,
_T("Call to RegisterClassEx failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
hInst = hInstance; // Store instance handle in our global variable
// The parameters to CreateWindow explained:
// szWindowClass: the name of the application
// szTitle: the text that appears in the title bar
// WS_OVERLAPPEDWINDOW: the type of window to create
// CW_USEDEFAULT, CW_USEDEFAULT: initial position (x, y)
// 500, 100: initial size (width, length)
// NULL: the parent of this window
// NULL: this application does not have a menu bar
// hInstance: the first parameter from WinMain
// NULL: not used in this application
HWND hWnd = CreateWindow(
szWindowClass,
szTitle,
WS_OVERLAPPEDWINDOW | WS_VSCROLL, //加入了WS_VSCROLL
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (!hWnd)
{
MessageBox(NULL,
_T("Call to CreateWindow failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
// The parameters to ShowWindow explained:
// hWnd: the value returned from CreateWindow
// nCmdShow: the fourth parameter from WinMain
ShowWindow(hWnd,
nCmdShow);
UpdateWindow(hWnd);
// Main message loop:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
TEXTMETRIC tm;
SCROLLINFO si;
int iVertPos, iPaintBeg, iPaintEnd;
TCHAR szBuffer[10];
static int cxChar, cxCaps, cyChar, cxClient, cyClient;
switch (message)
{
case WM_CREATE:
hdc = GetDC(hWnd);
GetTextMetrics(hdc, &tm);
cxChar = tm.tmAveCharWidth;
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2;
cyChar = tm.tmHeight + tm.tmExternalLeading;
ReleaseDC(hWnd, hdc);
break;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_ALL;
si.nMin = 0;
si.nMax = NUM - 1;
si.nPage = cyClient / cyChar;
SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
break;
case WM_VSCROLL:
si.fMask = SIF_ALL;
GetScrollInfo(hWnd, SB_VERT, &si);
iVertPos = si.nPos;
switch (LOWORD(wParam))
{
case SB_TOP:
si.nPos = 0;
break;
case SB_BOTTOM:
si.nPos = si.nMax;
break;
case SB_LINEUP:
--si.nPos;
break;
case SB_LINEDOWN:
++si.nPos;
break;
case SB_PAGEUP:
si.nPos -= si.nPage;
break;
case SB_PAGEDOWN:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
default:
break;
}
si.fMask = SIF_ALL;
SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
si.fMask = SIF_ALL;
GetScrollInfo(hWnd, SB_VERT, &si);
if (iVertPos != si.nPos)
{
ScrollWindow(hWnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL);
UpdateWindow(hWnd);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
si.fMask = SIF_ALL;
GetScrollInfo(hWnd, SB_VERT, &si);
iVertPos = si.nPos;
iPaintBeg = max(0, iVertPos + ps.rcPaint.top / cyChar);
iPaintEnd = min(NUM - 1, iVertPos + ps.rcPaint.bottom / cyChar);
for (int i = iPaintBeg; i <= iPaintEnd; ++i)
{
int y = cyChar * (i - iVertPos);
TextOut(hdc, 0, y, sysmetrics[i].szLabel, _tcslen(sysmetrics[i].szLabel));
TextOut(hdc, 22 * cxCaps, y, sysmetrics[i].szDesc, _tcslen(sysmetrics[i].szDesc));
SetTextAlign(hdc, TA_RIGHT);
int length = wsprintf(szBuffer, _T("%5d"), GetSystemMetrics(sysmetrics[i].iIndex));
TextOut(hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer,length);
SetTextAlign(hdc, TA_LEFT);
}
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
break;
}
return 0;
}
这里讲一下iPaintBeg和iPaintEnd,这两个变量为无效矩形对应的行数(范围),不是具体的像素点坐标。
iPaintBeg = max(0, iVertPos + ps.rcPaint.top / cyChar);
iPaintEnd = min(NUM - 1, iVertPos + ps.rcPaint.bottom / cyChar);
刚开始时,iVertPos为0,无效区域为整个窗口,ps.rePaint.top为0,ps.rcPaint.bottom为窗口的高度(也就是cyClient)。
此外ps.rcPaint.top和ps.rcPaint.bottom的值被限定在了[0,cyClient]这个范围之内。ps.rcPaint的具体值由MoveWIndow函数确定。因为传进来的参数是滚动的行数 * 每个字符的高度,所以这里也要除以每个字符的高度。
这里计算无效区域的坐标并且更新无效区域的内容的手法非常巧妙。
当然,这里也可以不计算无效区域,直接输出,当然效率会差那么一点点:
#include <windows.h>
#include <tchar.h>
#include "main.h"
// Global variables
// The main window class name.
static TCHAR szWindowClass[] = _T("win32app");
// The string that appears in the application's title bar.
static TCHAR szTitle[] = _T("Win32 Guided Tour Application");
HINSTANCE hInst;
// Forward declarations of functions included in this code module:
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wcex))
{
MessageBox(NULL,
_T("Call to RegisterClassEx failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
hInst = hInstance; // Store instance handle in our global variable
// The parameters to CreateWindow explained:
// szWindowClass: the name of the application
// szTitle: the text that appears in the title bar
// WS_OVERLAPPEDWINDOW: the type of window to create
// CW_USEDEFAULT, CW_USEDEFAULT: initial position (x, y)
// 500, 100: initial size (width, length)
// NULL: the parent of this window
// NULL: this application does not have a menu bar
// hInstance: the first parameter from WinMain
// NULL: not used in this application
HWND hWnd = CreateWindow(
szWindowClass,
szTitle,
WS_OVERLAPPEDWINDOW | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (!hWnd)
{
MessageBox(NULL,
_T("Call to CreateWindow failed!"),
_T("Win32 Guided Tour"),
NULL);
return 1;
}
// The parameters to ShowWindow explained:
// hWnd: the value returned from CreateWindow
// nCmdShow: the fourth parameter from WinMain
ShowWindow(hWnd,
nCmdShow);
UpdateWindow(hWnd);
// Main message loop:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
TEXTMETRIC tm;
SCROLLINFO si;
int iVertPos, iPaintBeg, iPaintEnd;
TCHAR szBuffer[10];
static int cxChar, cxCaps, cyChar, cxClient, cyClient;
switch (message)
{
case WM_CREATE:
hdc = GetDC(hWnd);
GetTextMetrics(hdc, &tm);
cxChar = tm.tmAveCharWidth;
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2;
cyChar = tm.tmHeight + tm.tmExternalLeading;
ReleaseDC(hWnd, hdc);
break;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_ALL;
si.nMin = 0;
si.nMax = NUM - 1;
si.nPage = cyClient / cyChar;
SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
break;
case WM_VSCROLL:
si.fMask = SIF_ALL;
GetScrollInfo(hWnd, SB_VERT, &si);
iVertPos = si.nPos;
switch (LOWORD(wParam))
{
case SB_TOP:
si.nPos = 0;
break;
case SB_BOTTOM:
si.nPos = si.nMax;
break;
case SB_LINEUP:
--si.nPos;
break;
case SB_LINEDOWN:
++si.nPos;
break;
case SB_PAGEUP:
si.nPos -= si.nPage;
break;
case SB_PAGEDOWN:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
default:
break;
}
si.fMask = SIF_ALL;
SetScrollInfo(hWnd, SB_VERT, &si, TRUE);
si.fMask = SIF_ALL;
GetScrollInfo(hWnd, SB_VERT, &si);
if (iVertPos != si.nPos)
{
// ScrollWindow(hWnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL); //这里就不需要计算无效区域了,自然没有WM_PAINT消息了
InvalidateRect(hWnd, NULL, TRUE); //所以这里手动添加一个WM_PAINT消息
UpdateWindow(hWnd);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
si.fMask = SIF_ALL;
GetScrollInfo(hWnd, SB_VERT, &si);
iVertPos = si.nPos;
iPaintBeg = max(0, iVertPos + ps.rcPaint.top / cyChar);
iPaintEnd = min(NUM - 1, iVertPos + ps.rcPaint.bottom / cyChar);
//for (int i = iPaintBeg; i <= iPaintEnd; ++i)
//{
// int y = cyChar * (i - iVertPos);
// TextOut(hdc, 0, y, sysmetrics[i].szLabel, _tcslen(sysmetrics[i].szLabel));
// TextOut(hdc, 22 * cxCaps, y, sysmetrics[i].szDesc, _tcslen(sysmetrics[i].szDesc));
// SetTextAlign(hdc, TA_RIGHT);
// int length = wsprintf(szBuffer, _T("%5d"), GetSystemMetrics(sysmetrics[i].iIndex));
// TextOut(hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer,length);
// SetTextAlign(hdc, TA_LEFT);
//}
for (int i = 0; i != NUM; ++i) //全部输出,落在窗口的显示,落在窗口外的就不显示了
{
int y = cyChar * (i - iVertPos);
TextOut(hdc, 0, y, sysmetrics[i].szLabel, _tcslen(sysmetrics[i].szLabel));
TextOut(hdc, 22 * cxCaps, y, sysmetrics[i].szDesc, _tcslen(sysmetrics[i].szDesc));
SetTextAlign(hdc, TA_RIGHT);
int length = wsprintf(szBuffer, _T("%5d"), GetSystemMetrics(sysmetrics[i].iIndex));
TextOut(hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer, length);
SetTextAlign(hdc, TA_LEFT);
}
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
break;
}
return 0;
}
第五章 绘图基础
视频显示器和打印机是两种不同的设备。最明显的区别在于对“分辨率”的定义:打印机的分辨率通常用每英寸的像素数表示,而视屏显示器的分辨率是水平或者垂直方向的像素总数。
GetSystemMetircs函数中使用SM_CXSCREEN和SM_CYSCREEN参数与GetDeviceCaps函数使用HORZRES和VERTRES参数一样,都是获取当前的屏幕分辨率。
在调用大多数GDI函数时,使用COLORREF值(一个32位的无符号长整型)来表示一种特定的颜色。注意前8位是0,COLOURREF值是按照红绿蓝的顺序指定一种颜色。
当Windows程序在屏幕、打印机或者其他输出设备上画图时,它并不是将像素点直接输出到设备上,而是将图绘制到由设备环境句柄表示的逻辑意义上的"显示平面"。当一个程序获取设备环境句柄时,Windows设置了所有属性的默认值。默认值如下表所示:
1.映射模式(Mapping Mode)
映射模式确定从逻辑坐标到设备坐标的转换方式。GDI函数里的坐标值是逻辑坐标值,而设备坐标是指窗口中像素点相对应的值。
打个比方,Rectangle(hdc,0,0,200,100),这时设备描述句柄是画一个200单位长,100单位宽的矩形。而在默认的映射模式MM_TEXT下,一个单位恰好就是一个像素。但是在其他映射模式下,逻辑单位可以被解释成不同的设备单位。特别值得注意的是坐标轴的方向,如果使用公制映射模式,为了使输出可见,y轴坐标应为负值。此外,使用视屏显示器里一逻辑英寸不是对应实际的一英寸(受分辨率的影响),而打印机则是对应实际的一英寸。
下面再介绍窗口和视口的概念,窗口的尺寸以逻辑单位计算,视口的尺寸以设备单位或者像素点计算。分别使用SetWiondowEx和SetViewportEx函数。而SetWindowsOrg是设置窗口原点,SetViewportOrg是设置视口原点。
这里给出这两个函数的运行方式:
如果想将视口原点改为(xViewOrg,yViewOrg),那么逻辑点(0,0)将被映射到设备点(xViewOrg,yViewOrg)。如果想将窗口原点改为(xWinOrg,yWinOrg),逻辑点(xWinOrg,yWinOrg)将被映射到设备点(0,0)。不管怎么改变窗口和视口原点,设备点(0,0)总是位于客户区左上角。
简单的记忆方法:逻辑点代表着坐标系,设备点代表着窗口,和如SetViewportOrgEx(hdc,cxClient/2,cyClient/2)就是调整视口为下图:
最后,LPtoDP可以把逻辑点转换成设备点,相反DPToLP可以把设备点转换成逻辑点。
再介绍我们最常使用的坐标系统,(0,0)是客户区左上角,客户区坐标是原点在窗口左上角的设备坐标值。屏幕坐标是原点位于屏幕左上角的设备坐标。
可以通过ClientToScreen函数将客户区坐标转换成屏幕坐标,ScreenToClient函数把屏幕坐标转换到客户区坐标。
2.绘图模式(Drawing Mode)
设备环境句柄将像素点输出到逻辑显示平面时,它不只是简单地输出像素的颜色,相反,它是通过一系列布尔运算将输出点的像素和输出目标位置上的像素点的颜色合成在一起。SetROP2函数可以更改绘图模式,默认的是R2_COPYPEN,它将像素点复制到逻辑显示平面上。特别注意一下R2_NOT,画橡皮线必备。
此外文本输出与文本颜色和背景颜色有关,而背景模式只有两种设置,另外一种设置为透明(TRANSPARENT)。
画直线必须调用两个函数:
MoveToEx(hdc,xBeg,yBeg,NULL);
LineTo(hdc,xEnd,yEnd);
其中MoveToEx第三个参数是一个指向POINT结构的指针,从MoveToEx函数返回后,POINT结构的x和y字段表示运行该函数之前的当前位置。
画矩形函数:
Rectangle(hdc,xLeft,yTop,xRight,yBottom);
画椭圆函数:
Ellipse(hdc,xLeft,yTop,xRight,yBottom);
画多边形函数:
Pogygon(hdc,apt,iCount);
apt是一个POINT结构的数组,iCount是点的数目,如果数组中最后一个点和第一个点不同,则Windows会再加一条线连接最后一个点和第一个点。
创建画笔分四步:
1.定义存储画笔的静态句柄变量。
2.在处理WM_CREATE消息中通过CreatePen函数创建画笔。
3.在处理WM_PAINT消息中通过SelectObject函数将画笔选入到设备环境中并用它绘制。
4.在处理WM_DESTROY消息中通过DeleteObject函数删除画笔。
当然有更简便的方法,因为SelecthPenObject会返回之前的画笔句柄,所以可以缩减为两步,都是在WM_PAINT消息中处理:
1.HPEN hPen = (HPEN)SelectObject(hdc,CreatePen(PS_DASH,0,RGB(255,0,0)));
2.DelectObject(SelectObject(hdc,hPen));
如果需要创建一个不含边框线的图形,则把NULL_PEN画笔选入设备环境即可:
SelectObject(hdc,GetStockObject(NULL_PEN));
如果只想绘制边框线,而不像填充矩形内部,可以将NULL_BRUSH选入设备环境:
SelectObject(hdc,GetStockObject(NULL_BRUSH));
注意:使用系统默认的画笔画刷是使用GetStockObject函数。
创建画刷和创建画笔步骤差不多,CreateSolidBrush函数创建画刷。
Windows还有几个使用矩形结构的函数。
FillRect(hdc,&rect,hBrush):使用指定画刷填充矩形,注意这个函数事先不需要把画刷选入设备环境。(此外要记得把创建的画刷句柄释放掉)
SetRect(&rect,xLeft,yTop,xRight,yBottom):把矩形的四个字段设定为特定值。
OffsetRect(&rect,x,y):将矩形沿x轴和y轴移动几个单位。
CopyRect(&DestRect,&SrcRect):将一个矩形结构复制到另一个矩形结构。DestRect = ScrRest也可以实现相同功能。
bInRect = PtInRect(&rect,point):判断点是否在矩形内部。
PeekMessage函数允许程序检查消息队列的下一个消息,最后一个参数确定消息如何处理(是否从消息队列删除)
GetMessage函数并不把控制权交还给程序,除非它从程序的消息队列中获得了消息。但是PeekMessage函数总是立即返回,不管消息是否出现。如果消息队列中有消息,返回值为TRUE,如果消息队列没有消息,则函数返回FALSE。
while (TRUE)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) //如果有消息
{
if (msg.message == WM_QUIT) //判断消息是否为WM_QUIT
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
//[other program lines to do some work]
}
}
此外,PeekMessage函数不会删除消息队列中的WM_PAINT消息。可以处理WM_PAINT消息,但是除非WM_PAINT消息中使无效区域有效,才会删除WM_PAINT消息,不然WM_PAINT消息一直存在于消息队列。
第六章 键盘
窗口过程通过捕获WM_SETFOCUS和WM_KILLFOCUS消息来确定自己的窗口是否具有输入焦点。
Windows接收的键盘事件消息可分为击键消息和字符消息。对于可以产生显示字符的击键组合,Windows发送击键消息和字符消息,而对于一些不产生字符的键,Windows只产生击键消息。
击键消息有四个,键按下会产生WM_KEYDOWN消息,键释放会产生WM_KEYUP消息。此外还有WM_SYSKEYDOWN和WM_SYSKEYUP这两个系统消息(当输入键和Alt键组合时通常产生系统消息)。应用程序通常忽略WM_SYSKEYDOWN和WM_SYSKEYUP消息,交由DefWindowProc函数完成默认处理。对于这四类消息,wParam是虚拟键代码,用于标识哪个键被按下或被释放,而lParam包含属于本次击键的一些其他数据。
虚拟键代码通常以VK_开头,值得注意的是,数字键和字母键虚拟键代码就是ASCII码。
当处理按键消息时,我们也许需要知道是否有转义键(Shift键、Ctrl键和Alt键)或切换键(Caps Lock键、Num Lock键和Scroll Lock键),可以通过GetKeyState函数获得此信息。
iState = GetKeyState(VK_SHIFT)
如果SHIFT键按下,则iState为负。
GetKeyState不是实时检查键盘状态,GetAsyncKeyState函数才是实时检查键盘状态。GetKeyState函数包含了正在被处理的那个消息里的键盘状态。也就是说,GetKeyState()是从消息队列获取的消息,不是实时的消息,所以只能在键盘消息处理程序中使用,如果需要在键盘消息处理程序以外查询按键状态,则需要使用GetAsyncKeyState()来代替。
GetAsyncKeyState函数的返回值表示两个内容,一个是最高位bit的值,代表这个键是否被按下,一个是最低位bit的值,代表在上次调用GetAsyncKeyState函数后,这个键是否被按下。所以使用GetAsyncKeyState函数查实时键盘状态时应该为(GetAsyncKeyState(VK_LSHIFT)&&Ox8000)。
SendMessage(hwnd,message,wParam,lParam):给窗口发送指定消息。SendMessage函数发送的是不进队消息,在窗口过程处理完之后才返回。
case WM_KEYDOWN:
switch (wParam)
{
case VK_HOME:
SendMessage(hWnd, WM_VSCROLL, SB_TOP, 0);
break;
case VK_END:
SendMessage(hWnd, WM_VSCROLL, SB_BOTTOM, 0);
break;
case VK_PRIOR:
SendMessage(hWnd, WM_VSCROLL, SB_PAGEUP, 0);
break;
case VK_NEXT:
SendMessage(hWnd, WM_VSCROLL, SB_PAGEDOWN, 0);
break;
case VK_UP:
SendMessage(hWnd, WM_VSCROLL, SB_LINEUP, 0);
break;
case VK_DOWN:
SendMessage(hWnd, WM_VSCROLL, SB_LINEDOWN, 0);
break;
case VK_LEFT:
SendMessage(hWnd, WM_VSCROLL, SB_PAGEUP, 0);
break;
case VK_RIGHT:
SendMessage(hWnd, WM_VSCROLL, SB_PAGEDOWN, 0);
break;
default:
break;
}