Category Archives: Windows

Windows XP Targeting with C++ in Visual Studio 2012

Just downloaded and tried Visual Studio 2012(with update 2, version 11.0.60315.01). The Windows XP targeting is available(actually already available in update 1):

 photo vs2012_xp_target_zps10d34c42.png

The executable generated by the original VS2012 toolchain does not run under Windows XP. A error message box is shown:

 photo vs2012_xp_target_2_zpsc799786a.png

In update 1, the static and dynamic link libraries for the CRT, STL and MFC have been updated in-place to add runtime support for Windows XP and Windows Server 2003. And the runtime version is upgraded to 11.0.51106.1 from 11.0.50727.1.

Except the library update, there’s none real difference when selecting “v110” or “v110_xp” toolchain. I wrote a simple HelloWorld application and compare the two generated binary.

And the output:

The first difference represents the timestamps of the two binary. The other two differences standard for “Operating System Version” and “Subsystem Version”. We have 5.1 for Windows XP, 6.0 for Windows Vista and later. That’s all. And we can easily build a Windows XP binary from the command line with only one additional linker switch:

I also built a simple MFC application(dynamic link to MFC) with Windows XP target in VS2012. It runs fine under Windows XP with MFC DLLs copied in the same directory. From VS2010, the SxS assembly is not used any more. All you need to do is copy the dependent DLLs to the application directory and run.

Reference: http://blogs.msdn.com/b/vcblog/archive/2012/10/08/10357555.aspx

Apache Modules 学习笔记(1)

最近看了这本<<The Apache Modules Book>>: http://www.amazon.com/gp/product/B000SEGRM8/, 记录一下.

每次我们学一个新的东西的时候, 似乎都会写一个程序叫做”hello world”, 今天的目的也在于此. 看这本书的目的主要是为了了解Apache 的扩展性到底是如何做到的. Apache 主要提供了hook, filter, provider 等机制. 其次, 就是Apache 的跨平台和平台相关的优化. 本人对这些东西的了解还比较粗浅, 本书感觉也只是在大量的贴Apache 的源码, 所以还是要看Apache 的manual. 最后, Apache 的源码确实写的非常有参考价值, 很多东西我都不知道原来能那么用的.

好了, 进正题, 我们要写的是一个”hello world” 的generator module. 运行的结果如图:

apache_1_1

然后是代码, 有点长:

这个module 的用途是打印接受到的request 的header 信息. 需要知道的有两部分: module 的声明, module 的hook 函数. Apache 模块的都是通过”module” 这个struct 来声明导出的, 在这个struct 中会初始化这个模块的各个函数指针. 在我们的代码中, 中间5 个值都是NULL, 它们是用来安装配置文件相关的hook 的, 暂时不用. 最后一个hook 则指向一个相当于运行时的hook 函数, 在这个函数, 即”helloworld_hooks” 中, 我们指定Apache 的那些处理过程会被我们hook 到. 这里我们使用了ap_hook_handler 这个函数, 它表明我们的模块是一个generator handler. 它的参数helloworld_handler 依然是一个函数指针, 表示具体的处理过程. 其它的代码都是html 的生成, 先随便看看吧.

接下来是编译的问题. 如果用VC 的话, 那么就是简单的把apache, apr, apr-util 的include 和lib 的路径加进去, 基本就通过编译了. 不过有的module 可能会依赖其它module, 个么这个也自己加. 我写了一个简单的Makefile 来编译, 如下:

vs2005, vs2008 皆可通过编译. vs 的-I 选项似乎不支持绝对路径, 所以编译之前请修改$(APACHE) 变量. 另外, 发现一个问题就是, debug 编译的Apache 不能加载release 编译的module. 后来发现是vs2005/2008 的CRT dll的SxS 的问题. 所以, Apache 和module 的编译器最好是同一版本和配置, 这样CRT 才能被正确加载进来. 或者就是静态链接到CRT.

把编译出来的*.so 文件拷贝到Apache 的modules 文件夹下. 最后来修改配置文件. 打开httpd.conf, 添加如下代码:

LoadModule 指令用来加载模块, 第一个参数是在代码中导出(export) 的模块名, 第二个参数是模块的路径. 然后来设置映射关系, 凡是URL 是/helloworld 开头的, 都用helloworld 这个handle 来处理, 而helloworld 这个handle, 实际上只是我们在代码中字符串比较用的, 参见helloworld_handler 这个函数.

