しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

WPF で WindowChrome を使った時のシステムメニューを消す

WPF アプリケーションを書いていて、ユーザーからは閉じることが出来ない Window を作る必要があったのですぐ出来ると調べていたら、思った以上にはまったのでメモとして残します。

Windows では Window を閉じるための方法がいくつか存在していますが、以下のようにタイトルバーを右クリックで出てくるシステムメニューをなかなか消せませんでした。

f:id:shiba-yan:20200904184553p:plain

とは言え通常の Window であれば P/Invoke を使って WS_SYSMENU をウィンドウスタイルから外せば出てこなくなります。Win32 のドキュメントが少しあったので引っ張ってきました。

P/Invoke と GWL / SWL の説明は不要だと思うのでサンプルコードだけ載せておきます。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

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

        var hwnd = new WindowInteropHelper(this).Handle;

        var style = GetWindowLong(hwnd, GWL_STYLE);
        SetWindowLong(hwnd, GWL_STYLE, style & ~WS_SYSMENU);
    }
}

これを実行すると Window からアイコンやボタンと共にシステムメニューも消えます。Alt+Space でもシステムメニューは出なくなるので、後は Alt+F4 を Hook で潰せば閉じれなくなります。

f:id:shiba-yan:20200904184610p:plain

通常の Window であればこれで良いのですが、独自デザインの Window を作成するために WindowChrome を使っている場合にはこれで済みませんでした。

WindowChrome の説明はドキュメントがちゃんと書かれているので、こっちを読んでおいてもらった方が良いです。要するに Non-client な領域まで Window を広げるやつです。

サンプルコードも一応載せておきます。適当に Attached Property で WindowChrome を追加すれば、あっという間にそれっぽい Window が完成します。

<Window x:Class="WpfApp3.MainWindow"
        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"
        mc:Ignorable="d" x:Name="window"
        Title="MainWindow" Height="450" Width="800">
    <WindowChrome.WindowChrome>
        <WindowChrome CaptionHeight="44"
                      GlassFrameThickness="1"
                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}"
                      UseAeroCaptionButtons="False" />
    </WindowChrome.WindowChrome>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="44" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBlock Text="{Binding Title, ElementName=window}" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="0" FontSize="14" />
    </Grid>
</Window>

割とめんどくさい NCA 周りをいい感じに処理してくれるので便利ですが、タイトルバーに相当する部分を右クリックするとシステムメニューが復活しています。WS_SYSMENU を外していますが無関係のようです。

f:id:shiba-yan:20200904185032p:plain

だからと言って WS_SYSMENU に意味がないわけではなく、Alt+Space は動かないので右クリックでのメニューのみ生きているようです。

一般的な Win32 レベルの方法では消すことは出来なかったので、仕方なくソースを辿ってこの辺りの実装を調べてみると、NCA への右クリックに対するハンドラーが実装されていました。

知らなかったのですが SystemCommands を使うと任意の場所にシステムメニューを表示できるようです。

これを使って独自にメッセージを処理してシステムメニューを表示していたので、Win32 レベルの方法では消すことが出来なかったというわけです。ちなみに消すオプションなどはありません。

internal な WindowChromeWorker 内での処理だったので正攻法では手を出せなかったため、リフレクションを使って WM_NCRBUTTONUP のハンドラーを削除することにしました。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

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

        var hwnd = new WindowInteropHelper(this).Handle;

        var style = GetWindowLong(hwnd, GWL_STYLE);
        SetWindowLong(hwnd, GWL_STYLE, style & ~WS_SYSMENU);

        var type = typeof(WindowChrome).Assembly.GetType("System.Windows.Shell.WindowChromeWorker");
        var method = type.GetMethod("GetWindowChromeWorker", BindingFlags.Public | BindingFlags.Static);
        var field = type.GetField("_messageTable", BindingFlags.Instance | BindingFlags.NonPublic);

        var windowChromeWorker = (DependencyObject)method.Invoke(null, new object[] { this });
        var messageTable = (IList)field.GetValue(windowChromeWorker);

        messageTable.RemoveAt(5);
    }
}

private なフィールドを弄る系のリフレクションは、名前が変わった瞬間に動かなくなるので避けたいところですが、もう色々と決め打ちで対応するしかないので諦めました。

コレクションも K-V が internal な型を参照しているので非 Generic で扱っています。

f:id:shiba-yan:20200904185238p:plain

邪悪なコードを書いてしまった感しかないですが、これで WM_NCRBUTTONUP は処理されなくなったため、タイトルバー領域を右クリックしてもシステムメニューが表示されなくなりました。

f:id:shiba-yan:20200904185610p:plain

久し振りに NCA を意識しましたが、やっぱり自分で書くものではないと思ったので、今後も WindowChrome で楽をしていきたい気持ちが高まりました。

システムメニューを出さないオプションは欲しいので暇な時に Issue を作成したいです。