Win32汇编--图形操作--GDI原理
Windows是基于图形界面的,所以在Win32编程中,图形操作是最常用的操作。GDI的意义在于将程序对图形界面的操作和硬件设备隔绝开来,在程序中可以将所有的图形设备都看成是虚拟设备,包括视频显示器和打印机等,然后通过GDI函数用同样的方法去操作它们,由Windows负责将函数调用转化成针对具体硬件的操作。只要一个设备提供了和Windows兼容的驱动程序,它就可以被看做是一个标准的设备。以前在DOS系统下写应用程序的时候,如果要进行图形操作,那么就要考虑到市场上每种显示卡的不同,否则在装配某种显卡的计算机上就可能无法正常运行,对汇编程序员来说,这真是一个恶梦。在Win32编程中,正是GDI函数让这个恶梦成为历史。
GDI函数全部包括在GDI32.DLL中,在编程的时候,注意要在源程序的开头加上相应的包含语句:
include gdi32.inc
includelib gdi32.lib
和GDI相关的内容真是太庞大了,只要查看一下gdi32.inc文件就可以发现,函数的总数达到了300多个,和GDI相关的数据结构也非常多,为了能了解GDI的原理和基本的使用方法:
归纳起来,GDI操作可以从3个方面去了解——When, Where和How:
When——指的是进行图形操作的时机,究竟什么时刻最适合程序进行图形操作呢?——“GDI程序的结构”
Where——指的是图形该往哪里画,既然Windows隔离了硬件图形设备,那么该把什么地方当做“下笔”的地方呢?——“设备环境”
How——了解了上面两个问题后,最后还要知道“如何画”,这就涉及如何使用大部分GDI函数的问题了。
一、GDI程序的结构
1、客户区的刷新
正如上面所说的,这里讨论的是“When”的问题,读者可能会问:为什么会有这个问题,如果要向窗口输出图形,程序想在什么时候输出那就是什么时候,难道这个时刻还有规定不成?
在DOS操作系统中编程的时候,程序把文字或图形输出到屏幕,在输出新的内容之前,这些内容总是保留在屏幕原处,这些内容会被意外覆盖的唯一情况是激活一个TSR程序,但TSR程序在退出之前有义务恢复原来的屏幕,如果它无法恢复屏幕的内容,那么这是它的责任,我们不会在自己的程序中去考虑屏幕内容会无缘无故消失这种情况,所以可以把屏幕看成是应用程序私有的。
如果程序输出的内容过多,如用dir显示一个含有很多文件的目录,用户根本无法看清快速上翻的屏幕,这时程序可以设计一个参数来暂停一下,如dir/p。这已经是DOS程序最“体贴”的做法了,如果用户想回过头去看已经滚出屏幕的内容,那可对不起,只能再执行一遍了!
所以对DOS程序来说,程序想在什么时候输出信息那就是什么时候,根本不存在When这个问题。
但在Windows操作系统中,屏幕是多个程序“公用”的,用户程序不要指望输出到窗口中的内容经过一段时间后还会保留在那里,它们可能被别的东西覆盖,如其他窗口、鼠标箭头或下拉的菜单等。在Windows中,恢复被覆盖内容的责任大部分属于用户程序自己,理由很简单:Windows是个多任务的操作系统,假如程序B覆盖了程序A的窗口内容,覆盖掉的内容由程序B负责恢复的话,它就必须保存它覆盖掉的内容,但是在它将保存的内容恢复之前,程序A也在运行,并可能在程序B恢复以前已经向它自己的窗口输出新的内容,结果当程序B恢复它保存的窗口内容时,保存的内容可能是过时的(而DOS的情况就不同,TSR程序激活的时候,用户程序是被挂起的),所以最好的办法就是让程序A自己来决定如何恢复。
Windows系统采用的方法是:当Windows检测到窗口被覆盖的地方需要恢复的时候,它会向用户程序发送一个WM_PAINT消息,消息中包括了需要恢复的区域,然后由用户程序来决定如何恢复被覆盖的内容。
如果程序因为忙于处理其他事务以至于无法及时响应WM_PAINT消息,那么窗口客户区原先被覆盖的地方可能会被Windows暂时画成一块白色(或者背景色)的矩形,或者根本就是保留被覆盖时的情形,直到程序有时间去响应WM_PAINT消息为止。我们常常可以看到这种情况发生在死锁程序的客户区内,这就是因为死锁的程序无法响应WM_PAINT消息来恢复客户区造成的。
所以对于“When”这个问题,答案是:程序应该在Windows要求的时候绘画客户区,也就是在收到WM_PAINT消息的时候。如果程序需要主动刷新客户区,那么可以通过调用InvalidateRect等函数引发一条WM_PAINT消息,因为在WM_PAINT消息中刷新客户区的代码是必须存在的,所以用这种看似“舍近求远”的办法实际上可以节省一份重复的代码。即使是在游戏程序这种“主动刷新”远远多于“被动刷新”的程序中,只要窗口有被其他东西覆盖的可能,那么这个原则就是适用的。
2、GDI程序的结构
对于Win32程序来说,WM_PAINT消息随时可能发生,这就意味着,程序再也不能像在DOS下一样输出结果后就不管了,反过来,程序在任何时刻都应该知道如何恢复整个或局部客户区中以前输出的内容。
如果程序的功能比较简单,可以将计算及刷新整个客户区的代码全部安排在WM_PAINT消息中完成,这样,每次当客户区的全部或部分需要被更新的时候,程序重新执行整个生成客户区屏幕数据的功能模块并刷新客户区。这种结构适用于功能模块很短小且执行速度很快的情况,整个过程的时间最好不超过几百ms,否则,用户会在一个明显的等待时间后才看到程序把客户区中的“空洞”补上。
当生成屏幕数据的功能模块有些复杂的时候,就应该考虑采用如下结构,即功能模块和客户区刷新模块分别在不同的子程序中实现,功能模块单独用一个子程序完成,这个子程序可以由用户通过选择菜单项在WM_COMMAND消息中执行,也可以新建另外一个线和来完成,总之,它最后把计算结果放到一个缓冲区中,而每当客户区需要刷新时,程序在WM_PAINT消息中调用客户区刷新子程序,这个子程序从计算好的缓冲区中取出数据并输出到客户区中,由于单纯的屏幕刷新过程是很快的,所以用户根本来不及看到客户区中的空洞。
3、探讨WM_PAINT消息
当客户区被覆盖并重新显示的时候,Windows并不是在所有的的下都发送WM_PAINT消息,下面是几种不同的情况:
l 当鼠标光标移过窗口客户区以及图标拖过客户区这两种情况,Windows总是自己保存被覆盖的区域并恢复它,并不需要发送WM_PAINT消息通知用户程序。
l 当窗口客户区被自己的下拉式菜单覆盖,或者被自己弹出的对话框覆盖后,Windows会尝试保存被覆盖的区域并在以后恢复它,如果因为某种原因无法保存并恢复的话,Windows会发送一个WM_PAINT消息通知程序。
l 别的情况造成窗口的一部分从不可见变到可见,如程序从最小化的状态恢复,其他的窗口覆盖客户区后移开,用户改变了窗口的大小不一和用户按动滚动条等,在这些情况下,Windows会向窗口发送WM_PAINT消息。
l 一些函数会引发WM_PAINT消息,如UpdateWindow,InvalidateRect以及InvalidateRgn函数等。
窗口过程收到WM_PAINT消息后,并不代表整个客户区都需要被刷新,有可能客户区被覆盖的区域只有一小块,这个区域就叫做“无效区域”,程序只需要更新这个区域。
和WM_TIMER消息类似,WM_PAINT消息也是一个低级别的消息,虽然它不会像WM_TIMER消息一样被丢弃,但Windows总是在消息循环空的时候才把WM_PAINT放入其中,实际上,Windows为每个窗口维护一个“绘图信息结构”,无效区域的坐标就在其中,每当消息循环空的时候,如果Windows发现存在一个无效区域,就会放入一个WM_PAINT消息。
无效区域的坐标并不附带在WM_PAINT消息的参数中,在程序中有其他方法可以获取,WM_PAINT消息只是通知程序有个区域需要更新而已,所以Windows也不会同时将两条WM_PAINT消息放入消息循环,当Windows要放入一条WM_PAINT消息的时候,如果发现已存在一个无效区域了,那么它只需要把新旧两个无效区域合并计算出一个新的无效区域就可以了,消息循环中还是只需要一条WM_PAINT消息。
由于存在“无效区域”这样一个东西,所以程序在WM_PAINT消息中对客户区刷新完毕后工作并没有结束,如果不使无效区域变得有效,Windows会在下一轮消息循环中继续放入一个WM_PAINT消息。当Windows检查“绘图信息结构”的时候发现没有了无效区域,也就不会继续发送WM_PAINT消息了。
WM_PAINT消息的处理流程一般是:
.if eax == WM_PAINT ;eax为uMsg
invoke BeginPaint, hWnd, addr stPS
;刷新客户区的代码
invoke EndPaint, hWnd, addr stPS
xor eax, eax
ret
读者可以发现中间并没有调用ValidateRect来使无效区域变得有效,这是因为BeginPaint函数和EndPaint函数隐含有这个功能,如果不是以BeginPaint/EndPaint当做消息处理代码的头尾的话,那么在WM_PAINT消息返回的时候就必须调用ValidateRect函数。
BeginPaint函数的第二个参数是一个绘图信息结构的缓冲区地址,Windows会在这里返回绘图信息结构,结构中包含了无效区域的位置和大小,绘图信息结构的定义如下:
PAINTSTRUCT STRUCT
hdc DWORD ?
fErase DWORD ?
rcPaint RECT <>
fRestore DWORD ?
fIncUpdate DWORD ?
rgbReserved BYTE 32 dup(?)
PAINTSTRUCT ENDS
其中hdc字段是窗口的设备环境句柄,rcPaint字段是一个RECT结构,它指定了无效区域矩形的对角顶点,fErase字段如果为非零值,表示Windows在发送WM_PAINT消息前已经用背景色擦除了无效区域,后面3个字段是Windows内部使用的,应用程序不必去理会它们。
二、设备环境
解决了“When”的总是后,再考虑一下“Where”的问题。
在DOS操作系统中,向屏幕输出数据实际上是把输出内容拷贝到视频缓冲区中,如果在文本模式下显示信息,只需要把内容拷贝到B8000h处的内存中;显示图形信息,可以把图形数据拷贝到A0000h处的内存中。
在Windows中,GDI接口把程序和硬件分隔出来,在Win32编程中,再也不能通过直接向视频缓冲区拷贝数据的办法来显示信息了,那么,究竟该往哪里输出图形呢——这就是“Where”的问题。答案是:通过“设备环境”来输出图形。
1、什么是设备环境
在Windows中,所有与图形相关的操作都是用统一的方法来完成的(不然就不能称为“图形设备接口”了)。不管是绘画屏幕上的一个窗口,还是把图形输出到打印机,或者对一幅位图进行绘画,使用的绘图函数都是相同的,为了实现方法上的统一,必须将所有的图形对象看成是一个虚拟的设备,这些设备可能有不同的属性,如黑白打印机和彩色屏幕的颜色深度是不同的,不同打印机的尺寸和分辨率可能是不同的,绘图仪只支持矢量而不支持位图等。不同设备的不同属性就构成了一个绘图的“环境”,就像DOS操作系统中把视频缓冲区当做图形操作的对象一样,这个绘图的“环境”就是Win32编程中图形操作的对象,把它叫做“设备环境”。设备环境实际上是一个数据结构,结构中保存的就是设备的属性,当对设备环境进行图形操作的时候,Windows可以根据这些属性找到对应的设备进行相关的操作。
在实际使用中,通过“设备环境”可以操作的对象很广泛,除了可以是打印机或绘图仪等硬件设备外,也可以是窗口的客户区,包括大大小小的所有可以被称为窗口的按钮与控件等的客户区,也可以是一个位图。总之,任何需要用到图形操作的东西都可以通过“设备环境”进行绘图。
为了更好地理解“设备环境”是什么,先来看一个例子:
//DcCopy.asm
.386
.model flat, stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ID_TIMER equ 1
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hInstance dd ?
hWin1 dd ?
hWin2 dd ?
.const
szClass1 db 'SourceWindow',0
szClass2 db 'DestWindow',0
szCaption1 db '请尝试用别的窗口覆盖本窗口!',0
szCaption2 db '本窗口图像拷贝自另一窗口',0
szText db 'Win32 Assembly, Simple and powerful!',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 定时器过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcTimer proc _hWnd, uMsg, _idEvent, _dwTime
local @hDc1, @hDc2
local @stRect:RECT
invoke GetDC, hWin1
mov @hDc1, eax
invoke GetDC, hWin2
mov @hDc2,eax
invoke GetClientRect, hWin1, addr @stRect
invoke BitBlt, @hDc2, 0, 0, @stRect.right, @stRect.bottom, @hDc1, 0, 0, SRCCOPY
invoke ReleaseDC, hWin1, @hDc1
invoke ReleaseDC, hWin2, @hDc2
ret
_ProcTimer endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 窗口过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hDc
mov eax, uMsg
mov ecx, hWnd
;****************************************************************
.if eax == WM_PAINT && ecx == hWin1
invoke BeginPaint, hWnd, addr @stPs
mov @hDc, eax
invoke GetClientRect, hWnd, addr @stRect
invoke DrawText, @hDc, addr szText, -1, addr @stRect, DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint, hWnd, addr @stPs
;****************************************************************
.elseif eax == WM_CLOSE
invoke PostQuitMessage, NULL
invoke DestroyWindow, hWin1
invoke DestroyWindow, hWin2
;****************************************************************
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
;****************************************************************
xor eax, eax
ret
_ProcWinMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
local @hTimer
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass
;****************************************************************
invoke LoadCursor, 0, IDC_ARROW
mov @stWndClass.hCursor, eax
push hInstance
pop @stWndClass.hInstance
mov @stWndClass.cbSize, sizeof WNDCLASSEX
mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc, offset _ProcWinMain
mov @stWndClass.hbrBackground, COLOR_WINDOW + 1
mov @stWndClass.lpszClassName, offset szClass1
invoke RegisterClassEx, addr @stWndClass
invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClass1, offset szCaption1, WS_OVERLAPPEDWINDOW, 450, 100, 300, 300, NULL, NULL, hInstance, NULL
mov hWin1, eax
invoke ShowWindow, hWin1, SW_SHOWNORMAL
invoke UpdateWindow, hWin1
;****************************************************************
mov @stWndClass.lpszClassName, offset szClass2
invoke RegisterClassEx, addr @stWndClass
invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClass2, offset szCaption2, WS_OVERLAPPEDWINDOW, 100, 100, 300, 300, NULL, NULL, hInstance, NULL
mov hWin2, eax
invoke ShowWindow, hWin2, SW_SHOWNORMAL
invoke UpdateWindow, hWin2
;****************************************************************
; 设置定时器
;****************************************************************
invoke SetTimer, NULL, NULL, 100, addr _ProcTimer
mov @hTimer, eax
;****************************************************************
; 消息循环
;****************************************************************
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
;****************************************************************
; 清除定时器
;****************************************************************
invoke KillTimer, NULL, @hTimer
ret
_WinMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
call _WinMain
invoke ExitProcess, NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
这个程序的代码用到的大部分知识都是前面已经讲到的,在_WinMain中,用一个同样的窗口类建立了两个窗口,两个窗口属于同一个窗口类,所以它们的窗口过程都是_ProcWinMain,为了关闭任何一个窗口都可以结束程序,WM_CLOSE消息中用DestroyWindow函数摧毁了两个窗口。程序设置了一个周期为100ms的定时器,Windows会每隔100ms调用_ProcTimer子程序。在_ProcTimer中,将其中一个窗口的客户区拷贝到另一个窗口的客户区中,方法是通过GetDC获取窗口的DC句柄,并用BitBlt函数完成拷贝工作,所以在右边的窗口显示了一句“Win32 Assembly, Simple and powerful!”,左边的窗口中也会出现这句话。
程序每100ms将右边窗口的客户区拷贝到左边的窗口客户区中,通过左边窗口的客户区就可以了解右边客户区的DC对应的究竟是什么内容。
通过左边窗口的变化可以惊奇地发现:右边窗口客户区的内容并不是程序自己输出到客户区的那句文本,而是以客户区为矩形区域的屏幕上我们真正看到的东西,它竟然包括其他窗口覆盖在上面的东西。这就意味着,扫雷游戏和纸牌游戏通过自己客户区对应的设备环境画图形,图形数据竟然画到了DcCopy窗口客户区对应的设备环境中。
这个例子验证了“设备环境”只是“环境”而不是“设备”,它并不存储发给它的图形数据,图形数据透过它写到了它所描述的“设备”上,每个窗口客户区的“设备环境”对应的设备都是屏幕,但它们在位置上可能重叠,所以向一个窗口的客户区写数据相当于同时写了下层窗口的客户区。
为了让当前激活的窗口在视觉上保持在最上面,下层窗口向自己客户区写的内容首先要经过Windows的“过滤”,只有没有被其他窗口覆盖掉的部分才真正被写到了屏幕上。
读者应该时刻提醒自己——“设备环境”只是一个环境,是设备属性的一组定义,程序输出的图形数据透过“设备环境”被定向到了具体的设备上,“设备环境”本身并不存储这些数据。
Device Context中Context的含义:设备环境的上面是应用程序,下面是具体设备,而它是用来“联系上下关系”用的。
有人可能认为:屏幕上的窗口就像放在桌面上的一张张纸,虽然一张纸可能暂时被另一张遮住,但纸上写的东西还是存在的,移开另一张纸就可以再次露出来。但实际情况是:桌面更像一个用粉笔写的公告黑板,一个窗口相当于划了一块空间写告示,写另一个告示的时候要把老告示的内容擦去一部分以便写新的内容,擦去的东西也就不存在了,如果要恢复老告示,那么必须把擦去的部分重新写上去。
2、获取设备环境句柄
要想对任何设备绘图,首先必须获取设备的“设备环境句柄”(hDC),几乎所有的GDI函数的操作目标都是hDC,在程序中得到一个hDC有几种方法。
最常用的方法是在WM_PAINT消息中用BeginPaint函数得到hDC,WM_PAINT消息的代码结构一般是:
.if eax == WM_PAINT ;eax为uMsg
invoke BeginPaint, hWnd, addr stPS
;刷新客户区的代码
invoke EndPaint, hWnd, addr stPS
xor eax, eax
ret
BeginPaint函数的返回值就是需要刷新区域的hDC。要注意的是:BeginPaint返回的hDC对应的尺寸仅是无效区域,无法用它绘画到这个区域以外的地方去。由于窗口过程每次接收WM_PAINT消息时的无效区域可能是不同的,所以这个hDC的值仅在WM_PAINT消息中有效,程序不应该保存它并把它用在WM_PAINT消息以外的代码中。基于同样的道理,BeginPaint和EndPaint函数只能用在WM_PAINT消息中,因为只有这时候才存在无效区域。
程序中常常有这种需求,就是在非WM_PAINT消息中主动绘画客户区,由于BeginPaint和EndPaint函数必须在WM_PAINT消息中使用,所以这时必须用另外的方法获取hDC,可以使用以下的方法:
invoke GetDC, hWnd ;获取hDC
;返回值是hDC
;绘图代码
invoke ReleaseDC, hWnd ;释放hDC
GetDC函数返回的hDC对应窗口的整个客户区,当使用完毕的时候,hDC必须用ReleaseDC函数释放。对于用GetDC获取的hDC,Windows建议使用的范围限于单条消息内,当程序在处理某条消息的时候需要绘画客户区时,可以用GetDC获取hDC,但在消息返回前,必须用ReleaseDC将它释放掉,如果在下一条消息中需要继续使用到hDC,那么必须重新用GetDC函数获取。
上面的两种方法获取的hDC都是窗口的hDC,如果要操作的是其他的东西,如打印机、位图等,就不能使用BeginPaint或GetDC函数了。当绘图的对象是一个设备的时候,可以用CreateDC函数来建立一个DC:
invoke CreateDC, lpszDriver, lpszDevice, lpszOutput, lpInitData
lpszDriver指向设备名称,如显示设备的设备名是DISPLAY,打印机的设备名一般为WINSPOOL,下面这几句代码建立的DC对应整个屏幕:
szDriver db “DISPLAY”,0
…
invoke CreateDC, addr szDriver, NULL, NULL, NULL
mov hDC, eax
当绘图对象是位图的时候,同样需要一个和位图句柄相联系的DC,这时可以用函数CreateCompatibleDC来创建一个显示表面仅存在于内存中的DC:
invoke CreateCompatibleDC, hDC
参数中的hDC是用来参考的DC句柄,如果指定的参数是NULL,那么建立的DC将和当前屏幕的设置兼容,为了用CreateCompatibleDC建立的DC绘画一个位图,还需要用SelectObject函数将hDC和位图句柄联系起来。
用CreateDC和CreateCompatibleDC函数建立的hDC在使用结束以后,必须用DeleteDC函数删除,注意这里不能用ReleaseDC,这个函数是和GetDC配合用的。
用BeginPaint/EndPaint以及GetDC获取的hDC的使用时间不能超出本条消息,与此相比,用CreateDC以及CreateCompatibleDC建立的hDC就没有这个限制,可以在任何时刻建立它并且一直使用到不再需要为止。
三、色彩和坐标
1、Windows中的色彩
可以表示的颜色总数由颜色深度决定,也就是存储每个像素所用的位数,各种显示设备可以显示的颜色总数可能大不相同,如果设备支持的颜色深度太浅,就会影响到图像的质量,会让人看起来觉得很粗糙和不自然。
一种颜色可以分解成红、绿、蓝三原色,所以可以用红、绿、蓝3个分量的组合来表示各种颜色。
当设备支持的颜色深度少于等于8位时(如8位(256色)、4位(16色)、2位(4色)或1位(2色)),总体位数太少,不足以用来表达3个颜色分量,这时系统建立一个色彩表,像素数据用来做索引在色彩表中获取颜色值,所以低于8位的颜色称为索引色。
只有当颜色深度大于8位的时候,像素数据中才直接包含红、绿、蓝3个分量。当颜色深度为16位的时候,红、绿、蓝各用5位表示,剩下的1位用做属性位,实际可以表示的颜色数目为2^15=32 768种,16位深度的彩色又称为16位色、高彩色或增强色。当颜色深度为24位的时候,3个分量各用8位表示,实际可以表示的颜色数目为2^24=16777216种,24位深度的彩色又称为24位色、16M色或真彩色。对于人的眼睛来说,超过16位的颜色就已经很难分辨了。
在Win32的编程中,统一使用32位的整数表示一个深度为24位的颜色,在这32位中只使用低24位,每一种原色分量占用8位,其中0~7位为红色,8~15位为绿色,16~23位为蓝色。在程序中用到一种颜色常数的时候,可以如下使用:
move ax, 红色 + 绿色*100h + 蓝色*10000h ;将颜色放入eax中
当显示设备无法表示24位色的时候,Windows会自动用设备可以显示的最接近的颜色来代替它,当显示设备的颜色深度比较低的时候,可以通过函数GetNearestColor来得知一种颜色(颜色)会被系统替换成哪种颜色:
invoke GetNearestColor, hDC, dwColor ;返回真正使用的颜色值
但是当显示设备颜色深度太低的时候,经过Windows自动转换的图像可能会让人觉得很不自然,所以在有些时候,程序员可能希望预先得知设备的颜色深度,然后根据具体情况显示不同的图形。
显示设备的颜色深度可以用以下函数获取:
invoke GetDeviceCaps, hDC, PLANES
mov ebx, dwPlanes
invoke GetDeviceCaps, hDC, BITSPIXEL
mul ebx
mov dwColorDepth, eax
第一个函数调用返回DC的色彩平面数,第二个函数调用返回每个像素的色彩位数,颜色深度最后可以通过dwPlanes乘以dwBitsPixel得到。
2、Windows中的坐标系
要用GDI函数绘图,就必须首先了解这些函数使用的坐标系,在默认的状态下,Windows坐标系以左上角做坐标原点,以右方当做X坐标的正方向,以下方当做Y坐标的正方向。坐标的数值要用一个有符号的16位数来表示,范围从-32 768~32767,坐标的单位为像素。这种坐标系定义方法的好处是:窗口中每一点的坐标不会因为窗口的大小改变而改变,试想一下,如以数学中通常的表示方法,以左下角做坐标原点,那么当窗口高度被用户调整的时候,客户区中每一点的Y坐标都会变化,在具体使用中就会有诸多不便。
但是Windows也提供了其他的一些坐标映射方法供程序员使用,可以用SetMapMode函数来为一个DC设置新的坐标映射方法:
invoke SetMapMode, hDC, iMapMode
可以设置的参数包括坐标原点、坐标和逻辑单位和坐标的正方向等,参数中的iMapMode为新的映射方式,其可以选择的取值如下表所示,Windows默认使用的映射方式为MM_TEXT。
Windows中可用的坐标映射方式
映射方式
|
原点
|
逻辑单位
|
X正方向
|
Y正方向
|
MM_TEXT(默认方式)
|
左上
|
像素
|
右
|
下
|
MM_HIENGLISH
|
左上
|
0.001英寸
|
右
|
上
|
MM_LOENGLISH
|
左上
|
0.01英寸
|
右
|
上
|
MM_HIMETRIC
|
左上
|
0.01毫米
|
右
|
上
|
MM_LOMETRIC
|
左上
|
0.1毫米
|
右
|
上
|
MM_TWIPS
|
左上
|
1/1440英寸
|
右
|
上
|
MM_ISOTROPIC
|
可变
|
可变(x=y)
|
可变
|
可变
|
MM_ANISOTROPIC
|
可变
|
可变(x!=y)
|
可变
|
可变
|
可以看到,除了默认的MM_TEXT方式外,下面5种映射方式:MM_HIENGLISH,MM_LOENGLISH,MM_HIMETRIC,MM_LOMETRIC和MM_TWIPS采用的都是原点位于左上角、X正方向向上的映射方式,另外,它们的坐标逻辑单位是不同的。
最后的两种映射方式MM_ISOTROPIC和MM_ANISOTROPIC提供了更灵活的选择,设置为这两种映射方式后,程序可以继续调用SetViewportOrgEx,SetViewportExtEx和SetWindowExtEx函数来自由设置坐标系的原点、逻辑单位和坐标的正方向等所有参数。在其他映射方式下的时候,不能使用这3个设置函数,这时任何对它们的调用都会被忽略。