使用winform自己做一个消息框, 以取代系统提供的MessageBox
摘要
1.是什么导致我们需要提供一个自定义的消息框?
2.说说我的大致思路
3.你的亮点在哪里?
4.难道就是这些吗?
下载本文讲述的项目源码包
是什么导致我们需要提供一个自定义的消息框?
最初产生这样一个需求是源于项目经理的近乎白痴般的要求,有一天,他告诉我说那个弹出来的消息框太小了。我告诉他,这是系统自带的东西,大小随着给定文本内容的长度自动变化,他说不行,给弄大点,而且字体也太小,换个字体吧!我的天,难道架构师都是从来不真实写代码的吗?
接 到这个要求,想想也许不大难吧。先看看系统给了我什么接口:MessageBox位于System.Windows.Forms命名空间内,是一个被密封了的静态类,能够使用的公共方法也就是MessageBox.Show,倒是有很多重载版本,也有一个选项参数,但该参数却没提供我真正感兴趣的东西。
碰 到问题我还是老规矩,首先请教Google,然后直接上codeproject或sourceforge查找,一般的问题都可以解决。一看,原来大家都曾 有过这样的需求啊;再来看别人的解决方法。大部分人使用Hook,也就是系统消息拦截,获取消息框的句柄,然后使用winApi篡改该窗体的某些特征;这样的示例非常多,而且有着非常有特色的特征:可以在上面添加个comboBox,告诉它Don't show this in the next time;可以赋予按钮自己想要的文本值;也能修改一些文本的字体和字号属性;
看起来这就是我要的解决方案,因为它几乎能做一切;作为 windows上的新手,我于是迫切需要去弄弄Hook,这东西也不难;可问题在于,我发现无论怎么去尝试修改那些作者使用Hook做的示例,最后的结果仍然是:该消息框上所有使用文本的地方最终都是同一个变化!也就是说你如果把Message Text部分的字体弄成Vadana,按钮上的文本的字体也会变为Vadana,这居然是一体的!
很好,我正不想弄Hook,这东西我就没弄成功过,别人例子都行,我就是不行!
也罢,逼急了,我自己来搞一个。
说说我的大致思路
.NET下画个窗体不难,事实上拖下鼠标就行了;问题在于一样东西总是牵扯到其它的很多东西。
ok,看看我自己的MessageBox究竟需要什么,其实需要的就是尽可能模仿系统自带的消息框,最好就是完全取代之。
必须能够自定义按钮的数目,给出不同的提示图标(错误,询问,信息等),最好还能发出相应的系统声音,消息框消失后获取按钮的某种枚举值,必须能够为其指定一个父窗体(作为此上的模式窗体),必须能够安全的在非UI线程下使用。大概就是这些吧。
自定义按钮的数目
按钮的数目会影响它们在窗体上的位置布局,于是需要知道单个按钮的宽度,相邻2个按钮之间的位置差。
在源代码中我提供了这样一个枚举:
{
/// <summary>
/// OK按钮
/// </summary>
OK = 0,
/// <summary>
/// OK按钮以及Cancel按钮
/// </summary>
OKCancel,
/// <summary>
/// OK按钮, No按钮, 以及Cancel按钮
/// </summary>
OKNoCancel,
}
因为我发现大多数情况下,这样的按钮组合足够应付需要了。
我发现我无法获取系统默认的按钮的大小,于是我作了个假设:
const int GapTwoBtns = 150;
const int GapTriBtns = 100;
以上同时包含了按钮之间的间距值。
自定义错误提示图标
目的是一眼看出来发生了什么:错误?提示性信息?询问选择?
为此我拖了一个imageList组件,嵌入了一组图标,并定义了如下枚举:
{
// NOTE: notice the order, corresponds to their images' order
/// <summary>
/// 成功标志
/// </summary>
Good = 0,
/// <summary>
/// 错误标志
/// </summary>
Error,
/// <summary>
/// 提示性标志
/// </summary>
Info,
/// <summary>
/// 询问标志
/// </summary>
Question,
/// <summary>
/// 不要出现任何标志
/// </summary>
None,
}
需要注意的是:这些枚举值与我的
imageList中的image的顺序是一一对应的,如果你要添加你自己的图标,你就应该保持这种对应关系。
弹出窗体的时候发出声音
这个功能完全是模仿来的,看代码吧
{
// Play the associated SystemSound
switch (icon)
{
case MsgBoxIcons.Error:
SystemSounds.Hand.Play();
break;
case MsgBoxIcons.Good:
case MsgBoxIcons.Info:
SystemSounds.Asterisk.Play();
break;
case MsgBoxIcons.Question:
SystemSounds.Question.Play();
break;
default:
SystemSounds.Beep.Play();
break;
}
}
获取窗体关闭时的DialogResult值
这个值很重要,它将反馈给调用者,以辅助其作出某种相应的决策。在这里我直接使用了系统框架类中的DialogResult枚举,在窗体中写死了每个按钮的DialogResult值。
为之提供父窗体参数
这个本身很简单,任何窗体的ShowDialog都有一个owner参数,我也提供就行了。可是你将会发现,如果你身处该父窗体所在的UI线程中,这样调用没有任何的不适,你甚至不需要为之提供owner参数,缺省的是将当前窗体作为父窗体。一切都运行良好;但是在非UI线程中,你就不能这么做了。看下文。
非UI线程中使用它
假定我们已经提供了ShowDialog方法,事实上我提供了该方法的很多重载版本,其易用性比系统自带的还好。在非UI线程中使用它时,你有2个选择:为之提供owner参数或者不提供;如果不提供,它就无法和你的UI主窗体联系起来,它不会挡在任何窗体的前面,成为其之上的模式窗体,这让人感觉很不爽,我发现尤其要解决这一点;那就提供这个参数就可以了嘛,把主窗体的Handle给它不就完了嘛,哈,这样一来,你的程序只能偶尔的工作。大家应该都知道,这就是跨线程访问控件的问题,原来问题在于,如果需要在A窗体上显示一个模式窗体b,这个任务必须委托给A窗体UI线程执行才行!请注意这句话,我花了很长时间才明确这一点!
ok,你明白了,你需要一个委托来做这件事情,很好,你在A窗体的非UI线程的代码中定义一个委托对象,使用某个方法(b窗体的ShowDialog方法)将其实例化,然后通过FormA.Invoke()来委托给A的UI线程来执行。
没错,这就是标准的解决方案,大家一直都是这样做的,.NET的MSDN就告诉并建议我们这样做。可是我们需要的消息框是一个独立的组件,你难道要求每个使用该组件的程序都去提供一个委托来在非UI线程中显示这样一个消息框吗?很麻烦不是吗?我就希望在任何线程中,我给你一个Handle,你给我挡在这个Handle的窗体前面显示就行了。于是这个问题成为这个小项目的难点。
这是本文最重要的一个部分。我花了很长时间领悟到,我希望你只需要不到1分钟就能理解并记住。
上面说了,.NET的这种跨线程调用机制要求我们必须使用委托来实现:如果需要在A窗体上显示一个模式窗体b,那么请将该任务委托给A的UI线程来执行!
委托对象放在调用者方会带来额外的编码,于是我把它放进了我自己的组件里,也就是这个自定义的消息框中!
private delegate DialogResult ShowItDelegate(Form owner);
private static DialogResult ShowIt(Form owner, MsgBox mbx)
{
if (owner != null
&& owner.InvokeRequired)
{
ShowItDelegate d = new ShowItDelegate(mbx.ShowDialog);
return (DialogResult)owner.Invoke(d, owner);
}
return mbx.ShowDialog(owner);
}
这样看起来,你的消息框确实比系统的要强,可以在非UI线程下方便的使用;但也就是一个很基本的消息框,那些Hook示例所展示的似乎比你的还要吸引人。
没错,以上Hook示例中展示了很多很有用的技术:添加comboBox,让窗体在一段时间没有被点击后自动消失(同时返回某个默认的DialogResult值),可以替换按钮上的文本字体。
可是这一切对于一个完全由你自己开发的窗体程序而言,是个难题么?太简单了,当然我把一些东西做死了,其实如果你想获得高度自定义的功能,你为自己的Show方法提供一个MsgBoxOptions参数就可以了,源代码在你自己手里,没有什么是不能做的!
当然这取决于你的需要了,就我而言,这已经非常足够了,我一直在用我自己编写的消息框,太方便了!赞一个自己先 :-)
后记
最初我使用的是Singleton模式,可是在很多地方可能先后都要求弹出一个窗体,而先前的这个窗体尚未消失,这就带来了一个问题,.NET抛出了这样一个异常:已经显示的模式窗体不能够再次被显示(大意如此)。经过自己观察,我发现系统自带的消息框类似于每次需要时创建,然后丢弃之,也就是说每个使用的地方都是一个新的消息框。于是就演化成了现在的这样一个版本。
谢谢各位大虾和菜鸟的阅读,祝您使用愉快!
(全文完,页面上会提供源码包下载)