
2.6 SDK编程基础
通常所说的Win32应用程序开发,就是利用C语言和Win32 API函数来开发Windows应用程序,这种开发方式也称为SDK方式。虽然现在直接用Win32 API开发应用程序的人已经不多了,但是深入理解Windows系统程序设计原理,仍然是成为Windows开发高手的必经之路,对于理解MFC同样有着举足轻重的作用。
2.6.1 消息的定义
消息系统对于一个Win32编程来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的整数值,并用宏表示,它向Windows发出一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序。产生消息的来源有三个:
(1)由操作系统产生。
(2)由用户触发的事件转换而来。
(3)由另一个消息产生。
操作系统可以向应用程序发送或投递消息来控制应用程序的行为并提供输入。例如,对每一个输入事件,用户按键、移动鼠标、单击都会产生一个消息。应用程序通过相应这些消息处理用户的动作,实现和用户的交互。应用程序也可以向操作系统发送或投递消息以控制预先注册的类型的空间窗口的行为。消息可以分为两类,一种是系统预定义的消息,另外一种是用户自定义的消息。其中,系统预定义的消息用一个宏来表示。在头文件winuser.h中这些消息宏被定义为32位整数,如:
#define WM_CREATE 0x0001 //窗口创建消息 #define BM_CLICK 0x00F5 //按钮单击消息
消息宏分为两部分,前缀和后缀。前缀表示处理该消息的窗口的类别,后缀描述了该消息的目的,比如窗口创建消息的宏定义为WM_CREATE,其中WM是前缀,CREATE是后缀,两者用下划线相连。常见消息的前缀和说明见下表。

