如何在。net中扩展本机消息框对话框

介绍 在本文中,我将解释如何在托管代码中扩展本机消息框。有两个函数,我将添加到消息框: 显示倒计时消息,并在指定超时过期时自动关闭消息框,并返回对话框的默认选项。在消息框中添加一个复选框控件,以给用户一个额外的选项。 背景 有些文章已经通过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

原文地址:https://www.cnblogs.com/Dincat/p/13461325.html