以上.

Building Apache Web Server with Visual Studio 2005

1. Source

a) apache 2.2.13: http://www.apache.org/dist/httpd/httpd-2.2.13-win32-src.zip
b) zlib 1.2.3 (for mod_deflate): http://www.zlib.net/zlib-1.2.3.tar.gz
c) openssl 0.9.8k (for mod_ssl): http://www.openssl.org/source/openssl-0.9.8k.tar.gz

2. Tools

a) ActivePerl: http://aspn.activestate.com/ASPN/Downloads/ActivePerl/
b) awk & patch tools: http://gnuwin32.sourceforge.net/packages.html

3. Steps

a) Setup Perl environment, add %Perl%/bin to %PATH%.
b) Also add awk, path tools to %PATH%.
c) Decompress the apache source code to %Apache%, D:Apache maybe.
d) Decompress the zlib into srclib subdirectory named zlib.
e) Decompress the openssl into srclib subdirectory named openssl.
f) Now the source tree should look like:

g) Patch zlib:
Download the patch from: http://www.apache.org/dist/httpd/binaries/win32/patches_applied/zlib-1.2.3-vc32-2005-rcver.patch. This patch contains minor fixes and enable generation of *.pdb files.
Copy the patch file into zlib subdirectory, swith to the directory in cmd.exe and run the command:

h) Patch openssl:
Download the patch from: http://www.apache.org/dist/httpd/binaries/win32/patches_applied/openssl-0.9.8k-vc32.patch. This patch will correct a link issue with zlib and enable generation of *.pdb files.
Copy the patch file into openssl subdirectory, swith to the directory in cmd.exe and run the command:

i) Build zlib:

j) Build openssl:

k) Patch Apache:
There’s an issue in the Makefile.win that build Apache in 2.2.13: https://issues.apache.org/bugzilla/show_bug.cgi?id=47659. Download the patch against branch into the %Apache% directory and run the command:

l) Build Apache using command line:
Now you can buid Apache by:

And install Apache by:

m) Build Apache using Visual Studio 2005:
There’s also a flaw in the *.vcproj conversion of *.dsp through Visual Studio 2005. We must run a perl script to fix it first:

Now, everything are OK. In Visual Studio 2005, open the Apache.dsw and convert all *.dsp files to *.vcproj files. Then build the project “BuildBin”. The project “InstallBin” project will distribute all the project in the Apache solution.

4. Debugging with Visual Studio 2005

It’s quite simple. After build the project “InstallBin”, open the property page of the “httpd” project. Switch to “Debugging” tab, change the Command value into your binary of installed directory. Now, add breakpoints, and press F5 to start your tracing or debugging.

5. Reference

a) Compiling Apache for Microsoft Windows
b) Apache 2.2.9 command line build with the Windows 2008 SDK

笔记 – Programming Windows (2)

今天来记录一下windows 中GDI (Graphics Device Interface) 的相关内容.

GDI 的目标是提供一种设备无关的绘图方式, 要支持不同的monitor 和graphics card.

1. WM_PAINT 消息

我们还是以以下的window procedure 代码为例:

这次关注的让然是WM_PAINT 消息的处理. 首先我们调用BeginPaint() 函数返回一个HDC 的handle. DC(Device Context) 可以理解成跟设备联系起来的, 可以在上面绘图的一个东西. 这里的设备一般指的是monitor, 但是也可以是printer. 得到这个handle 之后, 我们就可以在DC 上绘图了, DrawText() 函数就在HC 的正中间画了一个字符串. 最后就是EndPaint() 函数来说明绘图结束. 这里有2 个概念.

第一个概念叫做invalid region. 当一个被遮盖的窗口被重新显示的时候, 实际上要重画的只是被遮盖的部分. 而这些被遮盖的需要重画的部分就叫做invalid region. windows 会给窗口发WM_PAINT 消息来让窗口重画. 在代码中ps 变量(PAINTSTRUCT 结构)的rcPaint 域实际上包含了这个invalid region 的信息. 当我们调用BeginPaint() 的时候,实际上把这个invalid region 给validate 了. 不然的话, windows 检测到还有invalid region 没被重画就会不断的发送WM_PAINT 消息, CPU 会100% 的.