消息宏只是用来标识某个消息的名称,除此以外,还需要其他信息来描述消息,比如接受消息的窗口、消息发生的时间等,在winuser.h中,用一个结构体来定义一个消息,这个结构体类型叫做MSG,它在winuser.h中定义如下:
typedef struct tagMsg { HWND hwnd; // 接受该消息的窗口句柄 UINT message; // 消息常量标识符,也就是我们通常所说的消息号 WPARAM wParam; // 32位消息的特定附加信息,确切含义依赖于消息值 LPARAM lParam; // 32位消息的特定附加信息,确切含义依赖于消息值 DWORD time; // 消息创建时的时间 POINT pt; // 消息创建时的鼠标/光标在屏幕坐标系中的位置 }MSG;
其中,hwnd是32位的窗口句柄。窗口可以是任何类型的屏幕对象,因为Win32能够维护大多数可视对象的句柄(窗口、对话框、按钮、编辑框等); message是消息号,用来存放消息常量值(就是消息宏); wParam是消息参数,通常是一个与消息有关的常量值,也可能是窗口或控件的句柄;lParam也是消息参数,通常是一个指向内存中数据的指针。
2.6.2 预定义消息
Windows预定义的消息应用于Windows自带的控件和窗口中,可以直接使用。Windows预定义的消息大概有400多种,这些消息可以分为如下3类。
(1)窗口消息
窗口消息(Windows Message)用于窗口的内部运作,如创建窗口消息(WM_CREATE)、绘制窗口消息(WM_PAINT)、销毁窗口(WM_DESTROY)等。窗口消息可用于一般窗口,也可以是对话框、控件等,因为对话框和控件也是窗口。
不同的窗口消息其消息参数的含义是不同的,几个常见的窗口消息及其参数见下表。

(2)命令消息
命令消息(Command Message)用于处理用户请求,如用户单击菜单项或工具栏按钮或标准控件时,就会产生命令消息。命令消息的形式是WM_COMMAND,消息参数wParam的低字节,即LOWORD(wParam)表示菜单项或工具栏按钮。
值得注意的是,WM_COMMAND除了作为命令消息外,还能作为标准控件的控件通知消息。区别是消息参数lParam是否为NULL,如果是NULL,则WM_COMMAND是一个命令消息;如果lParam是控件句柄值,则WM_COMMAND是一个标准控件通知消息。这里所说的标准控件包括:按钮(Button)、静态文本控件(Static)、编辑框(Edit)、组合框(ComboBox)、列表框(ListBox)、滚动块(ScrollBar)等。除了标准控件外,还有一类控件叫通用控件,通用控件有情况要向父窗口发送消息时,并不通过WM_COMMAND,而是通过控件通知消息WM_NOTIFY。
(3)控件通知消息
控件通知就是控件有情况需要通知其父窗口。控件通知消息有三种形式:
① 作为窗口消息的子集
它的特征格式如下:WM_XXXX。它主要发生在以下3种场景:
● 控件窗口在创建或销毁之前,会发送WM_PARENTNOTIFY消息给它的父窗口。
● 控件窗口绘制自身窗口的消息,比如WM_CTLCOLOR、WM_DRAWITEM 、WM_MEASUREITEM 、WM_DELETEITEM 、WM_CHARTOITEM 、WM_VKTOITEM、WM_COMMAND和WM_COMPAREITEM
● 由滚动条控件发送,通知其父窗口滚动的消息,如WM_VSCROLL和WM_HSCROLL。
② WM_COMMAND形式
这种控件通知的方式是利用命令消息,就是向父窗口发送WM_COMMAND消息,但只有标准控件才采用这种方式,标准控件包括按钮(Button)、静态文本控件(Static)、编辑框(Edit)、组合框(ComboBox)、列表框(ListBox)、滚动块(ScrollBar)等。
在WM_COMMAND中,lParam用来区分是命令消息还是控件通知消息:如果消息参数lParam为NULL,则这是个命令消息;否则lParam里面放的必然就是控件的句柄,是一个控件通知消息。消息参数wParam的低字节表示控件的ID,高字节即HIWORD(wParam)表示控件通知码,通知码标记了控件所发生的各种事件,常见的标准控件的通知码和含义如下表所示。

这些不用去记忆,有个大致了解即可,开发的时候可以查询帮助。
③ WM_NOTIFY形式
上面说到标准控件发送信息给父窗口是通过WM_COMMAND,除此之外的通用控件(比如树型视图、列表视图)发送给父窗口的消息是WM_NOTIFY。单击或双击一个通用控件或在通用控件中选择部分文本、操作通用控件的滚动条都会产生WM_NOTIFY消息。消息WM_NOTIFY的消息参数wParam和lParam分别表示控件ID和指向结构体NMHDR的指针。NMHDR包含控件通知的内容,比如控件通知码。
有些朋友可能会疑惑了,既然有了WM_COMMAND,为何要出现一个WM_NOTIFY?这是因为控件日益增多,WM_COMMAND消息无法承载更多信息所致。很久以前,大概是Windows 3.x的时代,那时候Windows控件并不是很多,大致就是今天所谓的标准控件,这些控件发送信息给父窗口通过WM_COMMAND,然后两个消息参数里分别表示控件ID和通知码。对于简单的事件,这样的方式没问题,但是如果对于复杂的事件,比如控件绘制事件,它需要很多信息传给父窗口,而WM_COMMAND的两个消息参数已经被占用了,怎么办?人们就专门为控件绘制定义一个新的消息WM_DRAWITEM,并让wParam表示控件ID, lParam为指向结构体DRAWITEMSTRUCT的指针。结构体DRAWITEMSTRUCT包含了控件绘制所需要的全部信息,具体有9个,如下定义:
typedef struct tagDRAWITEMSTRUCT { UINT CtlType; UINT CtlID; UINT itemID; UINT itemAction; UINT itemState; HWND hwndItem; HDC hDC; RECT rcItem; ULONG_PTR itemData; } DRAWITEMSTRUCT;
这些内容现在不必去理解,只需知道它们是控件绘制的必要信息。
好了,看来要传给父窗口很多信息可以通过增加消息的方式来进行。但是,随着Windows控件越来越多(比如通用控件的出现),功能越来越复杂,难道要为每一个需要附加数据的通告消息增加一个新的WM_*?如果这样增加消息的话,岂不是消息泛滥了?
于是WM_NOTIFY消息出现了,此时消息参数wParam的值为发生WM_NOTIFY消息的控件ID, lParam的值分两种情况,对于一些通知码,它的值为指向结构体NMHDR(Notify Message Handler)的指针,它的定义如下:
typedef struct tagNMHDR { HWND hwndFrom; //相当于原WM_COMMAND传递方式的lParam UINT_PTR idFrom; //相当于原WM_COMMAND传递方式的wParam的低字节 UINT code; // 相当于原WM_COMMAND传递方式的通知码(wParam的高字节) } NMHDR;
对于另外一些通知码,lParam的值为指向包含更多信息结构体的指针,并且这个结构体的第一个字段必须是NMHDR类型的变量。比如我们在列表视图控件中按下键盘,列表视图控件会发生WM_NOTIFY消息给父窗口,并同时会发送NMLVKEYDOWN结构体的地址给消息参数lParam,此时lParam就是指向NMLVKEYDOWN结构体的指针,而事件通知码LVN_KEYDOWN放到该结构体第一个字段NMHDR类型变量的code字段中。结构体NMLVKEYDOWN的定义如下:
typedef struct tagLVKEYDOWN { NMHDR hdr; //通知信息处理者句柄 WORD wVKey; //按下的虚拟键值 UINT flags; //总为0 } NMLVKEYDOWN, *LPNMLVKEYDOWN;
可能现在还有些不大理解,以后可以结合具体程序再回过头来看。
至此,WM_NOTIFY形式的控件通知解决了不断需要增加新消息和消息参数所带信息容量不够的问题。现在Windows中的通用控件的通知方式都是采用该消息的形式。
2.6.3 自定义消息
除了Windows预定义消息外,开发者还可以自己定义消息,以达到特殊功能的目的,但注意不要和预定义消息的宏冲突。系统保留的消息标识符的值在0x0000到0x03ff(WM_USER-1)范围内,这些值被系统定义消息使用。开发者不能使用这些值定义自己的消息。开发者如果要自己定义消息,可以从WM_USER开始,比如可以这样定义一个自定义的消息:
#define MY_MSG WM_USER+1
2.6.4 消息和事件
消息(Message)就是用于描述某个事件所发生的信息,而事件(Event)则是用户操作应用程序产生的动作(比如用户按下鼠标左键这个动作)。事件和消息两者密切相关,事件是原因,消息是结果,事件产生消息,消息对应事件。事件是一个动作,由用户触发的动作。消息是一个信息,传递给系统的信息。
事件与消息的概念在计算机中较易混淆,但本质是不同的,事件由用户(操作电脑的人)触发且只能由用户触发,操作系统能够感觉到由用户触发的事件,并将此事件转换为一个(特定的)消息发送到程序的消息队列中。这里强调的是:可以说“用户触发了一个事件”,而不能说“用户触发了一个消息”。用户只能触发事件,而事件只能由用户触发。一个事件产生后,将被操作系统转换为一个消息,所以一个消息可能是由一个事件转换而来(或者由操作系统产生)。一个消息可能会产生另一个消息,但一个消息决不能产生一个事件(事件只能由用户触发)。总而言之,事件只能由用户通过外设的输入产生。
2.6.5 消息和窗口
Windows的消息机制就是“以消息为基础,以事件为驱动”,即Windows程序是依靠外部发生的事件来驱动的,也就是说:程序不断地等待消息,外部事件以消息的形式进入系统后放入相应的队列,然后程序调用API函数GetMessage取得相应的消息并做出相应的处理。窗口是用来接收并处理消息的,每个窗口都对应一个函数来处理消息,这个处理消息的函数就叫窗口函数(Windows Procedure)。
Win32应用程序(SDK)的实现主要分为以下步骤:
(1)WinMain函数
大家都知道,main函数是C程序的入口点,而WinMain函数是Windows程序的入口点。
(2)MSG结构体
定义了一个窗口类WNDCLASS。
(3)注册窗口
用API函数RegisterClass向系统注册窗口类。
(4)创建窗口
用API函数CreateWindow创建窗口,在创建窗口时它可以确定窗口类、窗口标题、窗口风格、大小以及初始位置等。
(5)显示窗口
创建窗口后需要使用API函数ShowWindow来显示窗口。
(6)刷新窗口
再调用API函数UpdateWindow函数来刷新窗口。
(7)消息循环
窗口显示完毕后,就要启动消息循环,来等待接收针对窗口的消息了。消息循环使用while循环,不断地调用GetMessage来获取消息队列中的消息,获得消息再使用TranslateMessage将消息转化,最后再调用函数DispatchMessage将消息传递给窗口函数去处理。
函数GetMessage的作用是从消息队列中获取消息,如果消息队列中没有消息,此功能函数则会一直等待消息。消息队列是操作系统维护的。
(8)WindowProc窗口函数
窗口函数WindowProc非常重要,所有发往本窗口的消息都将在这个函数中处理,该函数是一个回调函数(Callback),就是系统调用的函数,用户不需要调用,只需把窗口函数函数名赋值给窗口类WNDCLASS中的成员lpfnWndProc即可,系统就知道了。窗口函数中,常利用Switch/Case方式来判断消息的种类,然后进入各个分支针对某个具体消息进行处理。
前面我们通过SDK的方式编写了一个Win32程序,程序非常简单,通过API函数MessageBox在运行时会显示一个消息框。这个消息框只能用来显示信息,窗口上的元素基本是“死”(系统给予的)的,我们不能在窗口上面添加菜单,也无法在上面画画(比如画一个矩形)。现在我们来创建一个“活”的窗口,窗口上面的元素都是我们自己添加上去的。下面我们来看一个基本的Win32应用程序。
【例2.14】 一个基本的Win32应用程序
(1)新建一个Win32应用程序,即在“新建项目”对话框上选择“Win32项目”,如图2-30所示。

图2-30
输入名称和位置后,单击“确定”,出现“Win32应用程序向导”欢迎对话框,单击“下一步”,出现“Win32应用程序向导”设置对话框,如图2-31所示。

图2-31
确保选中“Windows应用程序”,默认情况下是选中的。然后单击“完成”。系统会自动生成一个Win32程序框架。
(2)在Test.cpp中找到函数WndProc,然后在该函数的case WM_PAINT分支内添加一段画字符串代码,并在case WM_CREATE分支内添加一个显示消息框的代码。最后完整代码如下:
// Test.cpp : 定义应用程序的入口点。 // #include "stdafx.h" #include "Test.h" #define MAX_LOADSTRING 100 // 全局变量: HINSTANCE hInst; // 当前实例 TCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本 TCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名 // 此代码模块中包含的函数的前向声明: ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACKWndProc(HWND, UINT, WPARAM, LPARAM); //声明窗口函数 INT_PTR CALLBACKAbout(HWND, UINT, WPARAM, LPARAM); int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // TODO: 在此放置代码。 MSG msg; HACCEL hAccelTable; // 初始化全局字符串 LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hInstance, IDC_TEST, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hInstance); // 执行应用程序初始化: if (! InitInstance (hInstance, nCmdShow)) { return FALSE; } hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TEST)); // 主消息循环: while (GetMessage(&msg, NULL, 0, 0)) { if (! TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return (int) msg.wParam; } // // 函数: MyRegisterClass() // // 目的: 注册窗口类。 // ATOM MyRegisterClass(HINSTANCE hInstance) { 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(hInstance, MAKEINTRESOURCE(IDI_TEST)); //加载图标 wcex.hCursor = LoadCursor(NULL, IDC_ARROW); //加载光标 wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); //窗口背景 wcex.lpszMenuName = MAKEINTRESOURCE(IDC_TEST); //获取菜单名称字符串 wcex.lpszClassName = szWindowClass; //保存窗口类名 wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassEx(&wcex); } // // 函数: InitInstance(HINSTANCE, int) // // 目的: 保存实例句柄并创建主窗口 // // 注释: // // 在此函数中,我们在全局变量中保存实例句柄并 // 创建和显示主程序窗口。 // BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HWND hWnd; hInst = hInstance; // 将实例句柄存储在全局变量中 //创建窗口 hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (! hWnd) //如果创建失败,则直接返回 { return FALSE; } ShowWindow(hWnd, nCmdShow); //显示窗口 UpdateWindow(hWnd); //更新窗口 return TRUE; } //功能:处理主窗口的消息。 //参数:窗口句柄,消息,消息参数,消息参数 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; RECT rt; switch (message) { case WM_CREATE: //新增创建窗口消息的处理 MessageBox(hWnd, _T("窗口即将出现..."), _T("你好"), MB_OK); //显示消息框 break; case WM_COMMAND: //处理应用程序菜单 wmId = LOWORD(wParam); //得到菜单ID wmEvent = HIWORD(wParam); // 分析菜单选择: switch (wmId) { case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd,About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: //处理窗口绘制消息,绘制主窗口 hdc = BeginPaint(hWnd, &ps); // TODO: 在此添加任意绘图代码... //新增在窗口客户区中央画字符串的代码 GetClientRect(hWnd, &rt); SetTextColor(hdc, RGB(255, 255, 0)); //设置要画的文本颜色 SetBkColor(hdc, RGB(0, 128, 0)); //设置要画的文本背景色 DrawText(hdc, _T("Hello, World"), -1, &rt, //开始画文本 DT_SINGLELINE | DT_CENTER | DT_Visual C++ENTER); //新增结束 EndPaint(hWnd, &ps); break; case WM_DESTROY: //窗口销毁消息 PostQuitMessage(0); //发送一个退出消息WM_QUIT到消息队列 break; default: //其他消息交给由系统提供的默认处理函数 return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } // “关于”对话框的窗口消息处理程序。 INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); switch (message) { case WM_INITDIALOG: //窗口初始化消息,这里没啥需要处理,也可以注释掉 return (INT_PTR)TRUE; case WM_COMMAND: //命令消息 //如果用户单击OK或Cancel按钮 if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); //退出对话框 return (INT_PTR)TRUE; } break; } //这里直接返回FALSE表示对于其他消息一概不处理 return (INT_PTR)FALSE; }
在上面程序中,先注册了一个窗口类,那么窗口类到底有什么用呢?在Windows中运行的程序,大多数都有一个或几个可以看得见的窗口,而在这些窗口被创建起来之前,操作系统怎么知道该怎样创建该窗口,以及用户操作该窗口的各种消息交给谁处理呢?因此Visual C++在调用API函数CreateWindow创建窗口之前,要求开发者必须定义一个窗口类(注意这个类不是传统C++意义上的类)来规定所要创建该窗口所需要的各种信息,主要包括窗口的消息处理函数名、窗口的风格、图标、鼠标、菜单等,并通过一个结构体WNDCLASSEXW(末尾有个W表示是Unicode下的版本,如果是多字节下的版本为WNDCLASSEXA)来定义窗口类,其定义如下:
typedef struct tagWNDCLASSEXW { UINT cbSize; UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCWSTR lpszMenuName; LPCWSTR lpszClassName; HICON hIconSm; } WNDCLASSEXW;
其中,参数cbSize表示该结构体的大小,一般为sizeof(WNDCLASSEX); style表示该类窗口的风格,如style为CS_HREDRAW | CS_VREDRAW表示窗口在水平移动、垂直移动或者调整大小时需要重画;lpfnWndProc为一指针,指向用户定义的该窗口的消息处理函数(也称窗口过程或窗口过程函数); cbClsExtra用于在窗口类结构中保留一定空间,用于存储自己需要的某些信息;cbWndExtra用于在Windows内部保存的窗口结构中保留一定空间;hInstance表示创建该窗口的程序的运行实体句柄;hIcon、hCursor、hbrBackground、lpszMenuName分别表示该窗口的图标、鼠标形状、背景色以及菜单;lpszClassName表示该窗口类别的名称,即标识该窗口类的标志;hIconSm表示窗口的小图标。
从上面可以看出一个窗口类就对应一个WNDCLASSW结构(这里以Unicode为例),当程序员将该结构按自己要求填写完成后,就可以调用RegisterClassEx函数将该窗口类向操作系统注册,这样以后凡是要创建该窗口,只需要以该类名(lpszClassName中指定)为参数调用CreateWindow,是不是很方便。注册窗口的函数是RegisterClassEx,该函数声明如下:
ATOM RegisterClassEx(CONST WNDCLASSEX *lpwcx);
其中,参数为指向窗口类结构体的指针。如果函数成功,返回ATOM类型的数值,该数值可以标识被注册成功的窗口类。如果失败返回0。ATOM其实就是WORD,它在minwindef.h中定义如下:
typedef WORD ATOM;
创建窗口的函数是CreateWindow,该函数声明如下:
HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);
其中,lpClassName指向窗口类名的指针;lpWindowName指向窗口名称的指针;dwStyle表示窗口风格,比如WS_OVERLAPPEDWINDOW表示一个可以重叠的窗口,这种风格的窗口含有一个标题栏,标题栏的左边有一个窗口图标和窗口标题,并且单击窗口图标会出现系统菜单,标题栏的右边有最小化、最大化和关闭按钮,窗口四周还包围着一个边框(窗口边框), WS_OVERLAPPEDWINDOW风格在WinUser.h中定义:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \ WS_CAPTION | \ WS_SYSMENU | \ WS_THICKFRAME | \ WS_MINIMIZEBOX | \ WS_MAXIMIZEBOX)
第4个到第7个参数用来表示窗口的起始位置和大小。在上面程序中,使用的CW_USERDEFAULT,表示使用默认的初始位置和默认的初始大小;参数hWndParent表示父窗口的句柄,如果设为NULL,则表示该窗口没父窗口,是程序中“最高级的”窗口;hMenu表示窗口拥有的菜单的句柄,如果设为NULL,表示该窗口没有菜单;hInstance表示该窗口应用程序实例句柄;最后一个参数lpParam表示窗口创建参数,可以通过该参数来访问想要引用的程序数据。如果函数执行成功,则返回创建成功的窗口句柄,否则为NULL。有了窗口句柄,就可以对窗口进行操作,比如调整位置、大小,让其关闭等等。
窗口创建完毕后,就要显示窗口,显示窗口的函数是ShowWindow,该函数声明如下:
void ShowWindow(HWND hwnd, int nCmdShow);
其中hwnd表示要显示窗口的句柄;nCmdShow是窗口的显示方式,比如最大化显示、最小化显示和常规大小显示等,具体取值如下表所示。

窗口显示完毕后,程序中用UpdateWindow函数来刷新窗口。所谓刷新窗口,就是发送一个WM_PAINT消息给窗口,在窗口函数WndProc中会做WM_PAINT的处理(见switch/case中的WM_PAINT)。本程序中可以不调用该函数,也能正常显示窗口,大家可以试试。
窗口注册、创建、显示完毕后,就要开始窗口的消息循环了。我们看到程序中消息循环的代码是这样的:
// 主消息循环: while (GetMessage(&msg, NULL, 0, 0)) { if (! TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } }
其中GetMessage从进程的主线程的消息队列中获取一个消息并将它复制到MSG结构,如果队列中没有消息,则GetMessage函数将等待一个消息的到来以后才返回。如果你将一个窗口句柄作为第二个参数传入GetMessage,那么只有指定窗口的消息可以从队列中获得。然后TranslateAccelerator判断该消息是不是一个按键消息并且是一个加速键消息,如果是,则该函数将把几个按键消息转换成一个加速键消息传递给窗口的回调函数,处理了加速键之后,函数TranslateMessage将把两个按键消息WM_KEYDOWN和WM_KEYUP转换成一个WM_CHAR,不过需要注意的是,消息WM_KEYDOWN和WM_KEYUP仍然将传递给窗口的回调函数。处理完之后,DispatchMessage函数将把此消息发送给该消息指定的窗口中已设定的回调函数。如果消息是WM_QUIT,则GetMessage返回0,从而退出循环体。应用程序可以使用PostQuitMessage来结束自己的消息循环。通常在主窗口的WM_DESTROY消息中调用。
总之,系统将会针对这个程序的消息依次放到程序的“消息队列”中,由程序自己依次取出消息,上,再分发到对应的窗口中去。因此,建立窗口后,将进入一个循环。在循环中,取出消息、派发消息,循环往复,直到取得的消息是退出消息。循环退出后,程序即结束。
分派了消息,就要引出Win32窗口编程中最重要的窗口(过程)函数了,WndProc函数和About函数都是窗口过程函数,分别用来处理各自窗口的消息。我们来看一下窗口函数的声明:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
好多类型都是第一次看到,它们定义如下:
typedef LONG_PTR LRESULT; //表示会返回多种long型值 //CALLBACK是一个宏,表示这个函数是回调函数(由系统调用,而不是开发者调用); #define CALLBACK __stdcall DECLARE_HANDLE (HWND); //HWND窗口句柄,一个整型值 typedef unsigned int UINT; //UINT就是unsigned int typedef UINT_PTR WPARAM; //就是unsigned int,因为typedef unsigned int UINT_PTR typedef LONG_PTR LPARAM; //就是unsigned long,因为typedef unsigned long ULONG_PTR
其中,参数hWnd表示窗口句柄;message表示消息;wParam表示消息参数;lParam也表示消息参数。
一个消息由一个消息名称message(UINT)和两个参数(WPARAM, LPARAM)组成。当用户进行了输入或是窗口的状态发生改变时系统都会发送消息到某一个窗口。例如当菜单转换之后会有WM_COMMAND消息发送,WPARAM的高字节(HIWORD(wParam))是命令的ID号,对菜单来讲就是菜单ID。当然用户也可以定义自己的消息名称,还可以利用自定义消息来发送通知和传送数据。
窗口过程是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类(就是窗口类名相同)的窗口使用同样的窗口过程来响应消息。系统发送消息给窗口过程将消息数据作为参数传递给它,消息到来之后,按照消息类型排序进行处理(程序中的swtich/case部分),其中的参数message则用来区分不同的消息。也可以忽略某个消息,比如如果我们不想要在窗口创建的时候显示一下信息框(MessageBox),那么我们就可以把“case WM_CREATE”注释掉,这样WM_CREATE消息将不处理。虽然开发者不对消息处理,但系统还是会将不处理的消息传给DefWindowProc函数进行默认处理。大多数窗口只处理小部分消息。
在框架自动生成的代码基础上,我们新增了两处代码,一个处理WM_CREATE消息,就是在窗口创建的时候,先跳出一个消息框;另一个是在处理WM_PAINT消息的时候,我们新增了在屏幕中央画出一个字符串的代码,注意,这个字符串“Hello, World”是画出来的。既然是画,首先要确定画的位置。其中函数GetClientRect是获取窗口客户区(现在暂时认为空白区域)的坐标和大小,并存入RECT类型的变量中,RECT是一个结构体,定义了一个矩形区域的位置:
typedef struct tagRECT { LONG left; //左边坐标 LONG top; //上方坐标 LONG right; //右方坐标 LONG bottom; //下方坐标 } RECT
接着,用SetTextColor函数设置要画的字符串的颜色,并用SetBkColor函数设置字符串的背景颜色,最后用函数DrawText画出文本字符串,该函数声明如下:
int DrawText(LPCTSTR lpszString, int nCount, LPRECT lpRect, UINT nFormat );
其中,lpszString是要画出的文本字符串,其类型是LPCTSTR,在unicode下就是wchar *;nCount确定要画出的字符个数,如果是-1,则画出lpszString中全部字符;lpRect指向文本所画位置的矩形区域;nFormat表示在矩形区域中的显示格式,比如DT_SINGLELINE表示单行输出,DT_CENTER表示在矩形区域的水平中央,DT_Visual C++ENTER表示在矩形区域的垂直中央。如果函数执行成功,则返回文本字符串的高度。
(3)保存工程并运行,运行结果如图2-32所示。

图2-32
通过这个程序我们要知道,Windows程序是事件驱动的,对于一个窗口,它的大部分例行维护是由系统维护的。每个窗口都有一个消息处理函数。在消息处理函数中,对传入的消息进行处理。系统内还有它自己的默认消息处理函数。客户写一个消息处理函数,在窗口建立前,将消息处理函数与窗口关联。这样,每当有消息产生时,就会去调用这个消息处理函数。通常情况下,客户都不会处理全部的消息,而是只处理自己感兴趣的消息,其他的消息则送回到系统的默认消息处理函数中去。Win32窗口编程最重要的概念就是窗口和消息,需要理解的要点如下:
(1)消息的组成:一个消息由一个消息名称message(UINT)和两个参数(WPARAM, LPARAM)组成。当用户进行了输入或是窗口的状态发生改变时系统都会发送消息到某一个窗口。例如当菜单转换之后会有WM_COMMAND消息发送,WPARAM的高字节(HIWORD(wParam))是命令的ID号,对菜单来讲就是菜单ID。当然用户也可以定义自己的消息名称(自己定义消息宏),也可以利用自定义消息来发送通知和传送数据。
(2)消息的接收者:一个消息必须由一个窗口接收。在窗口的过程(WNDPROC)中可以对消息进行分析,对自己感兴趣的消息进行处理。例如你希望对菜单选择进行处理那么你可以定义对WM_COMMAND进行处理的代码,如果希望在窗口中进行图形输出就必须对WM_PAINT进行处理。
(3)消息的默认处理:如果用户没有对某个消息进行处理,那么通常窗口会对其进行默认处理。微软为窗口编写了默认的窗口过程,这个窗口过程将负责处理那些你不处理的消息。正因为有了这个默认窗口过程我们才可以利用Windows的窗口进行开发而不必过多关注窗口各种消息的处理。例如窗口在被拖动时会有很多消息发送,而我们都可以不予理睬让系统自己去处理。
(4)窗口句柄:说到消息就不能不说窗口句柄,系统通过窗口句柄来在整个系统中唯一标识一个窗口,发送一个消息时必须指定一个窗口句柄表明该消息由那个窗口接收。而每个窗口都会有自己的窗口过程,所以用户的输入就会被正确地处理。例如有两个窗口共用一个窗口过程代码,你在窗口A上按下鼠标时消息就会通过窗口A的句柄被发送到窗口A而不是窗口B。
2.6.6 工程目录结构
本书中的工程和项目是同一概念,英文都是Project。在Visual C++ 2013中,每个Project都存在于一个Solution(解决方案)中,一个解决方案可以包含一个或多个Project。在新建项目的时候,输入的项目名称就和解决方案同名。
以上面的例2.14,我们看到最外层的目录是Test,这个Test文件夹就是解决方案文件夹,它下面包含有工程文件夹,并且名字也是Test,如图2-33所示。

图2-33
其中,Debug文件夹是用来存放Debug编译配置下生成的可执行程序,还有一种编译配置是Release,如果选中Release配置,则解决方案目录下会有一个Release文件夹,用来存放Release配置下的可执行文件。
Test.sln是解决方案文件,平时我们打开工程,只要打开这个文件,就能把该解决方案下面的所有工程都加载到Visual C++ 2013中。其他文件目录我们现在不必理会,都是一些Visual C++自己使用的东西。
下面我们打开图2-33中的Test文件夹,它是一个工程文件夹,里面存放有程序的源代码,如图2-34所示。

图2-34
在图2-34中红线框起来的部分就是源代码文件,我们的程序主要在Test.cpp中。图2-33中的Debug目录用来编译过程中生成的中间文件。Test.rc是资源文件。Test.Visual C++xproj文件是工程文件,但我们不直接打开它,而是通过打开解决方案文件Test.sln来加载工程。small.ico和Test.ico是两个图标文件。
以上就是新建Visual C++项目的时候,Visual C++自动生成的目录结构。当然源文件和生成的可执行文件也是可以更改存放位置的。但建议初学者不要这样做。
2.6.7 调试初步
Visual C++开发工具之所以能独步天下,与其强大的调试功能密不可分。调试技术是捕捉程序bug的强大核武器。最基本的调试方式就是在某行设个断点,然后按F5键启动调试,Visual C++就会在断点那一行停下来,此时开发者就可以查看此处变量的值,来确定其值是否为预期值。
要在某行设断点,可在某行前面空白处用鼠标单击,然后会出现一个红色的圆圈,如图2-35所示。

图2-35
设了断点后,就可以在程序调试执行中在断点行停下来。启动调试执行可以按F5键或单击菜单中“调试”|“启动调试”。我们按F5键,然后发现程序运行后在while处停下来了,如图2-36所示。

图2-36
程序停下来后,我们就可以查看现场变量的值了。比如我们要看结构体变量msg的内容。查看调试运行过程中变量的值叫监视,首先我们单击菜单“调试”|“窗口”|“监视”|“监视1”来打开监视窗口,然后在监视窗口的名称一列中输入变量名,这里是msg,因为msg是一个结构体变量,因此旁边有个小箭头可以用来展开查看结构体中的各个字段的值,如图2-37所示。

图2-37
图2-36中msg的各字段值都是乱值,因为GetMessage还没执行完,msg还没获取到消息。这也说明断点停下的那一行是正要(即将)执行的那一行。然后我们开始执行断点那一行,按F10键或单击菜单“调试”|“逐过程”使得程序向前走一步,停在下一个代码行,如图2-38所示。

图2-38
此时再看监视窗口,发现结构体变量msg各字段都有值了,如图2-39所示。

图2-39
图2-39中的msg变量值可以称为现成值,它可以和我们预期的理论值进行比较,如果不对,可以修改程序重新来运行。以上就是一个基本的调试过程。
另外说一下,逐过程(F10)和逐语句(F11),这两种方式都叫单步执行。对于某行只有语句来说,逐过程和逐语句是一样的效果,都是执行该行语句。如果某行有函数,逐过程不会进到函数内部中去,只会执行到函数行的下一代码行,并可以看到函数返回的结果。而逐语句指的是碰到函数的时候会进入函数内部单步执行。
2.6.8 Win32控件编程
前面程序介绍了窗口,除了菜单外,窗口上空空如也,无法提供更多的功能。现在我们来介绍控件,控件的作用就是和用户交互来完成相应的功能。Visual C++提供了很多控件,并且在不断更新中,每一版的Visual C++都会有新的控件出来。我们这里介绍几种常见控件的开发。控件无法单独存在,它必须放置在主窗口上。其实控件相当于一个子窗口,是附着在主窗口上的子窗口。常见控件及其功能见下表。

控件编程的共有操作有:
(1)创建窗口
控件也是窗口,因此创建控件和创建窗口一样,都要用到创建窗口,创建控件与创建窗口一样,也使用CreateWindow或CreateWindowEx函数,并且在窗口样式上必须要用到WS_CHILD这个样式,它表示控件是放在其他窗口上的,它是作为其他窗口的子窗口,WS_CHILD也可用WS_CHILDWINDOW来代替,效果一样,为了书写方便,通常用WS_CHILD。创建控件的地方通常是放在父窗口的WM_CREATE消息处理中。
因为系统已经预定义了常用控件的窗口类,因此使用这些常用控件的时候不需要再注册窗口。比如创建一个按钮可以这样写:
hButton = CreateWindow(_T("button"), //系统预定义的按钮(窗口)类名 _T("请单击按钮"), //按钮上显示的文本字符串 WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, //按钮的风格,因为是子窗口,所以必须有WS_CHILD 40, //按钮显示位置的横坐标 45, //按钮显示位置的纵坐标 150, //按钮的宽度 82, //按钮的高度 hWnd, //父窗口的窗口句柄 (HMENU)ID_MYBTN, //按钮控件的ID,每个控件都必须要有一个ID hInst, //程序实例句柄 NULL); //其他参数
其中,button是系统预定义的按钮(窗口)类名,它不区分大小写,也可写成Button;WS_VISIBLE表示创建按钮的同时就显示出来,如果没有改风格,则创建成功后不会显示,一直等到调用ShowWindow函数的时候才会显示;BS_PUSHBUTTON表示该按钮是一个下压按钮,在Visual C++中,按钮不仅仅包括下压按钮,还包括单选按钮和复选按钮,它们的窗口类名都是button,用风格参数来区分,WS_CHILD表示按钮控件是一个子窗口,它依附在其他窗口上;为了知道按钮创建在哪个窗口之上,所以必须要指明该窗口的句柄hWnd,该窗口也称为按钮的父窗口;每个控件都必须有一个唯一的ID(其实就是一个整数值), ID_MYBTN是自定义的按钮ID,有了ID,父窗口就能区分各个控件了,就像每个孩子都有一个唯一的身份证号一样,这个参数在创建非控件窗口的时候是用来指定菜单句柄的,用来创建控件窗口的时候(控件没有菜单),可以用这个菜单句柄参数存放控件ID,但需要强制转换一下,因为CreateWindow函数声明的时候,这个参数是一个菜单句柄,CreateWindow已经介绍过了,可以看看前面CreateWindow函数的声明。如果创建成功,函数返回的是按钮的窗口句柄,通过按钮句柄,可以对按钮进行操作,比如移动位置、使其失效等。
(2)控件和父窗口之间的消息交互
当用户对某窗口上的控件进行操作的时候,操作产生的事件会被控件转换为消息,然后发送给父窗口。消息的名字系统预定义为WM_COMMAND,消息的参数wParam和lParam用来存放相关信息,wParam的低字节部分LOWORD(wParam)表示控件ID, wParam的高字节部分HIWORD(wParam)表示通知码,通知码就是表示某种操作的宏,它们定义在WinUser.h中;lParam表示控件句柄。WM_COMMAND只是告诉父窗口控件产生消息了,但具体是什么操作,由通知码来获得,不同的控件通知码不同,因为不同的控件所拥有的操作是不同的。
除了控件向父窗口发送消息外,父窗口也能向控件发送消息让它执行某个操作或返回控件的当前内容。比如,父窗口需要知道编辑框控件当前所使用的字体句柄,可以这样:
HFONT hFont = SendMessage(hEdit, WM_GETFONT,0,0);
其中,hEdit是编辑框控件的窗口句柄;WM_GETFONT是系统预定义的获取字体的消息。又比如,父窗口想设置编辑框控件的字体,可以这样:
SendMessage(hEdit, WM_SETFONT, (WPARAM)hFont,0);
其中,WM_SETFONT是系统预定义的向编辑框设置字体的消息;hFont是要向编辑框控件设置的字体句柄,它一般在创建字体的时候获取。
(3)控件的大小、位置、使能、可见性和销毁
控件也是窗口,用来调整窗口的大小和位置的函数是MoveWindow或SetWindowPos,该函数也可以用于控件。同样判断窗口可见性的函数IsWindowVisible也可以用于控件。如果要让某个控件不可用,可以使用函数EnableWindow。如果要销毁某个控件可以使用DestroyWindow,但一般主窗口销毁的时候,其拥有的子窗口(包括控件)都会自动销毁,所以不需要显式地去调用DestroyWindow。
下面我们对常用控件进行介绍。
1.按钮控件
按钮控件是最常用的控件了。按钮控件不仅仅指下压按钮(下压按钮通常简称按钮),还包括单选按钮和复选按钮,主要通过按钮风格来区分。比如,风格BS_PUSHBUTTON表示普通的下压矩形按钮,下压按钮就是当用户鼠标单击的时候,它会按下去然后弹起来;BS_AUTORADIOBUTTON表示单选按钮,单选按钮左边有个小圆圈,如果单击它会出现一个小黑点,表示选中,单选按钮通常用于互斥选择的场合;BS_AUTOCHECKBOX表示复选按钮,复选按钮左边有个小矩形框,如果单击它,会出现一个勾,表示选中,再次单击则勾又消失,表示没选中。
按钮控件的窗口类名为“button”,类名是不区分大小写的,也可以使用类名的宏WC_BUTTON。
按钮控件可以向父窗口发送消息,消息的名称是WM_COMMAND,但具体是什么操作,需要同时附带通知码来告诉父窗口,即按钮控件向父窗口发送消息的同时会附带通知码,通过通知码父窗口就知道按钮发生了什么操作。通知码是放在消息参数wParam的高字节即HIWORD(wParam)中的。常用的按钮通知码定义如下表所示。

父窗口也可以向按钮控件发送消息,消息名通常以BM_为前缀(Button Message),常见的按钮消息如下表所示。

比如,要想让按钮保持按下的状态,可以向它发送BM_SETSTATE消息,并且设消息参数wParam为1,代码如下:
SendMessage(hBtn, BM_SETSTATE,1,0); // hBtn为按钮句柄
如果要模拟用户单击按钮的操作,可以向按钮发送BM_CLICK消息,代码如下:
SendMessage(hBtn, BM_CLICK,0,0);
这个代码和用户用鼠标单击按钮产生的效果是一样的。有些同志或许想,那上面的按下状态代码再加上弹起,不也是一个单击过程吗?即:
SendMessage(hBtn, BM_SETSTATE,1,0); // 让按钮按下 SendMessage(hBtn, BM_SETSTATE,0,0); // 让按钮弹起
虽然这个过程看起来是先按下再弹起,过程看起来和单击过程一样了,但其实产生的事件效果是不同的。用户用鼠标单击,产生的消息是WM_COMMAND,消息通知码是BN_CLICKED,这个通知码的效果和消息BM_CLICK的效果是一样的。而消息BM_SETSTATE只是改变按钮的状态。所以不能用连续改变两次按钮状态(一次按下,一次弹起)来模拟鼠标单击操作。要模拟鼠标单击操作应该用BM_CLICK消息。
如果要获得单选按钮或复选按钮的选择情况,可以发送BM_GETCHECK消息,比如:
int iCheckFlag = (int)SendMessage(hBtn, BM_GETCHECK,0,0);
如果单选按钮或复选按钮处于选中状态,则返回1(BST_CHECKED),否则返回0(BST_UNCHECKED)。如果按钮具有BS_3STATE风格和BS_AUTO3STATE风格,并且处于灰色不可用状态时,则返回2(BST_INDETERMINATE)。
也可以用消息BM_SETCHECK来设置单选按钮或复选按钮的选择情况,让SendMessage的第三个参数wParam为1表示选中,为0表示不选中,比如:
SendMessage(hBtn, BM_SETCHECK,1,0); //设置选中
也可以用下面一行代码实现根据当前选中状态来设置相反的选中状态:
SendMessage(hBtn, BM_SETCHECK, (WPARAM)! SendMessage(hBtn, BM_GETCHECK,0,0),0); //反选
【例2.15】 下压按钮的使用
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建下压按钮控件 CreateWindow(_T("button"), _T("请单击按钮"), WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,40, 45, 150, 82, hWnd, (HMENU)ID_MYBTN, hInst, NULL); break;
CreateWindow前面已经介绍过了。这里,_T("button")表示按钮控件的窗口类名,它是一个系统预定义的窗口类,不区分大小写,也可以写成Button,但button1就不可以了;ID_MYBTN是自定义的宏,表示按钮控件的ID,每个控件都需要一个ID,注意工程中所有控件的ID不能出现重复;hInst是程序实例句柄,它是一个全局变量。
此时运行工程,可以发现窗口上已经有一个按钮控件了。但单击它没有任何反应。
(3)在窗口上单击按钮,会向窗口发送WM_COMMAND消息,因此我们需要响应WM_COMMAND消息:
case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); // 分析菜单选择: switch (wmId) { case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; case IDM_EXIT: DestroyWindow(hWnd); break; case ID_MYBTN: //判断是否是单击ID为ID_MYBTN的按钮发来的消息 //更改按钮的标题文本 SendMessage((HWND)lParam, WM_SETTEXT, NULL, (LPARAM)_T("该按钮已被单击")); //显示消息框 MessageBox(hWnd, _T(" 您单击了按钮控件 "), _T(" 提示 "), MB_OK |MB_ICONINFORMATION); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break;
当单击按钮的时候,将向父窗口发送WM_COMMAND消息,并且消息的参数wParam的低字节部分LOWORD(wParam)的值就是按钮控件的ID;消息参数lParam表示按钮的这个控件的窗口句柄,有了它就可以用来操作按钮,比如更改按钮的标题文本,通过函数SendMessage,向按钮的窗口句柄发送WM_SETTEXT消息,该消息请求修改按钮窗口的标题,并且把新的标题字符串放在LPARAM类型的消息参数中。API函数SendMessage的声明如下:
LRESULT SendMessage( HWND hWnd, UINT message, WPARAM wParam = 0, LPARAM lParam = 0 );
其中,hWnd表示发送消息给目标窗口的窗口句柄;wParam和lParam表示消息参数,对于不同的消息,它们附带额外的信息。根据不同的消息,函数的返回值不同。这个函数一定要等到发送出去的消息处理完毕后才会返回。
MessageBox用来显示信息框,前面已经介绍过了。
(4)保存工程并运行,运行结果如图2-40所示。

图2-40
【例2.16】 单选控件的使用
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: // 第一组单选按钮 CreateWindow(_T("Button"), _T("0--50公斤"), //创建第一组中的第一个单选按钮 WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP, x, y, w, h, hWnd, (HMENU) IDC_RADIO1, hInst, NULL); y += 20; CreateWindow(_T("Button"), _T("50--100公斤"), //创建第一组中的第二个单选按钮 WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, x, y, w, h, hWnd, (HMENU) IDC_RADIO2, hInst, NULL); y += 20; CreateWindow(_T("Button"), _T("100公斤以上"), //创建第一组中的第三个单选按钮 WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, x, y, w, h, hWnd, (HMENU) IDC_RADIO3, hInst, NULL); //第二组单选按钮 x += 150; //横坐标递增 y = 20; //纵坐标初始化 CreateWindow(_T("Button"), _T("0--100厘米"), //创建第二组中的第一个单选按钮 WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP, x, y, w, h, hWnd, (HMENU) IDC_RADIO4, hInst, NULL); y += 20; CreateWindow(_T("Button"), _T("100--200厘米"), //创建第二组中的第二个单选按钮 WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, x, y, w, h, hWnd, (HMENU) IDC_RADIO5, hInst, NULL); y += 20; CreateWindow(_T("Button"), _T("200厘米以上"), //创建第二组中的第三个单选按钮 WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, x, y, w, h, hWnd, (HMENU) IDC_RADIO6, hInst, NULL); break;
其中,x和y是单选按钮显示的坐标,w和h是每个单选按钮的宽度和高度,它们在函数开头定义:
int x=20, y = 20, w = 110, h = 16;
每个单选按钮都有一个唯一ID, ID定义如下:
//单选按钮控件ID #define IDC_RADIO1 10001 #define IDC_RADIO2 10002 #define IDC_RADIO3 10003 #define IDC_RADIO4 10004 #define IDC_RADIO5 10005 #define IDC_RADIO6 10006
一共创建了6个单选按钮,并且每3个作为一组,前面说过单选常用于互斥选择的场合,因此通常把要互斥选择的几个单选按钮作为一组,分组的方法是每组第一个单选按钮创建的时候带有WS_GROUP风格,同组中其他单选按钮不需要此风格,直到碰到又一个单选按钮拥有WS_GROUP风格,说明本组结束。下一个拥有WS_GROUP风格的单选按钮是下一组中的第一个单选按钮。以此类推。比如我们上面代码中,第一组单选按钮让用户选择体重范围,第二组单选按钮让用户选择身高范围,那么体重范围一组中的单选按钮需要互斥选择,身高范围一组中的单选按钮需要互斥选择。互斥选择就是指要么选择A,要么选择B,不能同时选中A和B。
(3)在窗口左上角画上一行文字:请选择体重范围和身高范围。画文字需要在WM_PAINT消息中进行,代码如下:
GetClientRect(hWnd, &rt); //获取窗口客户区矩形坐标 DrawText(hdc, _T("请选择体重范围和身高范围"), -1, &rt, //在左上角画文本字符串 DT_SINGLELINE | DT_TOP | DT_LEFT);
其中,rt表示窗口客户区矩形坐标,在函数开头定义:
RECT rt;
(4)为了让用户选中某个单选按钮时产生反映,我们需要响应窗口的WM_COMMAND消息,并判断是哪个单选按钮发生了消息,并且判断是什么通知码,如果是BN_CLICKED,说明单选按钮被单击了,也就是被选中了。在WM_COMMAND消息处理中添加代码如下:
case WM_COMMAND: wmId = LOWORD(wParam); //控件ID wmEvent = HIWORD(wParam); //通知码 // 分析菜单选择: switch (wmId) { //开始新增部分-------------------------------------- case IDC_RADIO1: if (wmEvent==BN_CLICKED) MessageBox(hWnd, _T("你的体重范围是0-50公斤"), _T("结果"), MB_OK); break; case IDC_RADIO2: if (wmEvent == BN_CLICKED) MessageBox(hWnd, _T("你的体重范围是50-100公斤"), _T("结果"), MB_OK); break; case IDC_RADIO3: if (wmEvent == BN_CLICKED) MessageBox(hWnd, _T("你的体重范围是100公斤以上,要减肥了"), _T("结果"),MB_OK); break; case IDC_RADIO4: if (wmEvent == BN_CLICKED) MessageBox(hWnd, _T("你的身高范围是0-100厘米"), _T("结果"), MB_OK); break; case IDC_RADIO5: if (wmEvent == BN_CLICKED) MessageBox(hWnd, _T("你的身高范围是100-200厘米"), _T("结果"),MB_OK); break; case IDC_RADIO6: if (wmEvent == BN_CLICKED) MessageBox(hWnd, _T("你的身高范围是200厘米以上"), _T("结果"),MB_OK); break; //新增部分结束-------------------------------------- case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break;
(5)保存工程并运行,运行结果如图2-41所示。

图2-41
【例2.17】 复选按钮的使用
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: CreateWindow(_T("Button"), _T("西瓜"), //创建第一个复选按钮 WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x, y, w, h, hWnd, (HMENU)IDC_CHECK1, hInst, NULL); y += 30; CreateWindow(_T("Button"), _T("苹果"), //创建第二个复选按钮 WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x, y, w, h, hWnd, (HMENU)IDC_CHECK2, hInst, NULL); y += 30; CreateWindow(_T("Button"), _T("桃子"), //创建第三个复选按钮 WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x, y, w, h, hWnd, (HMENU)IDC_CHECK3, hInst, NULL); break;
其中,x和y是单选按钮显示的坐标,w和h是每个单选按钮的宽度和高度,它们在函数开头定义:
int x=20, y = 20, w = 110, h = 16;
每个复选按钮都有一个唯一ID, ID定义如下:
#define IDC_CHECK110001 #define IDC_CHECK210002 #define IDC_CHECK310003
一共创建了3个复选按钮,复选按钮是可以同时选中,或都不选中的。
(3)在窗口左上角画上一行文字:请选择你爱吃的水果。画文字需要在WM_PAINT消息中进行,代码如下:
GetClientRect(hWnd, &rt); //获取窗口客户区矩形坐标 DrawText(hdc, _T("请选择你爱吃的水果"), -1, &rt, //在左上角画文本字符串 DT_SINGLELINE | DT_TOP | DT_LEFT);
其中,rt表示窗口客户区矩形坐标,在函数开头定义:
RECT rt;
(4)我们最终的目标是要知道用户选择了哪些水果。所以当用户选中了一个复选按钮时,就要把该复选按钮对应的水果名称记录下来,如果取消选择了某个复选按钮则要把相应的水果名称缓冲区清空。用户单击复选框会向窗口产生WM_COMMAND消息,在这个消息中,我们判断哪个复选框被单击了,然后获取该复选框的选中状态,如果选中,则记录水果名称,如果没有选中,则清空名称缓冲区,同时还要进行窗口重画,在窗口上画出结果文本字符串,以反映出选择结果。代码如下:
case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); GetClientRect(hWnd, &rt); //获取窗口客户区矩形坐标 // 分析菜单选择: switch (wmId) { //我们新增的代码-------------------------------------------- case IDC_CHECK1: //如果是第一个复选框 iCheckFlag = (int)SendMessage((HWND)lParam, BM_GETCHECK, 0, 0); if (iCheckFlag) //判断是否选中 _tcscpy_s(szbuf1, _T("西瓜")); //记录水果名称 else _tcscpy_s(szbuf1, _T("")); //清空缓冲区 InvalidateRect(hWnd, &rt, TRUE); //让父窗口无效,系统则会发送WM_PAINT break; case IDC_CHECK2: //如果是第二个复选框 iCheckFlag=(int)SendMessage((HWND)lParam, BM_GETCHECK, 0, 0); if (iCheckFlag) //判断是否选中 _tcscpy_s(szbuf2, _T("苹果")); //记录水果名称 else //没选中 _tcscpy_s(szbuf2, _T("")); //清空缓冲区 InvalidateRect(hWnd, &rt, TRUE); //让父窗口无效,系统则会发送WM_PAINT break; case IDC_CHECK3: //如果是第三个复选框 iCheckFlag = (int)SendMessage((HWND)lParam, BM_GETCHECK, 0, 0); if (iCheckFlag) //判断是否选中 _tcscpy_s(szbuf3, _T("桃子")); //记录水果名称 else _tcscpy_s(szbuf3, _T("")); //清空缓冲区 InvalidateRect(hWnd, &rt, TRUE); //让父窗口无效,系统则会发送WM_PAINT break; //新增代码结束------------------------------------ case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break;
其中,函数_tcscpy_s就是strcpy的Uniocde和非Unicode的通用版本,结尾加了_s是Visual C++ 2013中推荐使用的,表示该函数的安全版本。szbuf1, szbuf2和szbuf3是一个字符串缓冲区,它们定义在函数开头:
static TCHAR szbuf1[20] = _T(""), szbuf2[20]=_T(""), szbuf3[20]=_T("")
注意,必须要用静态static,因为WndProc每次有消息都会调用到,如果不用静态,上次记录在缓冲区中的水果名称就会丢失,比如在消息WM_COMMAND中szbuf1被拷贝了“西瓜”字符串,然后调用窗口失效函数InvalidateRect,系统会发生WM_PAINT,则又会调用WndProc函数;如果不用静态,那么szbuf1又会被重新定义并初始化一次,内容变空了。当然不用局部静态方式,用全局变量也可以。
现在用户选择的水果名称都被保存在静态缓冲区里了,我们就可以在WM_PAINT消息中,进行结果显示了,即把水果名称显示出来。所以要在WM_PAINT处添加代码,最终代码如下:
case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: 在此添加任意绘图代码... GetClientRect(hWnd, &rt); //获取窗口客户区矩形坐标 DrawText(hdc, _T("请选择你爱吃的水果"), -1, &rt, //在左上角画文本字符串 DT_SINGLELINE | DT_TOP | DT_LEFT); rt.left = 0, rt.top = 100; rt.right = 350; rt.bottom = 150; //重新生成一个新的矩形范围 _stprintf_s(szRes, _T("你爱吃的水果是:%s%s%s"), szbuf1, szbuf2, szbuf3);//组成结果字符串 DrawText(hdc, szRes, -1, &rt, //在定义的矩形范围内的左上角画文本字符串 DT_SINGLELINE | DT_TOP | DT_LEFT); EndPaint(hWnd, &ps); break;
其中,函数_stprintf_s就是strcpy的Uniocde和非Unicode的通用版本,结尾加了_s是Visual C++ 2013中推荐使用的,表示该函数的安全版本。szRes是用来存放最终结果的字符串,在函数开头定义如下:
TCHAR szRes[256] = _T("");
(5)保存工程并运行,运行结果如图2-42所示。

图2-42
2.静态文本控件
静态文本控件通常只用来在窗口上显示文本字符串。它通常不和用户交互,只是静静地显示信息,因此称为静态。
静态文本控件的窗口类名为“static”,也可以使用类名的宏WC_STATIC。
该控件的风格以前缀SS_开头(Static Style),如SS_LEFT表示左对齐开始显示文本,SS_CENTER表示在静态文本控件的中间显示文本。
【例2.18】 静态文本控件的使用
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建静态文本控件 CreateWindow(_T("Static"), _T("我是静态文本控件"), SS_CENTER | WS_CHILD | WS_VISIBLE, 10, 20 ,160, 18, hWnd, (HMENU)10001, hInst, NULL); break;
CreateWindow前面已经介绍过了。这里,_T("Static")表示静态文本控件的窗口类名,它是一个系统预定义的窗口类,不区分大小写,也可以写成static,但Static1就不可以了;10001是控件的ID,注意工程中所有控件的ID不能出现重复;hInst是程序实例句柄,它是一个全局变量。
此时运行工程,可以发现窗口上已经有一个静态文本控件了,控件的颜色是灰色的,字符串显示在控件的中央,因为宽度定义了160,比较宽,所以两边留出了一些灰白,如图243所示。
如果想让字符串的显示宽度正好和静态文本控件宽度一样,可以用风格SS_SIMPLE,比如这样:
CreateWindow(_T("Static"), _T("我是静态文本控件"), SS_SIMPLE | WS_CHILD | WS_VISIBLE, 10, 20 ,160, 18, hWnd, (HMENU)10001, hInst, NULL);
此时运行工程,运行结果如图2-44所示。

图2-43

图2-44
3.编辑框
编辑框是显示字符串,并能接收用户输入字符串的控件。用户除了可以在编辑框中输入可添加或插入的文本外,还能进行复制、粘贴、剪切、删除等编辑功能。通过设置编辑框的只读属性,可以不让用户修改编辑框中的字符串。
编辑框控件的窗口类名为“edit”,也可以使用类名的宏WC_EDIT。
该控件的风格以前缀ES_开头(Edit Style),常见的编辑框控件风格如下表所示。

编辑框可以是多行的,也就是在编辑框中显示多行文字,这就需要设置ES_MULTILINE风格,如果想要多行编辑框支持回车键,则还要设置ES_WANTRETURN。
应用程序可以向编辑框控件发送消息,让它执行某个操作或返回编辑框的某个属性的当前状态,常见的编辑框消息如下表所示。

(续表)

(续表)

编辑框发生某些事件时也会向其父窗口发送通知消息。常用的编辑框的通知消息如下表所示。

下面我们通过实例来了解编辑框控件的使用。
【例2.19】 编辑框控件的基本使用
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: CreateWindow(_T("edit"), _T("我是编辑框"), ES_LEFT | WS_BORDER |WS_CHILD | WS_VISIBLE, 10, 20, 160, 20, hWnd, (HMENU)10001, hInst, NULL); break;
(3)保存工程并运行,运行结果如图2-45所示。

图2-45
【例2.20】 获取编辑框中的内容
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建编辑框 hEdit = CreateWindow(_T("edit"), _T("我是编辑框"), ES_LEFT | WS_BORDER | WS_CHILD | WS_VISIBLE, 10, 20, 160, 20, hWnd, (HMENU)ID_MYEDIT, hInst, NULL); //创建下压按钮控件 CreateWindow(_T("button"), _T("获取编辑框内容"), WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 40, 45, 150, 42, hWnd, (HMENU)ID_MYBTN, hInst, NULL); break;
其中,hEdit是一个全局变量,表示编辑框的句柄,定义如下:
HWND hEdit;
ID_MYEDIT表示编辑框的控件ID, ID_MYBTN表示按钮的控件ID。它们定义如下:
#define ID_MYBTN 10001 #define ID_MYEDIT 10002
(3)添加按钮响应。在消息WM_COMMAND下的switch中添加代码:
case ID_MYBTN: //判断是否是单击ID为ID_MYBTN的按钮发来的消息 //获取编辑框中内容 SendMessage(hEdit, WM_GETTEXT, 256 , (LPARAM)szText); //显示消息框 MessageBox(hWnd, szText, _T("提示"), MB_OK | MB_ICONINFORMATION); break;
通过向编辑框发送WM_GETTEXT消息,可以获取到编辑框中的内容,但需用消息参数wParam指定获取的最大长度,lParam指定获取到的字符串存放的缓冲区,szText是一个局部变量,定义如下:
TCHAR szText[256] = _T("");
(4)保存工程并运行,运行结果如图2-46所示。

图2-46
【例2.21】 修改编辑框的密码字符形式
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: hEdit = CreateWindow(_T("edit"), _T("我是编辑框"), ES_PASSWORD|ES_LEFT | WS_BORDER | WS_CHILD | WS_VISIBLE, 10, 20, 160, 20, hWnd, (HMENU)ID_MYEDIT,hInst, NULL); CreateWindow(_T("button"), _T(" 修改编辑框的密码形式 "), WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 40, 45, 160, 42, hWnd, (HMENU)ID_MYBTN, hInst, NULL); break;
其中,hEdit是一个全局变量,表示编辑框的句柄,定义如下:
HWND hEdit;
ID_MYEDIT表示编辑框的控件ID, ID_MYBTN表示按钮的控件ID。它们定义如下:
#define ID_MYBTN 10001 #define ID_MYEDIT 10002
在创建编辑框的时候加了ES_PASSWORD风格,则编辑框中的内容都会是*,这也是默认的密码字符形式。下面我们来改变默认密码字符形式。
(3)添加按钮响应。在消息WM_COMMAND下的switch中添加代码:
case ID_MYBTN: //判断是否是单击ID为ID_MYBTN的按钮发来的消息 SendMessage(hEdit, EM_SETPASSWORDCHAR, _T('+'), 0); //设置密码字符为+ SetFocus(hEdit); //设置编辑框具有焦点光标 break;
(4)保存工程并运行,运行结果如图2-47所示。

图2-47
4.列表框
列表框给出了一个选项清单,允许用户从中进行单项或多项选择,被选中的项会高亮显示。列表框可分为单选列表框和多选列表框,顾名思义,单选列表框中一次只能选择一个列表项,而多选列表框可以同时选择多个列表项。
列表框控件的窗口类名为“listbox”,也可以使用类名的宏WC_LISTBOX。
该控件的风格以前缀LBS_开头(List Box Style),常见的列表框控件风格如下表所示。

创建多选列表框时,只需要在单选列表框风格后添加LBS_MULTIPLESEL或LBS_EXTENDEDSEL风格。
列表框被操作时会向父窗口发送通知码,常用的列表框通知码定义如下表所示。

应用程序可以向列表框控件发送消息让它执行某个操作或返回列表框的某个属性的当前状态,常见的列表框消息如下表所示。

(续表)

下面我们通过实例来了解列表框控件的使用。
【例2.22】 列表框的基本使用
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建列表框控件 hListBox = CreateWindow(_T("listbox"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP| LBS_STANDARD, 20, 20, 60, 80, hWnd, (HMENU)10000, (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE), NULL); SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)_T("中国")); //向列表框添加数据 SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)_T("美国")); //向列表框添加数据 SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)_T("英国")); //向列表框添加数据 break;
其中hListBox是一个全局变量,用来存放列表框句柄,定义如下:
HWND hListBox = NULL;
函数GetWindowLong用来获取父窗口hWnd的某个属性,宏GWL_HINSTANCE表示需要获取父窗口所在程序的应用程序实例句柄,其实用全局变量hInst来代替这个函数也一样。
虽然我们添加列表框数据的时候先添加的是中国,但因为创建列表框时用了风格LBS_STANDARD,而这个风格是包括LBS_SORT这个排序风格的,因此添加进去的中文数据会按照拼音来排序,得到的结果是美国在第一行,英国在第二行,中国在第三行。
(3)为列表框添加选择改变事件的响应。在消息WM_COMMAND下的switch中添加代码:
case 10000: //10000是列表框控件的ID if (LBN_SELCHANGE == wmEvent) //是否为选择改变事件通知码 { //得到当前选中项索引 nCurIndex = SendMessage((HWND)lParam, LB_GETCURSEL, 0, 0); //得到选中项文本 SendMessage((HWND)lParam, LB_GETTEXT, nCurIndex, (LPARAM)buf); MessageBox(0, buf, 0, 0); //打印结果 } break;
(4)保存工程并运行,运行结果如图2-48所示。

图2-48
【例2.23】 每行背景色不同的自画列表框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建列表框 hListBox = CreateWindow(_T("listbox"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS, 20, 20, 60, 120, hWnd, (HMENU)10000, (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE), NULL); //添加3行数据 SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)_T("中国")); SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)_T("美国")); SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)_T("英国")); break;
其中hListBox是一个全局变量,用来存放列表框句柄,定义如下:
HWND hListBox = NULL;
函数GetWindowLong用来获取父窗口hWnd的某个属性,宏GWL_HINSTANCE表示需要获取父窗口所在程序的应用程序实例句柄,其实用全局变量hInst来代替这个函数也一样。
因为我们要实现一个自画的列表框,所以创建的时候需要有风格LBS_OWNERDRAWFIXED。
(3)创建完毕后,我们开始画出列表框,所谓自画列表框,就是列表框每个项目(每行)的文本字体颜色和背景需要开发者自己完成。对于具有LBS_OWNERDRAWFIXED风格的列表框,画出每个项目(每行)是在消息WM_DRAWITEM中完成的,并且系统在发送WM_DRAWITEM消息前,会发送WM_MEASUREITEM消息,发送该消息的目的是系统需要知道你要为列表框中每个项目(每行)设置的高度(宽度不需要设置,每个项目的宽度就是整个列表框的宽度,在创建列表框时已经确定)。
首先我们响应WM_MEASUREITEM消息,添加该消息响应代码如下:
case WM_MEASUREITEM: if (10000 == wParam) //10000是我们的列表框控件的ID { LPMEASUREITEMSTRUCT lpmis = (LPMEASUREITEMSTRUCT)lParam; lpmis->itemHeight = 30; //设置列表框每行高度,这是行的高度,不是字体高度 } break;
其中,参数wParam存放控件的ID值,lParam指向MEASUREITEMSTRUCT结构体的指针,该结构体包含自画控件或菜单项的尺寸信息。
设置完高度后,就可以画出列表框每个项了,添加消息WM_DRAWITEM,代码如下:
case WM_DRAWITEM: if (10000 == wParam) //10000是我们的列表框控件的ID { LPDRAWITEMSTRUCT pDI = (LPDRAWITEMSTRUCT)lParam; //创建一个固体画刷,用来刷背景 HBRUSH brsh = CreateSolidBrush(RGB(240+ pDI->itemID, 80 * pDI >itemID, 80 * pDI->itemID)); FillRect(pDI->hDC, &pDI->rcItem, brsh); //每行的矩形大小区域用刷子填充 DeleteObject(brsh); //用完要删除创建的刷子 SetBkMode(pDI->hDC, TRANSPARENT); //设置背景透明 //发送消息LB_GETTEXT来获取项文本,根据项的ID(pDI->itemID)来获取 SendMessage(hListBox, LB_GETTEXT, pDI->itemID, (LPARAM)szText); //定义画字符串的风格 const DWORD dwStyle = DT_LEFT | DT_SINGLELINE | DT_Visual C++ENTER | DT_NOPREFIX | DT_END_ELLIPSIS; //画出字符串 DrawText(pDI->hDC, szText, _tcslen(szText), &pDI->rcItem, dwStyle); } break;
涉及一些Visual C++画图方面的函数,现在只需了解即可。其中pDI->itemID就是行的索引号,三行分别为0、1、2。SetBkMode用来设置文本背景透明,文本的背景色就是画刷的颜色,如果没有这个函数,文本的背景是白色的。DrawText根据文本字符个数、文本所占区域大小(pDI->rcItem)和风格来画出文本字符串。
(4)保存工程并运行,运行结果如图2-49所示。

图2-49
【例2.24】 多列列表框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: hListBox = CreateWindow(_T("listbox"), //创建列表框 NULL, WS_CHILD|LBS_MULTICOLUMN| LBS_USETABSTOPS | WS_BORDER |WS_VISIBLE, 20, 10, 320, 100, hWnd, (HMENU)(100), (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE), NULL); SendMessage(hListBox, LB_SETCOLUMNWIDTH, 70, 0); //设置每列的宽度 for (i = 0; i < 20; i++) //添加20条数据 { _stprintf_s(buf, _T("%d:abcdefg"), i); SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)buf); } break;
其中,hListBox是一个全局变量,用来保存列表框控件的句柄,定义如下:
HWND hListBox;
风格LBS_MULTICOLUMN表示如果列表框数据项超过一列时,将继续从下一列开始存放,并且每列的宽度现在设为了70。
(3)保存工程并运行,运行结果如图2-50所示。

图2-50
5.组合框
组合框其实就是把一个编辑框和一个列表框组合到了一起,分为三种:简易(Simple)组合框、下拉式(Dropdown)组合框和下拉列表式(Drop List)组合框。下面讲讲它们的区别。
简易组合框中的列表框是一直显示的,效果如图2-51所示。
下拉式组合框默认不显示列表框,只有在单击了编辑框右侧的下拉箭头才会弹出列表框,列表框弹出后如图2-52所示。
下拉列表式组合框的编辑框是不能编辑的,只能由用户在下拉列表框中选择了某项后,在编辑框中显示其文本。下拉列表式组合框如图2-53所示。

图2-51

图2-52

图2-53
在实际项目开发中,最常用的当属下拉式组合框和下拉列表式组合框了,它们在很多时候能使程序看起来更专业,更简洁,让用户在进行选择操作时更方便。
组合框控件的窗口类名为“combobox”,也可以使用类名的宏WC_COMBOBOX。
该控件的风格以前缀CBS_开头(Combo Box Style),常见的组合框控件风格定义如下表所示。

组合框被操作时会向父窗口发送通知码,常用的组合框通知码定义如下表所示。

应用程序也可以向组合框控件发送消息让它执行某个操作或返回编辑框的某个属性的当前状态,常见的组合框消息如下表所示。

(续表)

下面我们通过实例来了解组合框控件的使用。
【例2.25】 组合框的简单使用
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建Simple风格的组合框 hSimple = CreateWindow(_T("combobox"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_SIMPLE | CBS_HASSTRINGS,10, 20, 80, 100, hWnd, (HMENU)10000, hInst, NULL); //添加数据 for (i = 0; i < 3; i++) SendMessage(hSimple, CB_ADDSTRING, 0, (LPARAM)buf[i]); //创建drop-dow风格的组合框 hDropdown = CreateWindow(_T("combobox"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWN | CBS_HASSTRINGS,100, 20, 80, 100, hWnd, (HMENU)10001, hInst, NULL); //添加数据 for (i = 0; i < 3; i++) SendMessage(hDropdown, CB_ADDSTRING, 0, (LPARAM)buf[i]); //创建drop-down list风格的组合框 hDroplist = CreateWindow(_T("combobox"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWNLIST | CBS_HASSTRINGS, 190, 20, 80, 100, hWnd, (HMENU)10002, hInst, NULL); //添加数据 for (i = 0; i < 3; i++) SendMessage(hDroplist, CB_ADDSTRING, 0, (LPARAM)buf[i]); break;
(3)为后面两个组合框添加选择改变事件的响应。在消息WM_COMMAND下的switch中添加代码:
case 10001:case 10002: //10001和10002是后两个组合框的ID if (CBN_SELCHANGE == wmEvent) //如果是选择改变通知码 { nCurIndex = SendMessage((HWND)lParam, CB_GETCURSEL, 0, 0); //获取当前选中项 //获取选中项的标题 SendMessage((HWND)lParam, CB_GETLBTEXT, nCurIndex, (LPARAM)buf[0]); MessageBox(0, buf[0], 0, 0); //显示结果 } break;
(4)保存工程并运行,运行结果如图2-54所示。

图2-54
6.列表视图控件
列表视图控件是通用控件的一种。它比列表框更适合用来显示表格类的数据,因为它有行和列,并且能对某行某列数据进行编辑。除了以表格方式(报告方式)显示数据外,列表视图控件还有三种方法来显示数据:大图标、小图标和列表。这些方法和在Winodws资源管理器中选择菜单“查看”|“大图标”“小图标”“列表”“详细信息”相对应。各种不同的显示方式只是显示了不同的外观而已,其中报告方式提供的消息最完全,其他的方式则要少得多。在刚创建一个列表视图时您可以选择一种初始显示方法,随后您可以调用SetWinodwLong函数并设置GWL_STYLE标志位来改变显示方式。当处于报告风格显示的时候,第一列通常称为项,这一行的其他列称为子项。
有两种方法创建一个列表视图控件。第一种也是最简单的方法是:通过资源编辑器来创建它,这种方式必须在对话框中进行,对话框我们在下一节会讲到,用该种方法需要在程序中加入函数InitCommonControls的调用(调用该函数只是为了隐式地加载包含通用控件的DLL)。另一种方法是完全手工的方式,即调用CreateWindow函数。
列表视图控件的窗口类名为“SysListView32”,也可以用类名的宏WC_LISTVIEWW。
该控件的风格以前缀LVS_开头(List View Style),常见的组合框控件风格如下表所示。

(续表)

以上风格是列表视图控件的标准风格,除此之外还有扩展风格,扩展风格的前缀是LVS_EX_,常见的扩展风格如下表所示。

值得注意的是,用CreateWindow创建列表视图控件的时候,只使用标准风格,而不使用扩展风格。要为列表视图控件使用扩展风格需要用专门的函数ListView_SetExtendedListViewStyleEx来设置。
列表视图控件被操作时会向父窗口发送WM_NOTIFY消息,并会在NMHDR结构体的code字段中存放通知码,常用的列表视图控件通知码定义如下表所示。

应用程序也可以向列表视图控件发送消息让它执行某个操作或返回编辑框的某个属性的当前状态,常见的列表视图消息如下表所示。

(续表)

上面的消息可以通过SendMessage来发送,如:
SendMessage(hWndControl, LVM_SETITEMTEXT, wParam, lParam );
除此之外,系统定义了很多宏,可以直接调用宏来发送,比如:
#define ListView_SetItemText(hwndLV, i, iSubItem_, pszText_) \ { LV_ITEM _macro_lvi; \ _macro_lvi.iSubItem = (iSubItem_); \ _macro_lvi.pszText = (pszText_); \ SNDMSG((hwndLV), LVM_SETITEMTEXT, (WPARAM)(i), (LPARAM)(LV_ITEM *)&_macro_lvi); \ }
以后要发送LVM_SETITEMTEXT消息,可以这样调用:
VOID ListView_SetItemText( HWND hwnd, int i, int iSubItem, LPCSTR pszText );
其中,hwnd是列表视图控件的句柄;i是项索引(相当于行号); iSubItem是子项索引(相当于列号); pszText是要设置的文本字符串。
要注意的是,ListView_SetItemText看起来像函数,但其实是一个带参数的宏。其他消息都有这样类似的宏,通过宏的方式更直观。
下面通过实例来了解列表视图控件的使用。
【例2.26】 双击某行返回行内容的列表视图控件
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建列表视图控件 hListview = CreateWindow(WC_LISTVIEW, NULL, LVS_REPORT |WS_CHILD | WS_BORDER | WS_VISIBLE, 20, 10, 260, 100, hWnd, (HMENU)(10000), (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE), NULL); //设置扩展风格 dwStyle = ListView_GetExtendedListViewStyle(hListview); //先获取当前扩展风格 dwStyle |= LVS_EX_FULLROWSELECT; //选中某行使整行高亮(只适用于report风格的listctrl) dwStyle |= LVS_EX_GRIDLINES; //网格线(只适用于report风格的listctrl) dwStyle |= LVS_EX_CHECKBOXES; //item前生成复选框控件 ListView_SetExtendedListViewStyleEx(hListview, 0, dwStyle); //设置扩展风格 //插入第一列列头 ColInfo1.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT; ColInfo1.iSubItem = 0; ColInfo1.fmt = LVCFMT_CENTER; ColInfo1.cx = 100; ColInfo1.pszText = _T("姓名"); ColInfo1.cchTextMax = 60; ::SendMessage(hListview, LVM_INSERTCOLUMN, WPARAM(0),LPARAM(&ColInfo1)); //插入第二列列头 ColInfo2.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT; ColInfo2.iSubItem = 0; ColInfo2.fmt = LVCFMT_CENTER; ColInfo2.cx = 50; ColInfo2.pszText = _T("年龄"); ColInfo2.cchTextMax = 20; ::SendMessage(hListview, LVM_INSERTCOLUMN, WPARAM(1),LPARAM(&ColInfo2)); //插入第一行项数据 item.mask = LVIF_TEXT; item.pszText = _T("李四"); item.iItem = 0; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //设置第一行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("31"); item.iItem = 0; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); //插入第二行项数据 item.mask = LVIF_TEXT; item.pszText = _T("张三"); item.iItem = 1; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //插入第二行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("49"); item.iItem = 1; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); //插入第三行项数据 item.mask = LVIF_TEXT; item.pszText = _T("王五"); item.iItem = 2; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //插入第三行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("63"); item.iItem = 2; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); break;
其中,hListview是一个全局变量,用来保存列表视图控件的句柄,定义如下:
HWND hListview;
ColInfo1和ColInfo2都是局部变量,分别存放构造列所需的数据,定义如下:
LVCOLUMN ColInfo1 = { 0 }; LVCOLUMN ColInfo2 = { 0 };
item也是局部变量,用来存放构造项或子项所需的数据,定义如下:
LVITEM item;
dwStyle也是局部变量,用来存放扩展风格,定义如下:
DWORD dwStyle;
项就是第一列数据,子项就是从第二列开始的数据,必须先插入项后才能再插入同行子项。在创建的时候,只能使用标准风格,而不能使用扩展风格。扩展风格要等创建完成后再调用宏ListView_SetExtendedListViewStyleEx来实现,该宏相当于发送LVM_SETEXTENDEDLISTVIEWSTYLE消息。
(3)因为列表视图控件是通用控件,所以需要包含通用控件的头文件和库,在Test.cpp开头添加如下内容:
#include "commctrl.h" #pragma comment (lib, "comctl32.lib")
此时运行工程,可以看到窗口上有一个列表视图控件了,并且每行前面还有一个复选框(Check Box)。
下面我们为它添加点小功能,就是双击列表视图控件中的某行,将跳出一个信息框,显示该行内容。在列表视图控件上双击,会引发控件向父窗口发送WM_NOTIYF消息,代码如下:
case WM_NOTIFY: if (10000 == wParam) //判断是否是列表视图控件,10000是列表视图控件ID { if (((LPNMHDR)lParam)->code==NM_DBLCLK)//判断通知码是否是双击 { //获取当前选中项索引 iSlected = SendMessage(hListview, LVM_GETNEXTITEM, -1,LVNI_SELECTED); if (iSlected == -1) { MessageBox(hWnd, _T(" 请对某行双击 "), _T(" 错误 "), MB_OK | MB_ICONINFORMATION); break; } //为获取项文本,填充结构 memset(&item, 0, sizeof(item)); item.mask = LVIF_TEXT; item.iSubItem = 0; item.pszText = Text; //让指针指向一个字符数组 item.cchTextMax = 256; item.iItem = iSlected; //获取项文本 SendMessage(hListview, LVM_GETITEMTEXT, iSlected,(LPARAM)&item); //暂存到 _stprintf_s(szTemp1, Text); //获取子项文本 item.iSubItem = 1; SendMessage(hListview, LVM_GETITEMTEXT, iSlected,(LPARAM)&item); _stprintf_s(szTemp, _T(" %s"), Text); _tcscat_s(szTemp1, szTemp); MessageBox(hWnd, Temp1, 0, MB_OK | MB_ICONINFORMATION); } } break;
SendMessage(hListview, LVM_GETNEXTITEM, -1, LVNI_SELECTED);获取当前选中的项索引,也可以用宏的形式:ListView_GetNextItem(hListview, -1, LVNI_SELECTED);。
(4)保存工程并运行,运行结果如图2-55所示。

图2-55
【例2.27】 支持按Delete键删除某行的列表视图控件
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建列表视图控件 hListview = CreateWindow(WC_LISTVIEW, NULL, LVS_REPORT |WS_CHILD | WS_BORDER | WS_VISIBLE, 20, 10, 260, 100, hWnd, (HMENU)(10000), (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE), NULL); //设置扩展风格 dwStyle = ListView_GetExtendedListViewStyle(hListview); //先获取当前扩展风格 dwStyle |= LVS_EX_FULLROWSELECT; //选中某行使整行高亮(只适用与report风格的listctrl) dwStyle |= LVS_EX_GRIDLINES; //网格线(只适用于report风格的listctrl) dwStyle |= LVS_EX_CHECKBOXES; //item前生成复选框控件 ListView_SetExtendedListViewStyleEx(hListview, 0, dwStyle); //设置扩展风格 //插入第一列列头 ColInfo1.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT; ColInfo1.iSubItem = 0; ColInfo1.fmt = LVCFMT_CENTER; ColInfo1.cx = 100; ColInfo1.pszText = _T("姓名"); ColInfo1.cchTextMax = 60; ::SendMessage(hListview, LVM_INSERTCOLUMN, WPARAM(0),LPARAM(&ColInfo1)); //插入第二列列头 ColInfo2.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT; ColInfo2.iSubItem = 0; ColInfo2.fmt = LVCFMT_CENTER; ColInfo2.cx = 50; ColInfo2.pszText = _T("年龄"); ColInfo2.cchTextMax = 20; ::SendMessage(hListview, LVM_INSERTCOLUMN, WPARAM(1),LPARAM(&ColInfo2)); //插入第一行项数据 item.mask = LVIF_TEXT; item.pszText = _T("李四"); item.iItem = 0; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //设置第一行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("31"); item.iItem = 0; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); //插入第二行项数据 item.mask = LVIF_TEXT; item.pszText = _T("张三"); item.iItem = 1; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //插入第二行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("49"); item.iItem = 1; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); //插入第三行项数据 item.mask = LVIF_TEXT; item.pszText = _T("王五"); item.iItem = 2; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //插入第三行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("63"); item.iItem = 2; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); break;
其中,hListview是一个全局变量,用来保存列表视图控件的句柄,定义如下:
HWND hListview;
ColInfo1和ColInfo2都是局部变量,分别存放构造列所需的数据,定义如下:
LVCOLUMN ColInfo1 = { 0 }; LVCOLUMN ColInfo2 = { 0 };
item也是局部变量,用来存放构造项或子项所需的数据,定义如下:
LVITEM item;
dwStyle也是局部变量,用来存放扩展风格,定义如下:
DWORD dwStyle;
项就是第一列数据,子项就是从第二列开始的数据,必须先插入项后才能再插入同行子项。在创建的时候,只能使用标准风格,而不能使用扩展风格。扩展风格要等创建完成后再调用宏ListView_SetExtendedListViewStyleEx来实现,该宏相当于发送LVM_SETEXTENDEDLISTVIEWSTYLE消息。
因为列表视图控件是通用控件,所以需要包含通用控件的头文件和库,在Test.cpp开头添加如下内容:
#include "commctrl.h" #pragma comment (lib, "comctl32.lib")
(3)在列表视图控件中按键盘,会触发控件发送WM_NOTIFY消息给父窗口。我们在WndProc的switch分支里添加WM_NOTIFY的处理:
case WM_NOTIFY: if (10000 ==wParam) //判断是否是我们创建的列表视图控件,10000是控件ID { if(((LPNMHDR)lParam)->code==LVN_KEYDOWN)//判断是否为按键通知码 { p = (LPNMLVKEYDOWN)lParam; //把消息参数lParam转换为LPNMLVKEYDOWN if (VK_DELETE == p->wVKey) //判断是否是按了Delete键 { //获取选中的行索引 iSlected = SendMessage(hListview, LVM_GETNEXTITEM, -1,LVNI_SELECTED); if (iSlected == -1) //如果没有选中,则提示错误 { MessageBox(hWnd, _T(" 请先选择要删除的行 "), _T(" 错误 "), MB_OK | MB_ICONINFORMATION); break; } ListView_DeleteItem(hListview, iSlected); //删除选中的行 } } } break;
对于LVN_KEYDOWN通知码,系统会把NMLVKEYDOWN结构体的地址传给消息参数lParam。VK_DELETE就是键盘上的Delete键的宏。如果用户按下Delete键,我们就获取当前选中的项的索引,然后删除选中项。ListView_DeleteItem是一个带参数的宏,向列表视图控件发送LVM_DELETEITEM消息。
(4)保存工程并运行,运行结果如图2-56所示。

图2-56
【例2.28】 支持主项可编辑的列表视图控件
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)在Test.cpp中找到WndProc函数,然后在switch (message)中添加WM_CREATE消息处理,代码如下:
case WM_CREATE: //创建列表视图控件 hListview = CreateWindow(WC_LISTVIEW, NULL, LVS_EDITLABELS| LVS_REPORT |WS_CHILD | WS_BORDER | WS_VISIBLE, 20, 10, 260, 100, hWnd, (HMENU)(10000), (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE), NULL); //设置扩展风格 dwStyle = ListView_GetExtendedListViewStyle(hListview); //先获取当前扩展风格 dwStyle |= LVS_EX_FULLROWSELECT; //选中某行使整行高亮(只适用于report风格的listctrl) dwStyle |= LVS_EX_GRIDLINES; //网格线(只适用于report风格的listctrl) dwStyle |= LVS_EX_CHECKBOXES; //item前生成复选框控件 ListView_SetExtendedListViewStyleEx(hListview, 0, dwStyle); //设置扩展风格 //插入第一列列头 ColInfo1.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT; ColInfo1.iSubItem = 0; ColInfo1.fmt = LVCFMT_CENTER; ColInfo1.cx = 100; ColInfo1.pszText = _T("姓名"); ColInfo1.cchTextMax = 60; ::SendMessage(hListview, LVM_INSERTCOLUMN, WPARAM(0),LPARAM(&ColInfo1)); //插入第二列列头 ColInfo2.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_FMT; ColInfo2.iSubItem = 0; ColInfo2.fmt = LVCFMT_CENTER; ColInfo2.cx = 50; ColInfo2.pszText = _T("年龄"); ColInfo2.cchTextMax = 20; ::SendMessage(hListview, LVM_INSERTCOLUMN, WPARAM(1),LPARAM(&ColInfo2)); //插入第一行项数据 item.mask = LVIF_TEXT; item.pszText = _T("李四"); item.iItem = 0; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //设置第一行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("31"); item.iItem = 0; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); //插入第二行项数据 item.mask = LVIF_TEXT; item.pszText = _T("张三"); item.iItem = 1; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //插入第二行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("49"); item.iItem = 1; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); //插入第三行项数据 item.mask = LVIF_TEXT; item.pszText = _T("王五"); item.iItem = 2; item.iSubItem = 0; ::SendMessage(hListview, LVM_INSERTITEM, 0, LPARAM(&item)); //插入第三行第二列的子项数据 item.mask = LVIF_TEXT; item.pszText = _T("63"); item.iItem = 2; item.iSubItem = 1; ::SendMessage(hListview, LVM_SETITEM, 0, LPARAM(&item)); break;
其中,hListview是一个全局变量,用来保存列表视图控件的句柄,定义如下:
HWND hListview;
ColInfo1和ColInfo2都是局部变量,分别存放构造列所需的数据,定义如下:
LVCOLUMN ColInfo1 = { 0 }; LVCOLUMN ColInfo2 = { 0 };
item也是局部变量,用来存放构造项或子项所需的数据,定义如下:
LVITEM item;
dwStyle也是局部变量,用来存放扩展风格,定义如下:
DWORD dwStyle;
项就是第一列数据,子项就是从第二列开始的数据,必须先插入项后才能再插入同行子项。在创建的时候,只能使用标准风格,而不能使用扩展风格。扩展风格要等创建完成后再调用宏ListView_SetExtendedListViewStyleEx来实现,该宏相当于发送LVM_SETEXTENDEDLISTVIEWSTYLE消息。值得注意的是,因为我们创建的是主项可以编辑的列表视图控件,所以必须包含风格LVS_EDITLABELS。
因为列表视图控件是通用控件,所以需要包含通用控件的头文件和库,在Test.cpp开头添加如下内容:
#include "commctrl.h" #pragma comment (lib, "comctl32.lib")
(3)响应WM_NOTIFY。当用户在主项上完成编辑的时候会向父窗口发送WM_NOTIFY消息。代码如下:
case WM_NOTIFY: if (10000 == wParam) //判断是否是我们创建的列表视图控件,10000是列表视图控件ID { if (((LPNMHDR)lParam)->code == LVN_BEGINLABELEDIT) //判断是否是开始编辑通知码 { hEdit = ListView_GetEditControl(hListview); //得到主项编辑框的窗口句柄 GetWindowText(hEdit, strRaw, sizeof(strRaw)); //保存编辑前主项编辑框的内容 } if (((LPNMHDR)lParam)->code == LVN_ENDLABELEDIT) //判断是否是结束编辑通知码 { int iIndex; TCHAR buf[255] = _T(""); //得到当前拥有焦点的项的索引 iIndex = SendMessage(hListview, LVM_GETNEXTITEM, -1,LVNI_FOCUSED); if (-1==iIndex) break; item.iSubItem = 0; if (! gbPreeEscKey) //判断是否按下Esc键 { //如果没有按下,则不需要还原原来的内容,用新输入的内容更新主项 item.pszText = buf; GetWindowText(hEdit, buf, sizeof(buf)); SendMessage(hListview, LVM_SETITEMTEXT, (WPARAM)iIndex,(LPARAM)&item); } else { //恢复主项原来的内容 item.pszText = strRaw; SendMessage(hListview, LVM_SETITEMTEXT, (WPARAM)iIndex,(LPARAM)&item); gbPreeEscKey = false; //重置是否按下Esc键标记 } } } break;
其中,gbPreeEscKey是一个全局变量,用来标记是否按下了Esc键,定义如下:
bool gbPreeEscKey = false;
hEdit也是个全局变量,用来存放列表视图控件的主项编辑框句柄,其实列表视图控件主项就是一个编辑框子窗口,因此可以支持编辑功能。hEdit的定义如下:
HWND hEdit;
strRaw也是一个全局变量,用来存放主项编辑前原来的文本内容,这样用户如果在编辑的时候不想更新了,可以通过按Esc键来恢复原来的文本内容。strRaw定义如下:
TCHAR strRaw[100]=_T("");
(4)判断用户是否按了Esc键。按键消息是WM_KEYDOWN,我们可以通过这个消息来判断用户是否按下了Esc键,并且这个消息要在WM_NOTIFY之前就要调用,以便我们设标记。因此,需要在主消息循环那里添加,代码如下:
// 主消息循环: while (GetMessage(&msg, NULL, 0, 0)) { if (! TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { //判断是否是按键消息 if (msg.message == WM_KEYDOWN) //这个if语句块就是我们添加内容 { if (VK_ESCAPE == msg.wParam)//判断按下的键的键值是否是Esc键对应的宏 gbPreeEscKey = true; //如果是,则标记一下 } TranslateMessage(&msg); DispatchMessage(&msg); } }
如果标记成功,则就能在WM_NOTIFY消息处理中判断出gbPreeEscKey为true,也就是按下Esc键了,那么就要还原原来主项的文本内容。
(5)保存工程并运行,运行结果如图2-57所示。

图2-57
2.6.9 Win32对话框编程
1.模态对话框和非模态对话框
对话框在Windows操作系统上经常看到。对话框的最重要功能就是在对话框上面可以放置各种控件,让控件和用户进行交互来完成相应的功能。对话框相当于控件的载体。其实,对话框也是一种窗口,它也有自己的窗口函数,并在窗口函数中处理发给对话框的消息。
最简单的对话框要数MessageBox弹出来的对话框了,但该对话框系统已经定制好了,功能十分有限,它就显示一条文本信息。现在我们来创建一个自己设计的对话框。
在创建自己设计的对话框之前,我们要了解下对话框的分类。对话框可以分为模态对话框和非模态对话框,模态对话框一旦创建,必须要关闭后,才能切换到同一程序的其他窗口,在关闭之前,用户只能在模态对话框上进行界面操作,同程序的其他窗口是无法获得焦点的,当然不是同程序的其他窗口照样可以切换;非模态对话框正好相反,创建后可以在非模态对话框和同程序的其他窗口之间进行切换,在关闭非模态对话框之前,同程序的其他窗口是可以获得焦点的。
其实,模态对话框又可分为两类:应用程序模式对话框和系统模式对话框,应用程序模式对话框在关闭对话框前不能切换到同一程序另一窗口,而系统模式对话框在关闭该对话框前无法切换到其他程序任何窗口(如关机时弹出的窗口)。我们主要是学习应用程序模式对话框。
上面所讲的是模式对话框和非模式对话框在运行时候的区别,而在创建的过程中也有很大的不同。主要有以下5点区别:
(1)创建的API函数不同。创建模式对话框调用的API函数是DialogBoxParam,而创建非模式对话框调用的API函数是CreatDialogParam。另外,创建应用程序模式对话框和系统模式对话框之间的差别是对话框模板的style参数的不同,若要创建系统模式对话框,该参数必须“或”(即用符号|)上DS_SYSMODAL标志位。
通常不直接使用函数DialogBoxParam,而是把该函数定义成带参数的宏DialogBox来使用,该宏定义如下:
INT_PTR DialogBox( HINSTANCE hInstance, LPCTSTR lpTemplate, HWND hWndParent, DLGPROC lpDialogFunc );
这个宏根据对话框资源,创建一个模式对话框,这个对话框必须用EndDialog来结束。其中,参数hInstance是当前应用程序实例句柄;lpTemplate标识对话框模板资源,有两种使用方式:一种是把对话框模板的ID强制转为LPCTSTR,如(LPCTSTR)(IDD_DIALOG1),另一种可以使用MAKEINTRESOURCE宏得到标识ID, MAKEINTRESOURCE(IDD_DIALOG1);hWndParent为父窗口的句柄;lpDialogFunc是指向对话框消息处理函数的函数指针。
类似的,创建非模态对话框也不直接使用CreatDialogParam,而是把该函数定义成带参数的宏CreateDialog来使用,该宏定义如下:
HWND CreateDialog( HINSTANCE hInstance, LPCTSTR lpTemplate, HWND hWndParent, DLGPROC lpDialogFunc );
这个函数根据对话框资源,创建一个非模态对话框,这个对话框应该用DestroyWindow来结束。它的参数跟上面的DialogBox用法相同。
(2)显示的条件不同。如果不设置对话框资源的属性Visible为True的话,非模态对话框创建完毕后是不可见的,必须通过调用ShowWindow(SW_SHOW)来显示对话框;如果设置了属性Visible为True,则不需要调用ShowWindow。而模态对话框则无须设置Visible,总会显示对话框。默认情况下,对话框资源的Visible属性为False。
(3)函数返回时间和返回值不同。CreateDialogParam在创建对话框后直接返回,返回值是对话框的窗口句柄;而DialogBoxParam在对话框关闭时才返回,返回值是EndDialog中的dwResult参数。
(4)使用的消息循环不同。模态对话框自己处理消息循环(这个消息循环在user32.dll里面维护,看不到)且在对话框关闭后函数才会返回(返回值是EndDialog的第二个参数,所以可以用EndDialog的第二个参数来标识子控件的ID);而非模态对话框使用主窗口的消息循环,所以如果要处理非模态对话框的消息,需要在主窗口的消息循环中进行判断拦截,如果是对话框消息,系统会主动调用对话框的消息处理函数来处理,具体代码可以这样写:
// 主消息循环: while (GetMessage(&msg, NULL, 0, 0)) { if (hDlgModeless == 0 || ! IsDialogMessage(hDlgModeless, &msg)) //判断是否是对话框消息 if (! TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } }
其中,IsDialogMessage是系统API,用来判断消息msg是否是对话框hDlgModeless的消息。如果是,则把该消息扔给对话框的消息处理函数。
不管是模态还是非模态对话框,对于不希望处理的消息,都不应该调用DefWindowProc来处理(否则会有问题),因为系统会主动对这些消息进行处理。对于不希望处理的消息,程序要做的只是返回FALSE即可;而对于处理过的消息,则应该返回TRUE。这种情况跟主窗口的处理不同,主窗口对不希望处理的消息也要调用DefWindowProc来处理。
(5)关闭时使用的函数不同。关闭模态对话框使用EndDialog函数;关闭非模态对话框使用DestroyWindow函数,并且调用了DestroyWindow函数后会引发向对话框发送WM_DESTROY消息和WM_NCDESTROY消息,并且可以把非模态对话框的全局窗口句柄的置NULL操作放在WM_DESTROY消息处理中,这样下次再创建非模态对话框时候就可以通过判断全局窗口句柄是否为NULL而直接创建了。
【例2.29】 模态对话框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)切换资源视图,然后展开Menu,双击IDC_TEST,在文件菜单项下添加一个菜单项“模态对话框”,然后设置该菜单项的ID为IDM_MOD。再在资源视图里,展开Dialog,对Dialog右击,然后在右键菜单上选择“插入Dialog”来新建一个对话框资源,该对话框的ID默认为IDD_DIALOG1,当然也可以修改,这里就保持默认。
(3)我们的目标是程序运行后,用户单击菜单项“模态对话框”后,就会出现一个模态对话框,为此要添加菜单IDM_MOD的响应。在Test.cpp中找到WndProc函数,然后在switch (message)中找到WM_COMMAND消息处理,然后添加代码如下:
case IDM_MOD: DialogBox(hInst, (LPCTSTR)(IDD_DIALOG1), hWnd, DialogProc); //创建模态对话框 break;
其中,DialogProc是自定义的对话框的消息处理函数,定义如下:
INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_INITDIALOG: SetWindowPos(hWnd, NULL, 50, 100, 0, 0, SWP_NOSIZE); return TRUE; //该消息已经处理了,所以要返回TRUE case WM_COMMAND: //判断是否是确定或取消按钮 if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hWnd, TRUE); //只用于模态对话框 return TRUE; //该消息已经处理了,所以要返回TRUE } break; } return FALSE; //消息没有被处理,交给父窗口继续处理 }
注意,DialogProc函数的类型是INT_PTR,而不是LRESULT。对话框创建完毕并在显示之前会发送WM_INITDIALOG消息,可以把一些控件初始化或对话框显示的个性定制操作放在这个消息中,比如这里用了函数SetWindowPos,该函数是用来设置对话框开始显示的位置的API函数,该函数声明如下:
BOOL SetWindowPos( HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags);
该函数用来改变窗口、弹出窗口和顶层窗口的大小、位置和Z轴次序,功能非常强大,窗口在屏幕上按照它们的Z轴次序排序。在Z轴次序上处于顶端的窗口将位于程序在所有其他窗口的顶部,处于顶部的窗口也叫顶层窗口。其中参数hWnd是将要调整的窗口的句柄;hWndInsertAfter标识了在Z轴次序上位于这个hWnd窗口之前的窗口,这个参数可以是窗口的句柄也可以是下列值:
● HWND_BOTTOM:将窗口放在Z轴次序的底部。如果这个hWnd是一个顶层窗口,则窗口将失去它的顶层状态;系统将这个窗口放在其他所有窗口的底部。
● HWND_TOP:将窗口放在Z轴次序的顶部。
● HWND_TOPMOST:将窗口放在所有非顶层窗口的上面。这个窗口将保持它的顶层位置,即使它失去了活动状态。
● HWND_NOTOPMOST:将窗口重新定位到所有非顶层窗口的顶部(这意味着在所有的顶层窗口之下)。这个标志对那些已经是非顶层窗口的窗口没有作用。
参数X指定了窗口左边的新位置;Y指定了窗口顶部的新位置;cx指定了窗口的新宽度;cy指定了窗口的新高度;nFlags指定了大小和位置选项,这个参数可以是下列值的组合:
● SWP_DRAWFRAME:围绕窗口画出边框(在创建窗口的时候定义)。
● SWP_FRAMECHANGED:向窗口发送一条WM_NCCALCSIZE消息,即使窗口的大小不会改变。如果没有指定这个标志,则仅当窗口的大小发生变化时才发送WM_NCCALCSIZE消息。
● SWP_HIDEWINDOW:隐藏窗口。
● SWP_NOACTIVATE:不激活窗口。如果没有设置这个标志,则窗口将被激活并移动到顶层或非顶层窗口组(依赖于pWndInsertAfter参数的设置)的顶部。
● SWP_NOCOPYBITS:废弃这个客户区的内容。如果没有指定这个参数,则客户区的有效内容将被保存,并在窗口的大小或位置改变以后被拷贝回客户区。
● SWP_NOMOVE:保持当前的位置(忽略x和y参数)。
● SWP_NOOWNERZORDER:不改变拥有者窗口在Z轴次序上的位置。
● SWP_NOREDRAW:不重画变化。如果设置了这个标志,则不发生任何种类的变化。这适用于客户区、非客户区(包括标题和滚动条)以及被移动窗口覆盖的父窗口的任何部分。当这个标志被设置的时候,应用程序必须明确地无效或重画要重画的窗口和父窗口的任何部分。
● SWP_NOREPOSITION :与SWP_NOOWNERZORDER相同。
● SWP_NOSENDCHANGING:防止窗口接收WM_WINDOWPOSCHANGING消息。
● SWP_NOSIZE:保持当前的大小(忽略cx和cy参数)。
● SWP_NOZORDER:保持当前的次序(忽略pWndInsertAfter)。
● SWP_SHOWWINDOW:显示窗口。
对话框上有“确定”和“取消”两个按钮,用户单击这2个按钮时候,会向对话框发送WM_COMMAND消息,因此处理这两个按钮消息应该在该消息中进行。当用户单击“确定”或“取消”按钮时候,将调用函数EndDialog来结束对话框,该函数将销毁一个模态对话框,声明如下:
BOOL EndDialog( HWND hDlg, INT_PTR nResult);
该成员函数用来销毁一个模态对话框,但它不会立即关闭对话框,它设置了一个标记,用以指定在当前消息处理程序返回时就关闭对话框。其中参数hDlg为要销毁的对话框句柄;nResult根据创建对话框的函数来确定返回给应用程序的值。如果函数成功,返回非零;否则,返回零。
(4)保存工程并运行,运行结果如图2-58所示。

图2-58
【例2.30】 非模态对话框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)切换资源视图,然后展开Menu,双击IDC_TEST,在文件菜单项下添加一个菜单项“非模态对话框”,然后设置该菜单项的ID为IDM_MOD。再在资源视图里,展开Dialog,对Dialog右击,然后在右键菜单上选择“插入Dialog”来新建一个对话框资源,该对话框的ID默认为IDD_DIALOG1,当然也可以修改,这里就保持默认。
(3)我们的目标是程序刚运行后就出现一个非模态对话框,并且用户随时单击菜单项“非模态对话框”后,就会创建一个非模态对话框(若还没创建)或激活对话框(已经创建),为此要添加菜单IDM_MODLESS的响应。在Test.cpp中找到WndProc函数,然后在switch (message)中找到WM_COMMAND消息处理,然后添加代码如下:
case IDM_MODLESS: if (hDlgModeless) //通过全局变量来判断是否已经创建了非模态对话框 DestroyWindow(hDlgModeless); //如果创建了,则先销毁 //创建非模态对话框 hDlgModeless = CreateDialog(hInst, MAKEINTRESOURCE(IDD_DIALOG1), hWnd,DialogProc); if (! hDlgModeless) break; ShowWindow(hDlgModeless, SW_SHOW); //显示非模态对话框 UpdateWindow(hDlgModeless); //重画 break;
CreateDialog根据资源名IDD_DIALOG1来创建一个非模态对话框。hDlgModeless是全局变量,为非模态对话框的句柄,定义如下:
HWND hDlgModeless; //非模态窗口句柄
DialogProc是对话框窗口处理过程,定义如下:
INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_INITDIALOG: //响应对话框初始化消息 SetWindowPos(hWnd, NULL, 200, 200, 0, 0, SWP_NOSIZE); return TRUE; // 表示已经初始化 case WM_COMMAND: //响应命令消息 //判断是否单击了“确定”或“取消”按钮 if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) // DestroyWindow(hDlgModeless); return TRUE; case WM_DESTROY: //响应销毁消息 hDlgModeless = NULL; //置全局变量hDlgModeless为NULL return TRUE; } return FALSE; //消息没有被处理,交给父窗口继续处理 }
和模态对话框一样,非模态对话框创建完毕后也会发送WM_INITDIALOG消息,用来进行一些初始化工作,这里调用SetWindowPos函数确定对话框的显示位置和大小(大小和设计时候的一样,因为使用了宏SWP_NOSIZE,它将忽略该函数的cx和cy参数)。
对话框上有“确定”和“取消”两个按钮,用户单击这2个按钮时候,会向对话框发送WM_COMMAND消息,因此处理这两个按钮消息应该在该消息中进行。当用户单击“确定”或“取消”按钮时候,将调用函数DestroyWindow来销毁对话框,该函数将销毁一个非模态对话框,声明如下:
BOOL DestroyWindow(HWND hWnd);
hWnd是要销毁的对话框的句柄。调用该函数将会引发向父窗口发送WM_DESTROY和WM_NCDESTROY两个消息,因此可以把全局变量的对话框句柄重置NULL操作放在WM_DESTROY消息处理中,这样任何地方调用DestroyWindow函数将都会确保对话框全局句柄同时被置空了。这样下次再要创建对话框的时候,就可以通过判断这个全局句柄是否为空来确定要不要创建还是激活显示。一定要确保不要同时创建出多个非模态对话框来,否则资源无法回收,每时每刻只能有一个非模态对话框。
(4)为了让程序一开始就出现非模态对话框,我们在InitInstance函数的末尾处也创建对话框,代码如下:
//创建非模态对话框,如果成功则把对话框句柄存于全局变量hDlgModeless中 hDlgModeless = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), hWnd,DialogProc); if (! hDlgModeless) return FALSE; //如果没有成功,则返回 ShowWindow(hDlgModeless, SW_SHOW); //显示非模态对话框 UpdateWindow(hDlgModeless); //更新重画
如果在设计对话框的时候设置属性Visible为True了,那么ShowWindow和UpdateWindow函数也可以不调用。
(5)非模态对话框使用主窗口的消息循环,所以如果要处理非模态对话框的消息,需要在主窗口的消息循环中进行判断拦截,如果是对话框消息,系统会主动调用对话框的消息处理函数来处理,我们必须要在主消息循环里面添加一个判断:
// 主消息循环: while (GetMessage(&msg, NULL, 0, 0)) { if (hDlgModeless == 0 || ! IsDialogMessage(hDlgModeless, &msg))//若是对话框消息,则对话框处理 if (! TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } }
(6)保存工程并运行,运行结果如图2-59所示。

图2-59
【例2.31】 在模态对话框上可视化创建树形控件
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)切换资源视图,然后展开Menu,双击IDC_TEST,在文件菜单项下添加一个菜单项“模态对话框”,然后设置该菜单项的ID为IDM_MOD。再在资源视图里,展开Dialog,对Dialog右击,然后在右键菜单上选择“插入Dialog”来新建一个对话框资源,该对话框的ID默认为IDD_DIALOG1,当然也可以修改,这里就保持默认。
(3)我们的目标是程序运行后,用户单击菜单项“模态对话框”后,就会出现一个模态对话框,为此要添加菜单IDM_MOD的响应。在Test.cpp中找到WndProc函数,然后在switch (message)中找到WM_COMMAND消息处理,然后添加代码如下:
case IDM_MOD: DialogBox(hInst, (LPCTSTR)(IDD_DIALOG1), hWnd, DialogProc); //创建模态对话框 break;
其中,DialogProc是自定义的对话框的消息处理函数,定义如下:
INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_COMMAND: //判断是否是确定或取消按钮 if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hWnd, TRUE); //只用于模态对话框 return TRUE; //该消息已经处理了,所以要返回TRUE } break; } return FALSE; //消息没有被处理,交给父窗口继续处理 }
注意,DialogProc函数的类型是INT_PTR,而不是LRESULT。
如果此时运行工程,可以发现单击菜单“模态对话框”后,能出现一个模态对话框了。下面我们来为其添加树形控件。
(4)可以采用可视化的方法添加。这比控件在窗口上(虽然对话框也是一个窗口,但不支持可视化操作)方便多了。切换到资源视图,然后双击“Dialog”下的IDD_DIALOG1,此时会出现对话框的设计界面,同时会出现一个“工具箱”的视图,在工具箱中,我们找到树形控件,如图2-60所示。
然后对其单击,并保持不松开,把它拖动到对话框上,如图2-61所示。

