今天首先我会介绍基本的数据类型, 以及说明什么是HANDLE和INSTANCE.
之后我会以一个小程序作为例子, 大致说明Windows编程都干了什么. 并在此期间穿插这介绍Window的概率和Windows程序运行的机制.
我认为这部分相当的枯燥, 也是Windows编程入门最难的地方. 我会仔细并努力将每个要点都点到. 大家看完后懂不懂, 就看大家造化了.
值得一提的是, 看完这篇文章后, 可能会发现:虽然明白怎么回事, 但是还是写不出属于自己的程序, 这是正常的.
HANDLE, INSTANCE
DWORD
MS似乎钟爱使用DWORD这个数据类型. 这里的DWORD和IA32汇编编程的DWORD不一样. 它其实是一个unsigned long. 在80x86环境下unsigned long是4字节. 所以DWORD是32位无符号整数.
HANDLE, INSTANCE
前者中文叫句柄, 后者叫实例. 如果你直接被告知这两个东东是用来识别各种对象的, 你可能还是不知道为什么要使用它和什么时候需要使用它. 所以, 我们需要从操作系统的角度来理解它们.
操作系统需要管理很多东西, 各种窗口, 各种按钮, 各种设备, 各种xxx. 操作系统需要识别他们, 就像给每个人一个名字.于是有个人想到:不如让他们每人拿着一个数字, 而且每人的数字都不一样不就行了. 这就是HANDLE.
不过又有问题出现了, 就拿设备来说, 分类太多了, 都不知道以后会有什么样的新设备出现. 于是程序员想, 干脆就别分类了. 所以你会发现, 一个窗口拿着一个HANDLE, 一个ICON(图标) 也拿着一个HANDLE, 一个用来输出输入的设备也拿着一个HANDLE. 到处都是HANDLE. 想知道你系统中有多少HANDLE? 也许你会在任务管理器中发现它.
接着接着… 程序员看着一堆HANDLE觉得头大, 突然想到了typedef. 于是, 窗口的HANDLE变成HWND, ICON的变成HICON, 菜单的变成HMENU. 等等. 其实他们都是HANDLE, 只是看上去似乎有点区别.
INSTANCE其实也是一种HANDLE, 但是它识别的东西有点特殊. 你暂且可以认为它是用来识别各种进程的HANDLE. 一般来说, 你仅会在载入”资源文件”, 和使用”dll”的时候使用它. 换句话说, 一个相同的exe, 如果你同时打开两个. 那么这两个具有不同的INSTANCE.
Window
也许之前你看了许多, 还是没觉得Windows编程有啥特别. 不过看来这一篇, 相信你会发现不一样的地方.
这里说的Window可不是一个打开的窗体, 而是Window. 算了, 还是让我们先来看一个窗体.
我知道这是一个窗体, 可是他有多少个Window呢, 我大致数了下, 大概20个. 下面这个图中的红框框全部是Window, 我没画全, 因为我不想让这张图变得连我都不认识.
是的, 每个按钮, 每个控件, 都是一个Window. 这点一定要理解, 我认为这是Windows设计相当独到的地方. 也是很难理解的地方 ,我在这会给予比较简要的说明. 不过会比较枯燥.
为了给予区别, 在下面我会使用”窗口”这个词来代表我们通常所说的窗口, 而使用Window 在指窗口里面的各种Window. (虽然Window的翻译就是窗口.)
每个Window都有自己独立的属性, 比较重要的有:
CLASS (window 类)
比如按钮, 他们的CLASS叫做”button”.
值得一提的是, 每个”窗口”包含了很多window, 但是其自身也是一个window, 而且拥有自定义的CLASS名称.
STYLE (样式)
比如说按钮, 有带只图标的, 有只带文字的. 有正在显示的, 有正在隐藏的 … 怎么控制呢? 拿STYLE. 另外, 一个window如果想成为”窗口”, 需要拥有特定的STYLE才行.
TEXT(内容)
在不同的window 类里面有着不同的意义, 比如在按钮里面, 这个就表示按钮要显示的字, 在”窗口”上, 这个代表”窗口”的标题.
PARENT(父窗体)
一个”窗口”里面拥有很多Window, 但是它自身也是一个Window. 后者就是前者的PARENT. 相对的, 就有CHILD(子窗体)的概率, 每个Window都可以拥有CHILD. (如果一个Window没有PARENT, 那么它一定是一个”窗口”)
MENU (菜单)
虽然名字叫菜单, 但是他仅仅在当前的window是一个”窗口”的时候才表示窗口的菜单. 如果当前的window是一个按钮, MENU则表示这个按钮被按下后所发出的命令.
PROC (消息处理函数)
严格上说, 这个可以是window CLASS的属性, 不过每个window可以拥有单独是处理函数来处理各种各样的事件.
如果你不理解上面所说的东西, 可以先暂时跳过. 这个东西的理解需要一定时间. 你可以在下一段落中慢慢理解.
但是你必须明白, 每个按钮, 每个控件啊什么的, 都是一个Window. 这是基础.
Windows程序的结构
第一个带窗口的WIN32程序.
接下来我会带大家分析一个带窗口的WIN32程序.
使用VS2008创建一个”Win32 Project”, 我起名叫"Win32 Basic". 什么配置都不要改, 创建完了之后编译运行, 能够看到下面的窗口.
接下来我会给大家分析这个示例代码的重要部分, 并借此给大家说明Windows程序的结构.
我跳过的部分建议你不必太关心, 他们大多数只是在实现一些细节的功能. 你暂时大可跳过.
入口:
如果你还在找main的话, 那么很遗憾的说, 没有main, 整个程序的入口在这里.
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
_tWinMain, 看上去也是一种main, 不过参数不太一样, 不过别担心, 你不需要主动调用这个函数. 真正调用它的是操作系统, 它会为你填好参数. 除了hInstance之外, 其他参数在这里你也无需关心. (_t作为前缀, 还记得上一篇中介绍的UNICODE吧).
注册CLASS
接着程序运行了MyRegisterClass()这个自定义函数. Window有个重要的属性是CLASS, 但是每个独立的窗口都必须拥有一个CLASS, 这个CLASS得自己创建. 操作系统提供了”注册窗体类”的功能, 对CLASS进行注册之后, 就可以使用了. 这个函数注册了一个名字为szWindowClass的CLASS (注意, szWindowClass不是CLASS的名字, 而是szWindowClass这个变量存的内容, szWindowClass已经通过LoadString这个系统函数得到了一个字符串.) 我们来看看这个函数干了什么.
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex; //定义一个结构, 这个结构要用来注册CLASS用的.
wcex.cbSize = sizeof(WNDCLASSEX); //初始化结构
wcex.style = CS_HREDRAW | CS_VREDRAW; //定义CLASS的STYLE, Window有STYLE, 其实CLASS也有的.
wcex.lpfnWndProc = WndProc; //所有”这个CLASS的Window”的消息处理函数都是WndProc (这个是函数指针, 很多C语言的书都没有提及)
wcex.cbClsExtra = 0;//忽略
wcex.cbWndExtra = 0;//忽略
wcex.hInstance = hInstance;//这个CLASS所属于的Instance, 一般来说, 别的Instance是不能使用的
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32BASIC)); //定义所有”这个CLASS的Window”的图标,它有可能是exe程序的图标
wcex.hCursor = LoadCursor(NULL, IDC_ARROW); //定义所有”这个CLASS的Window”的鼠标指针
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); //定义所有”这个CLASS的Window”的背景颜色
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_WIN32BASIC); //定义所有”这个CLASS的Window”的菜单
wcex.lpszClassName = szWindowClass; //这个CLASS的名字
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); //定义所有”这个CLASS的Window”的小图标
return RegisterClassEx(&wcex); //告诉操作系统关于这个CLASS的信息, 今后就可以使用它了.
}
创建Window
没错, 就在InitInstance这个自定义函数里. 这个函数最重要的是运行了下面的一个函数. 我们伟大的CreateWindow :
CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
这个函数非常重要和常用. 所以这里我会对这个函数的参数进行简单说明.
HWND CreateWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);
LPCTSTR是一个宏+typedef, 最终会等价于TCHAR*
HWND, HMENU, HINSTANCE都是HANDLE, 分别代表了窗口句柄, 菜单句柄, Instance
LPVOID 等价于 void *
lpClassName: Window的CLASS名, 在现在的例子中, 这个CLASS名已经在MyRegisterClass()函数中注册过.
lpWindowName: Window的 TEXT, 在这里表示”窗体”的标题
dwStyle: Window的STYLE值, 你可以在MSDN中找到很多STYLE值, 它们中的大多数是可以同时使用的, 同时使用的方法就是使用”或”运算. 如 WS_CHILD | WS_VISIBLE. 这里的WS_ OVERLAPPEDWINDOW是一个普通的独立”窗体”所需的STYLE. (事实上, 它也是多个STYLE的组合).
x, y, nWidth, nHeight: 就像他们的名字一样, 它代表了Window的位置. 如果它拥有父窗体, x, y则代表其在父窗体中的位置. CW_USEDEFAULT则是一个特殊的数值, 组合使用它可达到很多效果, 这里CW_USEDEFAULT, 0, CW_USEDEFAULT, 0组合能够让系统选择合适的位置和大小显示窗体.
hWndParent: 父窗体的句柄, 这里用NULL, 代表他是一个独立显示的”窗口”
hMenu: Window的MENU句柄. 这里使用NULL. (不过我们可以看到这个主窗体是有菜单的, 其实这是因为窗体的CLASS已经定义了MENU的属性)
hInstance: Instance的句柄, 程序入口_tWinMain的第一个参数就是它. 注意Instance都是由操作系统分配的, 你能做的只是在该使用它的时候使用它.
lpParam: 这是一个void指针, 它能够提供一些额外的数据. 很多Windows的函数都有类似的参数, 很显然这是一个成功的设计. 在这里不需要提供额外的信息, 则使用NULL. 至于如何通过它提供额外的信息, 你可以查阅MSDN.
接下来运行了ShowWindow();UpdateWindow(); 功能就是显示窗体和刷新窗体.
消息循环
函数的_tWinMain函数的末尾运行了以下代码.
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
你并不需要看懂每句话, 你只要明白这个循环的功能是:
接收每个消息, 并且调用对应的Window的消息处理函数处理这个消息. 要理解这点, 你需要理解Windows程序的运行机制.
Windows程序的消息处理机制
这个名字是我自己起的, 如果你去网上搜索, 可能一无所获.
大家可能习惯于C程序的那种逻辑. 从main开始, 一句一句执行知道退出. 大家是否想过, 我们平时运行的带窗口的应用程序是否是这样, 显然不是.
事实上, 纯ANSI C程序运行的原理应该是下面这个过程.
- 操作系统载入程序到内存.
- 操作系统找到程序入口: main函数.
- 操作系统运行main函数.
- 操作系统发现main函数运行完了, 清理程序信息, 释放内存.
我想说的就是一句话, 操作系统在控制你的程序. 你的程序是不是可运行的程序, 是操作系统说的算. 你的程序要运行, 怎么运行, 也是操作系统说的算.
Windows程序是有着特殊的结构的, 这就是传说中的PE结构. 这也就是为什么上面那个示例程序入口不再是main, 而变成WinMain. 操作系统找到WinMain后会逐步运行. 一般在WinMain中会有个消息循环, 就是上面讲到的循环函数. 里面调用了GetMessage之类的系统函数. 其作用就是不断接收系统消息, 并且经过一定处理后, 让操作系统调用对应的Window的PROC函数.说白了, 就是下面的过程.
创建了一个窗口, 里面N多Window, 这些Window会产生各种各样的事件, 操作系统会记下来.存在一个叫”消息列队”的地方.
while(给我一个系统消息) {
操作系统, 你给我把这个消息告诉属于它的Window去. (实质是调用这个Window的消息处理函数PROC)
}
你可能会想, 为什么操作系统不直接告诉属于它的Window. 我的回答是:每个程序默认只有一个线程. 所以它每次只能处理一个消息. 调用一个函数. 另外, 主函数需要控制所以的Window以及他们的消息, 对吧.
消息处理函数.
WndProc函数就是刚才建立的Window的PROC(”消息处理函数”). 之前说到PROC可以在Window的CLASS中定义. 我想你已经猜到了, 在运行MyRegisterClass()的时候, 已经告诉操作系统这个CLASS的所有Window都使用WndProc这个函数.
这个函数很是重要, 其实今后Windows编程, 界面部分主要就是由这个函数处理.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message)
{
case WM_COMMAND:
//….
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
LRESULT和CALLBACK分别定义函数的返回值和类型. 如果没听过函数也没有关系, 照猫画虎就行.
这个函数有4个参数:
HWND hWnd: 消息所属的Window, 要知道, 可能不同的窗体会使用相同的函数, 所以必须给予区别.
UINT message: 消息类型, 仅仅是一个数.
WPARAM wParam, LPARAM lParam: 消息类容, 通常都是通过这两个参数传过来. 前者是一个64位的数, 后者是一个指针. 不同的消息类型会以不同的方式使用这两个参数, 有些甚至不用这两个参数.
Window的消息类型有很多, WM_COMMAND, WM_DESTROY都是. 这些通常以宏的方式在各种头文件中定义, 你可以在MSDN中查到它的资料.
DefWindowProc 默认消息处理函数:
首先要意识到一点, 那就是Window的消息非常多, 你可能只关系你感兴趣的几个. 其他的Windows提供了默认的处理方案, 这就是DefWindowProc所干的事.
No comments:
Post a Comment