我们在使用windows系统时,有2类窗口:应用程序窗口和对话框。窗口中的每一个可视对象都是一个“子窗口”或“控制窗口”或“子窗口控件”。用户可以通过鼠标和键盘直接和窗口进行交互,窗口以“消息”的形式接收窗口的输入。在windows系统中,可以通过鼠标拖动窗口的边框改变窗口的大小,这些实际上是Windows本身而不是应用程序处理的。当用户改变窗口时,Windows给程序发送一条消息通知新窗口的变化,接着程序就可以调整窗口。
Windows给程序发送消息是说:Windows调用程序中的一个函数,该函数的参数描述了这个特定的消息,这种位于Windows程序中的函数被称为“窗口过程”。程序创建的每一个窗口都有相关的窗口过程,窗口过程是一个函数,可以在程序中也可以在动态链接库中。Windows通过调用窗口过程给窗口发送消息,窗口过程根据这个消息进行处理,接着将控制返回Windows。
窗口是在“窗口类”的基础上创建的。窗口类标识了处理窗口消息的窗口过程。使用窗口类使多个窗口能够基于同一个窗口类,并且使用同一个窗口过程。
Windows程序开始执行后,Windows为该程序创建一个消息队列,该队列存放该程序可能创建的各种不同窗口的消息。消息循环:用来从队列中取出消息,并且将他们发送给相应的窗口过程。有些消息可以直接发送给窗口过程,不用放入消息队列中。
1、实例
// TestWindowsMessage.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include "TestWindowsMessage.h"
#define MAX_LOADSTRING 100
// Global Variables:
HINSTANCE hInst; // current instance
TCHAR szTitle[MAX_LOADSTRING]; // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name
// Forward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: Place code here.
MSG msg;
HACCEL hAccelTable;
static TCHAR szAppName[] = TEXT("HelloWorld!");
HWND hWnd = NULL;
WNDCLASS wndClass;
wndClass.style = CS_HREDRAW | CS_VREDRAW;
wndClass.lpfnWndProc = WndProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = hInstance;
wndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = szAppName;
if (!RegisterClass(&wndClass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
return 0;
}
hWnd = CreateWindow(szAppName, TEXT("The Hello Program"), WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
// Initialize global strings
// LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
// LoadString(hInstance, IDC_TESTWINDOWSMESSAGE, szWindowClass, MAX_LOADSTRING);
// MyRegisterClass(hInstance);
//
// // Perform application initialization:
// if (!InitInstance (hInstance, nCmdShow))
// {
// return FALSE;
// }
//
// hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TESTWINDOWSMESSAGE));
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// FUNCTION: MyRegisterClass()
//
// PURPOSE: Registers the window class.
//
// COMMENTS:
//
// This function and its usage are only necessary if you want this code
// to be compatible with Win32 systems prior to the 'RegisterClassEx'
// function that was added to Windows 95. It is important to call this function
// so that the application will get 'well formed' small icons associated
// with it.
//
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_TESTWINDOWSMESSAGE));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_TESTWINDOWSMESSAGE);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassEx(&wcex);
}
//
// FUNCTION: InitInstance(HINSTANCE, int)
//
// PURPOSE: Saves instance handle and creates main window
//
// COMMENTS:
//
// In this function, we save the instance handle in a global variable and
// create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // Store instance handle in our global variable
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;
}
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_COMMAND - process the application menu
// 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)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
RECT rect;
switch (message)
{
case WM_CREATE:
{
PlaySound(NULL, NULL, SND_FILENAME | SND_ASYNC);
break;
}
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rect);
DrawText(hdc, TEXT("Hello Windows 98!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
break;
}
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
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_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// Message handler for about box.
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:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
因为程序中使用了PlaySound,但默认的项目中不包含多媒体对象库,因此需要链接Winmm.lib库,并且包含MMSystem.h头文件。否则的话编译失败。
1、解析
我们描述为在窗口的中央显示文本,其实是不完全准确的,应该是客户区的中央。客户区就是程序自由绘图并向用户交付可视输出的窗口区域。
代码中的WindProc实际上就是一个窗口过程。其中有很多大写的数值常量,根据前缀区分为以下
前缀 | 类别 | 说明 |
---|---|---|
CS | 类风格选项 | Class Style |
CW | 创建窗口选项 | Create Windows |
IDI | 图标ID号 | Icon ID |
IDC | 光标ID号 | Cursor ID |
MB | 消息框选项 | Message Box |
SND | 声音选项 | Sound |
WM | 窗口消息 | Window Message |
WS | 窗口风格 | Window Style |
MSG | 消息结构 | Message |
WNDCLASS | 窗口类结构 | Window Class |
PAINTSTRUCT | 绘图结构 | Paint Struct |
RECT | 矩形结构 | Rectangle |
HINSTANCE | 实例句柄Instance Handle | |
HWND | 窗口句柄 | Window Handle |
HDC | 设备描述表句柄 | Device description handle |
句柄是一个数,代表一个对象。
当程序中存在某个变量但是没有使用的情况下,编译器就会发出警告。如Windows程序的WinMain的hInstance和lpCmdLine在大多数情况下是不使用的,这个时候编译器就会发出警告。
若不希望编译器发出警告,就可以使用UNREFERENCED_PARAMETER宏处理对应的变量
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
这样就算hPrevInstance和lpCmdLine没有使用编译器也会忽略。
注册窗口类
窗口总是在窗口类的基础上创建的,窗口类用以标识处理窗口消息的窗口过程。在为程序创建窗口之前,必须首先调用RegisterClass注册一个窗口类,需要一个指向类型为WNDCLASS的结构指针:
typedef struct tagWNDCLASSA {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
} WNDCLASSA, *PWNDCLASSA, NEAR *NPWNDCLASSA, FAR *LPWNDCLASSA;
typedef struct tagWNDCLASSW {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCWSTR lpszMenuName;
LPCWSTR lpszClassName;
} WNDCLASSW, *PWNDCLASSW, NEAR *NPWNDCLASSW, FAR *LPWNDCLASSW;
这个结构最重要的是第2个和最后一个域,即lpfnWndProc和lpszClassName。第2个域是所有基于这个类来创建的窗口所使用的窗口过程的地址。最后一个域是窗口类的文本名。在16位的Windows中,启动正在运行的程序的一个新的实例,WinMain的hPrevInstance参数是前一个实例的实例句柄,为了节省内存,多个实例允许共享相同的窗口类,这样窗口类就只在hPrevInstance为NULL的时候注册;在32位的Windows中,hPrevInstance总是NULL,因此若需要检查互斥,需要程序员设置互斥量。
创建窗口
窗口类定义了窗口的一般特征,因此可以使用同一个窗口类创建多个窗口。窗口类没有定义窗口的所有特征,这是因为不同的窗口可能需要不同样式风格的子窗口,因此窗口类只负责处理消息。当在创建顶层窗口的时候,父窗口句柄的值应默认是NULL,若有窗口存在父子关系,那子窗口会显示在父窗口的上面。若窗口没有菜单,那么窗口菜单句柄也应设置为NULL。程序实例句柄设置为WinMain的实例句柄,创建参数指针设置为NULL,用于访问以后想访问的程序中的数据。
CreateWindow返回的是被创建窗口的句柄,Windows中的每个窗口都有一个句柄,程序用句柄引用窗口。Windows的函数大多有句柄这个参数,这样Windows才能知道是哪个窗口。
显示窗口
实际上在CreateWindow返回之后,Windows内部就已经创建了这个窗口了,即是Windows已经分配了一块内存用来保存CreateWindow调用中指定窗口的全部信息,以及Windows在之后可以找到的所有其他的信息,但是这个时候窗口还没有显示,这时候就需要其他的两个调用了:ShowWindows和UpdateWindow。
ShowWindows和UpdateWindow](https://blog.csdn.net/m0_48073095/article/details/123845565)
消息循环
在窗口显示之后,就可以准备读入用户输入的数据了。在发生输入事件之后,Windows将事件转换为一个“消息”,并将消息放入消息队列中,程序执行“消息循环”的代码从消息队列中取出消息:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
只要从消息队列中取出消息的message域不是WM_QUIT,GetMessage就会返回一个非0值。
TranslateAccelerator翻译加速键表,该函数处理菜单命令中的加速键;TranslateMessage将message结构传给Windows,进行一些键盘转换;
DispatchMessage将message结构回传给Windows;之后Windows鉴跟这个消息发送给适当的窗口过程,让窗口过程进行处理,处理完成之后,窗口过程会返回Windows,这个时候Windows还停留在DispatchMessage的调用中。在结束DispatchMessage调用的处理之后,Windows回到WinMain中,并且从下一个GetMessage调用开始消息循环。
窗口过程
我们知道,实际的动作处理是发生在窗口过程中的。窗口过程确定了在窗口的客户区域中如何显示,以及如何响应。窗口过程可任意命名,一个Windows程序可以包含多个窗口过程,一个窗口过程总是与调用RegisterClass注册的特定窗口类相关联。
程序通常是不直接调用窗口过程的,一般是由Windows本身调用。通过调用SendMessage函数,程序能够直接调用它自己的窗口过程。
处理消息
窗口过程接收的每个消息均是用一个数值来标识的,即窗口过程的message参数是个UINT类型的值。在Windows的头文件中对于消息类型以“WM”为前缀标识。在窗口过程中我们使用switch和case结构确定窗口过程接收的事件类型以及如何处理该事件。窗口过程在处理消息时必须返回0,窗口过程不予处理的所有消息应该都会被传给名为DefWindowProc的Windows函数。从DefWindowProc返回的值必须由窗口过程返回。示例的窗口过程如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
RECT rect;
switch (message)
{
case WM_CREATE:
{
PlaySound(NULL, NULL, SND_FILENAME | SND_ASYNC);
break;
}
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rect);
DrawText(hdc, TEXT("Hello Windows 98!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
break;
}
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
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_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
调用DefWindowProc来为窗口过程不予处理的消息提供默认处理,这是很重要的,除非能按常规运行。
在Windows中,“从程序外调用程序内”这种概念适用所有事件,窗口中发生的一切都以消息的形式传给窗口过程,接着由窗口过程以某种方式响应这个消息,或者将消息传给DefWindowsProc进行默认处理。有时候DefWindowsProc处理完消息之后会产生其他消息,典型的示例就是用户点击了窗口的CLOSE按键。
进队消息和不进队消息
Windows的消息分为“进队的”和“不进队的”。进队的消息是由Windows放入程序消息队列中的,在程序的消息循环中,重新返回并分配给窗口过程的。不进队的消息在Windows对用窗口时直接送给窗口过程。即是:进队的消息被发送给消息队列,不进队的消息发送给窗口过程。不管什么情况下窗口过程都将获得窗口所有的消息(进队的、不进队的),窗口过程是窗口的“消息中心”。
许多情况下不进队消息来自调用特定的Windows函数,从窗口过程的角度看,这些消息是以一种有序的、同步的方式进出的(在一个窗口过程中处理消息时,程序不会被其他消息突然中断),窗口过程可以处理也可以不处理。
Windows程序可以多线程执行,但每个线程的消息队列只为窗口过程在该线程中执行的窗口处理消息。即消息循环和窗口过程不是并发运行的。窗口过程能调用给窗口过程发送另一个消息的函数,窗口过程必须在函数调用返回之前完成对第二个消息的的处理,也就是说窗口过程必须是可重入的。我们在编译Windows程序时会关闭编译优化选项的原因之一就是因为:调用的Windows函数产生了另外一个消息,并且窗口过程在处理该消息时改变了一些变量的值,就会导致在Windows函数返回的时候变量的值可能就不是开始的值。在大多数情况下窗口过程必须保存它从消息中获取的信息,并在处理另一个消息时使用这些信息,可存储在窗口的static变量或全局变量中。
由于Windows构造的方式,抢占式多任务并不总是以希望的方式工作。虽然用户可以将控制切换到另一个程序,但是却不能对当前正忙的程序进行任何动作。这种情况在实际使用中会给用户很不好的体验,因此应想办法让程序从全部消息中快速的返回。