Winform控件优化之无边框窗体及其拖动、调整大小和实现最大最小化关闭功能的自定义标题栏效果

2,719 阅读13分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

Winform中实现无边框窗体只需要设置一个属性FormBorderStyle = FormBorderStyle.None;即可,或者在设计器中直接设置。

直接设置无边框窗体效果,会使Winform界面显得更大气、美观,且可以自定义标题栏,UI效果相对于原始界面好看很多,具体效果看最后的截图。

但是无边框窗体没有鼠标按住标题栏不放拖动窗体、双击标题栏最大化或还原、鼠标拖动右下角或右/下边框调整大小,以及最大化、最小化、关闭等功能。使得窗体的功能和使用不完整,因此下面介绍几种对这些功能的实现。

现在的Winform窗体应用,不仅仅在右边、下边或右下角可以实现拖动调整窗体的大小,所有的四个边、四个角都可以用以调整窗体大小。

新建项目CustomForm,将主窗体重命名为NoBorderForm

(拖动边框或四角)调整窗体大小

无边框窗体还有一个问题就是,无法通过右下边框或右下角实现拖动调整窗体大小。

判断鼠标位于边框位置并通过按下拖动调整窗体大小

在窗体的MouseMove事件中,判断鼠标的位置是否位于右侧边缘或底部边缘5像素内(可以修改大小,它表示窗体的边框),并根据位置设置光标的显示样式为双向箭头。

  • Cursors.SizeNWSE; // 双向对角线光标
  • Cursors.SizeWE; // 双向水平光标
  • Cursors.SizeNS; // 双向垂直光标
  • Cursors.Arrow; // 标准箭头鼠标

窗体大小的计算,首先获取鼠标的屏幕位置,水平方向上减去窗体Left即为窗体宽度;垂直方向上减去窗体Top即为窗体高度。

特别注意:代码中必须先进行调整大小,再处理鼠标状态。否则快速拖动(尤其向窗体内)可能导致鼠标位置变化(鼠标变更状态),但此时应该是调整窗体大小的过程中(鼠标状态变更导致大小调整终止)。

#region 拖拽调整窗体大小 
private void Main_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)//左键按下移动,拖拽调整大小
    {
        // MousePosition的参考点是屏幕的左上角,表示鼠标当前相对于屏幕左上角的坐标。this.Left和this.Top的参考点也是屏幕
        if (Cursor == Cursors.SizeNWSE) // 倾斜拖拽 
        {
            // 改变窗体宽和高的代码,其宽高为鼠标屏幕位置减去窗体的Left,Top距离
            this.Width = MousePosition.X - this.Left;
            this.Height = MousePosition.Y - this.Top;
        }
        else if (Cursor == Cursors.SizeWE) // 水平拖拽
        {
            Width = MousePosition.X - this.Left;
        }
        else if (Cursor == Cursors.SizeNS) // 垂直拖拽
        {
            Height = MousePosition.Y - this.Top;
        }
    }

    //鼠标移动过程中,坐标时刻在改变 
    //当鼠标移动时横坐标距离窗体右边缘5像素以内且纵坐标距离下边缘也在5像素以内时,要将光标变为倾斜的箭头形状
    if (e.Location.X >= this.Width - 5 && e.Location.Y > this.Height - 5)
    {
        this.Cursor = Cursors.SizeNWSE; // 右下角 双向对角线光标
    }
    //当鼠标移动时横坐标距离窗体右边缘5像素以内时,要将光标变为双向水平箭头形状
    else if (e.Location.X >= this.Width - 5)
    {
        this.Cursor = Cursors.SizeWE; // 双向水平光标
    }
    //当鼠标移动时纵坐标距离窗体下边缘5像素以内时,要将光标变为垂直水平箭头形状
    else if (e.Location.Y >= this.Height - 5)
    {
        this.Cursor = Cursors.SizeNS; // 双向垂直光标

    }
    //否则,以外的窗体区域,鼠标星座均为单向箭头(默认)             
    else this.Cursor = Cursors.Arrow;

}

