Home > Blockchain >  How to reproduce the same layout of a MessageBox in a new window?
How to reproduce the same layout of a MessageBox in a new window?

Time:10-28

I am searching for a way to create a "Yes to all - Yes - No - No to all" message box. I first tried to use a MessageBox instance but I understood that it is impossible to add custom buttons to it. So here I am with a custom window I created myself which does what I want but its design is far from being at least not ugly.

So my question is: How to reproduce the same layout of a MessageBox like below but with extra buttons?

MessageBox

CodePudding user response:

There is a lot of examples of custom message boxes, both for WPF and WinForms.

I'm very sorry about amount of text, but here is detailed explanation and code examples.

As @mm8 suggested, the easiest way is to create simple Window, build layout for Header (Caption), Icon, Message and Buttons.

XAML:

<Window x:Class="WPFApp.CustomMessageBox"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFApp"
        mc:Ignorable="d"
        Title=""
        MinHeight="150"
        Width="500" 
        SizeToContent="Height"
        ResizeMode="NoResize"
        WindowStyle="None"
        AllowsTransparency="True"
        Background="Transparent"
        FontSize="14"
        WindowStartupLocation="CenterScreen">
    <Grid Margin="5" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="64"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="28"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="40"/>
        </Grid.RowDefinitions>

        <!-- Border for our custom message box -->
        <Border Grid.ColumnSpan="3" 
                Grid.RowSpan="3" 
                BorderBrush="Gray" 
                BorderThickness="1" 
                HorizontalAlignment="Stretch" 
                VerticalAlignment="Stretch">
            <Border.Effect>
                <DropShadowEffect BlurRadius="4" 
                                  ShadowDepth="0"
                                  Direction="270" 
                                  Color="Black" 
                                  RenderingBias="Performance"/>
            </Border.Effect>
        </Border>
        <!-- Header of our message box to keep Caption and to be used for window move -->
        <TextBlock x:Name="CMBCaption" 
                   HorizontalAlignment="Stretch" 
                   Grid.Row="0" 
                   Text="Custom Message Box Caption"
                   Grid.ColumnSpan="2" 
                   Background="Gainsboro"
                   Foreground="Black"
                   FontWeight="SemiBold"
                   Margin="1,1,1,0"
                   Padding="5,2.5,0,0" 
                   MouseLeftButtonDown="OnCaptionPress"/>
        <!-- Icon for our custom message box -->
        <Image x:Name="CMBIcon" 
               Grid.Column="0" 
               Grid.Row="1" 
               HorizontalAlignment="Stretch" 
               VerticalAlignment="Stretch"
               Width="36" 
               Height="36"/>
        <!-- TextBlock for message content. Wrapped into Label because of alignment needs -->
        <Label Grid.Column="1" 
               Grid.Row="1" 
               VerticalContentAlignment="Center" 
               Margin="2,24,4,24">
            <TextBlock x:Name="CMBMessage" 
                       TextWrapping="Wrap"
                       Text="Custom Message Box Message"/>
        </Label>
        <!-- Background for button block -->
        <Rectangle Grid.Row="2" 
                   Grid.ColumnSpan="2" 
                   Fill="Gainsboro"
                   Margin="1,0,1,1"/>
        <!-- Buttons block -->
        <StackPanel x:Name="CMBButtons"                    
                    Grid.Row="2"
                    Grid.ColumnSpan="2" 
                    Orientation="Horizontal"
                    FlowDirection="RightToLeft" 
                    HorizontalAlignment="Right" 
                    VerticalAlignment="Stretch" 
                    Margin="0,0,6,0"/>
    </Grid>
</Window>

So here is TextBlock ("CMBCaption" for our Caption), Image ("CMBIcon" for our icon), TextBlock ("CMBMessage" for our message, putted into Label as Content property to make correct alignment) and StackPanel ("CMBButtons" for some amount of buttons). "CMB" (if not obvious) is abbreviation of CustomMessageBox. That will give you simple little window, which can be movable (by MouseLeftButtonDown="OnCaptionPress" handler on Caption TextBlock), looks simple and fresh, is stretchable (depending on content size) and has StackPanel at bottom to store any amount of buttons you wish.

enter image description here

Code-behind: (check comments and remarks below)

using System.Drawing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Brushes = System.Windows.Media.Brushes;

namespace WPFApp
{
    public partial class CustomMessageBox : Window
    {
        // Field that will temporarily store result before we return it and close CustomMessageBox
        private static CustomMessageBoxResult result = CustomMessageBoxResult.OK;


