需求:停用/移除/新增視窗系統選單項目 (標題列右鍵選單)。本文提供完整 **P/Invoke 簽章封裝**、**核心 API 解析**、**實戰擴充方法** 與 **WPF 附加屬性** 整合範例。
internal static class NativeMethods
{
[DllImport("User32.dll")]
internal static extern IntPtr GetSystemMenu(IntPtr hWnd, int bRevert);
// bRevert=0: 取得系統選單控制碼副本
// bRevert=1: 重置系統選單為預設狀態,傳回 0
[DllImport("User32.dll")]
internal static extern int GetMenuItemCount(IntPtr hMenu);
[DllImport("User32.dll")]
internal static extern bool EnableMenuItem(IntPtr hMenu, int uIDEnableItem, int uEnable);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern int AppendMenu(IntPtr hMenu, int uFlags, int uIDNewItem, string lpNewItem);
[DllImport("user32.dll")]
internal static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
[DllImport("User32.dll")]
internal static extern int DrawMenuBar(IntPtr hWnd); // 修改後必須呼叫重繪
}
internal const uint MF_ENABLED = 0x00000000;
internal const uint MF_GRAYED = 0x00000001; // 停用並灰顯
internal const uint MF_DISABLED = 0x00000002; // 僅停用不灰顯
internal const uint MF_BYCOMMAND = 0x00000000; // uIDEnableItem 為命令 ID (SC_CLOSE 等)
internal const uint MF_BYPOSITION = 0x00000400; // uIDEnableItem 為 0-based 位置索引
internal const uint MF_STRING = 0x00000000; // 字串項目
internal const uint MF_SEPARATOR = 0x00000800; // 分隔線
internal const uint MF_POPUP = 0x00000010; // 子選單
| 位置 (0-based) | 項目 | 命令 ID | 常數 |
|---|---|---|---|
| 0 | 還原 | 0xF120 | SC_RESTORE |
| 1 | 移動 | 0xF010 | SC_MOVE |
| 2 | 大小 | 0xF000 | SC_SIZE |
| 3 | 最小化 | 0xF020 | SC_MINIMIZE |
| 4 | 最大化 | 0xF030 | SC_MAXIMIZE |
| 5 | 分隔線 | - | - |
| 6 | 關閉 | 0xF060 | SC_CLOSE |
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
public static class WindowSystemMenuExtensions
{
public static bool RemoveUnwantedSystemMenuItems(this Window window)
{
if (window == null) return false;
var helper = new WindowInteropHelper(window);
IntPtr hwnd = helper.Handle;
if (hwnd == IntPtr.Zero)
{
// Handle 尚未建立 (SourceInitialized 前),延遲處理
window.SourceInitialized += (s, e) => ((Window)s).RemoveUnwantedSystemMenuItems();
return false;
}
IntPtr sysMenu = NativeMethods.GetSystemMenu(hwnd, 0);
if (sysMenu == IntPtr.Zero) return false;
// 由大索引往小索引刪除,避免索引位移
bool success = true;
success &= NativeMethods.DeleteMenu(sysMenu, 6, NativeMethods.MF_BYPOSITION); // Close
success &= NativeMethods.DeleteMenu(sysMenu, 5, NativeMethods.MF_BYPOSITION); // Separator
success &= NativeMethods.DeleteMenu(sysMenu, 4, NativeMethods.MF_BYPOSITION); // Maximize
success &= NativeMethods.DeleteMenu(sysMenu, 0, NativeMethods.MF_BYPOSITION); // Restore
NativeMethods.DrawMenuBar(hwnd); // 關鍵:重繪標題列
return success;
}
public static bool DisableCloseButton(this Window window)
{
var helper = new WindowInteropHelper(window);
IntPtr hwnd = helper.Handle;
if (hwnd == IntPtr.Zero) return false;
IntPtr sysMenu = NativeMethods.GetSystemMenu(hwnd, 0);
bool ok = NativeMethods.EnableMenuItem(sysMenu, (int)0xF060, (int)(NativeMethods.MF_BYCOMMAND | NativeMethods.MF_GRAYED));
NativeMethods.DrawMenuBar(hwnd);
return ok;
}
public static bool AddCustomMenuItem(this Window window, string text, int commandId, Action onClick)
{
var helper = new WindowInteropHelper(window);
IntPtr hwnd = helper.Handle;
if (hwnd == IntPtr.Zero) return false;
IntPtr sysMenu = NativeMethods.GetSystemMenu(hwnd, 0);
// 追加分隔線 + 自訂項目
NativeMethods.AppendMenu(sysMenu, (int)(NativeMethods.MF_SEPARATOR | NativeMethods.MF_BYPOSITION), 0, null);
int result = NativeMethods.AppendMenu(sysMenu, (int)(NativeMethods.MF_STRING | NativeMethods.MF_BYPOSITION), commandId, text);
NativeMethods.DrawMenuBar(hwnd);
if (result == 0) return false;
// 掛鉤 WndProc 處理 WM_SYSCOMMAND
HwndSource source = HwndSource.FromHwnd(hwnd);
source.AddHook((msg, wparam, lparam, handled) =>
{
if (msg == 0x0112 /* WM_SYSCOMMAND */ && (int)wparam == commandId)
{
onClick?.Invoke();
handled = true;
}
return IntPtr.Zero;
});
return true;
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += (s, e) =>
{
this.RemoveUnwantedSystemMenuItems(); // 移除 還原/最大化/關閉/分隔線
// this.DisableCloseButton(); // 僅停用關閉按鈕
// this.AddCustomMenuItem("關於系統", 0xF100, () => MessageBox.Show("自訂選單!")); // 新增項目
};
}
}
WindowInteropHelper.Handle 在 SourceInitialized 事件後才有效,**Loaded 事件通常安全**,建構子中為 IntPtr.Zero。DeleteMenu 使用 MF_BYPOSITION 時,**必須從大索引刪至小索引**,否則後續索引錯位導致刪錯項目。DrawMenuBar(hwnd),否則標題列不會即時更新。commandId 建議使用 0xF100 以上範圍 (WM_SYSCOMMAND 保留區),避免與系統命令衝突。系統選單操作核心三步驟:**GetSystemMenu 取得控制碼 → DeleteMenu/EnableMenuItem/AppendMenu 修改 → DrawMenuBar 重繪**。封裝為 WPF 擴充方法並處理 Handle 生命週期,即可在純 managed 代碼中靈活控制標題列行為。記住:**索引由大刪至小、DrawMenuBar 不可漏、自訂命令 ID 避開系統保留區**。