首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC 教學

Windows GDI 程式設計入門:從 DC、筆刷、字型到雙緩衝防閃爍

     編輯:VC 教學

GDI (Graphics Device Interface) 是 Windows 經典 2D 繪圖 API,雖有 Direct2D/DirectWrite 現代替代者,但 GDI 仍廣泛用於維護專案、輕量工具、列印輸出。本文系統介紹 DC 類型、GDI 物件管理、繪圖流程、雙緩衝防閃爍與除錯技巧。

一、裝置內容 (DC) 取得與釋放

API / 類別取得範圍座標原點釋放方式適用場景
BeginPaint/EndPaint客戶區 (Invalid Rect)客戶區左上 (0,0)EndPaint 自動WM_PAINT 處理函式
GetDC/ReleaseDC客戶區客戶區左上ReleaseDC即時繪圖、非 WM_PAINT
GetWindowDC/ReleaseDC整個視窗 (含非客戶區)視窗左上ReleaseDC繪製標題列、邊框、非客戶區
CreateCompatibleDC/DeleteDC記憶體 DC (離屏)位圖左上DeleteDC雙緩衝、位圖操作、列印

MFC 封裝類別

  • CPaintDC:建構呼叫 BeginPaint,解構呼叫 EndPaint。
  • CClientDC:GetDC/ReleaseDC。
  • CWindowDC:GetWindowDC/ReleaseDC。
  • CDC:基底類別,封裝所有 GDI 函式 (如 Rectangle → dc.Rectangle)。

二、GDI 物件建立、選入、刪除

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)

庫存物件 (Stock Objects) - 無需建立/刪除

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() 等便利函式。
  • GDI 函式多接受 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 繪圖 (黑) → 視覺上白閃。

記憶體 DC 實作

// 手動版
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);

MFC 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 繪圖...

六、GDI 除錯技巧

  1. 分離視窗:IDE 與應用程式視窗桌面不重疊,單步執行即時觀察繪圖結果。
  2. 關閉批次處理:GdiSetBatchLimit(1) (僅 _DEBUG) 強制每個 GDI 呼叫立即生效。
  3. 資源洩漏偵測:GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) 監控 GDI 物件數,超過 10,000 即可能耗盡系統配額 (預設 65,536)。
  4. Spy++ / GDIView:檢視視窗 DC 狀態、選入物件、剪裁區域。

七、現代化遷移建議

  • 新專案優先使用 Direct2D (ID2D1Factory, ID2D1HwndRenderTarget):硬體加速、向量繪圖、高 DPI 原生支援、文字排版 (DirectWrite)。
  • GDI/Direct2D 互通:ID2D1DCRenderTarget 繪製至 GDI DC,或 ID2D1Bitmap::CopyFromMemory 互轉。
  • 列印仍建議 GDI (StartDoc/StartPage/EndPage/EndDoc) 或 XPS Document API。

紮實掌握 GDI 基礎不僅能維護舊代碼,更能深入理解 Windows 繪圖管線,為遷移現代圖形 API 打下堅實基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved