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

打造磁性視窗:視窗自動吸附與聯動移動完整實作

     編輯:關於C++

前言

Winamp、CDOK 等經典播放器普及了「磁性視窗」體驗:多個視窗靠近時自動吸附,拖動主視窗時吸附視窗跟隨移動。本文提供基於 C++ Builder 的完整實作方案,涵蓋吸附判斷、狀態管理、聯動移動三大核心模組。

核心原理:兩大關鍵步驟

  1. 吸附偵測:在視窗移動過程中即時判斷與目標視窗距離,達閾值則調整位置並記錄吸附狀態
  2. 聯動移動:主視窗移動時,同步位移所有吸附於其上的視窗

為什麼不用 Timer?

輪詢 Timer 消耗 CPU 且即時性難保證。正解是攔截 Windows 視窗移動訊息:

  • WM_MOVING:移動中(可即時調整位置)
  • WM_MOVE:移動結束(最終定位)

兩者 lParam 均為 RECT*,比 WM_WINDOWPOSCHANGING 更純粹適合此場景。

資料結構設計

// 吸附狀態枚舉(對應八個方向與角落)
enum enumAttachStyle {
    AS_NONE, AS_TOP, AS_BOTTOM, AS_T_TOP,
    AS_LEFT, AS_RIGHT, AS_L_LEFT
};

// 封裝單一視窗吸附資訊
class CFormAttachStyle {
public:
    bool Enabled;       // 防重入旗標
    HWND AttachTo;      // 吸附目標視窗句柄
    int XStyle, YStyle; // 水平/垂直吸附狀態
    int xPos, yPos;     // 吸附後目標座標
    CFormAttachStyle() : Enabled(true), AttachTo(NULL),
        XStyle(AS_NONE), YStyle(AS_NONE) {}
};

關鍵演算法:距離判斷與吸附邏輯

// 判斷兩整數距離是否在閾值內
bool DistanceIn(unsigned int a, unsigned int b, unsigned int threshold) {
    if (a > b) std::swap(a, b);
    return (b - a) <= threshold;
}

// 判斷值是否在區間 [min, max]
bool Mid(unsigned int min, unsigned int val, unsigned int max) {
    return (min <= val) && (val <= max);
}

核心吸附函式 AttachToForm()

bool AttachToForm(TForm* My, TForm* Target, RECT* r, int distance) {
    CFormAttachStyle* style = (CFormAttachStyle*)My->Tag;
    if (!style) return false;

    RECT targetRect; GetWindowRect(Target->Handle, &targetRect);
    style->AttachTo = NULL;
    style->xPos = r->left; style->yPos = r->top;

    // 垂直方向判斷(上/下/頂部對齊)
    style->YStyle = AS_NONE;
    if (Mid(targetRect.left, r->left, targetRect.right) ||
        Mid(r->left, targetRect.left, r->right) ||
        style->XStyle != AS_NONE) {
        if (DistanceIn(r->top, targetRect.bottom, distance)) {
            style->YStyle = AS_BOTTOM; style->yPos = targetRect.bottom;
        } else if (DistanceIn(r->top, targetRect.top, distance)) {
            style->YStyle = AS_TOP; style->yPos = targetRect.top;
        } else if (DistanceIn(r->bottom, targetRect.top, distance)) {
            style->YStyle = AS_T_TOP; 
            style->yPos = targetRect.top - (r->bottom - r->top);
        }
    }

    // 水平方向判斷(左/右/左側對齊)
    style->XStyle = AS_NONE;
    if (Mid(targetRect.top, r->top, targetRect.bottom) ||
        Mid(r->top, targetRect.top, r->bottom) ||
        style->YStyle != AS_NONE) {
        if (DistanceIn(r->left, targetRect.left, distance)) {
            style->XStyle = AS_LEFT; style->xPos = targetRect.left;
        } else if (DistanceIn(r->left, targetRect.right, distance)) {
            style->XStyle = AS_RIGHT; style->xPos = targetRect.right;
        } else if (DistanceIn(r->right, targetRect.left, distance)) {
            style->XStyle = AS_L_LEFT;
            style->xPos = targetRect.left - (r->right - r->left);
        }
    }

    My->Left = style->xPos;
    My->Top = style->yPos;
    if (style->XStyle != AS_NONE || style->YStyle != AS_NONE)
        style->AttachTo = Target->Handle;

    return style->AttachTo != NULL;
}

訊息處理鏈

1. 子視窗處理 WM_MOVING(吸附偵測)

void Do_WM_MOVING(TForm* My, TMessage& Msg) {
    CFormAttachStyle* style = (CFormAttachStyle*)My->Tag;
    if (style && style->Enabled) {
        style->Enabled = false; // 防重入
        RECT* r = (RECT*)Msg.LParam;
        AttachToForm(My, Application->MainForm, r, 12); // 僅檢測主視窗
        style->Enabled = true;
    }
    Msg.Result = 0;
}

2. 主視窗處理 WM_MOVING(聯動移動)

void AdjustFormPos(TForm* Main, RECT* r) {
    int dx = r->left - Main->Left;
    int dy = r->top - Main->Top;
    Main->Left = r->left; Main->Top = r->top;

    for (int i = 0; i < Screen->FormCount; ++i) {
        TForm* f = Screen->Forms[i];
        if (f != Main) UnionOtherForm(Main, f, dx, dy);
    }
}

void UnionOtherForm(TForm* Main, TForm* Other, int dx, int dy) {
    CFormAttachStyle* style = (CFormAttachStyle*)Other->Tag;
    if (style && style->Enabled && style->AttachTo == Main->Handle) {
        style->Enabled = false;
        SetWindowPos(Other->Handle, Main->Handle,
            Other->Left + dx, Other->Top + dy,
            Other->Width, Other->Height,
            SWP_NOSIZE | SWP_NOACTIVATE);
        style->Enabled = true;
    }
}

3. 子視窗處理 WM_MOVE(吸附後最終定位)

void Do_WM_MOVE(TForm* My, TMessage& Msg) {
    CFormAttachStyle* style = (CFormAttachStyle*)My->Tag;
    if (style && style->Enabled && style->AttachTo) {
        My->Left = style->xPos;
        My->Top = style->yPos;
    }
    Msg.Result = 0;
}

視窗子類化掛鉤

// 子視窗
void __fastcall TForm1::WndProc(TMessage& Msg) {
    TForm::WndProc(Msg);
    switch (Msg.Msg) {
        case WM_MOVING: Do_WM_MOVING(this, Msg); break;
        case WM_MOVE:   Do_WM_MOVE(this, Msg);   break;
    }
}

void __fastcall TForm1::FormCreate(TObject* Sender) {
    Tag = (int)new CFormAttachStyle();
}
void __fastcall TForm1::FormDestroy(TObject* Sender) {
    delete (CFormAttachStyle*)Tag;
}

// 主視窗
void __fastcall TMainForm::WndProc(TMessage& Msg) {
    TForm::WndProc(Msg);
    if (Msg.Msg == WM_MOVING)
        AdjustFormPos(this, (RECT*)Msg.LParam);
}

優化建議

  • 利用 Screen->Forms 遍歷所有視窗,自動支援任意數量磁性視窗
  • 若需無主從關係(如 CDOK),主視窗同樣掛鉤 WM_MOVING 呼叫 AdjustFormPos,子視窗吸附目標可擴展為任意視窗
  • 吸附距離 distance 建議 10~15 像素,體驗最佳
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved