前言
設定 FormBorderStyle = None 後,視窗失去標題列,使用者無法拖曳移動。本文提供兩種經典解法:
- WM_SYSCOMMAND + SC_MOVE:模擬使用者點擊標題列移動,相容性最好。
- WM_NCHITTEST (HitTest):讓用戶區回傳
HTCAPTION,系統視為標題列,原生支援拖曳、最大化邊框吸附等行為。
方法一:發送 WM_SYSCOMMAND / SC_MOVE
核心原理
ReleaseCapture():釋放滑鼠捕獲,讓訊息傳遞給視窗程序。
SendMessage(WM_SYSCOMMAND, SC_MOVE, 0):發送系統命令「移動視窗」,系統會進入移動模式,直到滑鼠釋放。
完整程式碼 (C# WinForm)
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public partial class BorderlessForm : Form
{
// P/Invoke 宣告
[DllImport("user32.dll", EntryPoint = "SendMessage")]
private static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
[DllImport("user32.dll", EntryPoint = "ReleaseCapture")]
private static extern bool ReleaseCapture();
private const int WM_SYSCOMMAND = 0x0112;
private const int SC_MOVE = 0xF010; // 注意:SC_MOVE = 0xF010,非 0xF012 (SC_SIZE)
private const int HTCAPTION = 0x02;
public BorderlessForm()
{
InitializeComponent();
this.MouseDown += BorderlessForm_MouseDown;
}
private void BorderlessForm_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
ReleaseCapture();
SendMessage(this.Handle, WM_SYSCOMMAND, SC_MOVE, 0);
}
}
}
優缺點分析
| 優點 | 缺點 |
| 程式碼極少、易理解 | 移動時無法即時重繪視窗內容 (僅顯示輪廓) |
| 所有 Windows 版本相容 | 若滑鼠快速移出螢幕可能殘留移動模式 |
| 不需覆寫 WndProc | 無法精細控制邊框吸附、調整大小行為 |
方法二:覆寫 WndProc 處理 WM_NCHITTEST
核心原理
- 系統詢問「滑鼠座標屬於視窗哪個區域?」 →
WM_NCHITTEST (0x0084)
- 預設用戶區回傳
HTCLIENT (1),標題列回傳 HTCAPTION (2)
- 我們在
WndProc 攔截,若回傳值為 HTCLIENT 則改為 HTCAPTION,系統便視為點擊標題列,原生支援拖曳移動。
完整程式碼
using System;
using System.Windows.Forms;
public partial class BorderlessForm2 : Form
{
public BorderlessForm2()
{
InitializeComponent();
this.FormBorderStyle = FormBorderStyle.None;
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
const int WM_NCHITTEST = 0x0084;
const int HTCLIENT = 1;
const int HTCAPTION = 2;
if (m.Msg == WM_NCHITTEST)
{
// m.Result 為系統原始判定結果
if (m.Result == (IntPtr)HTCLIENT)
{
// 強制宣稱滑鼠在標題列區域
m.Result = (IntPtr)HTCAPTION;
}
}
}
}
進階:排除特定控制項區域 (如按鈕、文字框)
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == 0x0084 && m.Result == (IntPtr)1) // HTCLIENT
{
// 取得滑鼠在用戶區座標
Point clientPos = this.PointToClient(new Point(
(int)m.LParam & 0xFFFF,
(int)m.LParam >> 16));
// 檢查是否在「不可拖曳」控制項上方
Control c = this.GetChildAtPoint(clientPos);
if (c != null && (c is Button || c is TextBox || c is DataGridView))
{
// 保持 HTCLIENT,讓控制項正常接收點擊
return;
}
m.Result = (IntPtr)2; // HTCAPTION
}
}
優缺點分析
| 優點 | 缺點 |
| 原生拖曳體驗 (即時重繪、邊框吸附、Aero Snap) | 需覆寫 WndProc,稍微進階 |
| 可精細控制哪些區域可拖曳 | 若邏輯錯誤可能導致標題列功能異常 (無法雙擊最大化等) |
| 支援調整大小 (結合 HTLEFT/HTRIGHT/HTBOTTOM...) | 高 DPI 縮放下座標換算需注意 |
完整支援調整大小的 HitTest 實作 (進階)
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg != 0x0084) return; // WM_NCHITTEST
var pos = this.PointToClient(new Point((int)m.LParam & 0xFFFF, (int)m.LParam >> 16));
var bounds = this.ClientRectangle;
const int border = 6; // 邊框判定寬度 (像素)
// 角落
if (pos.X <= border && pos.Y <= border) { m.Result = (IntPtr)13; return; } // HTTOPLEFT
if (pos.X >= bounds.Width - border && pos.Y <= border) { m.Result = (IntPtr)14; return; } // HTTOPRIGHT
if (pos.X <= border && pos.Y >= bounds.Height - border) { m.Result = (IntPtr)16; return; } // HTBOTTOMLEFT
if (pos.X >= bounds.Width - border && pos.Y >= bounds.Height - border) { m.Result = (IntPtr)15; return; } // HTBOTTOMRIGHT
// 邊緣
if (pos.Y <= border) { m.Result = (IntPtr)12; return; } // HTTOP
if (pos.Y >= bounds.Height - border) { m.Result = (IntPtr)15; return; } // HTBOTTOM
if (pos.X <= border) { m.Result = (IntPtr)10; return; } // HTLEFT
if (pos.X >= bounds.Width - border) { m.Result = (IntPtr)11; return; } // HTRIGHT
// 用戶區 → 標題列 (可拖曳)
var c = this.GetChildAtPoint(pos);
if (c == null || !(c is Button || c is TextBox || c is DataGridView))
m.Result = (IntPtr)2; // HTCAPTION
}
常見問題 FAQ
- Q: 方法一移動時視窗內容不重繪?
A: 此為系統預設行為 (僅顯示輪廓)。若需即時重繪,請改用方法二。
- Q: 方法二雙擊標題列無法最大化?
A: 因攔截了 HTCAPTION 雙擊訊息。解法:在 WndProc 偵測雙擊 (WM_NCLBUTTONDBLCLK = 0x00A3) 並手動切換 WindowState。
- Q: 高 DPI (150%/200%) 下座標偏移?
A: m.LParam 為螢幕實體像素座標,PointToClient 會自動換算,通常無問題。若用自訂邊框寬度,建議用 LogicalToDeviceUnits 換算。
- Q: 子視窗 (MDI Child) 無法拖曳?
A: MDI Child 的 FormBorderStyle 鎖定,需在父視窗處理或改用方法一。
選型建議
- 簡單需求、快速原型、相容性優先 → 方法一 (
SC_MOVE)
- 生產級應用、需原生體驗、支援調整大小、Aero Snap → 方法二 (
HitTest)
- 需同時支援觸控螢幕拖曳 → 方法二 +
WM_POINTERDOWN 處理