图像处理之-位图
圖像處理之-位圖
- MD DoCumEnT: 3/16/2016 5:59:48 PM by Jimbowhy
 
自從發現MarkdownPad以后,就沉迷于寫作,從未有過這樣的浸淫,完全沒有了生物鐘的同期,基本上只要醒著,手眼就離不了屏幕,離不了鍵盤,一直敲著幾近光滑的按鍵,那種感覺就是滿足,如果要用個詞來形容,我覺得 F**KING WRITING! F**KING MY LIFE! 是恰當的。生命有終結的一天,而文字卻不會。 - by Jimbowhy 3/20/2016 4:32:04 PM
雖然本文的標題只說是位圖圖像處理,其實內容遠比標題豐富,原本計劃是只涉及位圖的文件結構分析和代碼實現。但一頭扎下去,就搞大了,從GDI到CONSOLE,從VGA到API繪圖,我覺得比較有趣的點基本都染指了。因為圖像處理本來就是很深又廣的領域,程序開發過程中不免也要和圖形打交道,而且BMP對于當前使用的Window操作系統是如此的重要,以致為它寫一本書的內容都是可以收集到的。
背景知識
位圖,BMP文件是 Windows、OS/2 操作系統的常用圖形文件格式,這里雖然用“常用”來形容BMP,其實把它稱為 Windows 基石也不為過,Windows 整個 GDI 系統都是圍繞它進行的。Win32程序的窗體都是通過位圖繪制出來的,可以說,沒有位圖就沒有Windows。它是設備無關的 DIB device-independent bitmaps,當然系統中常用的還有設備相關的位圖 DDB device-dependnt bitmaps。支持多種色深,色深用 BPP Bits-Per-Pixel 表示,有 1bpp 2bpp 4bpp 8bpp 16bpp 24bpp 32bpp 多種,可以保存使用色板的黑白雙色圖 monochrome,4色,16色,256色圖,對于其它更高的色深圖片則直接保存色值到像素所在的位置。對于低色深的位圖則在像素數據中保存色板顏色的索引號。為此,對于一個2色位置而言,每個像素只點一個比特,一個字節就可以保存8個像素了。至于DDB和DIB有什么具體差別,可以從后面的函數操作過程中理解,在這里可以將DDB理解為只有二維數據的位圖。另外,從BMP文件的信息頭也可以形像理解DIB的特點:
- 有獨立的顏色信息;
 - 有位圖創建時的設備尺寸信息;
 - 有位圖創建時的設備色板信息;
 - 有RGB三分量的色目數組數據映射到像素上;
 - 有數據的壓縮方式相關信息;
 
因此DIB位圖文件格式可以保存二維 two-dimensional 數字圖像,文件中包含了圖像寬度,高度,分辨率,色深信息,還有可選的壓縮方法,alpha通道,顏色配置信息,在 Windows Metafile (WMF) specification 中對位置文件格式有詳細說明,在C語言頭文件 wingdi.h 中定義了和位圖相關的常量、結構體。典型的BMP文件至少包含三個部分,BMP文件頭、DIB信息頭和像素數據,到于低色深位圖還有調色板 Palette,所謂調色板就是一個數組,每個元素使用四個字節定義一個RGB色值。調色板緊接DIB信息頭存儲。現在常用的BMP格式是 BMP Version 3,這個格式版本從 Windows 3.x 就開始使用了。以下是我重寫的結構體定義:
#pragma pack(push,2) typedef struct BitmapHeader { // BITMAPFILEHEADERunsigned short bfType; // 0x4d42; it occupy 4bytes if memory alignedunsigned int bfSize; // DWORD bitmap file sizeunsigned short bfReserved1;unsigned short bfReserved2;unsigned int bfOffBits; // offset to the bitmap bits data } BitmapHeader; #pragma pack(pop)typedef struct BitmapInfo{ // BITMAPINFOHEADERunsigned int biSize; // DWORD the size of this structure.long biWidth; // LONG the width of the bitmap, in pixels. long biHeight; unsigned short biPlanes; // WORD always is 1!unsigned short biBitCount; // the number of bits-per-pixel. 1 for monochrome bmiColorsunsigned int biCompression; // BI_RGB(uncompressed),BI_RLE8,BI_RLE4 ...unsigned int biSizeImage; // the size, in bytes, of the image. may be zero for BI_RGB.long biXPelsPerMeter; long biYPelsPerMeter; unsigned int biClrUsed; unsigned int biClrImportant; } BitmapInfo, *PBitmapInfo;一般Win32平臺的位圖總是 0x42 0x4D 兩個字節開頭的,即 BM 兩個字符,當然 bfType 有可能是以下的任意一種:
BM – Windows 3.1x, 95, NT, ... etc. BA – OS/2 struct bitmap array CI – OS/2 struct color icon CP – OS/2 const color pointer IC – OS/2 struct icon PT – OS/2 pointerBMP文件大小就保存在 bfSize 中,其實這個有點多余,通過文件讀取就可以得到BMP文件的大小了。然后就是 bfOffBits,它指出了BMP文件像素數據到文件開始字節的偏移量,結合BMP文件頭和DIB信息頭就可以計算到調色板的數據起止點。注意定義BMP文件頭 BitmapHeader 時,因為它第一個成員是2個字節的,如果編譯有對齊,那么文件頭結構體就會變成16個字節,這就不對了,因此需要設置編譯器對齊屬性。
biCompression 是壓縮信息,一般情況用得最多的是無壓縮格式 BI_RGB,可選值如下。但是只有自底向上 Bottom-up 的位圖才可以壓縮, Top-down DIB 不可壓縮。那么 Top-Down vs. Bottom-Up DIBs,什么是自底向上呢?所謂自底向上是指圖片的像素在內存存儲的順序是先保存圖片的最底下一行,再上一行這樣進行的。對 Bottom-up 的位圖,內存的第一個字節是保存圖片的左下角那個像素的。在GDI中所有的DIB都是 Bottom-up 方式處理的。
BI_RGB An uncompressed format. BI_RLE8 A run-length encoded (RLE) format for bitmaps with 8 bpp. Consisting of a count byte followed by a byte containing a color index. BI_RLE4 An 2-byte RLE format for bitmaps with 4 bpp. Consisting of a count byte followed by two word-length color indexes. BI_BITFIELDS Specifies that the color table consists of three DWORD color masks for 16/32-bpp. BI_JPEG Windows 98, Windows 2000: Indicates that the image is a JPEG image. BI_PNG Windows 98, Windows 2000: Indicates that the image is a PNG image.在BMP的壓縮方法基本都是游程碼方式,這是一種算法簡單的壓縮方式。如果說1億這個數,它在1的后面跟了8個0,那游程碼可以表示為1180,這種通過一個值來表示被壓縮內容長度的方法就是游程編程 RLE Run-Length Encode。前面講到位圖可以有16-bit/24-bit/32-bit幾種,如果中間這種,那么每個像素用3個字節表示,剛好每個顏色分量占一個字節。但是對于另外兩種,情況就不同了。16-bit的色深,每個顏色分量可以占5~6比特,這就涉及怎么安排RGB各顏色分量的位寬,Windows 95 只支持 RGB555 和 RGB565,還有32bpp模式的 RGB888。對于24bpp,還可以通過 DIB信息頭的 biClrUsed 來指定索引色數量,這樣在位圖使用的色彩數目較少的情況下來優化系統調色板,不過這種方法使用極少。當 biClrUsed 的數值為 0 時表示索引色為指定色深的最大索引數量。
為了使用不同的RGB分量位寬,需要指定壓縮信息為 BI_BITFIELDS,這里,調色板保存的就不是顏色定義,而是分量分割掩碼。有三個掩碼對應RGB三個分量,每個掩碼為32-bit,舉例來說 RGB555、RGB888兩個模式下每個分量的位寬是分別是5-bit、8-bit,掩碼定義如下:
The RGB555 format masks would look like: 0x00007C00 red (0000 0000 0000 0000 0111 1100 0000 0000) 0x000003E0 green (0000 0000 0000 0000 0000 0011 1110 0000) 0x0000001F blue (0000 0000 0000 0000 0000 0000 0001 1111)The RGB888 format masks would look like: 0x00FF0000 red (0000 0000 1111 1111 0000 0000 0000 0000) 0x0000FF00 green (0000 0000 0000 0000 1111 1111 0000 0000) 0x000000FF blue (0000 0000 0000 0000 0000 0000 1111 1111)在WIKI上有演示掩碼的定義格式:
 Diag. 2 – The BITFIELDS mechanism for a 32-bit pixel depicted in RGBAX sample length notation
?
DIB數據處理與應用
有了上面的數據結構,就可以通過 CreateDIBitmap() 函數來構造位圖了,注意這個函數創建的是DDB位圖,雖然名稱為CreateDIBitmap,這確實會讓人誤解,但它是通過DIB數據來創建DDB位圖的函數。給它設置參數 CBM_INIT 時,它就會使用色板和像素數據來初始化新建的DDB位圖,因此這里就會有DIB數據到DDB數據對拷。這個過程等價于使用 CreateCompatibleBitmap() 函數來創建DDB位圖,然后再使用 SetDIBits() 來向DDB拷貝DIB位圖數據,同樣這個函數名也有點容易誤解,它應該理解為設置DIB數據到位圖中。這里提到的兩個函數都是用來創建DDB的,所以第一個參數傳入的DC其實就是創建位圖所依賴的設備,特別是調色板。來看看MSDN對 CreateDIBitmap() 這個函數的最后一個參數 fuUsage 的解析:
Specifies whether the bmiColors member of the BITMAPINFO structure was initialized and, if so, whether bmiColors contains explicit red, green, blue (RGB) values or palette indexes. The fuUsage parameter must be one of the following values.
在不理解DIB和DDB的區別前,理解這段話是有難度,因為會不知所云。回到文章的開頭,DIB和DDB的最大的區別就是色值信息的保存,很有意思的。DDB可以理解為色值和像素數據是一體的,像DC中所使用位圖就是。而DIB則不是了,它可以將顏色保存在調色板,也可以在位圖像素中保存,如16bbp、24bbp、32bbp等等色深的位圖文件就是。因此這個參數的作用就是通過指定 DIB_PAL_COLORS 來使用輸入參數DC上的調色板,指定 DIB_RGB_COLORS 來使用像素數據的色值。系統中只有唯一一個結構體是描述DDB的:
typedef struct tagBITMAP {LONG bmType; LONG bmWidth; LONG bmHeight; LONG bmWidthBytes; WORD bmPlanes; WORD bmBitsPixel; LPVOID bmBits; } BITMAP, *PBITMAP;CreateDIBSection()才是真正創建DIB位圖的函數,其實bmp文件就是DIB位圖,所以通過文件流讀入的位圖文件二進位數據就可以用在這個函數中。在傳入參數 BITMAPINFO 結構體就是數據入口,這個結構體不包含了 bmp 文件頭和像素數據,只含有DIB信息頭和調色板兩部分數據。所以只要將讀取的位圖文件的開頭偏移一個14個字節,即一個bmp文件頭的長度后的數據傳入,并設置好偏移參數 dwOffset 即可。在輸出參數 ppvBits 就會指向包含DIB像素數據的內存地址。注意,輸入位圖的像素數據是通過 hSection 參數傳入的。通過這個函數的學習,其實可以將位圖文件的像素數據理解為 Section 更合適,這樣可以和MSDN文檔相統一,而且像素這概念通過用來表述顯示器上看得到的點,是具有顏色特征的。而BMP文件中的像素數據其實并不一定就是一個色值,還可以是色板的索引號碼。在使用這個函數時,需要傳入一個DC,當指定參數 DIB_PAL_COLORS 時,函數就會使用DC上的調色板來初始化像素。指定 DIB_RGB_COLORS 時則使用 bmiColors 的色板信息。
到這里可以理解DIB和DDB的另一個重要的區別,DDB可以在DC關聯的設備上顯示,而DIB則需要經過調色板的映射轉換,這就是DDB實用時效率更高,而DIB在各種設備之間轉換時兼容性更好。在轉換的過程中注涉及了位圖中的邏輯色板和設備上的物理色板,注意這里指的是硬件上設備,每種硬件可顯示的色彩都是有范圍的,這個色彩顯示能力就是物理色板的抽象概念。下面這幅圖可以幫助理解DDB是怎樣提高顯示效率的:
+----------------------------------------+----------------------------------------+ | Client Side | Server Side | | +--------+--------+ | | | Event | | | +--------+--------+ | | +------------+-----------+ | | | Memory Windows | | | +------------+-----------+ | | GDI via hBitmap +------------+-----------+ +------------+ | | ---------------------->| | BitBlt | | | | | DIB Section +--------+ DDB | | | ---------------------->| | | | | | Directly via pBits +------------+-----------+ +------------+ | | | | | +----------------------------------------+---------------------------+------------+ | Kernel Side | | +--------------------------------------------------------------------+------------+ | V | | Hardware Video Memory | | | +---------------------------------------------------------------------------------+正如前面一直在講DDB是依賴設備的位圖,對于依賴的設備可以通過DC來獲取相關信息。通過 GetDC(NULL) 可以獲取計算機屏幕DC,通常這是個彩色DC,將其傳入 CreateCompatibleDC() 就可以用來創建兼容的彩色DC。將其傳入 CreateCompatibleBitmap() 則可以創建一個彩色位圖。以下使用 GetDeviceCaps() 函數打印了一組DC的屬性,關于DC后面還要深入:
HWND hwnd = GetConsoleWindow(); HDC sc = GetDC( NULL ); HDC cc = GetDC( hwnd ); HDC dc = CreateCompatibleDC(NULL);Device Context Information: Device Context Information: Device Context Information: TECHNOLOGY:DT_RASDISPLAY TECHNOLOGY:DT_RASDISPLAY TECHNOLOGY:DT_RASDISPLAY HORZSIZE:482 HORZSIZE:482 HORZSIZE:482 VERTSIZE:271 VERTSIZE:271 VERTSIZE:271 HORZRES:1366 HORZRES:1366 HORZRES:1366 VERTRES:768 VERTRES:768 VERTRES:768 LOGPIXELSX:96 LOGPIXELSX:96 LOGPIXELSX:96 LOGPIXELSY:96 LOGPIXELSY:96 LOGPIXELSY:96 BITSPIXEL:32 BITSPIXEL:32 BITSPIXEL:32 NUMBRUSHES:-1 NUMBRUSHES:-1 NUMBRUSHES:-1 NUMPENS:-1 NUMPENS:-1 NUMPENS:-1 NUMCOLORS:-1 NUMCOLORS:-1 NUMCOLORS:-1 SIZEPALETTE:0 SIZEPALETTE:0 SIZEPALETTE:0 NUMRESERVED:20 NUMRESERVED:20 NUMRESERVED:20 COLORRES:24 COLORRES:24 COLORRES:24RASTERCAPS: 0x7e99 RC_BITBLT RC_BITMAP64 RC_GDI20_OUTPUT RC_DI_BITMAP RC_DIBTODEV RC_BIGFONT RC_STRETCHBLT RC_FLOODFILL RC_STRETCHDIB RC_OP_DX_OUTPUT通過檢索DC的光柵能力信息 RASTERCAPS,曲線能力 CURVECAPS,直線能力 LINECAPS 等等,上面最后一行輸出表示顯示設備直接支持 BitBlt()、SetDIBits()、GetDIBits()。通過 RASTERCAPS 還可以查詢是不是有 RC_PALETTE 調色板功能,而上面顯示沒有使用色板,SIZEPALETTE 和 NUMCOLORS 信息也表示沒有使用色板,-1 是指最大的色值范圍。這和現在使用的機器的真彩顯示器是對應的,不像以前VGA顯示器的色目只有幾十、百個的數量,可以使用色板來映射像素的色值。
這里著重點還是在設備的調色板,當設備使用了色板,不管是顯示器還是打印機還是其它任意設備,要提高DIB的顯示效率,可以先將DIB轉換成DDB來使用。轉換過程中涉及兩種方式,DIB_RGB_COLORS 和 DIB_PAL_COLORS,最簡單的情況是DIB沒有使用色板,前一種方式。24bpp位圖就是這種情況,它沒有使用色板,像素數據就是RGB色值。轉換DIB時,如果設備是真彩顯示器,那么就直接進行像素到像素的數據拷貝;如果設備使用了色板,那么就對DIB像素進行最接近色適配并轉換為色板的索引值。最復雜的情況是DIB和設備都使用了色板,提高效率的點就在色板的匹配上。使用系統調色板 GetSystemPaletteEntries() 來創建DIB可以優化像素的傳送效率,因為色板是匹配的,使用 SetDIBitsToDevice() 這樣的函數就可以省略配色的過程。
在使用 SelectObject() 函數為DC選擇位圖對象時,CreateCompatibleBitmap() 創建的位圖則會比 CreateBitmap() 創建的位圖更有效率,因為前者的位圖是兼容的不存在額外的色板匹配工作。色板的創建和使用相關的結構體、函數有 LOGPALETTE、CreatePalette()、RealizePalette()、SelectPalette()。詳細內容可以參考 MSDN 關于 Palette Manager 部分。
GDI繪圖API構架
古舊DOS平臺下,繪圖是通過VGA實現的,后來又有SuperVGA等等。VGA 就是 Video Graphics Array,也稱為視頻圖形適配器 Video Graphics Adapter,它把像素存儲到一個數組中即顯示緩沖區。符合VGA規范的顯示驅動器會定時獲取緩沖區的數據,并在顯示上呈現出對應圖像。VGA 的應用使得DOS平臺下的繪圖變得方便起來,數組化的像素也方便運算來處理。VGA支持多種顯示模式,從 2 色到 256 色,分辨率從 320x200 到 640x480。為了在Win32平臺下使用DOS的VGA進行繪圖,需要使用 DOSBOX 和 Borland C/C++ 3.1、DJGPP 2.0等等工具。設置 10h 中斷相關內容如下:
INT 10h, Service 0h Set Screen ModeInput: AH = 0hAL = Mode Number (see below) Output: The video mode is changed.Mode Number Text Res. Graphics Res. Description Adapters Max. Pages ---------------------------------------------------------------------------0h 40x25 ------ B&W Text CGA+ 81h 80x25 ------ B&W Text MDPA+ 82h 40x25 ------ Color Text CGA+ 4 or 83h 80x25 ------ Color Text (MDPA?)/CGA+ 4 or 84h 40x25 320x200 4 colors CGA+ 15h 40x25 320x200 2 colors CGA+ 16h 80x25 640x200 2 colors CGA+ 17h 80x25 ------ B&W MDPA (CGA+?) 18h to Ch -- PCjr or other adapters; no longer usedDh 40x25 320x200 16 colors EGA+ 8Eh 80x25 640x200 16 colors EGA+ 4Fh 80x25 640x350 2 colors EGA+ 210h 80x25 640x350 16 colors EGA+ 211h 80x25 640x480 2 colors VGA+ 112h 80x25 640x480 16 colors VGA+ 113h 40x25 320x200 256 colors VGA+ 1在 Mode 13h 即 256色模式下,每像素使用一個字節表示,總計剛好是 64K,即一個段的內存數量,它的地址約定分配在A000:0000 - A000:FFFF。而 B000:0000 則字符模式下的顯示緩沖區的映射地址。通過映射顯示器驅動卡的內存到計算機內存,程序可以直接通過操作計算機的內存來實現對顯卡的編程,這就大大方便了圖形的編程。在DOS平臺下,沒有大量的API要去掌握,你可以任意發揮想像,隨意修改VGA接口提供的顯示緩沖區來實現圖形繪畫,這是沒有API的一個大好處。
Windows 出現后,圖形編程有了統一的構架,因為需要掌握它的圖形設備接口 GDI Graphics Device Interface,而大量的不開放源代碼的API也成為開發人員的一種負擔,可能因為文檔還不太足夠以完全掌握好每一個API函數。在GDI框架下,顯示驅動接口映射的顯示緩沖區被分割成一塊塊小區域分發給Windows操作系統下運行的各式各樣的小窗口。而這些小塊的緩沖區域是通過設備上下方對象 DC device context 來管理的,它主要負責Windows系統與繪圖程序之間的信息交換,處理所有程序的圖形輸出。通過 GetDC() 來獲取任意程序窗口對象所分配的緩沖區信息,然后對這個緩沖區進行繪畫,就可以改變程序窗口的內容。當然,DC可以不跟顯示器直接有關系,可以在內存中建立一個DC,然后在它上面作畫,這就是離屏繪圖 Off-screen DC。DC可以分為四種類型,首先是直接在顯示器顯示繪畫的 Display DC,然后是可以在內存上作畫的 Memory DC,然后是可以在打印機上打印的 Printer DC,最后是包含DC信息的 Information DC。
Memory DC是使用較多的一種,在游戲開發中,需要在內存中對圖像進行操作,然后才是將圖像發送到顯示器上顯示。通過 CreateCompatibleDC() 可以創建一個 Memory DC,在使用它進行組圖之前,需要通過 SelectObject() 來設置尺寸修理工的位圖,位圖通過 CreateBitmap() CreateBitmapIndirect() CreateCompatibleBitmap() 就可以創建。繪圖完成后就通過 BitBlt() 函數將內存DC的圖像發送到顯示DC上顯示出來。
2001年XP系統推出時,GDI的擴展版 GDI+ 一并發布,后來 GDI+ 又被包裝進.NET框架的托管類庫中,成為.NET中窗體繪圖的主要工具。GDI+ 主要提供了以下三類功能:
- 矢量圖形:GDI+提供了存儲圖形基元自身信息的類或結構體、存儲圖形基元繪制方式信息的類以及實際進行繪制的類;
 - 圖像處理:GDI+為我們提供了Bitmap、Image等類。它們可用于顯示、操作和保存BMP、JPG、GIF等圖像。
 - 文字排版:GDI+支持使用各種字體、字號和樣式來顯示文本。
 
