タイトルバーで右クリックして出てくるコンテキストメニューから一部項目を消す(Winui3のタイトルバーを右クリックしたときのコンテキストメニューのバグ対応)

もくじ
https://tera1707.com/entry/2022/02/06/144447#WinUI3

やりたいこと

以前、WinUI3のウインドウを最大化できないようにしたくて、下記ページでOverlappedPresenterを使って実現した。 そこでは、最大化ボタンを無効表示にできていた。

https://tera1707.com/entry/2022/04/24/220519

また、その時、タイトルバーを右クリックしたときのコンテキストメニューでも、最大化が無効にできていた。

ただ、今回たまたま見つけた(職場のゴッドハンドが見つけてくれた)のだが、タイトルバーの、めっちゃ端っこを右クリックしてコンテキストメニューを出すと、なぜか最大化が有効になっていて、押すと実際に最大化されてしまう。

上の端スレスレを右クリックすると、下記のようになる...

なぜ?UWPの血を受け継いでアプリがコンテナ?に入って動くようになったときに、そのガワが何か悪さしてるのか? これでは最大化を防いだつもりが防げてないので、何とかしたい。

やり方

WinUI3のウインドウ関係でなんかかゆいときに手がとどかないってなったときは、Win32API直叩きだろう、と最近思ってるので、その辺でやり方探したところ、こちらの記事で、WPFでメニューの追加ができると知った。

なら無効化や削除もできるだろうということで、MSページを参考に試してみた。

サンプルコード

こういう流れでやってみた。

  • ウインドウハンドルを取得
  • OverlappedPresenterで最大化をできないようにする
    • これで、ウインドウの×ボタンの横の最大化ボタンは無効になる
    • タイトルバー(の真ん中あたり)を右クリックして出てくるコンテキストメニューの中の最大化ボタンも無効になる
    • が、タイトルバーの端っこギリギリを右クリックして出てくるコンテキストメニューの中の最大化ボタンは、無効にならない!
  • GetSystemMenu()関数で、システムメニュー(コンテキストメニュー)のハンドルをとる
  • DeleteMenu()関数で、最大化メニューを消す
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Runtime.InteropServices;

namespace MaximizeTestWinui3
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            
            // WinUI3のウインドウのハンドルを取得
            var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);
            Microsoft.UI.Windowing.AppWindow appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);

            // 標準タイプのウインドウ(OverlappedPresenter)に対して、最大化の無効設定を行う
            var op = OverlappedPresenter.Create();
            op.IsMaximizable = false;
            op.IsResizable = false;
            appWindow.SetPresenter(op);

            // Win32APIで、コンテキストメニューの最大化を削除する
            IntPtr systemMenuHandle = GetSystemMenu(hWnd, false);
            DeleteMenu(systemMenuHandle, (uint)4, (uint)(MF_BYPOSITION));
        }

        // Win32 API関連
        private readonly Int32 MF_BYPOSITION = 0x400;
        [DllImport("user32.dll")]
        private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
        [DllImport("user32.dll")]
        static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
    }
}

結果

上記コードを動かすと、下図のようになる。

コード中の、DeleteMenu(systemMenuHandle, (uint)4, (uint)(MF_BYPOSITION));の部分の「4」は、最大化の項目が、コンテキストメニュー内の上から0番始まりで数えて4番目だから。

注意点・メモなど

EnableMenuItem()関数で無効化ができなかった

MSのドキュメントによると、EnableMenuItem()という、メニュー項目の有効無効ができるAPIがある様子。

だが、試したところ、EnableMenuItem()では、メニューの無効化はできなかった。 WinUI3では、この関数はうまく動いてくれないのかもしれない。

DeleteMenu()関数でメニューの項目を削除することはできたので、仕方なくそっちで代用することにした。

サイズ変更も同じ現象が起きる

上のコードと結果の画面を見ると、コードでは最大化禁止のほかに、サイズ変更も禁止にしているのに、

端っこを右クリックしたときのコンテキストメニューでは、「サイズ変更」が押せてしまう。

どうもWinUI3に不具合があるっぽい。

「端っこ」は、Resizableなときにマウスポインタが「↕」の形に変化するあたりっぽい

色々試したところ、どうも掲題の通りっぽい。

Resizable = true でサイズ変更可能な時は、マウスポインタが矢印型になって右クリックできない場所が、今回、Resizable = false にして、サイズ変更を禁止したために右クリックできてしまって、そこがなんか変、ということっぽい。何かそこだけ特殊なことをしているのか。。

WPFでためすと、上端で右クリックしてもContextMenu出ない

WPFだと、上の端っこで右クリックしても、そもそもContextMenuが出てこなかった。

やっぱり、WinUI3のガワには、何かがいる?

追記・自分でInsertMenuしたメニュー項目は、EnableMenuItemで有効無効表示切替えれるっぽい

どうも、上でEnableMenuItemで有効無効を切り替えで出来なかったのは、元からある項目だったからっぽい。(最大化とか、最小化とか。)

試しに自分でInsertMenuして追加したメニュー項目だと、下図のようにEnableMenuItemで有効無効切り替えできた。

試したコード

using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using System;