private void Main_Leave(object sender, EventArgs e)
{
    Cursor = Cursors.Arrow;// 移出窗体变为正常
}
#endregion

直接添加窗体的MouseMove\MouseLeave事件。

MouseMove += Main_MouseMove;
MouseLeave += Main_Leave; // 有控件在边缘时,处理一下更好一些

调整大小效果如下:

在四边和四角(或右下角)分别添加(panel)控件,借助其他控件实现拖动改变大小

《C#开发实战1200例》中给出的实现,就是借助四个边添加很细的Panel控件(控件上添加图片或进行其他修改),借助鼠标在控件的位置改变光标样式,并实现拖动改变大小。好处是四边和四角都可以定制修改样式,坏处是有些麻烦,处理好几个控件。但是原理和上面都是一致的。

一种取巧的无默认标题栏实现可拖动调整大小的方式【不推荐】

设置Form的ControlBoxfalseText为空,实现隐藏标题栏,同时保留了在Form窗体的右下角拖拽调整大小的功能。

有不小的缺陷是,即使无标题内容,设置ShowIcon为false,最终在顶部都会有不高的标题栏,和整体的窗体内容有些不协调,因此不推荐。

拖动(移动)窗体的实现(6种方法)

WPF中直接提供了移动窗体的方法DragMove();this.DragMove();,需要时直接调用即可。

// 直接拖动窗体
dragMoveImg.MouseMove += (sender, e) =>
{
    if (e.LeftButton== MouseButtonState.Pressed)
    {
        DragMove();
    }
};
// 或
MouseMove += (sender, e) =>
{
    if (e.LeftButton== MouseButtonState.Pressed)
    {
        DragMove();
    }
};

但并不是所有元素的MouseMove事件都能调用DragMove(),Button就没有效果。

通过Win32 API ReleaseCapture 和 SendMessage 实现(简单方便)——1

直接通过两个Windows的API函数 ReleaseCapture 和 SendMessage ,在窗体的MouseDown事件处理中执行就可以实现拖动窗体功能。

#region Win32 API函数
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();//从当前线程中的窗口释放鼠标捕获(释放被当前线程中某个窗口捕获的光标),并恢复正常的鼠标输入处理
[DllImport("user32.dll")]
public static extern bool SendMessage(IntPtr hwdn, int wMsg, int mParam, int lParam);//向指定的窗体发送Windows消息
#endregion

其中SendMessage的参数,hwdn表示发送消息的目的窗口的句柄,wMsg表示发送的消息,mParam、lParam则取决于发送的消息,表示附加的消息信息。返回值表示发送是否成功。

定义发送移动窗体消息的变量。

#region SendMessage需要的变量
public const int WM_SYSCOMMAND = 0x0112;//该变量表示将向Windows发送的消息类型
public const int SC_MOVE = 0xF010;//该变量表示发送消息的附加消息
public const int HTCAPTION = 0x0002;//该变量表示发送消息的附加消息
#endregion

MouseDown中处理释放捕获和移动窗体消息

MouseDown += NoBorderForm_MouseDown;

// ...
private void NoBorderForm_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button== MouseButtons.Left)
    {
        ReleaseCapture(); // 释放鼠标捕获
        // 向Windows发送拖动窗体的消息,下面的消息仅在左键按下时有效
        SendMessage(this.Handle, WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
    }
}

通过Win32 API SendMessage 发送另一种消息实现——2

private const int VM_NCLBUTTONDOWN_ = 0XA1; //VM_NCLBUTTONDOWN //定义鼠标左键按下
private const int HTCAPTION_ = 2; // HTCAPTION

private void WindowMove_Win32_2_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        ReleaseCapture();
        //发送消息 让系统误以为在标题栏上按下鼠标
        SendMessage(this.Handle, VM_NCLBUTTONDOWN_, HTCAPTION_, 0);
    }
}

通过Mouse事件实现移动窗体(改变窗体的Left、Top,移动窗体位置)——3

添加MouseDown、MouseMove、MouseUp事件