GDI接口是基于函數的,而GDI+是基于OOP Object-Orient Programming,使用起來比GDI要方便。因為GDI+實際上是GDI的封裝和擴展,執行效率一般要低于GDI。
可能是 GDI+ 實在是太新鮮了,我使用 GCC 4.7.1 MinGW 移植版無法編譯,即使是實例化 Graphics 類,也添加了 gdiplus.h 頭文件,還是出錯 Graphics 類無定義聲明。然通過查看GDI+頭文件,發現這貨使用了命名空間,所以只需要幾條指令就可以解決問題,看來是會 F**king Code 的娃:
#define ULONG_PTR ULONG #include <gdiplus.h> using namespace Gdiplus; #pragma comment(lib, "C:/gdiplus/lib/gdiplus.lib")如果需要使用GDI+,我目前使用 MSDN 1999OCT 是找不到資料的了,回來GDI,在Windows消息系統中有一和DC相關的消息是 WM_DEVMODECHANGE,相關的GDI函數列表如下:
CancelDC DeviceCapabilities GetDC GetStockObject ChangeDisplaySettings DrawEscape GetDCBrushColor ReleaseDC ChangeDisplaySettingsEx EnumDisplayDevices GetDCEx ResetDC CreateCompatibleDC EnumDisplaySettings GetDCOrgEx RestoreDC CreateDC EnumDisplaySettingsEx GetDCPenColor SaveDC CreateIC EnumObjects GetDeviceCaps SelectObject DeleteDC EnumObjectsProc GetObject SetDCBrushColor DeleteObject GetCurrentObject GetObjectType SetDCPenColorGDI的常用圖形對象有 HBITMAP、Pen、Font、Brush等,這些對象基本都在 MFC 中包裝成一個個的類對象,而Windows為其定義的句柄卻是通用的:
MFC Class handle Graphic Associated attributes CBitmap HBITMAP Bitmap Size, dimensions, color-format, compression and so on. CBrush HBRUSH Brush Style, color, pattern, and origin. CPalette HPALETTE Palette Colors and size (or number of colors). CFont HFONT Font Typeface name, width, height, weight, character set... Path Shape. CPen HPEN Pen Style, width, and color. CRgn HRGN Region Location and dimensions.這些都 GDI 框架的是核心類,在繪制任何圖形之前,一定要先創建或得到一個GDI核心類的對象才能完成繪圖工作。GDI 的圖形可以理解成系統一個畫圖環境,它具體包括要在哪里畫,畫什么東西,用什么畫,(顏色,畫筆,畫刷),怎么畫,畫圓還是畫線等等。在MSDN上的安裝盤上有大量的GDI演示例子,位置在 SAMPLES -> VC98 -> SDK -> GRAPHICS。可以使用 MSDN Library - October 1999 版,這個版本比中文版的 MSDN Library for Visual Studio 6.0 內容要豐富。關于GDI的內容主要有兩部分,一是 Platform SDK 目錄下的 Windows GDI 包含的全面API文檔,第二部分則是來自MSDN社區的技術文章 Technical Articles,其中 Multimedia 目錄下有個GDI的分類。說到MSDN,后面補充說明不同MSDN版的共享安裝。
當程序創建一個DC時,系統會設置默認的對象,除了 bitmap 和 path 以外。經常和這些對象打交道的函數有 GetCurrentObject(),通過它可以獲取DC上的各種圖形對象的句柄,而 GetObject() 函數則功能更加強大,它可以根據不同的輸入來獲取諸如 BITMAP, DIBSECTION, EXTLOGPEN, LOGBRUSH, LOGFONT, LOGPEN 等等結構體對象。需要繪畫不同效果的圖像時,就需要為DC指定圖形對象,這時就要使用 SelectObject() 函數。配套的函數還有 SetDCBrushColor() GetDCBrushColor() SetDCPenColor() GetDCPenColor()。
GDI中不同的圖形對象可以有不同的工作模式,例如可以通過 SetBkMode() 函數為源位圖設置背景色的透明混合方式,這樣GDI函數在混合圖像時就可以得到透明的效果。盡管MSDN文檔中只說明了 OPAQUE 和 TRANSPARENT 兩種參數選項,但是 NEWTRANSPARENT 是另一個可以用來處理透明效果的選項。通過設置透明模式,再通過 SetBkColor() 為源位圖設置一個背景色,像 StretchDIBits() 這樣的函數就會將背景色當作透明色來處理。模式設置是GDI中的重要組成,通過不同的模式可以實現不同的繪圖效果,相關的API如下可以查閱MSDN:
Graphics mode Get Function Set Function Background GetBkMode SetBkMode Drawing GetROP2 SetROP2 Mapping GetMapMode SetMapMode Polygon-fill GetPolyFillMode SetPolyFillMode Stretching GetStretchBltMode SetStretchBltMode句柄那一套
前面看了這么多函數,它們基本都返回了一個指向由系統管理著的對象,比如說我現在最關心的 HBITMAP 句柄,實際上我更希望它是指向位圖文件數據在內存的位置,而不是一個所謂“句柄”的東西,這種感覺不好。
通過 windef.h 頭文件可以找到句柄的類型鏈:
DECLARE_HANDLE(HBITMAP);#ifdef STRICTtypedef void *HANDLE;#define DECLARE_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name #elsetypedef PVOID HANDLE;#define DECLARE_HANDLE(name) typedef HANDLE name #endif其中 ## 是一個內置宏定義,意思是在預處理時的字符串的連接。經過編譯器的預處理后,HBITMAP 就會成為這兩個樣子:
#ifdef STRICTtypedef void *HANDLE;struct HBITMAP__ { int unused; };typedef struct HBITMAP__ *HBITMAP; #elsetypedef PVOID HANDLE;typedef HANDLE HBITMAP; #endif生成的代碼有兩種形式,在開嚴格模式 STRICT 時,定義了一個只有一個整形數據的結構體,而句柄就是指向這個結構體的指針,另一種情況則定義了一個無類型的指針。
句柄這個東西到底如何理解呢,我覺得作為Windows系統開發團隊之外的人,應該從幾個方面來看,從語言標準上,句柄就是指針和結構體的組合,這一點是基本的理解。從系統的結構層次來看,句柄是系統管理虛擬內存的一種方法。在《深入x86的內存尋址》提到,現有的32位x86構架CPU可以尋址4GB內存,而實際上當前還有大量機器根本沒有配備這么多的內存。為了讓程序有這個尋址能力,Windows采用的是虛擬內存管理技術,通過CPU的內存分頁機制,將磁盤空間映射為虛擬內存,因此這個4GB的尋址空間稱為 Virtual Address Space,代表機器并不是真的一定要有4GB內存。通過虛擬內存,將一些不太可能使用的內存數據移動到磁盤上就可以節省出物理內存,這樣就可以為在運行的程序提供多多可用的內存,因此磁盤上映射到內存的文件也稱為頁交換文件。
CPU的內存管理單元 Memory Management Unit (MMU),通過內存分頁機制為操作系統實現虛擬內存提供了硬件上的支持,這個過程可以用以下的流程圖說明。CPU內部暫存著就近使用的頁表格映射,當接收到需要轉換的虛擬地址時,轉換備用緩沖 Translation Lookaside Buffer (TLB) 就從 MMU 暫存的頁表格映射中查找,如果找到對應的物理地址就直接返回給程序,表示命中緩存 Cache Hit。如果沒有找到,則表示緩存錯失 Cache Miss,這時就到內部的頁表格中查找,如果找到物理地址的映射,則回寫到 TLB。如果在頁表格也沒找到對應的物理地址映射,那么就引發異常,這時由操作系統接管異常,并且實現從硬盤到內存的映射。
Windows NT/2000 系統,將低端的2GB地址空間分配給程序進行使用,而高端的2GB尋址空間則保留為系統所用。0x00000000 - 0x7FFFFFFF 這些地址便是進行地址空間,0x80000000 - 0xFFFFFFFF 這些便是系統空間。而企業服務器或高級服務器則是保留高端的1GB為系統所用,留下更多的3GB為程序使用。而久經謾罵的前輩們,Windows 95/98,個人還是挺喜歡 Windows 95 的個頭才不到100MB的安裝盤,它們由于系統的設計上是DOS兼容的實現,所以虛擬內存空間的安排也是名目眾多:
0K - ~64K (0xFFFF) Not writable. Reserved for Microsoft? MS-DOS?. ~64K (0x10000) - 4 MB(0x3FFFFF) Reserved for MS-DOS compatibility. 4MB (0x400000) - 2GB (0x7FFFFFFF) Available for code and user data. 2GB (0x80000000) - 3GB (0xBFFFFFFF) Shared area, readable and writable by all processes, DLL. 3GB (0xC0000000) - 4GB (0xFFFFFFFF) System memory, readable or writable by any process. However, writing to this region may corrupt the system, with potentially catastrophic consequences.這些虛擬的內存按分頁來管理,典型的x86機上分頁為4KB,每個分頁又按使用狀態分成三種,空閑態,是可以被程序申請使用的內存頁;保留態,即系統有規劃的但沒有相關的物理內存、或磁盤空間關聯的內存頁,可以使用 VirtualAlloc() 和 VirtualFree() 來申請和釋放;提交態,內存頁被提交后就會和實際的物理內存或交換文件關聯,這就是程序在使用的內存,可以通過前面提到的兩個方法來提交內存申請,也可以使用 GlobalAlloc() 和 LocalAlloc() 來申請。
Windows 系統中的內存可以用以下這兩張邏輯圖來表達:
Virtual Address | CPU Paging System | Storage & MemoryLinear Address | | +------------+ | +-----------+ +----------+ | +------------+ | 0xFFFFFFFF | | | Directory +---->+ Storage | | | .......... | | .......... | | | Entry | | Entry | | | Page #N | | 0x7FFFFFFF +------>+-----------+ +-----+----+ | | PAGE NULL | | .......... | | +------------+ | | | Page #2 | | 0x00000000 | | | PageTablle |-----+ | | Page #1 | +------------+ | | Entry |---------------->+------------+| +------------+ |通過分頁機制,程序使用到的邏輯內存地址可能會被映射到任意的物理內存地址或交換文件上,前面講到操作系統保留的高端內存實際上也可能是在物理內存的低端上。可以猜想,作為系統的實現者,需要在一塊固定的內存,物理地址和邏輯地址持久
 不變的內存上來實現操作系統的內存管理模塊。而其中一項功能就是句柄的管理,句柄在系統實現的層面上看就是操作系統的核心資源的指針,每個句柄對應管理模塊中數據表中的一個元素,而這個元素就包含了句柄所指資源的實際地址和對象類型等等必要的信息。在系統進行頁交換時,相應地修改對象表中的相應元素以反映實際的地址指向。因為Windows是一個多任務系統,具有多線程的安全性,所以在修改句柄對應的元素時就會禁止程序的訪問,從而實現多線程的數據一致性。句柄的使用就相當在API和系統內核之間引入了一個隔火層,同時由于句柄的存在,操作系統可以動態地更新句柄關聯的對象。在 Windows 2000 的安全系統為每一個系統對象管理了一張訪問控制列表,Access-control list (ACL),就算程序通過可能的手段獲取到一個句柄,也需要通過ACL的檢查才能取得系統對象的訪問權。
