利用 Windows Hook 以及其他一些 Win32 API ,可以对大家经常使用的 System.Windows.Forms.MessageBox 进行手术式的改进,以取得一些很酷的效果。
本例实现了如何给 System.Windows.Forms.MessageBox 加装一个定时器。本例的开发冲动来自联众飞行棋。
很多朋友认为用 System.Windows.Forms.Form + 多线程来做不是更容易吗,不幸的是——我的观点恰恰相反。在 .Net 上开发高级的 WinForm 应用,(至少到目前为止)传统的 Win32 API 是无法回避的,许多高级的应用和技巧必须要用到 Win32 API。与其怕繁难,不如勇敢面对。
本文不介绍 Windows Hook 的原理和 C# 实现,详细的参考资料请参阅:
http://msdn.microsoft.com/msdnmag/issues/02/10/CuttingEdge/(关于 Windows Hook 的详细介绍,本例使用其类)
http://msdn.microsoft.com/msdnmag/issues/02/11/CuttingEdge/(关于 MessageBox 的其他手术式改进实例)
在读本文之前,请先拜读以上两文。
本文不介绍有关 Win32 API 的 C# 调用原理。文中引用的某些 Win32 枚举常量及大量 Win32 API 来自我个人的一个标准 Win32 库(特别是 WindowsAPI 这个工具类)。大多数方法和枚举的名称与 MSDN 的名称保持一致,可以说是一目了然的。
具体的实现代码如下所示,实现的技术细节问题以注释形式夹杂在代码之间,这里不再多费口舌了。
最后要申明的是,这个实例刚刚出炉,所以还未在 Windows 98 上进行测试。using System;
using System.Drawing;
using System.Windows.Forms;
using XXXX.Common.Win32;
namespace XXXX.Library.Forms
{
/// <summary>
/// 带有定时器功能的消息框
/// </summary>
/// <example>
/// <code>
///
/// TimableMessageBox tmMsgBox = new TimableMessageBox();
/// tmMsgBox.DelaySeconds = 15; // 默认值为 10 seconds.
/// // tmMsgBox.CusotmIcon = yourIcon // 可以在此设置自定义图标。
/// DialogResult dr;
/// dr = tmMsgBox.Show ( DialogResult.No, // 超时后的默认结果
/// "Your message text",
/// "Your title",
/// MessageBoxButtons.YesNo,
/// MessageBoxIcon.Question );
///
/// if ( dr == DialogResult.No )
/// return;
/// else
/// // Do your process ...
///
/// </code>
/// </example>
public class TimableMessageBox
{
// 一个缺省的时钟图标(如示意图中所示)
// 来自本程序集的一个作为嵌入资源的图标
//
恕不介绍如何设置及读入嵌入资源private static Icon DefaultIcon = Resources.icoTimer01;
// 定义静态文本控件的样式
private const int StaticTextControlStyle =
(int)StaticStyles.SS_CENTER |
(int)WindowStyles.WS_CHILD |
(int)WindowStyles.WS_VISIBLE ;
// Win32 常数
private const int ID_ICONWINDOW = 0x0014;
private const int STM_SETICON = 0x0170;
// 域变量
protected LocalCbtHook m_cbt;
protected IntPtr m_hwnd = IntPtr.Zero;
protected IntPtr m_hwndStatic = IntPtr.Zero;
protected IntPtr m_closeButton = IntPtr.Zero;
protected bool m_bInited = false;
protected bool m_bTimeout = false;
protected bool m_isCustom = false;
protected bool m_needEnumChild = false;
protected bool m_needReplaceIcon = false;
protected Icon m_customIcon;
private int m_timerCount = 0;
private int m_delaySecond = 10;
/// <summary>
/// 默认构造函数
/// </summary>
public TimableMessageBox()
{
m_cbt = new LocalCbtHook(); // 此类取自 MSDN Magzine,不过已转移到我的 WIN32 库中。
m_cbt.WindowCreated += new CbtEventHandler(WndCreated);
m_cbt.WindowActivated += new CbtEventHandler(WndActivated);
}
/// <summary>
/// 获取和设置自定义图标。
/// </summary>
public Icon CustomIcon
{
get { return m_customIcon; }
set { m_customIcon = value; }
}
/// <summary>
/// 获取和设置可以停留的时间,以秒为单位。
/// </summary>
public int DelaySeconds
{
get { return m_delaySecond; }
set { m_delaySecond = value; }
}
private void WndCreated(object sender, CbtEventArgs e)
{
if (e.IsDialogWindow)
{
m_bInited = false;
m_hwnd = e.Handle;
}
}
private void WndActivated(object sender, CbtEventArgs e)
{
// 不是相同的窗口不作处理
if (m_hwnd != e.Handle)
return;
// 是否已初始化,只有第一次激活消息框时才需进行后继的更改。
if (m_bInited)
return;
else
m_bInited = true;
// 更换图标
if (m_needReplaceIcon)
{
IntPtr hwndIcon1 = WindowsAPI.GetDlgItem(m_hwnd, ID_ICONWINDOW);
IntPtr hIcon;
if (m_isCustom)
{
hIcon = m_customIcon.Handle;
}
else
{
hIcon = DefaultIcon.Handle;
}
WindowsAPI.SendMessage(hwndIcon1, STM_SETICON, hIcon, IntPtr.Zero);
}
#region 添加一个静态文本控件
// 获取静态文本控件可使用的字体。
IntPtr hFont;
IntPtr hwndText = WindowsAPI.GetDlgItem(m_hwnd, 0xFFFF);
if(hwndText != IntPtr.Zero)
hFont = new IntPtr(WindowsAPI.SendMessage(hwndText, (int)Msg.WM_GETFONT, IntPtr.Zero, IntPtr.Zero));
else
hFont = new IntPtr(WindowsAPI.SendMessage(m_hwnd, (int)Msg.WM_GETFONT, IntPtr.Zero, IntPtr.Zero));
Font fCur = Font.FromHfont(hFont);
// 获取静态文本控件位置的 x 和 y 坐标值
int x = 0, y = 0;
IntPtr hwndIcon = WindowsAPI.GetDlgItem(m_hwnd, ID_ICONWINDOW);
RECT rcIcon = new RECT();
WindowsAPI.GetWindowRect(hwndIcon, ref rcIcon);
POINT pt = new POINT();
pt.x = rcIcon.left;
pt.y = rcIcon.top;
WindowsAPI.ScreenToClient(m_hwnd, ref pt);
x = pt.x;
y = pt.y + rcIcon.bottom - rcIcon.top + 2;
// 创建一个静态文本控件并添加至消息框
m_hwndStatic = WindowsAPI.CreateWindowEx(0,
"static", "0", StaticTextControlStyle,
x, y , rcIcon.right - rcIcon.left, (int)fCur.GetHeight(),
m_hwnd, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
// 更新所需的字体
WindowsAPI.SendMessage(m_hwndStatic, (int)Msg.WM_SETFONT, hFont, new IntPtr(1));
// 找一个消息框上的按钮
if (m_needEnumChild)
WindowsAPI.EnumChildWindows(this.m_hwnd,
new WindowsAPI.EnumChildProc(this.EnumChildWindowsProc), 0);
// 移除钩子
m_cbt.Uninstall();
#endregion
}
// 计时信息更新定时器的处理例程
private void TimerUpdateElapseHandler(IntPtr hWnd, uint uiMsg, uint idEvent, int dwTime)
{
if (this.m_hwndStatic == IntPtr.Zero)
return;
// 显示计时消息文本
WindowsAPI.SetWindowText(this.m_hwndStatic, (++m_timerCount).ToString());
}
// 关闭消息框定时器的处理例程
private void TimerCloseElapseHandler(IntPtr hWnd, uint uiMsg, uint idEvent, int dwTime)
{
// 需要模拟一个单击操作
if (m_closeButton != IntPtr.Zero)
{
int buttonCommand = WindowsAPI.GetDlgCtrlID(m_closeButton);
// Console.WriteLine("Command : " + buttonCommand);
// 发送命令消息
WindowsAPI.SendMessage(m_hwnd, (int)Msg.WM_COMMAND, buttonCommand, m_closeButton);
}
else
{
// 直接发送关闭窗口消息
WindowsAPI.SendMessage(m_hwnd, (uint)Msg.WM_CLOSE, 0, 0);
}
// 设置超时标记
m_bTimeout = true;
}
// 枚举子窗口的处理例程
// 用来找一个按钮。
private bool EnumChildWindowsProc(IntPtr hwnd, int lPram)
{
// 获取子窗口的 windows class 名称。
STRINGBUFFER sb;
int lRet = WindowsAPI.GetClassName(hwnd, out sb, 512);
string sClassName = sb.szText;
//Console.WriteLine(sClassName);
// 检查是否为按钮。
if (sClassName.ToUpper() == "BUTTON")
{
m_closeButton = hwnd;
return false; // 只要找到就立即停止枚举
}
else
{
return true;
}
}
#region Methods
/// <summary>
/// 显示消息框的主方法,其他重载方法最终均调用本方法。
/// </summary>
/// <param name="timeoutDialogResult">超时后的对话框结果</param>
/// <param name="strMessageText">要显示在消息框上的消息文本</param>
/// <param name="strMessageBoxTitle">消息框的标题文本</param>
/// <param name="buttons">按钮组合</param>
/// <param name="icon">图标样式,如果设置有自定义图标,此处设置的图标样式无意义。
/// 如果设置为 MessageBoxIcon.None,则将使用内置的默认图标:一个小钟。</param>
/// <param name="defaultButton">缺省按钮的位置</param>
/// <returns> System.Windows.Forms.DialogResult </returns>
public DialogResult Show(DialogResult timeoutDialogResult,
string strMessageText, string strMessageBoxTitle,
MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{
// 是否为自定义图标
// 自定义的图标最优先被考虑。
m_isCustom = m_customIcon != null && m_customIcon != DefaultIcon;
// 是否需要更换图标
// 自定义图标时,必须要更换图标。
// 另外,当没有设置自定义图标并且未设置任何标准图标样式时,
// 那么就使用缺省的图标,此时也需要更换图标。
m_needReplaceIcon = m_isCustom || icon == MessageBoxIcon.None;
// 如果未指定图标类型,添加一个占位图标类型。
// 只要不是 MessageBoxIcon.None 都可以,
// 但要注意 Warning、Stop、Error 会发出一个不同的音响哦。
// 确保有一个图标,这是为了能在图标下方显示计时信息。
if (icon == MessageBoxIcon.None)
{
icon = MessageBoxIcon.Information;
}
// 重置各标记位
m_bTimeout = false;
m_timerCount = 0;
m_hwnd = IntPtr.Zero;
m_closeButton = IntPtr.Zero;
// 由于以下两种按钮样式的消息框的窗口标题栏上的 Close 按钮是禁用的,
// 此时发送 WM_CLOSE 消息是不能关闭消息框的。
// 所以采用了一个迂回的方法:先找到消息框上的任意一个按钮,
// 在关闭消息框定时器的事件处理例程中针对该按钮模拟一个命令操作,
// 以达到关闭窗口的目的。至于什么按钮是不重要的,因为超时后返回的
// 对话框结果是由调用方设定的缺省值参数:timeoutDialogResutl。
m_needEnumChild = ( MessageBoxButtons.AbortRetryIgnore == buttons ||
MessageBoxButtons.YesNo == buttons);
// 创建两个定时器
// 关闭消息框的定时器
IntPtr timerColse = WindowsAPI.SetTimer(
IntPtr.Zero, 0, m_delaySecond * 1000, new WindowsAPI.TimerProc( TimerCloseElapseHandler ));
// 更新计时信息的定时器
IntPtr timerUpdate = WindowsAPI.SetTimer(
IntPtr.Zero, 0, 1000, new WindowsAPI.TimerProc( TimerUpdateElapseHandler));
// 设置钩子
m_cbt.Install();
// 显示消息框
DialogResult dr = MessageBox.Show(
strMessageText, strMessageBoxTitle, buttons, icon, defaultButton);
// 销毁定时器
WindowsAPI.KillTimer(IntPtr.Zero, timerColse);
WindowsAPI.KillTimer(IntPtr.Zero, timerUpdate);
// 视是否超时,返回相应的结果。
if (m_bTimeout)
return timeoutDialogResult; // 若超时,则返回参数设定的值。
else
return dr; // 若未超时,则返回用户单击的值。
}
public DialogResult Show(DialogResult timeoutDialogResult,
string strMessageText, string strMessageBoxTitle,
System.Windows.Forms.MessageBoxButtons buttons, MessageBoxIcon icon)
{
return Show(timeoutDialogResult, strMessageText, strMessageBoxTitle,
buttons, icon, MessageBoxDefaultButton.Button1);
}
public DialogResult Show(DialogResult timeoutDialogResult,
string strMessageText, string strMessageBoxTitle,
System.Windows.Forms.MessageBoxButtons buttons)
{
return Show(timeoutDialogResult,strMessageText, strMessageBoxTitle,
buttons, MessageBoxIcon.None);
}
public DialogResult Show(DialogResult timeoutDialogResult,
string strMessageText, string strMessageBoxTitle)
{
return Show( timeoutDialogResult,strMessageText, strMessageBoxTitle,
System.Windows.Forms.MessageBoxButtons.OK );
}
public DialogResult Show(DialogResult timeoutDialogResult,
string strMessageText)
{
return Show(timeoutDialogResult, strMessageText, "");
}
#endregion
}
}