SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

独自のMessageBoxを作る Part1

WPFでメッセージボックスを出したい場合、MessageBox.Showメソッドで表示することができます。
http://msdn.microsoft.com/ja-jp/library/system.windows.messagebox.aspx


しかし、このメッセージボックス、微妙に使い勝手が悪い。。。

  • 表示位置が常にディスプレイ中央
    • MessageBoxクラスで作成したメッセージボックスは、常に画面の中央に表示されます。
    • (マルチディスプレイの場合は、呼び出し元のウィンドウが表示されていたディスプレイ中央)
  • デザインの変更ができない
    • せっかくWPFでキレイな画面を作っていても、MessageBoxはいつものWindowsスタイルなデザインで表示されてしまいます。
  • ボタンの文字列が変えられない。
    • 「OK」「Cancel」などの文字列を、独自のものに変えられません。

ということで、カスタマイズ可能な独自のMessageBoxを作ってみました。
長くなりそうなので、何回かに分けてメモしときます。

カスタムコントロールとして独自のMessageBoxを作成

MessageBoxは、MessageBoxButton型の引数を渡すことで、表示するボタンの種類を変えられますが、
とりあえず今回は「OK」ボタンだけを備えたものを作ります。

カスタムコントロールとして以下のようなものを作りました。

CustomMessageBox.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CustomControls
{
    /// <summary>
    /// このカスタム コントロールを XAML ファイルで使用するには、手順 1a または 1b の後、手順 2 に従います。
    ///
    /// 手順 1a) 現在のプロジェクトに存在する XAML ファイルでこのカスタム コントロールを使用する場合
    /// この XmlNamespace 属性を使用場所であるマークアップ ファイルのルート要素に
    /// 追加します:
    ///
    ///     xmlns:MyNamespace="clr-namespace:CustomControls"
    ///
    ///
    /// 手順 1b) 異なるプロジェクトに存在する XAML ファイルでこのカスタム コントロールを使用する場合
    /// この XmlNamespace 属性を使用場所であるマークアップ ファイルのルート要素に
    /// 追加します:
    ///
    ///     xmlns:MyNamespace="clr-namespace:CustomControls;assembly=CustomControls"
    ///
    /// また、XAML ファイルのあるプロジェクトからこのプロジェクトへのプロジェクト参照を追加し、
    /// リビルドして、コンパイル エラーを防ぐ必要があります:
    ///
    ///     ソリューション エクスプローラーで対象のプロジェクトを右クリックし、
    ///     [参照の追加] の [プロジェクト] を選択してから、このプロジェクトを参照し、選択します。
    ///
    ///
    /// 手順 2)
    /// コントロールを XAML ファイルで使用します。
    ///
    ///     <MyNamespace:CustomMessageBox/>
    ///
    /// </summary>
    public class CustomMessageBox : Window
    {
        static CustomMessageBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomMessageBox), new FrameworkPropertyMetadata(typeof(CustomMessageBox)));
        }

        #region Dependency Properties
        /// <summary>
        /// メッセージボックス内に表示するテキストを取得または設定します。
        /// </summary>
        public string Message
        {
            get { return (string)GetValue(MessageProperty); }
            set { SetValue(MessageProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Message.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MessageProperty =
            DependencyProperty.Register("Message", typeof(string), typeof(CustomMessageBox), new PropertyMetadata(string.Empty));

        /// <summary>
        /// メッセージボックスの結果を取得します。
        /// </summary>
        public MessageBoxResult Result
        {
            get { return (MessageBoxResult)GetValue(ResultProperty); }
            protected set { SetValue(ResultProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Result.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ResultProperty =
            DependencyProperty.Register("Result", typeof(MessageBoxResult), typeof(CustomMessageBox), new PropertyMetadata(MessageBoxResult.None));

        /// <summary>
        /// ボタン領域背景部分のGridのスタイルを取得または設定します。
        /// </summary>
        public Style ButtonGridStyle
        {
            get { return (Style)GetValue(ButtonGridStyleProperty); }
            set { SetValue(ButtonGridStyleProperty, value); }
        }
        // Using a DependencyProperty as the backing store for ButtonGridStyle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ButtonGridStyleProperty =
            DependencyProperty.Register("ButtonGridStyle", typeof(Style), typeof(CustomMessageBox), new PropertyMetadata(null));

        /// <summary>
        /// OKボタンのスタイルを取得または設定します。
        /// </summary>
        public Style OkButtonStyle
        {
            get { return (Style)GetValue(OkButtonStyleProperty); }
            set { SetValue(OkButtonStyleProperty, value); }
        }
        // Using a DependencyProperty as the backing store for OkButtonStyle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OkButtonStyleProperty =
            DependencyProperty.Register("OkButtonStyle", typeof(Style), typeof(CustomMessageBox), new PropertyMetadata(null));
        #endregion

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);

            // ウィンドウ左上のアイコンを消す
            IconHelper.RemoveIcon(this);
            // ボタンの押下イベントを登録
            this.AddHandler(System.Windows.Controls.Primitives.ButtonBase.ClickEvent, new RoutedEventHandler(this.Button_Click));
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Button button = e.OriginalSource as Button;
            if (button == null)
                return;

            // OKボタンしか用意していないので、
            // メッセージボックスの結果は固定値を返す
            this.Result = MessageBoxResult.OK;
            this.DialogResult = true;
        }

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);
            if (this.Result == MessageBoxResult.None)
            {
                // メッセージボックスの「閉じる」ボタンが押された場合の処理
                this.Result = MessageBoxResult.OK;
            }
        }
    }

    internal static class IconHelper
    {
        [DllImport("user32.dll")]
        static extern int GetWindowLong(IntPtr hwnd, int index);

        [DllImport("user32.dll")]
        static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

        [DllImport("user32.dll")]
        static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter, int x, int y, int width, int height, uint flags);

        [DllImport("user32.dll")]
        static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);

        const int GWL_EXSTYLE = -20;
        const int WS_EX_DLGMODALFRAME = 0x0001;
        const int SWP_NOSIZE = 0x0001;
        const int SWP_NOMOVE = 0x0002;
        const int SWP_NOZORDER = 0x0004;
        const int SWP_FRAMECHANGED = 0x0020;
        const uint WM_SETICON = 0x0080;

        public static void RemoveIcon(Window window)
        {
            IntPtr hwnd = new WindowInteropHelper(window).Handle;

            int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
            SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_DLGMODALFRAME);

            SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
        }
    }
}
Generic.xaml
<?xml version="1.0" encoding="Shift_JIS"?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomControls">
    <Style TargetType="{x:Type local:CustomMessageBox}">
        <Setter Property="Background" Value=" White" />
        <Setter Property="MinWidth" Value="154" />
        <Setter Property="MinHeight" Value="140" />
        <Setter Property="ResizeMode" Value="NoResize" />
        <Setter Property="ShowInTaskbar" Value="False" />
        <Setter Property="SizeToContent" Value="WidthAndHeight" />

        <Setter Property="ButtonGridStyle">
            <Setter.Value>
                <Style TargetType="{x:Type Grid}">
                    <Setter Property="Background" Value="#F0F0F0" />
                </Style>
            </Setter.Value>
        </Setter>

        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CustomMessageBox}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition Height="48" />
                            </Grid.RowDefinitions>
                            <!--  メッセージ本文  -->
                            <TextBlock Margin="12,26,39,27"
                                       HorizontalAlignment="Left"
                                       VerticalAlignment="Top"
                                       Text="{TemplateBinding Message}"
                                       TextOptions.TextFormattingMode="Display">
                                <TextBlock.Style>
                                    <Style TargetType="{x:Type TextBlock}">
                                        <!--  メッセージが空の時は、TextBlockを非表示にする  -->
                                        <Style.Triggers>
                                            <Trigger Property="Text" Value="{x:Null}">
                                                <Setter Property="Visibility" Value="Collapsed" />
                                            </Trigger>
                                            <Trigger Property="Text" Value="">
                                                <Setter Property="Visibility" Value="Collapsed" />
                                            </Trigger>
                                        </Style.Triggers>
                                    </Style>
                                </TextBlock.Style>
                            </TextBlock>

                            <!--  メッセージボックス下部のボタン領域  -->
                            <Grid Grid.Row="1" Style="{TemplateBinding ButtonGridStyle}">
                                <Button MinWidth="88"
                                        Margin="8"
                                        HorizontalAlignment="Right"
                                        VerticalAlignment="Center"
                                        Content="OK"
                                        Padding="3"
                                        Style="{TemplateBinding OkButtonStyle}" />
                            </Grid>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