第二个概念叫做clipping region. 当我们调用BeginPaint() 的时候, 实际还把一个invalid region 转换成了一个clipping region. 什么意思呢? 就是之后的GDI 调用的绘图都会只限于这个clipping region 中. 如果某个GDI 调用画在了这个clipping region之外, 实际是不会有调用开销的, 这也是windows GDI 的一个优化. 虽然我觉得GDI 还是很慢的=v= (GDI+ 更慢…).

除了调用BeginPaint(), EndPaint() 来得到DC 外, 还可以通过调用GetDC() 这个API. 比如在处理一个鼠标消息, 可能需要在屏幕上画点什么的时候. 不过有这样几点需要注意: 1) BeginPaint(), EndPaint() 只能被用在WM_PAINT 消息中. 2) GetDC() 不会去把invalid region 给validate, 我们需要手动调用ValidateRect() 或ValidateRgn() 这2 个API. 完了之后, 记得调用ReleaseDC().

有的时候, 我们需要手动刷新一个window. 也有两种方法: 1) 调用InvalidateRect() 函数. 这种方法是post 一个WM_PAINT 消息到当前window 的message queue 中, 等待刷新. 一个message queue 中不会出现多个WM_PAINT 消息, windows 会自动合并. 2) 调用UpdateWindow() 函数来强制刷新window, 相当于send 一个WM_PAINT 消息来直接调用window procedure 中的处理代码.

2. GDI Objects

现在来看一下DC 的使用. 一个DC 可以有很多的属性. 有以下5 种: Bitmap, Brush, Font, Pen, Region. Bitmap 可以理解成DC 中的画布(canvas), 所有画在DC 上的元素实际都是画在Bitmap 这个属性中的. Brush 表示的是绘图的背景属性, 可以是单色, 渐变色, 或是一个图案(pattern). Font 自然是字体, 包括大小, 颜色, 粗细等. Pen 表示的是画笔的属性, 包括颜色, 粗细, 线段起始点, 拐点的一些绘画属性. Region 表示一个区域, 可以理解为Bitmap 这个canvas 上的一个clipping region.

我们通过SelectObject() 这个API 来设置这些属性. 以Brush 为例, 可以使用系统与定义的Brush:

也可以使用自定义的Brush:

其它4 种属性的调用相似. 只是最后自定义的属性, 使用完了一定要记得调用DeleteObject() 这个API 来删除, GDI 的object 也是会泄漏(leak) 的.

另外, 有一个logic object 的概念. 当我们调用CreateXXX() 函数的时候, 得到的GDI object 实际已经跟特定的DC 相关联了, 但是有时候, 我们并不要实际的GDI object, 而只是需要一个包含这些信息的一个数据结构. 于是我们就可以使用所谓的logic object. 以Font 为例. 在GDI 中CreateFont() 这个API 可以说是最麻烦的一个API 了, 光参数就有14 个. 相应的, 还有另外一个API 叫做CreateFontIndirect(), 它的参数是一个LOGFONT 结构, 其中的域对应了CreateFont() 的14 个参数, 所以可以用这两个API 完成同样的工作. 其它四种GDI object 也有类似的logic object.

3. GDI 坐标映射

到目前为止, 我们并没有设置过GDI 中的任何坐标. 所以我们到底是在用什么单位(unit) 来绘图的呢? 在GDI 中, 默认的坐标映射模式是MM_TEXT. 需要指出的是, 我们在GDI 函数中传入的数字都是逻辑坐标(logic coordinates), 而GDI 会根据当前的映射模式来重新加算设备相关的视点坐标(viewport coordinates).

其它的映射模式包括: MM_LOMETRIC, MM_HIMETRIC, MM_LOENGLISH, MM_HIENGLISH, MM_TWIPS, MM_ISOTROPIC, MM_ANISOTROPIC. 前5 个映射模式只是逻辑坐标的不同, 而x 轴和y 轴坐标都是等比例映射缩放的, 后两种映射模式允许非等比例的坐标映射. 具体的映射规则请查阅MSDN, 因为非常非常的麻烦, 丸子只给出一个映射的计算公式, 其中(xWinOrg, yWinOrg) 是逻辑坐标的原点, (xViewOrg, yViewOrg) 是视点坐标的原点:

  • xViewport = (xWindow – xWinOrg) * xViewExt/xWinExt + xViewOrg
  • yViewport = (yWindow – yWinOrg) * yViewExt/yWinExt + yViewOrg

