読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

システムメニューを操作するビヘイビア

Tips WPF

WPFはUI要素に関する操作は恐ろしいほどに柔軟です。(意味があるかは別として) コンボボックスの項目に動画を流すことすら簡単にできてしまいます。ですが、ウィンドウに関する操作はWindows Formsの方が楽だったなと思う場面がいくつもあります。そう感じるもののひとつにシステムメニューが挙げられます。ということで、システムメニューの操作を少し楽にしてみます。

システムメニューを操作するビヘイビア

WPF環境でシステムメニューを操作しようと思ったら、(私が知る範囲では) P/InvokeでWin32 APIを直接叩くしか方法がありません。システムメニューの操作はウィンドウのスタイルを変更することで行いますので、まずそれに必要なAPIの準備をします。

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

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

次に必要な定数を定義します。これはもうWin32 APIのリファレンスを参考にするしかないので、割り切って何も考えずに定義します。

public class Constant
{
    //--- GetWindowLong
    public const int GWL_STYLE          = -16;
    public const int GWL_EXSTYLE        = -20;
    //--- Window Style
    public const int WS_EX_CONTEXTHELP  = 0x00400;
    public const int WS_MAXIMIZEBOX     = 0x10000;
    public const int WS_MINIMIZEBOX     = 0x20000;
    public const int WS_SYSMENU         = 0x80000;
    //--- Window Message
    public const int WM_SYSKEYDOWN      = 0x0104;
    public const int WM_SYSCOMMAND      = 0x0112;
    //--- System Command
    public const int SC_CONTEXTHELP     = 0xF180;
    //--- Virtual Keyboard
    public const int VK_F4              = 0x73;
    //--- Constructor
    private Constant(){}
}

最後に本命のビヘイビアを作ります。機能を依存関係プロパティとして提供することでバインディングによる動的な変更に耐えられるようにします。

public class SystemMenuBehavior : Behavior<Window>
{
    #region Properties
    //--- 表示/非表示
    public bool? IsVisible
    {
        get{ return (bool?)this.GetValue(This.IsVisibleProperty); }
        set{ this.SetValue(This.IsVisibleProperty, value); }
    }
    public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register("IsVisible", typeof(bool?), typeof(This), new PropertyMetadata(null, This.OnPropertyChanged));


    //--- 最小化
    public bool? CanMinimize
    {
        get{ return (bool?)this.GetValue(This.CanMinimizeProperty); }
        set{ this.SetValue(This.CanMinimizeProperty, value); }
    }
    public static readonly DependencyProperty CanMinimizeProperty = DependencyProperty.Register("CanMinimize", typeof(bool?), typeof(This), new PropertyMetadata(null, This.OnPropertyChanged));


    //--- 最大化
    public bool? CanMaximize
    {
        get{ return (bool?)this.GetValue(This.CanMaximizeProperty); }
        set{ this.SetValue(This.CanMaximizeProperty, value); }
    }
    public static readonly DependencyProperty CanMaximizeProperty = DependencyProperty.Register("CanMaximize", typeof(bool?), typeof(This), new PropertyMetadata(null, This.OnPropertyChanged));


    //--- ヘルプボタン
    public bool? ShowContextHelp
    {
        get{ return (bool?)this.GetValue(This.ShowContextHelpProperty); }
        set{ this.SetValue(This.ShowContextHelpProperty, value); }
    }
    public static readonly DependencyProperty ShowContextHelpProperty = DependencyProperty.Register("ShowContextHelp", typeof(bool?), typeof(This), new PropertyMetadata(null, This.OnPropertyChanged));


    //--- Alt + F4の有効化/無効化
    public bool EnableAltF4
    {
        get{ return (bool)this.GetValue(This.EnableAltF4Property); }
        set{ this.SetValue(This.EnableAltF4Property, value); }
    }
    public static readonly DependencyProperty EnableAltF4Property = DependencyProperty.Register("EnableAltF4", typeof(bool), typeof(This), new PropertyMetadata(true));
    #endregion


    #region Events
    //--- ヘルプボタンをクリックしたときに発生するイベント
    public event EventHandler ContextHelpClick = null;
    #endregion


    #region Overrides
    protected override void OnAttached()
    {
        this.AssociatedObject.SourceInitialized += this.OnSourceInitialized;
        base.OnAttached();
    }


    protected override void OnDetaching()
    {
        var source = (HwndSource)HwndSource.FromVisual(this.AssociatedObject);
        source.RemoveHook(this.HookProcedure);
        this.AssociatedObject.SourceInitialized -= this.OnSourceInitialized;
        base.OnDetaching();
    }
    #endregion


    #region Event Handlers
    //--- プロパティが変更されたら表示更新
    private static void OnPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var self = obj as SystemMenuBehavior;
        if (self != null)
            self.Apply();
    }

    //--- Windowハンドルを取得できるようになったタイミングで初期化
    private void OnSourceInitialized(object sender, EventArgs e)
    {
        this.Apply();
        var source = (HwndSource)HwndSource.FromVisual(this.AssociatedObject);
        source.AddHook(this.HookProcedure);  //--- メッセージフック
    }

    //--- メッセージ処理
    private IntPtr HookProcedure(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        //--- コンテキストヘルプをクリックした
        if (msg == Constant.WM_SYSCOMMAND)
        if (wParam.ToInt32() == Constant.SC_CONTEXTHELP)
        {
            handled = true;
            var handler = this.ContextHelpClick;
            if (handler != null)
                handler(this.AssociatedObject, EventArgs.Empty);
        }

        //--- Alt + F4を無効化
        if (!this.EnableAltF4)
        if (msg == Constant.WM_SYSKEYDOWN)
        if (wParam.ToInt32() == Constant.VK_F4)
            handled = true;

        //--- ok
        return IntPtr.Zero;
    }
    #endregion


    #region Methods
    //--- 設定を反映
    private void Apply()
    {
        if (this.AssociatedObject == null)
            return;

        //--- スタイル
        var hwnd  = new WindowInteropHelper(this.AssociatedObject).Handle;
        var style = User32.GetWindowLong(hwnd, Constant.GWL_STYLE);
        if (this.IsVisible.HasValue)
        {
            if (this.IsVisible.Value)    style |=  Constant.WS_SYSMENU;
            else                         style &= ~Constant.WS_SYSMENU;
        }
        if (this.CanMinimize.HasValue)
        {
            if (this.CanMinimize.Value)  style |=  Constant.WS_MINIMIZEBOX;
            else                         style &= ~Constant.WS_MINIMIZEBOX;
        }
        if (this.CanMaximize.HasValue)
        {
            if (this.CanMaximize.Value)  style |=  Constant.WS_MAXIMIZEBOX;
            else                         style &= ~Constant.WS_MAXIMIZEBOX;
        }
        User32.SetWindowLong(hwnd, Constant.GWL_STYLE, style);

        //--- 拡張スタイル
        var exStyle = User32.GetWindowLong(hwnd, Constant.GWL_EXSTYLE);
        if (this.ShowContextHelp.HasValue)
        {
            if (this.ShowContextHelp.Value)  exStyle |=  Constant.WS_EX_CONTEXTHELP;
            else                             exStyle &= ~Constant.WS_EX_CONTEXTHELP;
        }
        User32.SetWindowLong(hwnd, Constant.GWL_EXSTYLE, exStyle);
    }
    #endregion
}

ヒッソリと基底クラスになっているBehavior<T>クラスを利用するにはSystem.Windows.Interactivity.dllの参照が必要です。これは.NET Framework標準のdllではないのでNuGetから取得してください。(一体いつになったら標準ライブラリの仲間入りするのでしょうか...)

System.Windows.Interactivity v4.0 for WPF

システムメニューを非表示にする

IsVisibleプロパティを利用すれば、システムメニューの表示/非表示を簡単に制御できます。例えば、ウィザードの最後にファイル出力をしている場合などでは進捗画面を出したりすると思いますが、そのようなウィンドウを消されては困るときに便利です。

HideSystemMenu

また、せっかくシステムメニューを非表示にして閉じるボタンを消してもAlt + F4でウィンドウ消されては困ります。なので、Alt + F4も無効化できるようにしました。

<Window x:Class="SampleApplication.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:b="clr-namespace:SampleApplication.Behaviors"
        xmlns:c="clr-namespace:SampleApplication.Converters"
        Title="Sample" Height="500" Width="500">
    <Window.Resources>
        <!-- 論理値を反転 -->
        <c:InverseBooleanConverter x:Key="InverseBooleanConverter" />
    </Window.Resources>

    <i:Interaction.Behaviors>
        <!-- 作業中はメニューを非表示にし、Alt + F4を無効化する -->
        <b:SystemMenuBehavior IsVisible="{Binding IsBusy, Converter={StaticResource InverseBooleanConverter}}"
                              EnableAltF4="{Binding IsBusy, Converter={StaticResource InverseBooleanConverter}}"/>
    </i:Interaction.Behaviors>
</Window>

最大化/最小化ボタンを無効化する

「閉じるボタンのみのサイズ可変ウィンドウ」のニーズというのは結構よくあるのではないかと思います。WPFでその辺りを変更しようと思ったらWindow.ResizeModeプロパティを利用します。これにNoResizeを設定すれば閉じるボタンのみにはなりますが、(当然)サイズは固定になります。CanResizeを指定するとウィンドウは可変になりますが、一緒に最大化/最小化ボタンが表示されてしまいます。このように、WPFだと標準では実現できません (Windows Formsだとプロパティ設定だけで簡単にできるというのに...)。しかしこのビヘイビアを利用すればそれを簡単に実現できます。個人的にはコレが最も利用頻度が高いと思いますし、実はコレがしたくて作りました。

<Window x:Class="SampleApplication.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:b="clr-namespace:SystemMenuSample.Behaviors"
        Title="Sample"
        ResizeMode="CanResize">
    <i:Interaction.Behaviors>
        <b:SystemMenuBehavior CanMinimize="False" CanMaximize="False" />
    </i:Interaction.Behaviors>
</Window>

ヘルプボタンを表示する

ほんと滅多に使わないのですが、システムメニューには以下のようにヘルプボタンを表示することができます。かなりオマケですが、これを実現する機能も付けてみました。ヘルプボタンがクリックされたときのイベントも提供しています。

ContextHelp

<Window x:Class="SampleApplication.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:b="clr-namespace:SystemMenuSample.Behaviors"
        Title="Sample">
    <i:Interaction.Behaviors>
        <b:SystemMenuBehavior CanMinimize="False" CanMaximize="False" ShowContextHelp="True"
                              ContextHelpClick="OnContextHelpClick" />
    </i:Interaction.Behaviors>
</Window>
private void OnContextHelpClick(object sender, EventArgs e)
{
    MessageBox.Show("OnContextHelpClick");
}

でもヘルプボタンは...たぶん使わないでしょう...。

サンプル

今回のソースコードとサンプルアプリケーションを以下に置いてあります。ご自由にご利用ください。