WPF アプリケーションを書いていて、ユーザーからは閉じることが出来ない Window を作る必要があったのですぐ出来ると調べていたら、思った以上にはまったのでメモとして残します。
Windows では Window を閉じるための方法がいくつか存在していますが、以下のようにタイトルバーを右クリックで出てくるシステムメニューをなかなか消せませんでした。
とは言え通常の 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 で潰せば閉じれなくなります。
通常の 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
を外していますが無関係のようです。
だからと言って 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 で扱っています。
邪悪なコードを書いてしまった感しかないですが、これで WM_NCRBUTTONUP
は処理されなくなったため、タイトルバー領域を右クリックしてもシステムメニューが表示されなくなりました。
久し振りに NCA を意識しましたが、やっぱり自分で書くものではないと思ったので、今後も WindowChrome
で楽をしていきたい気持ちが高まりました。
システムメニューを出さないオプションは欲しいので暇な時に Issue を作成したいです。