这些数值当然不需要手动计算, GDI 主要提供了这样5 个函数来进行坐标映射的操作: SetMapMode(), SetWindowOrgEx(), SettWindowExtEx(), SetViewportOrgEx(), SetViewportExtEx().

根据丸子的经验, 为了计算坐标更容易, 一般只需要调用SetViewportOrgEx() 来设置视点坐标就可以了, 其它API 函数基本可以不用的.

4. DIB和DDB

DIB(Device-Independent Bitmap) 设备独立的位图, DDB(Device-Dependent Bitmap) 设备相关的位图.

什么意思呢? 简单来说, DIB 就是存在硬盘上的bitmap 文件, DDB 就是要在一个DC 上画bitmap 的时候, 一个HBITMAP 的handle 在windows 内存中表示的bitmap. 这里有个很搞笑的事情就是: windows 的GDI 函数中, 并没有提供从文件中读取DIB 的API 函数(Gdiplus 中有), 于是我们要手动写把DIB 转换成DDB 的函数, 这就需要我们了解DIB 的文件结构.

bitmap, 位图, 就是把一张图的所有颜色信息按照pixel 来存储, 所以我们可以把一个DIB 读到内存中, 并把这些按pixel 存储的信息放在一个数组中. 注意, 这里不仅仅是bmp 图片, jpg, png 等其它图片格式, 如果要画到GDI 的DC 上, 都要经过这写操作步骤. 这些步骤搞定之后, 我们可以调用SetDIBitsToDevice() 函数来把这些数组中的信息画到一个DC 上去, 注意倒数第二个参数:

调用SetDIBitsToDevice() 只是画DIB 的一种方法, 第二种是把一个DIB 转换成一个DDB, 然后就可以调用windows 的Bitblt() 函数来更高效的绘制bitmap 了. 之所以说更高效, 是因为SetDIBitsToDevice() 是一个一个pixel 画的, 而DDB 是已经包含跟DC 相关的bitmap 信息的了. 可以调用CreateDIBitmap() 函数来生产一个DDB, 嗯.. 你没看错, 名字和用处居然不一致. 然而这种方法的缺点是, 不能按pixel 来访问bitmap 的信息了.

于是, 我们有第三种方法, 调用CreateDIBSection() 函数. 这个函数既能转换DIB 到DDB, 又能提供按pixel 访问bitmap 信息的方法. 缺点是, 很难用, 这个API 函数返回的虽然是一个HBITMAP 的handle, 但是跟CreateDIBitmap() 返回的handle 不一样, 有诸多需要特别注意的地方. 所以丸子也不推荐使用= =.

总结一下就是, 对于一张不大的bitmap, 且要精心按pixel 操作的话, 使用SetDIBitsToDevice(). 对于比较大的bitmap, 且要被画多次, 那么使用CreateDIBitmap(), 因为DDB 的绘制会比较快速. 当然, CreateDIBSection() 提供了前两者的好处, 问题就是比较难用.

以上.

笔记 – Programming Windows (1)

我终于读完了Charles Petzold 的这本圣书: http://www.amazon.com/Programming-Windows%C2%AE-Fifth-Microsoft/dp/157231995X

丸子觉得这本书虽然很多东西已经有些过时了, 比如palette, mci 之类的, 但是有2 个方面讲的非常的深入: 一个是windows 的message 机制, 还有一个是GDI 的绘图部分. 今天闲来讲一下windows 的message 机制.

1. 入口函数

先看一段最最最入门的代码, 这边写的代码都是纯C 代码:

跟console 的程序不同, windows 的窗口程序都是以WinMain 函数作为入口的. 不过这只是一个编译选项而已, 可以指定/entry 参数重设, 当然我们一般都用默认的. 第一个参数hInstance 表示的是这个程序实例的句柄(handle), 我们可以用它来load 内嵌的resource, 第二个参数在win98 以后的版本都不会用到不讲. 第三个参数是传入的命令行参数. 第四个也是运行参数, 控制的是窗口初始化时的现实模式, 最大化, 最小化还是其它什么, 这个参数可以在windows shortcut 的property 里设置, 也可以在CreateProcess() 和ShellExecute() 等API 函数里指定.

