QQ 2006 風格視窗採用不規則造型(圓角、特殊標題列),且無標準邊框仍支援任意拖曳縮放。許多仿製專案在 SetWindowRgn 呼叫時機、無邊框拖曳邏輯上存在缺陷(如拖曳時出現紫色遮罩殘影、吸附頂端異常)。本文針對這兩大痛點提供最佳實踐方案。
錯誤做法:在 OnInitDialog 或 OnCreate 中呼叫,視窗尚未繪製,區域計算不準;或在 WM_NCPAINT 中重複呼叫導致閃爍。
正確做法:在 OnPaint 內存繪製完成、即將 BitBlt 到螢幕前 呼叫,確保區域與最終像素完美對齊。
void CMyDlg::OnPaint()
{
if (IsIconic()) { /* 圖示繪製 */ return; }
CPaintDC dc(this);
CRect rcClient; GetClientRect(&rcClient);
// 1. 建立記憶體 DC 進行離屏繪製
CDC memDC; memDC.CreateCompatibleDC(&dc);
CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rcClient.Width(), rcClient.Height());
CBitmap* pOldBmp = memDC.SelectObject(&bmp);
// 2. 在記憶體 DC 上繪製整個視窗外觀(背景、標題、按鈕、內容...)
DrawEntireWindow(&memDC, rcClient);
// 3. ★ 關鍵:根據記憶體位圖建立不規則區域並套用
ChangeWindowRgn(&memDC); // 內部呼叫 SetWindowRgn
// 4. 一次性輸出到螢幕
dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
memDC.SelectObject(pOldBmp);
bmp.DeleteObject(); memDC.DeleteDC();
}
void CMyDlg::ChangeWindowRgn(CDC* pDC)
{
COLORREF clrMask = RGB(255, 0, 255); // 魔術粉紅色作為透明遮罩
CRect rc; GetClientRect(&rc);
CRgn rgn; rgn.CreateRectRgn(0, 0, rc.Width(), rc.Height());
// 掃描位圖,排除 clrMask 色像素,建立複合區域
// ... 略:逐行掃描 CreateRectRgn + CombineRgn(RGN_OR) ...
SetWindowRgn(rgn, TRUE); // TRUE = 重繪視窗
rgn.Detach(); // 所有權轉給系統,不可 DeleteObject
}
適用於固定形狀視窗(如圓角矩形),效能更高:
void CMyDlg::OnShowWindow(BOOL bShow, UINT nStatus)
{
CDialog::OnShowWindow(bShow, nStatus);
if (bShow) {
CRect rc; GetClientRect(&rc);
CRgn rgn;
rgn.CreateRoundRectRgn(0, 0, rc.Width(), rc.Height(), 10, 10); // 圓角半徑 10
// 可進一步排除標題列按鈕區域等
SetWindowRgn(rgn, TRUE);
rgn.Detach();
}
}
傳統做法:PostMessage(WM_NCLBUTTONDOWN, HTCAPTION) 模擬標題列拖曳。缺點:拖至螢幕頂端超出螢幕範圍時,系統會強制對齊,無法實現 QQ 式「自由拖曳至螢幕外」。
void CMyDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
CRect rcBtn(/* 可拖曳區域矩形 */);
if (rcBtn.PtInRect(point)) {
SetTimer(1, 20, NULL); // 50fps 輪詢滑鼠
GetCursorPos(&m_ptLastMouse);
GetWindowRect(&m_rcLastWnd);
SetCapture();
return;
}
CDialog::OnLButtonDown(nFlags, point);
}
void CMyDlg::OnTimer(UINT nIDEvent)
{
if (nIDEvent == 1) {
POINT pt; GetCursorPos(&pt);
CPoint delta(pt.x - m_ptLastMouse.x, pt.y - m_ptLastMouse.y);
// 判斷是移動還是縮放(根據點擊區域:標題列=移動,右下角=縮放)
if (m_bDraggingTitle) { // 移動
m_rcLastWnd.OffsetRect(delta);
} else { // 右下角縮放
m_rcLastWnd.right += delta.x;
m_rcLastWnd.bottom += delta.y;
// 限制最小尺寸
if (m_rcLastWnd.Width() < MIN_W) m_rcLastWnd.right = m_rcLastWnd.left + MIN_W;
if (m_rcLastWnd.Height() < MIN_H) m_rcLastWnd.bottom = m_rcLastWnd.top + MIN_H;
}
MoveWindow(&m_rcLastWnd);
m_ptLastMouse = pt;
Invalidate(); // 觸發 OnPaint 重繪(含 SetWindowRgn)
}
CDialog::OnTimer(nIDEvent);
}
void CMyDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
if (m_bDragging) { KillTimer(1); ReleaseCapture(); m_bDragging = FALSE; }
CDialog::OnLButtonUp(nFlags, point);
}
QQ 換膚後,標準邊框消失,需在 OnLButtonDown 中判斷點擊座標是否落在「標題列區域」(移動)或「右下角 20x20 區域」(縮放),而非全客戶區皆可拖曳。

OnPaint 記憶體 DC 繪製完、BitBlt 前)呼叫,區域才與視覺完美吻合,杜絕紫色遮罩殘留。WM_NCLBUTTONDOWN 更靈活,可突破螢幕邊界限制,實現真正的「自由拖曳」。WM_NCHITTEST 回傳 HTCAPTION/HTBOTTOMRIGHT 等,系統仍能提供標準游標回饋(雙向箭頭等)。CRegionBuilder 工具類,支援圓角矩形、多邊形、位圖透明色掃描等多種建模方式。QQ 界面的精髓在於「像素級控制」與「突破系統預設行為」。掌握 OnPaint 區域建立時機與 Timer+MoveWindow 自由拖曳技巧,是實現高品質非標準視窗的關鍵基石。