        // Buttons defined as properties, because couldn't be created (initialized) with event subscription at same time "on-the-fly".
        // You can add new different buttons by adding new one as property here
        // and to CustomMessageBoxButtons and CustomMessageBoxResult enums   
        private Button OK
        {
            get
            {
                var b = GetDefaultButton();
                b.Content = nameof(OK);
                b.Click  = delegate { result = CustomMessageBoxResult.OK; Close(); };
                return b;
            }
        }
        private Button Cancel
        {
            get
            {
                var b = GetDefaultButton();
                b.Content = nameof(Cancel);
                b.Click  = delegate { result = CustomMessageBoxResult.Cancel; Close(); };
                return b;
            }
        }
        private Button Yes
        {
            get
            {
                var b = GetDefaultButton();
                b.Content = nameof(Yes);
                b.Click  = delegate { result = CustomMessageBoxResult.Yes; Close(); };
                return b;
            }
        }
        private Button No
        {
            get
            {
                var b = GetDefaultButton();
                b.Content = nameof(No);
                b.Click  = delegate { result = CustomMessageBoxResult.No; Close(); };
                return b;
            }
        }            
        // Add another if you wish


        // There is no empty constructor. As least "message" should be passed to this CustomMessageBox
        // Also constructor is private to prevent create its instances somewhere and force to use only static Show methods
        private CustomMessageBox(string message,
                                 string caption = "",
                                 CustomMessageBoxButtons cmbButtons = CustomMessageBoxButtons.OKOnly,
                                 CustomMessageBoxIcon cmbIcon = CustomMessageBoxIcon.None)
        {    
            InitializeComponent();
   
            // Handle Ctrl C press to copy message from CustomMessageBox
            KeyDown  = (sender, args) =>
            {
                if (Keyboard.IsKeyDown(Key.LeftCtrl) && Keyboard.IsKeyDown(Key.C))
                    Clipboard.SetText(CMBMessage.Text);
            };

            // Set message
            CMBMessage.Text = message;
            // Set caption
            CMBCaption.Text = caption;

            // Setup Buttons (depending on specified CustomMessageBoxButtons value)
            // As StackPanel FlowDirection set as RightToLeft - we should add items in reverse
            switch (cmbButtons)
            {
                case CustomMessageBoxButtons.OKOnly:
                    _ = CMBButtons.Children.Add(OK);
                    break;
                case CustomMessageBoxButtons.OKCancel:
                    _ = CMBButtons.Children.Add(Cancel);
                    _ = CMBButtons.Children.Add(OK);
                    break;
                case CustomMessageBoxButtons.YesNo:
                    _ = CMBButtons.Children.Add(No);
                    _ = CMBButtons.Children.Add(Yes);
                    break;
                case CustomMessageBoxButtons.YesNoCancel:
                    _ = CMBButtons.Children.Add(Cancel);
                    _ = CMBButtons.Children.Add(No);
                    _ = CMBButtons.Children.Add(Yes);
                    break;  
                // Add another if you wish                 
                default:
                    _ = CMBButtons.Children.Add(OK);
                    break;
            }

            // Set icon (depending on specified CustomMessageBoxIcon value)
            // From C# 8.0 could be converted to switch-expression
            switch (cmbIcon)
            {
                case CustomMessageBoxIcon.Information:
                    CMBIcon.Source = FromSystemIcon(SystemIcons.Information);
                    break;
                case CustomMessageBoxIcon.Warning:
                    CMBIcon.Source = FromSystemIcon(SystemIcons.Warning);
                    break;
                case CustomMessageBoxIcon.Question:
                    CMBIcon.Source = FromSystemIcon(SystemIcons.Question);
                    break;
                case CustomMessageBoxIcon.Error:
                    CMBIcon.Source = FromSystemIcon(SystemIcons.Error);
                    break;
                case CustomMessageBoxIcon.None:
                default:
                    CMBIcon.Source = null;
                    break;
            }
        }


        // Show methods create new instance of CustomMessageBox window and shows it as Dialog (blocking thread)

        // Shows CustomMessageBox with specified message and default "OK" button
        public static CustomMessageBoxResult Show(string message)
        {
            _ = new CustomMessageBox(message).ShowDialog();
            return result;
        }

        // Shows CustomMessageBox with specified message, caption and default "OK" button
        public static CustomMessageBoxResult Show(string message, string caption)
        {
            _ = new CustomMessageBox(message, caption).ShowDialog();
            return result;
        }

        // Shows CustomMessageBox with specified message, caption and button(s)
        public static CustomMessageBoxResult Show(string message, string caption, CustomMessageBoxButtons cmbButtons)
        {
            _ = new CustomMessageBox(message, caption, cmbButtons).ShowDialog();
            return result;
        }

        // Shows CustomMessageBox with specified message, caption, button(s) and icon.
        public static CustomMessageBoxResult Show(string message, string caption, CustomMessageBoxButtons cmbButtons, CustomMessageBoxIcon cmbIcon)
        {
            _ = new CustomMessageBox(message, caption, cmbButtons, cmbIcon).ShowDialog();
            return result;
        }


        // Defines button(s), which should be displayed
        public enum CustomMessageBoxButtons
        {
            // Displays only "OK" button
            OKOnly,
            // Displays "OK" and "Cancel" buttons
            OKCancel,
            // Displays "Yes" and "No" buttons
            YesNo,
            // Displays "Yes", "No" and "Cancel" buttons
            YesNoCancel,
            // Add another if you wish
        }

