• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

【C#】分享一个弹出容器层,像右键菜单那样召即来挥则去

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

适用于:.net2.0+ Winform项目

------------------201508261813更新(源码有更新、Demo未更新)------------------

  • 重新绘制调整大小手柄(SizeGrip,右下角那个),因为系统自绘的太靠边角,在XP下会盖过那部分边框,视觉体验不好。改进如图:

    新增的DrawSizeGrip方法就是绘制方法,是protected virtual的,所以如果你看不上我画的这个,可以在子类重写该方法画你自己满意的(题外,画这个我还参考了VS2010的效果,不过是相反的,VS的是凸起效果,我这是塌陷style)
  • 支持四边+四角全方位拖动改变浮动层尺寸,改善体验。如图:

注:浮动层是否可以调整大小是根据SizeGripStyle属性决定,分3种情况:

  • SizeGripStyle为Show,则始终允许用户调整大小,手柄会出现、鼠标移至边缘边角会产生视觉变化并可以拖动
  • SizeGripStyle为Hide,则始终禁止用户调整大小,手柄会出现、鼠标移至边缘边角会产生视觉变化,也不可以拖动改变大小
  • SizeGripStyle为Auto,则在模式化打开(Modal为true,即通过ShowDialog打开的)时与Show一致,非模式化打开(Modal为false,通过Show打开)时与Hide一致,这也是原版Form的逻辑,只不过原版Form还会根据FormBorderStyle,但本类已将该属性固化,所以请注意Auto这货,建议始终显式指定Show/Hide为妙

------------------201508251458更新------------------

  • 激活首控件之前是在OnShown中进行,经过研究,改为令TopMost=true,就能使浮动层与正常窗体有一致的激活首控件行为,同时省却了对OnShown的重写
  • 解决子控件有时没有聚焦框(焦点虚线框)的问题。如图:

注:最后的demo没更新,请重新取FloatLayerBase.cs源码就好

------------------201508240846原文(已更新)------------------

背景:

有时候我们需要开一个简单的窗口来做一些事,例如输入一些东西、点选一个item之类的,可能像这样:

完了返回原窗体并获取刚刚的输入,这样做并没有什么问题,但在几天前我突然产生了一些想法:为什么非得有板有眼的弹出一个窗体给用户呢,是不是可以在按钮附近迅速呈现一个层来做这些事呢,类似快捷菜单那样,用户高兴就在里面做一下该做的事,不高兴就在其它地方点一下它就消失,本来很轻便快捷的操作,DUANG~弹出一个窗体来会不会令用户心里咯噔一下呢,感受层面的事情往往是很微妙的,不管怎样,我既然起了这个念头,just try it。

我首先找了一下现成的方案,果然在牛逼的codeproject.com已经有牛人做了这样的事情:

http://www.codeproject.com/Articles/17502/Simple-Popup-Control

简单体验了一下,的确是了不起的创造。原理是利用ToolStripControlHost可以承载自定义控件的这一能力,让下拉式控件ToolStripDropDown将任何自定义控件像右键菜单那样弹出来(别忘了右键菜单ContextMenuStrip就是继承自ToolStripDropDown),这样就等于把菜单作为一个容器,可以弹出任何或简单或复杂的控件组合,同时又具有菜单具有的便捷性,召之即来挥之即去。当时了解到这方案的时候真挺开心,正是我想要的效果,感觉这下好了,不用瞎费劲自己造了。

但很快发现一个在我看来还挺在意的不足,就是ToolStripDropDown只有Show,没有ShowDialog,就是不能以模式化(Modal,也有叫模态的,鉴于MSDN都称模式,我也随流叫它模式)的方式弹出,这是由ToolStripDropDown的固有能力决定的,该方案既然基于ToolStripDropDown,自然也受限于此,不能模式化弹出。这样带来的问题是某些情况下的调用体验不好(体验这种事当然不是用户才有的专利,俺们码农也是人,也要讲体验的说),比如弹出的控件是让用户输入一些东西,完了用户点击某个按钮什么的返回原窗体,然后在原窗体获取用户刚刚的输入,然后接着做后面的事。由于非模式的Show不会阻塞代码,所以就不能在Show的下方想当然的获取值、使用值~这是显然的。要想获得值可能就得额外采取一些做法,例如响应弹出控件的关闭事件,或者把原窗体传入弹出控件完了在后者中做原本应该在原窗体中做的事~等等,办法当然有很多,但这都是因为只能Show带来的多余的事,有什么比在一个方法中弹出控件、等待返回、继续处理来的爽滑的呢,像这样不是很自然吗:

