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

WinForm 無邊框視窗拖曳移動 — 兩種核心技術完整實作 (WM_SYSCOMMAND / HitTest)

     編輯:關於.NET

前言

設定 FormBorderStyle = None 後,視窗失去標題列,使用者無法拖曳移動。本文提供兩種經典解法:

  1. WM_SYSCOMMAND + SC_MOVE:模擬使用者點擊標題列移動,相容性最好。
  2. 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

  1. Q: 方法一移動時視窗內容不重繪?
    A: 此為系統預設行為 (僅顯示輪廓)。若需即時重繪,請改用方法二。
  2. Q: 方法二雙擊標題列無法最大化?
    A: 因攔截了 HTCAPTION 雙擊訊息。解法:在 WndProc 偵測雙擊 (WM_NCLBUTTONDBLCLK = 0x00A3) 並手動切換 WindowState。
  3. Q: 高 DPI (150%/200%) 下座標偏移?
    A: m.LParam 為螢幕實體像素座標,PointToClient 會自動換算,通常無問題。若用自訂邊框寬度,建議用 LogicalToDeviceUnits 換算。
  4. Q: 子視窗 (MDI Child) 無法拖曳?
    A: MDI Child 的 FormBorderStyle 鎖定,需在父視窗處理或改用方法一。

選型建議

  • 簡單需求、快速原型、相容性優先 → 方法一 (SC_MOVE)
  • 生產級應用、需原生體驗、支援調整大小、Aero Snap → 方法二 (HitTest)
  • 需同時支援觸控螢幕拖曳 → 方法二 + WM_POINTERDOWN 處理
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved