QQ 主面板的「邊緣吸附自動伸縮」是其標誌性交互:拖曳至螢幕四邊自動吸附並展開,滑鼠移開自動收縮為一條細線。網上流傳的 Demo 多與真機行為有出入。本文基於對 QQ 2005/2006 真機行為的 12 點詳細觀察,實現近乎完美的仿真版本。
INTERVAL(20px);底邊以工作列上緣為準。top >= 0)。WS_EX_TOPMOST 樣式。top = -edgeHeight 而非 0)。WM_MOUSELEAVE 觸發。WM_MOVING 參數語意,導致釋放滑鼠時高度不同步(非 Bug,需相容)。WS_EX_TOOLWINDOW)。class CQQHideWndDlg : public CDialog
{
protected:
void FixMoving(UINT fwSide, LPRECT pRect); // WM_MOVING 中修正位置/判定吸附模式
void DoShow(); // 從收縮態展開(動畫)
void DoHide(); // 從展開態收縮(動畫)
BOOL SetWindowPos(const CWnd*, LPCRECT, UINT = SWP_SHOWWINDOW);
private:
BOOL m_isSizeChanged; // 左/右吸附時高度是否已改變
BOOL m_isSetTimer; // 是否啟用滑鼠偵測計時器
int m_oldWndHeight; // 左/右吸附前的原始高度
int m_taskBarHeight; // 工作列高度
int m_edgeHeight; // 系統邊框高度 (SM_CYEDGE)
int m_edgeWidth; // 系統邊框寬度 (SM_CXFRAME)
int m_hideMode; // 當前吸附模式
BOOL m_hsFinished; // 動畫是否完成
BOOL m_hiding; // TRUE=收縮中, FALSE=展開中
};
#define HM_NONE 0
#define HM_TOP 1
#define HM_BOTTOM 2
#define HM_LEFT 3
#define HM_RIGHT 4
int CQQHideWndDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDialog::OnCreate(lpCreateStruct) == -1) return -1;
// 取得工作列高度
CWnd* pTask = FindWindow(_T("Shell_TrayWnd"), NULL);
if (pTask) { CRect rc; pTask->GetWindowRect(&rc); m_taskBarHeight = rc.Height(); }
// 不在工作列顯示按鈕
ModifyStyleEx(WS_EX_APPWINDOW, WS_EX_TOOLWINDOW);
// 系統邊框度量
m_edgeHeight = GetSystemMetrics(SM_CYEDGE);
m_edgeWidth = GetSystemMetrics(SM_CXFRAME);
return 0;
}
UINT CQQHideWndDlg::OnNcHitTest(CPoint point)
{
if (m_hideMode != HM_NONE && !m_isSetTimer &&
point.x < GetSystemMetrics(SM_CXSCREEN) + INFALTE) // 防右邊閃爍
{
SetTimer(1, CM_ELAPSE, NULL); // 滑鼠離開偵測計時器 (200ms)
m_isSetTimer = TRUE;
m_hsFinished = FALSE;
m_hiding = FALSE;
SetTimer(2, HS_ELAPSE, NULL); // 動畫幀計時器 (5ms)
}
return CDialog::OnNcHitTest(point);
}
void CQQHideWndDlg::OnTimer(UINT nIDEvent)
{
if (nIDEvent == 1) { // 滑鼠離開偵測
POINT pt; GetCursorPos(&pt);
CRect rc; GetWindowRect(&rc);
rc.InflateRect(INFALTE, INFALTE); // 10px 緩衝
if (!rc.PtInRect(pt)) {
KillTimer(1); m_isSetTimer = FALSE;
m_hsFinished = FALSE; m_hiding = TRUE;
SetTimer(2, HS_ELAPSE, NULL);
}
}
else if (nIDEvent == 2) { // 動畫幀
if (m_hsFinished) KillTimer(2);
else m_hiding ? DoHide() : DoShow();
}
CDialog::OnTimer(nIDEvent);
}
void CQQHideWndDlg::FixMoving(UINT fwSide, LPRECT pRect)
{
POINT pt; GetCursorPos(&pt);
int scrH = GetSystemMetrics(SM_CYSCREEN), scrW = GetSystemMetrics(SM_CXSCREEN);
int h = pRect->bottom - pRect->top, w = pRect->right - pRect->left;
if (pt.y <= INTERVAL) { // 吸附頂部
pRect->bottom = h - m_edgeHeight; pRect->top = -m_edgeHeight;
m_hideMode = HM_TOP;
}
else if (pt.y >= scrH - INTERVAL - m_taskBarHeight) { // 吸附底部
pRect->top = scrH - m_taskBarHeight - h; pRect->bottom = scrH - m_taskBarHeight;
m_hideMode = HM_BOTTOM;
}
else if (pt.x < INTERVAL) { // 吸附左側
if (!m_isSizeChanged) { CRect rc; GetWindowRect(&rc); m_oldWndHeight = rc.Height(); }
pRect->right = w; pRect->left = 0;
pRect->top = -m_edgeHeight; pRect->bottom = scrH - m_taskBarHeight;
m_isSizeChanged = TRUE; m_hideMode = HM_LEFT;
}
else if (pt.x >= scrW - INTERVAL) { // 吸附右側
if (!m_isSizeChanged) { CRect rc; GetWindowRect(&rc); m_oldWndHeight = rc.Height(); }
pRect->left = scrW - w; pRect->right = scrW;
pRect->top = -m_edgeHeight; pRect->bottom = scrH - m_taskBarHeight;
m_isSizeChanged = TRUE; m_hideMode = HM_RIGHT;
}
else { // 非吸附區
if (m_isSizeChanged) { pRect->bottom = pRect->top + m_oldWndHeight; m_isSizeChanged = FALSE; }
if (m_isSetTimer) { if (KillTimer(1)) m_isSetTimer = FALSE; }
m_hideMode = HM_NONE;
}
}
void CQQHideWndDlg::DoHide()
{
if (m_hideMode == HM_NONE) return;
CRect rc; GetWindowRect(&rc);
int h = rc.Height(), w = rc.Width(), step;
switch (m_hideMode) {
case HM_TOP:
step = h / HS_STEPS; rc.bottom -= step;
if (rc.bottom <= m_edgeWidth) { rc.bottom = m_edgeWidth; m_hsFinished = TRUE; }
rc.top = rc.bottom - h; break;
case HM_BOTTOM:
step = h / HS_STEPS; rc.top += step;
if (rc.top >= scrH - m_edgeWidth) { rc.top = scrH - m_edgeWidth; m_hsFinished = TRUE; }
rc.bottom = rc.top + h; break;
case HM_LEFT:
step = w / HS_STEPS; rc.right -= step;
if (rc.right <= m_edgeWidth) { rc.right = m_edgeWidth; m_hsFinished = TRUE; }
rc.left = rc.right - w; rc.top = -m_edgeHeight; rc.bottom = scrH - m_taskBarHeight; break;
case HM_RIGHT:
step = w / HS_STEPS; rc.left += step;
if (rc.left >= scrW - m_edgeWidth) { rc.left = scrW - m_edgeWidth; m_hsFinished = TRUE; }
rc.right = rc.left + w; rc.top = -m_edgeHeight; rc.bottom = scrH - m_taskBarHeight; break;
}
SetWindowPos(&wndTopMost, rc);
}
void CQQHideWndDlg::DoShow() // 邏輯對稱,step 方向相反
{
// ... 類似實現,rc.top += step (TOP), rc.top -= step (BOTTOM) 等
// 關鍵:展開完成時 rc.top = -m_edgeHeight (頂/左/右) 或 rc.top = scrH - h (底)
}
WM_SIZING 處理:限制最小寬/高,防止使用者拖曳過小。HS_STEPS=10、幀間隔 HS_ELAPSE=5ms 可調整手感。m_hiding 翻轉),極具 QQ 神韻。完美仿真需要「觀察細、邏輯密、實現異」。本實現覆蓋 12 大真機行為點,動畫流暢、邊界處理嚴謹。源碼已上傳知識庫,歡迎下載體驗與指正。