前言
在Windows中,大大小小的应用程序都是由一个个窗口构成的。开发桌面应用程序的第一步就是创建一个Windows窗口。那么我们该如何创建出这个窗口呢?很简单,只需六步你就能用C语言创建出自己的第一个Windows窗口。当我们创建出窗口之后,就可以按照我们的意愿开发自己想要的桌面应用程序了。
效果演示(创建第一个窗口)
目录
开始准备
我们先创建一个project 。选择Windows Application,即Windows应用程序。语言选择C或者C++均可。项目创建后,会生成一个源文件,把源文件中的代码删除,我们要自己写一个窗口。
入口函数
每一个C程序都有一个入口函数。在控制台(黑框)程序中,入口函数就是main函数。在Windows应用程序中,它的入口函数为WinMain。WinMain函数被定义在头文件windows.h中,因此我们要在代码的开头包含它。
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nCmdShow)
在WinMain函数的前面,有一个大写的WINAPI,这是函数的调用约定。在minwindef.h中有如下定义:
#define WINAPI __stdcall
在C语言中没有显示函数的调用约定,默认是__cdecl ,而Windows API(Windows应用程序接口)函数会显示函数的调用约定。
函数的调用约定规定了函数的入栈方式和栈的清理方式。
__cdecl | C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡 |
__stdcall | windows API默认方式,参数从右向左入栈,被调函数负责栈平衡 |
WinMain函数的第一个和第二个参数的类型为HINSTANCE,这是应用程序实例句柄类型。在winnt.h中有如下定义:
#define DECLARE_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name
可以写成如下形式:
#define DECLARE_HANDLE(HINSTANCE)
struct HINSTANCE__
{
int unused;
};
typedef struct HINSTANCE__ * HINSTANCE
由此可以清楚地看出, HINSTANCE其实就是结构体指针类型。
WinMain函数的第一个参数hInstance为当前应用程序实例句柄。所谓句柄,可以认为是应用程序的标签,这个句柄就代表了这个应用程序。第二个参数hPreInstance为前一个应用程序实例句柄。32位应用程序及以后,这个参数就没有使用了,其值NULL,只是在16位应用程序上面保留这个形式。
第三个参数为LPSTR类型,在winnt.h中有如下定义:
typedef char CHAR;
typedef CHAR *NPSTR,*LPSTR,*PSTR;
LPSTR类型实际上就是 char * 型。第三个参数lpCmdLine为命令行参数。
第四个参数nCmdShow为窗口的显示方式,根据其值的不同可以为窗口选择最大化、最小化、隐藏和正常等显示方式。
窗口过程函数
除了入口函数WinMain,Windows应用程序还有一个重要的函数:WndProc(窗口过程函数)。在应用程序中,窗口过程函数会对窗口的所有反应进行处理。如果没有窗口过程函数,应用程序几乎什么也做不了。WndProc函数的调用如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam);
可以看到, WndProc函数的调用约定为CALLBACK,在minwindef.h中有如下定义:
#define CALLBACK __stdcall
WndProc函数的调用约定与入口函数WinMain相同,均为__stdcall 。
WndProc函数的返回类型为LRESULT。在minwindef.h中有如下定义:
typedef LONG_PTR LRESULT;
可以在basetsd.h中找到LONG_PTR的定义:
__MINGW_EXTENSION typedef __int64 LONG_PTR,*PLONG_PTR;
在_mingw.h中可以继续找到__int64的定义:
#define __int64 long long
由此可知,LRESULT为long long类型。同理可得WndProc函数的后三个参数的类型:
UINT:unsigned int
WPARAM:unsigned long long
LPARAM:long long
WndProc函数的第一个参数的类型HWND为窗口句柄类型。按照winnt.h中的定义,可以得到:
#define DECLARE_HANDLE(HWND)
struct HWND__
{
int unused;
};
typedef struct HWND__ * HWND;
它与HINSTANCE相同,为结构体指针类型。但是不能将HWND与HINSTANCE混淆,HWND为窗口句柄类型,HINSTANCE为应用程序实例句柄。
在窗口过程函数中,会对消息进行处理。所谓消息,就是Windows发出的一个通知,告诉应用程序某个事情发生了 。窗口过程函数对消息的处理如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam)
{
switch (Message)
{
case WM_DESTROY: //窗口销毁消息
{
PostQuitMessage(0); //将WM_QUIT消息插入到消息队列
break;
}
default:
return DefWindowProc(hWnd, Message, wParam, lParam);
}
return 0;
}
我们用switch-case结构来对各种消息进行处理。这里我们只对窗口的销毁消息进行处理,其他消息交给DefWindowProc函数进行默认处理。当点击窗口的关闭按钮时,Windows会向应用程序发送WM_DESTROY消息。当该消息产生后, PostQuitMessage函数会把WM_QUIT消息插入到消息队列中。当该消息被获取后,程序结束运行。
创建第一个Windows窗口
了解完入口函数与窗口过程函数,就可以开始创建窗口了。
第一步:设计窗口类
所谓的窗口类其实不是C++中的类,而是一个结构体。结构体成员如下:
typedef struct
{
UINT style; //窗口类的风格
WNDPROC lpfnWndProc; //窗口过程函数
int cbClsExtra; //窗口类额外空间内存大小
int cbWndExtra; //窗口额外空间内存大小
HINSTANCE hInstance; //当前应用程序实例句柄
HICON hIcon; //图标
HCURSOR hCursor; //鼠标光标
HBRUSH hbrBackground; //背景
LPCTSTR lpszMenuName; //菜单名
LPCTSTR lpszClassName; //窗口类名
} WNDCLASS, *PWNDCLASS;
第一个成员为窗口类的风格,类型为unsigned int。窗口类的风格如下:
第二个成员为指向函数的指针,赋值时会将指向WndProc函数的指针赋给该成员。
第三个成员为当前应用程序实例句柄。
第三个和第四个成员为窗口类的额外空间内存大小和窗口的额外空间内存大小,用于在类结构和Windows内部维护的窗口结构中预留一些额外的空间。
后面三个成员分别加载窗口的图标、鼠标光标和背景。
最后两个成员为菜单名和窗口类名,类型为LPCTSTR。在winnt.h中有如下定义:
typedef char CHAR;
typedef CONST CHAR *LPCSTR,*PCSTR;
typedef LPCSTR PCTSTR,LPCTSTR,PCUTSTR,LPCUTSTR;
LPCTSTR实际为const char * 类型。如果没有菜单,则菜单名赋值为NULL。
要想设计窗口类,我们要先定义窗口类。定义好窗口类后对窗口类的成员进行赋值。代码如下:
WNDCLASS wc; //定义窗口类
wc.style = CS_HREDRAW | CS_VREDRAW; //窗口类的风格
wc.lpfnWndProc = WndProc; //窗口过程函数
wc.cbClsExtra = 0; //窗口类额外空间内存大小
wc.cbWndExtra = 0; //窗口额外空间内存大小
wc.hInstance = hInstance; //当前应用程序实例句柄
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); //加载系统图标
wc.hCursor = LoadCursor(NULL, IDC_ARROW); //加载系统鼠标光标
wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1); //加载背景
wc.lpszMenuName = NULL; //菜单名
wc.lpszClassName= "WindowClass"; //窗口类名
窗口类的风格可以使用多种,不同的风格之间用“|”符号分隔。 加载图标和鼠标光标用LoadIcon函数和LoadCursor函数,函数中的第二个参数为系统图标和系统鼠标光标的ID号。当加载系统图标和鼠标光标时,第一个参数设置为NULL,如果从保存在磁盘上的可执行文件中加载,则设置为 hInstance。在加载窗口背景时使用了CreateSolidBrush函数,通过此函数可以自定义窗口的背景颜色。最后将窗口类名"WindowClass"赋给窗口类名成员,窗口类名可以自定义,不一定是"WindowClass"。
还有几种不同的窗口类:WNDCLASSA、WNDCLASSW、WNDCLASSEX、WNDCLASSEXA、WNDCLASSEXW。
WNDCLASSA:
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,*NPWNDCLASSA,*LPWNDCLASSA;
WNDCLASSW:
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,*NPWNDCLASSW,*LPWNDCLASSW;
WNDCLASSEXA:
typedef struct tagWNDCLASSEXA {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEXA,*PWNDCLASSEXA,*NPWNDCLASSEXA,*LPWNDCLASSEXA;
WNDCLASSEXW:
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,*PWNDCLASSEXW,*NPWNDCLASSEXW,*LPWNDCLASSEXW;
其中,WNDCLASSW和WNDCLASSEXW分别为WNDCLASSA和WNDCLASSEXA的Unicode版本。仔细观察可以发现:Unicode版本的窗口类的菜单名和窗口类名为LPCWSTR类型;窗口类中有EX的版本比没有EX的版本多了cbSize和hIconSm这两个成员;WNDCLASS与WNDCLASSA的成员相同。
在winnt.h中有如下定义:
typedef wchar_t WCHAR;
typedef CONST WCHAR *LPCWSTR,*PCWSTR;
由此可知LPCWSTR类型为const wchar_t * 型。通过此类型,使得Unicode字符的菜单名和窗口类名的应用程序得以运行。
在EX版本的窗口类中,cbSize成员表示窗口类的字节大小,赋值为sizeof(窗口类)。hIconSm成员表示小图标句柄。
由WNDCLASS与WNDCLASSA的成员相同可以推出WNDCLASSEX与WNDCLASSEXA的成员相同。
在Windows中,为了满足Unicode字符的需要而设计与之相对应的函数和类型,如WNDCLASSW、WNDCLASSEXW等,还有下面要讲的注册窗口类函数、创建窗口函数等,这些函数和类型都有与之向对应的版本,之后就不再赘述了。
第二步:注册窗口类
设计好窗口类之后,就要进行窗口类的注册。窗口类用RegisterClass函数进行注册。注册函数的调用如下:
if(!RegisterClass(&wc))
{
MessageBox(NULL, "Window Registration Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK);
return 0;
}
如果注册窗口类失败就调用MessageBox函数生成一个对话框,提示注册失败,然后退出程序。
第三步:创建窗口
注册完窗口类之后,开始创建窗口。窗口用CreateWindow函数创建。CreateWindow函数参数如下:
HWND CreateWindow(
LPCTSTR lpClassName, //窗口类名
LPCTSTR lpWindowName, //窗口的标题
DWORD dwStyle, //窗口的风格
int x, //窗口横坐标
int y, //窗口纵坐标
int nWidth, //窗口的宽度
int nHeight, //窗口的高度
HWND hWndParent, //父窗口句柄
HMENU hMenu, //菜单句柄
HANDLE hInstance, //当前应用程序实例句柄
PVOID pParam //创建参数
);
(注:窗口的横纵坐标为窗口左上角相对于屏幕左上的像素点的个数。当一个窗口中包含另一个窗口时,被包含的窗口为包含窗口的子窗口,包含窗口为子窗口的父窗口。)
CreateWindow函数创建窗口后会返回一个为HWND类型(窗口句柄类型)的值。我们需要一个变量来保存这个值,这样我们才能在其他地方通过这个变量操作这个由CreateWindow函数创建的窗口。CreateWindow函数的调用如下:
HWND hWnd = CreateWindow(
"WindowClass", //窗口类名
"first window", //窗口的标题
WS_VISIBLE|WS_OVERLAPPEDWINDOW, //窗口的风格
CW_USEDEFAULT, //默认初始横坐标
CW_USEDEFAULT, //默认初始纵坐标
CW_USEDEFAULT, //默认初始宽度
CW_USEDEFAULT, //默认初始高度
NULL, //无父窗口句柄
NULL, //无菜单句柄
hInstance, //当前应用程序实例句柄
NULL //无创建参数
);
我们可以在调用CreateWindow函数时定义这个变量,也可以在入口函数的开头定义。在CreateWindow函数的参数中,窗口的标题为窗口标题栏中要显示的字符,窗口的坐标和大小使用了系统默认的初始横纵坐标和初始宽度、高度,由于我们创建的这个窗口没有父窗口和菜单,所以这两个参数的值为NULL。创建参数可以指向某些数据,以便后续在程序中使用,这里我们给它赋值为NULL。关于窗口的风格,有如下种类:
窗口的风格一般使用WS_VISIBLE和WS_OVERLAPPEDWINDOW。
另外,EX版本的窗口创建函数CreateWindowEx比CreateWindow多一个参数,其第一个参数为窗口的扩展风格,其余的参数与CreateWindow相同。
第四步:显示窗口
窗口创建好了,还要把窗口显示出来。显示窗口的函数为ShowWindow:
ShowWindow(HWND hWnd, int nCmdShow);
函数的第一个参数为窗口句柄,第二个参数为窗口的显示方式。显示的类型如下:
这里我们选择SW_SHOW。
ShowWindow(hWnd, SW_SHOW);
第五步:更新窗口
如果我们想要在窗口运行期间改变窗口中的内容,就需要通过更新窗口把不同的内容展现出来。更新窗口用UpdateWindow函数。
UpdateWindow(hWnd);
第六步:消息循环
Windows窗口是由消息驱动的,因此我们要不断地从消息队列中抓取消息。从消息队列中抓取消息用GetMessage函数。其参数如下:
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
函数的第二个参数为窗口句柄,第三个和第四个参数分别为检索到的消息的最低值和最高值。函数的第一个参数为一个结构体变量,该结构体被定义在winuser.h中,成员如下:
typedef struct tagMSG
{
HWND hwnd; //消息所指向的窗口的句柄
UINT message; //消息标识符
WPARAM wParam; //一个32位的消息参数
LPARAM lParam; //另一个32位的消息参数
DWORD time; //消息进入消息队列的时间
POINT pt; //消息进入消息队列时鼠标指针的位置坐标
} MSG,*PMSG,*NPMSG,*LPMSG;
为了保存消息的信息,我们需要定义一个消息的结构体。通常将该结构体变量定义在入口函数的开头。
MSG msg;
有了保存消息的结构体后,就要不断地从消息队列中抓取消息。我们可以用一个While循环让GetMessage函数不断地获取消息:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg); //转换键盘消息
DispatchMessage(&msg); //将消息发送给合适的窗口过程函数处理
}
在GetMessage函数中,第二、第三和第四个参数分别被设置位NULL和0,表明该希望获取由该程序所创建的所有窗口消息。在每一轮的消息循环中,会将获取到的消息发送给合适的窗口过程函数处理,如果获取的消息为键盘消息则先对键盘消息进行转换。将消息发送给窗口过程函数就意味着调用了窗口过程函数。当DispatchMessage函数结束后,又会进行下一轮的消息循环。
至此,就完成了第一个窗口的创建。接下来,你就可以按照自己的意愿开发自己想要的桌面应用程序了。
完整代码
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
HWND hWnd;
//一、设计窗口类
WNDCLASS wc; //定义窗口类
wc.style = CS_HREDRAW | CS_VREDRAW; //窗口类的风格
wc.lpfnWndProc = WndProc; //窗口过程函数
wc.cbClsExtra = 0; //窗口类额外空间内存大小
wc.cbWndExtra = 0; //窗口额外空间内存大小
wc.hInstance = hInstance; //当前应用程序实例句柄
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); //加载系统图标
wc.hCursor = LoadCursor(NULL, IDC_ARROW); //加载系统鼠标光标
wc.hbrBackground= (HBRUSH)(COLOR_WINDOW+1); //加载背景
wc.lpszMenuName = NULL; //菜单名
wc.lpszClassName= "WindowClass"; //窗口类名
//二、注册窗口类
if(!RegisterClass(&wc))
{
MessageBox(NULL, "Window Registration Failed!", "Error!", MB_ICONEXCLAMATION | MB_OK);
return 0;
}
//三、创建窗口
hWnd = CreateWindow(
"WindowClass", //窗口类名
"first window", //窗口的标题
WS_VISIBLE|WS_OVERLAPPEDWINDOW, //窗口的风格
CW_USEDEFAULT, //默认初始横坐标
CW_USEDEFAULT, //默认初始纵坐标
CW_USEDEFAULT, //默认初始宽度
CW_USEDEFAULT, //默认初始高度
NULL, //无父窗口句柄
NULL, //无菜单句柄
hInstance, //当前应用程序实例句柄
NULL //无创建参数
);
//四、显示窗口
ShowWindow(hWnd, SW_SHOW);
//五、更新窗口
UpdateWindow(hWnd);
//六、消息循环
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg); //转换键盘消息
DispatchMessage(&msg); //将消息分发给窗口过程函数处理
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam)
{
switch (Message)
{
case WM_DESTROY: //窗口销毁消息
{
PostQuitMessage(0); //将WM_QUIT消息插入到消息队列
break;
}
default:
return DefWindowProc(hWnd, Message, wParam, lParam);
}
return 0;
}