https://blog.csdn.net/majalis_C/article/details/88920674
https://blog.csdn.net/majalis_C/article/details/88827871
https://blog.csdn.net/majalis_C/article/details/88921745
https://blog.csdn.net/majalis_C/article/details/88927392
有了以上几篇文章的基础,就可以开始写第一个Windows应用了。
王叔叔告诉我们,做事先定一个小目标,比如赚他一个亿。在赚一个亿之前,你需要知道如何写一个空白的窗口。
就像这样。
我们先来贴实现它的完整代码,然后再逐个解释。
以下是实现这个窗口程序的完整代码。在贴代码之前呢,要插一句。我们现在用的编译器版本大多很高,会有很多的集成环境供人选择。例如MFC。但是要写win32程序,需要建立一个空项目。visual studio建立空项目的方法是点击新建,然后选择windows桌面向导,在弹出的对话框中勾选空项目,就可以建立一个完完全全什么都没有的空项目。在源文件的文件夹下新建.cpp文件,键入以下代码,执行。
#ifndef UNICODE
#define UNICODE
#endif
#include <windows.h>
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// Create the window.
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
if (hwnd == NULL)
{
return 0;
}
ShowWindow(hwnd, nCmdShow);
// Run the message loop.
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(hwnd, &ps);
}
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
其中wWinMain函数是程序的入口函数,程序一开始,它注册了一些有关于窗口行为的信息。其中最重要的是处理函数,在这里被命名为WindowProc,它定义了窗口的行为,包括窗口的显示,以及如何同内部控件交互等等。
接下来,程序创建了一个窗口,并且获得一个唯一标识窗口的handle。
当窗口创建成功,程序进入了一个循环体中,程序一直在循环体中,直到用户关闭窗口退出应用。
需要注意的是,程序并非明显地调用WindowPro函数。窗口通过一系列的Message(消息)来和程序进行通信。循环体中的代码驱动着这一过程。每当调用DispatchMessage函数的时候,它都会间接导致Windows对每个Message调用一次WindowPro函数。
我们先来看看程序是如何创建一个窗口的。
创建窗口之前,需要了解window class。window class定义了大多数窗口的都有的行为。例如,在一组button中,当用户点击button时,每个按钮都有相似的行为。当然了,每个button的行为都不是完全一致的。
每个窗口都必须关联一个window class,即使你的程序只为该类创建一个实例。很重要的一点是要理解window class不是C++意义上的类。它是操作系统内部使用的一个数据结构。在程序运行时向操作系统注册window class。
要注册一个window class,首先要填写一个WNDCLASS结构。
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
程序中所示的三个结构成员是必须要赋值的。class name是当前进程的本地名称,所以在当前进程中必须唯一。标准的windows控件也有类,在使用这些控件的时候,其控件名称不可与类名相同。例如,Button类型控件的名称不能为 'Button'。
向操作系统中注册窗口类的时候,需要将上诉窗口结构体的地址传递给RegisterClass函数。该函数将窗口类注册到操作系统。
RegisterClass(&wc);
做了以上工作,就可以开始创建一个窗口了。
可以调用CreateWindowEx函数来创建一个窗口实例。
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
if (hwnd == NULL)
{
return 0;
}
我们来大致说一下,CreateWindowEx函数中的各个参数是干什么用的。
第一个参数可以让你为窗口制定一些可选行为。当该值为0时表示默认设置。
CLASS_NAME是window class的名称,其定义了你创建的窗口类型(button,listview 或其他)。
对于不同的窗口类型,window text的使用方式是不一样的。如果你创建的window有标题栏,那么该值显示在标题栏当中。
Window style是一组定义window外观的标志,他们定义了窗口是否有标题栏,边界线,系统菜单等特性。
对于位置和大小,CW_USEDEFAULT 意为使用默认的位置和大小。
接下来的参数为新创建的window指定parent window或是owner window。
紧随其后的参数为窗口定义菜单,本例中并未给窗口定义菜单,故填NULL。
hInstance是程序实例handle,开头给出的文章中有介绍,当程序装入内存后,操作系统用它来唯一标识一个exe。
最后一个参数是指向*void的任意数据的指针,可以用此值来传递一个数据结构体给window procedure。
CreateWindowEx返回一个handle给新创建的window。如果创建window失败,它将返回0值。将window的handle当做参数传递给ShowWindow函数中,来使得window可见。
ShowWindow(hwnd, nCmdShow);
参数hwnd是CreateWindowEx返回的window的handle,nCmdShow在最大化或最小化时使用。操作系统通过wWinMain函数将以上两个参数的值传递给程序。
到这里为止,便是完整的创建了一个window。
但我们仅仅是创建了一个空白的window,没有任何内容,也没有任何交互性。在实际应用中,window是可以对操作系统或用户发出的事件进行响应的。window通过消息来传递事件以及响应事件。
下面我们就看看窗口的消息机制如何提供交互性。
GUI应用程序必须响应来自用户和操作系统的事件。事件可能在程序运行过程中的任何时候发生。那么怎么构建一个无法预先预测执行流程的程序响应结构呢?
windows采用消息传递模型来解决这一问题。操作系统通过传递消息来同window进行通信。一个消息(message)是指标识特定事件的数字代码。
例如鼠标的点击事件:
#define WM_LBUTTONDOWN 0x0201
一些消息含有与其相关联的数据,例如鼠标的点击消息含有点击位置的 x 坐标和 y 坐标。
操作系统通过window procedure函数来传递消息。就是本例中的windowpro函数。在注册window class时曾用到过。
一个应用在执行过程中将接收到很多的消息。另外,一个应用可以有多个窗口,每个窗口都有其自己的windowpro函数。那么程序是如何接收这么多的消息并且正确地做出响应的呢?
应用程序利用一个循环体来检索消息并将其分派到正确的窗口。
每个创建了窗口的线程,操作系统都为其创建一个窗口消息队列。此队列中包含了该线程所创建的所有窗口的消息。队列是隐藏的,无法显式地操作,但是可以通过GetMessage方法来从队列中取出一个消息。
MSG msg;
GetMessage(&msg, NULL, 0, 0);
该方法获取队列头的一个消息,如果队列是空的,那么该方法便阻塞,直到有消息进入队列。GetMessage函数的第一个参数是MSG结构体的地址,如果该函数执行成功,则将获取的消息信息写入MSG结构体变量中。消息中包含目标窗口及消息数字编码等信息。其他的三个参数可以让你筛选要获得的消息,不过在几乎所有的情况下,他们都被设置为0。
虽然MSG结构体中包含了消息的所有信息,但你往往不需要去直接访问其中的内容,而是将其传递给两个函数。
TranslateMessage(&msg);
DispatchMessage(&msg);
TranslateMessage函数同键盘输入的操作有关,无需关注它的内部细节,需要牢记的是,必须在DispatchMessage函数之前调用它。DispatchMessage告诉操作系统调用目标窗口的windows procedure。
举个栗子,假设用户点击了鼠标的左键,这一举动导致了一连串的事件:
1,操作系统将WM_LBUTTONDOWN消息放入消息队列。
2,程序调用GetMessage函数。
3,GetMessage函数获取WM_LBUTTONDOWN消息并将其写入MSG结构体中。
4,程序调用TranslateMessage函数和DispatchMessage函数。
5,在DispatchMessage函数中,操作系统调用目标窗口的window procedure函数。
6,Window procedure选择响应消息事件或者是忽略。
当window procedure执行完,其返回DispatchMessage函数。循环继续,程序进行下一次消息的处理。通常,GetMessage函数返回的是一个非0值,也就是说循环体的判断条件可能一直为真,其会一直循环下去。如果想要退出应用程序并且跳出循环体,调用PostQuitMessage函数。
PostQuitMessage(0);
PostQuitMessage向消息队列中添加一个WM_QUIT消息。WM_QUIT是一个特殊的消息,它使得GetMessage函数返回0值,以此让程序退出循环。window procedure从来都不接收WM_QUIT消息,没有必要将其写入window procedure函数中。
说了半天消息机制,该说说消息的处理过程了。大多数的消息都是在window procedure中处理的。
那么我们如何来实现window procedure呢?
以上提到过,DispatchMessage方法调用目标窗口的window procedure来响应消息。window procedure有如下声明结构。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
hwnd参数是window 的 handle。
uMsg是消息数字代码,例如WM_SIZE标识window要调整尺寸。
wParam和lParam包含于消息有关的其他数据,其意义同特定的消息有关。
LRESULT是一个整型数值,其值包含程序对特定消息的响应。其含义由传入的消息数字代码决定。
经典的window procedure是一个switch语句,case语句下用来处理特定的消息。
switch (uMsg)
{
case WM_SIZE: // Handle window resizing
// etc
}
其他的消息相关信息在wParam和lParam参数中,两个参数都是指针大小的整数。
如果没有处理特定的消息,则默认调用DefWindowPro函数。其定义如下
return DefWindowProc(hwnd, uMsg, wParam, lParam);
我们提到过,每个线程都有一个消息队列,那么在多线程的情况下,就要避免窗口程序阻塞。
当window procedure执行时,其阻塞了一个队列中的其他窗口的消息事件。因此,要避免在window procedure函数中处理耗时长的任务。否则,窗口将长时间处于无响应状态,直到该耗时任务执行完毕。在处理耗时任务是,可以创建一个线程去专门做这件事。
可以利用以下几种方式中的一种来处理。
1,创建一个新的线程。
2,使用线程池。
3,使用异步I/O调用。
4,使用异步过程调用。
那么,到现在,你已经创建了一个窗口,并且可以通过消息与其进行通信。还缺点儿什么?窗口是不是太空了,似乎就像一个空白的画布一样,等着你向里面绘制内容。
有时候,程序会启动窗口绘制程序来改变窗口的外观。在一些时候,操作系统会提醒你必须重新绘制窗口的特定区域。当此事发生时,操作系统发送一个WM_PAINT消息给窗口,需要绘制的位置称为update region。
当窗口第一次显示的时候,全部的client区域都需要被绘制。因此窗口至少会收到一次WM_PAINT信息。
我们只需要绘制client区域,边框的区域,包括标题栏,操作系统会帮我们绘制。绘制完成后,需要清除update region,该动作告诉操作系统,除非有改变,否则不需要发送WM_PAINT消息。
现在,我们假设用户将另一个窗口移到了现有窗口之上,使得现有窗口部分被遮挡。当窗口移开,被遮挡的部分重新显示,则该区域为update region,此时窗口将收到另一个WM_PAINT消息。
在开头给出的全部代码中,我们绘制client区域的方式很简单,只是为其填充了固定的颜色。但是它也能体现很多的概念。我们再来看看那段代码。
switch (uMsg)
{case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// All painting occurs here, between BeginPaint and EndPaint.
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(hwnd, &ps);
}
return 0;
}
调用BeginPaint函数来进行绘制的开始,该函数用重绘请求的信息来填写PAINTSTURCT结构。当前的绘制区域(update region)是由PAINTSTRUCT中的rcPaint成员提供的。
update region相对于client区域定义。
在你的处理绘制的代码中,有两种情况。一是绘制整个client区域。另一个是仅绘制特定的update region。
以下代码用单一颜色绘制了特定的update region。
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
COLOR_WINDOW是系统默认的背景颜色。第二个参数给出了绘制区域的坐标。
当绘制完成后,调用EndPaint函数,其清除update region,表明已经完成了绘制。
到这里,我们介绍了对窗口的基本操作,包括窗口如何创建,如何响应事件,如何绘制。
在最后,介绍窗口的关闭。
当用户关闭窗口时,该行为将触发WM_CLOSE消息。在处理此消息时,可以做一些关闭窗口之前需要做的操作。
如果想要关闭窗口,调用DestroyWindow函数。如果不想关闭,则可以直接返回0值,操作系统将忽略这一消息,不销毁窗口。
case WM_CLOSE:
if (MessageBox(hwnd, L"Really quit?", L"My application", MB_OKCANCEL) == IDOK)
{
DestroyWindow(hwnd);
}
// Else: User canceled. Do nothing.
return 0;
还记得前面介绍的DefWindowPro函数吗,它会执行一些消息的默认操作。如果忽略了在switch中处理WM_CLOSE消息,DefWindowPro函数会默认调用DestroyWindow函数来对窗口进行关闭。
当窗口被销毁的时候,会收到一个WM_DESTROY消息。这个消息在窗口从屏幕上移除之后,窗口销毁之前(特别是子窗口被销毁之前)发送。
case WM_DESTROY:
PostQuitMessage(0);
return 0;
在你的程序中,通常需要响应WM_DESTROY消息,在处理该消息时调用PostQuitMessage函数。
下图说明了窗口关闭到销毁的过程。
好了,到现在为止,一个窗口的从生到死就简单介绍完了。拿着这些基础去写代码吧!
原文链接:https://docs.microsoft.com/zh-cn/windows/desktop/LearnWin32/learn-to-program-for-windows