WPFのWindowを表示すると、ウィンドウ左上にアプリのアイコンが表示されてしまいます。
そこで、以下を参考にして、アイコンを消すようにしました。
http://www.wpftutorial.net/RemoveIcon.html

使ってみる

ここで作成した、カスタムメッセージボックスは、以下のようにして表示することができます。

var msg = new CustomMessageBox()
{
    Owner = this,
    Title = caption,
    Message = messageBoxText,
    WindowStartupLocation = System.Windows.WindowStartupLocation.CenterOwner,
};
msg.ShowDialog();
var result = msg.Result;

でもこれでは、今までのMessageBoxのように呼べなくて今一つ。。。
さらに、WindowクラスのShowDialogメソッドは戻り値がbool型なので、
通常のメッセージボックスように色んな戻り値を返すことができません。


ということで、もっと通常のメッセージボックスっぽく使えるようにカスタマイズしてみます。
静的メソッドで表示するようにします。

        #region メッセージボックス表示用のstaticメソッド
        public static MessageBoxResult Show(Window owner, string messageBoxText)
        {
            return CustomMessageBox.Show(owner, messageBoxText, string.Empty);
        }

        public static MessageBoxResult Show(Window owner, string messageBoxText, string caption)
        {
            var msg = new CustomMessageBox()
            {
                Owner = Window.GetWindow(owner),
                Title = caption,
                Message = messageBoxText,
                WindowStartupLocation = System.Windows.WindowStartupLocation.CenterOwner,
            };
            msg.ShowDialog();

            return msg.Result;
        }
        #endregion


