分享一个可携带附加消息的增强消息框messageboxex
--------------201507160917更新---------------
无意中发现标准消息框在windows7是有声音的,只是在windows server 2008(r2)无声,而我用的刚好是后者,所以误以为是messagebeep api在所有nt6系统都不工作造成~汗,有人在stackoverflow也提过这问题。但我仍然决定使用playsound api,不做修改
将声音处理交给processicon方法负责。之前考虑松耦合,所以将messageboxicon和声音分开处理,但其实声音就是根据前者而来,两者天然就是耦合的,分开处理多此一举
--------------201507091034更新---------------
首先感谢猿友e204在回复中的反馈。
解决双击【详细信息】按钮造成的checked状态改变问题,办法是让togglebutton忽略wm_lbuttondblclk消息
修正收起详细信息区逻辑,改为直接取用plattachzone.height。之前是取expandheight,会造成视觉体验问题
--------------201507082014原文(已更新)---------------
适用于:.net 2.0+的winform项目
样子:
有损录制+制图的原因不可能原样展示出真实效果,可至文章结尾下载demo体验。
功能和特点:
相对父窗体居中 可附带附加消息。附加消息可以是string和exception类型,【详细信息】按钮会根据是否传入附加信息显示和隐藏。传入exception实例时,呈现的是exception.tostring(),也就是可能携带stacktrace信息,所以如果你只是想呈现异常文本,还是老实传入ex.message 展开/收起附加信息时有动画效果。实用为王的你亦可设置enableanimate=false关闭动画效果 在windows server 2008 r2(未测试其它服务器系统)也有声音反馈。标准消息框在个人系统(xp/win7等)是有声音的,但在srv08却没有。同时亦提供了enablesound属性允许你关闭声音反馈 移除了标准messagebox提供的iwin32window、messageboxoptions和help相关参数,原因是我用不到,懒得实现 可拖拉改变消息框尺寸,消息文本和附加文本会随窗体大小重排。这是标准消息框未提供的能力。改变尺寸分两种情况有不同的行为:①详细信息未展开时,改变的是主消息区大小;②详细信息展开时,改变的是详细信息区的大小总体来说, 此消息框比较适合用在需要反馈大量消息文本的场合, 用标准消息框的话,文本太多可能会使消息框超出屏幕大小,比如codeproject测试数据上这位老兄举的,由于标准消息框不具备改变窗体大小的能力,将导致部分消息无法让用户看到。而就算没有超出屏幕,一下子让用户面对那么多消息文字,体验也不地道。使用本消息框就可以解决此类问题,比如可以将扼要信息显示在主消息区,将大量的明细消息(例如批量处理中的单项处理情况)、次要消息、异常信息等放置在详细信息区,由用户或it支持人员自己去展开获取这些信息。同时,在没有附加消息的时候,你仍然可以像标准消息框一样使用它,所以,如果你跟我一样不会用到标准消息框的iwin32window、messageboxoptions和help相关参数的话, 基本上你可以在整个项目中全程用此消息框替换掉标准消息框, 别忘了相比标准消息框,它还具备了可缩放、相对父窗体居中等额外能力。总言之,你值得拥有。至于如果你担心性能问题,这个~我想这么说,我对自己的代码质量还是有点信心的。也希望能得大侠指出槽点,感激!
使用说明:
先看公开成员:
//静态属性
messageboxex.enableanimate
messageboxex.enablesound
//静态方法
messageboxex.show( string , string , string )
messageboxex.show( string , string , string , messageboxbuttons)
messageboxex.show( string , string , string , messageboxbuttons, messageboxicon)
messageboxex.show( string , string , string , messageboxbuttons, messageboxicon, messageboxdefaultbutton)
messageboxex.show( string , string , exception)
messageboxex.show( string , string , exception, messageboxbuttons)
messageboxex.show( string , string , exception, messageboxbuttons, messageboxicon)
messageboxex.show( string , string , exception, messageboxbuttons, messageboxicon, messageboxdefaultbutton)
属性enableanimate和enablesound上面提过,分别是用来启用/关闭动画、声音效果的,默认是都启用。俩属性影响范围是全局的,比如设置enableanimate = false后,之后弹出的messageboxex都没有动画效果,直到重新设为true,enablesound亦然。最佳实践是将它俩与用户偏好设置相关联,允许用户自主控制
方法则只有一个:show(),从重载列表你大概都能知道如何使用。其中第3个参数就是附加消息,可接受string和exception类的实例,其余参数的位置和意义与标准消息框一致。简要示例如下:
messageboxex.show( "主消息" , "标题" , "附加消息" , messageboxbuttons.ok, messageboxicon.none, messageboxdefaultbutton.button1);
messageboxex.show( "主消息" , "标题" , ex, messageboxbuttons.ok, messageboxicon.none, messageboxdefaultbutton.button1);
前3个参数可以放心为null,内部有处理,后面的枚举你也null不了,如果传入无效枚举值,会抛异常
只有3个string参数的那个方法,后面俩参数是可选的。所以不讲究消息体验的你仍然可以这样使用:
messageboxex.show( "阿斯顿发" );
messageboxex.show( "阿斯顿发" , "士大夫" );
方案源码:
代码不少,原因自然是有的,有兴趣的童鞋请看后面的实现说明。另外,千万不要认为代码量跟性能有直接关系,有时候更多的代码恰恰是为了提升性能而存在,有时候则是为了健壮性。
using system;
using system测试数据ponentmodel;
using system.drawing;
using system.io;
using system.runtime.interopservices;
using system.threading;
using system.windows.forms;
namespace ahdung.winform
{
/// <summary>
/// 可以携带详细信息的消息框
/// </summary>
public static class messageboxex
{
//异常消息文本
private const string invalidbuttonexstring = "按钮参数不是有效的枚举项!" ;
private const string invalidiconexstring = "图标参数不是有效的枚举项!" ;
private const string invaliddfbuttonexstring = "默认按钮参数不是有效的枚举项!" ;
/// <summary>
/// 是否启用动画效果
/// </summary>
public static bool enableanimate { get ; set ; }
/// <summary>
/// 是否启用声音反馈
/// </summary>
public static bool enablesound { get ; set ; }
//静态构造
static messageboxex()
{
//默认启用动画+声音
enableanimate = true ;
enablesound = true ;
}
#region 公开方法
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="attachmessage">附加消息</param>
public static dialogresult show( string message, string caption = null , string attachmessage = null )
{
return showcore(message, caption, attachmessage, messageboxbuttons.ok, messageboxicon.none, messageboxdefaultbutton.button1);
}
/*下面这仨弄成重载而不是可选方法是为了避免不必要的参数检查*/
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="attachmessage">附加消息</param>
/// <param name="buttons">按钮组合</param>
public static dialogresult show( string message, string caption, string attachmessage, messageboxbuttons buttons)
{
if (! enum .isdefined( typeof (messageboxbuttons), buttons)) { throw new invalidenumargumentexception(invalidbuttonexstring); }
return showcore(message, caption, attachmessage, buttons, messageboxicon.none, messageboxdefaultbutton.button1);
}
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="attachmessage">附加消息</param>
/// <param name="buttons">按钮组合</param>
/// <param name="icon">图标</param>
public static dialogresult show( string message, string caption, string attachmessage, messageboxbuttons buttons, messageboxicon icon)
{
if (! enum .isdefined( typeof (messageboxbuttons), buttons)) { throw new invalidenumargumentexception(invalidbuttonexstring); }
if (! enum .isdefined( typeof (messageboxicon), icon)) { throw new invalidenumargumentexception(invalidiconexstring); }
return showcore(message, caption, attachmessage, buttons, icon, messageboxdefaultbutton.button1);
}
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="attachmessage">附加消息</param>
/// <param name="buttons">按钮组合</param>
/// <param name="icon">图标</param>
/// <param name="defaultbutton">默认按钮</param>
public static dialogresult show( string message, string caption, string attachmessage, messageboxbuttons buttons, messageboxicon icon, messageboxdefaultbutton defaultbutton)
{
if (! enum .isdefined( typeof (messageboxbuttons), buttons)) { throw new invalidenumargumentexception(invalidbuttonexstring); }
if (! enum .isdefined( typeof (messageboxicon), icon)) { throw new invalidenumargumentexception(invalidiconexstring); }
if (! enum .isdefined( typeof (messageboxdefaultbutton), defaultbutton)) { throw new invalidenumargumentexception(invaliddfbuttonexstring); }
return showcore(message, caption, attachmessage, buttons, icon, defaultbutton);
}
/********传入异常的重载********/
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="exception">异常实例</param>
public static dialogresult show( string message, string caption, exception exception)
{
return show(message, caption, exception == null ? string .empty : exception.tostring());
}
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="exception">异常实例</param>
/// <param name="buttons">按钮组合</param>
public static dialogresult show( string message, string caption, exception exception, messageboxbuttons buttons)
{
return show(message, caption, exception == null ? string .empty : exception.tostring(), buttons);
}
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="exception">异常实例</param>
/// <param name="buttons">按钮组合</param>
/// <param name="icon">图标</param>
public static dialogresult show( string message, string caption, exception exception, messageboxbuttons buttons, messageboxicon icon)
{
return show(message, caption, exception == null ? string .empty : exception.tostring(), buttons, icon);
}
/// <summary>
/// 显示消息框
/// </summary>
/// <param name="message">消息文本</param>
/// <param name="caption">消息框标题</param>
/// <param name="exception">异常实例</param>
/// <param name="buttons">按钮组合</param>
/// <param name="icon">图标</param>
/// <param name="defaultbutton">默认按钮</param>
public static dialogresult show( string message, string caption, exception exception, messageboxbuttons buttons, messageboxicon icon, messageboxdefaultbutton defaultbutton)
{
return show(message, caption, exception == null ? string .empty : exception.tostring(), buttons, icon, defaultbutton);
}
#endregion
//内部方法,不检查参数有效性
private static dialogresult showcore( string message, string caption, string attachmessage, messageboxbuttons buttons, messageboxicon icon, messageboxdefaultbutton defaultbutton)
{
using (messageform f = new messageform(message, caption, buttons, icon, defaultbutton, attachmessage, enableanimate, enablesound))
{
return f.showdialog();
}
}
/*----------------
下面是消息窗体相关
---------------*/
/// <summary>
/// 消息窗体
/// </summary>
/// <remarks>参数有效性由messageboxex负责</remarks>
private class messageform : form
{
/* todo 存在问题:
* 当消息区文本非常非常多时,且反复进行改变消息框窗口大小、位置、展开收起的操作,那么在某次展开时
详细信息文本框可能会在原位置(即消息区内某rect)瞬闪一下,
原因是文本框控件在显示时总会在原位置wm_ncpaint + wm_erasebkgnd一下,暂无解决办法。
实际应用中碰到的几率很小,就算碰到,影响也可以忽略。
*/
#region 控件初始化
/// <summary>
/// 必需的设计器变量。
/// </summary>
private system测试数据ponentmodel.icontainer components = null ;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected override void dispose( bool disposing)
{
if (disposing && (components != null ))
{
components.dispose();
}
base .dispose(disposing);
}
#region windows 窗体设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void initializecomponent()
{
this .button3 = new system.windows.forms.button();
this .txbattach = new textboxunselectallable();
this .button2 = new system.windows.forms.button();
this .button1 = new system.windows.forms.button();
this .plbuttonszone = new ahdung.winform.messageboxex.messageform.panelbasic();
this .ckbtoggle = new ahdung.winform.messageboxex.messageform.togglebutton( this .useanimate);
this .plattachzone = new ahdung.winform.messageboxex.messageform.panelbasic();
this .lbmsg = new ahdung.winform.messageboxex.messageform.messageviewer();
this .plbuttonszone.suspendlayout();
this .plattachzone.suspendlayout();
this .suspendlayout();
//
// button3
//
this .button3.anchor = system.windows.forms.anchorstyles.top | system.windows.forms.anchorstyles.right;
this .button3.location = new system.drawing.point(320, 8);
this .button3.margin = new system.windows.forms.padding(3, 2, 3, 2);
this .button3.name = "button3" ;
this .button3.size = new system.drawing.size(85, 27);
this .button3.tabindex = 2;
//
// txbattach
//
this .txbattach.anchor = ((system.windows.forms.anchorstyles.top | system.windows.forms.anchorstyles.bottom)
| system.windows.forms.anchorstyles.left)
| system.windows.forms.anchorstyles.right;
this .txbattach.location = new system.drawing.point(10, 7);
this .txbattach.margin = new system.windows.forms.padding(3, 1, 3, 1);
this .txbattach.name = "txbattach" ;
this .txbattach. readonly = true ;
this .txbattach.multiline = true ;
this .txbattach.scrollbars = system.windows.forms.scrollbars.vertical;
this .txbattach.size = new system.drawing.size(395, 105);
this .txbattach.tabindex = 0;
//
// button2
//
this .button2.anchor = system.windows.forms.anchorstyles.top | system.windows.forms.anchorstyles.right;
this .button2.location = new system.drawing.point(229, 8);
this .button2.margin = new system.windows.forms.padding(3, 2, 3, 2);
this .button2.name = "button2" ;
this .button2.size = new system.drawing.size(85, 27);
this .button2.tabindex = 1;
//
// button1
//
this .button1.anchor = system.windows.forms.anchorstyles.top | system.windows.forms.anchorstyles.right;
this .button1.location = new system.drawing.point(138, 8);
this .button1.margin = new system.windows.forms.padding(3, 2, 3, 2);
this .button1.name = "button1" ;
this .button1.size = new system.drawing.size(85, 27);
this .button1.tabindex = 0;
//
// plbuttonszone
//
this .plbuttonszone.controls.add( this .ckbtoggle);
this .plbuttonszone.controls.add( this .button1);
this .plbuttonszone.controls.add( this .button2);
this .plbuttonszone.controls.add( this .button3);
this .plbuttonszone.dock = system.windows.forms.dockstyle.bottom;
this .plbuttonszone.location = new system.drawing.point(0, 96);
this .plbuttonszone.margin = new system.windows.forms.padding(3, 1, 3, 1);
this .plbuttonszone.name = "plbuttonszone" ;
this .plbuttonszone.size = new system.drawing.size(415, 36);
this .plbuttonszone.tabindex = 1;
//
// ckbtoggle
//
this .ckbtoggle.location = new system.drawing.point(10, 8);
this .ckbtoggle.name = "ckbtoggle" ;
this .ckbtoggle.size = new system.drawing.size(93, 27);
this .ckbtoggle.tabindex = 3;
this .ckbtoggle.text = "详细信息(&d)" ;
this .ckbtoggle.checkedchanged += this .ckbtoggle_checkedchanged;
//
// plattachzone
//
this .plattachzone.controls.add( this .txbattach);
this .plattachzone.dock = system.windows.forms.dockstyle.fill;
this .plattachzone.location = new system.drawing.point(0, 130);
this .plattachzone.margin = new system.windows.forms.padding(3, 2, 3, 2);
this .plattachzone.name = "plattachzone" ;
this .plattachzone.size = new system.drawing.size(415, 114);
this .plattachzone.tabindex = 2;
this .plattachzone.visible = false ;
//
// lbmsg
//
this .lbmsg.dock = system.windows.forms.dockstyle.fill;
this .lbmsg.icon = null ;
this .lbmsg.location = new system.drawing.point(0, 0);
this .lbmsg.name = "lbmsg" ;
this .lbmsg.padding = new system.windows.forms.padding(21, 18, 21, 18);
//this.lbmsg.size = new system.drawing.size(415, 96);
this .lbmsg.tabindex = 0;
//
// fmmsg
//
this .autoscalemode = system.windows.forms.autoscalemode.none;
//this.clientsize = new system.drawing.size(415, 261);
this .controls.add( this .lbmsg);
this .controls.add( this .plbuttonszone);
this .controls.add( this .plattachzone);
this .doublebuffered = true ;
this .maximizebox = false ;
this .name = "messageform" ;
this .padding = new system.windows.forms.padding(0, 0, 0, 17);
this .showicon = false ;
this .showintaskbar = false ;
this .sizegripstyle = system.windows.forms.sizegripstyle.show;
this .plbuttonszone.resumelayout( false );
this .plattachzone.resumelayout( false );
this .plattachzone.performlayout();
this .resumelayout( false );
}
#endregion
private togglebutton ckbtoggle;
private textboxunselectallable txbattach;
private messageviewer lbmsg;
private system.windows.forms.button button2;
private system.windows.forms.button button1;
private panelbasic plbuttonszone;
private panelbasic plattachzone;
private system.windows.forms.button button3;
#endregion
/// <summary>
/// 最大默认窗体客户区宽度
/// </summary>
const int maxclientwidth = 700;
string messagesound; //存储供playsound api使用的系统消息音别名,在processicon中赋值,onshown中取用
int expandheight;
/// <summary>
/// 详细信息区展开高度
/// </summary>
private int expandheight
{
get { return expandheight < 150 ? 150 : expandheight; }
set { expandheight = value; }
}
#region 属性
/// <summary>
/// 是否启用动画效果
/// </summary>
/// <remarks>此处还弄该属性是为了保证窗体类的独立性</remarks>
private bool useanimate { get ; set ; }
/// <summary>
/// 是否启用声音反馈
/// </summary>
/// <remarks>此处还弄该属性是为了保证窗体类的独立性</remarks>
private bool usesound { get ; set ; }
/// <summary>
/// 消息按钮
/// </summary>
private messageboxbuttons messagebuttons { get ; set ; }
/// <summary>
/// 消息图标
/// </summary>
private messageboxicon messageicon { get ; set ; }
/// <summary>
/// 默认按钮
/// </summary>
private messageboxdefaultbutton defaultbutton { get ; set ; }
#endregion
/// <summary>
/// 创建消息窗体
/// </summary>
private messageform( bool enableanimate)
{
this .useanimate = enableanimate; //须尽早设置,要供展开按钮初始化用
initializecomponent();
this .startposition = form.activeform == null ? formstartposition.centerscreen : formstartposition.centerparent;
this .font = systemfonts.messageboxfont;
//注册事件
this .button1.click += button_click;
this .button2.click += button_click;
this .button3.click += button_click;
this .plattachzone.resize += plattachzone_resize;
}
/// <summary>
/// 创建消息窗体
/// </summary>
public messageform( string message, string caption, messageboxbuttons buttons, messageboxicon icon, messageboxdefaultbutton defaultbutton, string attachmessage, bool enableanimate, bool enablesound)
: this (enableanimate)
{
this .lbmsg.text = message;
this .text = caption;
this .txbattach.text = attachmessage;
this .messagebuttons = buttons;
this .messageicon = icon;
this .defaultbutton = defaultbutton;
this .usesound = enablesound;
}
#region 重写基类方法
protected override void onload(eventargs e)
{
//须在计算各种尺寸前搞掂
processicon();
processbuttons();
this .minimumsize = sizefromclientsize( new size(getpanelbuttonminwidth(), getclientminheight()));
//参数意义定为客户区最大大小,所以需刨掉非客户区高度后传入
this .clientsize = this .getpreferredsize( new size(maxclientwidth, screen.primaryscreen.workingarea.height - ( this .height - this .clientsize.height)));
base .onload(e);
}
protected override void onshown(eventargs e)
{
//设置默认按钮焦点。须在onshown中设置按钮焦点才有用
button dfbtn;
if ((dfbtn = this .acceptbutton as button) != null )
{
dfbtn.focus();
}
//播放消息提示音
if ( this .usesound) { playsystemsound( this .messagesound); }
base .onshown(e);
}
//重写窗体参数
protected override createparams createparams
{
get
{
createparams prms = base .createparams;
if ((convert.toint32( this .messagebuttons) & 1) == 0) //没有cancel按钮时屏蔽关闭按钮,刚好在偶数项
{
prms.classstyle |= 0x200;
}
return prms;
}
}
/// <summary>
/// 计算合适的窗口尺寸
/// </summary>
/// <param name="proposedsize">该参数此处定义为客户区可设置的最大尺寸</param>
public override size getpreferredsize(size proposedsize)
{
int reservedheight = plbuttonszone.height + padding.bottom;
size size = lbmsg.getpreferredsize( new size(proposedsize.width, proposedsize.height - reservedheight));
size.height += reservedheight;
return size;
}
#endregion
#region 事件处理方法
//展开收起
private void ckbtoggle_checkedchanged( object sender, eventargs e)
{
this .suspendlayout();
if (ckbtoggle. checked )
{
plbuttonszone.sendtoback();
lbmsg.sendtoback();
lbmsg.dock = dockstyle.top;
plbuttonszone.dock = dockstyle.top;
changeformheight(expandheight);
plattachzone.visible = true ;
}
else
{
expandheight = plattachzone.height; //为再次展开记忆高度
plattachzone.visible = false ;
changeformheight(-plattachzone.height); //收起时直接取pl高度,不要取expandheight
plbuttonszone.sendtoback();
plbuttonszone.dock = dockstyle.bottom;
lbmsg.dock = dockstyle.fill;
}
this .resumelayout();
}
//按钮事件
private void button_click( object sender, eventargs e)
{
this .dialogresult = (dialogresult)((sender as button).tag);
}
//用户手工收完详细区则触发折叠
private void plattachzone_resize( object sender, eventargs e)
{
if (ckbtoggle. checked && plattachzone.height == 0)
{
ckbtoggle. checked = false ;
}
}
#endregion
#region 辅助+私有方法
/// <summary>
/// 处理按钮相关
/// </summary>
private void processbuttons()
{
this .ckbtoggle.visible = txbattach.text.trim().length != 0; //无详细信息就不显示展开按钮
int btncount = 3; //按钮数量
switch (messagebuttons) //老实用case,可读点
{
case messageboxbuttons.abortretryignore:
button1.text = "中止(&a)" ; button1.tag = dialogresult.abort;
button2.text = "重试(&r)" ; button2.tag = dialogresult.retry;
button3.text = "忽略(&i)" ; button3.tag = dialogresult.ignore;
break ;
case messageboxbuttons.ok:
button1.visible = false ;
button2.visible = false ;
button3.text = "确定" ; button3.tag = dialogresult.ok;
btncount = 1;
break ;
case messageboxbuttons.okcancel:
button1.visible = false ;
button2.text = "确定" ; button2.tag = dialogresult.ok;
button3.text = "取消" ; button3.tag = dialogresult.cancel;
btncount = 2;
break ;
case messageboxbuttons.retrycancel:
button1.visible = false ;
button2.text = "重试(&r)" ; button2.tag = dialogresult.retry;
button3.text = "取消" ; button3.tag = dialogresult.cancel;
btncount = 2;
break ;
case messageboxbuttons.yesno:
button1.visible = false ;
button2.text = "是(&y)" ; button2.tag = dialogresult.yes;
button3.text = "否(&n)" ; button3.tag = dialogresult.no;
btncount = 2;
break ;
case messageboxbuttons.yesnocancel:
button1.text = "是(&y)" ; button1.tag = dialogresult.yes;
button2.text = "否(&n)" ; button2.tag = dialogresult.no;
button3.text = "取消" ; button3.tag = dialogresult.cancel;
break ;
default : break ;
}
//仅有ok和有取消按钮时设cancelbutton
if (( int )messagebuttons == 0 || (( int )messagebuttons & 1) == 1)
{
this .cancelbutton = button3;
}
//处理默认按钮
if (btncount == 1)
{
this .acceptbutton = button3;
}
else if (btncount == 2)
{
this .acceptbutton = defaultbutton == messageboxdefaultbutton.button2 ? button3 : button2;
}
else
{
button[] btnarray = { button1, button2, button3 };
this .acceptbutton = btnarray[convert.toint32(defaultbutton) / 0x100];
}
}
/// <summary>
/// 处理图标(含声音)
/// </summary>
private void processicon()
{
switch (messageicon)
{
//messageboxicon.information同样
case messageboxicon.asterisk:
lbmsg.icon = systemicons.information;
messagesound = "systemasterisk" ;
break ;
//messageboxicon.hand、messageboxicon.stop同样
case messageboxicon.error:
lbmsg.icon = systemicons.error;
messagesound = "systemhand" ;
break ;
//messageboxicon.warning同样
case messageboxicon.exclamation:
lbmsg.icon = systemicons.warning;
messagesound = "systemexclamation" ;
break ;
case messageboxicon.question:
lbmsg.icon = systemicons.question;
messagesound = "systemasterisk" ; //question原本是没声音的,此实现让它蹭一下information的
break ;
default : //messageboxicon.none
lbmsg.icon = null ;
messagesound = "systemdefault" ;
break ;
}
}
/// <summary>
/// 计算窗体客户区最小高度
/// </summary>
private int getclientminheight()
{
return lbmsg.minimumheight + plbuttonszone.height + padding.bottom;
}
/// <summary>
/// 计算按钮区最小宽度
/// </summary>
private int getpanelbuttonminwidth()
{
int r = 20 /*左右padding*/ , visiblecount = -1 /*因为两个以上才会有间距*/ ;
if (ckbtoggle.visible) { r += ckbtoggle.width; visiblecount++; }
if (button1.visible) { r += button1.width * 3; visiblecount += 3; }
else if (button2.visible) { r += button2.width * 2; visiblecount += 2; }
else { r += button3.width; visiblecount++; }
if (visiblecount != -1) { r += visiblecount * 6; } //按钮间距
return r;
}
/// <summary>
/// 改变窗体高度。内部有动画处理
/// </summary>
/// <param name="increment">增量(负数即为减小高度)</param>
private void changeformheight( int increment)
{
int finalheight = this .height + increment; //正确的目标高度
if (! this .useanimate) //不使用动画
{
this .height = finalheight;
return ;
}
const int step = 8; //帧数
for ( int i = 0; i < step; i++)
{
if (i == step - 1) //最后一步直达目标
{
this .height = finalheight;
return ;
}
this .height += increment / step;
application.doevents(); //必要
thread.sleep(10);
}
}
/// <summary>
/// 播放系统事件声音
/// </summary>
/// <remarks>之所以不用messagebeep api是因为这货在srv08上不出声,所以用playsound代替</remarks>
private static void playsystemsound( string soundalias)
{
playsound(soundalias, intptr.zero, 0x10000 /*snd_alias*/ | 0x1 /*snd_async*/ );
}
[dllimport( "winmm.dll" , charset = charset.auto)]
private static extern bool playsound([marshalas(unmanagedtype.lpwstr)] string soundname, intptr hmod, int soundflags);
#endregion
#region 嵌套类
/// <summary>
/// 基础面板
/// </summary>
private class panelbasic : control
{
public panelbasic()
{
setstyle(controlstyles.allpaintinginwmpaint, false ); //关键,不然其上的toolbar不正常
setstyle(controlstyles.optimizeddoublebuffer, true ); //重要。不设置的话控件绘制不正常
setstyle(controlstyles.containercontrol, true );
setstyle(controlstyles.selectable, false );
}
protected override void wndproc( ref message m)
{
//屏蔽wm_erasebkgnd。防止显示时在原位置快闪
//不能通过controlstyles.allpaintinginwmpaint=true屏蔽
//会影响其上的toolbar
if (m.msg == 0x14) { return ; }
base .wndproc( ref m);
}
protected override void setboundscore( int x, int y, int width, int height, boundsspecified specified)
{
//防dock时面板短暂滞留在原位置
base .setboundscore(x, y, width, height, specified | boundsspecified.y | boundsspecified.width);
}
}
/// <summary>
/// 消息呈现控件
/// </summary>
private class messageviewer : control
{
const textformatflags textflags = textformatflags.endellipsis //未完省略号
| textformatflags.wordbreak //允许换行
| textformatflags.nopadding //无边距
| textformatflags.externalleading //行间空白。nt5必须,不然文字挤在一起
| textformatflags.textboxcontrol; //避免半行
const int iconspace = 5; //图标与文本间距
const float preferredscale = 13; //最佳文本区块比例(宽/高)
/// <summary>
/// 最小高度。不要重写minimumsize,那会在窗体移动和缩放时都会执行
/// </summary>
public int minimumheight
{
get
{
return ( this .icon != null ? math.max( this .icon.height, this .fontheight) : this .fontheight) + padding.vertical;
}
}
/// <summary>
/// 获取或设置图标
/// </summary>
public icon icon { get ; set ; }
public messageviewer()
{
this .setstyle(controlstyles.cachetext, true );
this .setstyle(controlstyles.userpaint, true );
this .setstyle(controlstyles.allpaintinginwmpaint, true );
this .setstyle(controlstyles.selectable, false );
this .setstyle(controlstyles.resizeredraw, true ); //重要
this .doublebuffered = true ; //双缓冲
backcolor = environment.osversion.version.major == 5 ? systemcolors.control : color.white;
}
//防dock改变尺寸
protected override void setboundscore( int x, int y, int width, int height, boundsspecified specified)
{
base .setboundscore(x, y, width, height, specified | boundsspecified.size);
}
/// <summary>
/// 计算合适的消息区尺寸
/// </summary>
/// <param name="proposedsize">该参数此处定义为此控件可设置的最大尺寸</param>
/// <remarks>该方法对太长的单行文本有做比例优化处理,避免用户摆头幅度过大扭到脖子</remarks>
public override size getpreferredsize(size proposedsize)
{
if (proposedsize.width < 10) { proposedsize.width = int .maxvalue; }
if (proposedsize.height < 10) { proposedsize.height = int .maxvalue; }
int reservedwidth = padding.horizontal + ( this .icon == null ? 0 : ( this .icon.width + iconspace));
size wellsize = size.empty;
if (! string .isnullorempty( this .text))
{
//优化文本块宽高比例
size size = textrenderer.measuretext( this .text, this .font, new size(proposedsize.width - reservedwidth, 0), textflags); //用指定宽度测量文本面积
wellsize = convert.tosingle(size.width) / size.height > preferredscale //过于宽扁的情况
? size.ceiling(getsamesizewithnewscale(size, preferredscale))
: size;
//凑齐整行高,确保尾行显示
int lineheight = textrenderer.measuretext( " " , this .font, new size( int .maxvalue, 0), textflags).height; //单行高,font.height不靠谱
int differ;
wellsize.height += (differ = wellsize.height % lineheight) == 0 ? 0 : (lineheight - differ);
}
if ( this .icon != null )
{
wellsize.width += this .icon.width + iconspace;
wellsize.height = math.max( this .icon.height, wellsize.height);
}
wellsize += padding.size;
//不应超过指定尺寸。宽度在上面已确保不会超过
if (wellsize.height > proposedsize.height) { wellsize.height = proposedsize.height; }
return wellsize;
}
/// <summary>
/// 重绘
/// </summary>
protected override void onpaint(painteventargs e)
{
graphics g = e.graphics;
rectangle rect = getpaddedrectangle();
//绘制图标
if ( this .icon != null )
{
g.drawicon( this .icon, padding.left, padding.top);
//右移文本区
rect.x += this .icon.width + iconspace;
rect.width -= this .icon.width + iconspace;
//若文字太少,则与图标垂直居中
if ( this .text.length < 100)
{
size textsize = textrenderer.measuretext(g, this .text, this .font, rect.size, textflags);
if (textsize.height <= this .icon.height)
{
rect.y += ( this .icon.height - textsize.height) / 2;
}
}
}
//g.fillrectangle(brushes.gainsboro, rect);//test
//绘制文本
textrenderer.drawtext(g, this .text, this .font, rect, color.black, textflags);
base .onpaint(e);
}
/// <summary>
/// 根据原尺寸,得到相同面积、且指定比例的新尺寸
/// </summary>
/// <param name="src">原尺寸</param>
/// <param name="scale">新尺寸比例。需是width/height</param>
private static sizef getsamesizewithnewscale(size src, float scale)
{
int sqr = src.width * src.height; //原面积
double w = math.sqrt(sqr * scale); //新面积宽
return new sizef(convert.tosingle(w), convert.tosingle(sqr / w));
}
/// <summary>
/// 获取刨去padding的内容区
/// </summary>
private rectangle getpaddedrectangle()
{
rectangle r = this .clientrectangle;
r.x += this .padding.left;
r.y += this .padding.top;
r.width -= this .padding.horizontal;
r.height -= this .padding.vertical;
return r;
}
}
/// <summary>
/// 屏蔽全选消息的文本框
/// </summary>
private class textboxunselectallable : textbox
{
protected override void wndproc( ref message m)
{
//em_setsel
if (m.msg == 0xb1) { return ; }
base .wndproc( ref m);
}
}
/// <summary>
/// 包装toolbarbutton为单一控件
/// </summary>
private class togglebutton : control
{
/// <summary>
/// 展开/收起图标数据
/// </summary>
const string imgdatabase64 =
@"ivborw0kggoaaaansuheugaaacaaaaaqcayaaab3ah1zaaaagxrfwhrtb2z0d2fyzqbbzg9izsbj
bwfnzvjlywr5ccllpaaaa3njrefuenqklvlpfeeqx/8zpccue6gormd6gbegecaqd4w+ocx+ainx
ib4eftk8+g2mquutcbu8en0wmvigekymxgctjrruqhfvubrqqajgl2wpmbg6dzclwuiesf7t0739
666urqqvdjvcxt9pawkfqzkuy491ktpizarxgpv5l15j+dzirx26dqawf56c48+cx+1czddr//13
/seevvx3hz8oxmlxmzsvjht5z+nx8uokfhou31e+qwwzpbkombkwtavruae21quvjwnz5s6u25++
rv365dtc+4sxifjsfevwvscj2tozqyo2fsht1obsfeiqtitisohhw7jggbzm+s72tcovx+gcchgw
k7qttghj5slolne0txznsqgyjeehidejuslow4zmfzngjvv0qmhhyuiaup+ze+w5aftyc/xmurrh
acjikpowqdvhkhu5lcspiy6k0oil5s9mdrcnyp9sdkl+6pexew5awoebigrniivmkofipifwllcg
huim4mri3drpaqg38opmmd6nuz4wgn+korgh64/hxr1huhjl2qg8d8jcz4ztrctlsdjt1ijz51rs
5lfvzj2o2rwxxczdpcnnh3l5k5wntdhydaqng6cwa/ek+auk8sdusx65gualxr1zkcqlldbpkj+s
r8yovbxw+vx4goozsxlzyqqsk10pnldpjlvzdpms0fl55matll04c39+ewblff3l2zs+w7jzii1b
kkfw3idocdis5/g4yljknqccabrpw3j8plvmwlu8xgwosblmasyjfh3i3s4ss+w3vddg++6apj8t
own4hhh/p+g5aw3f+gbyvb632dwghigsyjdvpn4b9elzwf9aje6umaanjsolk3jdncaxue2y0veq
rcxfyect0vpces0funonrtjpgixsruqslbapogibvq8s47rkcorshqvbx7437ni6km8ol9sxeg7a
i2g0fnz2paq3tcjqgbw02ugwoqig8l7bweb1qcsfxhd3/nmmdkwdnj0op1yk6z529y1i8ovydavl
wxoaxxl3w7k4ykkyky/rdq8dofe9d+x6jonyw6wyu+pyj5/hzledpcu61ddjlh1t3e4brgyjchv0
4/qdj+bn/h+naw41kzpiwlh5kc3fms+vnxarybvt7ymdcm2228d6/ov/i8aapfki7yo+mm8aaaaa
suvork5cyii=" ;
readonly bool istogglemode;
bool ischecked;
bool useanimate;
readonly imagelist imglist;
/// <summary>
/// checked改变后
/// </summary>
public event eventhandler checkedchanged;
/// <summary>
/// 使用动画按钮效果
/// </summary>
private bool useanimate
{
get { return useanimate; }
set
{
if (useanimate == value) { return ; }
useanimate = value;
if (ishandlecreated) { this .createhandle(); }
}
}
/// <summary>
/// 获取或设置按钮是否处于按下状态
/// </summary>
[description( "获取或设置按钮是否处于按下状态" ), defaultvalue( false )]
public bool checked
{
get
{
if (ishandlecreated)
{
//保证ischecked与实情吻合。tb_isbuttonchecked
ischecked = convert.toboolean(sendmessage( this .handle, 0x40a, intptr.zero, intptr.zero).toint32());
}
return ischecked;
}
set
{
if (ischecked == value || !istogglemode) { return ; }
ischecked = value;
if (ishandlecreated)
{
//tb_checkbutton
sendmessage( this .handle, 0x402, intptr.zero, new intptr(convert.toint32(value)));
}
oncheckedchanged(eventargs.empty);
}
}
/// <summary>
/// 创建toolbarbuttoncontrol
/// </summary>
public togglebutton( bool useanimate)
{
setstyle(controlstyles.userpaint, false );
setstyle(controlstyles.allpaintinginwmpaint, true );
setstyle(controlstyles.optimizeddoublebuffer, true );
setstyle(controlstyles.resizeredraw, true );
this .istogglemode = true ; //写死好了,独立版才提供设置
this .useanimate = useanimate;
//将图标加入imagelist
imglist = new imagelist { imagesize = new system.drawing.size(16, 16), colordepth = colordepth.depth32bit };
using (memorystream ms = new memorystream(convert.frombase64string(imgdatabase64)))
{
imglist.images.addstrip(image.fromstream(ms));
}
}
/// <summary>
/// 执行左键单击
/// </summary>
public void performclick()
{
sendmessage( this .handle, 0x201, new intptr(0x1), intptr.zero); //wm_lbuttondown
application.doevents();
sendmessage( this .handle, 0x202, intptr.zero, intptr.zero); //wm_lbuttonup
}
protected override void wndproc( ref message m)
{
//忽略鼠标双击消息,wm_lbuttondblclk
if (m.msg == 0x203) { return ; }
//有节操的响应鼠标动作
if ((m.msg == 0x201 || m.msg == 0x202) && (! this .enabled || ! this .visible))
{
return ;
}
base .wndproc( ref m);
}
//创建toolbar
protected override createparams createparams
{
get
{
createparams prms = base .createparams;
prms.classname = "toolbarwindow32" ;
prms.style = 0x40000000
| 0x10000000
//| 0x2000000 //ws_clipchildren
//| 0x8000
| 0x1
| 0x4
| 0x8
| 0x40
| 0x1000 //tbstyle_list,图标文本横排
;
if (useanimate) { prms.style |= 0x800; } //tbstyle_flat。flat模式在nt6.x下,按钮按下会有动画效果
prms.exstyle = 0;
return prms;
}
}
protected override void onhandlecreated(eventargs e)
{
base .onhandlecreated(e);
//设置imglist
sendmessage( this .handle, 0x430, intptr.zero, imglist.handle); //tb_setimagelist
//准备添加按钮
int btnstructsize = marshal. sizeof ( typeof (tbbutton));
sendmessage( this .handle, 0x41e, new intptr(btnstructsize), intptr.zero); //tb_buttonstructsize,必须在添加按钮前
//构建按钮信息
tbbutton btnstruct = new tbbutton
{
//ibitmap = 0,
//idcommand = 0,
fsstate = 0x4, //tbstate_enabled
istring = sendmessage( this .handle, 0x44d, 0, this .text + '\0' ) //tb_addstring
};
if ( this .istogglemode) { btnstruct.fsstyle = 0x2; } //btns_check。作为切换按钮时
intptr btnstructstart = intptr.zero;
try
{
btnstructstart = marshal.allochglobal(btnstructsize); //在非托管区创建一个指针
marshal.structuretoptr(btnstruct, btnstructstart, true ); //把结构体塞到上述指针
//添加按钮
sendmessage( this .handle, 0x444, new intptr(1) /*按钮数量*/ , btnstructstart); //tb_addbuttons。从指针取按钮信息
//设置按钮尺寸刚好为toolbar尺寸
adjustbuttonsize();
}
finally
{
if (btnstructstart != intptr.zero) { marshal.freehglobal(btnstructstart); }
}
}
protected override bool processcmdkey( ref message m, keys keydata)
{
//将空格和回车作为鼠标单击处理
if (m.msg == 0x100 && (keydata == keys.enter || keydata == keys.space))
{
performclick();
return true ;
}
return base .processcmdkey( ref m, keydata);
}
/// <summary>
/// 处理助记键
/// </summary>
protected override bool processmnemonic( char charcode)
{
if (ismnemonic(charcode, this .text))
{
performclick();
return true ;
}
return base .processmnemonic(charcode);
}
protected override void onclick(eventargs e)
{
//忽略鼠标右键
mouseeventargs me = e as mouseeventargs;
if (me != null && me.button != system.windows.forms.mousebuttons.left)
{ return ; }
//若是切换模式,直接引发checked事件(不要通过设置checked属性引发,因为onclick发送之前就已经check了)
//存在理论上的不可靠,但暂无更好办法
if (istogglemode)
{ this .oncheckedchanged(eventargs.empty); }
base .onclick(e);
}
//重绘后重设按钮尺寸
protected override void oninvalidated(invalidateeventargs e)
{
base .oninvalidated(e);
adjustbuttonsize();
}
/// <summary>
/// 引发checkedchanged事件
/// </summary>
protected virtual void oncheckedchanged(eventargs e)
{
setimageindex( this . checked ? 1 : 0);
if (checkedchanged != null ) { checkedchanged( this , e); }
}
/// <summary>
/// 设置图标索引
/// </summary>
private void setimageindex( int index)
{
//tb_changebitmap
sendmessage( this .handle, 0x42b, intptr.zero, new intptr(index));
}
/// <summary>
/// 调整按钮尺寸刚好为toolbar尺寸
/// </summary>
private void adjustbuttonsize()
{
intptr lparam = new intptr(( this .width & 0xffff) | ( this .height << 0x10)); //makelparam手法
sendmessage( this .handle, 0x41f, intptr.zero, lparam); //tb_setbuttonsize
}
#region win32 api
[dllimport( "user32.dll" , charset = charset.auto)]
private static extern intptr sendmessage(intptr hwnd, int msg, intptr wparam, intptr lparam);
[dllimport( "user32.dll" , charset = charset.auto)]
private static extern intptr sendmessage(intptr hwnd, int msg, int wparam, string lparam);
[structlayout(layoutkind.sequential)]
private struct tbbutton
{
public int ibitmap;
public int idcommand;
public byte fsstate;
public byte fsstyle;
public byte breserved0;
public byte breserved1;
public intptr dwdata;
public intptr istring;
}
#endregion
}
#endregion
}
}
}
实现说明:
以下内容献给童鞋。这里先贴个概要类图,详细的后面有完整demo下载,你可以down回去慢慢研究。
若干show方法都是调用私有的showcore方法,这个是模仿标准messagebox的命名。至于意义,是因为公开方法要做参数检查,检查合格后的代码则可以重用。另外,几个存在参数检查的方法都是调用内部方法,而不是调参数最全的那个重载,也是因为要尽量避免无谓的参数检查,因为参数最全的那个公开方法,参数检查自然是做的最多的,那么少参方法本来已经能确保传入的是合法参数,却因为调它,就会造成无谓的检查,而调内部方法则可以避免,因为内部方法就应该设计为不做或少做参数检查的。啰嗦这个是想提醒初学者注意这些细节上的处理,性能要从细处抓起
静态类messageboxex内部维护着一个messageform窗体类(下文简称msgfm),每次show都会实例化一个msgfm,show完即释放。几乎所有能力都是由后者提供,前者只是简单的对其封装和暴露,所以下面主要说msgfm的事。另外,根据传入的messageboxbuttons有无cancel项,会启用/屏蔽窗体右上角的关闭按钮,因为单击关闭按钮的对话框结果始终是dialogresult.cancel,所以如果不屏蔽,在传入yesno这样的参数时候,调用者可能因为用户去点关闭按钮而得到yes、no以外的结果。标准消息框也是有这样的屏蔽处理的
msgfm由3个控件区构成,分别是 主消息区、按钮区、详细信息区 。
主消息区 是一个单一控件:messageviewer,直接继承自control写成。一开始是考虑用现成的label控件,但发现后者的图文混排效果差强人意(不要扯这个成语本来的意思),它是把文字直接盖在图标上,呵呵,大概此控件的编写者本意就是要把image当backgroundimage用,所以不得已另写一个messageviewer。mv主要做了两个事,绘制(图标和文本)+根据内容确定自身尺寸,另外它还控制了最小高度,避免图标和文本整体被淹没
按钮区 由一个容器类控件panelbasic托起4个按钮。pb同样是继承自control,没有直接选用panel的原因,主要是panel会在设置dock时跳一下,根源在control.setboundscore的specified参数通知了无谓的信息,所以干脆直接继承control重写该方法,顺便处理一下消息,解决瞬闪的问题,具体原因这里不细说,注释里有简短说明,总之相信我不是蛋疼就行了。此外按钮区会根据按钮可见情况控制最小宽度,它与上面的messageviewer的最小高度共同构成了整个对话框的最小尺寸minimumsize
panelbasic 上的4个按钮分别是【详细信息】按钮和其它3个对话框命令按钮。仨按钮根据传入的messageboxbuttons参数动态处理(按钮文本、是否可见等),没什么好说的。【详细信息】按钮(togglebutton)则费了番功夫,该按钮从外观上就可以看出不是标准的button,事实上它是个工具栏按钮:toolbarbutton,属于toolbar上的item,本身不是独立的控件(直接继承自component)。这里扯一点,由于.net 2.0起ms就建议用新式的toolstrip代替toolbar,类似的还有menustrip代替mainmenu、statusstrip代替statusbar、contextmenustrip代替contextmenu,vs2010更是默认就不在工具箱显示这些[控件](有些不算控件),所以估计知道的新童鞋不多。后者都是原生的win32组件,前者则是纯.net实现的,有office2003的控件风格。总之对于有win32 native控的我来说,对这些被建议替代的老式控件有特别的情结。所以这个togglebutton实际上是由一个toolbar和一个toolbarbutton组成的看起来像一个单一控件的东西,那为什么它还是继承自control而不是直接用toolbar呢,我承认这里面有练手的原因(迟些我可能会写一篇【教你一步步封装一个win32原生控件】的文章),hmmm~也就这个原因了,但它虽然增加了代码量,但请务必相信性能不比直接用toolbar差,理论上还要好过,因为作为一个完备的toolbar,ms要考虑的情况相当多,显然处理也少不了,而我这个togglebutton由于只负责一个单一按钮的功能,所以其实很simple很lite~聪明的你会理解的。最后为什么要费事弄成toolbarbutton而不是直接用一个button,是因为我看上了mstsc.exe的这个效果:
顺便说一点,enableanimate属性有作用到该按钮,原理是当toolbar具有flat样式的时候,按钮按下和弹起就有动画效果,否则没有
最后是 详细信息区 ,由一个panelbasic托起一个简单改造过的textbox构成。干嘛不单纯用一个textbox,而要在它底下垫一层呢,是因为在xp上的效果不好(控件狗要考虑的情况很多了啦好不好),xp窗口边框不如nt6粗,不加点衬料的话太单薄。话说回来,panelbasic上面已说过,而所谓改造过的这个textbox叫textboxunselectallable,就干一件事,忽略全选消息(em_setsel),避免焦点移进去的时候蓝莹莹一大片吓到观众。而为什么不用标准textbox的enter事件取消全选,一个字~太low
尚存在一个问题 ,这个注释里也有坦白,就是当主消息文本非常非常多时~大概整屏那么长(这其实是不正确的使用姿势,上面说过,大量信息应该放详细信息区),如果对对话框反复拖拉、展开/收起,那么在某次展开时,textboxunselectallable会瞬间在主消息区闪一下,这个问题在panelbasic得到了完美的解决,但textbox实在无能为力,尝试过直接用原生edit控件也如此,所以暂时留着吧,哪有没缺憾的人生呢
关于声音,由于messagebeep api在srv08系统无声,所以用了playsound api代替。另外,让原本没声音的messageboxicon.question蹭systemicons.information的声音,不能歧视人question
最后,【详细信息】按钮上那俩图标(展开、收起各一个)是我画的,本来想拣mstsc.exe上的,但发现效果不如意,还不如自己画
说了这么多,自以为很理想的实现,可能槽点也不少,再次恳请路过大侠指点,谢谢。
最后,demo 在此 ,里面有个tester供你体验:
-文毕-
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
dy("nrwz");
查看更多关于一个可携带附加消息的增强消息框MessageBoxEx的详细内容...