string s;
using (Popup p = new Popup())
{
    if (p.ShowDialog() != DialogResult.OK) { return; }

    s = p.InputText;
}
//go on
...

所以很遗憾,不得不挥别这个优秀的方案,造自己的轮子。不过受该方案的启发,我想到用ContextMenu来做容器(注意这个菜单类跟上面提到的继承自ToolStripDropDown的ContextMenuStrip大大的不同,前者是OS原生的菜单,就是在桌面、图标以及文本框中右键弹出的那种菜单,.net是通过调API的方式来操作这样的菜单,而后者则完全是.net实现,更多信息请参考MSDN,此处不展开),因为ContextMenu的Show是阻塞式的,正合我意。但一番尝试之后放弃,它的菜单项MenuItem不像ToolStripItem那样可以通过ToolStripControlHost承载自定义控件,希望是我能力有限,总之我做不到把自定义控件弄到ContextMenu上,也没见过原生菜单上出现过文本框、复选框等奇怪的东西,如果您知道怎么扩展原生菜单,还望不吝赐教,先行谢过!

我还是打回.net的主意,当中仍然是做了许多不同的尝试,Form、Panel、UserControl、ContainerControl、Control等等看起来适合做容器层的东西都试了个遍,甚至重新在ToolStripDropDown上打主意,最后选用Form,改造一番,自我感觉较理想的实现了我要的东西:一个叫做FloatLayerBase的基类,它本身继承自System.Windows.Forms.Form类,而需要作为浮动层显示的应用则继承自FloatLayerBase进行实现,例如下面这个接受用户输入数值的NumInputDemo实现:

样子和特点:

  • 不会令父窗口失去焦点(不会抢焦点的层才是好层):

    当然,男人不止一面:

    还有其它边框样式,有待用户自行体验,最后有demo提供。

  • 可以有调整尺寸的手柄

  • 可以点住客户区拖动

别的一些应用:

这些都只是demo,没那么好看和强大,重点是有了这个FloatLayerBase,就可以实现自己的浮动应用。

