适用于:.net2.0+ Winform项目
背景:
有时候我们需要开一个简单的窗口来做一些事,例如输入一些东西、点选一个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带来的多余的事,有什么比在一个方法中弹出控件、等待返回、继续处理来的爽滑的呢,像这样不是很自然吗:
所以很遗憾,不得不挥别这个优秀的方案,造自己的轮子。不过受该方案的启发,我想到用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,就可以实现自己的浮动应用。
使用说明:确保FloatLayerBase类在项目中~废话。源码在此:
using System; using System.ComponentModel; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; namespace AhDung.WinForm.Controls { /// <summary> /// 浮动层基类 /// </summary> 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; /// <summary> /// 获取或设置边框类型 /// </summary> [Description("获取或设置边框类型。")] [DefaultValue(BorderStyle.Fixed3D)] public BorderStyle BorderType { get { return _borderType; } set { if (_borderType == value) { return; } _borderType = value; Invalidate(); } } /// <summary> /// 获取或设置三维边框样式 /// </summary> [Description("获取或设置三维边框样式。")] [DefaultValue(Border3DStyle.RaisedInner)] public Border3DStyle Border3DStyle { get { return _border3DStyle; } set { if (_border3DStyle == value) { return; } _border3DStyle = value; Invalidate(); } } /// <summary> /// 获取或设置线型边框样式 /// </summary> [Description("获取或设置线型边框样式。")] [DefaultValue(ButtonBorderStyle.Solid)] public ButtonBorderStyle BorderSingleStyle { get { return _borderSingleStyle; } set { if (_borderSingleStyle == value) { return; } _borderSingleStyle = value; 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 立