如何实现一个无边框Form的移动和改变大小(二)
接着上文:这里写链接内容
我们来说说一个比较复杂的实现,
效果如图:
注意为了能够凸显没有NC(NotClient)区域,我们额外用了3个panel分别放在窗体的左右和下部。用来模拟客户自己的控件。
下面我们说下这种真正的无边框Form的实现方法
下面先无责任的贴下代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using norlib;
using norlib.Controls;
using norlib.Error;
using norlib.Native;
using norlib.SystemExtension;
namespace norlib.Controls
{
public partial class BorderlessForm
:Form
{
public BorderlessForm()
{
InitializeComponent();
_caption = 0;
_mp = new MousePreview(this);
#region 初始化_dtCursor
_dtCursor.Add(HT.HTBOTTOM, Cursors.SizeNS);
_dtCursor.Add(HT.HTTOP, Cursors.SizeNS);
_dtCursor.Add(HT.HTLEFT, Cursors.SizeWE);
_dtCursor.Add(HT.HTRIGHT, Cursors.SizeWE);
_dtCursor.Add(HT.HTTOPLEFT, Cursors.SizeNWSE);
_dtCursor.Add(HT.HTTOPRIGHT, Cursors.SizeNESW);
_dtCursor.Add(HT.HTBOTTOMLEFT, Cursors.SizeNESW);
_dtCursor.Add(HT.HTBOTTOMRIGHT, Cursors.SizeNWSE);
#endregion
_mp.AddMouseMessage(WM.WM_LBUTTONDOWN, mp_LButtonDownPreview);
_mp.AddMouseMessage(WM.WM_MOUSEMOVE, mp_MouseMovePreview);
//不能选这个
//this.Capture = true;
}
public int Border
{
get { return _border; }
set
{
if (value <= 0)
return;
_border = value;
}
}
public int Caption
{
get { return _caption; }
set { _caption = value; }
}
eMPResult mp_LButtonDownPreview(WM arg_wm, ref MOUSEHOOKSTRUCT argr_stInfo)
{
var p = argr_stInfo.pt;
var cp = this.PointToClient(new Point(p.x, p.y));
var hc = _GetHT(cp, _border, _caption);
if (hc != HT.HTERROR && hc != HT.HTCLIENT)
{
Task.Factory.StartNew(() =>
{
this.BeginInvoke(new Action(()=>
{
NativeMethods.ReleaseCapture();
NativeMethods.SendMessage(this.Handle, WM.WM_NCLBUTTONDOWN, (UIntPtr)hc, (IntPtr)0);
}));
});
return eMPResult.CutOffMessage;
}
else
{
return eMPResult.ContinueHook;
}
}
eMPResult mp_MouseMovePreview(WM arg_wm, ref MOUSEHOOKSTRUCT argr_stInfo)
{
var p = argr_stInfo.pt;
var cp = this.PointToClient(new Point(p.x, p.y));
var hc = _GetHT(cp, _border, _caption);
if (hc != HT.HTCLIENT && hc != HT.HTERROR)
{
var c = _GetCursor(hc);
if (Cursor != c)
Cursor = c;
return eMPResult.ContinueHook;
}
else
{
Cursor = Cursors.Default;
return eMPResult.ContinueHook;
}
}
HT _GetHT(Point arg_p, int arg_border, int arg_caption)
{
var pos = arg_p;
var border = arg_border;
var caption = arg_caption;
if (pos.X < 0 || pos.Y < 0)
{
//
//非法位置
//
return HT.HTERROR;
}
else if (pos.X <= border)
{
//
//左侧
//
if (pos.Y <= border)
{
//左上侧
return HT.HTTOPLEFT;
}
else if (pos.Y >= this.Height - border)
{
//左下侧
return HT.HTBOTTOMLEFT;
}
else
{
//左侧
return HT.HTLEFT;
}
}
else if (pos.X >= this.Width - border)
{
//
//右侧
//
if (pos.Y <= border)
{
//右上侧
return HT.HTTOPRIGHT;
}
else if (pos.Y >= this.Height - border)
{
//右下侧
return HT.HTBOTTOMRIGHT;
}
else
{
//右侧
return HT.HTRIGHT;
}
}
else
{
//
//中部
//
if (pos.Y <= border)
{
//上中侧
return HT.HTTOP;
}
else if (pos.Y >= this.Height - border)
{
//下中侧
return HT.HTBOTTOM;
}
else if (pos.Y <= caption)
{
return HT.HTCAPTION;
}
else
{
return HT.HTCLIENT;
}
}
}
Cursor _GetCursor(HT arg_ht)
{
var cursor = (Cursor)null;
if (_dtCursor.TryGetValue(arg_ht, out cursor))
{
return cursor;
}
else
{
return Cursors.Default;
}
}
MousePreview _mp;
readonly Dictionary<HT, Cursor> _dtCursor = new Dictionary<HT, Cursor>();
int _border = 5;
int _caption = 20;
}
}
其主要思想是通过设置SetWindowsHookEx的WH_MOUSE来截获当前应用程序的鼠标事件。
随后我们对WM_LBUTTONDOWN的消息加点料
var p = argr_stInfo.pt;
var cp = this.PointToClient(new Point(p.x, p.y));
var hc = _GetHT(cp, _border, _caption);
if (hc != HT.HTERROR && hc != HT.HTCLIENT)
{
Task.Factory.StartNew(() =>
{
this.BeginInvoke(new Action(()=>
{
NativeMethods.ReleaseCapture();
NativeMethods.SendMessage(this.Handle, WM.WM_NCLBUTTONDOWN, (UIntPtr)hc, (IntPtr)0);
}));
});
return eMPResult.CutOffMessage;
}
else
{
return eMPResult.ContinueHook;
}
_GetHT函数用于计算客户点(cp)是属于哪一个区域,包括但不限于HT_CAPTION,HT_CLIENT,HT_HTLEFT.
此函数主要通过给定的边框宽度border和给定的标题栏高度caption来确定NC(Not Client)区域的范围。(这里说一句题外话,如果你的border和caption设置的过大, 导致你的控件位于NC区域的部分将无法响应鼠标信息-_-#)接着说下去,点击区域位于我们认为的NC区域,我们截断此消息(Cut off message),否则放过此消息。截获消息的同时,异步发送WM_NCLBUTTONDOWN消息给窗体的消息处理函数,要求窗体处理NC消息
- 这样初步完成了Broderless的效果。为了进一步完善鼠标显示的效果,可以截获WM.WM_MOUSEMOVE消息,随后显示对应的光标
说一下实际使用中,强烈不推荐使用Caption这个属性,建议这个属性设置为0,然后自己实现一个Caption的控件,捕获MouseDown,然后自己发送NC消息。因为如果你使用Caption的话,你都没法在Caption上弄个关闭按钮,所以我其实是这么搞得:
public partial class FormBorderless2 : BorderlessForm
{
public FormBorderless2()
{
InitializeComponent();
Border = 0;
panelCaption.MouseDown += panelCaption_MouseDown;
btnClose.Click += btnClose_Click;
}
void btnClose_Click(object sender, EventArgs e)
{
this.Close();
}
void panelCaption_MouseDown(object sender, MouseEventArgs e)
{
if(e.Button == MouseButtons.Left)
{
NativeMethods.ReleaseCapture();
NativeMethods.SendMessage(this.Handle, WM.WM_NCLBUTTONDOWN, (UIntPtr)HT.HTCAPTION, (IntPtr)0);
}
}
}
我另外搞了一个panelCaption,来实现各种按钮以及Caption的效果。
最后贴一下MousePreivew的实现
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using NM = norlib.Native.NativeMethods;
using norlib.Native;
using norlib.SystemExtension;
namespace norlib.Controls
{
/// <summary>
/// 精简的 NM.HOOKMOUSEPROC
/// 1.去掉了HC nCode
/// 因为只响应HC.HC_ACTION
/// 2.将UIntPtr wParam直接转化为WM
/// 方便用户使用
/// 3.返回值的作用不同于NM.HOOKMOUSEPROC
/// </summary>
/// <param name="arg_wm"></param>
/// <param name="stHookStruct"></param>
/// <returns></returns>
public delegate eMPResult MPMOUSEPROC(WM wm, ref MOUSEHOOKSTRUCT stHookStruct);
public class MousePreview
{
/// <summary>
/// 把相关的鼠标消息发送给此(控件/窗体)
/// </summary>
/// <param name="arg_parent">
/// </param>
public MousePreview(Control arg_parent)
{
if (null == arg_parent)
throw new NotSupportedException(string.Format("{0}的构造函数不接受参数arg_parent为Null", typeof(MousePreview).Name));
_hookProc = new NM.HOOKMOUSEPROC(_MyMouseProc);
_hookHandler = NM.SetWindowsHookEx(WH.WH_MOUSE, _hookProc, 0, NM.GetCurrentThreadId());
_parent = arg_parent;
}
~MousePreview()
{
ReleasePreview();
}
/// <summary>
/// 将截获的消息改造,计算为适合此(控件/窗体)的正确格式后,
/// 发送给此(控件/窗体)
/// </summary>
/// <param name="msg"></param>
/// <param name="arg_mpc">
/// 截获子(控件/窗体)或者仅截获顶层窗体的消息
/// </param>
/// <returns></returns>
public bool AddMouseMessage(WM msg, eMPCategory arg_mpc = eMPCategory.Sub)
{
try
{
if (null == _parent)
return false;
_dtCategory.Add(msg, arg_mpc);
_dtWM.Add(msg, _DefaultMPMouseProc);
return true;
}
catch (System.Exception ex)
{
return false;
}
}
public bool AddMouseMessage(WM msg, MPMOUSEPROC mouseProc, eMPCategory arg_mpc = eMPCategory.Sub)
{
try
{
_dtCategory.Add(msg, arg_mpc);
_dtWM.Add(msg, mouseProc);
return true;
}
catch (System.Exception ex)
{
return false;
}
}
public void ReleasePreview()
{
if(_hookHandler != IntPtr.Zero)
{
NM.UnhookWindowsHookEx(_hookHandler);
_hookHandler = IntPtr.Zero;
}
}
/// <summary>
///
/// </summary>
/// <param name="nCode"></param>
/// <param name="wParam">
/// 将要被传递的消息Id
/// </param>
/// <param name="stHookStruct"></param>
/// <returns></returns>
IntPtr _MyMouseProc(HC nCode, UIntPtr wParam, ref MOUSEHOOKSTRUCT stHookStruct)
{
if(nCode != HC.HC_ACTION)
return NM.CallNextHookEx(_hookHandler, nCode, wParam, ref stHookStruct);
var msg = (WM)wParam;
var fn = (MPMOUSEPROC)null;
if(!_dtWM.TryGetValue(msg, out fn))
return NM.CallNextHookEx(_hookHandler, nCode, wParam, ref stHookStruct);
var e = _dtCategory[msg];
if(_parent != null && !_Belong(_parent, ref stHookStruct, e))
return NM.CallNextHookEx(_hookHandler, nCode, wParam, ref stHookStruct);
var r = fn(msg, ref stHookStruct);
if (eMPResult.ContinueHook == r)
return NM.CallNextHookEx(_hookHandler, nCode, wParam, ref stHookStruct);
else if (eMPResult.CutOffMessage == r)
{
return (IntPtr)(int)-1;
}
else if (eMPResult.CutOffNextHook == r)
{
return (IntPtr)0;
}
else
{
throw new NotImplementedException();
}
}
bool _Belong(Control arg_control, ref MOUSEHOOKSTRUCT stHookStruct, eMPCategory arg_e)
{
var b = false;
if(arg_e.HasFlag(eMPCategory.Sub))
{
b = b | ((IntPtr)stHookStruct.hWnd).Belong(arg_control, arg_e.HasFlag(eMPCategory.Myself)/*false*/);
}
if (arg_e.HasFlag(eMPCategory.InRangeNoFocus))
{
var form = arg_control.GetRootForm();
if (!arg_control.IsDisposed &&
stHookStruct.hWnd == arg_control.Handle &&
(form.GetFocusedControl()==null) &&
stHookStruct.pt.ToPoint().IsIn(arg_control, true))
{
b = b | true;
}
else
{
b = b | false;
}
}
return b;
}
eMPResult _DefaultMPMouseProc(WM msg, ref MOUSEHOOKSTRUCT stHookStruct)
{
var wParam = (UIntPtr)_GetKeyStates();
var st = stHookStruct;
_parent.BeginInvoke(new Action(() =>
{
var p = _parent.PointToClient(new Point(st.pt.x, st.pt.y));
var lParam = (IntPtr)(p.X + (p.Y << 16));
Native.NativeMethods.SendMessage(_parent.Handle, msg, wParam, lParam);
}));
return eMPResult.ContinueHook;
}
int _GetKeyStates()
{
int retval = 0;
if (NM.HIWORD(NM.GetKeyState(VK.VK_LBUTTON))> 0)
retval += 1;
if (NM.HIWORD(NM.GetKeyState(VK.VK_RBUTTON)) > 0)
retval += 2;
if (NM.HIWORD(NM.GetKeyState(VK.VK_SHIFT)) > 0)
retval += 4;
if (NM.HIWORD(NM.GetKeyState(VK.VK_CONTROL)) > 0)
retval += 8;
if (NM.HIWORD(NM.GetKeyState(VK.VK_MBUTTON)) > 0)
retval += 16;
if (NM.HIWORD(NM.GetKeyState(VK.VK_XBUTTON1)) > 0)
retval += 32;
if (NM.HIWORD(NM.GetKeyState(VK.VK_XBUTTON2)) > 0)
retval += 64;
return retval;
}
/// <summary>
/// 把此(控件/窗体)的子控件消息传递给此父窗体
/// </summary>
Control _parent;
NM.HOOKMOUSEPROC _hookProc;
IntPtr _hookHandler;
readonly Dictionary<WM, MPMOUSEPROC> _dtWM = new Dictionary<WM, MPMOUSEPROC>();
readonly Dictionary<WM, eMPCategory> _dtCategory = new Dictionary<WM, eMPCategory>();
}
public enum eMPCategory
:int
{
/// <summary>
/// 所有子(控件/窗体)的消息发送给目标(控件/窗体)
/// </summary>
Sub = 1,
/// <summary>
/// 消息来源是没有Focus的Form下的目标(控件/窗体)的子控件
/// 例如一个没有焦点的Form被客户点击了此Form中的子控件
/// 一般用于捕获窗体的自定义Caption区域( 没有焦点的Form的子控件第一次被单击时,子控件没有OnMouseDown消息)
/// InRange表示鼠标点击在目标(控件/窗体)
/// NoFocus表示目标(控件/窗体)所在的Form没有焦点
/// </summary>
InRangeNoFocus = 2,
/// <summary>
/// Hook得到的消息是目标(控件/窗体)本身发送的数据是否也做进一步处理
/// </summary>
Myself = 4,
All = Sub|InRangeNoFocus|Myself,
}
public enum eMPResult
: int
{
/// <summary>
/// 要求MousePreview不调用CallNextHookEx,直接返回-1,
/// 告诉Windows不要将消息传递到stHookStruct.hWnd去
/// </summary>
CutOffMessage =-1,
/// <summary>
/// 0:要求MousePreview不调用CallNextHookEx, 直接返回0,
/// Windows要将消息传递到stHookStruct.hWnd
/// </summary>
CutOffNextHook = 0,
/// <summary>
/// 要求MousePreview调用CallNextHookEx
/// </summary>
ContinueHook = 1,
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能