2. Unicode 编码

接下来是一个Unicode 的问题, 上面的代码用到的其实是ANSI 编码的API, 从windows nt 开始, Unicode 已经是windows 的内建编码了, 所以Unicode 版本的程序在windows nt 之后的版本会运行的更快一些. 然而windows nt 之前的os 就不一定能运行了. 还是看代码:

区别有这样几个: WinMain–>wWinMain, PSTR–>PWSTR, MessageBoxA–>MessageBoxW, “Hello”–>L”Hello”. 其实就是把函数改成Unicode 版本, 字符串改成宽字符. 我们也可以写一个ANSI 和Unicode 通用的版本:

_tWinMain 实际上是一个宏定义(macro)(注意下划线), 它会根据编译器是否定义了UNICODE 这个宏来分别预处理(preprocess) 成WinMain 和wWinMain, 同样的情况适用于PTSTR, TEXT, MessageBox 宏. 嗯.. 你没看错, MessageBox 实际是一个宏, 具体参阅windows sdk 的头文件.

3. Windows 消息机制

接下来看一个稍微复杂一些的代码:

好长好长的代码呀.. 运行的效果是, 现实一个窗口, 中间有一行字, 启动时还有音效.

我们要显示一个window(指的是程序窗口), 大概要做两件事, 第一是注册这个window 的class, 第二是用这个class 来create 这个window. 分别对应的API 就是RegisterClass() 和CreateWindow().

调用RegisterClass() 的时候, 我们指定这个class 的名字, 样式(style), 实例句柄(instance handle), 以及最最重要的window procedure 的回调函数(callback function)地址, 我们将通过这个回调函数来处理对于属于这个window class 所创建出的window 的消息处理.

RegisterClass() 并不创建实际的window, 而只是创建了一个window 的类别, 比如button 就是一个windows 内建的window class. 我们调用CreateWindow() 来创建一个具体的可现实的window. 可以看到, 这个API 的参数里有一个叫做window style, 这个东西很容易跟之前的class style 搞混. 怎么区分呢? 记住class style 指定的是所有这个class 的window 都会有的style, 比如一个button, 它总是可以click 的, 这就是一个class style. 而window style 则是在class style 的基础上各个window 定制的style, 比如不同的button 可以有不同的观感(look and feel), 这就是要给window style).

之后, 终于讲到windows 的message 机制了. 每个windows 的程序都会有一个message queue(消息队列), 这个queue 保存了从windows 系统或是其它程序发给这个程序的各种message. windows 可以通过这种方法来实现各个应用程序之间的交互. 有了这个message queue 之后, windows 又定义了叫做message loop(消息循环) 的机制来从这个queue 里拿出消息并进行处理. GetMessage() 这个API 就是用来实现这个功能的, 而代码中GetMessage() 之外的while 循环就叫做message loop.

注意, 这边GetMessage() 拿到的message 是针对整个程序来说的, 并不针对某个特别的窗口. 当GetMessage() 收到的是WM_QUIT 消息的时候返回0, 发生错误的时候返回-1, 其它情况下返回其它非0 值. 所以在代码中我们根据它的返回值是否为0 来判断要不要结束message loop. 在GetMessage() 的输出参数中, 会返回得到的message 数据, 即一个MSG 结构. 在message loop 中, 又有2 个API 调用.

TranslateMessage() 的作用是用来转换键盘消息. 当我们按下一个键的时候, 会产生WM_KEYDOWN 或WM_SYSKEYDOWN 消息, 放开按键的时候则会产生WM_KEYUP 或WM_SYSKEYUP 消息. WM_SYS 开头的消息是系统消息, 一般是Alt 跟其它键的组合键, 比如Alt+Tab, 这个没什么值得讨论的. 在WM_KEYDOWN 的时候, 我们有时要根据shift 是否也被按了来进行大小写字母的判断, 但这不是很好的做法. 比如, caps lock 的状态怎么判断? 如果输入的是中文呢(中文没有对应的virtual key code)? 所以TranslateMessage() 这个API 会帮我们做把所谓keystroke message 转换成character message, 即如果接受到WM_KEYDOWN 消息且shift 被按下又能够组合成一个字符(character), 那么另外一个WM_CHAR 消息会被插入到当前的message queue 中, 紧接在WM_KEYDOWN 消息之后. windows 在处理键盘消息的时候, 其实有一个system message queue. 为什么要有这个东西, 而不把每次的按键直接加到当前窗口的message queue 里呢? 因为无法判断下一个键盘消息到底要发给哪个窗口. 比如要是按了Alt+Tab 怎么办呢?