        // Defines icon, which should be displayed
        public enum CustomMessageBoxIcon
        {       
            None,
            Question,
            Information,
            Warning,
            Error         
        }

        // Defines button, pressed by user as result
        public enum CustomMessageBoxResult
        {
            OK,
            Cancel,
            Yes,
            No
            // Add another if you wish
        }


        // Returns simple Button with pre-defined properties
        private static Button GetDefaultButton() => new Button
        {
            Width = 72,
            Height = 28,
            Margin = new Thickness(0, 4, 6, 4),
            Background = Brushes.White,
            BorderBrush = Brushes.DarkGray,
            Foreground = Brushes.Black
        };

        // Converts system icons (like in original message box) to BitmapSource to be able to set it to Source property of Image control 
        private static BitmapSource FromSystemIcon(Icon icon) =>
            Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());

        // Handler on CustomMessageBox caption-header to allow move window while left button pressed on it
        private void OnCaptionPress(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
                DragMove();
        }
    }
}

Remarks.

First of all, to simulate original MessageBox and call it only with CustomMessageBox().Show() (not with new CustomMessageBox().Show() as regular window) you should hide window constructor by making it private.

Enums CustomMessageBoxButtons, CustomMessageBoxIcon and CustomMessageBoxResult are replacements for MessageBoxButton, MessageBoxIcon and MessageBoxResult enums from original MessageBox.

Each Button stored as private property in CustomMessageBox class. On Buttons added to StackPanel on switch statement. They added in order to lay "from right to left" (so first added button will be at most right, next - left of first etc.).

GetDefaultButton method returns, as commented, simple button with pre-defined properties. You can customize it in any way, using gradients, styles, magic - whatever. Even you can remove it and set different style for each Button in its property (in private Button OK, private Button Cancel I mean) to make OK button green, Cancel button red, Yes button pink etc. GetDefaultButton may be rewrited to some kind of common GetButton if you want one method to create any button with specified text and click action as arguments:

private static Button GetButton(string buttonText, RoutedEventHandler clickAction)
{
    Button button = new Button
    {
            Width = 72,
            Height = 28
           // and other
    };
    button.Content = buttonText;
    button.Click  = clickAction;
    return button;
}

// In switch statement, when adding buttons to StackPanel, you create and add it instantly
private CustomMessageBox(...)
{
    InitializeComponent();
    // ...
    switch (cmbButtons)
    {
        case CustomMessageBoxButtons.OKOnly:
            _ = CMBButtons.Children.Add(GetButton("OK", delegate 
                                                        { 
                                                            result = CustomMessageBoxResult.OK;
                                                            Close(); 
                                                        }));
            break;
        // ...
    }
}

private static BitmapSource FromSystemIcon methods, again, as commented, uses default MessageBox icon (or if correct, System Icon) to convert it to BitmapSource. It is needed, because you can't set System Icon to default WPF Image control as Source. If you want to use own icons/images you can remove it and rewrite switch statement where icons set to CMBIcon.Source with paths (URIs) to your own icons/images.

private void OnCaptionPress is handler from TextBlock which stores Caption (or is Header) of CustomMessageBox. Because in XAML WindowStyle property setted to "None" - we can't move window as it is borderless, so this handler uses header to move window until left mouse button pressed on it.

This CustomMessageBox doesn't have Close (x) button. I don't add it because each dialog button has Close() call at click action. You can add if you wish, but this have no sense.

I set window Width property to 500px, so it stretches only by heigth. You also can change it, even make resizable or return WindowStyle to default value (with minimize, maximize and close buttons).

I've also added different "Caption" TextBlock background color in switch statement where icon sets to make MessageBox be better perceived "by eye", but code is huge enough so I removed it from example.

And finally.

To add your Yes to all, No to all or other buttons, on this example, I placed comments // Add another if you wish in places, where you should add new ones:

  1. Add YesToAll, NoToAll or other entries to CustomMessageBoxButtons enum to be able specify it when call, for example, as CustomMessageBox.Show(..., ..., CustomMessageBoxButtons.YesToAllNoToAll).
  2. Add also entries to CustomMessageBoxResult enum to be able return them as result.
  3. Add new Button property, named YesToAll, NoToAll or anyway you need. Set a .Content property with text, that should be displayed on this button ("Yes to all", "No to all" etc.). Add .Click handler, in which set result field with CustomMessageBoxResult enum value of this button and put Close() method to call window close.

And usage is simple, as original MessageBox:

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    var result1 = CustomMessageBox.Show("Some message");
    var result2 = CustomMessageBox.Show("Some message", "My caption");
    var result3 = CustomMessageBox.Show("Some message", "My caption", CustomMessageBoxButtons.OKOnly);
    var result4 = CustomMessageBox.Show("Some message", "My caption", CustomMessageBoxButtons.OKCancel, CustomMessageBoxIcon.Warning);

    // Do whatever with result
}

Few examples:

I specially removed Yes to all, No to all button from example to allow you try create it by yourself. You have, i think, detailed guidance now.

  • Related