而從API的使用者的角度來理解,句柄就是門把,用來實現系統對象訪問的關鍵。每個進行可以使用的句柄數量是限量的,不能超出 65536 個,即兩個字節可以表示的數量。出于節省內存的目的,對于同一個對象,比如一個DLL文件,系統可以產生多個關聯的句柄,并進行計數,當句柄數為0時表示系統可以進行內存回收了,因為已經沒有程序需要使用這個對象了。《windows核心編程》講關閉句柄就表示創建者放棄對該內核對象的操作,系統就可以對句柄所占的資源進行回收,作為API的使用者盡早關閉句柄是一項基本操作要求,如果放任句柄開放,只會持續霸占系統資源導致性能問題,引發句柄泄漏 Handle Leak。如果根本不需要對系統對象進行訪問,像下面這條語句的做法是十分正確的,因為關閉句柄只是關閉句柄而不是關閉系統對象:
CloseHandel(CreateThread(...));和句柄關聯的對象有三類,用戶對象、GDI對象和內核對象,在MSDN中關于句柄的內容分類在基礎服務中,進程間通信 Interprocess communications (IPC) 講到句柄與系統資源對象。
User Object GDI Object Kernel Object --------------------------------------------------------------------------------------------- Accelerator table Bitmap Access token Job Caret Brush Change notification Mailslot Cursor DC Communications device Module DDE conversation Enhanced Console input Mutex Desktop Enhanced-metafile DC Console screen buffer Pipe Hook Font Event Process Icon Memory DC Event Semaphore Menu Metafile File Socket Window Metafile DC File Thread Window position Palette Find Timer Window station Pen and extended pen Heap Update resource Region除內核對象個,每個用戶對象或GDI對象只能對應一個句柄,但程序可以通過句柄的繼承來復用句柄。
關于句柄的部分就到這里吧,再下去,只能是 F**king Kernel!
游程碼壓縮
游程碼全稱 Run-length Encode,主要應用在 4bpp 和 8bpp 位圖上,又色圖和24位真彩圖總是為RI_RGB不壓縮格式。
Byte 1 Byte 2 Byte 3 Byte 4 Meaning 00 00 End of row 00 01 End of image 00 02 dx dy Move to(x+dx, y+dy) 00 n = 03 through FF Use next n pixels n = 01 through FF pixel Repeat pixel n timesIf the first byte is nonzero (the case shown in the last row of the table), then that’s a run-length repetition factor. The following pixel value is repeated that many times. For example, the byte pair
0x05 0x27
decodes to the pixel values:
0x27 0x27 0x27 0x27 0x27
The DIB will, of course, have much data that does not repeat from pixel to pixel. That’s the case handled by the second-to-last row of the table. It indicates a number of pixels that follow that should be used literally. For example, consider the sequence
0x00 0x06 0x45 0x32 0x77 0x34 0x59 0x90
It decodes to the pixel values
0x45 0x32 0x77 0x34 0x59 0x90
These sequences are always aligned on 2-byte boundaries. If the second byte is odd, then there’s an extra byte in the sequence that is unused. For example, the sequence
0x00 0x05 0x45 0x32 0x77 0x34 0x59 0x00
decodes to the pixel values
0x45 0x32 0x77 0x34 0x59
圖像透明處理
對于位圖,透明意味著什么呢?在現實世界,透明就是物體可以被光線穿透。而計算機顯示的透明則理解為不被處理或渲染的像素,這樣位于透明區域的其它內容得以呈現在顯示器上,形成透明的效果。當然,對于位圖,Windows并沒直接支持透明的API,要想位圖變得透明,還需要一點技巧。在GDI框架中,控制像素的混合操作方式的就是光柵操作 Raster Operation,所謂的光柵操作其實就是將不同的圖形對象的像素值進行邏輯運算操作,可以分為兩元和三元兩類。對于兩元光柵操作就是在 PEN 與位圖間進行的操作方式,即前面提到的GDI模式設置中的 Drawing 部分的內容,通過 SetROP2() 函數可以給指定的DC設置一種光柵模式。三元光柵操作則是兩個位圖間增加一個筆刷的操作方式,這種模式會在 BitBlt(), PatBlt(), StretchBlt() 這些函數中使用到。這些函數名看起來還真的不太雅觀,Blt 是什么意思呢,其實全稱就是 Block Tranpfer,也就是塊傳輸,對啊,這些函數可以是比 SetPixel() 效率要高上千百倍的啊。
要理解光柵操作,先要了解白色和黑色,在RGB色系中,白色是指色值所有比特位都是1,而黑色則相反,所有比特位都是0。兩色進行邏輯與運算,那么所有比特位結果就是0,也就黑色了,如果進行或運算,那么結果就是白色。如果參與光柵運算的顏色不是黑色或白色,那么就要看它的比特位有那些是0那些又是1了。以下是幾種最基本的光柵操作,記下來會有很大幫助的:
ROP Name Boolean OP Operation Use in transparency simulations SRCCOPY src S Copies the source directly to the destination. SRCAND src AND dest DSa Blacks out sections of the destination. SRCINVERT src XOR dest DSx Inverts the source onto the destination. SRCPAINT src OR dest SDo Paints the nonblack sections of the source onto the dest. NOTSRCCOPY src NOT Sn Inverts the source before paint it the the destination.先來解釋一下光柵操作碼,大寫字母 D、S、P 代目標DC、來源DC 和選選擇筆刷,也叫 Pattern,a、n、o、x 代表四種邏輯運算 AND、NOT、OR 和 XOR,通過光柵操作碼就可以了解具體某種光柵做了什么。SRCCOPY 就是源位圖說了算,源位圖是什么色繪圖后目標就是什么色,黑色也照抄過去。SRCAND則是雙方協定,只有兩邊的對應比特位同時為1,才會保留,否則就清零,這種混合結果有種融合的效果。SRCPAINT 則比較容易得到淺色調的結果,SRCINVERT可以得到互補色效果,NOTSRCCOPY 則是負片效果。關于光柵操作的MSDN參考內容在 Platform SDK => Graphics => Windows GDI => Painting & Drawing。雖然系統定義好了幾十個光柵操作模式常數,但實現上GDI系統可以使用的光柵模式多達幾百個,由于數量太多本文就不引用了。通過一個32-bit數值就可以設置光柵模式,例如,SRCCOPY 就可以通過傳入 0x00CC0020 來設置,功能是一樣的。
想要實現透明的位圖,一個方法就使用一個黑白雙色的遮罩圖,因為黑色已經比特位全0,將這個遮罩位圖與需要透明處理的位圖進行 SRCAND 光柵操作就可以將需要透明的,即遮罩位圖上黑色部分就被過濾掉了,從而實現透明效果。當然,利用遮罩位圖的白色區域來過濾透明區也是可以的,因為白色的比特位都是1,通過光柵操作 SRCINVERT 就即可以將透明區的內容變成黑色,然后再使用 SRCAND 就來組畫出透明效果。然而,還有比使用遮罩圖層更好的辦法,雖然說遮罩也不是一件特別特別麻煩的事。另一個方法是使用帶有 color-keying 的函數,如 TransparentBlt,所謂 color-keying 即通過指定一種透明色,在混合時過濾掉它而形成透明效果的一種技術。由于我使用 MinGW 找不到這個函數的定義,所以只好手動通過 LoadLibrary() 來加載了:
typedef bool (WINAPI *TBLT)(HDC, int, int, int, int, HDC, int, int, int, int, UINT); bool TransparentBlt(HDC d, int x, int y, int dx, int dy, HDC s, int xs, int ys, int dxs, int dys, UINT ck) {HMODULE h = LoadLibrary("msimg32.dll"); //GetModuleHandleif( !h ){if( GetLastError()==ERROR_MOD_NOT_FOUND ) cout << "ERROR_MOD_NOT_FOUND";return false;}TBLT f = (TBLT)GetProcAddress(h,"TransparentBlt");if( !f ){if( GetLastError()==ERROR_PROC_NOT_FOUND ) cout << "ERROR_PROC_NOT_FOUND";return false;}return f( d,x,y,dx,dy,s,xs,ys,dxs,dys,ck); }如果不是在API的基礎上,直接通過點陣繪圖,獲取位圖的透明效果是十分簡單的一件事,直接約定透明色來跳過匹配到的像素就完了。這也算是不使用API的一種天大的優點,簡單!先看一下效果圖 Console drawing with Super Mario:
在GDI框架中,DC可以說是溝通所有東西的十字路,像現在想要處理位圖也一樣,MSDN關于位圖的內容很多,但是卻都沒有能指出位圖其實可以理解為在內存里的顯示器。通過 LoadImage() 可以很方便地加載位圖文件,當然自己寫位圖文件的加載方法也可以,得到圖片文件的句柄扣,首先就要考慮將位圖文件轉換到DC上來操作。前面介紹了 SelectObject() 函數,它可以將圖形對象附加到DC上,但要求位圖圖形對象必須是 CreateDIBitmap() CreateBitmap() CreateDIBSection() 等函數返回的圖形對象。這里就使用 CreateDIBitmap() 來獲位圖文件的圖形對象,但是這個函數需要一個 BITMAPINFO 結構體,而MSDN上也沒在這個函數的主頁上標明那些函數可以取得 BITMAPINFO 結構體,這可算是MSDN的一大毛病!還好,事先有備而來,知道 GetDIBits() 這個函數可以。還有個 GetObject() 這個函數,它可以返回位圖文件的信息頭,通過它們就可以從位圖句柄上獲取圖形對象,現由 SelectObject() 附加到DC進行繪畫。注意,對于位圖,SelectObject() 只能對 Memory DC 操作。
其實從數據底層來講,這個過程不像MSDN上講得這么復雜,LoadImage()這個函數雖然是返回 HANDLE,但注意函數名是加了Image的,如果它不能解析出位圖文件的結構,就枉作為一個獨立函數存在了。事實上通過強制轉型,即 (HBITMAP)LoadImage(…) 這樣就可以得到,可以附加到DC上的位圖對象了。SelectObject()要做的其實就是將分配給位圖像素的內容和 Memory DC 關聯起來就完成工作了。
在繪制透明背景之前測試了控制臺對透明背景的支持情況,發現它不支持,需要通過其它API方法來實現透明,注意 C1_TRANSPARENT 這個常量在 mmsystem.h 定義的,對于VC6由于太舊的原因是沒有定義的。無論如何要實現透明效果一定是離不了光柵操作了,不然就像上面一樣自己寫代碼處理:
if( GetDeviceCaps( hdc,CAPS1 ) & C1_TRANSPARENT ){cout << "C1_TRANSPARENT support! ";int om = SetBkMode ( hcc,NEWTRANSPARENT );int oc = SetBkColor( hcc,RGB(0xe0,0x75,0x50) );BitBlt( hdc, 0, 0, bmp.bmWidth, bmp.bmHeight, hcc, 0, 0, SRCCOPY );SetBkMode ( hcc,om );SetBkColor( hcc,oc ); }這里就提供一種通過 Memory DC 來實現位圖透明的示例,通過光柵操作來實現 Color-Keying 透明。需要使用 CreateCompatibleDC() 來創建內存DC,然后通過 CreateCompatibleBitmap() 來創建兼容位圖。需要注意的是,每一個設備都會有一個物理調色板,電腦顯示器也具有特定的顯示色彩范圍,那么顯示器這個顯色能力范圍在GDI中就抽像為物理調色板。而創建DC時也一樣,它需要和特定的繪圖對象關聯,如果是顯示器,那么這個DC使用的調色板就是顯示器的物理調色板,如果通過 BitBlt() 等GDI函數往DC寫入色值數據,就需要先匹配色值是不是在可以接收的范圍,如果超出顯示范圍,那么GDI系統就要進行預處理,以物理調色板上最接近的一個色來替換。而對于 CreateCompatibleDC() 創建的DC,如果參數沒有引用其它DC,那么新建的DC就默認選擇了一個1像素的黑白雙色位圖,這也就是說,在這樣的DC下繪圖只能得到黑白圖片。同理,如果沒有給 CreateCompatibleBitmap() 引用一個彩色DC,那么它創建出兼容設備位圖時,也只兼容黑白兩色,而且默認像素為黑色大小也是1個像素。CreateBitmap()函數可以創建指定色深的位圖。無論如何,彩色或黑白都是有用的,下面來演示怎么用好它們。
例子使用超級瑪麗游戲的截圖,目標是藍色天空,通過 color-keying 將天空透明處理。首先通過 LoadImage() 加載位圖文件,然后通過 GetObject() 獲取位圖對象信息,主要是位圖的尺寸信息,然后創建兩個DC,一個用來裝入原圖片,另一個用來生成 mask 圖層。mask 圖層一定要又色位圖,這一點很重要。這是因為GDI系統默認背景色為白色,單色位圖向彩色位圖轉換,單色位圖白色部分會轉換為彩色位圖的背景色,單色位圖黑色部分會轉換為彩色位圖的前景色。彩色位圖向單色位圖轉換,彩色位圖的背景色轉換到單色位圖的白色,其他色值則轉換為黑色。
創建好DC和對應的位圖對像后,在原圖DC上設置背景色為天空藍,然后將它繪制到 mark DC上。這樣就可以得到除天空藍區域外,位圖其它全部都是黑色的遮罩圖層。因為需要透明的是天空藍,而遮罩層黑色的不是天空藍對應的區域,所以需要將 mask 進行反轉后與原圖進行 SRCAND 光柵操作,這樣就可以得到天空藍為黑色的源圖,最后只需要再經過一次 SRCPAINT 就可以顯示透明效果了。前面提到的幾種基本光柵操作就有求負片的 NOTSRCOPY 光柵操作,但是使用這種方法還不是最簡單的,通過MSDN文檔可以找到一個無名稱的光柵,它的代碼為 0xa00220326,操作定義為“DSna”,可以把這個光柵稱作COLKEY。和 SRCERASE 有十分相似,它則定義為 SDna,先對目標DC進行反轉,再和源DC相與操作。因此天空白的 mask 與原圖進行 DSna 混合后,就得到天空區域為黑色,其余區域保持不變的圖像。將目標區域變成黑色就意味著 color-keying 工作完成了。最后就是通過 SRCPAINT 將其繪制到顯示器DC上,這就得到透明效果輸出了,在做最后這個操作時,可以考慮使用遮罩與目標DC進行一次 SRCAND 操作,去掉非透明區的像素以免在 SRCPAINT 操作時留下印記。對于多個透明色的情況,就需要進行多次光柵運算處理。這里先貼出這部分的完整代碼塊,配合圖片演示就可以起到很清晰的說明作用,最終效果可以參考前面貼出的圖片:
HWND hwnd = GetConsoleWin(); HDC hdc = GetDC( hwnd );HBITMAP mario = LoadBitmap( GetModuleHandle(NULL),(char*)(16) ); if( !mario ) printf(" error LoadBitmap %d\n", GetLastError() ); HANDLE hm = LoadImage(NULL, "mario256.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); //mario = (HBITMAP)hm;HDC hdc = GetDC( GetConsoleWin() ); //GetConsoleWindow() HDC mask = CreateCompatibleDC(hdc); HDC draw = CreateCompatibleDC(hdc); //int blue = RGB(0x60,0x80,0xc0); int blue = RGB(0x50,0x75,0xe0); int white = RGB(0xff,0xff,0xff); int black = RGB(0x00,0x00,0x00); int sw = GetSystemMetrics(SM_CXSCREEN);//GetDeviceCaps( mask,ASPECTX ); int sh = GetSystemMetrics(SM_CYSCREEN);//GetDeviceCaps( mask,ASPECTY ); BITMAP bmp; GetObject( mario, sizeof(BITMAP), &bmp ); int w = bmp.bmWidth, h = bmp.bmHeight; HBITMAP mono = CreateCompatibleBitmap( mask, w, h ); SelectObject( mask,mono ); SelectObject( draw,mario ); printf(" Bitmap size (%d,%d) Screen size(%d,%d)\n", w, h, sw, sh); SetBkColor( draw, blue ); BitBlt( mask, 0, 0, w, h, draw, 0, 0, SRCCOPY ); BitBlt( hdc, w, 0, w, h, mask, 0, 0, SRCAND ); BitBlt( draw, 0, 0, w, h, mask, 0, 0, 0x00220326); // Color-keying BitBlt( hdc, w, 0, w, h, draw, 0, 0, SRCPAINT); DeleteDC(hdc); DeleteDC(mask); DeleteDC(draw); DeleteObject(mario); DeleteObject(mono);控制臺下繪圖
Win32平臺下,無論是控制臺程序還是GUI程序,都可以進行點陣繪圖,也可以通過DC來進行區塊繪圖。基本流程就是 GetDC() -> SetPixel(),對于控制臺程序需要使用 GetConsoleWindow() 來獲取程序窗口句柄,這個函數要求宏定義 _WIN32_WINNT >= 0x0500。需要讀取像素色值,用 GetPixel(),也可以使用 GetBitmapBits() SetBitmapBits() 在圖像的緩沖區之間對拷,BitBlt() 則是DIB的對拷。在第一小節中講解的 CreateDIBSection() 函數可以創建一個 DIB,這樣就可以直接操作圖像數據緩沖區,相似的還有 CreateDIBitmap(),它們都可以返回一個 HBITMAP 句柄。前者創建的是設備無關位圖DIB句柄,對應一個DIBSECTION結構,后者創建與設備有關的位圖 DDB 句柄,更像是打開位圖文件。SetDIBits() 和 GetDIBits() 兩者可以在 DDB 和 DIB 間對拷。
例如,以下代碼就可以在控制臺上繪制圖片,注意使用了 RGB 這個宏,因為位圖的色彩分量是 GBR 排序的,所以通過RGB來轉換順序:
HWND hwnd = GetConsoleWindow(); HDC hdc = GetDC( hwnd ); HBITMAP hbmp = (HBITMAP)LoadImage(NULL, "mario16.bmp", IMAGE_BITMAP, 256, 240, LR_LOADFROMFILE); BITMAP bmp; GetObject( hbmp, sizeof(bmp), &bmp ); unsigned char *bits = new unsigned char[bmp.bmHeight * bmp.bmWidthBytes];GetBitmapBits( hbmp, bmp.bmHeight * bmp.bmWidthBytes, bits); int x = 0, y = 0, color = RGB(bits[l+2],bits[l+1], bits[l]); SetPixel( hdc, x, y, color );bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = bmp.bmWidth; bmi.bmiHeader.biHeight = -bmp.bmHeight; bmi.bmiHeader.biPlanes = bmp.bmPlanes; bmi.bmiHeader.biBitCount = bmp.bmBitsPixel; bmi.bmiHeader.biCompression = BI_RGB; GetDIBits( hcDC, hbmp, 0, bmp.bmHeight, bits, &bmi, DIB_RGB_COLORS );GetBitmapBits() 這個函數主要是為了兼容 16-bit Windows程序的,Win32 程序則優先采用 GetDIBits() 來獲取設備無關的DIB信息。但是 GetDIBits() 這個方法太蛋痛了,也不知道微軟的程序員怎么腦子,在設置了參數 lpvBits 的情況下,就非要手動設置 BITMAPINFO 前6個成員即 biSize 到 biCompression 設置好才能正確加載位圖。對于 bottom-up DIB 自底向上的 DIB,高度值要設置為正數,反之 top-down DIB 自頂向下的 DIB 則要設置高度為負數。需要注意的是,經過 LoadImage() 加載的位圖文件會自動轉換成32-bit的模式,所以設置給 GetDIBits() 設置 biBitCount 參數時需要注意。LoadImage() 還可以設置 LR_CREATEDIBSECTION 參數,這樣載入時就不會自行轉換色彩深度,也不會映射顯示設備的色彩。
另外GetObject()獲取到的 BITMAP 結構時,長寬信息是對的但是像素數據 bmBits 卻不對,太操蛋了MS這API寫得,只會讓人在做 fruitless work。在MSDN上資料說,只有在使用 CreateDIBSection() 創建的 HGDIOBJ 才會設置 bmBits 指向像素數據緩存區。而且,另外兩個參數還要使用 DIBSECTION 結構才能有效返回像素數據。DIB 顏色使用兩種模式之一,DIB_PAL_COLORS,DIB_RGB_COLORS,前者對應索引色。要用 CreateDIBSection() 來創建 DIB,可以選擇在內存映射文件對象上來創建,dwOffset 表示從這個內存映射文件的偏移處開始申請內存,這樣可以將DIB共享給其它進程,hSection 參數設置為 NULL 表示在常規全局內存區申請。為了搞清 CreateDIBSection() 這個函數,可以參考MSDN ATL Sample - ATLFIRE 的實現,MSDN有關于ATL的文檔,位于 Visual Studio Documentation 的參考部分。CFireWnd 這個例子是演示ATL控件編程的,在它的源代碼 CFireWnd 類的成員方法 CreateBitmap() 中就有使用 CreateDIBSection() 這個函數的相關代碼,下圖是這個例子的VB測試項目,效果是不是很棒,CPU占用率還極低!
StretchDIBits()這個函數可以將像素區塊拷貝到DC的DIB上,但不是所有驅動設備都完全支持這個函數的功能的。因此GDI在需要的時候,會通過整合 SetDIBits() GetDIBits() BitBlt() 等等函數來模擬它,這種情況下,多半會有性能問題。StretchDIBits() 的參數可以設置 DIB_RGB_COLORS 或 DIB_PAL_COLORS方式,后者指示需要使用調色板來對源像素進行索引色轉換,調色板則通過 BITMAPINFO 結構體來傳入。
在MSDN上的Multimedia技術文章GDI分類下有一篇 Animation in Windows,講到如何通過BID來提升動態繪圖的性能,文章還講到了離屏的繪圖,MSDN光盤上還有對應的項目代碼 SHOWDIB。
點陣繪圖的模式下,如果算法不夠好,就很容易出現線條不平滑的情況,如下圖顯示了特定斜率的畫線出現的鋸齒狀態,如果是水平、豎直或45度角這些情況都不會出現鋸齒現象,一個很實用的抗鋸齒算法就是在像素出現行列跳躍的接點進行弱色填充,如右圖:
y axis y axis A A | ******** | __cg******** | ******** | __cg*****^^ | ******** | __cg*****^^ | ******** | *****^^ +--------------------------------> x axis +--------------------------------> x axis你要看不出差別,你可以對著兩張圖使勁眨眼睛。當年玩DOS編程的時候硬是拿鋸齒問題沒辦法,實在是太丑了,又沒有一點視覺上的常識,不會用顏色來欺騙眼睛。會畫線后,也要會畫圓什么的吧,這和畫直線是一個道理,都是通過設置像素的色值,只不過畫圓需要用到三角函數來計算像素的坐標,橢圓 ellipses 曲線 curves 什么的自然也是按曲線方程來設置相應的像素。
輸入輸出
鼠標和鍵盤支持是最基本的輸入輸出,在DOS平臺下,鼠標需要通過 0x33 BIOS 中斷,或者檢測串行口的鼠標狀態。在Win32平臺下可以選擇GUI程序,它直接支持鼠標功能,當然使用控制臺程序也可以通過API間接實現鼠標支持。控制臺程序默認是不使用消息環進行交互的,而是通過消息隊列來獲取輸入信息的。當然消息環作為系統的一個核心功能,控制臺也可以使用它。例如通過 SetTimer() 實現控制臺的定時器時就可能需要使用消息環,因為這個函數是通過消息機制來實現的。通過 SetConsoleMode() 設置控制臺的ENABLE_WINDOW_INPUT 模式,然后就可以使用 ReadConsoleInput() 來響應用戶輸入了,包括鍵盤和鼠標,注意要先禁止控制臺 的快速編輯功能,否則鼠標消息就會被過濾掉。
通過 ReadConsoleInput() 函數可以讀取到輸入事件數據結構體 INPUT_RECORD,對應各種事件列表如下:
FOCUS_EVENT_RECORD &ife = INPUT_RECORD.Event.FocusEvent;KEY_EVENT_RECORD &ike = INPUT_RECORD.Event.KeyEvent;MENU_EVENT_RECORD &imu = INPUT_RECORD.Event.MenuEvent;MOUSE_EVENT_RECORD &ime = INPUT_RECORD.Event.MouseEvent; WINDOW_BUFFER_SIZE_RECORD &iwb = INPUT_RECORD.Event.WindowBufferSizeEvent;通過對事件數據判斷就可以知道詳細的按鍵信息,控制鍵狀態,鼠標位置等等。詳細的文檔可以參考 MSDN 的 Platform SDK => Base Services => Files & I/O 部分。如果需要可以通過 AllocConsole() CreateProcess() 函數來創建新的控制臺窗口。
在控制臺模式下,有兩套API,一套以字符為計量單位,一套則是以像素為計量的GUI函數。C語言的標準置入輸出只是負責將字符內容發往顯示緩沖區,并沒有對緩沖區操作的功能。只有通過操作系統提供的API,如 SetConsoleWindowInfo() 等函數來修改輸出緩沖區的狀態。通過 GetStdHandle() 和 GetConsoleScreenBufferInfo() 可以獲取緩沖區的詳細信息,如控制臺窗口的實際大小即控制臺可以通過滾動條拖出來所有區域。還有當前顯示區域的大小,光標位置,這些都是以字符為單位的,每個字符占用的位置就用一個 Cell 來計量。每個位置實際占用的像素數量需要根據字體的大小和類型來計算,在這里所有字符其實就是一個個點組成的點陣字符。通過 SetConsoleCursorPosition() 函數就可以為控制臺出來函數指定字符輸出的位置。一個典型的控制臺有80列字符,行數可以為幾十至幾百行不等,參考程序打印的信息:
Connsole Screen Buffer Infomation:size: 80,300cursor position: 0,22view window: 0,79,30,0max window: 80,62標準C語言庫函數是沒有定位光標的,需要在指定的光標位置輸出內容就要使用操作系統操作的API,如 WriteConsole() WriteConsoleOutput() WriteConsoleOutputCharacter()。需要改變字符顏色可以使用 WriteConsoleOutputAttribute()、SetConsoleTextAttribute(),前者可以單獨設置每個字符的顏色屬性。
對于底層的按鍵輸入,如 Ctrl+C、Ctrl+Break 等等,可以通過 SetConsoleCtrlHandler() 來設置一個回調函數 ConsoleHandlerRoutine 來響應。通常情況下這些消息是不用處理的,用戶要通過強制終止自然是有原因,如果屏蔽掉駝些消息倒是有幾分流氓特質。使用 SetConsoleTitle() GetConsoleTitle() 可以操作控制臺的標題欄的文字內容。可將控制臺讀寫的API分為高層和底層兩類,又可以分為輸入或輸出兩類,對于底層的API列表如下:
Function Description ReadConsoleInput Reads and removes input records from an input buffer. PeekConsoleInput Reads without removing the pending input records in an input buffer. GetNumberOfConsoleInputEvents Determines the number of unread input records in an input buffer. WriteConsoleInput Places input records into the input buffer behind pending records. FlushConsoleInputBuffer Discards all unread events in the input buffer.ReadConsoleOutputCharacter Copies a string of Unicode or ANSI characters from a screen buffer. WriteConsoleOutputCharacter Writes a string of Unicode or ANSI characters to a screen buffer. ReadConsoleOutputAttribute Copies text and background color attributes from a screen buffer. WriteConsoleOutputAttribute Writes text and background color attributes to a screen buffer. FillConsoleOutputCharacter Writes a single Unicode or ANSI character to a consecutive cells. FillConsoleOutputAttribute Writes a text and background color to a consecutive cells. ReadConsoleOutput Copies character and color data from a specified buffer cells. WriteConsoleOutput Writes character and color data to a specified buffer cells.為了適應多語言,可以使用控制臺的代碼頁函數,輸入端的代碼頁使用 SetConsoleCP() 和 GetConsoleCP(),輸出端的代碼頁使用 SetConsoleOutputCP() 和 GetConsoleOutputCP()。當前系統支持的代碼頁保存在注冊表中,常用的代碼頁有 54936(GB18030), 936(GBK), 65001(UTF8) :
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage在另一個層面,通過GDI函數 GetClientRect() 可以獲取控制臺當前內容區域占用的像素數量。結合控制緩沖區的詳細信息,就可以將當前光標所在的字符位置轉換為像素坐標,參考程序如下:
COORD GetCursorPositionPixel(COORD poc) {HWND hwnd = GetConsoleWindow();RECT rect;GetClientRect( hwnd, &rect );CONSOLE_SCREEN_BUFFER_INFO bi;HANDLE hao = GetStdHandle(STD_OUTPUT_HANDLE);GetConsoleScreenBufferInfo( hao, &bi );COORD pos = bi.dwCursorPosition;SMALL_RECT v = bi.srWindow;pos.X = poc.X*(rect.right/v.Right);pos.Y = poc.Y*(rect.bottom/v.Bottom);return pos; }有了像素座標就可以使用 SetPixel() 這樣的GDI函數來在指定點進行繪圖操作。系統還提供了一個函數 GetCursorPos() 來獲取鼠標在屏幕上的坐標,通過 ScreenToClient() ClientToScreen() 函數可以在屏幕到窗口的坐標系之間轉換。GetWindowPlacement() 可以用來獲取窗口在屏幕上的坐標及顯示狀態,GetClientRect() 可以獲取四限。關于窗口的內容MSDN上的 User Interface Services 上有齊全的資料。
動畫與實現
動畫是什么呢?其實動畫就是人眼的由于視覺停留產生的假像,由一幅幅幾乎相同的圖像在一定的時間內連續切換,比如每秒切換12張就會產生動畫的現像。從技術上來講,動畫就是定時器,從藝術層面上講,動畫就是一種感覺,如何掌握繪畫技術,這個繪畫是真的指繪畫啊,不是用代碼作圖,有繪畫技能再玩編程必定是個有趣的事。
之前看過 Richard Williams 出版的 The Animators Survival Kit Animated《動畫師生存手冊》,其中分解了大量的動畫技巧,這是我首次接觸到的讓我震撼的關于動畫技術方面的知識。之前或到目前,還沒有接觸到國內的有關方面的知識,只是知道小蝌蚪找媽媽、大鬧天宮、七彩鹿這些作品,但對于其中技術層面的東西卻沒接觸到,這是為什么呢?還是為什么呢!而國外在這方面卻有大量的知識輸出,比如說迪士尼九老 Disney’s Nine Old Men 之一,弗蘭克在辭去工作后,和奧利.約翰斯頓,九老之一,開始寫作的 The Illusion of Life《生命的幻象:迪斯尼動畫造型設計》一書。他們那一幫人創建性地給出了動畫的12條法則:
- Squash & Stretch 擠壓與拉伸,物體在接觸或受外力后出現運動反轉時使用,小球彈地而起的動畫就用到這個原理;
 - Anticipation預備動作,加入一反向的動作以加強正向動作的張力,借以表示下一個將要發生的動作。這個原理就像說笑話時要先鋪墊一下背景,否則講到笑點的時候只有講的人笑就不好了;
 - Staging,分場就是要分步表現動作的意圖使之容易理解;
 - Straight-ahead vs. Pose-to-pose,逐幀畫法 VS 關鍵幀畫法,標準動畫的一秒鐘有24幀,顧名思義逐幀畫法是一幀一陣接著畫,關鍵幀則是先畫出關鍵的動作點幀,然后再再有加中間幀畫手畫中間的畫;
 - Follow-through & Overlapping Action,慣性跟隨和動作重疊,理解不了嗎?想想波動是如何發生的;
 - Slow-in & Slow-out 慢入與慢出,動作的起勢和收勢都慢,而中間的部分則是快的,這樣一個動作才不會特別平板勻速,而會更有力度感一些;
 - Arcs 弧形運動軌跡,凡所有會動的生物,其組成的任何部分之運動軌跡皆為平滑的弧形曲線,可以用三角函數的關系來理解,比如說手臂的擺動,擺到接近關節限位時,速度就會慢下來,這就像三角函數的波峰與波谷,變化會變得很緩和;
 - Secondary Action,次要動作是用來增加動畫的趣味性和真實性,豐富動作的細節的。它要控制好度,既要能被察覺,又不能超過了主要動作,如甩頭和眨眼的關系;
 - Timing & Spacing,應該譯作節奏,而不是時間和間隔,節奏感可以由不同速度的交替變化產生,但又不僅僅是速度上的交替;
 - Exaggeration 夸張,利用擠壓與伸展的效果、夸大的肢體動作、或是以加快或放慢動作來加乘角色的情緒及反應,這是動畫有別于一般表演的重要技巧;
 - Solid drawing,厚實感,在二維的物品加入三維因素的考慮,例如燈光陰影,重量感和平衡感等;
 - Appeal 吸引力,動作的精彩程度,也就是動作的表演。
 
回到大鬧天宮,這個片在國外有個名字 the Monkey King - Uproar in Heaven,在國外大有粉在啊,來看看人家是怎么評價的:
I can’t believe what my eyes are seeing.
 Un - fucking - believable. I cannot believe I am privleged to see this amazing cultural artifact.
 Which studio? What year? Who was the director? Tell me or so help me, I’ll drive up there and beat it out of you.
Fantastic! There’s also a Japanese version of the Monkey Legend from the 1960’s. The one I saw was dubbed by Frankie Avalon & Jonathan Winters. Not so authentic as this clip, but just as colorful and WEIRD!
I saw this when I was a little girl. When Channel 4 in the UK went on the air, one of the first things it aired was this movie. I never saw it again…I’ve wondered for a long time if it was some fantastically beautiful dream I had. Its just as wonderful as I remember it! Thank you so much for uploading.
Oh man, I haven’t seen this in over a decade! My parents had it recorded on VHS tape, but it’s forever lost. I recently got the name of this show from the library. Thank you so much for posting this up!
I LOVE this movie, I remember watching it the first time on TV when I was like 6-7 years old (I’m 23 now) and taped it, I still to this day have the tape.
Hey man, thanks for uploading this movie~
 Is there no way to get subtitles, though, for non-chinese speakers? It’d be awesome if americans could watch this too. I’m guessing you’re chinese and can speak/write it? Is it you don’t want to do subs, or don’t know how to?
 ……
本文作為一篇技術層面的書自然離不開技術上的實現,為了添加動畫顯示,就要使用 SetTimer() 設置定時器運行,如果不使用回調函數,到時間點系統就會發出一個 WM_TIMER 消息給程序,程序就可以通過處理這個消息來實現實時器功能。即使提供回調函數時,SetTimer() 也不能脫離消息機制,它需要系統默認的窗口過程函數來調用回調函數,所以需要 DispatchMessage() 來分發內部的定時器事件消息。注意它使用在一個精度要求不高的情況下,在幾十毫秒的水平,如果需要高精度的定時可以考慮多媒體定時器 timeSetEvent() 實現,它還可以支持 SetEvent。注意定時器配對清理函數為 KillTimer()、timeKillEvent()。通過 GetTickCount() 可以獲取系統的毫秒級別時間,但是精度也不高 55ms,一個 DOS 時鐘的 tick,因為是通過 IRQ0 18.2 Hz 實現的,而多媒體時間函數 timeGetTime() 則精度高得多。多媒體系統函數通過 mmsystem.h 和 winmm.lib 引用。注意定時器回調函數定義的方式是不同的,不能混用,以下回調函數的定義格式中左邊的是 SetTimer() 函數使用的定義:
void CALLBACK TimerProc( void CALLBACK TimeProc(HWND hwnd, // handle to window UINT uID, // Identifier of the timer event.UINT uMsg, // WM_TIMER message UINT uMsg, // Reserved; do not use.UINT_PTR idEvent, // timer identifier DWORD dwUser, // User instance dataDWORD dwTime // current system time DWORD dw1, DWORD dw2 // Reserved; do not use. ); );在控制臺下嘗試使用 GetConsoleWindow() 來給定時器提供窗口綁定,總是問題錯誤 Access is denied!當然可選擇的方法大把,除了上面提到的多媒體時間函數,還可以使用同步等待API,例如 WaitForSingleObject() 函數就可以實現定時器功能。同步函數還有其它可以實現定時器的,如 CreateWaitableTimer(),它需要和 SetWaitableTimer() 配合使用,由于它的時間精度是以 100 納秒為基準的,因此會使用一個64-bit的大數 LARGE_INTEGER,通過設置一個負數來實現定時。
關于同步功能,參考MSDN Platform => Base Services => DLLs,Processes, and Threads => Synchronization。基本的用法是通過一個可用來同步的對象,如線程、進程、作業等等,分別通過 CreateJobObject() CreateProcess() CreateThread() 來創建,然后使用等待函數 Wait Functions 來暫停程序的執行,等待目標對象完成工作,以實現程序同步。同步功能可以實現定時器的功能,但它絕對不是為了實現定時功能設置的。
在閱讀MSDN文檔有讀者可能會理解不了什么時回調函數,就連MSDN文檔對 TimeProc 的說明也指它是一個占位符號 placeholder,而不是說它是一個函數。其實回調函數是十分有用的一種編程手段,假設用戶按要求定義一個函數A,然后當作參數傳給其它函數B,由這個函數B來調用用戶定義的函數A,這樣的函數A就是回調函數。在這里就是 SetTime 使用的 TimeProc 定義,因為用戶可以隨便定義它的名字所以稱之為點位符號。唯一的要求就是確保參數列表符合要求,因為主調函數 SetTimer 已經定義好怎么回調這個函數了。
如果程序足夠好,可以很好地處理完相關的工作,或者機器跢(沒想還有這字,本來想打足夠二字)快,就可以考慮使用 Sleep() 或 SleepEx() 來讓程序進行等待狀態了,這樣可以避免程序空跑,浪費電力資源!下開始貼代碼:
/** Timer in console Demo by Jimbowhy* compiler:g++ -o setTimer setTimer.cpp -lwinmm && settimer*/#include <iostream> #include <cstdio> #include <windows.h> #include <mmsystem.h>#pragma comment( lib, "Winmm.lib" )using namespace std;typedef HWND (WINAPI *TGetConsoleWindow)(); HWND GetConsoleWin() {HMODULE hKernel32 = GetModuleHandle("kernel32");TGetConsoleWindow f = (TGetConsoleWindow)GetProcAddress(hKernel32,"GetConsoleWindow");return f(); }void CALLBACK TimerProc( HWND hwnd, UINT uMsg, UINT id, DWORD ms ) {PostMessage( NULL, WM_USER+3, 9, 8 );cout << "TimeProc callback" << endl; }void CALLBACK TimeSetProc( UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2 ) {static int count;if(count++>9) return;PostMessage( NULL, WM_USER+2, 9, 8 );cout << "TimeSet callback \t" << hex << dwUser << endl; }ULONG WINAPI thread( PVOID pv ){MSG msg;cout << "thread run " << *(int*)pv << endl;;HWND hwnd = GetConsoleWin();SetTimer( hwnd, 0, 500, &TimerProc ); // GetLastError 5 Access is deniedcout << "GetLastError " << GetLastError() << endl;unsigned int timer = SetTimer(0, 0, 500, &TimerProc);unsigned int tmm = SetTimer(0, 0, 500, NULL); // use WM_TIMER//KillTimer(timer);MMRESULT mm = timeSetEvent( 10,0,TimeSetProc,0xABCD,TIME_PERIODIC);while(GetMessage(&msg,NULL,0,0)){switch( msg.message ){case WM_TIMER: cout << "WM_TIMER\t"; break;case WM_USER+3: cout << "WM_USER+3\t"; break;case WM_USER+2: cout << "WM_USER+2\t"; break;}DispatchMessage(&msg);} }int main() { DWORD tid;cout<<"Use Timers in console ";int p = 999;HANDLE h = CreateThread( NULL, 0, thread, &p, 0, &tid );DWORD w = WaitForSingleObject(h,1000*3); switch(w) { case WAIT_ABANDONED: printf("WaitForSingleObject => WAIT_ABANDONED\n"); break; case WAIT_OBJECT_0: printf("WaitForSingleObject => WAIT_OBJECT_0\n"); break; case WAIT_TIMEOUT: printf("WaitForSingleObject => WAIT_TIMEOUT\n"); break; } CloseHandle(h);int periodic = 1000;LARGE_INTEGER li;li.QuadPart = -30000000; // 3s, in 100 nanosecond intervalsHANDLE timer = CreateWaitableTimer( NULL,true, "Waitable" );SetWaitableTimer( timer,&li,periodic,NULL,NULL,true );if( WaitForSingleObject( timer, INFINITE) == WAIT_OBJECT_0 ){cout << "WaitForSingleObject Final" << endl;}CloseHandle(timer);return 0; }既然說到動畫了,搞了這么多的定時器,也沒點真家伙出來怕是對不住觀眾的。記得之前玩 TVPaint Animation Pro、Toon Boom Studio、Toon Boom Storyboard Pro 3.0、Toon Boom Pencil 等等動畫軟件的時候有些素材,待我翻箱倒柜一翻找找。就是它了,Animation Charts 1-2-3-4 for Toon Boom Studio。來看看要用到的素材,對了就只有四張圖,這四張圖就是上面提到的關鍵幀 Pose to pose:
制作單獨執行文件
資源文件是Win32程序的一個重要組成,如果要開發Win32程序,不使用資源文件就不很有效地提高文件資源的使用效率。通過資源文件,可以程序使用到的任何數據打包到 exe 程序文件中得到一個獨立運行的程序,也可以打包資源到一個DLL文件中,這樣可以很容易地實現程序的國際,加載不同的DLL就可以變換程序的不同語言運行狀態。一個資源文件像代碼文件一樣,注解都和C語言一樣,也可以使用 include 包含頭文件。#define #if #ifdef 這些語法都是C語言風格的。資源文件類型是 .rc,經過編譯后得到二進制格式 .res 文件,這種格式可以通過 link.exe 鏈接命令和主程序鏈接成為最終的 exe 程序文件。
在資源文件中,定義一些數據,程序使用的字符串,菜單及快捷鍵 ACCELERATORS,數據可以有各種類型,圖標等等自然是基本功能,二進制數據的文件也可以使用,像 WAV BMP 等等。
Resource Topic Accelerator table Keyboard Accelerators Bitmap Bitmaps Cursor Cursors Dialog box Dialog Boxes Enhanced metafile Metafiles Font Fonts and Text Icon Icons Menu Menus Message-table entry Your message-compiler documentation String-table entry Strings Version information Version Information即使資源經過編譯后成為 exe 文件的一部分,但使用 UpdateResource() 函數還是可以修改資源的。通過 FindResource() LoadResource() 就可以加載資源數據,玩VB還可以通過 LoadResData() 加載資源文件,如聲波文件,然后可以通過 sndPlaySoundA() 來播放。當年開始學編程,就是用VB玩資源文件給玩壞的,忽略了基本原理的學習,荒廢大量時間。在MSDN的子集合 Platform SDK => User Interface Services => Resources 中有完整的參考文檔,包括資源編譯命令 rc.exe 的文檔,其中還一個更新資源的示例。
要使用資源文件開發程序時,像我一樣如果不使用IDE該怎么辦呢?好像不太可能的事吧,不用IDE不是自找麻煩?其實不然,不用IED自用不用的道理,只要學會任意一個MAKE自動化編譯工具,其實有IDE和沒有都不是問題。編譯工程只是一個MAKE編譯命令而已,簡單的不得了的事,為什么非要IDE呢。
Function Action To remove resource FormatMessage Loads message-table entry No action needed LoadAccelerators Loads an accelerator table DestroyAcceleratorTable LoadBitmap Loads a bitmap resource DeleteObject LoadCursor Loads a cursor resource DestroyCursor LoadIcon Loads an icon resource DestroyIcon LoadMenu Loads a menu resource DestroyMenu LoadString Loads a string resource No action needed一般情況下使用 STRINGTALBE 來定義字符串內容就很好用了,還可以結合語言指令 LANGUAGE 來定義多種語言的文字內容定義。而 MESSAGETABLE 則是個增強,它可以結合 FormatMessage() 函數來格式字符串,可以給它指定語言ID來實現多國語言。FormatMessage 這個函數也用來格式化程序的錯誤信息,可以配合 GetLastError() 使用。但它一個主要的功能是事件日志,ReportEvent() 用來上報日志, RegisterEventSource() 函數可以注冊包含消息表定義的程序。當然自行實現一個類似于 gettext 一樣的國際化模塊并不是什么難事,但已經有了而且又不是難用那就用吧。資源文件這種技術在 Visual C++ 2.0 時代就已經成熟,而且一直使用到現在,事實驗證了這是成功的技術。
MESSAGETABLE 使用獨立的編譯命令 mc.exe,使用 Message Compiler 可以很方便地編譯多字節符字符集內容,它可以使用獨立文本文件,mc 將文件編譯為二進制格式 .h,.bin 文件,并生成在 rc 文件來包含引用。每個mc文件可以兩個部分組成 Header Section 和 Message Definitions,頭部可以設置一些基本信息,如語言分類,每一種語言經過編譯后會形成一個對應的二進制文件,下面的例子就會生成 Resource_ENU.bin 和 Resource_GER.bin 兩種語言對應的二進制數據文件。消息定義區則定義特定消息相關屬性,如消息ID,文字內容和語言歸屬。Message Table 主要功能在于日志消息處理,所有消息都會帶上消息ID,而程序使用時關心的是ID符號而不ID的具體數值。每個消息可以定義多個不同語言的內容,這一點就可以利用來實現程序的國際化。定義字符串內容時使用最基本的鍵值對形式:
keyword=value ;commentskeyword 不區分大小寫,= 等號兩邊的空格會忽略。value 可以是雙引號包括的字符串,也可以是數值,可以使用C語言的數值常量形式定義,可以使用 // 和 ; 兩種注解格式。以下是一個mc文件樣板:
;#ifndef __MESSAGES_H__ ;#define __MESSAGES_H__; ;// Header Section ;// LanguageNames =(English = 0x0409:Resource_ENUChinese = 0x0804:Resource_CHSTaiwan = 0x0404:Resource_CHTGerman = 0x0407:Resource_GER); ;// Message Definitions - Eventlog categories ;// MessageId = 1 SymbolicName = CATEGORY_ONE Severity = SuccessLanguage = English First category event . Language = German Ereignis erster Kategorie . Language = Chinese 事件消息 . Language = Taiwan 事件消息 .MessageId = +1 SymbolicName = CATEGORY_TWO Severity = SuccessLanguage = English Second category event . Language = German Ereignis zweiter Kategorie . Language = Chinese 另一事件消息 . Language = Taiwan 另一事件消息 .; ;// Message Definitions - Events ;//MessageId = +1 SymbolicName = EVENT_STARTED_BYLanguage = English The app %1 has been started by user %2 . Language = German Der Benutzer %2 konnte das Programm %1 erfolgreich starten . Language = Chinese 程序 %1 已經由 %2 用戶啟動 . Language = Taiwan 程序 %1 已經由 %2 用戶啟動 .MessageId = +1 SymbolicName = EVENT_BACKUPLanguage = English You should backup your data regulary! . Language = German Sie sollten Ihre Daten regelm??ig sichern! . Language = Chinese 是時候做備份了! . Language = Taiwan 是時候做備份了! .; ;#endif //__MESSAGES_H__ ;注意 mc 文件中各種語言的內容的定義基本格式,使用一個圓點來結束:
Language=language_name messagetext .為了形像地理解不同語言的資源定義是如果組織到程序內部的,可以使用 PE Explorer 來瀏覽程序文件的資源內容。在資源文件中定義資源類型時,需要用到系統定義的常量,比如說虛擬快捷鍵的定義 VK_F1,它就在 WinUser.h 中定義,需要引用它。MFC的頭文件 afxres.h 也引用了它,可以直接使用。以下是一個資源文件的示例,注意使用 Unicode 編碼保存,因為指定了代碼頁為 65001:
#include "res.h" #include "afxres.h"#include "resource.rc" MMT MESSAGETABLE resource.mcMoveCursor CURSOR movetab.cur WxIcon ICON wxwin.ico DING WAVE DISCARDABLE "pass.wav" 16 BITMAP mario16.bmp mario BITMAP mario.bmp #pragma code_page(65001) STRINGTABLE LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED {IDS_HELLO, "夢見你來了,還好嗎?"IDS_GOODBYE, "再見" }STRINGTABLE LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL {IDS_HELLO, "夢見你來了,還好嗎!"IDS_GOODBYE, "再見" } STRINGTABLE LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_UK {IDDSTAR, "Colourful day!" }STRINGTABLE LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US {IDDSTAR, "Colorful day!" }ShapesMenu MENU {POPUP "&Shape"{MENUITEM "&Rectangle", ID_RECTMENUITEM "&Triangle", ID_TRIANGLEMENUITEM "&Ellipse", ID_ELLIPSE} }1 ACCELERATORS {"^C", IDDCLEAR ; control C"K", IDDCLEAR ; shift K"k", IDDELLIPSE, ALT ; alt k98, IDDRECT, ASCII ; b66, IDDSTAR, ASCII ; B (shift b)"g", IDDRECT ; g"G", IDDSTAR ; G (shift G)VK_F1, IDDCLEAR, VIRTKEY ; F1VK_F1, IDDSTAR, CONTROL, VIRTKEY ; control F1VK_F1, IDDELLIPSE, SHIFT, VIRTKEY ; shift F1VK_F1, IDDRECT, ALT, VIRTKEY ; alt F1VK_F2, IDDCLEAR, ALT, SHIFT, VIRTKEY ; alt shift F2VK_F2, IDDSTAR, CONTROL, SHIFT, VIRTKEY ; ctrl shift F2VK_F2, IDDRECT, ALT, CONTROL, VIRTKEY ; alt control F2 }資源文件的ID定義全部在頭文件中定義,主程序中需要引用它:
#ifndef _res_h_ #define _res_h_#define IDS_HELLO 1 #define IDS_GOODBYE 2 #define ID_RECT 3 #define ID_TRIANGLE 4 #define ID_ELLIPSE 5 #define IDDELLIPSE 8 #define IDDRECT 9 #define IDDCLEAR 13 #define IDDSTAR 14#endif上面兩個文件名為 res.rc 和 resource.mc,使用這些資源文件時,就可以通過以下兩條指令命令來生成二進制文件:
mc resource rc res注意,文件名不要相同,不然 mc.exe 會覆蓋掉而且不給你提示信息!編譯得到資源文件后,再和主程序鏈接就可以為程序使用了。
在資源文件中,#pragma 是用來告訴 rc.exe 命令當前的內容是什么代碼頁編碼的。如果 rc 文件是UTF8編碼保存的,你卻用 #pragma code_page(950) 來告訴 rc.exe 是繁體中文編碼就不對了,它令會真的將內容看作是 BIG5 來處理的。假設系統當前的默認代碼頁是GBK,rc.exe 就會進行 BIG5到GBK 的編碼轉換,所以最后程序運行就會亂碼了。
#ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32作為一個GNU飯,怎么用TDM-GCC來編譯資源文件呢,GNU Binary Utility提供了 windres.exe 這個工具,注意是 winDres.exe。不過至于 mc 資源,還是算了吧,因為它是二進制編碼,直接用mc.exe就可以了,mc.exe 直接支持 Unicode,在編譯選項中可以通過 -u 和 -U 來啟用Unicode文件讀入和輸出,實測中 mc.exe 由于支持 Unicode,所以它和 FormatMessage() 結合時對多語言的支持好過 rc.exe 千百萬。而且,通過 LoadString() 方法來加載多語言內容并不是特別靠譜的做法。另外測試中還發現 rc.exe在處理 UTF8 編碼文件時不行,根本就直接忽略掉資源文件了。指定代碼頁為 65001 也沒有報錯,說明是支持65001的,幾次嘗試后發現,這個代碼頁竟然需要對應UNICODE,特別說明一下當前用的是VC6自帶的。
windres 對資源文件的注解不能用 ; 號,上面的示例需要修改一下才能通過 windres.exe 編譯。如果在繁體中文環境下使用 rc.exe 編譯含有GBK的文件時,不能兼容BIG5的簡體字將會被破壞,即使指定代碼頁沒用。因為rc.exe本身不能處理各種字符集,需要依賴系統,而Windows系統的設計上是不許可BIG5和GBK同時使用的。倒是使用GBK時情況會更好,因為GBK兼容繁體。如果使用 windres.exe 則可以通過以下參數來設置不同的編譯語言,語言參數中指定用16進制表示的本地化語言代碼,如中文簡、繁體對應 0x0804、0x0404:
-c --codepage=<codepage> Specify default codepage -l --language=<val> Set language when reading rc file使用 windres.exe 編譯命令使用如下:
mc.exe -u resource.mc windres.exe -c 936 -l 0804 -J rc -O coff -i res.rc -o res.obj不過,在我把系統的設置為繁體中文時,再編譯工程發現,所有不兼容的簡體字都被換成了?號,而繁體字符正常得很。windres.exe 不支持 Unicode 但支持 UTF8,而且是正確對應代碼頁 65001。
在使用資源時,如位圖,可以直接通過ID名,注意沒有數值常量定義,使用LoadBitmap(NULL,”mario”)來加載,也可以使用ID常量來加載,使用宏 MAKEINTRESOURCE 可以將參數的最高16-bit設置為0,低16-bit設置為ID值,其實直接顯式將ID數值轉型為(char*)就可以了,或者將ID數值轉換成”#ID”這種格式的字符串也可以。微軟這個用法確實算是奇葩的,因為參數是指向字符串的指針類型,而 LoadBitmap() 卻可以將它當成資源ID值來用。測試中不給 LoadBitmap() 傳入進程句柄時,總是返回1814,說資源找不到,只好使用 GetModuleHandle(NULL) 來獲取當前進行句柄給它。
以下是程序演示環節:
/** Resource Demo by Jimbowhy, compile it with TDM-GCC:g++ -o resource resource.cpp resource.res -lgdi32 -lwinmm && resource*/#include <cstring> #include <cstdlib> #include <cstdio> #include <windows.h> #include <mmsystem.h> #include "res.h"typedef HWND (WINAPI *GCW)(); HWND GetConsoleWin() {HMODULE hKernel32 = GetModuleHandle("kernel32");GCW f = (GCW)GetProcAddress(hKernel32,"GetConsoleWindow");return f(); }HWND wconsole = GetConsoleWin(); void CALLBACK TimeSetProc( UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2 ) {static int count;char title[64];POINT p;GetCursorPos(&p);ScreenToClient(wconsole,&p);sprintf( title, "TimeSet %d (%d,%d) %x", ++count,p.x,p.y,dwUser );SetConsoleTitle( title );HDC idc = GetDC( wconsole ); //GetConsoleWindow()HICON wx = *((HICON*) dwUser);DrawIcon( idc, p.x, p.y, wx ); }void Loop() {HANDLE hai = GetStdHandle( STD_INPUT_HANDLE );if( !hai ) {printf("GetStdHandle(%d)!\n", GetLastError() );return ;}SetConsoleMode( hai, ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT );INPUT_RECORD buffer[4];DWORD numberRead;printf("------=========Press ESC/x to quit=========-------\n");while( true ){ReadConsoleInput( hai, buffer, 2, &numberRead);for(int i=0; i<numberRead; i++){KEY_EVENT_RECORD &ike = buffer[i].Event.KeyEvent;char c = ike.uChar.AsciiChar;switch( buffer[i].EventType ){case KEY_EVENT:if( c=='x' || ike.wVirtualKeyCode==VK_ESCAPE ) return ;break;}}} }int main(){// --------===========--------- use resource stringchar rs[1024];LoadString( NULL,IDS_HELLO,rs,1024 );printf( "greeting from resource: %s \n", rs );// --------===========--------- use resource waveprintf( "wave play \n", rs );HRSRC ding_inf = FindResource( NULL, "DING", "WAVE");if( !ding_inf ) printf(" error FindResource %d\n", GetLastError() );HGLOBAL ding = LoadResource( NULL,ding_inf );//sndPlaySound( (char*)ding, SND_MEMORY | SND_SYNC | SND_NODEFAULT ); // this OK!if( !ding ) printf(" error LoadResource %d\n", GetLastError() );HANDLE wave = LockResource( ding );if( !wave ) printf(" error LockResource %d\n", GetLastError() );sndPlaySound( (char*)wave, SND_MEMORY | SND_SYNC | SND_NODEFAULT );UnlockResource( wave );FreeResource( ding );// --------===========--------- use resource iconHICON wx = LoadIcon( GetModuleHandle(NULL),"WxIcon" );if( !wx ) printf(" error LoadIcon %d\n", GetLastError() );MMRESULT mm = timeSetEvent( 1000/12,0,TimeSetProc,(DWORD)&wx,TIME_PERIODIC);// --------===========--------- use resource bitmapprintf( "bitmap show \n", rs );HBITMAP mario = LoadBitmap( GetModuleHandle(NULL),(char*)(16) );if( !mario ) printf(" error LoadBitmap %d\n", GetLastError() );HANDLE hm = LoadImage(NULL, "mario256.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);//mario = (HBITMAP)hm;HDC hdc = GetDC( GetConsoleWin() ); //GetConsoleWindow()HDC mask = CreateCompatibleDC(hdc);HDC draw = CreateCompatibleDC(hdc);//int blue = RGB(0x60,0x80,0xc0);int blue = RGB(0x50,0x75,0xe0);int white = RGB(0xff,0xff,0xff);int black = RGB(0x00,0x00,0x00);int sw = GetSystemMetrics(SM_CXSCREEN);//GetDeviceCaps( mask,ASPECTX );int sh = GetSystemMetrics(SM_CYSCREEN);//GetDeviceCaps( mask,ASPECTY );BITMAP bmp;GetObject( mario, sizeof(BITMAP), &bmp );int w = bmp.bmWidth, h = bmp.bmHeight;HBITMAP mono = CreateCompatibleBitmap( mask, w, h );SelectObject( mask,mono );SelectObject( draw,mario );printf(" Bitmap size (%d,%d) Screen size(%d,%d)\n", w, h, sw, sh);SetBkColor( draw, blue );BitBlt( mask, 0, 0, w, h, draw, 0, 0, SRCCOPY );BitBlt( hdc, w, 0, w, h, mask, 0, 0, SRCAND );BitBlt( draw, 0, 0, w, h, mask, 0, 0, 0x00220326); // Color-keyingBitBlt( hdc, w, 0, w, h, draw, 0, 0, SRCPAINT);/*BitBlt( hdc, 0*w, 0, w, h, draw, 0, 0, SRCCOPY );BitBlt( mask, 0, 0, w, h, draw, 0, 0, SRCCOPY );BitBlt( hdc, 4*w, 0, w, h, mask, 0, 0, SRCAND);BitBlt( hdc, 1*w, 0, w, h, mask, 0, 0, SRCCOPY );BitBlt( mask, 0, 0, w, h, mask, 0, 0, NOTSRCCOPY );BitBlt( hdc, 2*w, 0, w, h, mask, 0, 0, SRCCOPY );//SetTextColor( draw, white );SetBkColor( draw, white );BitBlt( draw, 0, 0, w, h, mask, 0, 0, SRCAND );BitBlt( hdc, 3*w, 0, w, h, draw, 0, 0, SRCCOPY );BitBlt( hdc, 4*w, 0, w, h, draw, 0, 0, SRCPAINT);*/DeleteDC(hdc);DeleteDC(mask);DeleteDC(draw);DeleteObject(mario);DeleteObject(mono);Loop();return 0; }內容國際化
國際化問題簡單來講,就是程序適應不同字符編碼方案,不同語言習慣的問題。比如說貨幣符號,人民幣是¥,美元是$。時間格式顯示也是一個方面,不同的地區有不同的使用習慣,通過 GetTimeFormat() 可以獲取系統設置的格式字符串。一個國際化的程序在不同的系統區域設置具有適應能力,而國際化更見的實現是字符的國際化,允許用戶設置使用什么語言。因此,使用資源文件就是一種非常好的辦法,Windows系統也集成了資源文件的API。通常程序需要為不同的語言生成不同的資源文件,編譯成為一個個DLL資源文件,然后程序通過 LoadLibrary() 來加載。通過資源API函數如 LoadAccelerators(), LoadBitmap(), LoadCursor(), LoadIcon(), LoadMenu() 就可以訪問到指定的語言的資源數據。
說到國際化,就必須要講一下中的編碼問題,中文編碼是全世界所有編碼中最巨大的,因為符號個個都不同,第個漢字都要占一個編碼。忽略不常用的編碼方案,最早標準的中文編碼是GB2312,它的代碼頁對應 20936,這個方案太簡單了,只包含了幾千漢字,連有些日常使用的字都沒有定義,根本不夠用。因此后來發展出來一個GBK,即代碼頁 936 所對應的中文編碼方案,它兼容了GB2312的所有字符,并增加了許多常用和不常用的漢字,字符數量達到萬數級別。這也是Windows系統支持最好的簡體中文編碼方案,繁體方案則主要是BIG5,它不兼容簡體漢字,代碼頁為 950。對于后來的漢字編碼超集 GB18030,是不完整支持的,這個方案有單字節、雙字節、四字節幾種字符定義,Windows 只能通過雙字節的Unicode兼容其中一部分字符。如果在rc文件中使用 54936 這個代碼頁,編譯結果是不對的:
#pragma code_page(54936)C語言的本地化庫 locale.h 函數 setlocale(),它可以用來改變程序的使用的語言或代碼頁,如果系統支持,則可以通過它來讓程序運行在指定的語言和代碼頁環境中:
setlocale( LC_ALL, "C" );參數 LC_ALL 是指所有內容都要實現本地化,包括日期時間、貨幣符號、相關庫函數等等。第二個參考是本地化的國別語言和代碼頁設置,C語言默認值為“C”,它的格式如下:
locale :: "lang[_country[.code_page]]"國別語言的定義是規范的,有簡寫和全稱兩種形式,例如德國就可以表示為 “deu”,”germany”,中國為 “china”, “chn”。而德語默認有 “deu”,”german”,還有瑞士地區的德語 “des”,”german-swiss”。簡體中文有 “chinese”,”chinese-simplified” or “chs”,繁體中文為 “chinese-traditional”,”cht”。可以參考MSDN的 Country/Region Strings 和 Language Strings 相關內容,以下是一些例子:
setlocale( LC_ALL, "English" ); setlocale( LC_ALL, ".1252" ); setlocale( LC_ALL, "English_United States.1252"); setlocale( LC_ALL, "French_Canada.1252" ); setlocale( LC_ALL, "French_Canada.ACP" ); setlocale( LC_ALL, "French_Canada.OCP" ); setlocale( LC_ALL, "German");關于代碼頁,在99OCT版的MSDN上有一本書《Developing International Software for Windows 95 and Windows NT》,里面有大量關于國際化的內容,這本書是單獨的一個 DEVINTL.CHM 文件,將近50MB,可謂內容豐富啊。本書Amazon有售只要 $1.97 刀,寶啊!關于作者 Kano 女士:
Nadine Kano joined Microsoft Corporation in 1989 after graduating from Princeton University with a degree in computer science engineering. She worked for three years as the international developer responsible for localized editions of Microsoft Word for Windows. In 1993 she joined the Developer Relations Group as a member of the Globalization Team. Nadine regularly publishes articles in the Microsoft Developer Network News on software internationalization and travels around the world giving lectures on internationalization techniques. She lives in Palo Alto, California.
以下這份表是 Windows 95 對各種代碼的支持情況,1200 在后來的XP等系統中支持:
Code Page Name Windows 95 Code Page Name Windows 95 1200 Unicode (BMP of ISO 10646) O 862 Hebrew X 1250 Windows 3.1 Eastern European X 863 MS-DOS Canadian French X 1251 Windows 3.1 Cyrillic X 864 Arabic X 1252 Windows 3.1 US (ANSI) X 865 MS-DOS Nordic X 1253 Windows 3.1 Greek X 866 MS-DOS Russian X 1254 Windows 3.1 Turkish X 869 IBM Modern Greek X 1255 Hebrew X 874 Thai X 1256 Arabic X 932 Japanese X 1257 Baltic X 936 Chinese X 1361 Korean (Johab) X 949 Korean X 437 MS-DOS United States X 950 Chinese (Hong Kong SAR, Taiwan) X 708 Arabic (ASMO 708) X 10000 Macintosh Roman X 709 Arabic (ASMO 449+, BCON V4) X 10001 Macintosh Japanese X 710 Arabic (Transparent Arabic) X 10006 Macintosh Greek 1 X 720 Arabic (Transparent ASMO) X 10007 Macintosh Cyrillic X 737 Greek (formerly 437G) X 10029 Macintosh Latin 2 X 775 Baltic X 10079 Macintosh Icelandic X 850 MS-DOS Multilingual (Latin X 10081 Macintosh Turkish X 852 MS-DOS Slavic (Latin 2) X 037 EBCDIC O 855 IBM Cyrillic X 500 EBCDIC 500V1 O 857 IBM Turkish X 1026 EBCDIC O 860 MS-DOS Portuguese X 875 EBCDIC O 861 MS-DOS Icelandic X系統默認狀態下具有一個代碼頁稱為默認代碼頁,這是對于非 Unicode 程序來說的,比如控制臺就是,通過函數 GetACP() 可以獲取到。對于DOS程序,還有一個稱為OEM代碼頁的,也即是指代那些將一個字節256個值都用來編碼的字符集,OEM 全稱 Original Equipment Manufacturer,可以通過 GetOEMCP()。這兩個函數差別在于兩種環境,DOS 和 Windows 使用的代碼頁集合的不同,例如俄語 Cyrillic 的OEM代碼頁為 855,而在 Windows 下使用的代碼頁是 1251。以下是一組和語言地區設置信息有關的API及其輸出,系統是 Windows 7 簡體中文版,但是設置了非Unicode程序語言和地區設置成臺灣、繁體:
GetACP(950) GetOEMCP(950)GetThreadLocale(0x804)GetSystemDefaultLangID(0x404)GetSystemDefaultLCID(0x404) GetSystemDefaultUILanguage(0x804)GetUserDefaultLangID(0x804)GetUserDefaultLCID(0x804)GetUserDefaultUILanguage(0x804)如果將地區和語言中的格式設置為臺灣、繁體,則 GetThreadLocale()、GetUserDefaultLangID()、GetUserDefaultLCID() 就會改變輸出。位置設置則不會影響以上函數的輸出的,但和 GetUserGeoID()、SetUserGeoID() 有關。其中還有兩個函數需要辯解一下, GetSystemDefaultUILanguage()是安裝Windows選擇的語言,這個是不會變的,而GetSystemDefaultLangID()則是設置好系統默認語言,它和系統默認代碼頁直接相關,因此和 GetACP() 的輸出是相關的。
對于簡體中文Window一般都為 936,即默認代碼頁是GBK。由于GBK和BIG5 CP950 是不兼容的,所以在Windows系統中只能選擇其中一種,而且不能用 chcp 來選擇另一種。看到代碼頁就知道程序使用什么字符集了,但API可以看不懂什么是代碼頁,API是通過一組稱為本地化代碼 Locale Code 的數值約義來處理各種地區語言的。先來看下面兩組語言定義,第一組是一級語言分類,定義了中文 、英文、德文等等代碼。第二組稱為語言地區分支定義,例如中文就分為大陸地區的簡體中文和港臺地區的繁體中文。將這兩組數據合到一起就可以形成一個特定語言的標識碼,首先將二級代碼右移位10位,再與一級代碼相加。如簡體中文就是 0x02<<10+0x04=0x0802,換算為十進制數值就是 2052,同樣美式英文為 1033,英式英語為 2057 等等。這些代碼就是資源文件用來分類的依據,ID相同一個字符串在編譯時就會歸類到一個語言代碼分類下。如果用 Visual Stuido 查看資源,看到的就是 Chinese(P.R.C)、English(U.K)、English(U.S)等等字樣。先拋開實用性不講,個人覺得微軟這種操作法確實屬于找事做的那種,為了得到地區相關的本地化代碼,還要搞兩個宏來處理,本來就是邏輯運算可以做的事。
#define LANG_NEUTRAL 0x00 #define LANG_CHINESE 0x04 #define LANG_ENGLISH 0x09 #define LANG_DUTCH 0x13 #define LANG_BULGARIAN 0x02#define SUBLANG_NEUTRAL 0x00 #define SUBLANG_DEFAULT 0x01 #define SUBLANG_BULGARIAN_BULGARIA 0x01 #define SUBLANG_CHINESE_TRADITIONAL 0x01 #define SUBLANG_CHINESE_SIMPLIFIED 0x02 #define SUBLANG_DUTCH 0x01 #define SUBLANG_DUTCH_BELGIAN 0x02 #define SUBLANG_ENGLISH_US 0x01 #define SUBLANG_ENGLISH_UK 0x02#define MAKELANGID(p,s) ((((WORD)(s))<<10)|(WORD)(p)) #define MAKELCID(l,s) ((DWORD)((((DWORD)((WORD)(s)))<<16)|((DWORD)((WORD)(l)))))完整的本地化語言標識可以在 MSDN 上的 Platform SDK => Base Services => International Feeatures 中找到 Language Identifiers 部分,其中也包含了輸入法的相關API、消息內容,想要實現輸入法可以參考。本地默認代碼頁在整個資源系統中是非常重要的東西,API如何檢索到正確的資料數據就看它的了。API定位資源有許多邏輯條件,首先會定位一個特定的語言,然后再從里面查詢相應的ID定義。經過墨盒測試,主要條件有系統安裝時的語言、默認代碼頁、英文語言資源、語言中性 Neutral,其它條件。當前系統為 GBK,即默認語言代碼為 LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED,以STRINGTALBE中的字符串為例,使用 LoadString() 加載資源的優先順序如下:
- 一級語言和二及語言同為中性:LANG_NEUTRAL, SUBLANG_NEUTRAL,這種情況最優先,而且即這個語言找不到相應的ID定義,也不會采用其它語言定義的內容。可以定義多個這種STRINGTABLE,編譯時會合并。
 - 系統安裝語言,即 GetSystemDefaultUILanguage() 查詢到的語言,簡體中文系統為 936 對應 LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED,找不到ID定義時也不采用其它內容,注意和系統默認代碼頁沒關系;
 - 其次是沒有指明語言的定義內容和 LANG_ENGLISH, SUBLANG_ENGLISH_US 可以并存。如果這兩情況有定義,那么即找不到相應的ID定義,也不會采用其它語言定義的內容;
 - LANG_ENGLISH, SUBLANG_NEUTRAL 語言定義,并且不采用其它定義;
 - LANG_ENGLISH, SUBLANG_ENGLISH_UK和LANG_BULGARIAN, SUBLANG_NEUTRAL,
 - LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL、LANG_ENGLISH, SUBLANG_ENGLISH_UK、LANG_BULGARIAN, SUBLANG_NEUTRAL,即其它語言組合,這種情況下就可以開始使用 SetThreadLocale() 函數來指定優先那一個。注意C語言的庫函數 setlocale() 不起作用。
 
新的系統API中有 SetProcessPreferredUILanguages()、SetThreadPreferredUILanguages()、SetThreadUILanguage(),它們可以修改資源在檢索時的條件,優先順序為 Thread > Process > User UI > System UI。
Thread preferred UI languages and its neutral form. Process preferred UI languages and its neutral form. User UI language and its neutral form. System UI language and its neutral form. System default UI language and its neutral form.不要試圖使用 SetThreadLocale() 來嘗試讓 LoadString() 等等函數選擇不同語言的資源,在這方面它并不是那么好用。而 FormatMessage() 和 FindResourceEx() 則可以指定語言來檢索資源。如果給 FormatMessage() 指定了語言ID,那么它就只會按指定的語言的資源定義中查找。如果沒有指定語言ID,那么它會按以下邏輯來檢索:
- Language neutral
 - Thread LANGID, based on the thread’s locale value
 - User default LANGID, based on the user’s default locale value
 - System default LANGID, based on the system default locale value US English
 - returns any language message string that is present.
 - If that fails, it returns ERROR_RESOURCE_LANG_NOT_FOUND.
 
對于 FindResourceEx(),MSDN文檔指出,當使用它來加載二進制數據,就一定需要配合使用 LoadResource() LockResource(),用它來獲取字符串數據也是可以的,但會多出來一個二進制頭部。想要獲取指定語言的資源數據,需要獲取通過 LANGUAGE 語句定義的資源,就必需通過 FindResourceEx()函數來定位,它會調用NTDLL的LdrFindResource()來定位資源。由LoadResource()加載的字符內容是相當的難搞,微軟官網上一段代碼定義了一個函數 GetStringFromStringTable() 處理。基本邏輯是處理前導的頭部,通過 LoadResource() 得到的指針類型是 (wchar_t *),即雙指針。
如果想要深入資源文件的工作原理,可以去分解PE文件如何組織資源文件,MSDN中的 Specifications => Platforms => Microsoft Portable Executable and Common Object File Format Specification 就是關于PE格式的文檔。
這里來了解字各種語言符串定義是如何編譯到程序文件中保存的。在資源文件中,每個字符串都有一個ID值,這些值是按每16個連續的號碼進行分組,例如0-15這16個ID歸為第一組,16-31歸為第二組,依此推理。每一組的ID可以用來在各種語言中定義關聯一個字符串內容。要計算特定的ID值屬于那一組,只要將這個ID整除16再加1就可以得到組號。這定義資源時,即某個組段的ID只有一個有定義內容,在編譯時這一個資源定義也要被分成一組。舉例來說,0-31這16個ID中,只使用了15、16這兩個ID來定義字符串,那么編譯后將生成兩組內容,即使15、16是兩個結連的ID值,但是15\16+1=1,屬于第一組,而16\16+1=2,屬于第二組,所以它們是兩個不同的分組。當你理解到這些的時時候,再來看看MSDN文檔 FindResourceEx() 的說明,其中 Remarks 最后一段就是最重要的內容:
String resources are stored in sections of up to 16 strings per section. The strings in each section are stored as a sequence of counted (not null-terminated) Unicode strings. The LoadString function will extract the string resource from its corresponding section.
上面這段話間意思是說,資源中的字符串像這樣存儲的:
wchar_t * array = { len,L"abcd", len,L"xyz" ...}在資源文件 .rc 中可以用 Name 或者數值ID 來定義不同的數據類型,在PE的資源分區也有不同的數據保存方式。字符串數據就是 Resource Directory String,即上面構造的這個C語言定義,每一條字符串都是UNICODE編碼的,在字符串開始的兩個字節指示了長度,然后跟著字符串內容,通過 LoadResource() 方法加載字符串資源時返回的就是這個目錄條目的開始地址。
一個典型的PE格式文件,這里指 .exe 可執行程序文件,大體可以分為文件頭和分區頭信息,分區 Section 是PE文件的基本組織結構。而資源編譯后就在 .rsrc Section 中保存,按 Type、Name(ID)、Language 構成三層的有序二叉樹數據結構,這三個屬性都是用來定位資源具體位置的關鍵信息,它們就是當成三個ID來使用的。這個二叉樹中的節點是一個的資源目錄 Resource Directory,目錄下還有細分條目指向下一層資源目錄,最后一層就定位了具體的資源位置。來看看資源目錄和目錄下的條目數據結構:
Off. Size Field Description 0 4 Characteristics Resource flags, reserved, currently set to zero. 4 4 Time/Date Stamp Time the resource data was created. 8 2 Major Version Major version number, set by the user. 10 2 Minor Version Minor version number. 12 2 Number of Named Number of directory entries that use strings to identifiers. 14 2 Number of IDed Number of directory entries that use numeric identifiers.Off. Size Field Description 0 4 Name RVA Address of string that gives the Type/Name/Language identifier. 0 4 Integer ID 32-bit integer that identifies Type/Name/Language. 4 4 Data Entry RVA High bit 0. Address of a Resource Data Entry (a leaf). 4 4 Subdirectory RVA High bit 1. Lower 31 bits are the address of another Resource Directory.注意目錄條目是8個字節的,分成兩個32-bit部分,前一部分可以表示 Name RVA 也可以表示 Integer ID。相對虛擬地址 Relative Virtual Address (RVA) 是指相對基址的一個偏移地址值,比如說 PE 文件加載到內存地址 0x10000 開始的位置,那么如果以這個PE的為基址,而PE中的一個資源目錄開始位置在 0x10100,那么RVA就是 0x100。
在資源目錄后面跟著和特定數據類型相關的數據,Resource Data Entry,即 Resource Data Description,用來描述資源實際數據的 RVA、數據大小和代碼頁的信息:
Off. Size Field Description 0 4 Data RVA Address of a unit of resource data in the Resource Data area. 4 4 Size Number bytes of the resource data at address [Data RVA]. 8 4 Codepage Code page used to decode code point values within the resource data. 12 4 Reserved Must be set to 0.下面構造的這個數據可以更形像地描述資源的數據結構,假設 .rc 文件中定義了以下一組數據:
TypeId# NameId# Language ID Data1 1 0 000100011 1 1 100100011 2 0 000100021 3 0 000100032 1 0 000200012 2 0 000200022 3 0 000200032 4 0 000200049 1 0 000900019 9 0 000900099 9 1 100900099 9 2 20090009通過編譯后生成的二進制資源內容對應如下:
Offset Data 0000: 00000000 00000000 00000000 00030000 (3 Type entries in this directory) 0010: 00000001 80000028 (TypeId #1, Subdirectory at offset 0x28)-------+ 0018: 00000002 80000050 (TypeId #2, Subdirectory at offset 0x50)-------|--+ 0020: 00000009 80000080 (TypeId #9, Subdirectory at offset 0x80)-------|--|--+ 0028: 00000000 00000000 00000000 00030000 (3 NameID entries for this) <--+ | | 0038: 00000001 800000A0 (NameId #1, Subdirectory at offset 0xA0)------+ | | 0040: 00000002 00000108 (NameId #2, data desc at offset 0x108) | | | 0048: 00000003 00000118 (NameId #3, data desc at offset 0x118) | | | 0050: 00000000 00000000 00000000 00040000 (4 NameID entries for this) <-|---+ | 0060: 00000001 00000128 (NameId #1, data desc at offset 0x128) | | 0068: 00000002 00000138 (NameId #2, data desc at offset 0x138) | | 0070: 00000003 00000148 (NameId #3, data desc at offset 0x148) | | 0078: 00000004 00000158 (NameId #4, data desc at offset 0x158) | | 0080: 00000000 00000000 00000000 00020000 (2 NameID entries for this) <-|------+ 0090: 00000001 00000168 (NameId #1, data desc at offset 0x168) | 0098: 00000009 800000C0 (NameId #9, Subdirectory at offset 0xC0)------|---+ 00A0: 00000000 00000000 00000000 00020000 (2 entries in this directory)<+ | 00B0: 00000000 000000E8 (Language ID 0, data desc at offset 0xE8 | 00B8: 00000001 000000F8 (Language ID 1, data desc at offset 0xF8 | 00C0: 00000000 00000000 00000000 00030000 (3 entries in this directory) <---+ 00D0: 00000001 00000178 (Language ID 0, data desc at offset 0x178 00D8: 00000001 00000188 (Language ID 1, data desc at offset 0x188 00E0: 00000001 00000198 (Language ID 2, data desc at offset 0x198[Resource Data Entry from here now] 00E8: 000001A8 00000004 00000000 00000000 (for TypeId #1, NameId #1,Language id #0) 00F8: 000001AC 00000004 00000000 00000000 (for TypeId #1, NameId #1,Language id #1) 0108: 000001B0 00000004 00000000 00000000 (for TypeId #1, NameId #2) 0118: 000001B4 00000004 00000000 00000000 (for TypeId #1, NameId #3) 0128: 000001B8 00000004 00000000 00000000 (for TypeId #2, NameId #1) 0138: 000001BC 00000004 00000000 00000000 (for TypeId #2, NameId #2) 0148: 000001C0 00000004 00000000 00000000 (for TypeId #2, NameId #3) 0158: 000001C4 00000004 00000000 00000000 (for TypeId #2, NameId #4) 0168: 000001C8 00000004 00000000 00000000 (for TypeId #9, NameId #1) 0178: 000001CC 00000004 00000000 00000000 (for TypeId #9, NameId #9,Language id #0) 0188: 000001D0 00000004 00000000 00000000 (for TypeId #9, NameId #9,Language id #1) 0198: 000001D4 00000004 00000000 00000000 (for TypeId #9, NameId #9,Language id #2)[The raw data from here now] 01A8: 00010001 10010001 00010002 00010003 01B8: 00020001 00020002 00020003 00020004 01C8: 00090001 00090009 10090009 20090009如果通過 FindResourceEx() 來加載字符串,像下面這樣是不會成功的了,即使成功返回了,也只有在ID是1的時候:
FindResourceEx( NULL,RT_STRING,ID,LANG_NEUTRAL );而正確的使用方法應該是下面這種方式,將字符串的組號作為參數傳入,雖然兩條語句都沒使用MAKEINTRESOURCE這個宏,但這不是重點:
FindResourceEx( NULL,RT_STRING,ID/16+1,LANG_NEUTRAL );對于這一點MSDN竟然沒有示例,只能說他們真會玩啊!FindResource() 也是,需要注意的還有一點,因為資源中的字符是UNICODE編碼,所以ASCII字符也是兩個字節編碼的,因此每個字符的高8位是一個”\0”,即ASCII字符的后一個字節就是C語言的字符串的終結標識,而事實上它不是。到這里,如果想要自己寫代碼處理UNICODE,那么就要有點《編程編碼》的功夫了。如果想省事,那么就用VC提供的 wsprintf() 和 sprintf() 兩個函數來轉換 UNICODE、ASCII,使用格式化字符 %S,注意是大S,不是小S,輸出時用 wprintf()。 WideCharToMultiByte() 和 WideCharToMultiByte() 可以用來在系統使用寬字符和其它多字節編碼進行互相轉換。在 WINNLS.H 頭文件中定義系統所支持的代碼預定義符號,如系統默認代碼頁 CP_ACP,還有 CP_UTF8 等。如果在控制臺下進行寬字符做MBS轉換時,需要使用 GetConsoleCP()、GetConsoleOutputCP() 這樣的API來獲控制臺設置的代碼,前面講控制臺的輸入輸出時已經提到,到這時幾乎想不起來了。在控制臺中,如果用戶通過CHCP設置了特定的代碼頁,程序就按控制臺設置來變動輸出內容的編碼。另一個不是方法的辦法就是通過 syste() 調用 chcp 命令來讀取設置值。
通過搜索頭文件還發現 GetCPInfoEx() 這個函數,我使用的99OCT版MSDN上資料也沒說它可以查詢 CP_THREAD_ACP,倒是官網上有說明,可以用來查詢系統預定義的、當前程序設置的代碼頁信息。這里要分清兩個概念,一個是控制臺,另一個是控制臺中運行的程序。這是兩個不同的概念,控制是一個程序的運行環境,看著控制臺運行的程序并在控制臺中輸出內容,會很容易讓人分不清這兩者的關系。如果通過 chcp 設置了65001及1251等等其它多個代碼頁來測試,試著查詢 CodePage 和 CodePageName 的內容,顯然結果不會像預期那樣。在我的中文版系統上,默認是 936 代碼頁,這個沒問題。但是當代碼被修改成其它的,比如UTF8,結果就不準確了,換其它的代碼頁也一樣,都會顯示為 1252 代碼頁。這個代碼頁是微軟為 Windows 設計的,參考了ANSI草案,后來發展成為 ISO 8859-1 所以稱為拉丁語 I:
936 (ANSI/OEM - 簡體中文 GBK) 1252 (ANSI - 拉丁語 I)因為通過 chcp 改變的是控制所使用的代碼頁,并沒有修改程序的默認代碼頁。倒是通過 SetThreadLocale( LOCALE_USER_DEFAULT ) 這樣的函數來改變當前程序的代碼頁設置,然后可以用 GetCPInfoEx() 檢測系統區域和語言的格式選擇中設置了什么語言。GetCPInfoEx() 這個函數會受到 SetThreadLocale() 的影響,但是C語言的庫 setlocale() 卻沒有影響,看來它只是在為C庫服務的。
綜合以上,如果要LoadString()這樣的API函數來實現程序的國際化還是比較蛋痛的選擇。況且,rc.exe 和 mc.exe 在多語言編譯上都有問題,這才是根本的問題。反倒是使用API來獲取當前的本地化信息很有用處,再結合自己開發的多語言支持,我是指不依靠系統 API,這種做法才比較靠譜,畢竟自己掌握著源代碼。對于系統的API,沒有幾個人會知道它們都在你的機器上干了些什么,經歷過3Q大戰的人會深有體會的吧,我是沒遇到過那樣一種壯烈的情境。以下上自己的 i18n.cpp 測試代碼,需要前面定義的資源文件:
/** i18n Demo by Jimbowhy, compile it with TDM-GCC:g++ -o i18n i18n.cpp resource.res -lgdi32 -lkernel32 -ladvapi32 -lwinmm && i18n*/#include <cstring> #include <cstdlib> #include <cstdio> #include <clocale> #include <windows.h> #include <winnls.h> #include <mmsystem.h> #include <wchar.h> #include <tchar.h> #include "res\res.h"UINT LoadMessage( DWORD id, DWORD lang_ID, PTSTR buf, UINT size, ...) {va_list args;va_start(args,size);return FormatMessage( FORMAT_MESSAGE_FROM_HMODULE,NULL,id, lang_ID, buf, size, &args);va_end( args ); }wchar_t * GetTableString(HMODULE h, UINT ID, DWORD wLanguage=LANG_NEUTRAL) {HRSRC rs = FindResourceEx( h,RT_STRING,MAKEINTRESOURCE(ID/16+1),wLanguage);if( !rs ) return NULL;HGLOBAL rc = LoadResource( h,rs );if( !rc ) return NULL;wchar_t * s = (wchar_t*) rc;int i = 0, j = 0, c = 0;for( i=0; i<=ID%16; i++ ) {if( *s ){s += c;c = *s; // element lengths++;}else s++;}if( c==0 ) return NULL;// The unicode string directory element.wchar_t *t = new wchar_t[ c*2 ]; // double size in case UTF8 3byte per MBS' charint size = (c*2)*sizeof(wchar_t);//wcsncpy( t, s, c );memset( t, 0, size );WideCharToMultiByte( CP_UTF8,0,s,c,(char *)t,size,NULL,NULL);printf("\tL:%2d UTF8:%s \n\t", c, t);for( j=0; j<c; j++) printf("0x%02x ", (unsigned char)((char*)t)[j] );memset( t, 0, size );WideCharToMultiByte( CP_THREAD_ACP,0,s,c,(char *)t,size,NULL,NULL); //CP_ACPprintf("\n\tL:%2d ACP:%s \n\t", c, t);for( j=0; j<c; j++) printf("0x%02x ", (unsigned char)((char*)t)[j] );printf("\n");return t; }int main(){printf("\n--------====== use resource string & i18n ======---------\n");DWORD DEU = MAKELANGID( LANG_GERMAN, SUBLANG_GERMAN );DWORD enNU = MAKELANGID( LANG_ENGLISH,SUBLANG_NEUTRAL );DWORD enUK = MAKELANGID( LANG_ENGLISH,SUBLANG_ENGLISH_UK );DWORD enUS = MAKELANGID( LANG_ENGLISH,SUBLANG_ENGLISH_US );DWORD zhCN = MAKELANGID( LANG_CHINESE,SUBLANG_CHINESE_SIMPLIFIED );DWORD zhTW = MAKELANGID( LANG_CHINESE,SUBLANG_CHINESE_TRADITIONAL );DWORD en_NU = MAKELCID( enNU, SORT_DEFAULT );DWORD en_UK = MAKELCID( enUK, SORT_DEFAULT );DWORD en_US = MAKELCID( enUS, SORT_DEFAULT );DWORD zh_CN = MAKELCID( zhCN, SORT_DEFAULT );DWORD zh_TW = MAKELCID( zhTW, SORT_DEFAULT );//setlocale( LC_ALL, "English_UK" );//setlocale( LC_ALL, "English_United States.1252" );//setlocale( LC_ALL, "chinese-simplified" );//setlocale( LC_ALL, "chinese-traditional" );//SetThreadLocale( LOCALE_SYSTEM_DEFAULT );//SetThreadLocale( LOCALE_USER_DEFAULT );SetThreadLocale( zh_CN );CPINFOEX cpi;GetCPInfoEx(CP_THREAD_ACP,0,&cpi);printf( "GetACP(%d) GetOEMCP(%d) \n", GetACP(), GetOEMCP() );printf( "GetCPInfoEx(%x) %s\n", cpi.CodePage, cpi.CodePageName );printf( " GetThreadLocale(0x%x)\n", GetThreadLocale() );printf( " GetSystemDefaultLangID(0x%x)\n", GetSystemDefaultLangID() );printf( " GetSystemDefaultLCID(0x%x)\n", GetSystemDefaultLCID() );//printf( "GetSystemDefaultUILanguage(0x%x)\n", GetSystemDefaultUILanguage() );printf( " GetUserDefaultLangID(0x%x)\n", GetUserDefaultLangID() );printf( " GetUserDefaultLCID(0x%x)\n", GetUserDefaultLCID() );//printf( " GetUserDefaultUILanguage(0x%x)\n", GetUserDefaultUILanguage() );char rs[1024];wchar_t *cs;HRSRC hrc = FindResource( NULL, MAKEINTRESOURCE(IDS_HELLO),RT_STRING );HGLOBAL hrs = LoadResource( NULL,hrc );cs = (WCHAR *)LockResource(hrs);printf(" Find: %x %d [%s]\n", hrc, cs, cs );hrc = FindResource( NULL, MAKEINTRESOURCE(IDDSTAR/16+1),RT_STRING );hrs = LoadResource( NULL,hrc );cs = (WCHAR *)LockResource(hrs);printf(" Find: %x %d [%s]\n", hrc, cs, cs );LoadString( NULL,IDS_HELLO,rs,1024 );printf( "LoadString: %s \n", rs );LoadString( NULL,IDDSTAR,rs,1024 );printf( "LoadString: %s \n", rs );cs = GetTableString( NULL,IDS_HELLO, LANG_NEUTRAL);printf("GetTableString Neutral: %s \n\n", cs);cs = GetTableString( NULL,IDS_HELLO, LANG_ENGLISH );printf("GetTableString English: %s \n\n", cs);cs = GetTableString( NULL,IDS_HELLO, enUK );printf(" GetTableString enUK: %s \n\n", cs);cs = GetTableString( NULL,IDS_HELLO, zhTW );printf(" GetTableString zhTW: %s \n\n", cs);cs = GetTableString( NULL,IDS_HELLO, zhCN );printf(" GetTableString zhCN: %s \n\n", cs);cs = GetTableString( NULL,IDS_HELLO, 0x0804 );printf(" GetTableString 0x0804: %s \n\n", cs);cs = GetTableString( NULL,IDDSTAR, LANG_NEUTRAL );printf("GetTableString Neutral: %s \n\n", cs);cs = GetTableString( NULL,IDDSTAR, LANG_ENGLISH );printf("GetTableString English: %s \n\n", cs);cs = GetTableString( NULL,IDDSTAR, enUK );printf(" GetTableString enUK: %s \n\n", cs);cs = GetTableString( NULL,IDDSTAR, zhCN );printf(" GetTableString zhCN: %s \n\n", cs);cs = GetTableString( NULL,IDDSTAR, zhTW );printf(" GetTableString zhTW: %s \n\n", cs);printf("\n--------====== use resource messagetable ======---------\n");char app[] = "Res.exe";char user[64];DWORD ulen = 64;GetUserName( user,&ulen );LoadMessage( EVENT_STARTED_BY, enUS, rs, 1024, app, user);printf("enUS: %s", rs);LoadMessage( EVENT_STARTED_BY, DEU, rs, 1024, app, user);printf("DEU: %s", rs);LoadMessage( EVENT_STARTED_BY, zhTW, rs, 1024, app, user);printf("zhTW: %s", rs);LoadMessage( EVENT_STARTED_BY, zhCN, rs, 1024, app, user);printf("zhCN: %s", rs);LoadMessage( EVENT_STARTED_BY, LANG_NEUTRAL, rs, 1024, app, user );printf("Neutral: %s" , rs);return 0; }至于編譯命令就直接給出 GNU Make 和 MS NMake 兩個平臺的自動編譯腳本:
# # GNU makefile demo by Jimbowhy @ 2016/3/18 14:56:39 # Usage: # mingw32-make BUILD=RELEASE mouse # mingw32-make BUILD=RELEASE all # mingw32-make allMC=mc.exe RC=windres.exeCC=g++ CFLAGS:=-g libs:=-lwinmm -lgdi32 -lkernel32ifeq "$(BUILD)" "RELEASE" CFLAGS:=-s -O3 endififeq "$(BUILD)" "DYNAMIC" CFLAGS:=-s -Wl,-Bdynamic endifCFLAGS:=$(CFLAGS) -I"." -L"C:\sdks\PSDK2k3SP1\Lib"all : bitmap mouse resource setTimerbitmap:$(CC) $(CFLAGS) -o bitmap bitmap.cpp $(LFLAGS) $(libs) mouse:$(CC) $(CFLAGS) -o mouse mouse.cpp $(LFLAGS) $(libs) resource:$(MC) -u -U -r .\res -h .\res res\resource.mc$(RC) -c=65001 -J rc -O coff -i res\res.rc -o res.obj$(CC) $(CFLAGS) -o resource resource.cpp res.obj $(libs) setTimer:$(CC) $(CFLAGS) -o setTimer setTimer.cpp $(LFLAGS) $(libs)clean:del *.obj# # Nmake makefile demo by Jimbowhy @ 2016/3/20 1:58:36 # Usage: # nmake BUILD=RELEASE mouse # nmake BUILD=RELEASE all # nmake allCC=cl -c /ZI /Yd /MLd /Od /D "DEBUG" /nologo -c -GX /D "_MBCS" CL=link /nologo MC=mc.exe RC=rc.exeCFLAGS=/I"C:\Program Files (x86)\Microsoft Visual Studio\VC98\Include" LFLAGS=/LIBPATH:"C:\Program Files (x86)\Microsoft Visual Studio\VC98\lib" libs=winmm.lib gdi32.lib user32.lib advapi32.lib kernel32.lib#CFLAGS=$(CFLAGS) /I"C:\sdks\PSDK2k3SP1\Include" #LFLAGS=$(LFLAGS) /LIBPATH:"C:\sdks\PSDK2k3SP1\Lib" !IF "$(BUILD)" == "RELEASE" CC=cl -c /D "NDEBUG" /nologo -GX /D "_MBCS" !ENDIFall : bitmap mouse resource setTimerbitmap:$(CC) $(CFLAGS) -o bitmap bitmap.cpp$(CL) $(LFLAGS) bitmap.obj $(libs) mouse:$(CC) $(CFLAGS) -o mouse mouse.cpp$(CL) $(LFLAGS) mouse.obj $(libs) record:$(CC) $(CFLAGS) -o record record.cpp$(CL) $(LFLAGS) record.obj $(libs) resource:$(MC) -u -U -r .\res -h .\res res\resource.mc$(RC) /w /fo .\res.obj res\res.rc$(CC) $(CFLAGS) -o resource resource.cpp$(CL) $(LFLAGS) resource.obj res.obj $(libs) setTimer:$(CC) $(CFLAGS) -o setTimer setTimer.cpp$(CL) $(LFLAGS) setTimer.obj $(libs)clean:del *.obj *.idb *.pdb *.res部分程序的測試輸出內容,系統為簡體中文,控制臺代碼頁設置為 936:
LoadString: 夢見你來了,還好嗎? LoadString: 《白日夢!》L:10 UTF8:姊﹁浣犳潵浜嗭紝榪樺ソ鍚楋紵0xe6 0xa2 0xa6 0xe8 0xa7 0x81 0xe4 0xbd 0xa0 0xe6L:10 ACP:夢見你來了,還好嗎?0xc3 0xce 0xbc 0xfb 0xc4 0xe3 0xc0 0xb4 0xc1 0xcb GetTableString Neutral: 夢見你來了,還好嗎?GetTableString English: (null)GetTableString enUK: (null)L:10 UTF8:澶㈣浣犱締浜嗭紝閭勫ソ鍡庯紒0xe5 0xa4 0xa2 0xe8 0xa6 0x8b 0xe4 0xbd 0xa0 0xe4L:10 ACP:夢見你來了,還好嗎!0x89 0xf4 0xd2 0x8a 0xc4 0xe3 0x81 0xed 0xc1 0xcbGetTableString zhTW: 夢見你來了,還好嗎!MSDN補充
說到MSDN,最早在 Visual Studio 5.0 即 VS97 的時候,它叫 InfoViewer,文件的格式是 IVT,目錄索引文件是 IVI。我能找到的是 Visual Studio 97 MSDN CD191。而國內第一個大量使用可能是1998隨中文版 Visual Studio 6.0 發行的,而每一個MSDN和VS卻不通過升級安裝來共享,反而是單獨安裝使用,F1功能也只響應特定的版本。這樣就好浪費磁盤空間,因為每個MSDN都是上GB的。其實 HTMLHelp,Windows 程序的的F1上下文幫助是通過 VSHELP.DLL、hhctrl.ocx 組件實現的,DLL位置 Common Files 目錄下,丟失它們會導致幫助內容無法顯示。通過 Sysinternals 的 Process Monitor 工具就可以追蹤到這些信息。在安裝MSDN時,會在注冊表上記錄MSDN的目錄COL文件位置:
Windows Registry Editor Version 5.00[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\HTML Help Collections\Developer Collections\0x0804] [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\HTML Help Collections\Developer Collections\0x0804\0x0358e0f00] "Filename"="C:\\Program Files (x86)\\Microsoft Visual Studio\\MSDN98\\98VS\\2052\\msdnvs98.col" @="MSDN Library - Visual Studio 6.0" "Full"=dword:00000001下面有各種語言的設置,其中簡體中文就是 0x0804,轉換成10進制就是 2052。至于為什么用這個代碼表示簡體中文,這是因為程序的國際化功能,不同的地區編排了一個本地化標識 locale identifier (LCID),它就像生活中使用的郵編一個道理。
zh-cn 0x0804 2052 中文 - 中華人民共和國 zh-hk 0x0C04 3076 中文 - 香港特別行政區 zh-sg 0x1004 4100 中文 - 新加坡 zh-tw 0x0404 1028 中文 - 臺灣地區 ja 0x0411 1041 日語 ko 0x0412 1042 朝鮮語 en-gb 0x0809 2057 英語 - 英國 en-us 0x0409 1033 英語 - 美國 es 0x040A 1034 西班牙語 - 標準 fr 0x040C 1036 法語 - 標準 de 0x0407 1031 德語 - 標準 ru 0x0419 1049 俄語 th 0x041E 1054 泰語為了使用MSDN99OCT,需要增加注冊表設置,拷貝上面的內容,只需要將本地化代碼修改為 0x0809,然后將目錄文件指向對應的 MSDN930.COL,添加注冊信息后,打開 Visual Studio 的選項設置,在幫助系統欄目中,就可以看到英文版的MSDN了,在安裝MSDN 99OTC時,它會在公共目錄下安裝一個工具 COLCHNG.EXE,它就可以用來選擇MSDN版本,不過還是要手動添加注冊表信息。看圖,是不是感覺有天都亮了,再見了MSDN98!再見了2052!
Visual Studio 6.0 補充
編譯代碼使用的工具是 TDM-GCC 4.71,由于它包含的頭文件內容和 VC6 不同,而且是 include 上的功能也有所差別。所以代碼改用 CL.exe 編譯時可能會有問題,比如我嘗試過的幾個例子。GCC 只要引用 cstring 和 iostream 就可以使用 cout 的輸出重載操作符了,因為通過 g++ 編譯時它會自動添加 C++ 的標準類庫:
#include <iostream> #include <cstring>但是在VC6下就要包含C++的字符串頭文件:
#include <string>否則,就會出離奇的編譯錯誤,因為編譯會當移位運算符來處理:
mouse.cpp(241) : error C2679: binary '<<' : no operator defined which takes a ri ght-hand operand of type 'class std::basic_string<char,struct std::char_traits<c har>,class std::allocator<char> >' (or there is no acceptable conversion)另外提醒一下,如果不注意C語言和C++語言的頭文件引用語法差別也會導致編譯不能通過。以標準庫文件為例,C語言的頭文件后面有 .h 擴展名,而C++的沒有,所以下面這條引用會被當成C語言來處理:
#include <iostream.h>如果要想在C++中使用C語言的庫文件,最佳的引用方法是去掉后綴名,加前綴c,如C語言的數學庫文件:
#include <cmath>這樣看代碼就可以知道這是在用C++寫的程序,如果要用C語言寫則可以后綴 .h 來引用庫文件。但是盡量不要混用,盡管 C++ 兼容 C。
直接在代碼字符中使用中文多字節符號也會出現常量斷行的錯誤,只要中文編碼中出現 0xE2 0x80 0xA2 這些值,就會有這個錯誤提示,比如說中文的句號它在UTF8編碼時就會包含一個 0x80,還有中的一字也是一樣。這個只能算是VC6的八啊哥了:
error C2001: newline in constant這個問題即使引用 TCHAR.H 的 _T、_TCHAR 也不能解決。如果一定要在代碼中使用中文,可以考改變代碼文件的編譯方案,列如 UTF8,GBK,UNICODE 互換試試,也許就可以通過編譯,但是字符串就要求相應的編譯支持,否則程序輸出就會亂碼。更好的解決方法是使用資源文件,通過 rc.exe 命令來編譯:
RC.EXE /l 0x804 /c 936 /d_DEBUG /fo.\GENERIC.RES /r GENERIC.RC資源參考
- MSDN - Platform SDK: Windows GDI BITMAPINFO
 - WiKI - BMP file format
 - Microsoft Windows Bitmap File Format Summary
 - use GDI to draw on the console window
 - 256-Color VGA Programming in C
 - DOS development tool - DJGPP 2.0
 - Graphics Programming Using VGA Mode 13h
 - Top-Down vs. Bottom-Up DIBs
 - Guide to Image Composition with MsImg32.dll - Paul M. Watt
 - 透明位圖的顯示 - 王駿 (光柵操作太過復雜,沒能說明白SetBkColor的作用)
 - Platform SDK Redistributable: GDI+
 - Windows? Server 2003 SP1 Platform SDK ISO
 - Using mc.exe message resources and the NT event log - Daniel Lohmann
 - Get World-Class Noise and Total Joy from Your Games - Dave Edson
 - Developing International Software for Windows 95 and Windows NT - Nadine Kano
 - How to localize an RC file?
 - How To Use LoadResource to Load Strings
 - User Interface Language Management (Windows)
 - 深入了解Windows句柄到底是什么 - 文洲
 - 偵測程序句柄泄露的統計方法 - 梅 勝,王 寧,楊承川
 - Programming Windows 3.1, Third Edition by Charles Petzold
 - Managing Virtual Memory 1993 - Randy Kath
 
總結
                            
                        - 上一篇: MySQL多表查询之纵向合并
 - 下一篇: 折腾!在树莓派上运行DOS