#region 方法1: 鼠标按下、移动和抬起事件中,Left、Top直接变化
MouseDown += WindowMove_LeftTop_MouseDown;
MouseMove += WindowMove_LeftTop_MouseMove;
MouseUp += WindowMove_LeftTop_MouseUp;
// 子控件拖动
// roundPanel1.MouseDown += WindowMove_LeftTop_MouseDown;
// roundPanel1.MouseMove += WindowMove_LeftTop_MouseMove;
// roundPanel1.MouseUp += WindowMove_LeftTop_MouseUp;
#endregion

实现方法是,在鼠标按下时记录位置,并设置可移动;鼠标移动事件中如果是可移动的,则计算Left、Top变化量,并直接改变即可。

#region 方法1:窗体移动,直接变化Left、Top
private Point originMouseLocation;
private bool isMove = false;
private void WindowMove_LeftTop_MouseUp(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        isMove = false;
    }
}
private void WindowMove_LeftTop_MouseMove(object sender, MouseEventArgs e)
{
    if (isMove)
    {
        #region 通过Left、Top计算直接+=变化即可
        Left += e.Location.X - originMouseLocation.X;
        Top += e.Location.Y - originMouseLocation.Y;
        #endregion
    }
}
private void WindowMove_LeftTop_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        originMouseLocation = e.Location;
        isMove = true;
    }
}
#endregion

问题,有一定不理解的是,为什么 (原始的鼠标按下时的Left、Top) 加上 【移动后的现在位置相较原始的鼠标按下时位置的变化量】计算结果反而不正确?

而此刻的Left、Top直接移动 【移动后的现在位置相较原始的鼠标按下时位置的变化量】反而是正确的,不是应该由原始的Left、Top移动对应的变化量吗?(因为变化量也是相较于原始的位置)

但是这种方式,在直接计算Location位置时确实是正确的(计算鼠标位置相较于按下时原始位置的变化量,然后通过原始的窗体位置修改对应的该变化量,得到最终的位置),原因不知。。。

上面是通过计算Top、Left,也可以计算Location,实现移动。

通过Mouse事件实现移动窗体(计算窗体的Location坐标)——4

与方法2同样的原理,在MouseDown、MouseMove、MouseUp事件中,计算移动前鼠标的原始位置、窗体的原始位置,移动过程中鼠标新的位置相对原始的鼠标位置的变化量,窗体的原始位置加上改变量,就是窗体的新位置,从而实现位置的移动。

#region 方法2:鼠标按下、移动和抬起事件中,计算移动后的Location
MouseDown += WindowMove_Location_MouseDown;
MouseMove += WindowMove_Location_MouseMove;
MouseUp += WindowMove_Location_MouseUp; 
#endregion

// ........

#region 方法2:窗体移动,通过计算Location位置
// 鼠标按下
private bool isMouse = false; // 鼠标是否按下
// 移动开始前的原始位置
private int originX = 0;
private int originY = 0;
// 鼠标按下位置
private int mouseX = 0;
private int mouseY = 0;
private void WindowMove_Location_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    { // 判断鼠标按键
        isMouse = true;
        // 屏幕坐标位置
        originX = this.Location.X;
        originY = this.Location.Y;
        // 鼠标按下位置
        mouseX = originX + e.X;
        mouseY = originY + e.Y;
    }
}
// 鼠标移动
private void WindowMove_Location_MouseMove(object sender, MouseEventArgs e)
{
    if (isMouse)
    {
        // 移动距离
        int moveX = (e.X + this.Location.X) - mouseX;
        int moveY = (e.Y + this.Location.Y) - mouseY;
        int targetX = originX + moveX;
        int targetY = originY + moveY;
        this.Location = new Point(targetX, targetY);
    }
}
// 鼠标释放
private void WindowMove_Location_MouseUp(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        isMouse = false;
    }
}
#endregion

通过Mouse事件实现移动窗体(从鼠标的屏幕坐标计算相对的窗体坐标)——5

同样时Mouse事件,在鼠标按下时计算窗体相对于鼠标的位置,鼠标移动时从鼠标的屏幕坐标,计算按下时相对的窗体坐标。