使用说明:

  1. 确保FloatLayerBase类在项目中~废话。源码在此:
    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;
    
    namespace AhDung.WinForm.Controls
    {
        /// <summary>
        /// 浮动层基类
        /// </summary>
        //Update:201508251451
        //- 将由OnShow中负责的首控件激活改为设TopMost=true实现,同时移除OnShow重写
        //- 解决子控件无聚焦框(焦点虚线框,FocusCues)的问题
        //Update:201508261806
        //- 重绘右下角调整大小手柄,解决系统自绘在XP下太靠边角从而覆盖边框的问题
        //- 支持边缘和边角拖动改变窗体大小
        //- 启用双缓冲
        public class FloatLayerBase : Form
        {
            /// <summary>
            /// 鼠标消息筛选器
            /// </summary>
            //由于本窗体为WS_CHILD,所以不会收到在窗体以外点击鼠标的消息
            //该消息筛选器的作用就是让本窗体获知鼠标点击情况,进而根据鼠标是否在本窗体以外的区域点击,做出相应处理
            readonly AppMouseMessageHandler _mouseMsgFilter;
    
            /// <summary>
            /// 指示本窗体是否已ShowDialog过
            /// </summary>
            //由于多次ShowDialog会使OnLoad/OnShown重入,故需设置此标记以供重入时判断
            bool _isShowDialogAgain;
    
            //边框相关字段
            BorderStyle _borderType;
            Border3DStyle _border3DStyle;
            ButtonBorderStyle _borderSingleStyle;
            Color _borderColor;
            int _borderWidth;//边框宽度,用于绘制SizeGrip时计算边角偏移
    
            /// <summary>
            /// 获取所绘制的边框尺寸(边框宽度x2)
            /// </summary>
            [Browsable(false)]
            public Size BorderSize
            {
                get { return new Size(_borderWidth, _borderWidth); }
            }
    
            /// <summary>
            /// 指示窗体是否处于可调整大小状态
            /// </summary>
            [Browsable(false)]
            public bool CanReSize
            {
                get
                {
                    return this.SizeGripStyle == System.Windows.Forms.SizeGripStyle.Show
                    || (this.SizeGripStyle == System.Windows.Forms.SizeGripStyle.Auto && Modal);
                }
            }
    
            /// <summary>
            /// 获取或设置边框类型
            /// </summary>
            [Description("获取或设置边框类型。")]
            [DefaultValue(BorderStyle.Fixed3D)]
            public BorderStyle BorderType
            {
                get { return _borderType; }
                set
                {
                    if (_borderType == value) { return; }
                    _borderType = value;
                    this.UpdateBorderWidth();
                    Invalidate();
                }
            }
    
            /// <summary>
            /// 获取或设置三维边框样式
            /// </summary>
            [Description("获取或设置三维边框样式。")]
            [DefaultValue(Border3DStyle.RaisedInner)]
            public Border3DStyle Border3DStyle
            {
                get { return _border3DStyle; }
                set
                {
                    if (_border3DStyle == value) { return; }
                    _border3DStyle = value;
                    this.UpdateBorderWidth();
                    Invalidate();
                }
            }
    
            /// <summary>
            /// 获取或设置线型边框样式
            /// </summary>
            [Description("获取或设置线型边框样式。")]
            [DefaultValue(ButtonBorderStyle.Solid)]
            public ButtonBorderStyle BorderSingleStyle
            {
                get { return _borderSingleStyle; }
                set
                {
                    if (_borderSingleStyle == value) { return; }
                    _borderSingleStyle = value;
                    this.UpdateBorderWidth();
                    Invalidate();
                }
            }
    
            /// <summary>
            /// 获取或设置边框颜色(仅当边框类型为线型时有效)
            /// </summary>
            [Description("获取或设置边框颜色(仅当边框类型为线型时有效)。")]
            [DefaultValue(typeof(Color), "DarkGray")]
            public Color BorderColor
            {
                get { return _borderColor; }
                set
                {
                    if (_borderColor == value) { return; }
                    _borderColor = value;
                    Invalidate();
                }
            }
    
            protected override sealed CreateParams CreateParams
            {
                get
                {
                    CreateParams prms = base.CreateParams;
    
                    //prms.Style = 0;
                    //prms.Style |= -2147483648;   //WS_POPUP
                    prms.Style |= 0x40000000;      //WS_CHILD  重要,只有CHILD窗体才不会抢父窗体焦点
                    prms.Style |= 0x4000000;       //WS_CLIPSIBLINGS
                    prms.Style |= 0x10000;         //WS_TABSTOP
                    prms.Style &= ~0x40000;        //WS_SIZEBOX       去除
                    prms.Style &= ~0x800000;       //WS_BORDER        去除
                    prms.Style &= ~0x400000;       //WS_DLGFRAME      去除
                    //prms.Style &= ~0x20000;      //WS_MINIMIZEBOX   去除
                    //prms.Style &= ~0x10000;      //WS_MAXIMIZEBOX   去除
    
                    prms.ExStyle = 0;
                    //prms.ExStyle |= 0x1;         //WS_EX_DLGMODALFRAME 立体边框
                    //prms.ExStyle |= 0x8;         //WS_EX_TOPMOST
                    prms.ExStyle |= 0x10000;       //WS_EX_CONTROLPARENT
                    //prms.ExStyle |= 0x80;        //WS_EX_TOOLWINDOW
                    //prms.ExStyle |= 0x100;       //WS_EX_WINDOWEDGE
                    //prms.ExStyle |= 0x8000000;   //WS_EX_NOACTIVATE
                    //prms.ExStyle |= 0x4;         //WS_EX_NOPARENTNOTIFY
    
                    return prms;
                }
            }
    
            //构造函数
            public FloatLayerBase()
            {
                //初始化消息筛选器。添加和移除在显示/隐藏时负责
                _mouseMsgFilter = new AppMouseMessageHandler(this);
    
                this.DoubleBuffered = true;
    
                //初始化基类属性
                InitBaseProperties();
    
                //初始化边框相关
                _borderType = BorderStyle.Fixed3D;
                _border3DStyle = System.Windows.Forms.Border3DStyle.RaisedInner;
                _borderSingleStyle = ButtonBorderStyle.Solid;
                _borderColor = Color.DarkGray;
                this.UpdateBorderWidth();
            }
    
            protected override void OnLoad(EventArgs e)
            {
                //防止重入
                if (_isShowDialogAgain) { return; }
    
                //为首次ShowDialog设标记
                if (Modal) { _isShowDialogAgain = true; }
    
                //需得减掉两层边框宽度,运行时尺寸才与设计时完全相符,原因不明
                //确定与ControlBox、FormBorderStyle有关,但具体联系不明
                if (!DesignMode)
                {
                    Size size = SystemInformation.FrameBorderSize;
                    this.Size -= size + size;//不可以用ClientSize,后者会根据窗口风格重新调整Size
                }
                base.OnLoad(e);
            }
    
            protected override void WndProc(ref Message m)
            {
                //当本窗体作为ShowDialog弹出时,在收到WM_SHOWWINDOW前,Owner会被Disable
                //故需在收到该消息后立即Enable它,不然Owner窗体和本窗体都将处于无响应状态
                if (m.Msg == 0x18 && m.WParam != IntPtr.Zero && m.LParam == IntPtr.Zero
                    && Modal && Owner != null && !Owner.IsDisposed)
                {
                    if (Owner.IsMdiChild)
                    {
                        //当Owner是MDI子窗体时,被Disable的是MDI主窗体
                        //并且Parent也会指向MDI主窗体,故需改回为Owner,这样弹出窗体的Location才会相对于Owner而非MDIParent
                        NativeMethods.EnableWindow(Owner.MdiParent.Handle, true);
                        NativeMethods.SetParent(this.Handle, Owner.Handle);//只能用API设置Parent,因为模式窗体是TopLevel,.Net拒绝为顶级窗体设置Parent
                    }
                    else
                    {
                        NativeMethods.EnableWindow(Owner.Handle, true);
                    }
                }
                else if (m.Msg == 0x84 && this.CanReSize)//WM_NCHITTEST。实现边缘和边角拖动改变窗体大小
                {
                    Point pt = this.PointToClient(NativeMethods.MakePoint(m.LParam));
                    Size size = this.ClientSize;
                    if (new Rectangle(0, 0, 5, 5).Contains(pt))
                    {
                        m.Result = (IntPtr)13;//HTTOPLEFT
                        return;
                    }
                    if (new Rectangle(5, 0, size.Width - 10, 3).Contains(pt))
                    {
                        m.Result = (IntPtr)12;//HTTOP
                        return;
                    }
                    if (new Rectangle(size.Width - 5, 0, 5, 5).Contains(pt))
                    {
                        m.Result = (IntPtr)14;//HTTOPRIGHT
                        return;
                    }
                    if (new Rectangle(size.Width - 3, 5, 3, size.Height - 5 - 16).Contains(pt))
                    {
                        m.Result = (IntPtr)11;//HTRIGHT
                        return;
                    }
                    if (new Rectangle(5, size.Height - 3, size.Width - 5 - 16, 3).Contains(pt))
                    {
                        m.Result = (IntPtr)15;//HTBOTTOM
                        return;
                    }
                    if (new Rectangle(0, size.Height - 5, 5, 5).Contains(pt))
                    {
                        m.Result = (IntPtr)16;//HTBOTTOMLEFT
                        return;
                    }
                    if (new Rectangle(0, 5, 3, size.Height - 10).Contains(pt))
                    {
                        m.Result = (IntPtr)10;//HTLEFT
                        return;
                    }
                }
                base.WndProc(ref m);
            }
    
            //画边框
            protected override void OnPaintBackground(PaintEventArgs e)
            {
                base.OnPaintBackground(e);
    
                if (_borderType == BorderStyle.Fixed3D)//绘制3D边框
                {
                    ControlPaint.DrawBorder3D(e.Graphics, ClientRectangle, Border3DStyle);
                }
                else if (_borderType == BorderStyle.FixedSingle)//绘制线型边框
                {
                    ControlPaint.DrawBorder(e.Graphics, ClientRectangle, BorderColor, BorderSingleStyle);
                }
            }
    
            protected override void OnPaint(PaintEventArgs e)
            {
                if (this.CanReSize)
                {
                    Size clientSize = this.ClientSize;
                    Rectangle rect = new Rectangle(clientSize.Width - 16, clientSize.Height - 16, 16, 16);
    
                    //画手柄
                    DrawSizeGrip(e.Graphics, new Rectangle(rect.Location - BorderSize - new Size(1, 1), rect.Size));
    
                    //刨掉SizeGrip区域,防止基类再画
                    e.Graphics.SetClip(rect, System.Drawing.Drawing2D.CombineMode.Exclude);
                }
                base.OnPaint(e);
                e.Graphics.ResetClip();
            }
    
            /// <summary>
            /// 绘制SizeGrip(调整大小的手柄),子类可重写
            /// </summary>
            /// <param name="g">绘制器</param>
            /// <param name="rect">建议作图区域</param>
            protected virtual void DrawSizeGrip(Graphics g, Rectangle rect)
            {
                Color backColor = this.BackColor;
                Brush color1 = new SolidBrush(ControlPaint.Dark(backColor));
                Brush color2 = new SolidBrush(ControlPaint.Dark(backColor, -0.5F));
                Brush color3 = new SolidBrush(ControlPaint.Dark(backColor, -0.1F));
                Brush color4 = new SolidBrush(ControlPaint.Light(backColor));
                Point pt = new Point(rect.X + 5, rect.Y + 5);//左上角偏移
    
                for (int i = 0; i < 4; i++)
                {
                    for (int j = 0; j < 4; j++)
                    {
                        if (j >= 3 - i)
                        {
                            g.FillRectangle(color1, new Rectangle(pt.X + j * 3, pt.Y + i * 3, 1, 1));
                            g.FillRectangle(color2, new Rectangle(pt.X + j * 3 + 1, pt.Y + i * 3, 1, 1));
                            g.FillRectangle(color3, new Rectangle(pt.X + j * 3, pt.Y + i * 3 + 1, 1, 1));
                            g.FillRectangle(color4, new Rectangle(pt.X + j * 3 + 1, pt.Y + i * 3 + 1, 1, 1));
                        }
                    }
                }
            }
    
            protected override void OnVisibleChanged(EventArgs e)
            {
                if (!DesignMode)
                {
                    if (Visible)
                    {
                        //使焦点子控件拥有聚焦框,重写ShowFocusCues较麻烦
                        NativeMethods.SendMessage(this.Handle, 0x127/*WM_CHANGEUISTATE*/, (IntPtr)0x10002/*UISF_HIDEFOCUS | UIS_CLEAR*/, IntPtr.Zero);
                        NativeMethods.SendMessage(this.Handle, 0x128/*WM_UPDATEUISTATE*/, (IntPtr)0x10002/*UISF_HIDEFOCUS | UIS_CLEAR*/, IntPtr.Zero);
    
                        //显示后添加鼠标消息筛选器以开始捕捉
                        Application.AddMessageFilter(_mouseMsgFilter);
                    }
                    else
                    {
                        //隐藏时则移除筛选器。之所以不放Dispose中是想尽早移除筛选器
                        Application.RemoveMessageFilter(_mouseMsgFilter);
                    }
                }
                base.OnVisibleChanged(e);
            }
    
            //实现窗体客户区拖动
            //在WndProc中实现这个较麻烦,所以放到这里做
            protected override void OnMouseDown(MouseEventArgs e)
            {
                //让鼠标点击客户区时达到与点击标题栏一样的效果,以此实现客户区拖动
                NativeMethods.ReleaseCapture();
                NativeMethods.SendMessage(Handle, 0xA1/*WM_NCLBUTTONDOWN*/, (IntPtr)2/*CAPTION*/, IntPtr.Zero);
    
                base.OnMouseDown(e);
            }
    
            /// <summary>
            /// 显示为模式窗体
            /// </summary>
            /// <param name="control">显示在该控件下方</param>
            public DialogResult ShowDialog(Control control)
            {
                return ShowDialog(control, 0, control.Height);
            }
    
            /// <summary>
            /// 显示为模式窗体
            /// </summary>
            /// <param name="control">触发弹出窗体的控件</param>
            /// <param name="offsetX">相对control水平偏移</param>
            /// <param name="offsetY">相对control垂直偏移</param>
            public DialogResult ShowDialog(Control control, int offsetX, int offsetY)
            {
                return ShowDialog(control, new Point(offsetX, offsetY));
            }
    
            /// <summary>
            /// 显示为模式窗体
            /// </summary>
            /// <param name="control">触发弹出窗体的控件</param>
            /// <param name="offset">相对control偏移</param>
            public DialogResult ShowDialog(Control control, Point offset)
            {
                return this.ShowDialogInternal(control, offset);
            }
    
            /// <summary>
            /// 显示为模式窗体
            /// </summary>
            /// <param name="item">显示在该工具栏项的下方</param>
            public DialogResult ShowDialog(ToolStripItem item)
            {
                return ShowDialog(item, 0, item.Height);
            }
    
            /// <summary>
            /// 显示为模式窗体
            /// </summary>
            /// <param name="item">触发弹出窗体的工具栏项</param>
            /// <param name="offsetX">相对item水平偏移</param>
            /// <param name="offsetY">相对item垂直偏移</param>
            public DialogResult ShowDialog(ToolStripItem item, int offsetX, int offsetY)
            {
                return ShowDialog(item, new Point(offsetX, offsetY));
            }
    
            /// <summary>
            /// 显示为模式窗体
            /// </summary>
            /// <param name="item">触发弹出窗体的工具栏项</param>
            /// <param name="offset">相对item偏移</param>
            public DialogResult ShowDialog(ToolStripItem item, Point offset)
            {
                return this.ShowDialogInternal(item, offset);
            }
    
            /// <summary>
            /// 显示窗体
            /// </summary>
            /// <param name="control">显示在该控件下方</param>
            public void Show(Control control)
            {
                Show(control, 0, control.Height);
            }
    
            /// <summary>
            /// 显示窗体
            /// </summary>
            /// <param name="control">触发弹出窗体的控件</param>
            /// <param name="offsetX">相对control水平偏移</param>
            /// <param name="offsetY">相对control垂直偏移</param>
            public void Show(Control control, int offsetX, int offsetY)
            {
                Show(control, new Point(offsetX, offsetY));
            }
    
            /// <summary>
            /// 显示窗体
            /// </summary>
            /// <param name="control">触发弹出窗体的控件</param>
            /// <param name="offset">相对control偏移</param>
            public void Show(Control control, Point offset)
            {
                this.ShowInternal(control, offset);
            }
    
            /// <summary>
            /// 
                           
                        
                        

    鲜花

    握手

    雷人

    路过

    鸡蛋
    该文章已有0人参与评论

    请发表评论

    全部评论

    专题导读
    上一篇:
    C#调用脚本的实现发布时间:2022-07-13
    下一篇:
    C#bindingNavigator1绑定后如何重写按钮事件发布时间:2022-07-13
    热门推荐
    热门话题
    阅读排行榜

    扫描微信二维码

    查看手机版网站

    随时了解更新最新资讯

    139-2527-9053

    在线客服(服务时间 9:00~18:00)

    在线QQ客服
    地址:深圳市南山区西丽大学城创智工业园
    电邮:jeky_zhao#qq.com
    移动电话:139-2527-9053

    Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap