久久精品国产精品国产精品污,男人扒开添女人下部免费视频,一级国产69式性姿势免费视频,夜鲁夜鲁很鲁在线视频 视频,欧美丰满少妇一区二区三区,国产偷国产偷亚洲高清人乐享,中文 在线 日韩 亚洲 欧美,熟妇人妻无乱码中文字幕真矢织江,一区二区三区人妻制服国产

歡迎訪問 生活随笔!

生活随笔

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

编程问答

图像处理之-位图

發布時間: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

總結

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

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

51国偷自产一区二区三区 | 亚洲成a人一区二区三区 | 少妇被粗大的猛进出69影院 | 天堂亚洲免费视频 | 日韩 欧美 动漫 国产 制服 | 精品无人区无码乱码毛片国产 | 成人无码精品1区2区3区免费看 | 四虎4hu永久免费 | 成人性做爰aaa片免费看不忠 | 青青青爽视频在线观看 | 无码福利日韩神码福利片 | 国产亚洲精品久久久久久大师 | 午夜理论片yy44880影院 | 亚洲精品国偷拍自产在线观看蜜桃 | 欧美日本免费一区二区三区 | 无码av中文字幕免费放 | 天堂久久天堂av色综合 | 色欲人妻aaaaaaa无码 | 无码人妻丰满熟妇区毛片18 | 国产亚洲日韩欧美另类第八页 | 亚洲精品国偷拍自产在线麻豆 | 精品亚洲成av人在线观看 | 国产精品内射视频免费 | 无码av岛国片在线播放 | 亚洲成a人片在线观看无码 | 九月婷婷人人澡人人添人人爽 | 精品国产一区av天美传媒 | 亚洲精品午夜国产va久久成人 | 色欲久久久天天天综合网精品 | 青春草在线视频免费观看 | 超碰97人人做人人爱少妇 | 麻豆精产国品 | 色一情一乱一伦一区二区三欧美 | 超碰97人人做人人爱少妇 | 一个人看的www免费视频在线观看 | 亚拍精品一区二区三区探花 | 大肉大捧一进一出好爽视频 | 麻花豆传媒剧国产免费mv在线 | 亚洲成a人一区二区三区 | 亚洲阿v天堂在线 | 丝袜足控一区二区三区 | 亚洲综合伊人久久大杳蕉 | 亚洲国产精品美女久久久久 | 国产口爆吞精在线视频 | 任你躁国产自任一区二区三区 | 性色欲网站人妻丰满中文久久不卡 | 国产激情精品一区二区三区 | 88国产精品欧美一区二区三区 | 国产深夜福利视频在线 | 99精品国产综合久久久久五月天 | 国产人妻人伦精品 | 久久99热只有频精品8 | 性欧美熟妇videofreesex | 无遮挡啪啪摇乳动态图 | 久久久中文字幕日本无吗 | 狠狠色噜噜狠狠狠7777奇米 | 性欧美疯狂xxxxbbbb | 中文字幕人妻无码一区二区三区 | 色偷偷av老熟女 久久精品人妻少妇一区二区三区 | 亚洲精品欧美二区三区中文字幕 | 波多野结衣 黑人 | 国产精品久久精品三级 | 97精品国产97久久久久久免费 | 久久99国产综合精品 | 少妇人妻偷人精品无码视频 | 中文精品久久久久人妻不卡 | 99久久人妻精品免费一区 | 久激情内射婷内射蜜桃人妖 | 亚洲 日韩 欧美 成人 在线观看 | 久久国产36精品色熟妇 | 成人av无码一区二区三区 | 高潮喷水的毛片 | 丰满少妇高潮惨叫视频 | 国产精品18久久久久久麻辣 | 国产精品久久久久久久9999 | 亚洲国产精品一区二区第一页 | 国产亚洲精品久久久久久久久动漫 | 精品久久8x国产免费观看 | 亚洲成av人综合在线观看 | 亚洲欧洲无卡二区视頻 | 中文字幕无码人妻少妇免费 | 成人影院yy111111在线观看 | 亚洲の无码国产の无码影院 | 国产亚洲精品精品国产亚洲综合 | 欧美熟妇另类久久久久久多毛 | 无码午夜成人1000部免费视频 | 中文无码精品a∨在线观看不卡 | 丝袜美腿亚洲一区二区 | 久激情内射婷内射蜜桃人妖 | 国产精品无套呻吟在线 | 99久久婷婷国产综合精品青草免费 | 成人免费无码大片a毛片 | 日韩欧美群交p片內射中文 | 色婷婷欧美在线播放内射 | 日本高清一区免费中文视频 | 国产精品久久久久影院嫩草 | 国产一区二区三区日韩精品 | 日本精品少妇一区二区三区 | 亚洲另类伦春色综合小说 | 啦啦啦www在线观看免费视频 | 亚洲の无码国产の无码步美 | 高清国产亚洲精品自在久久 | 日日橹狠狠爱欧美视频 | 国产无遮挡又黄又爽免费视频 | 国产精品丝袜黑色高跟鞋 | 四虎永久在线精品免费网址 | 欧美国产亚洲日韩在线二区 | 无码人妻久久一区二区三区不卡 | 国产精品第一区揄拍无码 | 熟女体下毛毛黑森林 | 小sao货水好多真紧h无码视频 | 日本一区二区三区免费播放 | 永久黄网站色视频免费直播 | 久久99精品久久久久婷婷 | 亚洲毛片av日韩av无码 | 亚洲国产欧美日韩精品一区二区三区 | 97精品国产97久久久久久免费 | 性色av无码免费一区二区三区 | 亚洲一区av无码专区在线观看 | 欧美老熟妇乱xxxxx | 亚洲成a人片在线观看无码 | 成人一在线视频日韩国产 | 久久综合九色综合欧美狠狠 | 东京无码熟妇人妻av在线网址 | 真人与拘做受免费视频一 | 国产精华av午夜在线观看 | 国产成人精品优优av | 久久精品国产一区二区三区 | 亚洲毛片av日韩av无码 | 精品久久综合1区2区3区激情 | 国内揄拍国内精品少妇国语 | 四虎国产精品免费久久 | 欧美高清在线精品一区 | 国产麻豆精品精东影业av网站 | 久久国产精品偷任你爽任你 | 麻豆果冻传媒2021精品传媒一区下载 | 国产精品久久久久影院嫩草 | 亚洲色www成人永久网址 | 国产精品人妻一区二区三区四 | 人妻中文无码久热丝袜 | 国产亚洲视频中文字幕97精品 | 国产亚洲精品久久久久久久久动漫 | 久久97精品久久久久久久不卡 | 18精品久久久无码午夜福利 | 国产亲子乱弄免费视频 | 国产高清不卡无码视频 | 天天爽夜夜爽夜夜爽 | 在线观看国产午夜福利片 | 久久精品国产大片免费观看 | 真人与拘做受免费视频 | 人妻少妇被猛烈进入中文字幕 | 成 人 免费观看网站 | 性欧美牲交在线视频 | 一个人看的www免费视频在线观看 | 国产av人人夜夜澡人人爽麻豆 | 黑人粗大猛烈进出高潮视频 | 亚洲日本在线电影 | 国产午夜无码视频在线观看 | 国产热a欧美热a在线视频 | 领导边摸边吃奶边做爽在线观看 | 欧美人与善在线com | 国产suv精品一区二区五 | 久久婷婷五月综合色国产香蕉 | 老司机亚洲精品影院 | 亚洲综合伊人久久大杳蕉 | 亚洲 a v无 码免 费 成 人 a v | 一本久道久久综合狠狠爱 | 麻豆国产丝袜白领秘书在线观看 | 精品无人区无码乱码毛片国产 | 国产av剧情md精品麻豆 | 欧洲vodafone精品性 | 国产精品久久久 | 四虎4hu永久免费 | 午夜福利不卡在线视频 | 久久伊人色av天堂九九小黄鸭 | 欧美日韩人成综合在线播放 | 中文字幕av伊人av无码av | 双乳奶水饱满少妇呻吟 | 国产明星裸体无码xxxx视频 | 少妇愉情理伦片bd | 成人性做爰aaa片免费看不忠 | 免费观看黄网站 | 性色欲情网站iwww九文堂 | 人人妻人人澡人人爽精品欧美 | 亚洲国产精品无码一区二区三区 | 欧美 日韩 亚洲 在线 | 天天燥日日燥 | 欧美性生交xxxxx久久久 | 欧洲美熟女乱又伦 | 18黄暴禁片在线观看 | 亚洲综合伊人久久大杳蕉 | 亚洲一区二区三区无码久久 | 99久久精品午夜一区二区 | 人人爽人人澡人人人妻 | 亚洲日本va午夜在线电影 | 成熟妇人a片免费看网站 | 一本久道久久综合婷婷五月 | √8天堂资源地址中文在线 | 强辱丰满人妻hd中文字幕 | 免费观看又污又黄的网站 | 欧美日韩精品 | 中文字幕亚洲情99在线 | 国产女主播喷水视频在线观看 | 欧美成人家庭影院 | 亚洲成av人影院在线观看 | 精品欧美一区二区三区久久久 | 内射老妇bbwx0c0ck | 久久午夜夜伦鲁鲁片无码免费 | 97无码免费人妻超级碰碰夜夜 | 亚洲无人区午夜福利码高清完整版 | 男女超爽视频免费播放 | 伊在人天堂亚洲香蕉精品区 | 麻豆md0077饥渴少妇 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 在线成人www免费观看视频 | 亚洲а∨天堂久久精品2021 | 一区二区传媒有限公司 | 国产精品人人爽人人做我的可爱 | 真人与拘做受免费视频一 | 国产精品无码一区二区三区不卡 | 中文字幕 人妻熟女 | 伊人久久大香线蕉av一区二区 | 亚洲成a人片在线观看无码3d | 久久成人a毛片免费观看网站 | 国产亚洲精品久久久久久大师 | 77777熟女视频在线观看 а天堂中文在线官网 | www一区二区www免费 | 初尝人妻少妇中文字幕 | 久久精品人人做人人综合 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 性色av无码免费一区二区三区 | 亚洲国产精品一区二区第一页 | 77777熟女视频在线观看 а天堂中文在线官网 | 2020最新国产自产精品 | 国产精品高潮呻吟av久久 | 亚洲中文字幕在线无码一区二区 | 日韩精品无码免费一区二区三区 | 亚洲精品久久久久久一区二区 | 四虎4hu永久免费 | 美女黄网站人色视频免费国产 | 国产欧美熟妇另类久久久 | 老熟妇乱子伦牲交视频 | 精品亚洲韩国一区二区三区 | 性生交大片免费看女人按摩摩 | 免费视频欧美无人区码 | 任你躁国产自任一区二区三区 | 亚洲精品国偷拍自产在线麻豆 | 欧美性猛交xxxx富婆 | 97精品人妻一区二区三区香蕉 | 精品国精品国产自在久国产87 | 欧美丰满老熟妇xxxxx性 | 俺去俺来也在线www色官网 | а天堂中文在线官网 | 国产女主播喷水视频在线观看 | 国产香蕉97碰碰久久人人 | 亚洲熟悉妇女xxx妇女av | 国内老熟妇对白xxxxhd | 欧美xxxxx精品 | 国产精品久久精品三级 | 国产乡下妇女做爰 | 国产成人综合美国十次 | 国产高潮视频在线观看 | 国产明星裸体无码xxxx视频 | 亚洲狠狠色丁香婷婷综合 | 中文字幕无码免费久久9一区9 | 四虎4hu永久免费 | 欧洲极品少妇 | 无人区乱码一区二区三区 | 成人免费视频一区二区 | 国产午夜亚洲精品不卡 | 激情内射日本一区二区三区 | 亚洲毛片av日韩av无码 | 国产av人人夜夜澡人人爽麻豆 | 无码人妻丰满熟妇区五十路百度 | 国产精品香蕉在线观看 | 少妇性俱乐部纵欲狂欢电影 | 思思久久99热只有频精品66 | 亚洲第一网站男人都懂 | 欧美日本精品一区二区三区 | 99国产欧美久久久精品 | 国产内射爽爽大片视频社区在线 | aa片在线观看视频在线播放 | 亚洲成av人影院在线观看 | 国产色xx群视频射精 | 99久久人妻精品免费一区 | 久久天天躁狠狠躁夜夜免费观看 | 少妇无套内谢久久久久 | 国产办公室秘书无码精品99 | 国产午夜福利100集发布 | 蜜桃视频插满18在线观看 | 少妇厨房愉情理9仑片视频 | 天天摸天天透天天添 | 国产精品美女久久久 | 亚洲成a人片在线观看无码 | 亚洲一区二区三区在线观看网站 | 亚洲精品成人福利网站 | 亚洲经典千人经典日产 | 露脸叫床粗话东北少妇 | 亚洲一区二区三区含羞草 | 日韩少妇白浆无码系列 | 7777奇米四色成人眼影 | 国产精品久久福利网站 | 国产乱人伦偷精品视频 | 成人性做爰aaa片免费看不忠 | 少妇厨房愉情理9仑片视频 | 在线欧美精品一区二区三区 | 亚洲乱码国产乱码精品精 | 国产精品久久久久9999小说 | 欧美 日韩 人妻 高清 中文 | 狂野欧美性猛xxxx乱大交 | 国产亚洲精品精品国产亚洲综合 | 久久zyz资源站无码中文动漫 | 无码福利日韩神码福利片 | 精品偷拍一区二区三区在线看 | 国内精品人妻无码久久久影院 | 狠狠亚洲超碰狼人久久 | 欧美日本日韩 | 国语自产偷拍精品视频偷 | 国产无套内射久久久国产 | 日本护士毛茸茸高潮 | 黑人巨大精品欧美黑寡妇 | 在线 国产 欧美 亚洲 天堂 | 久久亚洲国产成人精品性色 | 色婷婷久久一区二区三区麻豆 | 永久免费精品精品永久-夜色 | 天天做天天爱天天爽综合网 | 免费看男女做好爽好硬视频 | 女人被爽到呻吟gif动态图视看 | 日日橹狠狠爱欧美视频 | 少妇无套内谢久久久久 | 在线欧美精品一区二区三区 | 日韩成人一区二区三区在线观看 | 亚洲精品国产精品乱码视色 | 激情国产av做激情国产爱 | 久久人人爽人人爽人人片av高清 | 国产麻豆精品一区二区三区v视界 | 一本无码人妻在中文字幕免费 | 亚洲日韩乱码中文无码蜜桃臀网站 | 男女爱爱好爽视频免费看 | 久久久久99精品成人片 | 亚洲热妇无码av在线播放 | 国产超碰人人爽人人做人人添 | 台湾无码一区二区 | 亚洲va中文字幕无码久久不卡 | 久久精品国产大片免费观看 | 人人澡人人妻人人爽人人蜜桃 | 国产乱子伦视频在线播放 | 日本大乳高潮视频在线观看 | 亚洲精品一区二区三区婷婷月 | 人人爽人人爽人人片av亚洲 | 四虎国产精品一区二区 | 国内精品久久久久久中文字幕 | 女人被男人爽到呻吟的视频 | 少妇无码av无码专区在线观看 | 人妻少妇精品无码专区二区 | 国产精品久久久久久亚洲毛片 | 国产亚洲美女精品久久久2020 | 性做久久久久久久久 | 中文字幕人妻丝袜二区 | 67194成是人免费无码 | 色综合视频一区二区三区 | 老头边吃奶边弄进去呻吟 | 亚洲国产日韩a在线播放 | 少妇被粗大的猛进出69影院 | 国产美女极度色诱视频www | 亚洲精品国产品国语在线观看 | 色综合久久久无码网中文 | 欧美阿v高清资源不卡在线播放 | 国产精品理论片在线观看 | 捆绑白丝粉色jk震动捧喷白浆 | 人妻少妇精品视频专区 | 色婷婷综合激情综在线播放 | 久久www免费人成人片 | 无遮挡国产高潮视频免费观看 | 无码国产激情在线观看 | 爱做久久久久久 | 亚洲精品久久久久avwww潮水 | 妺妺窝人体色www在线小说 | 亚洲综合无码久久精品综合 | 亚洲综合无码一区二区三区 | 亚洲一区二区三区四区 | 久久亚洲日韩精品一区二区三区 | а√资源新版在线天堂 | 亚洲熟女一区二区三区 | 亚拍精品一区二区三区探花 | 日韩精品无码一本二本三本色 | 中文字幕无码乱人伦 | 国产成人精品必看 | 蜜桃av抽搐高潮一区二区 | 精品成在人线av无码免费看 | 国产精品无码久久av | 国产精品毛多多水多 | 国产午夜无码精品免费看 | 亚洲国产精品久久久久久 | 丝袜 中出 制服 人妻 美腿 | 亚洲成a人片在线观看日本 | 人妻熟女一区 | 免费观看的无遮挡av | 日韩亚洲欧美中文高清在线 | 国产亚洲精品久久久久久 | 中文字幕 亚洲精品 第1页 | 97无码免费人妻超级碰碰夜夜 | 少妇性荡欲午夜性开放视频剧场 | 人妻aⅴ无码一区二区三区 | 色诱久久久久综合网ywww | 国产成人无码av片在线观看不卡 | 亚洲精品中文字幕乱码 | 动漫av网站免费观看 | 东京热一精品无码av | 亚洲国产一区二区三区在线观看 | 国产美女极度色诱视频www | 久久精品国产99精品亚洲 | 人妻天天爽夜夜爽一区二区 | 国产婷婷色一区二区三区在线 | 国产成人精品久久亚洲高清不卡 | 精品人妻中文字幕有码在线 | 欧美人与牲动交xxxx | 久久精品人人做人人综合 | 国产精品久久久久久亚洲影视内衣 | 帮老师解开蕾丝奶罩吸乳网站 | 日本精品久久久久中文字幕 | 一本大道久久东京热无码av | 欧美激情内射喷水高潮 | 久久国产自偷自偷免费一区调 | 中文字幕av伊人av无码av | 国产精品二区一区二区aⅴ污介绍 | 久久五月精品中文字幕 | 无码乱肉视频免费大全合集 | 亚洲成色www久久网站 | 国产精品欧美成人 | 曰韩少妇内射免费播放 | 色妞www精品免费视频 | 大乳丰满人妻中文字幕日本 | 初尝人妻少妇中文字幕 | 无码国产色欲xxxxx视频 | 夜夜躁日日躁狠狠久久av | 老太婆性杂交欧美肥老太 | 国产精品免费大片 | 日日摸夜夜摸狠狠摸婷婷 | 激情内射日本一区二区三区 | 全黄性性激高免费视频 | 国产偷自视频区视频 | 在线看片无码永久免费视频 | 久精品国产欧美亚洲色aⅴ大片 | 国产精品亚洲а∨无码播放麻豆 | 久久精品99久久香蕉国产色戒 | 丝袜美腿亚洲一区二区 | 久久97精品久久久久久久不卡 | 国产成人一区二区三区别 | 成人免费视频一区二区 | 亚洲一区av无码专区在线观看 | 亚洲区小说区激情区图片区 | 成人性做爰aaa片免费看 | 亚洲一区二区三区含羞草 | 色综合久久88色综合天天 | 亚洲乱码国产乱码精品精 | 天天摸天天透天天添 | aⅴ亚洲 日韩 色 图网站 播放 | 国产明星裸体无码xxxx视频 | 狂野欧美性猛交免费视频 | 成人女人看片免费视频放人 | 国产精品二区一区二区aⅴ污介绍 | 亚洲の无码国产の无码影院 | 久久人人爽人人人人片 | 欧美成人高清在线播放 | 激情五月综合色婷婷一区二区 | 精品一区二区三区波多野结衣 | 精品国产成人一区二区三区 | 国产av剧情md精品麻豆 | 久久人人97超碰a片精品 | 久久国产精品偷任你爽任你 | 欧美三级a做爰在线观看 | 精品国产麻豆免费人成网站 | 国产精品第一国产精品 | 久久久精品欧美一区二区免费 | 国产偷国产偷精品高清尤物 | 青草视频在线播放 | 久久这里只有精品视频9 | 国产手机在线αⅴ片无码观看 | 97久久超碰中文字幕 | 婷婷五月综合激情中文字幕 | 毛片内射-百度 | 亚洲第一网站男人都懂 | 成人亚洲精品久久久久 | 欧美国产日韩亚洲中文 | 东京热一精品无码av | 午夜免费福利小电影 | 久久久久亚洲精品男人的天堂 | 精品人妻中文字幕有码在线 | 欧美第一黄网免费网站 | 特黄特色大片免费播放器图片 | 少妇人妻av毛片在线看 | 亚洲人成网站免费播放 | 中文字幕乱码人妻二区三区 | 丁香花在线影院观看在线播放 | 国产成人午夜福利在线播放 | 亚洲国产成人a精品不卡在线 | 色综合久久88色综合天天 | 亚洲а∨天堂久久精品2021 | 四虎永久在线精品免费网址 | 精品 日韩 国产 欧美 视频 | 亚洲色成人中文字幕网站 | 未满小14洗澡无码视频网站 | 波多野结衣av一区二区全免费观看 | 久久午夜无码鲁丝片午夜精品 | 蜜桃av抽搐高潮一区二区 | 无码播放一区二区三区 | 男人扒开女人内裤强吻桶进去 | 性色av无码免费一区二区三区 | 国产疯狂伦交大片 | 中文字幕人妻无码一夲道 | 一本加勒比波多野结衣 | 窝窝午夜理论片影院 | 欧美一区二区三区视频在线观看 | 无码午夜成人1000部免费视频 | 2020最新国产自产精品 | 精品国产精品久久一区免费式 | 中文字幕无码日韩专区 | 欧美兽交xxxx×视频 | 成在人线av无码免费 | 麻豆国产97在线 | 欧洲 | 日韩成人一区二区三区在线观看 | 97无码免费人妻超级碰碰夜夜 | 疯狂三人交性欧美 | 乱码av麻豆丝袜熟女系列 | 国产av一区二区精品久久凹凸 | 澳门永久av免费网站 | 亚洲一区二区三区 | 精品国产麻豆免费人成网站 | 欧美自拍另类欧美综合图片区 | 亚洲日韩一区二区三区 | 国内精品一区二区三区不卡 | 无码一区二区三区在线观看 | 亚洲va欧美va天堂v国产综合 | 中文字幕av无码一区二区三区电影 | 国语自产偷拍精品视频偷 | 大肉大捧一进一出好爽视频 | 99久久婷婷国产综合精品青草免费 | 国产成人一区二区三区在线观看 | 久久精品国产一区二区三区 | 色欲综合久久中文字幕网 | 乱中年女人伦av三区 | 久久亚洲国产成人精品性色 | 国产成人综合色在线观看网站 | 日韩精品久久久肉伦网站 | 青青青手机频在线观看 | 久久综合色之久久综合 | 啦啦啦www在线观看免费视频 | 欧美日韩在线亚洲综合国产人 | 欧美兽交xxxx×视频 | av在线亚洲欧洲日产一区二区 | 亚洲日韩精品欧美一区二区 | 色欲综合久久中文字幕网 | 狠狠cao日日穞夜夜穞av | 中文亚洲成a人片在线观看 | 亚洲精品成人福利网站 | 亚洲理论电影在线观看 | 亚洲欧美国产精品专区久久 | 国产精华av午夜在线观看 | a在线观看免费网站大全 | 国产乱码精品一品二品 | 蜜桃视频插满18在线观看 | 人妻少妇精品久久 | 中文字幕av无码一区二区三区电影 | 一本久久a久久精品vr综合 | 蜜桃臀无码内射一区二区三区 | 久久综合狠狠综合久久综合88 | 人妻互换免费中文字幕 | 欧美熟妇另类久久久久久多毛 | 国产热a欧美热a在线视频 | 亚洲日韩av一区二区三区中文 | 亚洲理论电影在线观看 | 97夜夜澡人人爽人人喊中国片 | 人妻少妇被猛烈进入中文字幕 | 荫蒂添的好舒服视频囗交 | 亚洲经典千人经典日产 | 在线а√天堂中文官网 | 久久婷婷五月综合色国产香蕉 | 亚洲中文字幕久久无码 | 亚洲区小说区激情区图片区 | 免费网站看v片在线18禁无码 | 亚洲热妇无码av在线播放 | 亚洲一区二区三区无码久久 | 成人无码精品一区二区三区 | 天堂亚洲2017在线观看 | 香蕉久久久久久av成人 | 国产综合在线观看 | 鲁一鲁av2019在线 | 人人爽人人澡人人高潮 | 亚洲一区二区三区香蕉 | 欧美日韩一区二区三区自拍 | 国产精品国产三级国产专播 | 麻豆精产国品 | 日韩欧美中文字幕公布 | 国产午夜精品一区二区三区嫩草 | 色一情一乱一伦一区二区三欧美 | 国产精品亚洲五月天高清 | 老子影院午夜精品无码 | 日本www一道久久久免费榴莲 | 久久久婷婷五月亚洲97号色 | 久久午夜无码鲁丝片 | 国产欧美亚洲精品a | 国产精品a成v人在线播放 | 午夜理论片yy44880影院 | 桃花色综合影院 | 欧美性生交xxxxx久久久 | 中文毛片无遮挡高清免费 | 亚洲s码欧洲m码国产av | 免费人成在线观看网站 | 午夜无码区在线观看 | 亚洲s码欧洲m码国产av | 人妻天天爽夜夜爽一区二区 | 国产一区二区三区精品视频 | 骚片av蜜桃精品一区 | 一本色道婷婷久久欧美 | 亚洲国产av精品一区二区蜜芽 | av在线亚洲欧洲日产一区二区 | 久久99国产综合精品 | 国内精品人妻无码久久久影院蜜桃 | 全球成人中文在线 | 久久久久国色av免费观看性色 | 色爱情人网站 | 一本久久a久久精品vr综合 | 正在播放东北夫妻内射 | 婷婷丁香五月天综合东京热 | 国产情侣作爱视频免费观看 | 久久久国产精品无码免费专区 | 免费国产黄网站在线观看 | 国产9 9在线 | 中文 | 日韩在线不卡免费视频一区 | 国产农村妇女高潮大叫 | 无码中文字幕色专区 | 国语自产偷拍精品视频偷 | 欧美自拍另类欧美综合图片区 | 97资源共享在线视频 | 国产人成高清在线视频99最全资源 | 国产色xx群视频射精 | 黑人玩弄人妻中文在线 | 国产亚洲精品久久久久久久久动漫 | 国产熟女一区二区三区四区五区 | 国产精品香蕉在线观看 | 亚洲色欲色欲欲www在线 | 人妻少妇被猛烈进入中文字幕 | 色诱久久久久综合网ywww | 欧美freesex黑人又粗又大 | 色婷婷av一区二区三区之红樱桃 | 男女猛烈xx00免费视频试看 | 玩弄少妇高潮ⅹxxxyw | 日韩人妻无码一区二区三区久久99 | 亚洲天堂2017无码中文 | 妺妺窝人体色www婷婷 | 欧洲vodafone精品性 | 日本熟妇浓毛 | 熟女俱乐部五十路六十路av | 国产热a欧美热a在线视频 | 天天躁日日躁狠狠躁免费麻豆 | 精品国偷自产在线 | 国产人妻人伦精品 | 欧美精品免费观看二区 | 中文字幕无码日韩专区 | 国产凸凹视频一区二区 | 久久久精品成人免费观看 | 好男人www社区 | 亚洲码国产精品高潮在线 | 久久久久se色偷偷亚洲精品av | 青青久在线视频免费观看 | 亚洲va中文字幕无码久久不卡 | 久久天天躁狠狠躁夜夜免费观看 | 自拍偷自拍亚洲精品10p | 亚洲精品久久久久中文第一幕 | 图片小说视频一区二区 | 国产乱人偷精品人妻a片 | 国产人妻大战黑人第1集 | 狠狠噜狠狠狠狠丁香五月 | 男女作爱免费网站 | 乱码av麻豆丝袜熟女系列 | 成人无码精品一区二区三区 | 自拍偷自拍亚洲精品被多人伦好爽 | 亚洲精品无码人妻无码 | 一本久久伊人热热精品中文字幕 | 亚洲一区二区三区四区 | 午夜理论片yy44880影院 | 玩弄少妇高潮ⅹxxxyw | 欧美国产亚洲日韩在线二区 | 亚洲国产午夜精品理论片 | 六月丁香婷婷色狠狠久久 | 亚洲小说图区综合在线 | 国产精品国产自线拍免费软件 | 少妇厨房愉情理9仑片视频 | 波多野结衣av一区二区全免费观看 | 久久熟妇人妻午夜寂寞影院 | 亚洲国产精品久久久久久 | 日欧一片内射va在线影院 | 久久久久av无码免费网 | 丰满人妻一区二区三区免费视频 | 色综合久久中文娱乐网 | 国产精品igao视频网 | 在线 国产 欧美 亚洲 天堂 | 综合人妻久久一区二区精品 | 国产亚洲欧美在线专区 | 内射老妇bbwx0c0ck | 综合人妻久久一区二区精品 | 人妻插b视频一区二区三区 | 国产精品第一国产精品 | 国产亚洲精品精品国产亚洲综合 | 日本精品少妇一区二区三区 | 帮老师解开蕾丝奶罩吸乳网站 | 中文字幕av无码一区二区三区电影 | 久久国语露脸国产精品电影 | 伊人久久大香线蕉亚洲 | 18禁黄网站男男禁片免费观看 | 亚洲阿v天堂在线 | 亚洲综合伊人久久大杳蕉 | 国产精品a成v人在线播放 | 人妻aⅴ无码一区二区三区 | 日韩视频 中文字幕 视频一区 | 久久亚洲精品中文字幕无男同 | 久久人人爽人人爽人人片av高清 | 中文无码成人免费视频在线观看 | 最新国产麻豆aⅴ精品无码 | 岛国片人妻三上悠亚 | 国产成人综合美国十次 | 久久久久久久人妻无码中文字幕爆 | 国产av剧情md精品麻豆 | 麻豆精品国产精华精华液好用吗 | 亚洲毛片av日韩av无码 | 波多野结衣高清一区二区三区 | 成人无码精品1区2区3区免费看 | 国模大胆一区二区三区 | 精品国精品国产自在久国产87 | 久久亚洲a片com人成 | 久久综合激激的五月天 | 成人动漫在线观看 | 人人妻人人澡人人爽人人精品 | 国产精品.xx视频.xxtv | 久久久精品国产sm最大网站 | 亚洲日韩av一区二区三区中文 | 好屌草这里只有精品 | 国产人妻人伦精品 | 国内揄拍国内精品少妇国语 | 国产超碰人人爽人人做人人添 | 久久人人97超碰a片精品 | 亚洲精品国产第一综合99久久 | 欧美性色19p | 亚洲人成影院在线观看 | 精品国产青草久久久久福利 | 国产精品视频免费播放 | 午夜理论片yy44880影院 | 国产精品无码一区二区桃花视频 | 日韩 欧美 动漫 国产 制服 | 国产精品久久久久久久9999 | 亚洲精品无码国产 | 日韩人妻无码一区二区三区久久99 | 国产精品第一国产精品 | 又大又紧又粉嫩18p少妇 | 丝袜人妻一区二区三区 | 乱码av麻豆丝袜熟女系列 | 国产精品视频免费播放 | 最新国产乱人伦偷精品免费网站 | 亚洲精品综合一区二区三区在线 | 国产亚洲精品久久久久久大师 | 午夜理论片yy44880影院 | 亚洲精品中文字幕乱码 | 3d动漫精品啪啪一区二区中 | 国产精品人人爽人人做我的可爱 | 97久久国产亚洲精品超碰热 | 亚洲乱码日产精品bd | 荫蒂添的好舒服视频囗交 | 人妻人人添人妻人人爱 | 久久综合给合久久狠狠狠97色 | 永久免费观看美女裸体的网站 | 无码av免费一区二区三区试看 | 久久精品国产大片免费观看 | 人妻少妇精品无码专区动漫 | 日本大香伊一区二区三区 | 亚洲成av人片在线观看无码不卡 | 狠狠色色综合网站 | 色婷婷欧美在线播放内射 | 精品国产aⅴ无码一区二区 | 5858s亚洲色大成网站www | 日韩人妻无码中文字幕视频 | √8天堂资源地址中文在线 | 国内揄拍国内精品少妇国语 | 免费乱码人妻系列无码专区 | 人人澡人摸人人添 | 精品国产aⅴ无码一区二区 | 日本护士毛茸茸高潮 | 色一情一乱一伦一视频免费看 | 久久久婷婷五月亚洲97号色 | 日韩 欧美 动漫 国产 制服 | 噜噜噜亚洲色成人网站 | 欧美 日韩 人妻 高清 中文 | 午夜精品一区二区三区的区别 | 午夜精品一区二区三区的区别 | 蜜臀aⅴ国产精品久久久国产老师 | 免费观看黄网站 | 无码免费一区二区三区 | 欧美人与禽zoz0性伦交 | 欧美第一黄网免费网站 | 强开小婷嫩苞又嫩又紧视频 | 高潮毛片无遮挡高清免费 | 美女黄网站人色视频免费国产 | 久在线观看福利视频 | 在线欧美精品一区二区三区 | 亚洲综合无码一区二区三区 | 成人无码精品一区二区三区 | 中文字幕日韩精品一区二区三区 | 久久久国产一区二区三区 | 亚洲欧美精品aaaaaa片 | 日本护士毛茸茸高潮 | 色欲综合久久中文字幕网 | 少妇性俱乐部纵欲狂欢电影 | 超碰97人人做人人爱少妇 | 狠狠cao日日穞夜夜穞av | 丝袜美腿亚洲一区二区 | 国产一区二区三区精品视频 | 欧美国产日韩久久mv | 精品久久久中文字幕人妻 | 国产精品无码久久av | 亚洲精品国产精品乱码不卡 | 国产精品丝袜黑色高跟鞋 | 波多野结衣一区二区三区av免费 | 国产精品igao视频网 | 久久国产劲爆∧v内射 | 久久久久久亚洲精品a片成人 | 国产午夜无码视频在线观看 | 免费无码肉片在线观看 | 奇米影视7777久久精品 | 欧美变态另类xxxx | 久久精品99久久香蕉国产色戒 | 荫蒂添的好舒服视频囗交 | 国产成人人人97超碰超爽8 | 国产片av国语在线观看 | 四虎永久在线精品免费网址 | 高清无码午夜福利视频 | 久久久久se色偷偷亚洲精品av | 麻豆av传媒蜜桃天美传媒 | 激情综合激情五月俺也去 | 精品国产一区二区三区av 性色 | 蜜臀aⅴ国产精品久久久国产老师 | 国产亚洲欧美日韩亚洲中文色 | 日韩精品久久久肉伦网站 | 欧美丰满熟妇xxxx | 日韩成人一区二区三区在线观看 | 国产精品二区一区二区aⅴ污介绍 | 欧美性猛交xxxx富婆 | 亚洲の无码国产の无码影院 | 久久精品人人做人人综合 | 久久精品丝袜高跟鞋 | 精品熟女少妇av免费观看 | 中文字幕乱码亚洲无线三区 | 久久熟妇人妻午夜寂寞影院 | 精品国产成人一区二区三区 | 中文字幕亚洲情99在线 | 亚洲成a人片在线观看日本 | 亚洲va欧美va天堂v国产综合 | 激情人妻另类人妻伦 | 成人一区二区免费视频 | 国产精品第一国产精品 | 狠狠色噜噜狠狠狠狠7777米奇 | 无码成人精品区在线观看 | 纯爱无遮挡h肉动漫在线播放 | 国产明星裸体无码xxxx视频 | 国产suv精品一区二区五 | 狠狠噜狠狠狠狠丁香五月 | 国产精品久久久久9999小说 | 性生交片免费无码看人 | 东京一本一道一二三区 | 日韩精品成人一区二区三区 | 亚洲区小说区激情区图片区 | 成人性做爰aaa片免费看不忠 | 欧美兽交xxxx×视频 | 亚洲色偷偷偷综合网 | 国产亚洲精品久久久久久国模美 | 亚洲综合色区中文字幕 | 久久99精品久久久久久 | 国产精品久久久av久久久 | 国产午夜福利亚洲第一 | 无码国产色欲xxxxx视频 | 国产亚洲精品精品国产亚洲综合 | 国产猛烈高潮尖叫视频免费 | 国产三级精品三级男人的天堂 | 亚洲啪av永久无码精品放毛片 | 午夜精品一区二区三区的区别 | 又紧又大又爽精品一区二区 | 亚洲色www成人永久网址 | 午夜理论片yy44880影院 | 国产成人午夜福利在线播放 | 少妇性俱乐部纵欲狂欢电影 | 色婷婷av一区二区三区之红樱桃 | 日本熟妇浓毛 | 精品成人av一区二区三区 | 国产免费久久久久久无码 | 久久综合九色综合欧美狠狠 | 国产一区二区三区精品视频 | 成人性做爰aaa片免费看不忠 | 日本肉体xxxx裸交 | 国产无遮挡又黄又爽又色 | 欧洲极品少妇 | 又色又爽又黄的美女裸体网站 | av在线亚洲欧洲日产一区二区 | 少妇一晚三次一区二区三区 | 人妻人人添人妻人人爱 | 国产精品久久久一区二区三区 | 成人女人看片免费视频放人 | 国产av一区二区精品久久凹凸 | 欧美丰满老熟妇xxxxx性 | 成熟妇人a片免费看网站 | 日本一卡二卡不卡视频查询 | 香蕉久久久久久av成人 | 欧美午夜特黄aaaaaa片 | 亚洲aⅴ无码成人网站国产app | 日本大香伊一区二区三区 | 亚洲日韩av一区二区三区四区 | 日韩 欧美 动漫 国产 制服 | 欧美亚洲国产一区二区三区 | 亚洲国产一区二区三区在线观看 | 精品水蜜桃久久久久久久 | 无码人妻精品一区二区三区不卡 | 国产精品a成v人在线播放 | 国产激情无码一区二区 | 青春草在线视频免费观看 | 色综合久久88色综合天天 | 男女超爽视频免费播放 | a在线亚洲男人的天堂 | 天堂无码人妻精品一区二区三区 | 国产精品无套呻吟在线 | 一本无码人妻在中文字幕免费 | 亚洲精品国偷拍自产在线麻豆 | 日本熟妇乱子伦xxxx | 国产成人亚洲综合无码 | 亚洲毛片av日韩av无码 | 夜夜夜高潮夜夜爽夜夜爰爰 | 久久久中文字幕日本无吗 | 骚片av蜜桃精品一区 | 国产亚洲视频中文字幕97精品 | 天堂а√在线地址中文在线 | 九九热爱视频精品 | 国产后入清纯学生妹 | 国产人妻大战黑人第1集 | 亚洲综合在线一区二区三区 | 少妇厨房愉情理9仑片视频 | 国内揄拍国内精品少妇国语 | 成人免费视频一区二区 | 影音先锋中文字幕无码 | 中文毛片无遮挡高清免费 | 免费看少妇作爱视频 | 在线播放无码字幕亚洲 | 六月丁香婷婷色狠狠久久 | 蜜桃臀无码内射一区二区三区 | 亚洲精品综合五月久久小说 | 中文精品久久久久人妻不卡 | 国产无套粉嫩白浆在线 | 无套内谢的新婚少妇国语播放 | 乱人伦人妻中文字幕无码久久网 | 日本一卡2卡3卡四卡精品网站 | 初尝人妻少妇中文字幕 | 无码国产乱人伦偷精品视频 | 精品aⅴ一区二区三区 | 美女极度色诱视频国产 | 久久久久久久人妻无码中文字幕爆 | 波多野42部无码喷潮在线 | 亚洲欧洲中文日韩av乱码 | 亚洲国产精品一区二区第一页 | 国产精品va在线播放 | 少妇高潮喷潮久久久影院 | 色综合久久久久综合一本到桃花网 | 最新国产乱人伦偷精品免费网站 | 国产精品美女久久久网av | 国内少妇偷人精品视频 | 一本久道久久综合狠狠爱 | 一二三四社区在线中文视频 | 国产片av国语在线观看 | √8天堂资源地址中文在线 | 大乳丰满人妻中文字幕日本 | 国产无套内射久久久国产 | 免费视频欧美无人区码 | 国产精品毛片一区二区 | 亚洲 欧美 激情 小说 另类 | 国产精品久久久久影院嫩草 | 蜜桃视频韩日免费播放 | 美女毛片一区二区三区四区 | 国产va免费精品观看 | 蜜臀aⅴ国产精品久久久国产老师 | 成人欧美一区二区三区黑人 | 中文无码成人免费视频在线观看 | 嫩b人妻精品一区二区三区 | 久久国产36精品色熟妇 | 成人欧美一区二区三区黑人 | 性色欲情网站iwww九文堂 | 亚洲一区二区三区四区 | 老司机亚洲精品影院无码 | 久久综合久久自在自线精品自 | 午夜成人1000部免费视频 | 无码中文字幕色专区 | 大肉大捧一进一出视频出来呀 | 帮老师解开蕾丝奶罩吸乳网站 | 国产乱人无码伦av在线a | 国产一区二区不卡老阿姨 | aa片在线观看视频在线播放 | 色噜噜亚洲男人的天堂 | 清纯唯美经典一区二区 | 色窝窝无码一区二区三区色欲 | 国产无套内射久久久国产 | 亚洲理论电影在线观看 | 99国产精品白浆在线观看免费 | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 精品日本一区二区三区在线观看 | 激情人妻另类人妻伦 | 天堂а√在线中文在线 | 2019午夜福利不卡片在线 | 人人澡人人透人人爽 | 丝袜 中出 制服 人妻 美腿 | 国产精品久久福利网站 | 欧美黑人巨大xxxxx | 欧美真人作爱免费视频 | 国内精品久久久久久中文字幕 | 六十路熟妇乱子伦 | 久久精品国产99精品亚洲 | 九一九色国产 | 国产莉萝无码av在线播放 | 中文字幕无码热在线视频 | 国内精品久久毛片一区二区 | 亚洲中文字幕久久无码 | 日本丰满护士爆乳xxxx | 中文无码成人免费视频在线观看 | 久久久久99精品成人片 | 日本一区二区更新不卡 | 麻豆果冻传媒2021精品传媒一区下载 | 夜夜夜高潮夜夜爽夜夜爰爰 | 日日噜噜噜噜夜夜爽亚洲精品 | 99视频精品全部免费免费观看 | 精品少妇爆乳无码av无码专区 | 骚片av蜜桃精品一区 | а√资源新版在线天堂 | 日日噜噜噜噜夜夜爽亚洲精品 | 青青青爽视频在线观看 | 成人免费无码大片a毛片 | 六月丁香婷婷色狠狠久久 | 无码帝国www无码专区色综合 | 精品偷自拍另类在线观看 | 又粗又大又硬毛片免费看 | 久久国产劲爆∧v内射 | 精品亚洲韩国一区二区三区 | 思思久久99热只有频精品66 | 无遮无挡爽爽免费视频 | 在线观看国产午夜福利片 | 亚洲一区二区三区国产精华液 | 丝袜足控一区二区三区 | 无码人妻少妇伦在线电影 | 欧美日韩视频无码一区二区三 | v一区无码内射国产 | 无码国产激情在线观看 | 久久久国产精品无码免费专区 | 国内少妇偷人精品视频免费 | 午夜精品一区二区三区的区别 | 国产精品无套呻吟在线 | 少妇厨房愉情理9仑片视频 | 国产成人精品久久亚洲高清不卡 | 亚洲色欲久久久综合网东京热 | 国产艳妇av在线观看果冻传媒 | 色欲久久久天天天综合网精品 | 久久这里只有精品视频9 | 国产成人无码午夜视频在线观看 | 99国产欧美久久久精品 | 久久久久久国产精品无码下载 | 日韩在线不卡免费视频一区 | 久久久中文久久久无码 | 色综合久久久无码中文字幕 | 国产va免费精品观看 | 最新国产麻豆aⅴ精品无码 | 少妇人妻大乳在线视频 | 精品国产麻豆免费人成网站 | 无套内谢的新婚少妇国语播放 | 国产69精品久久久久app下载 | 免费无码午夜福利片69 | 久久99精品国产麻豆 | 国产综合在线观看 | 久久国产精品二国产精品 | 未满成年国产在线观看 | 天天躁日日躁狠狠躁免费麻豆 | 国产乱人偷精品人妻a片 | 亚洲人成无码网www | 18禁黄网站男男禁片免费观看 | 内射巨臀欧美在线视频 | 免费视频欧美无人区码 | 狠狠色丁香久久婷婷综合五月 | 亚洲综合久久一区二区 | 无码精品国产va在线观看dvd | 老熟妇仑乱视频一区二区 | 亚洲日韩乱码中文无码蜜桃臀网站 | 无码人妻精品一区二区三区不卡 | 久久综合激激的五月天 | 最新版天堂资源中文官网 | 少妇人妻大乳在线视频 | 中文字幕 亚洲精品 第1页 | 精品国产av色一区二区深夜久久 | 日韩视频 中文字幕 视频一区 | 日韩精品一区二区av在线 | 77777熟女视频在线观看 а天堂中文在线官网 | 国产免费无码一区二区视频 | 亚洲精品一区二区三区四区五区 | 日日摸天天摸爽爽狠狠97 | 无码人妻丰满熟妇区五十路百度 | 丰腴饱满的极品熟妇 | 无套内谢的新婚少妇国语播放 | 精品国产成人一区二区三区 | 亚洲七七久久桃花影院 | 久久综合网欧美色妞网 | 国产美女极度色诱视频www | 亚洲精品中文字幕乱码 | 人人爽人人澡人人人妻 | 亚洲国产成人av在线观看 | 国语精品一区二区三区 | 欧美xxxx黑人又粗又长 | 成人无码视频免费播放 | 国内精品久久久久久中文字幕 | 国产日产欧产精品精品app | 国产猛烈高潮尖叫视频免费 | 熟女少妇人妻中文字幕 | 亚洲欧美日韩国产精品一区二区 | 国产艳妇av在线观看果冻传媒 | 国产精品无套呻吟在线 | 中文字幕乱码人妻无码久久 | 国产午夜亚洲精品不卡下载 | 久久综合香蕉国产蜜臀av | 综合激情五月综合激情五月激情1 | 欧美精品无码一区二区三区 | 国产精品久久久久无码av色戒 | 自拍偷自拍亚洲精品被多人伦好爽 | 国产情侣作爱视频免费观看 | 国产人妻精品一区二区三区不卡 | 亚洲春色在线视频 | 中文字幕乱码人妻无码久久 | 水蜜桃亚洲一二三四在线 | 久久亚洲a片com人成 | 曰韩无码二三区中文字幕 | 亚洲国产日韩a在线播放 | 亚洲无人区一区二区三区 | 亚洲人交乣女bbw | 老熟女乱子伦 | 精品无人国产偷自产在线 | 国产精品香蕉在线观看 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | 中文字幕无码免费久久99 | 色噜噜亚洲男人的天堂 | 一区二区三区乱码在线 | 欧洲 | 久久久久久久久888 | 国产小呦泬泬99精品 | 77777熟女视频在线观看 а天堂中文在线官网 | 少妇无套内谢久久久久 | 综合网日日天干夜夜久久 | 久久国产精品精品国产色婷婷 | 亚洲精品国偷拍自产在线麻豆 | 国产手机在线αⅴ片无码观看 | 永久免费观看国产裸体美女 | 亚洲s码欧洲m码国产av | 秋霞成人午夜鲁丝一区二区三区 | 久久久精品成人免费观看 | √8天堂资源地址中文在线 | 又粗又大又硬又长又爽 | 国产亚洲精品精品国产亚洲综合 | 欧美熟妇另类久久久久久不卡 | 精品午夜福利在线观看 | 国产亚洲美女精品久久久2020 | 亚洲中文字幕无码中文字在线 | 国产亚洲精品久久久久久 | 亚洲中文字幕无码中字 | 无码人妻出轨黑人中文字幕 | 乱人伦人妻中文字幕无码 | 乱人伦中文视频在线观看 | 影音先锋中文字幕无码 | 久久99精品国产麻豆 | 性欧美熟妇videofreesex | 久久久精品国产sm最大网站 | 亚洲人亚洲人成电影网站色 | 伊人久久大香线蕉亚洲 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 日本精品久久久久中文字幕 | 乌克兰少妇xxxx做受 | 免费人成在线观看网站 | 少妇被黑人到高潮喷出白浆 | 男女作爱免费网站 | 综合人妻久久一区二区精品 | 精品亚洲成av人在线观看 | 亚洲精品国偷拍自产在线麻豆 | 丁香花在线影院观看在线播放 | 国产精品久久国产三级国 | 国产成人无码区免费内射一片色欲 | 骚片av蜜桃精品一区 | 久久久av男人的天堂 | 国产亚av手机在线观看 | 国产精品久免费的黄网站 | 黑森林福利视频导航 | 色欲人妻aaaaaaa无码 | 无码人妻av免费一区二区三区 | 日本精品人妻无码免费大全 | 国产卡一卡二卡三 | 国产97色在线 | 免 | 秋霞成人午夜鲁丝一区二区三区 | 牲欲强的熟妇农村老妇女 | 欧美 日韩 亚洲 在线 | 日本饥渴人妻欲求不满 | 黑人大群体交免费视频 | 亚洲精品国产品国语在线观看 | 亚洲а∨天堂久久精品2021 | 欧美阿v高清资源不卡在线播放 | 亚洲中文字幕无码中字 | 亚洲国产av美女网站 | 波多野结衣高清一区二区三区 | 亚洲国产精品美女久久久久 | 激情亚洲一区国产精品 | 夜精品a片一区二区三区无码白浆 | 伊在人天堂亚洲香蕉精品区 | 国产小呦泬泬99精品 | 国产超级va在线观看视频 | 国产精品无码永久免费888 | 影音先锋中文字幕无码 | 99久久人妻精品免费二区 | 亚洲一区二区三区国产精华液 | 婷婷综合久久中文字幕蜜桃三电影 | 国产成人人人97超碰超爽8 | 精品国产麻豆免费人成网站 | 国产精品99爱免费视频 | 精品久久8x国产免费观看 | 久久久婷婷五月亚洲97号色 | 爽爽影院免费观看 | 欧美freesex黑人又粗又大 | 精品无码国产自产拍在线观看蜜 | 亚洲精品一区三区三区在线观看 | 久久久久成人片免费观看蜜芽 | 国产乱人伦av在线无码 | 午夜精品一区二区三区在线观看 | 日日碰狠狠躁久久躁蜜桃 | 亚洲欧美日韩成人高清在线一区 | 亚洲熟妇自偷自拍另类 | 亚洲の无码国产の无码影院 | 无套内射视频囯产 | 97夜夜澡人人双人人人喊 | 鲁大师影院在线观看 | 国产精品无码永久免费888 | aa片在线观看视频在线播放 | 国产国语老龄妇女a片 | 国产美女极度色诱视频www | 日韩av无码中文无码电影 | а√天堂www在线天堂小说 | 国产激情无码一区二区 | 欧美日韩在线亚洲综合国产人 | 国产99久久精品一区二区 | 欧美日韩一区二区三区自拍 | 最近的中文字幕在线看视频 | 国产精品永久免费视频 | 亚洲欧洲日本无在线码 | 国产美女极度色诱视频www | 精品亚洲韩国一区二区三区 | 三上悠亚人妻中文字幕在线 | 国产精品igao视频网 | 成人试看120秒体验区 | 精品人妻中文字幕有码在线 | 娇妻被黑人粗大高潮白浆 | 小泽玛莉亚一区二区视频在线 | 国产精品久久久久久久9999 | 一区二区三区乱码在线 | 欧洲 | 久久综合久久自在自线精品自 | 午夜福利试看120秒体验区 | 国产又爽又猛又粗的视频a片 | 自拍偷自拍亚洲精品被多人伦好爽 | 1000部啪啪未满十八勿入下载 | 国产在线无码精品电影网 | 一本色道久久综合狠狠躁 | 无码人妻少妇伦在线电影 | 中文字幕人妻无码一夲道 | 中文字幕无码视频专区 | 国产亚洲精品久久久ai换 | 熟妇女人妻丰满少妇中文字幕 | 麻豆国产丝袜白领秘书在线观看 | 国产人成高清在线视频99最全资源 | 午夜嘿嘿嘿影院 | 最新国产乱人伦偷精品免费网站 | 人妻中文无码久热丝袜 | 日本精品高清一区二区 | 大地资源网第二页免费观看 | 色情久久久av熟女人妻网站 | 亚洲中文字幕乱码av波多ji | 精品无码成人片一区二区98 | 久久综合狠狠综合久久综合88 | 国产午夜福利100集发布 | 国产九九九九九九九a片 | 欧美熟妇另类久久久久久多毛 | 东北女人啪啪对白 | 婷婷五月综合激情中文字幕 | 国产一区二区三区四区五区加勒比 | 国产精品嫩草久久久久 | 久久精品丝袜高跟鞋 | 国产人妻精品一区二区三区不卡 | 激情人妻另类人妻伦 | 国产亲子乱弄免费视频 | 玩弄少妇高潮ⅹxxxyw | 人妻无码αv中文字幕久久琪琪布 | 色欲综合久久中文字幕网 | 久久久久久亚洲精品a片成人 | 亚洲s码欧洲m码国产av | 秋霞成人午夜鲁丝一区二区三区 | 国产深夜福利视频在线 | 精品人妻人人做人人爽夜夜爽 | 国产精华av午夜在线观看 | 色诱久久久久综合网ywww | 无码av免费一区二区三区试看 | 成人aaa片一区国产精品 | 久久久久99精品成人片 | 亚洲一区二区三区四区 | 亚洲国产精品久久久久久 | 无码一区二区三区在线观看 | 久久国语露脸国产精品电影 | 丰满人妻一区二区三区免费视频 | 麻豆精品国产精华精华液好用吗 | 99精品无人区乱码1区2区3区 | 亚洲欧洲日本综合aⅴ在线 | 亚洲 高清 成人 动漫 | 老司机亚洲精品影院无码 | 99麻豆久久久国产精品免费 | 国产精品高潮呻吟av久久4虎 | aⅴ亚洲 日韩 色 图网站 播放 | 51国偷自产一区二区三区 | 波多野结衣aⅴ在线 | 未满成年国产在线观看 | 3d动漫精品啪啪一区二区中 | 亚洲精品一区三区三区在线观看 | 在线播放免费人成毛片乱码 | 精品日本一区二区三区在线观看 | 无码人中文字幕 | 日产精品99久久久久久 | 国产精品无码永久免费888 | 无码av岛国片在线播放 | 亚洲中文字幕久久无码 | 成人欧美一区二区三区黑人免费 | 日本饥渴人妻欲求不满 | 三级4级全黄60分钟 | 国产高清av在线播放 | 亚洲国产欧美日韩精品一区二区三区 | 乱人伦人妻中文字幕无码 | 国产成人无码av一区二区 | 国产亚洲精品久久久久久久 | 国产亲子乱弄免费视频 | 欧美怡红院免费全部视频 | 两性色午夜视频免费播放 | 亚洲大尺度无码无码专区 | 国产午夜精品一区二区三区嫩草 | 国产人成高清在线视频99最全资源 | 一本加勒比波多野结衣 | 亚洲 另类 在线 欧美 制服 | 亚洲呦女专区 | 久久久久久国产精品无码下载 | 青青久在线视频免费观看 | 日韩 欧美 动漫 国产 制服 | 国产特级毛片aaaaaaa高清 | 天天拍夜夜添久久精品 | 九九久久精品国产免费看小说 | 久久精品国产日本波多野结衣 | 色婷婷综合激情综在线播放 | 麻豆md0077饥渴少妇 | 性色欲网站人妻丰满中文久久不卡 | 亚洲国产成人av在线观看 | 国产精品嫩草久久久久 | 亚洲欧洲中文日韩av乱码 | 久久无码人妻影院 | 人妻无码αv中文字幕久久琪琪布 | 暴力强奷在线播放无码 | 久久天天躁狠狠躁夜夜免费观看 | 漂亮人妻洗澡被公强 日日躁 | 偷窥日本少妇撒尿chinese | 蜜桃臀无码内射一区二区三区 | 在线成人www免费观看视频 | 日韩精品a片一区二区三区妖精 | 亚洲色大成网站www | 丰腴饱满的极品熟妇 | 老熟女重囗味hdxx69 | 欧美人与牲动交xxxx | 77777熟女视频在线观看 а天堂中文在线官网 | 真人与拘做受免费视频 | 亚洲一区av无码专区在线观看 | www一区二区www免费 | 377p欧洲日本亚洲大胆 | 国产超碰人人爽人人做人人添 | 亚洲色大成网站www | 国产亚洲精品久久久ai换 | 色一情一乱一伦一视频免费看 | 亚洲综合色区中文字幕 | 国产午夜精品一区二区三区嫩草 | 国产午夜无码视频在线观看 | 亚洲熟熟妇xxxx | 国产午夜视频在线观看 | 日韩人妻无码一区二区三区久久99 | 亚洲国产欧美国产综合一区 | 国产亲子乱弄免费视频 | 大胆欧美熟妇xx | 天堂а√在线地址中文在线 | 国产免费无码一区二区视频 | 亚洲国产欧美在线成人 | 在线观看国产午夜福利片 | 欧美老熟妇乱xxxxx | 欧美 日韩 亚洲 在线 | 野外少妇愉情中文字幕 | 人人妻在人人 | 噜噜噜亚洲色成人网站 | 色诱久久久久综合网ywww | 亚洲国产欧美日韩精品一区二区三区 | 中文字幕无码热在线视频 | 欧美亚洲国产一区二区三区 | 久久99久久99精品中文字幕 | 国产手机在线αⅴ片无码观看 | 亚洲欧美精品伊人久久 | 撕开奶罩揉吮奶头视频 | 国产三级精品三级男人的天堂 | 亚洲欧美日韩综合久久久 | 一区二区传媒有限公司 | 人人妻人人澡人人爽人人精品浪潮 | 女人被男人躁得好爽免费视频 | 女人被男人躁得好爽免费视频 | 欧美丰满熟妇xxxx | 亚洲色www成人永久网址 | 欧美野外疯狂做受xxxx高潮 | 国产成人av免费观看 | 激情内射亚州一区二区三区爱妻 | www成人国产高清内射 | av香港经典三级级 在线 | 国产人妻精品午夜福利免费 | a片在线免费观看 | 久久久久人妻一区精品色欧美 | 国产午夜无码视频在线观看 | 亚洲成av人综合在线观看 | 人人澡人人妻人人爽人人蜜桃 | av小次郎收藏 | 欧美日韩视频无码一区二区三 | 欧美国产日产一区二区 | 精品偷自拍另类在线观看 | 国产在线一区二区三区四区五区 | 久久午夜无码鲁丝片秋霞 | а天堂中文在线官网 | 日本一区二区三区免费播放 | 人妻天天爽夜夜爽一区二区 | 日本饥渴人妻欲求不满 | 18黄暴禁片在线观看 | 国产国语老龄妇女a片 | 中文字幕人妻无码一区二区三区 | 亚洲欧洲无卡二区视頻 | 日本精品人妻无码免费大全 | 久久视频在线观看精品 | 色综合久久久久综合一本到桃花网 | 黑人大群体交免费视频 | 精品无码国产自产拍在线观看蜜 | 国产色视频一区二区三区 | 欧美一区二区三区视频在线观看 | 国产一区二区三区四区五区加勒比 | 55夜色66夜色国产精品视频 | 国产69精品久久久久app下载 | 狠狠色噜噜狠狠狠7777奇米 | 在教室伦流澡到高潮hnp视频 | 黑人巨大精品欧美一区二区 | 午夜不卡av免费 一本久久a久久精品vr综合 | 免费观看的无遮挡av | 欧美乱妇无乱码大黄a片 | 日韩欧美中文字幕公布 | 日本成熟视频免费视频 | 国产精品对白交换视频 | 内射白嫩少妇超碰 | 国产人成高清在线视频99最全资源 | 欧美日本精品一区二区三区 | 中文字幕无码热在线视频 | 成人免费视频在线观看 | 国产成人无码av在线影院 | 性色av无码免费一区二区三区 | 亚洲一区二区三区四区 | 亚洲の无码国产の无码步美 | 亚洲 激情 小说 另类 欧美 | 老熟女乱子伦 | 97无码免费人妻超级碰碰夜夜 | 欧美zoozzooz性欧美 | 丝袜人妻一区二区三区 | 日产精品99久久久久久 | 亚洲s色大片在线观看 | 亚洲精品中文字幕久久久久 | 国产人妻久久精品二区三区老狼 | av无码久久久久不卡免费网站 | 天堂在线观看www | 久久久久亚洲精品中文字幕 | 人妻无码久久精品人妻 | 樱花草在线播放免费中文 | 在线亚洲高清揄拍自拍一品区 | 黑人巨大精品欧美黑寡妇 | 国产精品-区区久久久狼 | 日本一卡2卡3卡4卡无卡免费网站 国产一区二区三区影院 | 东北女人啪啪对白 | 日本高清一区免费中文视频 | 国产av无码专区亚洲awww | 国产乡下妇女做爰 | 捆绑白丝粉色jk震动捧喷白浆 | 亚洲色欲色欲欲www在线 | 亚洲爆乳大丰满无码专区 | 亚洲欧洲无卡二区视頻 | 牲欲强的熟妇农村老妇女视频 | 狠狠综合久久久久综合网 | 51国偷自产一区二区三区 | 久久精品99久久香蕉国产色戒 | 学生妹亚洲一区二区 | 日韩亚洲欧美中文高清在线 | 久久天天躁夜夜躁狠狠 | 欧美变态另类xxxx | 东京热男人av天堂 | 性做久久久久久久免费看 | 精品aⅴ一区二区三区 | 欧美成人午夜精品久久久 | 露脸叫床粗话东北少妇 | 国产精品久久久久久无码 | 性色欲网站人妻丰满中文久久不卡 | 免费无码午夜福利片69 | 国产精品怡红院永久免费 | 久久午夜无码鲁丝片 | 国产精品久久国产三级国 | 99久久亚洲精品无码毛片 | 亚洲国产av美女网站 | 亚洲成a人一区二区三区 | 76少妇精品导航 | 伊人久久大香线焦av综合影院 | 成熟妇人a片免费看网站 | 国产亚洲精品久久久久久久 | 国产农村乱对白刺激视频 | 国产9 9在线 | 中文 | 岛国片人妻三上悠亚 | 欧美freesex黑人又粗又大 | 国产午夜手机精彩视频 | 蜜臀av在线播放 久久综合激激的五月天 | 国产免费观看黄av片 | 人妻aⅴ无码一区二区三区 | 免费无码肉片在线观看 | 伊人久久大香线蕉亚洲 | 特大黑人娇小亚洲女 | 免费观看激色视频网站 | 97se亚洲精品一区 | √天堂资源地址中文在线 | 国产电影无码午夜在线播放 | 精品无码国产一区二区三区av | 日韩欧美群交p片內射中文 | 久久精品人人做人人综合试看 | 一个人看的视频www在线 | 久久精品女人天堂av免费观看 | 欧美一区二区三区视频在线观看 | 天天摸天天透天天添 | 高潮喷水的毛片 | 亚洲色欲色欲欲www在线 | 国产激情综合五月久久 | 国产九九九九九九九a片 | 亚洲日韩中文字幕在线播放 | 久久久久久亚洲精品a片成人 | 亚洲色偷偷偷综合网 | 亚洲爆乳无码专区 | 亚洲精品国产第一综合99久久 | 精品国偷自产在线 | 97久久国产亚洲精品超碰热 | а√资源新版在线天堂 | 精品国产青草久久久久福利 | 国内少妇偷人精品视频免费 | 日韩av无码中文无码电影 | 国产无套内射久久久国产 | a片在线免费观看 | 亚洲人成无码网www | 免费无码肉片在线观看 | 又大又硬又爽免费视频 | 国产手机在线αⅴ片无码观看 | 久久99热只有频精品8 | 亚洲 高清 成人 动漫 | 日韩人妻系列无码专区 | 国产人妻精品一区二区三区不卡 | 国精品人妻无码一区二区三区蜜柚 | 青青青爽视频在线观看 | 曰本女人与公拘交酡免费视频 | 55夜色66夜色国产精品视频 | 国产精品无码一区二区三区不卡 | 国产精品自产拍在线观看 | 日韩人妻系列无码专区 | 亚洲高清偷拍一区二区三区 | 亚洲色欲久久久综合网东京热 | 日韩人妻无码中文字幕视频 | 久久午夜夜伦鲁鲁片无码免费 | 小sao货水好多真紧h无码视频 | 欧美一区二区三区视频在线观看 | 国产麻豆精品精东影业av网站 | 国产精品无码一区二区三区不卡 | 久久综合九色综合欧美狠狠 | 少妇人妻av毛片在线看 | 2019nv天堂香蕉在线观看 | 久久久久久久久888 | 日韩精品无码免费一区二区三区 | 亚洲爆乳精品无码一区二区三区 | 亚洲精品国偷拍自产在线麻豆 | 天堂亚洲2017在线观看 | 人人妻人人藻人人爽欧美一区 | 永久黄网站色视频免费直播 | 精品国产一区二区三区四区在线看 | 丰满妇女强制高潮18xxxx | 丝袜美腿亚洲一区二区 | 精品一区二区三区波多野结衣 | 精品国产av色一区二区深夜久久 | 蜜桃臀无码内射一区二区三区 | 日韩av无码一区二区三区不卡 | 亚洲精品一区二区三区大桥未久 | av无码不卡在线观看免费 | 国产精品人人妻人人爽 | 一本久道高清无码视频 | 欧洲极品少妇 | 久久亚洲日韩精品一区二区三区 | 国产超级va在线观看视频 | 欧美猛少妇色xxxxx | 熟妇人妻无码xxx视频 | 欧美肥老太牲交大战 | 天堂无码人妻精品一区二区三区 | 亚洲 另类 在线 欧美 制服 | 夜夜影院未满十八勿进 | 免费观看激色视频网站 | 国产精品毛多多水多 | 无码人妻久久一区二区三区不卡 | 久久久久se色偷偷亚洲精品av | 暴力强奷在线播放无码 | 午夜熟女插插xx免费视频 | 国产99久久精品一区二区 | 色一情一乱一伦一视频免费看 | 好爽又高潮了毛片免费下载 | 亚洲人亚洲人成电影网站色 | 又色又爽又黄的美女裸体网站 | 日本熟妇人妻xxxxx人hd | 国产精品99久久精品爆乳 | 国产亚洲人成在线播放 | 在线观看免费人成视频 | 成人综合网亚洲伊人 | 亚洲国产欧美国产综合一区 | 夜夜夜高潮夜夜爽夜夜爰爰 | 精品人人妻人人澡人人爽人人 | 色综合久久久无码网中文 | 婷婷综合久久中文字幕蜜桃三电影 | 激情人妻另类人妻伦 | 麻豆国产97在线 | 欧洲 | 东京热一精品无码av | 中文字幕无线码 | 亚洲一区二区三区含羞草 | 日本大香伊一区二区三区 | 性欧美疯狂xxxxbbbb | 少妇性l交大片欧洲热妇乱xxx | 国产sm调教视频在线观看 | 亚洲精品综合五月久久小说 | 日本乱人伦片中文三区 | 99精品无人区乱码1区2区3区 | 中文字幕无码日韩欧毛 | 我要看www免费看插插视频 | 国产卡一卡二卡三 | 人妻有码中文字幕在线 | 国产午夜视频在线观看 | 亚洲精品国产精品乱码不卡 | 一本色道久久综合亚洲精品不卡 | 少妇激情av一区二区 | 国产人妻精品午夜福利免费 | 久久国产精品萌白酱免费 | 国产人妻精品一区二区三区 | 在线播放亚洲第一字幕 | 四虎国产精品一区二区 | 窝窝午夜理论片影院 | 日本乱人伦片中文三区 | 欧美老妇交乱视频在线观看 | 婷婷六月久久综合丁香 | 国产精品沙发午睡系列 | 麻豆国产丝袜白领秘书在线观看 | 午夜精品一区二区三区在线观看 | 国产精品第一区揄拍无码 | 久在线观看福利视频 | 国产成人无码a区在线观看视频app | 国产午夜无码视频在线观看 | 亚洲日韩精品欧美一区二区 |