#region 方法3 计算鼠标相对于(窗体)左上角的位置,借助 Offset 鼠标屏幕坐标点平移(相对位置) 实现
MouseDown += FrmMain_MouseDown;
MouseMove += FrmMain_MouseMove;
MouseUp += FrmMain_MouseUp;
#endregion

// .........

#region 方法3 计算鼠标相对于(窗体)左上角的位置,借助 Offset 鼠标屏幕坐标点平移(相对位置) 实现
private Point mouseOff; //抓取窗体Form中的鼠标的坐标,需要设置一个参数
private bool leftFlag;  //标签,用来标记鼠标的左键的状态
private void FrmMain_MouseDown(object sender, MouseEventArgs e)  //鼠标左键按下后触发的MouseDown事件
{
    if (e.Button == MouseButtons.Left)   //判断鼠标左键是否被按下
    {
        mouseOff = new Point(e.X, e.Y); //通过结构,将鼠标在窗体中的坐标(e.X,e.Y)赋值给mouseOff参数
        leftFlag = true;    //标记鼠标左键的状态
    }
}
private void FrmMain_MouseMove(object sender, MouseEventArgs e)  //鼠标移动触发的MouseMove事件
{
    if (leftFlag)    //判断,鼠标左键是否被按下
    {
        Point mouseSet = Control.MousePosition; //抓取屏幕中鼠标光标所在的位置
        mouseSet.Offset(-mouseOff.X, -mouseOff.Y);  //Offset按指定量平移坐标,两个坐标相减,得到窗体左上角相对于屏幕的坐标
        Location = mouseSet;    //将上面得到的坐标赋值给窗体Form的Location属性
    }
}
private void FrmMain_MouseUp(object sender, MouseEventArgs e)    //鼠标释放按键后触发的MouseUp事件
{
    if (e.Button == MouseButtons.Left)
    {
        leftFlag = false;
    }
}
#endregion

通过Mouse事件实现移动窗体(原理同上,都是计算相对位置,方法不一样)——6

同样是在MouseDown中记下鼠标相对窗体的位置,MouseMove移动中计算鼠标位置,并计算相对位置的窗体。

 MouseDown += WindowMove_MouseDown;
MouseMove += WindowMove_MouseMove;
roundPanel1.MouseDown += WindowMove_MouseDown;
roundPanel1.MouseMove += WindowMove_MouseMove;

// .....

Point offsetPoint;
/// <summary>
/// 鼠标按下
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void WindowMove_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        offsetPoint = new Point(e.X, e.Y); // 鼠标相对于(窗体)左上角位置
    }
}
/// <summary>
/// 鼠标移动
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void WindowMove_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        this.Location = new Point(this.Location.X + e.X - offsetPoint.X, this.Location.Y + e.Y - offsetPoint.Y);
    }
}

通过Mouse事件中计算窗体位置实现拖动窗体(借助MouseButtons.Left简化)

借助MouseButtons.Left判断可以简化上面移动的处理,去掉isMove变量 和 MouseUp事件。

如下,直接使用MouseDown、MouseMove事件即可。

#region 方法1:简化方法 窗体移动,直接变化Left、Top
private Point originLocation;

private void WindowMove_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        #region 通过Left、Top计算直接+=变化即可
        Left += e.Location.X - originLocation.X;
        Top += e.Location.Y - originLocation.Y;
        #endregion
    }
}

private void WindowMove_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        originLocation = e.Location;
    }
}
#endregion

上面的几种方法,都可以借此实现简化。

通过重写WndProc方法实现拖动窗体、拖拽改变窗体大小【相对推荐】

下面直接通过重写WndProc方法,实现移动窗体、拖拽改变窗体大小两种效果。

相对来说比较推荐这种方式,不仅因为两种功能在一个方法内实现,还有拖拽四边和四角都可以改变窗体大小的效果。

唯一注意的是拖动窗体,通过事件方法的方式,可以将拖动窗体的方法添加到其他控件的事件上,实现拖动其他控件拖动窗体。

