Typer.cpp
04SimpleTyper 工程下。先创建一个 Win32 应用程序工程,再将上节例子中的代码复制过来,然后一步步添加新的功能就可以了。
///
// Typer.cpp文件
#include <windows.h>
#include "resource.h"
#include <string>
// 窗口函数的函数原形
LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
char szClassName[] = "MainWClass";
WNDCLASSEX wndclass;
// 用描述主窗口的参数填充WNDCLASSEX结构
wndclass.cbSize = sizeof(wndclass); // 结构的大小
wndclass.style = CS_HREDRAW | CS_VREDRAW; // 指定如果大小改变就重画
wndclass.lpfnWndProc = MainWndProc; // 窗口函数指针
wndclass.cbClsExtra = 0; // 没有额外的类内存
wndclass.cbWndExtra = 0; // 没有额外的窗口内存
wndclass.hInstance = hInstance; // 实例句柄
wndclass.hIcon = ::LoadIcon(hInstance,
(LPSTR)IDI_TYPER); // 使用自定义图标
wndclass.hCursor = ::LoadCursor(NULL,
IDC_ARROW); // 使用预定义的光标
wndclass.hbrBackground =
(HBRUSH)(COLOR_3DFACE + 1); // 使用预定义画刷
wndclass.lpszMenuName = (LPSTR)IDR_TYPER;
wndclass.lpszClassName = szClassName; // 窗口类的名称
wndclass.hIconSm = NULL; // 没有类的小图标
// 注册这个窗口类
::RegisterClassEx(&wndclass);
// 创建主窗口
HWND hwnd = ::CreateWindowEx(
WS_EX_CLIENTEDGE, // dwExStyle,扩展样式
// 注意,WS_EX_CLIENTEDGE风格指定了这个窗口有一个边框和一个下沉的边缘
szClassName, // lpClassName,类名
"My first Window!", // lpWindowName,标题
WS_OVERLAPPEDWINDOW, // dwStyle,窗口风格
CW_USEDEFAULT, // X,初始 X 坐标
CW_USEDEFAULT, // Y,初始 Y 坐标
CW_USEDEFAULT, // nWidth,宽度
CW_USEDEFAULT, // nHeight,高度
NULL, // hWndParent,父窗口句柄
NULL, // hMenu,菜单句柄
hInstance, // hlnstance,程序实例句柄
NULL); // lpParam,用户数据
if (hwnd == NULL)
{
::MessageBox(NULL, "创建窗口出错!", "error", MB_OK);
return -1;
}
// 显示窗口,刷新窗口客户区
::ShowWindow(hwnd, nCmdShow);
::UpdateWindow(hwnd);
// 从消息堆中取出消息
MSG msg;
while (::GetMessage(&msg, NULL, 0, 0))
{
// 转化键盘消息
::TranslateMessage(&msg);
// 将消息发送到相应的窗口函数
::DispatchMessage(&msg);
}
// 当GetMessage返回0时程序结束
return msg.wParam;
}
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// str对象用于保存窗口客户区显示的字符串
// 为了使用string类,应该包含头文件:“#include <string>”
static std::string str;
switch (message)
{
case WM_CREATE:
{
// 设置窗口的标题
::SetWindowText(hwnd, "最简陋的打字程序");
//::SendMessage(hwnd, WM_SETTEXT, 0, (long)"最简陋的打字程序");
return 0;
}
case WM_COMMAND:
switch (LOWORD(wParam))
{
case ID_FILE_EXIT:
// 向hwnd指定的窗口发送一个WM_CLOSE消息。
::SendMessage(hwnd, WM_CLOSE, 0, 0);
// ::PostMessage(hwnd, WM_CLOSE, 0, 0);
break;
}
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = ::BeginPaint(hwnd, &ps);
// 设置输出文本的背景颜色和文字颜色
::SetTextColor(hdc, RGB(255, 0, 0));
::SetBkColor(hdc, ::GetSysColor(COLOR_3DFACE));
::TextOut(hdc, 0, 0, str.c_str(), str.length());
::EndPaint(hwnd, &ps);
return 0;
}
case WM_CHAR:
{
// 保存ansi码
str = str + char(wParam);
// 使整个客户区无效
::InvalidateRect(hwnd, NULL, 0);
return 0;
}
case WM_LBUTTONDOWN:
{
char szPoint[56];
// 保存坐标信息
wsprintf(szPoint, "X =%d,Y =%d", LOWORD(lParam), HIWORD(lParam));
str = szPoint;
if (wParam & MK_SHIFT)
str = str + " Shift Key is down";
::InvalidateRect(hwnd, NULL, 1);
return 0;
}
case WM_DESTROY: // 正在销毁窗口
// 向消息队列投递一个WM_QUIT消息,促使GetMessage函数返回0,结束消息循环
::PostQuitMessage(0);
return 0;
}
// 将我们不处理的消息交给系统做默认处理
return ::DefWindowProc(hwnd, message, wParam, lParam);
}
使用资源
资源是一些二进制数据,它能够添加到基于窗口的应用程序的可执行文件中。资源可以是标准的或者自定义的。标准资源中数据描述的资源类型有图标、光标、菜单、对话框、bitmap图像、字符串表入口等。应用程序定义的资源,也称为自定义资源,可以包含应用程序需要的任意数据。
资源文件的“源文件”是以rc为扩展名的脚本文件,由资源编译器Rc.exe编译成为以res为扩展名的二进制文件,在链接的时候,由Link.exe链入到可执行文件中。所以要想使用资源,必须首先建立一个资源脚本文件。具体过程如下:
单击菜单“File/New…",在弹出的对话框中选择Files选项卡,选中 Resource Script选项。在右边的File窗口下输入文件名resource(名字是任意的),单击“OK”按钮,如图4.4所示。
此时在工作区左边中多了一个栏目Resource View。而 VC++会自动在工程目录下创建两个文件:resource.rc和l resource.h。
.rc 文件中的所有资源都关联了一个字符串名称或数字,当使用数字标识资源的时候,resource.h文件对这些数字都定义了宏名,比如:
#define IDI_MAIN 101
这说明程序中有一个ID号为101的资源。要引用此资源,在程序中包含resource.h头文件以后,直接用宏名IDI_MAIN就可以。一般不要直接修改resource.h文件中的内容,这些定义语句是由VC++自动加上去的,读者可以通过可视化资源编译器对它们进行修改。
资源脚本文件 resource.rc是用简单的脚本语言对所添加的资源的描述。用记事本打开它,会看到类似下面的代码。
IDI_MAIN ICON DISCARDABLE "Main.ico"
资源编译器Rc.exe看到这行代码以后会将文件名为Main.ico的图标添加到目标二进制文件中。IDI_MAIN是这个图标的标识,许多与资源有关的函数都要以它为参数。
向工程中添加资源,也就是向资源脚本中添加资源描述代码,并向resource.h中添加宏定义。在 VC++下完成这个过程是很简单的,选择菜单命令“Insert/Resource…",将弹出图4.5所示的对话框。在这个对话框里选择要添加的资源类型即可。
菜单和图标
先为打字程序添加菜单。在Insert Resource对话框中选中 Menu,单击 New按钮即把菜单资源加入到了工程中,如图4.5所示。
打开工作区的 Resource选项卡,找到刚刚添加的菜单资源,双击它,这时系统会弹出一个属性对话框中。在这个对话框里将菜单的ID号改为IDR_TYPER(默认为IDR_MENU1)。选中资源,按下alt+enter即可。
作为示例我们只向菜单中添加一个File项:双击菜单中的第一个菜单项,在弹出的对话框的Caption窗口下键入名称“文件”。
继续添加子项,ID号设为ID_FILE_EXIT,Caption为退出,如图4.6所示。
菜单设置完后在程序的开头加上包含头文件的代码#include "resource.h"
,就可以在程序中引用添加的资源了。上一节中讲到,WNDCLASSEX结构成员lpszMenuName用于指定基于此类的窗口的菜单名称,所以程序代码要做如下修改。
wndclass.lpszMenuName = (LPSTR) IDR_TYPER;
也可以通过 SetMenu 函数设置窗口上的菜单。比如,要实现同样的效果,可以在处理WM_CREATE 消息时,添加如下的代码。
HMENU hMenu = ::LoadMenu(::GetModuleHandle(NULL), (LPCTSTR)IDR_TYPER);
::SetMenu(hwnd, hMenu);
LoadMenu函数用于从指定的模块中加载菜单资源,第一个参数是包含要加载资源的模块句柄,第二个参数是资源ID号。无论用哪一种方法,运行程序,都可以看到一个菜单。
当用户点击菜单中的某一个选项时,Windows即向应用程序发送一个 WM_COMMAND消息,其中参数wParam 的低字节包含了用户点击菜单的ID号。04SimpleTyper 响应WM_COMMAND消息的代码如下。
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_FILE_EXIT:
// 向hwnd 指定的窗口发送一个WM_CLOSE 消息。
::SendMessage(hwnd, WM_CLOSE, 0, 0);
break;
}
return 0;
窗口接收到WM_COMMAND消息时,用宏LOWORD 从wParam参数中取出菜单ID号。LOWORD宏定义用于取出 wParam变量的低16位。相应地,wParam参数的高字节包含了一些消息通知码,可以用HIWORD宏取出。这些消息通知码只对子窗口控件有效。
之后,如果用户单击的是“退出”命令,则调用SendMessage函数向此窗口发送一个wM_CLOSE的消息。由SendMessage函数发送的消息并不进入消息队列等待GetMessage函数取出,而是直接传给窗口函数 MainWndProc,并等待MainWndProc函数返回时再返回,其返回值也就是 WndProc函数的返回值。
WM_CLOSE消息在默认情况下由DefWindowProc函数处理,它会调用DestroyWindow函数来销毁窗口。窗口销毁时,窗口函数 MainWndProc会收到一个 WM_DESTROY消息(由DestroyWindow函数发出),表示窗口正在销毁。程序用PostQuitMessage(0);
语句来响应这个消息。PostQuitMessage函数向消息队列中投递了一个WM_QUIT消息,GetMessage函数取到此消息后返回0,消息循环结束,程序退出。
可以处理 WM_CLOSE 消息,以决定是否允许关闭窗口。比如,向MainWndProc函数添加如下代码。
case WM_CLOSE: // 屏蔽掉WM_CLOSE 消息
return 0;
这样此窗口是无论如何也关不掉了。但是更一般的做法是:
case WM_CLOSE:
【处理关闭前的工作】
::DestroyWindow ( hwnd ) ;
return 0;
另外一个常用的函数 PostMessage,它的功能是向指定窗口投递消息,原型如下。
BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
与SendMessage函数不同的是PostMessage函数发送消息后马上返回,并不等待消息的运行结果。它不是把消息直接发给窗口处理函数MainWndProc,而是像邮递员一样把消息投放到窗口所在线程的消息队列中等待GetMessage函数取出。
添加完菜单后,还要再添加一个图标资源。选择菜单命令“InsertResource…",在弹出的Insert Resource对话框中选中Icon选项。如果想新建一个图标,则点击“New”按钮;想导入一个已存在的图标,则点击“Import…”按钮再选择图标的路径。
双击新加入的图标,把ID号改为IDI_TYPER。在 WNDCLASSEX 结构中有一个hlcon成员,就是为接受图标句柄而存在的,这里将它设置为:
wndclass.hIcon = ::LoadIcon(hInstance, (LPSTR)IDI_TYPER); // 使用自定义图标
LoadIcon 是装载图标的函数。现在运行程序,窗口标题栏的左上角将出现一个小图标。
接收键盘输入
当按下一个键时,Windows会向获得输入焦点的那个窗口所在线程的消息队列投递一个WM_KEYDOWN或WM_SYSKEYDOWN消息。当释放这个键时,Windows就会投递一个WM_KEYUP或WM_SYSKEYUP消息。WM_SYSKEYDOWN和 WM_SYSKEYUP是用户敲击系统键时产生的消息。比如Alt十F4是关闭窗口的快捷键。默认情况下应该把它们交给DefWindowProc 函数来处理。
在这几个消息中,wParam参数包含了敲击键的虚拟键码,IParam参数则包含了另外一些状态信息。
当一个WM_KEYDOWN消息被TranslateMessage函数转化以后会有一个WM_CHAR消息产生,此消息的wParam参数包含了按键的ANSI 码。例如,用户敲击一次“A”键,窗口会顺序地收到以下3个消息。
WM_KEYDOWN lParam 的含义为虚拟键码“A”(0x41)
WM_CHAR lParam 的含义为 ANSI 码“a”(0x61)
WM_KEYUP lParam 的含义为虚拟键码“A”(0x41)
本节的打字程序通过处理 WM_CHAR 消息来接受用户输入,相关代码如下。
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// str对象用于保存窗口客户区显示的字符串
// 为了使用string类,应该包含头文件:“#include <string>”
static std::string str;
switch (message)
{
case WM_CREATE:
{
// 设置窗口的标题
::SetWindowText(hwnd, "最简陋的打字程序");
//::SendMessage(hwnd, WM_SETTEXT, 0, (long)"最简陋的打字程序");
return 0;
}
case WM_COMMAND:
switch (LOWORD(wParam))
{
case ID_FILE_EXIT:
// 向hwnd指定的窗口发送一个WM_CLOSE消息。
::SendMessage(hwnd, WM_CLOSE, 0, 0);
// ::PostMessage(hwnd, WM_CLOSE, 0, 0);
break;
}
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = ::BeginPaint(hwnd, &ps);
// 设置输出文本的背景颜色和文字颜色
::SetTextColor(hdc, RGB(255, 0, 0));
::SetBkColor(hdc, ::GetSysColor(COLOR_3DFACE));
::TextOut(hdc, 0, 0, str.c_str(), str.length());
::EndPaint(hwnd, &ps);
return 0;
}
case WM_CHAR:
{
// 保存ansi码
str = str + char(wParam);
// 使整个客户区无效
::InvalidateRect(hwnd, NULL, 0);
return 0;
}
case WM_LBUTTONDOWN:
{
char szPoint[56];
// 保存坐标信息
wsprintf(szPoint, "X =%d,Y =%d", LOWORD(lParam), HIWORD(lParam));
str = szPoint;
if (wParam & MK_SHIFT)
str = str + " Shift Key is down";
::InvalidateRect(hwnd, NULL, 1);
return 0;
}
case WM_DESTROY: // 正在销毁窗口
// 向消息队列投递一个WM_QUIT消息,促使GetMessage函数返回0,结束消息循环
::PostQuitMessage(0);
return 0;
}
// 将我们不处理的消息交给系统做默认处理
return ::DefWindowProc(hwnd, message, wParam, lParam);
}
std:string是标准C++库中的字符串类,为了使用这个类,应该将文件 string包含进来。 str变量保存着要显示给用户的字符串,每次处理WM_PAINT消息的时候,程序都会试图将它包含的内容全部绘制到客户区。
用户敲击键盘时,产生 WM_CHAR消息,程序把对应的字符加到str变量里。InvalidateRect函数使整个客户区变得无效,迫使Windows再次发送WM_PAINT消息。这个函数原型是:
BOOL InvalidateRect ( HWND hWnd , CONST RECT *lpRect, BOOL bErase ) ;
第二个参数 lpRect 指定了无效区域的范围,它是一个指向 RECT 类型的指针。
typedef struct tagRECT
{
LONG left; // 左上角的X 坐标
LONG top; // 左上角的Y 坐标
LONG right; // 右下角的X 坐标
LONG bottom; // 右下角的Y 坐标
} RECT, *PRECT, NEAR *NPRECT, FAR *LPRECT;
(left, top)指定了区域左上角的坐标,(right,bottom)指定了区域右下角的坐标。把这个参数设为NULL,则是要Windows无效整个客户区。
bErase参数指定在更新区域时,其背景是否要擦除。
窗口处理函数接收到WM_PAINT消息,可能是因为整个客户区变的无效了,也可能是因为客户区中的某一块区域变得无效了。BegainPaint函数的第二个参数为一个指向PAINTSTRUCT结构的指针,此结构包含了应用程序重画窗口客户区时所需的信息。
typedef struct tagPAINTSTRUCT {
HDC hdc; // 设备环境句柄
BOOL fErase; // 指定背景是否必须删除
RECT rcPaint; // 指定要求重画的区域
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;
这里面有用的一个成员就是 rcPaint,它指定了客户区中无效区域的范围。
接收鼠标输入
应用程序以接收发送或者投递到它的窗口的消息的形式接收鼠标输入。当用户移动鼠标,或按下、释放鼠标的一个键时,鼠标会产生输入事件。系统将鼠标输入事件转化成消息,投递它们到相应线程的消息队列。和处理键盘输入一样,Windows将捕捉鼠标动作,并把它们发送到相关窗口。
当用户移动光标到窗口的客户区时,系统投递WM_MOUSEMOVE消息到这个窗口。当用户在客户区按下或者释放鼠标的键时,它投递下面的消息:
发送这些消息时,lParam 参数包含了鼠标的位置坐标,可以这样读出坐标信息。
xPos = LOWORD (lParam);
yPos = HIWORD (lParam);
这些坐标都以客户区的左上角为原点,向右是x轴正方向,向下是 y轴正方向。ClientToScreen 函数可以把坐标转化为以屏幕的左上角为原点的坐标。
BOOL ClientToScreen(HWND hWnd, LPPOINT lpPoint) ;
BOOL ScreenToClient(HWND hWnd, LPPOINT lpPoint );
同样,ScreenToClient 函数又可把坐标转化回来。lpPoint参数是指向POINT结构的指针,把要转化的坐标信息写入lpPoint参数指向的内存,Windows把转化后的结果也返回到这块内存中。
wParam参数包含鼠标按钮的状态,这些状态都以MK_(意为mouse key)为前缀,,取值如下:
MK_LBUTTON 左键按下
MK_MBUTTON 中间的键按下
MK_RBUTTON 右键按下
MK_SHIFT <Shift>键按下
MK_CONTROL <Ctrl>键按下
例如,收到WM_LBUTTONDOWN消息时,如果wParam&MK_SHIFT的值为TRUE,就会知道当单击左键时,<Shift>
键也被按下了。
为了进行试验,可以在程序中加入如下响应WM_LBUTTONDOWN消息的代码。
case WM_LBUTTONDOWN:
{
char szPoint[56];
// 保存坐标信息
wsprintf(szPoint, "X =%d,Y =%d", LOWORD(lParam), HIWORD(lParam));
str = szPoint;
if (wParam & MK_SHIFT)
str = str + " Shift Key is down";
::InvalidateRect(hwnd, NULL, 1);
return 0;
}
wsprintf函数比printf 函数多了一个参数szPoint,它的作用可以认为是接收 printf 函数的输出结果。运行程序,在窗口客户区单击鼠标左键,就可以看到程序具体的运行效果。
设置文本颜色和背景色
设置窗口客户区背景色的方法在4.2.3节已经有所讨论,也就是给WNDCLASSEX结构的 hbrBackground成员安排一个画刷对象句柄。当然,这个画刷对象也可以是预定义的。比如要把背景色设置为最常见的灰色(对话框的颜色)可以用下面的代码。
wcex.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
可是TextOut函数输出文本时,默认的文本背景色为白色,文本颜色为黑色,而不管客户区的背景色是什么。为了使文本的背景色同客户区的背景色一致,有必要在输出文本时先设置它的背景色。
PAINTSTRUCT ps;
HDC hdc = ::BeginPaint(hwnd, &ps);
// 设置输出文本的背景颜色和文字颜色
::SetTextColor(hdc, RGB(255, 0, 0));
::SetBkColor(hdc, ::GetSysColor(COLOR_3DFACE));
::TextOut(hdc, 0, 0, str.c_str(), str.length());
::EndPaint(hwnd, &ps);
return 0;
SetTextColor函数把输出文本的颜色设置为了红色。COLOR_3DFACE是Windows预定义的显示元素(显示元素是屏幕上窗口的一部分),GetSysColor 函数则可以获取它的颜色。