3atv精品不卡视频,97人人超碰国产精品最新,中文字幕av一区二区三区人妻少妇,久久久精品波多野结衣,日韩一区二区三区精品

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

图像处理之-位图

發布時間:2023/12/14 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 图像处理之-位图 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

圖像處理之-位圖

  • 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 pointer

BMP文件大小就保存在 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 SetDCPenColor

GDI的常用圖形對象有 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 times

If 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 ;comments

keyword 不區分大小寫,= 等號兩邊的空格會忽略。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

總結

以上是生活随笔為你收集整理的图像处理之-位图的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

女人被男人爽到呻吟的视频 | 激情内射亚州一区二区三区爱妻 | 国产成人一区二区三区别 | 精品国产一区二区三区四区 | 国内揄拍国内精品人妻 | 精品国产aⅴ无码一区二区 | 2019nv天堂香蕉在线观看 | 呦交小u女精品视频 | 午夜福利试看120秒体验区 | 欧美一区二区三区 | 99国产精品白浆在线观看免费 | 久久天天躁夜夜躁狠狠 | 久久99国产综合精品 | 精品国偷自产在线 | 丰满少妇熟乱xxxxx视频 | 丁香啪啪综合成人亚洲 | 牛和人交xxxx欧美 | 亚洲一区二区三区在线观看网站 | 国产精品va在线观看无码 | 国产乡下妇女做爰 | 欧洲美熟女乱又伦 | 丝袜美腿亚洲一区二区 | 超碰97人人射妻 | 日韩无套无码精品 | 亚洲精品久久久久中文第一幕 | 日本熟妇浓毛 | 97久久国产亚洲精品超碰热 | 精品久久久无码人妻字幂 | 97se亚洲精品一区 | 国产精华av午夜在线观看 | 国产精品久久久久久久9999 | 少妇性俱乐部纵欲狂欢电影 | 久久午夜无码鲁丝片 | 久久久久久a亚洲欧洲av冫 | 午夜男女很黄的视频 | 久久国产精品精品国产色婷婷 | 成人精品一区二区三区中文字幕 | 亚洲呦女专区 | 成人免费无码大片a毛片 | 国精产品一品二品国精品69xx | 亚洲男人av天堂午夜在 | 免费无码午夜福利片69 | 牲欲强的熟妇农村老妇女 | 综合人妻久久一区二区精品 | 亚洲精品国偷拍自产在线麻豆 | 麻豆果冻传媒2021精品传媒一区下载 | 久久人人97超碰a片精品 | 国语自产偷拍精品视频偷 | 国产网红无码精品视频 | 久久国产精品萌白酱免费 | 国产av久久久久精东av | 麻豆果冻传媒2021精品传媒一区下载 | 在线а√天堂中文官网 | 中文字幕乱码人妻二区三区 | 99久久精品无码一区二区毛片 | 欧美国产亚洲日韩在线二区 | 日韩精品成人一区二区三区 | 国产极品美女高潮无套在线观看 | 欧美日韩亚洲国产精品 | 日韩欧美群交p片內射中文 | 夜夜躁日日躁狠狠久久av | 丰满妇女强制高潮18xxxx | 精品一区二区不卡无码av | 51国偷自产一区二区三区 | 77777熟女视频在线观看 а天堂中文在线官网 | 一本久道久久综合狠狠爱 | 东京无码熟妇人妻av在线网址 | 伦伦影院午夜理论片 | а天堂中文在线官网 | 高潮毛片无遮挡高清免费 | 精品国产福利一区二区 | 国产乱人偷精品人妻a片 | 亚洲欧洲无卡二区视頻 | 中文字幕中文有码在线 | 国产特级毛片aaaaaa高潮流水 | aⅴ在线视频男人的天堂 | 乱中年女人伦av三区 | 亚洲人亚洲人成电影网站色 | 午夜精品久久久久久久 | 无套内谢的新婚少妇国语播放 | 兔费看少妇性l交大片免费 | 国产麻豆精品一区二区三区v视界 | 男人和女人高潮免费网站 | 夜夜高潮次次欢爽av女 | 男人的天堂2018无码 | 欧洲美熟女乱又伦 | 成人无码精品一区二区三区 | 99在线 | 亚洲 | 精品亚洲成av人在线观看 | 骚片av蜜桃精品一区 | 高潮毛片无遮挡高清免费视频 | 国产精品沙发午睡系列 | 久久精品国产一区二区三区肥胖 | av无码不卡在线观看免费 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 国产精品久久久久久久9999 | 18禁黄网站男男禁片免费观看 | 久久精品国产大片免费观看 | 日本高清一区免费中文视频 | 亚洲中文字幕无码一久久区 | 色一情一乱一伦一视频免费看 | 亚洲の无码国产の无码步美 | 国产精品香蕉在线观看 | 人人妻人人澡人人爽人人精品 | 国精产品一区二区三区 | 又粗又大又硬毛片免费看 | 国产热a欧美热a在线视频 | 国产精品久久国产精品99 | 亚洲の无码国产の无码影院 | 国产精品香蕉在线观看 | 人人澡人摸人人添 | 久久精品国产一区二区三区肥胖 | 亚洲爆乳无码专区 | 欧美一区二区三区视频在线观看 | 中文字幕无码人妻少妇免费 | 131美女爱做视频 | 亚洲人成网站色7799 | 无码人妻出轨黑人中文字幕 | 亚洲中文字幕av在天堂 | 久久午夜夜伦鲁鲁片无码免费 | 久青草影院在线观看国产 | 一本精品99久久精品77 | 99久久人妻精品免费二区 | 久久精品人妻少妇一区二区三区 | 在线a亚洲视频播放在线观看 | 图片小说视频一区二区 | 又大又紧又粉嫩18p少妇 | 日本饥渴人妻欲求不满 | 亚洲精品久久久久久久久久久 | 日本一区二区三区免费播放 | 日韩 欧美 动漫 国产 制服 | 好男人www社区 | 亚洲精品一区国产 | 天天爽夜夜爽夜夜爽 | 久久精品国产一区二区三区肥胖 | 精品人妻av区 | 国产亚洲人成a在线v网站 | 久久99精品国产麻豆 | 日本精品久久久久中文字幕 | 亚洲s色大片在线观看 | 成人性做爰aaa片免费看 | 玩弄人妻少妇500系列视频 | 白嫩日本少妇做爰 | 亚洲欧美日韩国产精品一区二区 | 欧美人与物videos另类 | 人人妻人人澡人人爽人人精品浪潮 | 亚洲日本va午夜在线电影 | 老熟女乱子伦 | 人妻夜夜爽天天爽三区 | 丝袜 中出 制服 人妻 美腿 | 无码国内精品人妻少妇 | 亚洲国产精品久久久久久 | 国产人妻久久精品二区三区老狼 | 国产精品第一区揄拍无码 | 超碰97人人射妻 | 日本又色又爽又黄的a片18禁 | 国产精品自产拍在线观看 | 3d动漫精品啪啪一区二区中 | 最近免费中文字幕中文高清百度 | 国产97在线 | 亚洲 | 亚洲自偷精品视频自拍 | 国产av无码专区亚洲a∨毛片 | 丰满人妻一区二区三区免费视频 | 2020久久超碰国产精品最新 | 无码乱肉视频免费大全合集 | 好男人社区资源 | 国产精品二区一区二区aⅴ污介绍 | 久久久精品成人免费观看 | 在线成人www免费观看视频 | 18禁黄网站男男禁片免费观看 | 男人的天堂2018无码 | 亚洲高清偷拍一区二区三区 | 欧美国产日韩亚洲中文 | 国产成人精品视频ⅴa片软件竹菊 | 久久精品国产99精品亚洲 | 亚洲国产午夜精品理论片 | 中文字幕无码av激情不卡 | 国产色精品久久人妻 | 久久99精品久久久久久 | 欧美亚洲国产一区二区三区 | 999久久久国产精品消防器材 | 亚洲精品综合一区二区三区在线 | 性生交大片免费看l | 国产97人人超碰caoprom | 国产精品无码mv在线观看 | 免费国产黄网站在线观看 | 亚洲一区二区三区含羞草 | 亚洲中文字幕在线观看 | 欧美老人巨大xxxx做受 | 亚洲区欧美区综合区自拍区 | 荫蒂被男人添的好舒服爽免费视频 | 久激情内射婷内射蜜桃人妖 | 婷婷五月综合激情中文字幕 | 高潮毛片无遮挡高清免费 | 少妇性俱乐部纵欲狂欢电影 | 国产亚洲精品久久久久久国模美 | 国产精品高潮呻吟av久久 | 乱码午夜-极国产极内射 | 国产精品久久精品三级 | 国产欧美熟妇另类久久久 | 国产精品18久久久久久麻辣 | 亚洲国产精华液网站w | 亚洲国产av精品一区二区蜜芽 | 领导边摸边吃奶边做爽在线观看 | 蜜桃臀无码内射一区二区三区 | www一区二区www免费 | 国产av一区二区精品久久凹凸 | 乱码av麻豆丝袜熟女系列 | 久久综合激激的五月天 | 男女下面进入的视频免费午夜 | 一区二区三区高清视频一 | 久久99国产综合精品 | 亚洲精品国偷拍自产在线麻豆 | 少妇一晚三次一区二区三区 | 欧美freesex黑人又粗又大 | 国产综合色产在线精品 | 熟妇人妻激情偷爽文 | 狠狠亚洲超碰狼人久久 | 国产特级毛片aaaaaa高潮流水 | 18禁止看的免费污网站 | 久久精品女人的天堂av | 老头边吃奶边弄进去呻吟 | 久久久久免费精品国产 | 国产精品久久精品三级 | 在线精品亚洲一区二区 | 久激情内射婷内射蜜桃人妖 | 国产精品爱久久久久久久 | 国产精品久久久久久亚洲毛片 | 国产情侣作爱视频免费观看 | 波多野结衣乳巨码无在线观看 | 亚洲精品中文字幕乱码 | 国产精品亚洲lv粉色 | 精品国产麻豆免费人成网站 | 亚洲中文字幕av在天堂 | 久久午夜无码鲁丝片 | 日本欧美一区二区三区乱码 | 2019午夜福利不卡片在线 | 亚洲a无码综合a国产av中文 | 久久精品人妻少妇一区二区三区 | 熟女少妇人妻中文字幕 | 玩弄人妻少妇500系列视频 | 精品久久8x国产免费观看 | 大肉大捧一进一出好爽视频 | 131美女爱做视频 | 精品久久久无码人妻字幂 | 精品人人妻人人澡人人爽人人 | 中国大陆精品视频xxxx | 黄网在线观看免费网站 | 精品久久综合1区2区3区激情 | av无码久久久久不卡免费网站 | 亚洲无人区一区二区三区 | 麻豆果冻传媒2021精品传媒一区下载 | 亚洲成av人片在线观看无码不卡 | 亚洲精品一区二区三区婷婷月 | 国内丰满熟女出轨videos | 国产又爽又黄又刺激的视频 | 国产精品久久国产三级国 | 色婷婷综合激情综在线播放 | 最新国产乱人伦偷精品免费网站 | 国产精品手机免费 | 丰满妇女强制高潮18xxxx | 3d动漫精品啪啪一区二区中 | 久久久久人妻一区精品色欧美 | 97色伦图片97综合影院 | 特黄特色大片免费播放器图片 | 一本一道久久综合久久 | 久久99精品久久久久久动态图 | 最近中文2019字幕第二页 | 中文字幕 人妻熟女 | 99riav国产精品视频 | 伊在人天堂亚洲香蕉精品区 | 国内老熟妇对白xxxxhd | 亚洲日韩av一区二区三区四区 | 精品久久久无码人妻字幂 | 婷婷综合久久中文字幕蜜桃三电影 | 久久精品中文闷骚内射 | 欧洲精品码一区二区三区免费看 | 午夜精品久久久久久久 | 中文毛片无遮挡高清免费 | 久久国产精品_国产精品 | 国产美女极度色诱视频www | 97色伦图片97综合影院 | 亚洲成a人片在线观看日本 | 欧美日韩视频无码一区二区三 | av无码电影一区二区三区 | 亚洲日韩中文字幕在线播放 | 欧美亚洲日韩国产人成在线播放 | 国产亲子乱弄免费视频 | 成人免费视频视频在线观看 免费 | 中文字幕无码视频专区 | 青草视频在线播放 | 精品久久综合1区2区3区激情 | 狠狠躁日日躁夜夜躁2020 | 成人精品一区二区三区中文字幕 | 欧美老人巨大xxxx做受 | 亚洲国产精品一区二区第一页 | 性欧美熟妇videofreesex | 俺去俺来也www色官网 | 亚洲精品国产精品乱码不卡 | 久久zyz资源站无码中文动漫 | 激情五月综合色婷婷一区二区 | 免费无码av一区二区 | 欧美一区二区三区视频在线观看 | 无码福利日韩神码福利片 | 中文字幕日韩精品一区二区三区 | 精品国产一区二区三区av 性色 | 乱人伦人妻中文字幕无码久久网 | 亚洲国产成人av在线观看 | 久久久国产精品无码免费专区 | 中文无码成人免费视频在线观看 | a片免费视频在线观看 | 亚洲精品久久久久久一区二区 | 日本精品高清一区二区 | 成人一在线视频日韩国产 | 国内老熟妇对白xxxxhd | 131美女爱做视频 | 色妞www精品免费视频 | 久久精品中文字幕大胸 | av小次郎收藏 | 最近免费中文字幕中文高清百度 | 亚洲欧美日韩成人高清在线一区 | 免费人成在线观看网站 | 图片区 小说区 区 亚洲五月 | 欧美成人午夜精品久久久 | 日韩人妻少妇一区二区三区 | 亚洲日本va午夜在线电影 | 精品夜夜澡人妻无码av蜜桃 | 色欲人妻aaaaaaa无码 | 少女韩国电视剧在线观看完整 | 日韩视频 中文字幕 视频一区 | 成熟妇人a片免费看网站 | 伊人久久大香线蕉午夜 | 四虎永久在线精品免费网址 | 日本一区二区更新不卡 | 女人色极品影院 | 又粗又大又硬毛片免费看 | 国产精品亚洲一区二区三区喷水 | 国产精品高潮呻吟av久久4虎 | 98国产精品综合一区二区三区 | 精品人妻中文字幕有码在线 | 日产精品高潮呻吟av久久 | 宝宝好涨水快流出来免费视频 | aⅴ亚洲 日韩 色 图网站 播放 | 美女黄网站人色视频免费国产 | 午夜精品久久久久久久久 | 亚洲va中文字幕无码久久不卡 | 少妇人妻av毛片在线看 | 草草网站影院白丝内射 | 精品久久久久久人妻无码中文字幕 | 人妻aⅴ无码一区二区三区 | 日韩人妻系列无码专区 | 乌克兰少妇性做爰 | 色综合久久久无码中文字幕 | 亚洲国产精华液网站w | 少妇人妻大乳在线视频 | 亚洲成色在线综合网站 | 国内揄拍国内精品人妻 | 人人妻人人澡人人爽人人精品 | 久在线观看福利视频 | 亚洲s码欧洲m码国产av | 国产做国产爱免费视频 | 亚洲区小说区激情区图片区 | 久久精品国产一区二区三区肥胖 | 久久99久久99精品中文字幕 | 久久精品国产99久久6动漫 | 国产精品第一国产精品 | 日本饥渴人妻欲求不满 | 日本精品少妇一区二区三区 | 国产人妻精品午夜福利免费 | 久久久无码中文字幕久... | 久久天天躁狠狠躁夜夜免费观看 | 久久精品国产亚洲精品 | 小sao货水好多真紧h无码视频 | 午夜福利试看120秒体验区 | 国产乱人偷精品人妻a片 | 漂亮人妻洗澡被公强 日日躁 | 男女性色大片免费网站 | 成人免费无码大片a毛片 | 欧美丰满熟妇xxxx性ppx人交 | 午夜精品久久久内射近拍高清 | 蜜臀av在线播放 久久综合激激的五月天 | 麻豆国产人妻欲求不满 | 亚洲成a人片在线观看日本 | 国产精品第一区揄拍无码 | 老子影院午夜伦不卡 | 成 人 网 站国产免费观看 | 99久久久无码国产精品免费 | 亚洲国产精品毛片av不卡在线 | 久久国产精品萌白酱免费 | 男人的天堂av网站 | 激情亚洲一区国产精品 | 久久久久久av无码免费看大片 | 国产无遮挡又黄又爽免费视频 | 国产熟妇另类久久久久 | 最新国产麻豆aⅴ精品无码 | 国产成人无码午夜视频在线观看 | 亚洲日本一区二区三区在线 | 伊在人天堂亚洲香蕉精品区 | 老熟妇仑乱视频一区二区 | 久久精品国产一区二区三区 | 色噜噜亚洲男人的天堂 | 日本一区二区更新不卡 | 亚洲乱亚洲乱妇50p | 青青草原综合久久大伊人精品 | 国产乱子伦视频在线播放 | 欧美丰满熟妇xxxx | 无码人妻少妇伦在线电影 | 亚洲精品中文字幕乱码 | 人人妻人人澡人人爽欧美一区 | 最新国产乱人伦偷精品免费网站 | 蜜桃av抽搐高潮一区二区 | 蜜桃臀无码内射一区二区三区 | 3d动漫精品啪啪一区二区中 | 中文字幕无码免费久久9一区9 | 乌克兰少妇性做爰 | 色一情一乱一伦 | 国产午夜亚洲精品不卡下载 | 少妇性荡欲午夜性开放视频剧场 | 国产69精品久久久久app下载 | 老司机亚洲精品影院 | 天干天干啦夜天干天2017 | 少妇人妻大乳在线视频 | 亚洲中文字幕乱码av波多ji | 学生妹亚洲一区二区 | 国产人妻大战黑人第1集 | 极品尤物被啪到呻吟喷水 | 帮老师解开蕾丝奶罩吸乳网站 | 久久国产精品精品国产色婷婷 | 国产疯狂伦交大片 | 亚洲狠狠色丁香婷婷综合 | 九九久久精品国产免费看小说 | 少妇厨房愉情理9仑片视频 | 伊人色综合久久天天小片 | 亚洲精品午夜无码电影网 | 亚洲人成影院在线无码按摩店 | 日本又色又爽又黄的a片18禁 | 亚洲日韩一区二区 | 丰满人妻一区二区三区免费视频 | 亚洲中文字幕av在天堂 | 97色伦图片97综合影院 | 2020久久超碰国产精品最新 | 四虎4hu永久免费 | 精品夜夜澡人妻无码av蜜桃 | 免费无码午夜福利片69 | 自拍偷自拍亚洲精品被多人伦好爽 | 成人精品视频一区二区 | 中文字幕无码人妻少妇免费 | 76少妇精品导航 | 国产成人精品一区二区在线小狼 | 鲁鲁鲁爽爽爽在线视频观看 | 国产人成高清在线视频99最全资源 | 国产国语老龄妇女a片 | 久久精品国产精品国产精品污 | 国产熟妇高潮叫床视频播放 | 内射爽无广熟女亚洲 | 少妇性荡欲午夜性开放视频剧场 | 国产美女精品一区二区三区 | 中文字幕无码乱人伦 | 丁香啪啪综合成人亚洲 | 99久久精品日本一区二区免费 | 高潮毛片无遮挡高清免费 | 天天躁日日躁狠狠躁免费麻豆 | 狠狠色丁香久久婷婷综合五月 | 在线精品国产一区二区三区 | 久久人人97超碰a片精品 | 麻豆果冻传媒2021精品传媒一区下载 | 国产农村妇女高潮大叫 | 国产美女精品一区二区三区 | 国产精品无码久久av | 人人爽人人澡人人人妻 | 国产免费无码一区二区视频 | а√天堂www在线天堂小说 | 亚洲va欧美va天堂v国产综合 | 窝窝午夜理论片影院 | 久久99精品国产.久久久久 | 国产亚洲精品久久久久久久久动漫 | 久久久久亚洲精品中文字幕 | 久久午夜无码鲁丝片秋霞 | 亚洲精品成人福利网站 | 未满小14洗澡无码视频网站 | 成人无码精品一区二区三区 | 亚洲人成影院在线无码按摩店 | 亚洲s色大片在线观看 | 国产熟妇高潮叫床视频播放 | 欧洲欧美人成视频在线 | 爽爽影院免费观看 | 未满小14洗澡无码视频网站 | 性色av无码免费一区二区三区 | 日本一区二区三区免费高清 | 欧美性黑人极品hd | 人人妻人人澡人人爽精品欧美 | 久久久久久a亚洲欧洲av冫 | 天堂久久天堂av色综合 | 人人爽人人爽人人片av亚洲 | 美女极度色诱视频国产 | 亚洲国产成人av在线观看 | 欧美喷潮久久久xxxxx | 无套内谢老熟女 | 精品成人av一区二区三区 | 亚拍精品一区二区三区探花 | 亚洲人成人无码网www国产 | 精品亚洲成av人在线观看 | 国产综合色产在线精品 | 国产精品欧美成人 | 伊人色综合久久天天小片 | 国产亚洲精品久久久久久久久动漫 | 奇米影视7777久久精品人人爽 | 亚洲另类伦春色综合小说 | 国产艳妇av在线观看果冻传媒 | 丰满人妻一区二区三区免费视频 | 激情内射日本一区二区三区 | 久久午夜无码鲁丝片 | 自拍偷自拍亚洲精品10p | 大色综合色综合网站 | 国产综合色产在线精品 | 久久综合激激的五月天 | 老熟妇乱子伦牲交视频 | 午夜时刻免费入口 | 丰满诱人的人妻3 | 日本精品高清一区二区 | 亚洲日韩中文字幕在线播放 | 女人被男人躁得好爽免费视频 | 久久伊人色av天堂九九小黄鸭 | 国产亚洲精品久久久久久 | 丰满人妻翻云覆雨呻吟视频 | 东京一本一道一二三区 | 亚洲精品成a人在线观看 | 日产国产精品亚洲系列 | av无码久久久久不卡免费网站 | 在线а√天堂中文官网 | 丝袜足控一区二区三区 | 国产精品嫩草久久久久 | 国产精华av午夜在线观看 | 久久精品女人的天堂av | 国产小呦泬泬99精品 | 久久久无码中文字幕久... | 日韩人妻少妇一区二区三区 | 国产无遮挡又黄又爽又色 | 亚洲国产成人a精品不卡在线 | 色五月五月丁香亚洲综合网 | 欧美午夜特黄aaaaaa片 | 奇米影视7777久久精品 | 成 人 免费观看网站 | 中文字幕av无码一区二区三区电影 | 国产精品第一国产精品 | 激情国产av做激情国产爱 | 成熟妇人a片免费看网站 | 人妻少妇精品视频专区 | 熟女少妇人妻中文字幕 | 在线精品国产一区二区三区 | 久久久久se色偷偷亚洲精品av | 亚洲国产精华液网站w | 九九在线中文字幕无码 | 日日摸天天摸爽爽狠狠97 | 亚洲成a人片在线观看日本 | 国产精品视频免费播放 | 久9re热视频这里只有精品 | 男人的天堂av网站 | 一本加勒比波多野结衣 | 亚洲区欧美区综合区自拍区 | 亚洲精品国产精品乱码视色 | 欧美午夜特黄aaaaaa片 | 精品国产一区二区三区av 性色 | 鲁鲁鲁爽爽爽在线视频观看 | 九月婷婷人人澡人人添人人爽 | 久久久久成人片免费观看蜜芽 | 久久精品国产一区二区三区 | 99久久婷婷国产综合精品青草免费 | 少妇无码吹潮 | 成人无码视频免费播放 | 国产精品亚洲一区二区三区喷水 | 亚洲 日韩 欧美 成人 在线观看 | 日本va欧美va欧美va精品 | 好男人www社区 | 无码人妻出轨黑人中文字幕 | 国产亚洲tv在线观看 | 97精品国产97久久久久久免费 | 综合人妻久久一区二区精品 | 丰腴饱满的极品熟妇 | 国产麻豆精品一区二区三区v视界 | 久久婷婷五月综合色国产香蕉 | 99国产精品白浆在线观看免费 | www国产亚洲精品久久久日本 | 成人无码精品一区二区三区 | 四十如虎的丰满熟妇啪啪 | 7777奇米四色成人眼影 | 欧美性猛交内射兽交老熟妇 | 欧美熟妇另类久久久久久多毛 | 天堂亚洲免费视频 | 久久久中文久久久无码 | 久激情内射婷内射蜜桃人妖 | 成人精品一区二区三区中文字幕 | 国产后入清纯学生妹 | 中文字幕久久久久人妻 | 99国产欧美久久久精品 | 乱人伦人妻中文字幕无码久久网 | 十八禁视频网站在线观看 | 国产成人无码av一区二区 | 亚洲欧洲中文日韩av乱码 | 4hu四虎永久在线观看 | 妺妺窝人体色www婷婷 | 成人影院yy111111在线观看 | 国产做国产爱免费视频 | 领导边摸边吃奶边做爽在线观看 | 嫩b人妻精品一区二区三区 | 日韩av无码中文无码电影 | 欧美 丝袜 自拍 制服 另类 | 亚洲 另类 在线 欧美 制服 | 成人欧美一区二区三区 | 亚洲 高清 成人 动漫 | 噜噜噜亚洲色成人网站 | 国产精品二区一区二区aⅴ污介绍 | 综合网日日天干夜夜久久 | 国产麻豆精品精东影业av网站 | 国产欧美熟妇另类久久久 | 中文字幕无码热在线视频 | 欧美精品免费观看二区 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | 无码人妻久久一区二区三区不卡 | 国产午夜精品一区二区三区嫩草 | 宝宝好涨水快流出来免费视频 | 狠狠噜狠狠狠狠丁香五月 | 麻豆成人精品国产免费 | 午夜无码人妻av大片色欲 | 中文精品无码中文字幕无码专区 | 欧美freesex黑人又粗又大 | 最新国产乱人伦偷精品免费网站 | 欧美丰满老熟妇xxxxx性 | 色 综合 欧美 亚洲 国产 | 天天拍夜夜添久久精品大 | 国产无遮挡吃胸膜奶免费看 | 青青青手机频在线观看 | 乌克兰少妇性做爰 | 人人澡人摸人人添 | 亚洲毛片av日韩av无码 | 狂野欧美性猛xxxx乱大交 | 99在线 | 亚洲 | 国产精品美女久久久网av | 樱花草在线社区www | 中文字幕 人妻熟女 | 成人欧美一区二区三区黑人 | 精品国产麻豆免费人成网站 | 无码午夜成人1000部免费视频 | 国产成人无码午夜视频在线观看 | 国产偷抇久久精品a片69 | 国内揄拍国内精品人妻 | 久久精品国产一区二区三区 | 99久久精品国产一区二区蜜芽 | 男女超爽视频免费播放 | 成人无码影片精品久久久 | 少妇一晚三次一区二区三区 | 强伦人妻一区二区三区视频18 | 亚洲色欲色欲欲www在线 | 乱码午夜-极国产极内射 | 欧美老妇交乱视频在线观看 | 亚洲精品一区二区三区大桥未久 | 久久熟妇人妻午夜寂寞影院 | 久久99精品国产麻豆 | 亚洲春色在线视频 | 1000部夫妻午夜免费 | 黑人巨大精品欧美一区二区 | 乌克兰少妇xxxx做受 | 国产另类ts人妖一区二区 | 色综合视频一区二区三区 | 中文字幕日产无线码一区 | 全黄性性激高免费视频 | 中文精品无码中文字幕无码专区 | 日韩精品乱码av一区二区 | 丰满人妻一区二区三区免费视频 | 成人动漫在线观看 | 中国大陆精品视频xxxx | 国产午夜精品一区二区三区嫩草 | 欧美日韩亚洲国产精品 | 性史性农村dvd毛片 | 秋霞成人午夜鲁丝一区二区三区 | 在线播放免费人成毛片乱码 | 久久综合九色综合欧美狠狠 | 亚洲人成影院在线观看 | 奇米影视7777久久精品人人爽 | 黑人巨大精品欧美一区二区 | 无码国模国产在线观看 | 精品偷拍一区二区三区在线看 | 亚洲热妇无码av在线播放 | 国产激情精品一区二区三区 | 婷婷丁香五月天综合东京热 | 麻豆人妻少妇精品无码专区 | 精品日本一区二区三区在线观看 | 黑森林福利视频导航 | 国产欧美亚洲精品a | 狠狠cao日日穞夜夜穞av | 亚洲色欲色欲欲www在线 | 真人与拘做受免费视频 | 全黄性性激高免费视频 | 伊人久久大香线焦av综合影院 | 国产一区二区三区四区五区加勒比 | 久久久久久av无码免费看大片 | 日本一区二区三区免费播放 | 亚洲一区二区三区在线观看网站 | 日本一区二区三区免费播放 | 丰满人妻一区二区三区免费视频 | 亚洲综合精品香蕉久久网 | 丰满护士巨好爽好大乳 | 亚洲中文字幕在线无码一区二区 | 丰满人妻翻云覆雨呻吟视频 | 精品偷拍一区二区三区在线看 | 人人妻人人澡人人爽欧美一区 | 无码人妻出轨黑人中文字幕 | 色老头在线一区二区三区 | 日本免费一区二区三区最新 | 少妇被黑人到高潮喷出白浆 | 久久综合香蕉国产蜜臀av | 久久无码中文字幕免费影院蜜桃 | 精品国产一区二区三区av 性色 | 亚洲中文字幕无码一久久区 | 国产真实乱对白精彩久久 | 久久久精品456亚洲影院 | 一本久久a久久精品亚洲 | 国产欧美精品一区二区三区 | 蜜臀aⅴ国产精品久久久国产老师 | 天天躁日日躁狠狠躁免费麻豆 | 岛国片人妻三上悠亚 | 麻豆国产丝袜白领秘书在线观看 | 久久亚洲a片com人成 | 国产后入清纯学生妹 | 亚洲 欧美 激情 小说 另类 | 久久久精品456亚洲影院 | 亚洲色欲久久久综合网东京热 | 久久99精品国产.久久久久 | 给我免费的视频在线观看 | 国产真实乱对白精彩久久 | 天天av天天av天天透 | 亚洲国产精品毛片av不卡在线 | 亚洲精品国产a久久久久久 | 亚洲精品国产a久久久久久 | 国产熟妇高潮叫床视频播放 | 香港三级日本三级妇三级 | 亚洲成色在线综合网站 | 国产精品久久久久久亚洲影视内衣 | 精品国精品国产自在久国产87 | 亚洲午夜久久久影院 | 亚洲欧美精品伊人久久 | 国产精品毛片一区二区 | 性做久久久久久久久 | 中文字幕无码免费久久99 | 两性色午夜视频免费播放 | 午夜男女很黄的视频 | 男人的天堂2018无码 | 久久精品女人天堂av免费观看 | 国产69精品久久久久app下载 | 国产成人综合美国十次 | 无码播放一区二区三区 | 国产精品国产自线拍免费软件 | 久久国产自偷自偷免费一区调 | 国产人妻精品一区二区三区不卡 | 精品国产一区二区三区四区 | 国产午夜精品一区二区三区嫩草 | 亚洲经典千人经典日产 | 亚洲午夜久久久影院 | 亚洲精品www久久久 | 国产精品办公室沙发 | 性欧美熟妇videofreesex | 中文字幕乱码人妻二区三区 | 综合人妻久久一区二区精品 | 国产 精品 自在自线 | 国产国语老龄妇女a片 | 亚洲性无码av中文字幕 | 亚洲成av人综合在线观看 | 四虎永久在线精品免费网址 | 欧美三级a做爰在线观看 | 欧美老人巨大xxxx做受 | 亚洲熟妇色xxxxx亚洲 | 久久人人爽人人爽人人片ⅴ | 国内丰满熟女出轨videos | 精品午夜福利在线观看 | 精品aⅴ一区二区三区 | 男人的天堂2018无码 | 狠狠色丁香久久婷婷综合五月 | 99er热精品视频 | 亚洲熟悉妇女xxx妇女av | 亚洲熟妇色xxxxx亚洲 | 撕开奶罩揉吮奶头视频 | 色一情一乱一伦一视频免费看 | 成人综合网亚洲伊人 | 亚洲国产一区二区三区在线观看 | 天天av天天av天天透 | 国产成人精品一区二区在线小狼 | 捆绑白丝粉色jk震动捧喷白浆 | 色欲av亚洲一区无码少妇 | 网友自拍区视频精品 | 国产精品亚洲а∨无码播放麻豆 | 无码国产色欲xxxxx视频 | 成人试看120秒体验区 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 欧美丰满老熟妇xxxxx性 | 国产麻豆精品精东影业av网站 | 中文无码成人免费视频在线观看 | 特大黑人娇小亚洲女 | 好男人www社区 | 午夜精品一区二区三区的区别 | 日本一区二区三区免费播放 | 国产精品国产三级国产专播 | 天堂亚洲免费视频 | 全球成人中文在线 | 香港三级日本三级妇三级 | 99久久精品日本一区二区免费 | 国产婷婷色一区二区三区在线 | 欧洲vodafone精品性 | 黑人大群体交免费视频 | 成人性做爰aaa片免费看不忠 | 人人妻人人澡人人爽精品欧美 | 国内精品久久毛片一区二区 | 黑人巨大精品欧美一区二区 | 日本熟妇浓毛 | 黑人玩弄人妻中文在线 | 麻豆国产97在线 | 欧洲 | 激情综合激情五月俺也去 | 一本久道久久综合狠狠爱 | 久久人人爽人人人人片 | 中文字幕+乱码+中文字幕一区 | 麻豆精品国产精华精华液好用吗 | 色狠狠av一区二区三区 | 免费无码午夜福利片69 | 国产网红无码精品视频 | 粉嫩少妇内射浓精videos | 图片区 小说区 区 亚洲五月 | 无码国产乱人伦偷精品视频 | 婷婷丁香五月天综合东京热 | 国产亲子乱弄免费视频 | 少妇的肉体aa片免费 | 强开小婷嫩苞又嫩又紧视频 | 疯狂三人交性欧美 | 久久综合激激的五月天 | 国产另类ts人妖一区二区 | 妺妺窝人体色www在线小说 | 欧美黑人乱大交 | 成在人线av无码免观看麻豆 | 中文字幕乱码亚洲无线三区 | 国产激情一区二区三区 | 国内揄拍国内精品人妻 | 性欧美牲交在线视频 | 亚洲色欲色欲欲www在线 | 日本熟妇人妻xxxxx人hd | 亚洲va中文字幕无码久久不卡 | 欧美怡红院免费全部视频 | 98国产精品综合一区二区三区 | 久久综合色之久久综合 | 国产一精品一av一免费 | 风流少妇按摩来高潮 | 无码毛片视频一区二区本码 | 久久亚洲日韩精品一区二区三区 | 装睡被陌生人摸出水好爽 | 日韩 欧美 动漫 国产 制服 | a片在线免费观看 | 国产av一区二区三区最新精品 | 久久亚洲日韩精品一区二区三区 | 国产精品久久久久9999小说 | 久久zyz资源站无码中文动漫 | 国产艳妇av在线观看果冻传媒 | 国产乱人伦av在线无码 | 黑人巨大精品欧美黑寡妇 | 久久久久av无码免费网 | 野狼第一精品社区 | 女人被爽到呻吟gif动态图视看 | 国产激情艳情在线看视频 | 未满小14洗澡无码视频网站 | 亚洲爆乳大丰满无码专区 | 久久久久久a亚洲欧洲av冫 | 狠狠色噜噜狠狠狠狠7777米奇 | 精品无码国产一区二区三区av | 亚洲成a人片在线观看无码3d | 1000部啪啪未满十八勿入下载 | 国产成人综合美国十次 | 久久婷婷五月综合色国产香蕉 | 亚洲乱亚洲乱妇50p | 色婷婷欧美在线播放内射 | 国产精品多人p群无码 | 国产成人精品三级麻豆 | 国产精品无码永久免费888 | 97无码免费人妻超级碰碰夜夜 | 18禁止看的免费污网站 | a在线观看免费网站大全 | 中文字幕久久久久人妻 | 亚洲日韩av一区二区三区四区 | 中文字幕无码av激情不卡 | 午夜时刻免费入口 | 国产成人无码av片在线观看不卡 | 午夜免费福利小电影 | 久久综合给久久狠狠97色 | 日韩少妇白浆无码系列 | 国产极品视觉盛宴 | 国产精品内射视频免费 | 国产午夜精品一区二区三区嫩草 | 亚洲娇小与黑人巨大交 | 无码福利日韩神码福利片 | 东京热男人av天堂 | 亚洲gv猛男gv无码男同 | 男女作爱免费网站 | 欧美xxxx黑人又粗又长 | 精品熟女少妇av免费观看 | 蜜桃无码一区二区三区 | 国产无遮挡吃胸膜奶免费看 | 亚洲精品中文字幕久久久久 | 少女韩国电视剧在线观看完整 | 两性色午夜视频免费播放 | 少妇厨房愉情理9仑片视频 | 亚洲大尺度无码无码专区 | 最新国产乱人伦偷精品免费网站 | 中文无码精品a∨在线观看不卡 | 在线视频网站www色 | 成人欧美一区二区三区黑人免费 | 中文字幕人妻无码一夲道 | 国产亚洲精品久久久ai换 | 精品国产成人一区二区三区 | 中文字幕+乱码+中文字幕一区 | 亚洲の无码国产の无码步美 | 亚欧洲精品在线视频免费观看 | а√天堂www在线天堂小说 | 久久国产劲爆∧v内射 | 波多野结衣 黑人 | 久久久成人毛片无码 | 亚洲成在人网站无码天堂 | 2020最新国产自产精品 | 国产偷自视频区视频 | 久久亚洲精品中文字幕无男同 | 国内揄拍国内精品少妇国语 | 国产成人av免费观看 | 国产人妻大战黑人第1集 | 久久精品国产精品国产精品污 | 亚洲中文字幕va福利 | 99精品视频在线观看免费 | 女人被爽到呻吟gif动态图视看 | 久久天天躁夜夜躁狠狠 | 国产区女主播在线观看 | 欧美人与物videos另类 | 国产人妻人伦精品 | 国产特级毛片aaaaaaa高清 | 国产一区二区三区影院 | 亚洲国产欧美日韩精品一区二区三区 | 东京热一精品无码av | 亚洲成av人影院在线观看 | 久久综合色之久久综合 | 夜夜影院未满十八勿进 | 狠狠色噜噜狠狠狠7777奇米 | 国产乱子伦视频在线播放 | 亚洲阿v天堂在线 | 国产亚洲人成在线播放 | 亚洲国产一区二区三区在线观看 | 色欲综合久久中文字幕网 | 精品无码国产一区二区三区av | 学生妹亚洲一区二区 | 乱人伦中文视频在线观看 | 夜精品a片一区二区三区无码白浆 | 又大又紧又粉嫩18p少妇 | 人妻无码αv中文字幕久久琪琪布 | 久久无码专区国产精品s | 中文字幕人妻无码一区二区三区 | 波多野结衣高清一区二区三区 | 久久97精品久久久久久久不卡 | 高清国产亚洲精品自在久久 | 精品 日韩 国产 欧美 视频 | 麻豆国产丝袜白领秘书在线观看 | 又大又黄又粗又爽的免费视频 | 乱人伦中文视频在线观看 | 美女张开腿让人桶 | 久久综合狠狠综合久久综合88 | 色老头在线一区二区三区 | 永久免费观看国产裸体美女 | 天海翼激烈高潮到腰振不止 | 中文字幕人妻无码一区二区三区 | 夜先锋av资源网站 | 国精产品一品二品国精品69xx | 欧美一区二区三区视频在线观看 | 亚洲色欲色欲欲www在线 | 一个人看的www免费视频在线观看 | 国产内射爽爽大片视频社区在线 | 麻豆成人精品国产免费 | 黑人玩弄人妻中文在线 | 天堂亚洲免费视频 | 午夜成人1000部免费视频 | 国产97在线 | 亚洲 | 亚洲国产精品毛片av不卡在线 | 亚洲男人av香蕉爽爽爽爽 | 亚洲男人av香蕉爽爽爽爽 | 久久天天躁夜夜躁狠狠 | 国产亚洲美女精品久久久2020 | 亚洲精品国产a久久久久久 | 欧美成人家庭影院 | 两性色午夜视频免费播放 | 国产乱人偷精品人妻a片 | 国产午夜亚洲精品不卡下载 | 天天拍夜夜添久久精品大 | 国产又粗又硬又大爽黄老大爷视 | 国产免费久久精品国产传媒 | 国产色在线 | 国产 | 久久伊人色av天堂九九小黄鸭 | 一区二区三区高清视频一 | 色五月五月丁香亚洲综合网 | 300部国产真实乱 | 中文无码精品a∨在线观看不卡 | 波多野结衣高清一区二区三区 | 成人免费视频在线观看 | 欧美放荡的少妇 | 亚洲精品一区二区三区大桥未久 | 欧美精品国产综合久久 | 日产精品高潮呻吟av久久 | 精品偷拍一区二区三区在线看 | 久久亚洲国产成人精品性色 | 欧美日韩综合一区二区三区 | 国产黄在线观看免费观看不卡 | 亚洲中文无码av永久不收费 | 亚洲精品国产品国语在线观看 | 中文字幕无码免费久久99 | 无码福利日韩神码福利片 | 欧美日本精品一区二区三区 | 久久久精品人妻久久影视 | 日本一区二区三区免费高清 | 国产亚洲精品久久久久久大师 | 国产成人午夜福利在线播放 | 中文字幕 亚洲精品 第1页 | 大色综合色综合网站 | 无码毛片视频一区二区本码 | 人人妻人人澡人人爽人人精品浪潮 | 日韩在线不卡免费视频一区 | 人人妻人人澡人人爽欧美一区 | 妺妺窝人体色www在线小说 | 熟妇人妻无码xxx视频 | 中国女人内谢69xxxx | 精品国产一区二区三区av 性色 | 日韩 欧美 动漫 国产 制服 | 欧美激情内射喷水高潮 | 蜜桃无码一区二区三区 | 欧美野外疯狂做受xxxx高潮 | 国产精品对白交换视频 | 国产人妻久久精品二区三区老狼 | 麻豆国产97在线 | 欧洲 | 初尝人妻少妇中文字幕 | 日韩精品无码一本二本三本色 | 1000部啪啪未满十八勿入下载 | 老熟女乱子伦 | 牲交欧美兽交欧美 | 日产精品高潮呻吟av久久 | 欧美日韩视频无码一区二区三 | 亚洲日韩精品欧美一区二区 | 日本肉体xxxx裸交 | 色综合久久久无码中文字幕 | 亚洲精品综合一区二区三区在线 | 日韩精品久久久肉伦网站 | 男女猛烈xx00免费视频试看 | 免费播放一区二区三区 | 国产疯狂伦交大片 | 欧美激情一区二区三区成人 | 亚洲中文字幕久久无码 | 露脸叫床粗话东北少妇 | 又黄又爽又色的视频 | 久久这里只有精品视频9 | 亚洲乱码国产乱码精品精 | 国产乱人无码伦av在线a | 亚洲色欲色欲欲www在线 | 欧美熟妇另类久久久久久多毛 | 亚洲人成网站在线播放942 | 国产精品-区区久久久狼 | 婷婷丁香五月天综合东京热 | 国产内射老熟女aaaa | 国产亚洲人成a在线v网站 | 国产亚洲精品久久久ai换 | 亚洲一区二区三区香蕉 | 女人被男人躁得好爽免费视频 | 一个人看的视频www在线 | 夜精品a片一区二区三区无码白浆 | 丰满岳乱妇在线观看中字无码 | 国产在线精品一区二区三区直播 | √天堂中文官网8在线 | 一个人看的www免费视频在线观看 | 熟妇人妻中文av无码 | 美女张开腿让人桶 | 婷婷综合久久中文字幕蜜桃三电影 | 亚洲日韩精品欧美一区二区 | 免费无码一区二区三区蜜桃大 | 领导边摸边吃奶边做爽在线观看 | 欧洲vodafone精品性 | 亚洲精品一区二区三区婷婷月 | 女人高潮内射99精品 | av无码久久久久不卡免费网站 | 国产绳艺sm调教室论坛 | 无码成人精品区在线观看 | 国产精品久久久 | 性欧美videos高清精品 | 精品人人妻人人澡人人爽人人 | 人人澡人摸人人添 | yw尤物av无码国产在线观看 | 精品国产乱码久久久久乱码 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 午夜嘿嘿嘿影院 | 久久久www成人免费毛片 | 婷婷五月综合激情中文字幕 | 欧美兽交xxxx×视频 | 午夜精品久久久久久久 | 在线播放无码字幕亚洲 | 久久国产精品二国产精品 | 一本久道高清无码视频 | 对白脏话肉麻粗话av | 亚洲男人av香蕉爽爽爽爽 | 亚洲一区二区观看播放 | 帮老师解开蕾丝奶罩吸乳网站 | 中文字幕日产无线码一区 | 亚洲熟熟妇xxxx | 中文无码伦av中文字幕 | 成人性做爰aaa片免费看 | 欧美老妇与禽交 | 欧美黑人乱大交 | 小鲜肉自慰网站xnxx | 成人一在线视频日韩国产 | 男女猛烈xx00免费视频试看 | 特级做a爰片毛片免费69 | 中文字幕日韩精品一区二区三区 | 日本免费一区二区三区最新 | 99视频精品全部免费免费观看 | 中国大陆精品视频xxxx | 97精品国产97久久久久久免费 | 任你躁在线精品免费 | a在线亚洲男人的天堂 | 国产成人精品久久亚洲高清不卡 | 国产片av国语在线观看 | 日本欧美一区二区三区乱码 | 国产免费久久久久久无码 | 狠狠色噜噜狠狠狠7777奇米 | 中文字幕人成乱码熟女app | 久久伊人色av天堂九九小黄鸭 | 欧美放荡的少妇 | 国产精品va在线播放 | 欧美真人作爱免费视频 | 欧美猛少妇色xxxxx | 久久久久成人片免费观看蜜芽 | 久久久无码中文字幕久... | 精品乱子伦一区二区三区 | 亚洲日韩精品欧美一区二区 | 无码av中文字幕免费放 | 无码福利日韩神码福利片 | 露脸叫床粗话东北少妇 | 伊人久久大香线蕉亚洲 | 中文字幕精品av一区二区五区 | 99国产欧美久久久精品 | 国产av一区二区精品久久凹凸 | 国产人妻大战黑人第1集 | 女人高潮内射99精品 | 国产av一区二区精品久久凹凸 | 国产绳艺sm调教室论坛 | 亚洲熟妇色xxxxx欧美老妇 | 色窝窝无码一区二区三区色欲 | 久久精品国产一区二区三区 | 国产真实乱对白精彩久久 | 欧美一区二区三区视频在线观看 | 色偷偷人人澡人人爽人人模 | 国产精品高潮呻吟av久久4虎 | 我要看www免费看插插视频 | 无码吃奶揉捏奶头高潮视频 | 久久无码专区国产精品s | 俺去俺来也在线www色官网 | 久久五月精品中文字幕 | 熟妇人妻中文av无码 | 亚洲综合伊人久久大杳蕉 | 国产精品无码一区二区桃花视频 | 亚洲国产精品久久人人爱 | 永久免费精品精品永久-夜色 | 国产成人久久精品流白浆 | 天堂一区人妻无码 | 国产精品手机免费 | 久久综合狠狠综合久久综合88 | 国产午夜亚洲精品不卡下载 | 欧美 丝袜 自拍 制服 另类 | 久久精品女人的天堂av | 人人妻人人澡人人爽欧美精品 | 黑人玩弄人妻中文在线 | 国产精品久久久久7777 | 两性色午夜免费视频 | 97久久国产亚洲精品超碰热 | 精品国产精品久久一区免费式 | 中文字幕 人妻熟女 | 中文无码精品a∨在线观看不卡 | 中文精品久久久久人妻不卡 | 又湿又紧又大又爽a视频国产 | 亚洲a无码综合a国产av中文 | 天干天干啦夜天干天2017 | 99精品久久毛片a片 | 一本精品99久久精品77 | 久久精品视频在线看15 | 亚洲综合另类小说色区 | 亚洲精品综合一区二区三区在线 | 免费无码午夜福利片69 | 欧美成人高清在线播放 | 成年美女黄网站色大免费全看 | 俺去俺来也www色官网 | 亚洲中文字幕av在天堂 | 男女爱爱好爽视频免费看 | 国产精品怡红院永久免费 | 300部国产真实乱 | 亚洲精品午夜无码电影网 | 亚洲一区二区观看播放 | 亚洲色www成人永久网址 | 免费观看黄网站 | 欧美真人作爱免费视频 | 国产亚洲精品久久久久久大师 | 亚洲人亚洲人成电影网站色 | 无码播放一区二区三区 | 成人无码视频免费播放 | 亚洲成av人综合在线观看 | 亚洲成a人片在线观看日本 | 国产在线精品一区二区高清不卡 | 久久人人爽人人爽人人片ⅴ | 亚洲色欲色欲天天天www | 男女性色大片免费网站 | 俄罗斯老熟妇色xxxx | 国产亚av手机在线观看 | 亚洲а∨天堂久久精品2021 | 97夜夜澡人人爽人人喊中国片 | 少妇人妻大乳在线视频 | 99久久精品午夜一区二区 | 国产av一区二区精品久久凹凸 | ass日本丰满熟妇pics | 人妻有码中文字幕在线 | 国产成人精品视频ⅴa片软件竹菊 | 日韩av无码一区二区三区不卡 | 国产精品美女久久久 | 久久无码专区国产精品s | 狠狠色噜噜狠狠狠狠7777米奇 | 性色欲情网站iwww九文堂 | 欧美日韩人成综合在线播放 | 国产精品视频免费播放 | 中文字幕av日韩精品一区二区 | 国产在热线精品视频 | 中文字幕日韩精品一区二区三区 | 亚洲精品无码国产 | 精品国产一区av天美传媒 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 亚洲成av人影院在线观看 | 丰腴饱满的极品熟妇 | 又大又硬又黄的免费视频 | 日日鲁鲁鲁夜夜爽爽狠狠 | 国产精品99久久精品爆乳 | 亚洲综合色区中文字幕 | 国产精品久免费的黄网站 | 日韩av激情在线观看 | 亚洲码国产精品高潮在线 | 精品aⅴ一区二区三区 | 又大又硬又爽免费视频 | 天海翼激烈高潮到腰振不止 | 在线 国产 欧美 亚洲 天堂 | 中文亚洲成a人片在线观看 | 国精产品一品二品国精品69xx | 国产精品嫩草久久久久 | 亚洲一区二区三区偷拍女厕 | 久久伊人色av天堂九九小黄鸭 | 国产黄在线观看免费观看不卡 | 丰满妇女强制高潮18xxxx | 亚洲人成无码网www | 午夜精品久久久久久久久 | 成人精品视频一区二区三区尤物 | 综合人妻久久一区二区精品 | 日韩人妻无码一区二区三区久久99 | 人妻熟女一区 | 暴力强奷在线播放无码 | 东京热男人av天堂 | 亚洲精品国偷拍自产在线观看蜜桃 | 精品熟女少妇av免费观看 | 免费播放一区二区三区 | 夜夜夜高潮夜夜爽夜夜爰爰 | 国产精品怡红院永久免费 | 精品无码国产自产拍在线观看蜜 | 综合网日日天干夜夜久久 | 国产一区二区三区影院 | 人妻人人添人妻人人爱 | 99久久精品无码一区二区毛片 | 欧美午夜特黄aaaaaa片 | 国产情侣作爱视频免费观看 | 久久人人爽人人人人片 | 国产极品视觉盛宴 | 亚洲综合另类小说色区 | 天下第一社区视频www日本 | 亚洲高清偷拍一区二区三区 | 久久久久久久久蜜桃 | 欧美喷潮久久久xxxxx | 偷窥村妇洗澡毛毛多 | 特黄特色大片免费播放器图片 | 日本精品少妇一区二区三区 | 无码一区二区三区在线 | 久久久久亚洲精品中文字幕 | 伊在人天堂亚洲香蕉精品区 | 98国产精品综合一区二区三区 | 亚洲精品www久久久 | 18黄暴禁片在线观看 | 久久无码中文字幕免费影院蜜桃 | 国产婷婷色一区二区三区在线 | 亚洲精品久久久久avwww潮水 | 国产午夜福利亚洲第一 | 蜜臀aⅴ国产精品久久久国产老师 | 久久人人97超碰a片精品 | 影音先锋中文字幕无码 | 精品国产国产综合精品 | 欧美野外疯狂做受xxxx高潮 | 中文字幕无线码 | 国产真实夫妇视频 | 久久视频在线观看精品 | 色妞www精品免费视频 | 精品国偷自产在线 | 性欧美熟妇videofreesex | 扒开双腿吃奶呻吟做受视频 | 美女黄网站人色视频免费国产 | 国产精品无码一区二区三区不卡 | 色婷婷欧美在线播放内射 | 亚洲国产精品美女久久久久 | 动漫av网站免费观看 | 久久久精品人妻久久影视 | 亚洲国产高清在线观看视频 | 成人毛片一区二区 | 无码福利日韩神码福利片 | 国产免费无码一区二区视频 | 国产亚洲精品精品国产亚洲综合 | 久久99精品久久久久久 | 暴力强奷在线播放无码 | 黑人玩弄人妻中文在线 | 久久无码人妻影院 | 国产网红无码精品视频 | 2019午夜福利不卡片在线 | 国产高潮视频在线观看 | 国产美女极度色诱视频www | 日本高清一区免费中文视频 | 撕开奶罩揉吮奶头视频 | 夜夜夜高潮夜夜爽夜夜爰爰 | 风流少妇按摩来高潮 | 日产精品99久久久久久 | 人人妻人人藻人人爽欧美一区 | 国产精品免费大片 | 亚洲国产午夜精品理论片 | 无码纯肉视频在线观看 | 国产成人人人97超碰超爽8 | 欧美国产日产一区二区 | 欧美精品在线观看 | 日日天日日夜日日摸 | 少妇太爽了在线观看 | 亚洲成a人一区二区三区 | 国产成人无码a区在线观看视频app | 性欧美牲交xxxxx视频 | 国产精品对白交换视频 | 亚洲男人av香蕉爽爽爽爽 | 国产 精品 自在自线 | 欧洲熟妇精品视频 | 2019nv天堂香蕉在线观看 | 日本乱人伦片中文三区 | 久久综合久久自在自线精品自 | 撕开奶罩揉吮奶头视频 | 国产av一区二区三区最新精品 | 亚洲无人区午夜福利码高清完整版 | 亚洲精品一区三区三区在线观看 | 国产精品久久久av久久久 | 国产艳妇av在线观看果冻传媒 | 狠狠cao日日穞夜夜穞av | 99久久精品日本一区二区免费 | 亚洲人交乣女bbw | 对白脏话肉麻粗话av | 中文字幕av日韩精品一区二区 | 东京热男人av天堂 | 精品无码一区二区三区爱欲 | 丰满少妇高潮惨叫视频 | 国内精品九九久久久精品 | 亚洲va中文字幕无码久久不卡 | 好爽又高潮了毛片免费下载 | 在线精品亚洲一区二区 | 国产精品自产拍在线观看 | 国产成人综合在线女婷五月99播放 | 久9re热视频这里只有精品 | 久久精品无码一区二区三区 | 亚洲中文字幕va福利 | 又粗又大又硬毛片免费看 | 亚洲日本va午夜在线电影 | 中文字幕人妻丝袜二区 | 国产真实乱对白精彩久久 | 中文字幕无码免费久久9一区9 | 一本久道高清无码视频 | 377p欧洲日本亚洲大胆 | 日韩少妇内射免费播放 | 精品国偷自产在线视频 | 中文字幕人妻丝袜二区 | 美女黄网站人色视频免费国产 | 狠狠色丁香久久婷婷综合五月 | 国内少妇偷人精品视频免费 | 免费人成网站视频在线观看 | 久久综合激激的五月天 | 极品嫩模高潮叫床 | 中文字幕乱码中文乱码51精品 | 无遮无挡爽爽免费视频 | 亚洲精品无码国产 | 18禁黄网站男男禁片免费观看 | 国产人妻精品一区二区三区不卡 | 人人妻人人藻人人爽欧美一区 | 日本饥渴人妻欲求不满 | 人妻aⅴ无码一区二区三区 | ass日本丰满熟妇pics | 99久久精品国产一区二区蜜芽 | 噜噜噜亚洲色成人网站 | 无套内谢的新婚少妇国语播放 | 狠狠色噜噜狠狠狠狠7777米奇 | 熟妇人妻激情偷爽文 | 麻豆国产人妻欲求不满谁演的 | 三上悠亚人妻中文字幕在线 | 欧美35页视频在线观看 | 狠狠色噜噜狠狠狠狠7777米奇 | 婷婷五月综合缴情在线视频 | 国产精品国产自线拍免费软件 | 中文字幕乱妇无码av在线 | 久久精品无码一区二区三区 | 2019nv天堂香蕉在线观看 | 77777熟女视频在线观看 а天堂中文在线官网 | 日韩亚洲欧美精品综合 | 熟女俱乐部五十路六十路av | 黑人玩弄人妻中文在线 | 丰满岳乱妇在线观看中字无码 | 亚拍精品一区二区三区探花 | 久久亚洲精品中文字幕无男同 | 欧美日本精品一区二区三区 | 俄罗斯老熟妇色xxxx | 水蜜桃色314在线观看 | 中文字幕av伊人av无码av | 国产成人无码av片在线观看不卡 | 免费看少妇作爱视频 | 未满小14洗澡无码视频网站 | 无遮挡国产高潮视频免费观看 | 国产人妻久久精品二区三区老狼 | 久久久精品人妻久久影视 | 久热国产vs视频在线观看 | 天下第一社区视频www日本 | a国产一区二区免费入口 | 国产免费久久精品国产传媒 | 夜夜躁日日躁狠狠久久av | 日本精品少妇一区二区三区 | 少妇无码av无码专区在线观看 | 精品 日韩 国产 欧美 视频 | 无码人妻精品一区二区三区不卡 | 久久97精品久久久久久久不卡 | 樱花草在线播放免费中文 | 成人无码影片精品久久久 | 国内揄拍国内精品人妻 | 377p欧洲日本亚洲大胆 | 国产农村乱对白刺激视频 | 亚洲一区二区三区 | 亚洲欧美中文字幕5发布 | 成人女人看片免费视频放人 | 精品偷拍一区二区三区在线看 | 中文字幕无码av波多野吉衣 | 久热国产vs视频在线观看 | 高潮毛片无遮挡高清免费视频 | 成人免费视频视频在线观看 免费 | 国产亚洲精品久久久久久国模美 | 久久视频在线观看精品 | aa片在线观看视频在线播放 | 国产区女主播在线观看 | 国产另类ts人妖一区二区 | 中国女人内谢69xxxxxa片 | 中文字幕人成乱码熟女app | 国产明星裸体无码xxxx视频 | 国产乱人偷精品人妻a片 | 成人亚洲精品久久久久 | 中文字幕无码免费久久9一区9 | 欧美 日韩 人妻 高清 中文 | 国内丰满熟女出轨videos | 76少妇精品导航 | 国产精品理论片在线观看 | 亚洲欧洲日本无在线码 | 日本饥渴人妻欲求不满 | 久久综合九色综合欧美狠狠 | 国产乱人伦偷精品视频 | 红桃av一区二区三区在线无码av | 乱中年女人伦av三区 | 久久久久99精品成人片 | 久久精品国产日本波多野结衣 | 人妻插b视频一区二区三区 | 成人欧美一区二区三区黑人免费 | 成人综合网亚洲伊人 | 成人无码视频免费播放 | 欧美黑人乱大交 | 中文字幕精品av一区二区五区 | 在线亚洲高清揄拍自拍一品区 | 无码人妻黑人中文字幕 | 狠狠亚洲超碰狼人久久 | 欧美喷潮久久久xxxxx | 色婷婷久久一区二区三区麻豆 | 精品一区二区不卡无码av | 国产亚洲精品久久久ai换 | 国产精品久久国产精品99 | 精品无码一区二区三区的天堂 | 无码精品国产va在线观看dvd | 亚洲午夜久久久影院 | 亚洲无人区午夜福利码高清完整版 | а√资源新版在线天堂 | 日韩精品乱码av一区二区 | 国产精品igao视频网 | 伦伦影院午夜理论片 | 色综合久久88色综合天天 | 亚洲综合精品香蕉久久网 | 双乳奶水饱满少妇呻吟 | 性开放的女人aaa片 | 亚洲国产精品毛片av不卡在线 | 波多野42部无码喷潮在线 | 夜精品a片一区二区三区无码白浆 | 1000部啪啪未满十八勿入下载 | 亚洲国产成人av在线观看 | 300部国产真实乱 | 一本久道高清无码视频 | 亚洲成a人片在线观看日本 | 蜜臀aⅴ国产精品久久久国产老师 | 国产情侣作爱视频免费观看 | 波多野42部无码喷潮在线 | 精品亚洲韩国一区二区三区 | www国产亚洲精品久久网站 | 欧洲vodafone精品性 | 国产国语老龄妇女a片 | 国产精品久久久久久亚洲影视内衣 | 亚洲欧美中文字幕5发布 | 中文字幕中文有码在线 | 国产亚洲日韩欧美另类第八页 | 国产人妻人伦精品 | 亚洲成a人片在线观看无码3d | 性欧美牲交在线视频 | 偷窥村妇洗澡毛毛多 | 亚洲成在人网站无码天堂 | 国产舌乚八伦偷品w中 | 国产无套粉嫩白浆在线 | 国产美女精品一区二区三区 | 在线亚洲高清揄拍自拍一品区 | 55夜色66夜色国产精品视频 | 久久精品中文字幕一区 | 国产精品久久久久7777 | 在线a亚洲视频播放在线观看 | 精品一区二区不卡无码av | 内射爽无广熟女亚洲 | 中文亚洲成a人片在线观看 | 激情五月综合色婷婷一区二区 | 欧美激情一区二区三区成人 | 亚洲国产精品无码一区二区三区 | 蜜臀aⅴ国产精品久久久国产老师 | 婷婷综合久久中文字幕蜜桃三电影 | 亚洲啪av永久无码精品放毛片 | 99久久精品午夜一区二区 | 中文字幕久久久久人妻 | 丰满少妇熟乱xxxxx视频 | 亚洲va欧美va天堂v国产综合 | 国产午夜无码精品免费看 | 亚洲精品国偷拍自产在线观看蜜桃 | 少妇激情av一区二区 | 性色av无码免费一区二区三区 | 日韩欧美中文字幕在线三区 | 久久久久亚洲精品中文字幕 | 福利一区二区三区视频在线观看 | 天天综合网天天综合色 | 激情内射日本一区二区三区 | 中文字幕无码人妻少妇免费 | 乱中年女人伦av三区 | 国产精品国产三级国产专播 | 成人免费视频视频在线观看 免费 | 日本一卡2卡3卡四卡精品网站 | 日韩成人一区二区三区在线观看 | 日韩av无码一区二区三区不卡 | 欧美兽交xxxx×视频 | 伦伦影院午夜理论片 | 国产真人无遮挡作爱免费视频 | 久久国语露脸国产精品电影 | 成人亚洲精品久久久久 | 中文字幕精品av一区二区五区 | 亚洲男人av天堂午夜在 | 日韩精品久久久肉伦网站 | 精品国产一区二区三区四区 | 欧美亚洲日韩国产人成在线播放 | 中文字幕av无码一区二区三区电影 | 给我免费的视频在线观看 | 国产午夜亚洲精品不卡 | 欧美自拍另类欧美综合图片区 | 欧美成人家庭影院 | 国精品人妻无码一区二区三区蜜柚 | 日本欧美一区二区三区乱码 | 狠狠亚洲超碰狼人久久 | 精品无码国产自产拍在线观看蜜 | 亚洲中文字幕无码中字 | 中文字幕无码日韩专区 | 成人免费无码大片a毛片 | 亚洲精品综合五月久久小说 | 强开小婷嫩苞又嫩又紧视频 | 亚洲欧美精品伊人久久 | 亚洲欧洲中文日韩av乱码 | 欧美激情综合亚洲一二区 | 成人av无码一区二区三区 | 无遮挡啪啪摇乳动态图 | 中文字幕人妻丝袜二区 | 成人女人看片免费视频放人 | 午夜福利不卡在线视频 | 国产一区二区三区四区五区加勒比 | 久久精品国产一区二区三区 | 亚洲欧洲中文日韩av乱码 | 欧美人与动性行为视频 | 国产内射爽爽大片视频社区在线 | 日日摸日日碰夜夜爽av | а√资源新版在线天堂 | 98国产精品综合一区二区三区 | 精品乱子伦一区二区三区 | √8天堂资源地址中文在线 | 精品成人av一区二区三区 | 欧美 日韩 亚洲 在线 | 少妇久久久久久人妻无码 | 小鲜肉自慰网站xnxx | 亚洲色www成人永久网址 | aⅴ在线视频男人的天堂 | 亚洲国产日韩a在线播放 | 久久伊人色av天堂九九小黄鸭 | 初尝人妻少妇中文字幕 | 国产激情精品一区二区三区 | 亚洲 欧美 激情 小说 另类 | 久久久久久亚洲精品a片成人 | 亚洲精品国产第一综合99久久 | 天下第一社区视频www日本 | 色诱久久久久综合网ywww | 色综合久久88色综合天天 | 国产亚洲精品久久久久久久 | 国产午夜亚洲精品不卡 | 青青青爽视频在线观看 | 久久久久人妻一区精品色欧美 | 野外少妇愉情中文字幕 | 亚洲欧洲中文日韩av乱码 | 久久成人a毛片免费观看网站 | 亚洲日本va中文字幕 | 精品无码国产一区二区三区av | 无码av中文字幕免费放 | 国产综合久久久久鬼色 | 国产精品无套呻吟在线 | 国产sm调教视频在线观看 | 又大又硬又黄的免费视频 | 免费人成在线视频无码 | 免费网站看v片在线18禁无码 | 亚洲色大成网站www | 国产精品久久久午夜夜伦鲁鲁 | 国产两女互慰高潮视频在线观看 | 亚洲一区二区三区国产精华液 | 夜先锋av资源网站 | 欧美精品一区二区精品久久 | 国产香蕉尹人综合在线观看 | 东北女人啪啪对白 | 久久99精品久久久久婷婷 | 亚洲の无码国产の无码影院 | 色综合久久久久综合一本到桃花网 | 亚洲一区二区三区在线观看网站 | 国产午夜福利亚洲第一 | 国内精品人妻无码久久久影院蜜桃 | 精品无码国产一区二区三区av | 丁香花在线影院观看在线播放 | 又大又黄又粗又爽的免费视频 | 婷婷丁香六月激情综合啪 | 在线а√天堂中文官网 | 久久综合狠狠综合久久综合88 | 香港三级日本三级妇三级 | 久久综合给合久久狠狠狠97色 | 少妇无码一区二区二三区 | 蜜臀aⅴ国产精品久久久国产老师 | 久久久精品456亚洲影院 | 精品国产精品久久一区免费式 | 人人超人人超碰超国产 | 少妇无码av无码专区在线观看 | 久久亚洲日韩精品一区二区三区 | 少妇性俱乐部纵欲狂欢电影 | 窝窝午夜理论片影院 | 欧美激情内射喷水高潮 | 性生交大片免费看女人按摩摩 | 亚洲中文字幕久久无码 | 日本大乳高潮视频在线观看 | 少妇太爽了在线观看 | 中文字幕av无码一区二区三区电影 | 国产成人久久精品流白浆 | 欧美人与善在线com | 蜜臀aⅴ国产精品久久久国产老师 | 中文字幕日产无线码一区 |