namespace MaximizeTestWinui3
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            
            // WinUI3のウインドウのハンドルを取得
            var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);
            Microsoft.UI.Windowing.AppWindow appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);

            // 標準タイプのウインドウ(OverlappedPresenter)に対して、最大化の無効設定を行う
            var op = OverlappedPresenter.Create();
            op.IsMaximizable = false;
            op.IsResizable = false;
            appWindow.SetPresenter(op);

            IntPtr systemMenuHandle = GetSystemMenu(hWnd, false);

            for (int i = 0; i < 6; i++)
            {
                InsertMenu(systemMenuHandle, 0, (int)MF_BYPOSITION, ITEMONEID, "Item " + i);
            }

            for (int i = 0; i < 12; i++)
            {
                EnableMenuItem(systemMenuHandle, (uint)i, MF_BYPOSITION | ((i % 2 == 0) ? MF_GRAYED : 0));//1個飛ばしで無効にしてみる
            }
        }

        uint MF_BYCOMMAND = 0x00000000;
        uint MF_BYPOSITION = 0x00000400;
        uint MF_ENABLED = 0x00000000;
        uint MF_DISABLED = 0x00000002;
        uint MF_GRAYED = 0x00000001;
        uint MF_STRING = 0x00000000;

        private const Int32 ITEMONEID = 1000;

        [DllImport("user32.dll")]
        private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
        [DllImport("user32.dll")]
        private static extern bool InsertMenu(IntPtr hMenu, Int32 wPosition, Int32 wFlags, Int32 wIDNewItem, string lpNewItem);

        [DllImport("user32.dll")]
        static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
    }
}

追記:WM_INITMENUPOPUP が来たときにEnableMenuItem()で無効化できる

上で、EnableMenuItem()では、最大化のメニューを無効にできなかったと書いたが、Winuiのissueに上記を投げたところ、やり方あることを教えてもらった。

  • ウインドウプロシージャでWM_INITMENUPOPUP をひっかけて、
  • その中でEnableMenuItem()で、指定のitemを無効にする

ができるらしい。ので、下記でためした。

結果、うまくいった。
下記で、Workaroundとしてはつかえるかもしれない。(WAコード長いが....)

using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Runtime.InteropServices;

namespace MaximizeTestWinui3
{
    public sealed partial class MainWindow : Window
    {
        private NativeMethods.WinProc newWndProc = null;
        private IntPtr oldWndProc = IntPtr.Zero;

        public MainWindow()
        {
            this.InitializeComponent();

            var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            newWndProc = new NativeMethods.WinProc(NewWindowProc);
            oldWndProc = NativeMethods.SetWindowLongPtr64(hwnd, PInvoke.User32.WindowLongIndexFlags.GWL_WNDPROC, newWndProc);

            var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd);
            var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);
            ((OverlappedPresenter)appWindow.Presenter).IsMaximizable = false;
            ((OverlappedPresenter)appWindow.Presenter).IsMinimizable = true;
            ((OverlappedPresenter)appWindow.Presenter).IsResizable = false;
        }

        private IntPtr NewWindowProc(IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam)
        {
            switch (Msg)
            {
                case PInvoke.User32.WindowMessage.WM_INITMENUPOPUP:
                {
                    NativeMethods.EnableMenuItem(wParam, NativeMethods.SC_MAXIMIZE, NativeMethods.MF_BYCOMMAND | NativeMethods.MF_DISABLED | NativeMethods.MF_GRAYED); 
                    return IntPtr.Zero;
                }

            }
            return NativeMethods.CallWindowProc(oldWndProc, hWnd, Msg, wParam, lParam);
        }
    }

    public class NativeMethods
    {
        public delegate IntPtr WinProc(IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
        public static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, PInvoke.User32.WindowLongIndexFlags nIndex, WinProc dwNewLong);
        
        [DllImport("user32.dll")]
        public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, PInvoke.User32.WindowMessage Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);

        public const int MF_BYCOMMAND = 0x00000000;
        public const int MF_BYPOSITION = 0x00000400;
        public const int MF_ENABLED = 0x00000000;
        public const int MF_GRAYED = 0x00000001;
        public const int MF_DISABLED = 0x00000002;

        public const int SC_SIZE = 0xF000;
        public const int SC_MOVE = 0xF010;
        public const int SC_MINIMIZE = 0xF020;
        public const int SC_MAXIMIZE = 0xF030;
        public const int SC_NEXTWINDOW = 0xF040;
        public const int SC_PREVWINDOW = 0xF050;
        public const int SC_CLOSE = 0xF060;
        public const int SC_RESTORE = 0xF120;
    }
}

備考

初めてmiscrosoftのWinuiのリポジトリにissueを出した。 どうなるものか。。

https://github.com/microsoft/microsoft-ui-xaml/issues/8201

⇒上のWAを教えてもらった。投げてみるもんですね。

参考

MS公式 メニュー関連のAPI

https://learn.microsoft.com/ja-jp/windows/win32/menurc/menu-functions

WPFでのメニューへの項目追加

https://qiita.com/todu/items/a3d053f514a4fe36bf29

その他参考

http://wisdom.sakura.ne.jp/system/winapi/win32/win81.html