图2-60

图2-61
拖放的过程,就相当于上一节的控件在窗口上创建的过程,现在如果运行工程,可以发现对话框上有一个已经“创建”好了的树形控件了,虽然我们没有写一行创建控件的代码,这个创建是Visual C++自动为我们进行的,很方便吧。除了可以拖动外,还可以对其进行属性设置,对对话框上的树形控件右击,选择“属性”菜单项,此时会出现属性视图,或者直接单击树形控件也会出现属性视图。在属性视图里,找到“Has Button”“Has Lines”“Lines At Root”三个属性,分别在其右边选择True。设置完毕后,树形控件上的节点前会出现虚线和“+”小按钮了,“+”表示这个节点下还有子节点,单击它可以展开,“-”按钮表示这个节点已经展开了。这三个属性能让树形控件中的数据看起来更一目了然。如图2-62所示。

图2-62
下面我们来为树形控件添加数据。
(5)在对话框的窗口过程函数DialogProc中添加局部变量:
TV_INSERTSTRUCT tvinsert; //要插入节点的信息结构体 HTREEITEM hParent; //树形控件节点父句柄
然后添加WM_INITDIALOG消息,代码如下:
case WM_INITDIALOG: hTreeView = GetDlgItem(hWnd, IDC_TREE1); //获取树形控件句柄 //初始化根节点数据 tvinsert.hParent = NULL; tvinsert.hInsertAfter = TVI_ROOT; //表示这是一个根节点 tvinsert.item.mask = TVIF_TEXT ; tvinsert.item.pszText = _T("亚洲"); //节点标题 tvinsert.item.iImage = 0; tvinsert.item.iSelectedImage = 0; //向树形控件发送消息插入节点 hParent = (HTREEITEM)SendMessage(hTreeView, TVM_INSERTITEM, 0,(LPARAM)&tvinsert); tvinsert.hParent = hParent; //设置上面的根节点为当前将要插入节点的父节点 tvinsert.hInsertAfter = TVI_LAST; //在末尾插入节点 tvinsert.item.pszText = _T("中国"); //设置节点文本 SendMessage(hTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvinsert); //发送消息插入节点 tvinsert.item.pszText = _T("韩国"); //设置节点文本 SendMessage(hTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvinsert); //发送消息插入节点 tvinsert.item.pszText = _T("日本"); //设置节点文本 SendMessage(hTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvinsert); //发送消息插入节点 tvinsert.item.pszText = _T("印度"); //设置节点文本 SendMessage(hTreeView, TVM_INSERTITEM, 0, (LPARAM)&tvinsert); //发送消息插入节点 return TRUE; //该消息已经处理了,所以要返回TRUE
通常,初始化对话框上的控件都在对话框的WM_INITDIALOG消息中进行。首先通过GetDlgItem函数来获得对话框上的树形控件的句柄,并存放到全局变量hTreeView中,hTreeView是一个句柄。IDC_TREE1是树形控件默认的ID。TVM_INSERTITEM是向树形控件插入节点的消息。
由于树形控件是一个通用控件,所以需要包含头文件:
#include "commctrl.h"
(6)保存工程并运行,运行结果如图2-63所示。

