使用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个按钮之间的位置差。
在源代码中我提供了这样一个枚举:

 

public enum MsgBoxButtons
{
    
/// <summary>
    
/// OK按钮
    
/// </summary>

    OK = 0,
    
/// <summary>
    
/// OK按钮以及Cancel按钮
    
/// </summary>

    OKCancel,
    
/// <summary>
    
/// OK按钮, No按钮, 以及Cancel按钮
    
/// </summary>

    OKNoCancel,
}

 

因为我发现大多数情况下,这样的按钮组合足够应付需要了。

我发现我无法获取系统默认的按钮的大小,于是我作了个假设:

 

const int DefBtnWidth = 75;  // TODO -- this may not be right
const int GapTwoBtns = 150;
const int GapTriBtns = 100;


以上同时包含了按钮之间的间距值。

自定义错误提示图标
目的是一眼看出来发生了什么:错误?提示性信息?询问选择?
为此我拖了一个imageList组件,嵌入了一组图标,并定义了如下枚举:

 

 

public enum MsgBoxIcons
{
    
// 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的顺序是一一对应的,如果你要添加你自己的图标,你就应该保持这种对应关系。

 

弹出窗体的时候发出声音

这个功能完全是模仿来的,看代码吧

 

 

internal static void DoBeep(MsgBoxIcons icon)
{
    
// 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()来委托给AUI线程来执行。

 

没错,这就是标准的解决方案,大家一直都是这样做的,.NETMSDN就告诉并建议我们这样做。可是我们需要的消息框是一个独立的组件,你难道要求每个使用该组件的程序都去提供一个委托来在非UI线程中显示这样一个消息框吗?很麻烦不是吗?我就希望在任何线程中,我给你一个Handle,你给我挡在这个Handle的窗体前面显示就行了。于是这个问题成为这个小项目的难点。

 

你的亮点在哪里?

 

这是本文最重要的一个部分。我花了很长时间领悟到,我希望你只需要不到1分钟就能理解并记住。

上面说了,.NET的这种跨线程调用机制要求我们必须使用委托来实现:如果需要在A窗体上显示一个模式窗体b,那么请将该任务委托给AUI线程来执行!

委托对象放在调用者方会带来额外的编码,于是我把它放进了我自己的组件里,也就是这个自定义的消息框中!

 

// 跨线程使用之必须
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抛出了这样一个异常:已经显示的模式窗体不能够再次被显示(大意如此)。经过自己观察,我发现系统自带的消息框类似于每次需要时创建,然后丢弃之,也就是说每个使用的地方都是一个新的消息框。于是就演化成了现在的这样一个版本。

 

谢谢各位大虾和菜鸟的阅读,祝您使用愉快!

 

(全文完,页面上会提供源码包下载)

posted @ 2013-06-07 17:02  郑文亮  阅读(2593)  评论(5编辑  收藏  举报