これで、以下のように呼び出せるようになりました。

var result = CustomMessageBox.Show(this, "メッセージボックスのテスト\nサンプル", "タイトル");

オーナーとなるWindowを省略することができないのがちょっと残念だけど、だいぶスッキリ書けるようになりました。

結果画像

通常のMessageBoxと、今回作成したCustomMessageBoxの結果を比較してみました。

通常のMessageBox

カスタマイズ可能にしたメッセージボックス

CustomMessageBoxは、WPFで描画しているので、ボタンがWin8のAero2スタイルになっています。
また、WindowStartupLocationプロパティをCenterOwnerにしているので、呼び出し元の画面中央に表示されます。

表示内容のカスタマイズ

カスタムコントロールとして作成しているので、このCustomMessageBoxを利用する際に、デザインをカスタマイズして表示することもできます。
App.xamlに以下のようにCustomMessageBox型をターゲットにしたスタイルを定義することで、デザインのカスタマイズができます。

App.xaml
<Application x:Class="CustomMessageBoxSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ctrl="clr-namespace:CustomControls;assembly=CustomControls"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <Style TargetType="{x:Type ctrl:CustomMessageBox}">
            <Setter Property="Background">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0,1">
                        <GradientStop Color="#FFAEC9F7"/>
                        <GradientStop Color="LightGray" Offset="1"/>
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="ButtonGridStyle">
                <Setter.Value>
                    <Style TargetType="{x:Type Grid}">
                        <Setter Property="Background" Value="Transparent" />
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="OkButtonStyle">
                <Setter.Value>
                    <Style TargetType="{x:Type Button}">
                        <Setter Property="Height" Value="30" />
                        <Setter Property="Foreground" Value="White" />
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type Button}">
                                    <Border CornerRadius="5"
                                            BorderThickness="2"
                                            Background="#AA000000"
                                            BorderBrush="#AAFFFFFF">
                                        <ContentPresenter VerticalAlignment="Center"
                                                          HorizontalAlignment="Center"/>
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>
</Application>
問題点

Window派生クラスのCustomMessageBoxをターゲットとしたStyle定義をしているのに、
上記StyleをApp.xamlに定義すると、VSのデザイナ上ではMainWindowなどにも同様のスタイルが適用されてしまいました。
もちろん実行時にはMainWindowには影響ないのですが、、、
VSのデザイナのバグなのかなぁ・・・??