介绍 在本文中,我将解释如何在托管代码中扩展本机消息框。有两个函数,我将添加到消息框: 显示倒计时消息,并在指定超时过期时自动关闭消息框,并返回对话框的默认选项。在消息框中添加一个复选框控件,以给用户一个额外的选项。 背景 有些文章已经通过Windows挂钩实现了这些函数。虽然它们非常有效,但我认为这种技术太复杂,难以掌握,特别是对于初学者。所以,我决定尝试另一种方法。Thammadi的文章给了我一个更好的主意。他使用Windows计时器服务以编程方式关闭消息框;我也可以做同样的事来实现我的目标。 声明本地对象 由于消息框窗口是一个本机窗口,我们只能通过Windows api访问它。下面是我们完成此任务所需的api列表。其中大部分都是使用Lutz Roeder's Reflector从System.Window.Forms.dll中分解出来的。:) 隐藏,收缩,复制Code
[DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern bool SetWindowText(IntPtr hWnd, string text); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int maxCount); [DllImport("user32.dll", CharSet = CharSet.Auto)] public extern static IntPtr FindWindow(string className, string caption); [DllImport("user32.dll", CharSet = CharSet.Auto)] public extern static IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string className, string caption); [DllImport("user32.dll", CharSet = CharSet.Auto)] public extern static int GetWindowLong(IntPtr hWnd, int index); [DllImport("user32.dll", CharSet = CharSet.Auto)] public extern static IntPtr SetWindowLong(IntPtr hWnd, int index, IntPtr newLong); [DllImport("user32.dll", CharSet = CharSet.Auto)] public extern static IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern bool EnumChildWindows(IntPtr hWndParent, EnumChildProc callback, IntPtr param); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetClassName(IntPtr hWnd, StringBuilder className, int maxCount); [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern bool GetWindowRect(IntPtr hWnd, [In, Out] ref NativeMethods.RECT rect); [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern bool GetClientRect(IntPtr hWnd, [In, Out] ref NativeMethods.RECT rect); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int flags); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern bool ScreenToClient(IntPtr hWnd, [In, Out] ref NativeMethods.POINT point); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern bool DestroyWindow(IntPtr hWnd);
我们还需要两种结构类型来处理本机窗口,见下面的代码: 隐藏,复制Code
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; } [StructLayout(LayoutKind.Sequential)] public struct POINT { public int x; public int y; }
我们还需要一些常数。详细信息请参考源代码中的nativememethods .cs文件。 处理消息框 托管MessageBox类调用本机API消息框来显示消息框。如果我们想控制它,并避免使用Windows挂钩,我们必须在消息框显示之后做一些事情。众所周知,消息框是一个模态对话框,我们不能仅仅在MessageBox.Show()语句之后编写代码来处理对话框。我们需要在程序被模态对话框阻塞时执行代码。计时器组件可以帮助我们做到这一点。 设置计时器组件 可以在“工具箱”的“组件”部分中找到计时器组件,然后将其拖放到窗体中。该组件有四个重要成员:间隔属性、启动和停止方法以及滴答事件。我们需要做的是为Tick事件创建一个事件处理方法。默认情况下,该方法的名称为timer1_Tick,所有处理代码都在那里。 准备显示消息框 只需一条语句就可以显示消息框。但是为了处理它,我们必须首先启动计时器并进行一些初始化工作。由于我们将在第一次调用timer1_Tick方法时进行一些奇妙的工作,所以我们应该将timer1的Interval属性设置为一个非常小的值,这样用户就不会注意到对消息框所做的更改。 隐藏,复制Code
// initializations seconds = -1; timer1.Interval = 10; // starts the timer timer1.Start(); // shows the message box with Yes, No, Cancel bttons, // and the default button is No. System.Windows.Forms.MessageBox.Show(this, "Message body goes here.", "Dialog Caption", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Information, MessageBoxDefaultButton.Button2);
超时函数的实现 超时功能使消息框在指定的秒数之后自动关闭。因此,我们需要一个字段变量来保存经过的秒,并将该变量命名为seconds。因为我们想要向用户显示倒计时消息,所以还需要一个Label控件。 实现timer1_Tick方法 首先,在这个方法中,我们需要确保消息框对话框已经实际打开。调用FindWindow函数来获得这个对话框的句柄。如上所示,这个API有两个参数。第一个参数指定我们想要找到的窗口的类名,我们可以传递null来忽略它。第二个是窗口标题栏中显示的文本。如果返回值非零,则表示已经找到消息框,返回值是消息框的句柄。 在找到消息框之后,我们应该确定这是否是第一次调用方法(seconds == -1)。如果是这样,我们需要停止计时器,装饰消息框,将timer1的间隔更改为1000,并重新启动它。每次调用该方法时,变量秒数都应该增加。然后,检查变量是否超过超时。当检测到过期时,立即关闭消息框。当计时器滴答作响时,我们也可以显示一个倒计时消息来通知用户。为此,我们应该在消息框的第一个刻度处放置一个Label控件。有关详细信息,请参阅以下两个部分。 如果未找到该消息框,则应认为该消息框已被关闭。所以,停止计时器。 下面的代码演示了timer1_Tick方法的一个简单实现: 隐藏,收缩,复制Code
IntPtr hWndMsgBox; // looks for our message box hWndMsgBox = FindWindow(null, "Dialog Caption"); if (hWndMsgbox != IntPtr.Zero) { if (seconds == -1) { timer1.Stop(); // todo: adds Label control to the message box at the first tick // ... timer1.Interval = 1000; timer1.Start(); } // updates the countdown message. // suppose the timeout value is 30 seconds. label.Text = string.Format("{0} seconds elapsed.", 30 - ++seconds); if (seconds >= 30) { // timeout expired // todo: closes the message box // ... // stops the timer timer1.Stop(); } } else timer1.Stop();
向消息框添加Label控件 可以通过调用API函数SetParent向本机窗口添加控件。我们应该关心的是在哪里以及如何放置这个控件。在我的项目中,我将Label控件放置在消息框的左下角。当然,仅仅放置控件是不够的,我们还应该修改消息框的尺寸以适应标签。 这里,我们需要以下API函数: GetWindowRect——受潮湿腐烂rieves消息框边界矩形的尺寸。SetWindowPos——更改消息框的大小。GetClientRect—检索消息框的客户区域的坐标。 添加Label控件的代码示例(请参见消息框)。详细说明DecorateMessageBox方法): 隐藏,复制Code
RECT rect = new RECT(); // retrives current dimensions of the message box GetWindowRect(hWndMsgBox, ref rect); // increase the message box's height to fit label SetWindowPos(hWndMsgBox, IntPtr.Zero, 0, 0, rect.right - rect.left, rect.bottom - rect.top + lable.Height, SWP_NOZORDER | SWP_NOMOVE); // adds label to the message box SetParent(lable.Handle, hWndMsg); // retrieves the size of the message box's client area GetClientRect(hWndMsgBox, ref rect); // sets labels location label.Location = new Point(0, rect.bottom - label.Height);
以编程方式关闭消息框 要以编程方式关闭消息框,需要考虑一件重要的事情。如果我们只是简单地发送一个WM_CLOSE消息或调用DestoryWindow函数,返回的值将不是默认按钮的相应值。我所做的是发送一个WM_COMMAND消息来模拟按钮点击。 WM_COMMAND消息需要三个参数,一个通知代码,一个标识符,以及被单击按钮的句柄。可以直接从MSDN检索通知代码,但是其他参数应该通过编程方式检索。考虑到重用这些参数,我创建了另外两个分类来保存消息框的所有子控件的信息,MessageBoxChild和MessageBoxChildCollection。 要获得所有子控件的信息,我们应该调用EnumChildWindows函数。此函数枚举消息框的所有子控件,并调用应用程序定义的回调函数。在托管代码中,回调函数可以与委托交替使用。委托声明如下: 隐藏,复制Code
public delegate bool EnumChildProc(IntPtr hWnd, IntPtr param);
在回调函数中,我们检索每个子元素的句柄、标识符、类名、样式等。这些信息存储在MessageBoxChild类中,并将逐个添加到MessageBoxChildCollection类的实例中。在向集合对象添加时,它确定控件是什么。因此,我们可以方便地使用他们以后。 枚举消息框的子控件的代码示例如下: 隐藏,复制Code
// clears the collection object collection.Clear(); // enumerates all child controls of the message box EnumChildWindows(hWndMsgBox, new EnumChildProc(EnumChildren), IntPtr.Zero);
这里显示了EnumChildren方法的代码示例(请参见消息框)。EnumChildren方法): 隐藏,收缩,复制Code
private bool EnumChildren(IntPtr hWnd, IntPtr lParam) { // local variable declearation StringBuilder name = new StringBuilder(1024); StringBuilder caption = new StringBuilder(1024); RECT rect = new RECT(); int style, id; // retrieves control's class name. GetClassName(hWnd, name, 1024); // retrieves control's caption. GetWindowText(hWnd, caption, 1024); // retrieves control's bounds. GetWindowRect(hWnd, ref rect); // retrieves control's style. style = GetWindowLong(hWnd, GWL_STYLE); // retrieves control's identifier. id = GetWindowLong(hWnd, GWL_ID); // creates a new instance of MessageBoxChild // and add it to the collection collection.Add(new MessageBoxChild(hWnd, name.ToString(), caption.ToString(), rect, style, id)); // returns true for continue enumerating return true; }
这里显示了用于确定控件是什么的代码示例(参见MessageBoxChildCollection)。添加方法): 隐藏,复制Code
if (child.ClassName == "Button") this.buttons.Add(child); else if (child.ClassName == "Static") { if ((child.Style & SS_ICON) != 0) this.icon = child; else this.label = child; }
一旦我们需要的所有信息都准备好了,我们就可以从集合对象中获取默认按钮的信息。然后,我们调用SendMessage函数来发送WM_COMMAND消息来模拟默认按钮的单击。 隐藏,复制Code
// retrives information of the default button control MessageBoxChild bn = collection.GetButton(MessageBoxDefaultButton.Button2); // send message to the message box the simulate button clicking SendMessage(hWndMsgBox, WM_COMMAND, (BN_CLICKED << 16) | bn.Id, bn.Handle);
实现一个复选框函数 通过向消息框中添加一个ChecBox控件,我们可以给用户一个额外的选项来进行选择。通常,这个选项是“不要再显示这个”。此函数的实现类似于添加Label控件。不同之处在于位置。我将复选框控件放置在按钮上方,并将其左边缘与对话框的消息对齐。因此,我们不仅应该增加消息框的高度,还应该将按钮降低。 关于如何检索子控件的信息,我已经在前一节中解释过了。但是,我们之前检索到的坐标是相对于屏幕左上角的。因此,我们必须将它们转换为相对于消息框窗口客户区域左上角的坐标。 添加复选框控件的代码示例如下所示(请参见MessageBox)。详细说明DecorateMessageBox方法): 隐藏,收缩,复制Code
POINT point = new POINT(); RECT rect = new RECT(); int top; // move the buttons lower foreach (MessageBoxChild bn in collection.Buttons) { point.x = bn.Rectangle.left; point.y = bn.Rectangle.top; // converts screen relative coordinates to client relative corrdinates ScreenToClient(hWndMsgBox, ref point); // moves button SetWindowPos(bn.Handle, IntPtr.Zero, point.x, point.y + 10 /* vertical space between the checkbox and the buttons */ + checkBox.Height, 0, 0, SWP_NOZORDER | SWP_NOSIZE); } // saves Y-coordinate for the CheckBox control top = point.y; // retrieves and converts the coordinates of the native label control point.x = collection.Label.Rectangle.left; point.y = collection.Label.Rectangle.top; ScreenToClient(hWndMsgBox, point); // adds the CheckBox control SetParent(checkBox.Handle, hWndMsgBox); // sets the CheckBox control's location checkBox.Location = new Point(point.x, top); // retrieves current dimensions of the message box GetWindowRect(hWndMsgBox, ref rect); // resizes the message box SetWindowPos(hWndMsgBox, IntPtr.Zero, 0, 0, rect.right - rect.left, rect.bottom - rect.top + checkBox.Height + 10, SWP_NOZORDER | SWP_NOMOVE);
创建自己的组件 我已经介绍了本文的所有关键知识。接下来,我们应该将它们组合起来并封装到单个组件中。在该组件中,我们需要建立串行属性和方法,并提供设计时支持,以使用户能够更好地使用该组件。我所建立的成员见下图: 对于设计时的支持,我们只需要在属性中添加一些属性,如DescriptionAttribute、CategoryAttribute、DefaultValueAttribute等。 隐藏,收缩,复制Code
[DefaultValue(0), Category("Timeout")] [Description("Specifies the amount of seconds that the " + "message box appears. Set it to 0 to disable the function.")] public int Timeout { get { return timeout; } set { if (value < 0) throw new ArgumentOutOfRangeException("Timeout", "Timeout cannot be less than 0."); timeout = value; } } [Category("Behavior"), Description("The text to display in the message box.")] [Editor("System.ComponentModel.Design.MultilineStringEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(System.Drawing.Design.UITypeEditor))] public string Message { get { return message; } set { message = value; } }
请注意上面示例代码中的Message属性。我给它添加了一个EditorAttribute。这允许用户在设计时在属性网格中输入多行文本。 为了显示消息框,需要一个ShowDialog方法。在前面的“准备显示消息框”一节中,我已经解释了如何显示消息框。但是,为了使消息框具有灵活的外观,我们必须使用变量而不是常量值。代码是这样的: 隐藏,复制Code
System.Windows.Forms.MessageBox.Show(owner, this.Message, this.Caption, this.Buttons, this.Icon, this.DefaultButton);
使用组件 在成功生成包含组件的项目后,工具箱中将显示一个新图标。要使用它,可以将其拖放到窗体中,或者在运行时创建新实例。下面的代码演示了第二个选项: 隐藏,复制Code
// creates a new instance of this component MessageBox msgBox = new MessageBox(); // sets the text to display msgBox.Message = "Hello, world!"; // sets the icon of the message box msgBox.Icon = MessageBoxIcon.Information; // sets the timeout to 10 seconds msgBox.Timeout = 10; // shows the message box msgBox.ShowDialog(this);
的兴趣点 我从来没有写过这么大的文章re,这是我第一次,哈哈。因为我的英语水平很差,所以花了很长时间才完成。在写这篇文章的时候,我参考了一些翻译新单词和拼写的软件。所以我认为这是一项艰苦的工作,哈哈,开个玩笑。最后,我希望你能明白我的意思。 历史 一个也没有。 本文转载于:http://www.diyabc.com/frontweb/news3727.html