C# 桌面软件开发之超精简WinForm无边框方案(可靠边自动分屏)

前言

最近“解决自媒体一键多平台发布”项目立项以来,桌面端选择了WinForm进行开发(虽然丢了很多年,但总算多少了解点)。
为了重绘标题栏、最大最小化按钮和关闭按钮,需要对WinForm的边框进行重绘。在网上搜了很多资料,都是将FormBorderStyle设置为none。虽然这也能实现无边框化,但存在一个很大的问题,就是窗体靠边后,无法实现系统自带的分屏功能。为了解决这一个问题,在搜索查阅了大量资料后,终于解决了,现将其分享给各位小伙伴。

一、开发环境

  • vs2019
  • Windows 10

二、具体代码

要实现窗口的重绘,我们需要重写系统消息的处理方法。这个时候我们就需要重写Form基类的WndProc()方法,代码如下:

protected override void WndProc(ref Message m)
{
    //重写消息处理过程
}

重写WndProc()方法,首先要判断当前是否为设计模式,防止运行过程的执行结果影响设计窗体,代码如下:

protected override void WndProc(ref Message m)
{
    if (DesignMode)
    {
        base.WndProc(ref m);
        return;
    }
}

Windows消息有很多类型,要实现无边框我们需要重写WM_NCCALCSIZEWM_WINDOWPOSCHANGEDWM_NCACTIVATE三个消息类型处理过程。

首先判断消息类型的具体代码如下:

switch (m.Msg)
{
    case (int)0x0083://WM_NCCALCSIZE:
        break;
    case (int)0x0047://WM_WINDOWPOSCHANGED:
        break;
    case (int)0x0086://WM_NCACTIVATE:
        break;
    default:
        base.WndProc(ref m);
        break;
}

其中WM_NCCALCSIZE可以在应用程序可以在窗口的大小或位置发生更改时控制窗口工作区的内容,通过此消息需要将无边框化的窗体大小去掉设计窗口里面窗体的标题和边框宽高度,以及当启动位置设置为屏幕中心时将大小变化带来的位置坐标影响进行修改,具体代码如下:

private void ModifyFormSizeAndLcation()
{
    var diffHeight = this.Height - this.ClientRectangle.Height;
    var diffWidth = this.Width - this.ClientRectangle.Width;
    if (diffHeight != 0 || diffWidth != 0)
    {
        var newWidth = this.Width - diffWidth;
        var newHeight = this.Height - diffHeight;
        if (this.StartPosition == FormStartPosition.CenterScreen)
        {
            var screenWorkingArea = Screen.GetWorkingArea(this);
            var centerX = screenWorkingArea.Width / 2 - newWidth / 2;
            var centerY = screenWorkingArea.Height / 2 - newHeight / 2;
            this.Location = new Point(centerX , centerY );
        }
        this.Size = new Size(newWidth, newHeight);
    }
}

其中WM_WINDOWPOSCHANGED消息是用以处理由于窗体边框的修改带来的窗体大小、位置等的更改,现在我们需要拦截这个消息重绘控件布局,具体代码如下:

//向默认窗口过程发送指定消息。
DefWndProc(ref m);
//用当前大小和位置更新控件的边界。
UpdateBounds();
//获取窗口的位置
 var pos = (WINDOWPOS)Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));
//设置窗口的区域
static void SetWindowRegion(Form form, IntPtr hwnd, int left, int top, int right, int bottom)
{
    var rgn = User32.CreateRectRgn(0, 0, 0, 0);
    var hrg = new HandleRef((object)form, rgn);
    User32.GetWindowRgn(hwnd, hrg.Handle);
    RECT box;
    User32.GetRgnBox(hrg.Handle, out box);
    if (box.left != left || box.top != top || box.right != right || box.bottom != bottom)
    {
        var hr = new HandleRef((object)form, User32.CreateRectRgn(left, top, right, bottom));
        User32.SetWindowRgn(hwnd, hr.Handle, User32.IsWindowVisible(hwnd));
    }
    User32.DeleteObject(rgn);
}

m.Result = new IntPtr(1);

其中WM_NCACTIVATE是在需要更改其非client 区域以指示活动或非活动状态时,发送到窗口。拦截进行重写,防止如文件选择框等无法获取焦点,具体代码如下:

private void WmNCActivate(ref Message msg)
{
    if ((User32.GetWindowLong(this.Handle, -16) & (int)0x20000000) > 0)
    {
        DefWndProc(ref msg);
    }
    else
    {
        msg.Result = new IntPtr(1);
    }
}