图2-63
2.通用对话框
通用对话框是系统提供的,预先封装了某种功能的对话框,可以直接拿来使用,大大减少了编程的工作量。常见的通用对话框包括颜色选择对话框、字体对话框、打开文件和保存对话框、查找和替换对话框和页面设置对话框等。要注意的是,所有使用通用对话框的地方,都要包含头文件。下面演示几个对话框的使用。
【例2.32】 颜色选择对话框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)切换到资源视图,然后展开Menu,双击IDC_TEST,在文件菜单项下添加一个菜单项“颜色选择对话框”,然后设置该菜单项的ID为IDM_CLRDLG。
(3)我们的目标是程序运行后,用户单击菜单项“颜色选择对话框”后,就会出现一个颜色选择对话框,为此要添加菜单IDM_CLRDLG的响应。在Test.cpp中找到WndProc函数,然后在switch (message)中找到WM_COMMAND消息处理,然后添加代码如下:
case IDM_CLRDLG: clr = ChooseClrDlg(hWnd); if (clr) //如果选择了颜色,则创建画刷并重画客户区 { hbrush = CreateSolidBrush(clr); //创建画刷 InvalidateRect(hWnd, NULL, 1); //重画客户区 } break;
其中,clr、hbrush是局部变量,定义如下:
COLORREF clr; //定义一个颜色 static HBRUSH hbrush = NULL; // 画刷的句柄
COLORREF就是DWORD,用来表示颜色,它定义如下:
typedef DWORD COLORREF;
hbrush表示画刷的句柄,它必须是局部静态变量,因为我们调用InvalidateRect函数会使得窗口客户区无效而需要重画,系统会发送WM_PAINT消息,然后在WM_PAINT消息中,我们需要使用上次创建的画刷来画窗口客户区的背景。
InvalidateRect函数可以使得窗口某个区域无效而需要重画,它会引起WM_PAINT消息的发送。如果第二个参数是NULL,则表示窗口客户区的全部区域都要重画。
函数ChooseClrDlg是我们自定义的函数,用于显示颜色选择对话框,并选择颜色后返回颜色值,代码如下:
COLORREF ChooseClrDlg(HWND hOwner) { CHOOSECOLOR dlgClr; // 定义颜色选择对话框 static COLORREF acrCustClr[16]; //颜色对话框下方有16个自定义颜色,它们需要存储空间 static DWORD rgbCurrent; //初始化CHOOSECOLOR结构体 ZeroMemory(&dlgClr, sizeof(dlgClr)); dlgClr.lStructSize = sizeof(dlgClr); dlgClr.hwndOwner = hOwner; dlgClr.lpCustColors = (LPDWORD)acrCustClr; dlgClr.rgbResult = rgbCurrent; dlgClr.Flags = CC_FULLOPEN | CC_RGBINIT; if (ChooseColor(&dlgClr) )//判断是否单击了“确定” return dlgClr.rgbResult; //返回选定的颜色值 return 0; //如果单击取消,则返回0 }
要使用通用对话框,必须包含头文件:
#include "Commdlg.h"
(4)为WM_PAINT消息添加画背景代码:
case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: 在此添加任意绘图代码... GetClientRect(hWnd, &rt); if (hbrush) FillRect(hdc, &rt, hbrush); EndPaint(hWnd, &ps); break;
其中,GetClientRect是获取窗口客户区矩形大小的函数,rt是一个局部变量,用来存放客户区大小,定义如下:
RECT rt;
FillRect函数表示用画刷填充矩形区域,所谓填充就是用画刷刷背景,画刷是什么颜色的,那么背景也是什么颜色。如同油漆工用刷子刷墙壁一样,墙壁的颜色取决于刷子的颜色。
(5)保存工程并运行,运行结果如图2-64所示。

图2-64
【例2.33】 字体选择对话框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)切换到资源视图,然后展开Menu,双击IDC_TEST,在文件菜单项下添加一个菜单项“字体选择对话框”,然后设置该菜单项的ID为IDM_CLRDLG。
(3)我们的目标是程序运行后,用户单击菜单项“字体选择对话框”后,就会出现一个字体选择对话框,为此要添加菜单IDM_CLRDLG的响应。在Test.cpp中找到WndProc函数,在文件开头定义局部变量:
CHOOSEFONT cf; static COLORREF rgbCurrent; HFONT hfontPrev; static LOGFONT lf; static HFONT hfont; TCHAR sz[] = _T("中国崛起!! ! ABC"); //要显示的文本
其中,cf是字体选择对话框结构体;rgbCurrent是选中的字体颜色;hfontPrev是以前的字体句柄;lf是逻辑字体结构体;hfont是用选择的字体创建的字体句柄。
要使用通用对话框,必须包含头文件:
#include "Commdlg.h"
在switch (message)中找到WM_COMMAND消息处理,然后添加代码如下:
case IDM_FONTDLG: // 初始化CHOOSEFONT结构体 ZeroMemory(&cf, sizeof(cf)); //结构体清0 cf.lStructSize = sizeof(cf); //分配大小 cf.hwndOwner = hWnd; //设置对话框句柄为其拥有者句柄 cf.lpLogFont = &lf; //指向一个逻辑字体 cf.rgbColors = rgbCurrent; //让字体对话框刚显示时字体颜色值就是上次选中的颜色 cf.Flags = CF_SCREENFONTS | CF_EFFECTS; //设置字体对话框选项 if (ChooseFont(&cf))//如果选择了字体,则创建画刷并重画客户区 { hfont = CreateFontIndirect(cf.lpLogFont); //创建逻辑字体 rgbCurrent = cf.rgbColors; //保存字体颜色 InvalidateRect(hWnd, NULL, 1); //重画客户区 } break;
其中,CF_SCREENFONTS表示字体选择框上只出现屏幕字体;CF_EFFECTS选项表示字体选择对话框上有删除线、下划线和颜色等选项。
调用了InvalidateRect函数后,系统会发送WM_PAINT消息,然后我们在这个消息中用创建的字体和选中的颜色画出一行字,代码如下:
case WM_PAINT: hdc = BeginPaint(hWnd, &ps); //TODO: 在此添加任意绘图代码... hfontPrev = (HFONT)SelectObject(hdc, hfont); //把我们创建的字体选进上下文 SetTextColor(hdc, rgbCurrent); //设置字体颜色 TextOut(hdc, 10, 20, sz, _tcslen(sz)); //画一行文本 SelectObject(hdc, hfontPrev); //恢复以前的字体 EndPaint(hWnd, &ps); break;
(4)保存工程并运行,然后在字体选择对话框上选择“华文新魏”,并选择“下划线”,颜色选择紫色,运行结果如图2-65所示。

图2-65
【例2.34】 打开文件对话框和另存为对话框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)切换到资源视图,然后展开Menu,双击IDC_TEST,在文件菜单项下添加一个菜单项“打开文件对话框”,然后设置该菜单项的ID为IDM_OPENFILEDLG。
(3)我们的目标是程序运行后,用户单击菜单项“打开文件对话框”后,就会出现一个打开文件对话框,为此要添加菜单IDM_ OPENFILEDLG的响应。在Test.cpp中找到WndProc函数,在函数开头加入局部变量:
OPENFILENAME ofn; TCHAR szFile[260];
ofn是文件打开对话框结构体变量;szFile用来缓存所选的路径。
然后在switch (message)中找到WM_COMMAND消息处理,然后添加代码如下:
case IDM_OPENFILEDLG: // 初始化OPENFILENAME结构体 ZeroMemory(&ofn, sizeof(ofn)); //清0 ofn.lStructSize = sizeof(ofn); //设置结构体大小 ofn.hwndOwner = hWnd; //设置拥有者句柄 ofn.lpstrFile = szFile; //设置文件名字缓冲区 ofn.lpstrFile[0] = '\0'; //如果不设0,则开始的时候就会默认选择szFile ofn.nMaxFile = sizeof(szFile); //设置所选文件路径缓冲区最大长度 ofn.lpstrFilter=_T("所有文件\0*.*\0文本文档\0*.TXT\0\0"); //设置过滤字符串 ofn.nFilterIndex = 1; ofn.lpstrFileTitle = NULL; ofn.nMaxFileTitle = 0; ofn.lpstrInitialDir = _T("C:"); //设置C盘为当前默认显示路径 ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; //设置标记 if (GetOpenFileName(&ofn)) //显示文件打开对话框 MessageBox(hWnd, ofn.lpstrFile, _T("你所选的文件是:"), MB_OK); //显示所选路径 break;
其中,OFN_PATHMUSTEXIST标记表示显示路径必须存在,OFN_FILEMUSTEXIST表示显示文件必须存在。要注意的是过滤字符串,过滤字符串是用来设置文件后缀名的,只有在过滤字符串中出现的文件后缀名才会显示在文件打开对话框中。过滤字符串的基本格式是文件类型名,然后用\0的隔开,接着再加相应后缀名,然后用\0隔开,再开始新的文件类型名。以此类推,直到最后碰到两个\0的时候表示过滤字符串结束,比如上面“文本文档”是一个文件类型名,“.TXT”是一个后缀名,文件类型名是可以自己定义的,“文本文档”可以写成“我的文本文件”,而后缀名是固定的,但大小写均可,“.txt”也可以。lpstrInitialDir表示对话框的初始化目录,这个字段可以是NULL。lpstrDefExt指定默认扩展名,如果用户输入了一个没有扩展名的文件,那么函数会自动加上这个默认扩展名。
GetOpenFileName函数可以用来显示打开文件对话框。如果要显示另存为对话框,只需要把函数GetOpenFileName改为GetSaveFileName即可。
值得注意的是,要使用通用对话框,必须包含头文件:
#include "Commdlg.h"
(4)保存工程并运行,运行结果如图2-66所示。

