从一开始编程时,大家面对着黑乎乎的控制台窗口,就开始幻想什么时候能进入windows下进行编程。能够编写出具有形形色色窗口的人往往都被我们认为是牛人。而从控制台窗口进入windows下进行编程,对于初学者来讲,绝对是一个很难跨越的坎。对此,自己也深有体会。因此,一直想写一些真正能够引导大家入门的windows基础应用程序编程的文章。也顺便把自己所知道的东西顺便好好的整理一番。以此与大家共勉。
lpfnWndProc指向窗口过程函数的指针,至于什么是窗口过程将在下面讲解。
如果我们想使用系统预先定义的图标,那么第一个参数应该设置为NULL。
lpszMenuName描述菜单的资源名,如果使用整数标识菜单,则需要使用MAKEINTRESOURCE宏。如果为空,则表明窗口没有默认菜单。
我们在“填写设计表格”之后,就需要把表格提交给“生产车间”进行备案,从而才能够使生产车间根据我们的要求进行生产,这个过程我们成为注册。windows系统中注册窗口我们使用 RegisterClass( RegisterClassEx)函数来完成。我们只需要把WNDCLASS(WNDCLASSEX)的地址传入即可。如
在开始学习windows应用程序编程时,往往大家都有很多误区,大家可能不重视那些“从底做起”的所谓比较low的技术,而直接去追求高效率的框架技术。比如MFC等,然后,在一接触MFC时,往往被这个庞大的东西弄得晕头转向,从而信心全无。从本人的经验以及教训来看,首先学习windows的SDK编程往往会起到事半功倍的效果。所以,在接下来的文章中,我们将首先接触一下这个所谓的“windows底层编程”。
主函数
相信大家在学习C语言编程的过程中,在教材中都看到过这样一段话“任何一个程序都是从main函数开始执行的,并且每一个程序有且仅有一个main函数”。那么在windows程序中,存不存在这样的一个函数呢?答案是肯定的,这个函数就叫做WinMain函数。它的定义如下:
其中_tWinMain即为WinMain,这是由于适应宽字节所产生的结果,暂时可不用去理会。其中APIENTRY为__stdcall如下所示:
其指定了如何由程序产生机械码以及如何在堆栈中放置函数的参数等。一般windows程序都指定为APIENTRY(WINAPI)。
我们还可以看到这个函数还存在着四个参数。并且参数的类型都不知所云,不用着急,我们一个个来分析它。
我们可以看到第一个和第二个参数的类型都叫做HINSTANCE。我们称之为实例句柄。在windows中,运行一个程序,我们就叫做执行一个实例。而句柄在windows中就是用来标识某个东西的数字。有点类似于我们在学生在学校中的学号,我们通过这个学号就可以找到具体的这个人。当我们运行这个程序的时候,windows系统就会为我们的实例分配这样一个“学号”。从而,我们可以根据这个句柄找到我们执行的这个实例。第二个参数在早期的windows版本中有意义,32位的版本中已经被抛弃,传给它的值永远为空。它实际上是指我们多次运行同一个程序的时候,我们也就创建了该程序的多个实例,这些实例共享一些资源。程序可以通过检查第二个参数来确定是否有其他的实例在运行。
第三个参数的类型是LPTSTR,这是一个字符指针类型。用于执行程序的命令列。
最后一个参数指示了程序执行后最初的显示方式,是最大化还是正常显示还是最小化等等。
头文件
我们知道在利用C编写控制台下的程序是,我们首先会毫不犹豫的写上#include <stdio.h>。这个文件中包含了我们经常使用的标准输入输出函数的定义。那么在windows程序中我们也会首先包含一个文件,即windows.h文件。这个文件中又包含了许多其他的表头文件。一般来讲包含基本类型的定义,windows的一些内核函数,使用者接口函数等等。
窗口的诞生
windows程序之所以迷人就是有这友好的用户接口,而这个接口就是窗口。那么如何去产生一个窗口呢?看似是一个非常复杂的问题。不用怕,windows系统已经为我们做好了基本上所有的一切,我们只需要按照windows系统窗口产生的流程去提交我们的需求即可。
我们可以把windows系统想象成一个的生产车间。如果我们需要去生产一个产品,那么第一步我们需要做的就是去设计这个产品。我们要规定这个产品是个什么样子,拥有哪些功能等等。那么对于窗口来讲,就是窗口的风格,有没有菜单,以及光标,图标是什么样子等等。为此,windows为我们提供了一
个WNDCLASS(WNDCLASSEX)结构体来进行窗口的设计。这就像为我们提供一张关于产品特点的表格一样,我们根据这个表格所提供的项去填写,填写完成之后,我们就完成了对产品的设计。
WNDCLASSEX结构体是WNDCLASS的扩展版本。其中WNDCLASSEX结构体的定义如下:
UINT即为无符号整数类型。cbSize表示结构体的字节大小,一般设置为sizeof(WNDCLASSEX)。
style表示窗口类风格,可以窗口类风格的任意组合,常见的窗口类风格有:
Style | 说明 |
CS_DBLCLKS | 允许用户向窗口发送双击鼠标键的消息 |
CS_HREDRAW | 当窗口水平长度改变或移动窗口时,重画整个窗口 |
CS_VREDRAW | 当窗口垂直长度改变或移动窗口时,重画整个窗口 |
CS_NOCLOSE | 禁用窗口系统菜单的关闭选项 |
lpfnWndProc指向窗口过程函数的指针,至于什么是窗口过程将在下面讲解。
cbClsExtra和cbWndExtra是在窗口类别结构和windows内部保存的窗口结构中预留的一些额外空间。一般设置为0即可。
hInstance是实例句柄,即上面所讲的。
hIcon是图标句柄,还记得上面提到过windows用句柄来识别东西吗?同样,windows利用图标句柄来识别图标资源。我们可以利用LoadIcon函数来给它赋值。函数原型如下:
HICON LoadIcon( HINSTANCE hInstance, LPSTSTR lpIconName )
它包含两个参数,第一个参数为实例句柄,第二个参数为一个字符串指针,指向图标的名称。我们一般用MAKEINTRESOURCE宏来把图标的ID转换成所需要的值。如VS2010为我们自动生成的windows程序框架中这样来使用:
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32));
hCursor表示光标的句柄。和图标类似,我们使用LoadCursor函数来设置。
hbrBackground是背景画刷类的句柄,它必须是用于绘制背景的物理画刷的句柄,或者是一个颜色值,如果给出一个颜色的值,它必须是下列标准系统颜色之一(系统将对所选颜色加1)。如果给出了颜色值,它必须转换成下列HBRUSH类型之一的颜色。
COLOR_ACTIVEBORDER
COLOR_ACTIVECAPTION
COLOR_APPWORKSPACE
COLOR_BACKGROUND
COLOR_BTNFACE
COLOR_BTHSHADOW
COLOR_BTNTEXT
COLOR_CAPTIONTEXT
COLOR_GRAYTEXT
COLOR_HIGHLIGHT
COLOR_HIGHLIGHTTEXT
COLOR_INACTIVEBORDER
COLOR_INACTIVECAPTION
COLOR_MENU
COLOR_MENUTEXT
COLOR_SCROLLBAR
COLOR_WINDOW
COLOR_WINDOWFRAME
COLOR_WINDOWTEXT
例如我们可以这样来使用:
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
lpszClassName指向窗口类名,好比我们为自己设计的产品所取得名称。
hIconSm是分配给窗口类的小图标句柄,如果设置为NULL,系统自动搜索一个由hIcon所指示的接近小图标尺寸的图标资源作为小图标。
我们在“填写设计表格”之后,就需要把表格提交给“生产车间”进行备案,从而才能够使生产车间根据我们的要求进行生产,这个过程我们成为注册。windows系统中注册窗口我们使用 RegisterClass( RegisterClassEx)函数来完成。我们只需要把WNDCLASS(WNDCLASSEX)的地址传入即可。如
RegisterClassEx(&wcex);
注册失败将返回0。
注册之后我们就需要根据要求进行生产了,这一步我们通过CreateWindow函数来完成。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 // 窗口创建数据
);
参数意义比较简单,不再相信讲解,最后一个参数涉及WM_CREATE消息。在此也先不讲述。关于窗口的风格常见的有以下几种:
Style | 说明 |
WS_BORDER | 窗口具有瘦线条边界 |
WS_CAPTION | 窗口具有标题栏(包含了WS_BORDER风格) |
WS_CHILD | 子窗口,这种窗口不能有菜单栏,不能够使用WS_POPUP风格 |
WS_CHILDWINDOW | 同WS_CHILD风格 |
WS_DLGFRAME | 拥有对话框似的边界,没有标题栏 |
WS_HSCROLL | 有水平的滚动条 |
WS_MAXIMIZE | 初始最大化 |
WS_MAXIMIZEBOX | 有最大化按钮 |
WS_MINIMIZE | 初始最小化 |
WS_MINIMIZEBOX | 有最小化按钮 |
WS_OVERLAPPED | 有标题栏和边界 |
WS_OVERLAPPEDWINDOW | WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX风格的组合 |
WS_POPUP | 弹出窗口,这种风格不能使用WS_CHILD风格 |
WS_SYSMENU | 有系统菜单,必须指定WS_CAPTION风格 |
WS_THICKFRAME | 有尺寸边界,和WS_SIZEBOX风格一样 |
WS_VISIBLE | 初始化可见,可以通过ShowWindow函数或SetWindowLong函数来打开或关闭 |
WS_VSCROLL | 有垂直滚动条 |
关于窗口的显示位置和宽度及高度可以设置成自己想要的大小,也可以设置为CW_USEDEFAULT,这样系统将使用默认大小,如果x(nWidth)被设置为CW_USEDEFAULT,那么系统将忽视参数y(nHeight)的作用。
我们可以这样来使用CreateWindow函数
CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
函数返回创建的窗口句柄。
创建完窗口之后,我们还需要把窗口显示出来,我们需要使用ShowWindow函数和UpdateWindow函数,如下所示:
BOOL ShowWindow(HWND hWnd, int nCmdShow);
hWnd为窗口句柄,nCmdShow为显示方式,一般可以取值为SW_MAXIMIZE(最大化显示),SW_MINIMIZE(最小化显示),SW_SHOW(按当前的尺寸和位置进行显示)
UpdateWindow(HWND hWnd);
至此一个窗口就诞生了,那么这样就结束了吗?
“消息”
我们知道,如果我们打开一个windows程序之后,不再进行任何操作,那么这个程序将会永无止境的等待下去,除非你在上面进行什么操作,它才会给你反应。那么windows系统是如何知道我们什么时候操作了程序,什么时候没有操作呢?windows系统为我们每一个程序都建立一个消息队列的东西。当对程序进行某种操作,这时就会产生一系列对应的消息,windows系统把这些消息投放到对应的消息队列中,而程序内部必然有一个循环不断的从消息队列中取消息进行逐个处理。所以在没有事件发生时(即没有对应用程序进行任何操作)就不会产生任何消息,消息队列为空,程序不处理任何操作。
说了那么多关于消息的东西,那么在程序中消息到底是什么呢?消息其实是一个名为MSG的结构体变量。其定义如下:
typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
}MSG;
hwnd:接受消息的窗口句柄。
message:消息标识符,一个数值,每个消息都对应一个标识符。用来区分是哪个消息。比如是键盘按下消息还是单击鼠标左键消息。
wParam和lParam:消息的附加信息,具体指根据消息的不同而不同,比如在键盘按下时,根据他们的值可以判断是哪个键按下。
time:消息放入消息队列中的时间
pt:这是一个POINT类型的值,POINT类型是一个包含x和y值的结构体,pt表示事件发生时鼠标的坐标值。(在屏幕坐标中,原点位于左上角,水平向右为x轴的正方向,垂直向下为y轴的正方向)。
那么我们在程序中可以使用下面这个循环来不断的从消息队列中取消息。
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
我们使用GetMessage函数来从消息队列中取得一个消息。第一个参数指向取得的消息。第二个参数表示对应的窗口句柄,第三个和第四个参数分别表示第一个消息和最后一个消息,用来进行消息过滤。如果第二个参数设置为NULL,第三个和第四个参数设置为0表示,程序接受它自己建立的所有窗口的所有消息。
TranlateMessage(&msg); 用来进行一些键盘转换。暂时不详细讲解。
DispatchMessage(&msg); 把取到的消息重新传给windows,然后由windows寻找响应的窗口消息处理函数进行处理。(希望大家不要对这句话视而不见,这是我们下面所要讲述的内容)。
那么消息循环什么时候结束呢?即GetMessage函数返回一个零值时,GetMessage函数每取到一个消息,都会查看这个消息的message字段,如果该字段的值不为WM_QUIT(由宏对应一个UINT值),那么该函数就返回非零值,否则返回零值,退出循环。
消息处理函数
我们上面所产生的窗口可以说是空有其表,其所有的功能也仅仅是显示一个窗口,什么都做不了,甚至连最起码的关闭都关闭不了。那么如何去添加这些功能呢?在开始讲解之前,我们必须要解开windows系统中的一层面纱。那就是大名鼎鼎的“Don't call me,I'll call you”!
我们现在已经很清楚的知道,如果要使程序有反应,那么我们就必须要对取到的消息进行处理。我们上面在DispatchMessage函数中讲到,该函数又将取到的msg消息送回给windows,然后由windows系统根据消息的窗口句柄来调用该窗口的消息处理函数,这个消息处理函数是让我们自己来编写的。一个windows应用程序的主要编写工作就在于对消息的处理,即编写这个消息处理函数。这和传统上的C语言编程有很大的区别,在之前的编程中,我们都是去调用系统提供给我们的函数,而在这里我们则需要编写函数来供windows系统来调用。这就是“Don't call me, I'll call you”的含义。换句话说,我们只需要编写如何对各个消息进行处理,而不用去管,这个消息的处理程序什么时候被调用。
那么如何编写一个窗口的消息处理函数呢?其定义如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
LRESULT是一个64位整数值类型。
其中我们看到有4个参数,其中的含义之前已经说过,这里不再赘述。
我们可以在消息处理函数体中,使用switch/case结构通过判断message的值来判定是哪个消息,最后,不要忘记调用默认的消息处理函数,因为我们不可能对所有的消息都处理完。例如:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_DESTROY:
消息的处理...
break;
case WM_....
...
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
一个完整的例子
打开VS2010,文件->新建->项目,选择Win32项目,在设置中选择空文件,输入以下代码
// win32.cpp : 定义应用程序的入口点。
//
#include <windows.h>
#include <tchar.h>
// 全局变量:
HINSTANCE hInst; // 当前实例
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
MyRegisterClass(hInstance);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
// 主消息循环:
while (GetMessage(&msg, NULL, 0, 0))
{
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(NULL, MAKEINTRESOURCE(IDI_APPLICATION));
wcex.hCursor = LoadCursor(NULL, MAKEINTRESOURCE(IDC_ARROW));
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = TEXT("MyFirstApp");
wcex.hIconSm = NULL;
return RegisterClassEx(&wcex);
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目的: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // 将实例句柄存储在全局变量中
hWnd = CreateWindow(TEXT("MyFirstApp"), TEXT("MyFirstApp"), WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目的: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
运行结果
相信经过上面的分析,你能够很轻松的看懂这个具体的框架了。