在繪圖、CAD、圖像處理等 MFC 視圖應用中,滑鼠移動時於游標旁即時顯示座標 ([x, y]) 是基本需求。傳統 Invalidate() 重繪會導致整視圖閃爍、效能低落。本文採用離屏備份 + BitBlt 還原技術,實現零閃爍、零殘影、不觸發 OnDraw的高效能解決方案,並對原始碼進行現代化重構 (RAII 資源管理、錯誤處理、可配置常數)。
// CoordTracker.h
#pragma once
namespace CoordTrackerConfig
{
constexpr int CHAR_HEIGHT = 18; // 字元高度 (像素)
constexpr int CHAR_WIDTH = 7; // 字元寬度 (像素)
constexpr int OFFSET_X = 15; // 文字相對游標 X 偏移
constexpr int OFFSET_Y = 10; // 文字相對游標 Y 偏移
constexpr COLORREF TEXT_COLOR = RGB(255, 0, 0); // 紅色文字
}
// CoordTracker.h (續)
class CCoordBackup
{
public:
CCoordBackup() = default;
~CCoordBackup() { Release(); }
// 禁止複製,支援移動
CCoordBackup(const CCoordBackup&) = delete;
CCoordBackup& operator=(const CCoordBackup&) = delete;
CCoordBackup(CCoordBackup&& other) noexcept
: m_dc(other.m_dc), m_bmp(other.m_bmp), m_size(other.m_size)
{
other.m_dc = nullptr; other.m_bmp = nullptr;
}
CCoordBackup& operator=(CCoordBackup&& other) noexcept
{
if (this != &other) { Release(); m_dc = other.m_dc; m_bmp = other.m_bmp; m_size = other.m_size; other.m_dc = nullptr; other.m_bmp = nullptr; }
return *this;
}
// 建立備份:從 pDC 複製 rc 區域
bool Create(CDC* pDC, const CRect& rc)
{
Release();
m_size = rc.Size();
if (!m_dc.CreateCompatibleDC(pDC)) return false;
if (!m_bmp.CreateCompatibleBitmap(pDC, m_size.cx, m_size.cy)) { m_dc.DeleteDC(); return false; }
CBitmap* pOld = m_dc.SelectObject(&m_bmp);
m_dc.BitBlt(0, 0, m_size.cx, m_size.cy, pDC, rc.left, rc.top, SRCCOPY);
m_dc.SelectObject(pOld);
return true;
}
// 還原至 pDC 的 rc 區域
void Restore(CDC* pDC, const CRect& rc) const
{
if (!m_dc || !m_bmp) return;
CBitmap* pOld = m_dc.SelectObject(&m_bmp);
pDC->BitBlt(rc.left, rc.top, m_size.cx, m_size.cy, &m_dc, 0, 0, SRCCOPY);
m_dc.SelectObject(pOld);
}
bool IsValid() const { return m_dc && m_bmp; }
CSize GetSize() const { return m_size; }
private:
void Release()
{
if (m_bmp) { m_bmp.DeleteObject(); m_bmp = nullptr; }
if (m_dc) { m_dc.DeleteDC(); m_dc = nullptr; }
m_size = CSize(0,0);
}
CDC m_dc;
CBitmap m_bmp;
CSize m_size;
};
// MyView.h
#include "CoordTracker.h"
class CMyView : public CView
{
// ...
protected:
CCoordBackup m_backup; // 座標區域備份
CPoint m_ptLastMouse; // 上一次游標位置
bool m_bFirstMove = true; // 是否首次移動
int m_nTextWidth = 0; // 當前文字寬度
// 內部輔助函式
CRect CalcTextRect(CPoint ptMouse) const;
void DrawCoordText(CDC* pDC, CPoint ptMouse);
void OnMouseMove(UINT nFlags, CPoint point) override;
void OnDestroy() override;
};
#include "stdafx.h"
#include "MyView.h"
#include "CoordTracker.h"
using namespace CoordTrackerConfig;
CRect CMyView::CalcTextRect(CPoint ptMouse) const
{
return CRect(
ptMouse.x + OFFSET_X,
ptMouse.y + OFFSET_Y,
ptMouse.x + OFFSET_X + m_nTextWidth,
ptMouse.y + OFFSET_Y + CHAR_HEIGHT
);
}
void CMyView::DrawCoordText(CDC* pDC, CPoint ptMouse)
{
if (!m_backup.IsValid()) return;
CString str;
str.Format(_T("[%d, %d]"), ptMouse.x, ptMouse.y);
m_nTextWidth = str.GetLength() * CHAR_WIDTH;
CRect rcText = CalcTextRect(ptMouse);
pDC->SetBkMode(TRANSPARENT);
COLORREF oldColor = pDC->SetTextColor(TEXT_COLOR);
pDC->DrawText(str, &rcText, DT_LEFT | DT_TOP | DT_SINGLELINE);
pDC->SetTextColor(oldColor);
}
void CMyView::OnMouseMove(UINT nFlags, CPoint point)
{
CClientDC dc(this); // 自動 ReleaseDC
if (!dc.GetSafeHdc()) return;
CRect rcCurrent = CalcTextRect(point);
CString str;
str.Format(_T("[%d, %d]"), point.x, point.y);
m_nTextWidth = str.GetLength() * CHAR_WIDTH;
rcCurrent.right = rcCurrent.left + m_nTextWidth; // 更新寬度
if (!m_bFirstMove && m_backup.IsValid())
{
// 還原上一幀
m_backup.Restore(&dc, CalcTextRect(m_ptLastMouse));
}
else
{
m_bFirstMove = false;
}
// 備份當前區域
if (!m_backup.Create(&dc, rcCurrent))
{
TRACE(_T("CoordTracker: Backup creation failed\n"));
return;
}
// 繪製新座標
DrawCoordText(&dc, point);
m_ptLastMouse = point;
CView::OnMouseMove(nFlags, point);
}
void CMyView::OnDestroy()
{
// CCoordBackup 解構子自動釋放資源
CView::OnDestroy();
}
| 指標 | 傳統 Invalidate | 本方案 (BitBlt 雙緩衝) |
|---|---|---|
| 繪圖呼叫 | 完整 OnDraw (所有物件) | 1 次 BitBlt (還原) + 1 次 BitBlt (備份) + 1 次 DrawText |
| 閃爍 | 明顯 (WM_ERASEBKGND + OnDraw) | 無 (像素級還原) |
| 殘影 | 無 (背景重繪) | 無 (精確備份還原) |
| CPU 佔用 (滑鼠高速移動) | 高 (大量重繪) | 極低 (固定 3 GDI 呼叫) |
| 記憶體 | 無額外 | 1 個小型位圖 (~文字區域大小,通常 < 2KB) |
| 程式碼複雜度 | 低 | 中 (封裝後極簡單) |
ClientDC 時座標已為用戶區,無需額外轉換。若用 GetDC() 需確認座標系統一致性。CHAR_WIDTH 為近似值。精確做法:pDC->GetTextExtent(str).cx。但會增加 GetTextExtent 呼叫開銷,視專案效能需求取捨。CHAR_WIDTH/HEIGHT 需隨 DPI 縮放。建議在 OnInitialUpdate 以 GetDeviceCaps(LOGPIXELSX) 計算實際字元大小。OnMouseMove 的 point 為用戶區座標 (已扣除捲動位移)。若需邏輯座標 (文件座標),需加上 GetScrollPosition()。以 CCoordBackup RAII 類別封裝 BitBlt 備份/還原邏輯,視圖類僅需 3 行核心代碼即可實現專業級滑鼠座標跟蹤顯示。此模式適用於所有「非破壞性疊加繪製」場景,是 MFC/GDI 開發者必備的高效能繪圖技巧。建議將 CoordTracker.h/.cpp 加入專案共用庫,下次需求直接複用。