图2-66
【例2.35】 打印对话框
(1)打开Visual C++ 2013,新建一个Win32项目,应用程序类型为Windows应用程序。
(2)切换到资源视图,然后展开Menu,双击IDC_TEST,在文件菜单项下添加一个菜单项“打印对话框”,然后设置该菜单项的ID为ID_PRINTDLG。
(3)我们的目标是程序运行后,用户单击菜单项“打印对话框”后,就会出现一个打印对话框,为此要添加菜单ID_PRINTDLG的响应。在Test.cpp中找到WndProc函数,在函数开头加入局部变量:
PRINTDLG pd;
pd是打印对话框结构体变量。然后在switch (message)中找到WM_COMMAND消息处理,然后添加代码如下:
case ID_PRINTDLG: // 初始化PRINTDLG结构体 ZeroMemory(&pd, sizeof(pd)); //结构体清0 pd.lStructSize = sizeof(pd); //设置结构体大小 pd.hwndOwner = hWnd; //设置拥有者句柄 pd.hDevMode = NULL; pd.hDevNames = NULL; pd.Flags = PD_USEDEVMODECOPIESANDCOLLATE | PD_RETURNDC; pd.nCopies = 1; //打印1份 pd.nFromPage =1; //设置打印起始页号 pd.nToPage =10; //设置打印结束页号 pd.nMinPage = 1; pd.nMaxPage = 0xFFFF; if (PrintDlg(&pd) == TRUE) //显示打印对话框 { //开始实际打印 DeleteDC(pd.hDC); //用完要删除DC } break;
(4)保存工程并运行,运行结果如图2-67所示。

图2-67