MFC 開發中畫面閃爍是常見痛點,尤其在頻繁重繪、大量控件或複雜繪圖時。本文系統整理四大核心防閃技巧,並提供可直接套用的通用巨集 ERASE_BKGND_*,解決「計算空白區域」的繁瑣問題,另附 CListCtrl 拖曳防閃實戰範例。
Invalidate() 會標記整個客戶區需重繪,耗時且易閃爍。改用 InvalidateRect(&rect, FALSE) 僅標記實際變更的矩形區域,FALSE 參數告知系統不擦除背景 (配合技巧二使用)。
系統預設在 WM_ERASEBKGND 中用背景刷子填滿客戶區,導致控件被覆蓋後再重繪產生閃爍。覆寫並直接回傳 TRUE (非 FALSE),表示「我已處理擦除,系統勿再處理」:
BOOL CMyWnd::OnEraseBkgnd(CDC* pDC)
{
return TRUE; // 禁止系統擦除
// return CWnd::OnEraseBkgnd(pDC); // 註解掉原始呼叫
}
隨後在 OnPaint 中自行以 dc.FillRect 或 FillRgn 只擦除必要區域。
利用 CRgn 的 CombineRgn(RGN_XOR) 排除控件佔用區域,僅填充剩餘背景:
CRect rectClient; GetClientRect(&rectClient);
CRgn rgnTotal, rgnEdit;
rgnTotal.CreateRectRgnIndirect(&rectClient);
rgnEdit.CreateRectRgnIndirect(&m_rectEdit); // 大型 Edit 控件區域
rgnTotal.CombineRgn(&rgnTotal, &rgnEdit, RGN_XOR); // 排除 Edit 區
CBrush brush(GetSysColor(COLOR_WINDOW));
pDC->FillRgn(&rgnTotal, &brush);
注意: 必須搭配技巧二 (禁止系統擦除) 使用,否則系統會先全域擦除一次。
適用於繪圖邏輯複雜、耗時長的情況:
void CMyWnd::OnPaint()
{
CPaintDC dc(this);
CMemDC memDC(&dc, &rectClient); // MFC 內建雙緩衝類別 (VC6 需自行實作)
// 或手動建立:
CDC dcMem; dcMem.CreateCompatibleDC(&dc);
CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rectClient.Width(), rectClient.Height());
dcMem.SelectObject(&bmp);
// ... 在 dcMem 上進行所有繪圖 ...
dc.BitBlt(0, 0, rectClient.Width(), rectClient.Height(), &dcMem, 0, 0, SRCCOPY);
}
解決「手動計算排除矩形」的麻煩,支援多控件、任意矩形:
#define ERASE_BKGND_BEGIN \
CRect bgRect; GetWindowRect(&bgRect); \
CRgn bgRgn; bgRgn.CreateRectRgnIndirect(bgRect);
#define ADD_NOERASE_CONTROL(IDC) \
{ CRect cr; GetDlgItem(IDC)->GetWindowRect(&cr); \
CRgn crRgn; crRgn.CreateRectRgnIndirect(&cr); \
if (bgRgn.CombineRgn(&bgRgn, &crRgn, RGN_XOR) == ERROR) return FALSE; }
#define ADD_NOERASE_RECT(noEraseRect) \
{ CRgn nrRgn; nrRgn.CreateRectRgnIndirect(noEraseRect); \
if (bgRgn.CombineRgn(&bgRgn, &nrRgn, RGN_XOR) == ERROR) return FALSE; }
#define ERASE_BKGND_END(pDC, clrBrush) \
CBrush br; br.CreateSolidBrush(clrBrush); \
CPoint ptOrg = pDC->GetWindowOrg(); \
pDC->SetWindowOrg(bgRect.TopLeft()); \
pDC->FillRgn(&bgRgn, &br); \
pDC->SetWindowOrg(ptOrg); \
br.DeleteObject();
BOOL CMyDlg::OnEraseBkgnd(CDC* pDC)
{
ERASE_BKGND_BEGIN;
ADD_NOERASE_CONTROL(IDC_BUTTON1);
ADD_NOERASE_CONTROL(IDC_BUTTON2);
ADD_NOERASE_CONTROL(IDC_LIST_STAT);
ERASE_BKGND_END(pDC, GetSysColor(COLOR_3DFACE));
return TRUE; // 關鍵:告知系統已處理
}
BOOL CExListCtrl::OnEraseBkgnd(CDC* pDC)
{
// 計算有資料的項目區域 (不需擦除)
CRect rect, dataRgnRect;
CPoint ptTL, ptBR;
GetItemPosition(0, &ptTL);
GetItemPosition(GetItemCount()-1, &ptBR);
if (!GetHeaderCtrl()->GetItemRect(GetHeaderCtrl()->GetItemCount()-1, &rect))
return CListCtrl::OnEraseBkgnd(pDC);
ptBR.x = rect.right;
dataRgnRect.SetRect(ptTL, ptBR - CPoint(2,2));
ClientToScreen(&dataRgnRect);
ERASE_BKGND_BEGIN;
ADD_NOERASE_RECT(dataRgnRect); // 排除資料區
ERASE_BKGND_END(pDC, GetBkColor());
return TRUE;
}
原理:CListCtrl 捲動時系統會先擦背景再畫項目,空白區保留背景色。巨集確保只擦除空白區,資料區保持原樣,消除閃爍。
Message Maps → Object IDs 選擇視窗類別。Messages 選擇 WM_ERASEBKGND → Add Function → Edit Code。綜合運用上述技巧,可徹底解決 90% 以上的 MFC 閃爍問題。針對極複雜場景,建議搭配 WS_EX_COMPOSITED (WinXP+) 或 WM_PRINTCLIENT 進階技術。