const int HTLEFT = 10;
const int HTRIGHT = 11;
const int HTTOP = 12;
const int HTTOPLEFT = 13;
const int HTTOPRIGHT = 14;
const int HTBOTTOM = 15;
const int HTBOTTOMLEFT = 0x10;
const int HTBOTTOMRIGHT = 17;

protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);

    if (m.Msg == 0x84)
    {
        // 拖拽调整窗体大小
       Point vPoint = new Point((int)m.LParam & 0xFFFF, (int)m.LParam >> 16 & 0xFFFF);

        vPoint = PointToClient(vPoint);

        if (vPoint.X <= 5)
            if (vPoint.Y <= 5)
                m.Result = (IntPtr)HTTOPLEFT;
            else if (vPoint.Y >= ClientSize.Height - 5)
                m.Result = (IntPtr)HTBOTTOMLEFT;
            else m.Result = (IntPtr)HTLEFT;
        else if (vPoint.X >= ClientSize.Width - 5)
            if (vPoint.Y <= 5)
                m.Result = (IntPtr)HTTOPRIGHT;
            else if (vPoint.Y >= ClientSize.Height - 5)
                m.Result = (IntPtr)HTBOTTOMRIGHT;
            else m.Result = (IntPtr)HTRIGHT;
        else if (vPoint.Y <= 5)
            m.Result = (IntPtr)HTTOP;
        else if (vPoint.Y >= ClientSize.Height - 5)
            m.Result = (IntPtr)HTBOTTOM;

        // 鼠标左键按下实现拖动窗口功能
        if (m.Result.ToInt32() ==1)
        {
            m.Result = new IntPtr(2);
        }
    }
}

自定义标题栏实现最大化、最小化、关闭等功能

通过添加Panel控件作为标题栏,左侧添加一个PictureBox(TitleIconPicb)作为标题栏图标,Dock Panel为Left,为空时不显示;右侧添加三个PictureBox(MinimizePicbMaximizeNormalPicbClosePicb),向右Anchor为Right或Dock为Right,中间添加Lable(TitlePanelTitle)作为标题,几乎占满中间区域,并设置Anchor。并未相关突变添加对应的最大化、最小化、关闭图标【图标是关键,要找到合适并正确显示,以完全显示的和标题高度稍小一些的大小为比较好】

添加实现拖动标题栏、窗体实现窗体的移动:

#region 拖动标题栏、窗体,移动窗体
TitlePanelTitle.MouseMove += WindowMove_MouseMove;
TitlePanelTitle.MouseDown += WindowMove_MouseDown;
TitleIconPicb.MouseMove += WindowMove_MouseMove;
TitleIconPicb.MouseDown += WindowMove_MouseDown;

MouseMove += WindowMove_MouseMove;
MouseDown += WindowMove_MouseDown;
#endregion

WindowMove_MouseMove移动窗体的事件方法上面已经介绍。

自定义标题栏最大、最小、还原、关闭按钮和图标的变化和功能实现

#region 自定义标题栏 标题、icon、最大、最小、还原、关闭按钮和图标
TitlePanelTitle.ForeColor = Color.WhiteSmoke;

MinimizePicb.MouseEnter += MinimizePicb_MouseEnter;
MinimizePicb.MouseLeave += MinimizePicb_MouseLeave;
MaximizeNormalPicb.MouseEnter += MaximizeNormalPicb_MouseEnter;
MaximizeNormalPicb.MouseLeave += MaximizeNormalPicb_MouseLeave;
ClosePicb.MouseEnter += ClosePicb_MouseEnter;
ClosePicb.MouseLeave += ClosePicb_MouseLeave;

MinimizePicb.Click += MinimizePicb_Click;
MaximizeNormalPicb.Click += MaximizeNormalPicb_Click;
ClosePicb.Click += ClosePicb_Click;

// 处理无Icon时Title标题的位置和最大化范围
Load += CustomTitleBar_Load;
#endregion

对应的事件方法中需要处理窗体的状态。

  • WindowState = FormWindowState.Maximized设置窗体最大化,默认的最大化使用中类似全屏的效果,会覆盖Windows底部的任务栏,可以通过在Load中设置MaximizedBounds不覆盖任务栏
  • WindowState = FormWindowState.Normal;设置窗体正常
  • 鼠标移入是改变对应按钮的图片和背景颜色
#region 自定义标题栏操作 标题、icon、最大、最小、还原、关闭按钮和图标 
private void ClosePicb_Click(object sender, EventArgs e)
{
    Close();
}

private void MaximizeNormalPicb_Click(object sender, EventArgs e)
{
    if (WindowState == FormWindowState.Normal)
    {
        WindowState = FormWindowState.Maximized;
        MaximizeNormalPicb.Image = Properties.Resources.Normal_16_16_Gray;
    }
    else
    {
        WindowState = FormWindowState.Normal;
        MaximizeNormalPicb.Image = Properties.Resources.Maximize_16_16_Gray;
    }
}
private void MinimizePicb_Click(object sender, EventArgs e)
{
    WindowState = FormWindowState.Minimized;
}
private void ClosePicb_MouseLeave(object sender, EventArgs e)
{
    ClosePicb.Image = Properties.Resources.Close_16_16_White;
    ClosePicb.BackColor = Color.Transparent;
}
private void ClosePicb_MouseEnter(object sender, EventArgs e)
{
    ClosePicb.Image = Properties.Resources.Close_16_16_White;
    ClosePicb.BackColor = Color.Crimson;
}
private void MaximizeNormalPicb_MouseLeave(object sender, EventArgs e)
{
    if (WindowState == FormWindowState.Normal)
    {
        MaximizeNormalPicb.Image = Properties.Resources.Maximize_16_16_White;
    }
    else
    {
        MaximizeNormalPicb.Image = Properties.Resources.Normal_16_16_White;
    }
    MaximizeNormalPicb.BackColor = Color.Transparent;
}
private void MaximizeNormalPicb_MouseEnter(object sender, EventArgs e)
{
    if (WindowState == FormWindowState.Normal)
    {
        MaximizeNormalPicb.Image = Properties.Resources.Maximize_16_16_Black;
    }
    else
    {
        MaximizeNormalPicb.Image = Properties.Resources.Normal_16_16_Black;
    }
    MaximizeNormalPicb.BackColor = SystemColors.GradientActiveCaption;
}
private void MinimizePicb_MouseLeave(object sender, EventArgs e)
{
    MinimizePicb.Image = Properties.Resources.Minimize_16_16_White;
    MinimizePicb.BackColor = Color.Transparent;
}
private void MinimizePicb_MouseEnter(object sender, EventArgs e)
{
    MinimizePicb.Image = Properties.Resources.Minimize_16_16_Black;
    MinimizePicb.BackColor = SystemColors.GradientActiveCaption;
}
#endregion
private void CustomTitleBar_Load(object sender, EventArgs e)
{
    if (TitleIconPicb.Image==null)
    {
        TitlePanelTitle.Left -= TitleIconPicb.Width - 3;
    }
    MaximizedBounds = Screen.GetWorkingArea(this); // 设置最大化时显示为窗体所在工作区(不包含任务栏)  Screen.PrimaryScreen.WorkingArea
}

基本效果如下:

设置最大化时窗体显示为所在工作区大小(而不是覆盖任务栏的全屏)

通过设置MaximizedBounds的大小,可以设置窗体最大化时的范围。默认最大化为全屏效果,覆盖住任务栏。

可通过Screen.PrimaryScreen.WorkingAreaScreen.GetWorkingArea(this)获取Windows系统的工作区的大小,它不包含任务栏,从而实现普通窗口的最大化

MaximizedBounds = Screen.GetWorkingArea(this); // 设置最大化时显示为窗体所在工作区(不包含任务栏)  Screen.PrimaryScreen.WorkingArea

标题栏双击窗体最大化或正常

#region 标题栏双击窗体最大化或正常
TitlePanelTitle.DoubleClick += MaximizeNormalPicb_Click;
#endregion

自定义标题栏最后整体效果

参考

相关参考出处由于未及时记录,暂时无法列出,感谢各个技术大佬的无私分享。