三、完整代码

1、FormFrameless.cs

public class FormBorderless : Form
{
    protected override void WndProc(ref Message m)
    {
        if (DesignMode)
        {
            base.WndProc(ref m);
            return;
        }
        switch (m.Msg)
        {
            case (int)0x0083://WM_NCCALCSIZE:
                ModifyFormSizeAndLcation();
                break;
            case (int)0x0047://WM_WINDOWPOSCHANGED:
                {
                    DefWndProc(ref m);
                    UpdateBounds();
                    var pos = (WINDOWPOS)Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));
                    SetWindowRegion(this, m.HWnd, 0, 0, pos.cx, pos.cy);
                    m.Result = new IntPtr(1);
                    break;
                }
            case (int)0x0086://WM_NCACTIVATE:
                {
                    WmNCActivate(ref m);
                    break;
                }
            default:
                {
                    base.WndProc(ref m);
                    break;
                }
        }
    }
    /// <summary>
    /// 将窗体的大小去掉原始标题和边框尺寸
    /// 同时当启动位置设置为屏幕中心时,对坐标进行校准
    /// </summary>
    private void ModifyFormSizeAndLcation()
    {
        var diffHeight = this.Height - this.ClientRectangle.Height;
        var diffWidth = this.Width - this.ClientRectangle.Width;
        if (diffHeight != 0 || diffWidth != 0)
        {
            var newWidth = this.Width - diffWidth;
            var newHeight = this.Height - diffHeight;
            if (this.StartPosition == FormStartPosition.CenterScreen)
            {
                var screenWorkingArea = Screen.GetWorkingArea(this);
                var centerX= screenWorkingArea.Width / 2 - newWidth / 2;
                var centerY = screenWorkingArea.Height / 2 - newHeight / 2;
                this.Location = new Point(centerX, centerY);
            }
            this.Size = new Size(newWidth, newHeight);
        }
    }
    private void WmNCActivate(ref Message msg)
    {
        if ((User32.GetWindowLong(this.Handle, -16) & (int)0x20000000) > 0)
        {
            DefWndProc(ref msg);
        }
        else
        {
            msg.Result = new IntPtr(1);
        }
    }
    static void SetWindowRegion(Form form, IntPtr hwnd, int left, int top, int right, int bottom)
    {
        var rgn = User32.CreateRectRgn(0, 0, 0, 0);
        var hrg = new HandleRef((object)form, rgn);
        User32.GetWindowRgn(hwnd, hrg.Handle);
        RECT box;
        User32.GetRgnBox(hrg.Handle, out box);
        if (box.left != left || box.top != top || box.right != right || box.bottom != bottom)
        {
            var hr = new HandleRef((object)form, User32.CreateRectRgn(left, top, right, bottom));
            User32.SetWindowRgn(hwnd, hr.Handle, User32.IsWindowVisible(hwnd));
        }
        User32.DeleteObject(rgn);
    }
}

2、调用非托管代码的存放类User32.cs

class User32
{
    [DllImport("user32.dll")]
    public static extern Int32 GetWindowLong(IntPtr hWnd, Int32 Offset);
    [DllImport("gdi32.dll")]
    public static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect
int nRightRect, int nBottomRect);
    [DllImport("user32.dll")]
    public static extern int GetWindowRgn(IntPtr hWnd, IntPtr hRgn);
    [DllImport("gdi32.dll")]
    public static extern int GetRgnBox(IntPtr hrgn, out RECT lprc);
    [DllImport("user32.dll")]
    public static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);
    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool IsWindowVisible(IntPtr hWnd);
    [DllImport("gdi32.dll")]
    public static extern bool DeleteObject(IntPtr hObj);
}

3、WINDOWPOS.cs

[StructLayout(LayoutKind.Sequential)]
internal struct WINDOWPOS
{
    internal IntPtr hwnd;
    internal IntPtr hWndInsertAfter;
    internal int x;
    internal int y;
    internal int cx;
    internal int cy;
    internal uint flags;
}

4、RECT.cs

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int left;
    public int top;
    public int right;
    public int bottom;
}

四、最后

各位小伙伴如果有更好的分屏方案,可以告诉我哦~

posted @ 2022-10-18 21:38  好先生FX  阅读(1005)  评论(1编辑  收藏  举报