GDI (Graphics Device Interface) 是 Windows 經典 2D 繪圖 API,雖有 Direct2D/DirectWrite 現代替代者,但 GDI 仍廣泛用於維護專案、輕量工具、列印輸出。本文系統介紹 DC 類型、GDI 物件管理、繪圖流程、雙緩衝防閃爍與除錯技巧。
| API / 類別 | 取得範圍 | 座標原點 | 釋放方式 | 適用場景 |
|---|---|---|---|---|
BeginPaint/EndPaint | 客戶區 (Invalid Rect) | 客戶區左上 (0,0) | EndPaint 自動 | WM_PAINT 處理函式 |
GetDC/ReleaseDC | 客戶區 | 客戶區左上 | ReleaseDC | 即時繪圖、非 WM_PAINT |
GetWindowDC/ReleaseDC | 整個視窗 (含非客戶區) | 視窗左上 | ReleaseDC | 繪製標題列、邊框、非客戶區 |
CreateCompatibleDC/DeleteDC | 記憶體 DC (離屏) | 位圖左上 | DeleteDC | 雙緩衝、位圖操作、列印 |
CPaintDC:建構呼叫 BeginPaint,解構呼叫 EndPaint。CClientDC:GetDC/ReleaseDC。CWindowDC:GetWindowDC/ReleaseDC。CDC:基底類別,封裝所有 GDI 函式 (如 Rectangle → dc.Rectangle)。GDI 物件 (筆 HPEN、刷 HBRUSH、字型 HFONT、位圖 HBITMAP、區域 HRGN、調色盤 HPALETTE) 由 DC 狀態管理。
// 1. 建立
CPen pen(PS_SOLID, 2, RGB(255,0,0));
CBrush brush(RGB(0,255,0));
CFont font; font.CreatePointFont(120, _T("微軟正黑體"));
// 2. 選入 DC,保存舊物件
CPen* pOldPen = dc.SelectObject(&pen);
CBrush* pOldBrush = dc.SelectObject(&brush);
CFont* pOldFont = dc.SelectObject(&font);
// 3. 繪圖
dc.Rectangle(10, 10, 100, 100);
dc.TextOut(20, 20, _T("Hello GDI"));
// 4. 恢復舊物件 (關鍵!)
dc.SelectObject(pOldPen);
dc.SelectObject(pOldBrush);
dc.SelectObject(pOldFont);
// 5. 區域變數 pen/brush/font 解構時自動 DeleteObject
// 前提:已從 DC 移除 (步驟 4)
dc.SelectStockObject(NULL_BRUSH); // 透明刷
dc.SelectStockObject(BLACK_PEN); // 黑筆
dc.SelectStockObject(SYSTEM_FONT); // 系統字型
// 直接選入,不需恢復 (系統管理生命週期)
RECT / CRect:left, top, right, bottom (右/下 不包含)。CRect 提供 Width(), Height(), CenterPoint(), OffsetRect(), InflateRect(), PtInRect() 等便利函式。RECT* 或 x, y, cx, cy (寬高)。OnPaint)void CStatusBarEx::OnPaint() {
CPaintDC dc(this);
CRect rcClient; GetClientRect(&rcClient);
// 1. 背景填充
dc.FillSolidRect(rcClient, 0xF2F2F2);
// 2. 設定繪圖模式
dc.SetBkMode(TRANSPARENT);
dc.SelectStockObject(NULL_BRUSH);
// 3. 字型
CFont* pOldFont = nullptr;
if (CFont* pFont = GetFont()) pOldFont = dc.SelectObject(pFont);
// 4. 筆:圓角矩形邊框
CPen pen(PS_SOLID, 1, RGB(0xBD, 0xBA, 0xBD));
CPen* pOldPen = dc.SelectObject(&pen);
CBrush bgBrush(0xF2F2F2);
for (int i = 0; i < m_nCount; ++i) {
CRect rcItem; GetItemRect(i, rcItem);
rcItem.bottom--; // 視覺微調
if (i == 0) rcItem.left += 2;
// 面板背景
dc.FillRect(rcItem, &bgBrush);
// 圓角矩形
dc.RoundRect(rcItem, CPoint(5, 5));
// 文字
UINT style = GetPaneStyle(i);
if ((style & SBPS_DISABLED) == 0) {
CString text = GetPaneText(i);
CRect rcText = rcItem; rcText.left += 3; rcText.top += 3;
dc.DrawText(text, rcText, DT_SINGLELINE | DT_NOPREFIX | DT_TOP | DT_LEFT);
}
}
// 5. 右下角尺寸把手 (六個小圓)
if (GetStyle() & SBARS_SIZEGRIP) {
CRect rcGrip = rcClient;
rcGrip.left = rcGrip.right - rcGrip.Width()/4;
rcGrip.top = rcGrip.bottom - rcGrip.Width();
int w = rcGrip.Width();
dc.SelectStockObject(GRAY_BRUSH);
for (int row = 0; row < 2; ++row) {
for (int col = 0; col < 3; ++col) {
CRect rcDot = rcGrip;
rcDot.OffsetRect(col*w, row*w);
dc.Ellipse(rcDot);
}
}
}
// 6. 恢復
if (pOldFont) dc.SelectObject(pOldFont);
dc.SelectObject(pOldPen);
// brush/font/pen 區域變數自動銷毀
}
閃爍成因:WM_ERASEBKGND 擦除背景 (白) → WM_PAINT 繪圖 (黑) → 視覺上白閃。
// 手動版
HDC hMemDC = CreateCompatibleDC(hdc);
HBITMAP hBmp = CreateCompatibleBitmap(hdc, rc.Width(), rc.Height());
HBITMAP hOldBmp = SelectObject(hMemDC, hBmp);
// 在 hMemDC 繪圖...
BitBlt(hdc, rc.left, rc.top, rc.Width(), rc.Height(), hMemDC, 0, 0, SRCCOPY);
SelectObject(hMemDC, hOldBmp);
DeleteObject(hBmp);
DeleteDC(hMemDC);
CMemDC 封裝 (RAII)class CMemDC : public CDC {
CDC* m_pDC; CRect m_rc; CBitmap m_bmp; CBitmap* m_pOldBmp;
public:
CMemDC(CDC* pDC, CRect* pRect = nullptr) : m_pDC(pDC) {
m_rc = pRect ? *pRect : CRect(0,0,0,0);
if (m_rc.IsRectEmpty()) pDC->GetClipBox(&m_rc);
CreateCompatibleDC(pDC);
m_bmp.CreateCompatibleBitmap(pDC, m_rc.Width(), m_rc.Height());
m_pOldBmp = SelectObject(&m_bmp);
SetWindowOrg(m_rc.left, m_rc.top);
FillSolidRect(m_rc, pDC->GetBkColor());
}
~CMemDC() {
m_pDC->BitBlt(m_rc.left, m_rc.top, m_rc.Width(), m_rc.Height(), this, 0, 0, SRCCOPY);
SelectObject(m_pOldBmp);
}
};
CRect rc; GetClientRect(&rc);
#ifdef _DEBUG
CPaintDC dc(this); // 除錯:直接繪製,可見每步
#else
CPaintDC dcScreen(this);
CMemDC dc(&dcScreen, &rc); // 發布:雙緩衝
#endif
// 統一使用 dc 繪圖...
GdiSetBatchLimit(1) (僅 _DEBUG) 強制每個 GDI 呼叫立即生效。GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) 監控 GDI 物件數,超過 10,000 即可能耗盡系統配額 (預設 65,536)。ID2D1Factory, ID2D1HwndRenderTarget):硬體加速、向量繪圖、高 DPI 原生支援、文字排版 (DirectWrite)。ID2D1DCRenderTarget 繪製至 GDI DC,或 ID2D1Bitmap::CopyFromMemory 互轉。StartDoc/StartPage/EndPage/EndDoc) 或 XPS Document API。紮實掌握 GDI 基礎不僅能維護舊代碼,更能深入理解 Windows 繪圖管線,為遷移現代圖形 API 打下堅實基礎。