然后是DispatchMessage() 这个API. 它的作用是把得到的message 传回给windows 系统, 让windows 根据MSG 结构里的信息, 把这个消息dispatch 到相应的window procedure(关于window procedure 之后会讲). 这里其实很神奇, 在我们自己的程序里, 居然要手动dispatch 系统的消息.

接下来来看window procedure 的回调函数WndProc. 这个函数提供了对于各种发送给窗口的对于处理方法. 这个回调函数有4 个参数: 第一个参数是一个HWND 的handle, 由于多个的window 可以指定同一个window procedure, 可以用这个handle 来区分. 第二个参数是收到的message 的类型. 第三第四个参数是对于当前收到message 的参数, 比如对于WM_KEYDOWN 消息, wParam 就是key 的virtual code, lParam 则是其它一切附加信息. 代码中我们处理了3 个消息: WM_CREATE 是window 在被创建时收到的消息, 我们播放了一段音频. WM_PAINT 是window 要被重画时收到的消息, 我们简单画了一个字符串(关于GDI 相关的内容, 下次笔记再讲). WM_DESTROY 消息则是window 被销毁的时候收到的消息, 我们让整个程序退出. 对于其它我们不感兴趣的消息, 我们简单的把它们扔给DefWindowProc() 来做默认处理.

现在应该对windows 的消息机制有一定了解了, 让我们来看看以上程序中, 当我们按了Alt+F4 的系统热键发生了些什么吧. 我们的WinProc 首先会有到一个WM_SYSCOMMAND 消息, 并传给DefWindowProc() 处理. DefWindowProc() 的默认处理方法就是发一个WM_CLOSE 消息给这个window. 又一次, 我们的WinProc 吧这个消息传给DefWindowProc(). DefWindowProc() 的默认处理方法是调用DestoryWindow(), 而DestroyWindow() 会给这个window 发一个WM_DESTROY 消息. 这次终于有自定义处理代码了, PostQuitMessage() 简单的发送一个WM_QUIT 消息给当前的程序. WM_QUIT 消息导致message loop 的退出(GetMessage() 返回0), 于是整个程序结束. PostQuitMessage() 的参数表示exit code, 会被存在MSG 结构的wParam 域里.

以上说了message loop 中的消息处理. 这里消息叫做queued message, 但是还有一类叫做nonqueued message, 即它们是不会进入message loop, 而是直接通过上面的window procedure 函数调用的. 有两个函数: SendMessage() 和PostMessage(), 它们分别对应queued 和nonqueued. 调用SendMessage() 产生一个nonqueued message, 结果就是直接调用window procedure 函数, 且函数返回了, SendMessage() 才会返回, 它是同步的. 调用PostMessage() 产生一个queued message, 并把这个message 加到message queue 中, 之后通过程序的GetMessage() 来polling 处理, PostMessage() 是直接返回的, 不需要等到message 被处理完, 它是异步的. 一般, 通过windows API 间接发送的消息都是nonqueued message, 比如代码中的UpdateWindow() 会把WM_PAINT 作为参数直接调用window procedure 回调函数来强制刷新.

另外要提到作为timer 使用的WM_TIMER 消息. 这是一个queued message, 虽然用来计时, 但却不是那么的准确. 如果WM_TIMER 的前一次消息处理时间太长的话, 会把多个WM_TIMER 合并在一起. 因此, 需要更加准确的计时的话, 我们需要调用Thread 的相关API 函数.

4. 子控件相关

稍微加少下子控件的概念, 所谓的子控件, 就是window上的控件, 真废话=v=. 这里要说的其实没多少东西, 只是一笔带过.

当子控件被点击的时候, 子控件就会给父控件发送一个WM_COMMAND 消息. 当一个push button 被点击, radio button 被选中, check button 被选中都是这个消息. 注意, 是从子控件发送给父控件, 也就是说WM_COMMAND 消息的处理是在父控件的window procedure 回调函数中.

当我们用CreateWindow() 创建window 的时候, 上面的子控件是不具备Tab 遍历功能的, 我们添加对于Tab 键的处理. 但是如果它是通过CreateDialog() 或DialogBox() API 函数创建的话, 只要子控件指定了WS_TABSTOP 的window style 的flag, 就会自动添加Tab 键的遍历功能.

当我们要拿子控件的属性的时候, 我们其实也是通过windows 的message 来实现的. MFC 类库中把这些message 都封装成了宏(macro) 来方便使用. 比如我们要那一个check button 的选中状态, 则可以发送一个BM_GETCHECK 消息.

关于子控件的自定义有3 种方法或者说概念, 从最简单的说起:

a) Owner Draw:
这种方法一般用来定制子控件的外观. 举个例子, 如果要owner-draw 一个push button 的外观, 那么这个button 的style 里要有BS_OWNERDRAW 这个flag. window procedure 回调函数遇到owner-draw 的控件时, 会收到一个WM_DRAWITEM 的消息, 你要做的就是在这个消息处理中把button 画出来.

b) Sub-classing:
这种方法一般用来定制子控件的行为. 还是以push button 为例, button 的点击的默认行为是发送一个WM_COMMAND 消息给父窗口, 那么我们怎么修改这个默认行为呢? 由于button 这类系统与定义的控件的window procedure 都是在windows 系统中写死的, 我们只能想办法替换这个window procedure 回调函数. 具体的方法是调用SetWindowLong() 这个API, 并传入DWL_DLGPROC 这个flag.

c) Custom Control:
不用说了, 最全面的方法. 如果你要完全定义一个全新的子控件. 你需要完全自己coding 所有的代码, 调用RegisterClass(), CreateWindow(), 实现window procedure 回调函数. 很大的工程那…

5. Dialog 对话框

Dialog 可以分成2 类, modal 和modeless(注意拼写=v=). 具体概念我就不解释了.

一个modal 的dialog 调用DialogBox() 函数创建, 一个modeless 的dialog 调用CreateDialog() 创建. 跟CreateWindow() 的调用一样, 我们也要把一个所谓的dialog procedure 当作参数传进去. 一下是这个procedure 的signature:

对比window procedure:

dialog precedure 虽然返回值是一个INT_PTR类型, 但通常情况下返回的是TRUE 或这FALSE, 特殊值的返回请参阅MSDN. 除了返回值意外, 这两个回调函数似乎没什么不一样, 但实际上并不那么简单. 一个dialog 是一个window, 而它对应的window procedure 是包含在windows 系统中的, 在那个window procedure 中, 会调用到我们的dialog procedure.

dialog 不会收到WM_CREATE 消息, 但是会收到对应的WM_INITDIALOG 消息, 想来是windows 系统在WM_CREATE 消息中重新发的消息吧. 如此一来便可以完成一些dialog 特有的初始化操作. 当我们要用代码销毁一个dialog 的时候, modal 和modeless 这2 中dialog 的方法不同. modal dialog 调用EndDialog(), 而modeless dialog 调用DestroyWindow().

最后, 书上有这样一句话: Unlike messages to modal dialog boxes and message boxes, messages to modeless dialog boxes come through your program’s message queue. The message queue must be altered to pass these messages to the dialog box window procedure. 然后说我们要调用IsDialogMessage() 这个API 来传递message. 经过实践, 上面那句话的意思其实是说: modeless dialog 跟window 一样, 默认不支持Tab 键的遍历等键盘消息处理, 但是调用了这个IsDialogMessage() 之后, 所有标记过WS_TABSTOP 的子控件就都支持了. MSDN 上还说, 传入的window 句柄不一定要是dialog, window 也可以. 通过这种方法, 我们通过CreateWindow() 出来的window 也毫不费力的实现Tab 的遍历了. 于是最终, 我们的message loop 的代码大概是这样的(假设hDlgModeless 是一个modeless 的dialog 的handle):

如果我们有多个modeless 的dialog, 这种写法岂不是要死人? M$ 的knowledge base 上有篇文章可以参考: How To Use One IsDialogMessage() Call for Many Modeless Dialogs

以上. 请继续期待下次GDI 的笔记.