Windows 中有各種圖形使用者介面 GUI (Graphics User Interface) 物件,繪圖時需利用這些物件。各物件擁有各種屬性,首先介紹幾種 GUI 物件與其屬性。
CFont:輸出文字時選用不同風格與大小字型。可選風格:斜體、粗體、字型名稱、底線等。CBrush 物件:決定填充區域時所用顏色或樣板。固定色刷子屬性為顏色、是否使用網格與網格類型(水平、垂直、交叉等)。也可用 8×8 點陣圖建立自訂樣板刷子,系統利用點陣圖逐步填充區域。CPen:畫點與畫線時使用。屬性包含顏色、寬度、線條風格(虛線、實線、點劃線等)。CBitmap 物件:可包含一幅圖像,可保存在資源中。CPalette 調色盤:顏色對應介面,允許應用程式在不影響其他應用程式前提下,充分利用輸出裝置顏色描繪能力。系統另有庫存 GUI 物件,可用 CDC::SelectStockObject 選入,包含固定顏色刷子、畫筆與基本字型,如 BLACK_BRUSH、NULL_BRUSH、WHITE_PEN、DEVICE_DEFAULT_FONT。
Windows 使用 GUI 物件必須遵守規則:先建立合法物件(不同物件建立方法不同),再將 GUI 物件選入 DC 並保存 DC 原物件。選入非法物件會引發例外。使用完畢應恢復原物件,特別重要——若保存暫時物件在 DC 中,物件銷毀後可能引發例外。每個物件重新建立前必須銷毀。安全使用範例:
OnDraw(CDC* pDC)
{
CPen pen1, pen2;
pen1.CreatePen(PS_SOLID, 2, RGB(128,128,128)); // 建立畫筆一
pen2.CreatePen(PS_SOLID, 2, RGB(128,128,0)); // 建立畫筆二
CPen* pOldPen = (CPen*)pDC->SelectObject(&pen1); // 選入 DC
// drawWithPen1...
pDC->SelectObject(&pen2); // 選入畫筆二
// drawWithPen2...
pen1.DeleteObject(); // 重建前先銷毀
pen1.CreatePen(PS_SOLID, 2, RGB(0,0,0)); // 重建
pDC->SelectObject(&pen1);
// drawWithPen1...
pDC->SelectObject(pOldPen); // 恢復
}
OnDraw(CDC* pDC) 是 VC 最常見的圖形輸出重繪函式,pDC 為 CDC 類別指標,透過它進行畫圖操作。
MFC 中 CDC 封裝物理裝置輸出相關功能,為裝置環境類別基類,直接由 CObject 衍生。每個 C++ 裝置環境物件對應 Windows 裝置環境,透過 32 位元 HDC 控制代碼識別。CDC 虛擬性使我們易於撰寫適用多種裝置的程式碼。例如 pDC->TextOut(0,0,"Hello") 既適用螢幕,也適用列印預覽與列印,只需 CView::OnDraw 的 pDC 參數指向不同物件類別。
CClientDC 和 CWindowDC 為顯示裝置環境類別,由 CDC 衍生。差別在:CClientDC 為視窗工作區(不含邊框、標題列、功能表列),(0,0) 指工作區左上角。CWindowDC 的 (0,0) 指整個螢幕左上角,可在螢幕任意處繪圖,包含視窗邊框、標題列、功能表列等。CWindowDC 通常用於框架視窗而非檢視視窗。
CDC 物件建立後必須在適當時機刪除,否則造成記憶體洩漏。以下程式段實現 DC 環境下輸出文字:
long CImg::OutImgFromText(LPCTSTR vFileName, LPCTSTR lpText, LPCTSTR lpBgImg,
long lCSet, LPCTSTR lpFont, long lWidth, long lHeight, long lLeft,
long lTop, long llfHeight, long lWeight, long l3D)
{
// i-viii: 參數合法性檢查與修正
m_nWidth = lWidth; m_nHeight = lHeight;
if ((m_nWidth % 8) != 0) m_nWidth = ((int)(m_nWidth/8) + 1) * 8;
if (m_nWidth < 3 * lLeft) m_nWidth = 3 * lLeft;
if (m_nHeight < 3 * lTop) m_nHeight = 3 * lTop;
int nFHeight = llfHeight;
if (0 == nFHeight) nFHeight = 1;
int nRealClientWidth = (m_nWidth - 2 * lLeft);
// ix-x: 定義並建立與指定裝置相容的 DC 控制代碼
HDC hDC = CreateCompatibleDC(NULL);
// xi-xviii.1: 定義 LOGFONT 邏輯字型結構並填充
LOGFONT lf; memset(&lf, 0, sizeof(lf));
lf.lfCharSet = GB2312_CHARSET;
lf.lfHeight = nFHeight;
lstrcpy(lf.lfFaceName, lpFont);
lf.lfPitchAndFamily = 8;
lf.lfWeight = lWeight;
HFONT hFont = CreateFontIndirect(&lf);
HFONT hOldFont = (HFONT)SelectObject(hDC, hFont); // 選入字型
// xix-xxiv: 取得輸入文字長度,計算輸出矩形
CComBSTR bstrText(lpText);
RECT rectClient = {lLeft, lTop, m_nWidth - lLeft, m_nHeight - lTop};
::DrawText(hDC, bstrText.m_str, bstrText.Length(), &rectClient,
DT_WORDBREAK | DT_LEFT | DT_CALCRECT); // 計算輸出矩形
int nRealHeight = rectClient.bottom + lTop;
if (m_nHeight < nRealHeight) m_nHeight = nRealHeight;
else rectClient.bottom = m_nHeight - lTop;
// xxv-xxvii: 建立點陣圖控制代碼並選入 DC
HBITMAP hBitmap = CreateDiscardableBitmap(hDC, m_nWidth, m_nHeight);
SelectObject(hDC, hBitmap);
// xxviii-xxxvii: 繪圖準備與 3D 效果判斷
HBRUSH hBBg = CreateSolidBrush(RGB(255,255,255));
RECT rectFull = {0, 0, m_nWidth, m_nHeight};
FillRect(hDC, &rectFull, hBBg);
if (l3D > 0) {
SetTextColor(hDC, ::GetSysColor(COLOR_3DDKSHADOW));
SetBkMode(hDC, OPAQUE);
} else {
SetBkColor(hDC, RGB(255,255,255));
SetTextColor(hDC, RGB(0,0,0));
SetBkMode(hDC, TRANSPARENT);
}
// xxxviii: 輸出文字
::DrawText(hDC, bstrText.m_str, bstrText.Length(), &rectClient, DT_WORDBREAK);
// xxxix-xli: 3D 效果第二次輸出(偏移矩形)
if (l3D > 0) {
SetTextColor(hDC, ::GetSysColor(COLOR_3DHILIGHT));
SetBkMode(hDC, TRANSPARENT);
rectClient.left += l3D; rectClient.top -= 1;
rectClient.right += l3D; rectClient.bottom -= 1;
::DrawText(hDC, lpText, wcslen(lpText), &rectClient, DT_WORDBREAK);
}
// xlii-xlix: 清理與儲存
SelectObject(hDC, hOldFont);
DeleteObject(hFont); DeleteObject(hBBg);
SaveDCBmp(hDC, hBitmap, vFileName); // 儲存點陣圖,下節討論
DeleteObject(hBitmap);
::ReleaseDC(NULL, hDC);
return 0;
}
函式功能:輸入特定長度文字,輸出影像至指定檔案。參數說明見原文。程式碼各段落說明:i-viii 參數檢查;ix-x 建立相容 DC;xi-xviii.1 定義邏輯字型並選入 DC;xix-xxiv 計算輸出矩形;xxv-xxvii 建立點陣圖選入 DC;xxxii 判斷 3D 效果;xxxviii 輸出文字;xxxix-xli 3D 效果第二次輸出;xlii 之後刪除物件、儲存點陣圖、恢復現場。
點陣圖檔案由三部分組成:檔案頭 + 影像資訊 + 影像像素資料
typedef struct tagBITMAPFILEHEADER {
WORD bfType; // 應為 "BM" (0x4d42)
DWORD bfSize; // 檔案大小
WORD bfReserved1; // 保留,為 0
WORD bfReserved2; // 保留,為 0
DWORD bfOffBits; // 檔案大小與 DIB 資料大小之差
} BITMAPFILEHEADER;typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO;
資訊頭:包含像素位元數、顏色格式、寬高、位平面數、壓縮格式等。
typedef struct tagBITMAPINFOHEADER {
DWORD biSize; // 結構大小
LONG biWidth; // 寬度(像素)
LONG biHeight; // 高度(像素)
WORD biPlanes; // 位平面數
WORD biBitCount; // 每像素位元數
DWORD biCompression; // 壓縮格式
DWORD biSizeImage; // 影像資料大小
LONG biXPelsPerMeter; // 水平每米像素數
LONG biYPelsPerMeter; // 垂直每米像素數
DWORD biClrUsed; // 調色盤實際使用顏色數
DWORD biClrImportant; // 重要顏色數
} BITMAPINFOHEADER;
biBitCount 意義:0=JPEG、1=單色(2色)、4=16色、8=256色(灰階)、16=64K色、24=16M真彩色、32=4G真彩色(RGBA)。RGBQUAD 結構順序為 BGR 而非 RGB。
typedef struct tagRGBQUAD {
BYTE rgbBlue, rgbGreen, rgbRed, rgbReserved;
} RGBQUAD;認識結構後操作簡單。建立畫筆、刷子、點陣圖物件,在 DC 環境繪圖後,將 DC 圖像儲存為點陣圖檔案:
BOOL CImg::SaveDCBmp(HDC hDC, HBITMAP hBitmap, LPCTSTR lpFileName)
{
int iBits; WORD wBitCount;
DWORD dwPaletteSize=0, dwBmBitsSize=0, dwDIBSize=0, dwWritten=0;
BITMAP Bitmap;
BITMAPFILEHEADER bmfHdr;
BITMAPINFOHEADER bi;
LPBITMAPINFOHEADER lpbi;
HANDLE fh, hDib, hPal, hOldPal=NULL;
// 計算每像素位元組數
iBits = GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES);
if (iBits <= 1) wBitCount = 1;
else if (iBits <= 4) wBitCount = 4;
else if (iBits <= 8) wBitCount = 8;
else wBitCount = 24;
GetObject(hBitmap, sizeof(Bitmap), (LPSTR)&Bitmap);
bi.biSize = sizeof(BITMAPINFOHEADER);
bi.biWidth = Bitmap.bmWidth; bi.biHeight = Bitmap.bmHeight;
bi.biPlanes = 1; bi.biBitCount = wBitCount;
bi.biCompression = BI_RGB; bi.biSizeImage = 0;
bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0;
bi.biClrImportant = 0; bi.biClrUsed = 0;
dwBmBitsSize = ((Bitmap.bmWidth * wBitCount + 31) / 32) * 4 * Bitmap.bmHeight;
hDib = GlobalAlloc(GHND, dwBmBitsSize + dwPaletteSize + sizeof(BITMAPINFOHEADER));
lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDib);
*lpbi = bi;
// 處理調色盤
hPal = GetStockObject(DEFAULT_PALETTE);
if (hPal) {
hOldPal = ::SelectPalette(hDC, (HPALETTE)hPal, FALSE);
}
// 取得新像素值
GetDIBits(hDC, hBitmap, 0, (UINT)Bitmap.bmHeight,
(LPSTR)lpbi + sizeof(BITMAPINFOHEADER) + dwPaletteSize,
(BITMAPINFO*)lpbi, DIB_RGB_COLORS);
// 恢復調色盤
if (hOldPal) {
::SelectPalette(hDC, (HPALETTE)hOldPal, TRUE);
RealizePalette(hDC);
}
// 建立點陣圖檔案
fh = CreateFile(lpFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if (fh == INVALID_HANDLE_VALUE) return FALSE;
// 設定檔案頭
bmfHdr.bfType = 0x4D42; // "BM"
dwDIBSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize + dwBmBitsSize;
bmfHdr.bfSize = dwDIBSize;
bmfHdr.bfReserved1 = 0; bmfHdr.bfReserved2 = 0;
bmfHdr.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize;
// 寫入檔案頭
WriteFile(fh, (LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
// 寫入其餘內容
WriteFile(fh, (LPSTR)lpbi, dwDIBSize, &dwWritten, NULL);
// 清理
GlobalUnlock(hDib); GlobalFree(hDib); CloseHandle(fh);
return TRUE;
}
儲存前透過 GetObject 取得點陣圖長寬,GetDIBits 取得掃描資料,填充 BITMAPFILEHEADER、BITMAPINFOHEADER,依序寫入檔案頭、資訊、像素資料。