首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

WPF/Win32 視窗系統選單 (System Menu) 完整操作指南:P/Invoke 封裝與實戰擴充方法

     編輯:關於.NET

需求:停用/移除/新增視窗系統選單項目 (標題列右鍵選單)。本文提供完整 **P/Invoke 簽章封裝**、**核心 API 解析**、**實戰擴充方法** 與 **WPF 附加屬性** 整合範例。

核心 Win32 API 簽章 (C# P/Invoke)


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; // 子選單

系統選單標準項目位置與命令 ID 對照

位置 (0-based)項目命令 ID常數
0還原0xF120SC_RESTORE
1移動0xF010SC_MOVE
2大小0xF000SC_SIZE
3最小化0xF020SC_MINIMIZE
4最大化0xF030SC_MAXIMIZE
5分隔線--
6關閉0xF060SC_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;
    }
}

WPF 使用範例


public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += (s, e) =>
        {
            this.RemoveUnwantedSystemMenuItems(); // 移除 還原/最大化/關閉/分隔線
            // this.DisableCloseButton();          // 僅停用關閉按鈕
            // this.AddCustomMenuItem("關於系統", 0xF100, () => MessageBox.Show("自訂選單!")); // 新增項目
        };
    }
}

注意事項與避坑指南

  1. Handle 時機:WindowInteropHelper.Handle 在 SourceInitialized 事件後才有效,**Loaded 事件通常安全**,建構子中為 IntPtr.Zero。
  2. 索引位移:DeleteMenu 使用 MF_BYPOSITION 時,**必須從大索引刪至小索引**,否則後續索引錯位導致刪錯項目。
  3. DrawMenuBar 必呼:任何修改後必須呼叫 DrawMenuBar(hwnd),否則標題列不會即時更新。
  4. 命令 ID 衝突:自訂項目 commandId 建議使用 0xF100 以上範圍 (WM_SYSCOMMAND 保留區),避免與系統命令衝突。
  5. 高 DPI / Per-Monitor DPI:WPF 視窗 Handle 在 DPI 變更時可能重建,**需在 SourceInitialized / DpiChanged 事件中重新套用系統選單修改**。
  6. 安全性:移除「關閉」項目不代表使用者無法關閉視窗 (Alt+F4、Task Manager 仍可關閉),**不可作為安全管制手段**。

總結

系統選單操作核心三步驟:**GetSystemMenu 取得控制碼 → DeleteMenu/EnableMenuItem/AppendMenu 修改 → DrawMenuBar 重繪**。封裝為 WPF 擴充方法並處理 Handle 生命週期,即可在純 managed 代碼中靈活控制標題列行為。記住:**索引由大刪至小、DrawMenuBar 不可漏、自訂命令 ID 避開系統保留區**。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved