Winamp、CDOK 等經典播放器普及了「磁性視窗」體驗:多個視窗靠近時自動吸附,拖動主視窗時吸附視窗跟隨移動。本文提供基於 C++ Builder 的完整實作方案,涵蓋吸附判斷、狀態管理、聯動移動三大核心模組。
輪詢 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;
}
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;
}
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;
}
}
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 遍歷所有視窗,自動支援任意數量磁性視窗WM_MOVING 呼叫 AdjustFormPos,子視窗吸附目標可擴展為任意視窗distance 建議 10~15 像素,體驗最佳