How to Manipulate a Windows System Menu Using the C# Language and Native Windows API Calls
Prologue
This “trick” might be known by experienced C/++ programmers who are still working with the native Windows API. Because I am a son of this philosophy, I have tried to port this trick into .NET. The .NET Framework does not provide a SystemMenu property or a GetSystemMenu member function, so I hard coded it, using the native Windows API.
This is the first part of my work, where I will show how to add items and how to obtain when they are clicked. Any other operations and tricks will come. I Promise.
What Is the System Menu?
The System Menu of a window shows up if you click the icon of the window, right-click the title bar or right-click the taskbar panel of the window. It contains default actions on the window, depending on the state of the window. The System Menu will look different on different types of windows. A System Menu of a normal overlapped window will look different from a System Menu of a toolbox child dialog, for example.
What Benefits Do You Receive by Modifying It?
It has several benefits:
- You can do the same as the MFC Class Wizard: Add an “About” or other application defined items.
- The System Menu is a perfect place to put actions that should also be accessible while the window is minimized because the System Menu can also be shown by right-clicking the window’s pane in the task bar.
- Disabling or removing “Maximize,” “Minimize,” or “Close” from the System Menu will also affect on the three buttons in the upper right corner of the window. This is a smart way of disabling the “X” button of a window.
How Can You Manipulate This Menu?
If you call the Windows API Function GetSystemMenu, you retrieve a copy of the System Menu of a window you have passed. A second parameter specifies whether you want to reset the System Menu to its default state. By using other Windows API functions, such as AppendMenu, InsertMenu, and many others, you can manipulate it.
In this first article, I will only show how to add menu items, and how you can check whether an application-defined item is clicked.
The “SystemMenu” Class
I have designed this class to make the entire access easier. You use this class to modify the SystemMenu of a window. You gain an object by calling the static member function “FromForm”. This function requires a valid Form object or a class that inherits from Form as a parameter. It creates a new SystemMenu object or throws a NoSystemMenuException if the GetSystemMenu API call fails.
Now, let me explain the the working of the menu functions provided by the Windows API. Every function requires the handle of the menu it should modify. You don’t have to handle that because my class does that work for you. Because this is a pointer in C/++, you will use IntPtr for it in .NET. Most functions also require a bitmask of flags that tell the subsystem how the new item should act or look like. You will find them in the MSDN by searching for the MF_XXX constants. You don’t have to take them from MSDN and pass them to the member functions of the SystemMenu class manually because they are all defined in the public enumeration “ItemFlags”. For example, mfString will tell the subsystem to display the string passed by the “Item” parameter in the menu item. If you specify mfSeparator, the “ID” and “Item” parameters are ignored. The mfBarBreak functions the same as the mfBreak flag for a menu bar. For a drop-down menu, submenu, or shortcut menu, the new column is separated from the old column by a vertical line. The flag mfBreak places the item on a new line (for menu bars) or in a new column (for a drop-down menu, submenu, or shortcut menu) without separating columns. If you specify more that one flag, you have to add them by using the bitwise or operator |. For example:
// Will create an item labeled "Test" which is checked. mySystemMenu.AppendMenu(myID, "Test", ItemFlags.mfString | ItemFlags.mfChecked);
The “Item” parameter specifies the text that will be displayed in the new item. The ID should be an unique number that will be used to identify your items.
Note: Ensure that your IDs are lower than 0xF000 and higher than zero. Because 0xF000 and above are reserved for the system commands, you cannot use them. You also can call the static method VerifyItemID of the SystemMenu class to check whether your ID is correct.
Two constants, mfByCommand and mfByPosition, need better explanation. The first important thing: By default, mfByCommand is used. The interpretation of “Pos” depends on these flags: If you specify mfByCommand, the “Pos” parameter is the ID of the item before the new item will be inserted. If you specify mfByPosition, the “Pos” parameter is a zero-based relative position of the new item. If nPos is -1 and you have mfByPosition specified, the item will be inserted at the end. This is why the AppendMenu() is superseded by InsertMenu(), but I still recommend using AppendMenu because the name tells other readers of your code more clearly what you do with this line.
The SystemMenu Class
using System; using System.Windows.Forms; using System.Diagnostics; using System.Runtime.InteropServices; public class NoSystemMenuException : System.Exception { } // Values taken from MSDN. public enum ItemFlags { // The item ... mfUnchecked = 0x00000000, // ... is not checked mfString = 0x00000000, // ... contains a string as label mfDisabled = 0x00000002, // ... is disabled mfGrayed = 0x00000001, // ... is grayed mfChecked = 0x00000008, // ... is checked mfPopup = 0x00000010, // ... Is a popup menu. Pass the // menu handle of the popup // menu into the ID parameter. mfBarBreak = 0x00000020, // ... is a bar break mfBreak = 0x00000040, // ... is a break mfByPosition = 0x00000400, // ... is identified by the position mfByCommand = 0x00000000, // ... is identified by its ID mfSeparator = 0x00000800 // ... is a seperator (String and // ID parameters are ignored). } public enum WindowMessages { wmSysCommand = 0x0112 } /// <summary> /// A class that helps to manipulate the system menu /// of a passed form. /// /// Written by Florian "nohero" Stinglmayr /// </summary> public class SystemMenu { // I havn't found any other solution than using plain old // WinAPI to get what I want. // If you need further information on these functions, their // parameters, and their meanings, you should look them up in // the MSDN. // All parameters in the [DllImport] should be self explanatory. // NOTICE: Use never stdcall as a calling convention, since Winapi // is used. // If the underlying structure changes, your program might cause // errors that are hard to find. // First, we need the GetSystemMenu() function. // This function does not have an Unicode counterpart [DllImport("USER32", EntryPoint="GetSystemMenu", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.Winapi)] private static extern IntPtr apiGetSystemMenu(IntPtr WindowHandle, int bReset); // And we need the AppendMenu() function. Since .NET uses Unicode, // we pick the unicode solution. [DllImport("USER32", EntryPoint="AppendMenuW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.Winapi)] private static extern int apiAppendMenu( IntPtr MenuHandle, int Flags, int NewID, String Item ); // And we also may need the InsertMenu() function. [DllImport("USER32", EntryPoint="InsertMenuW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.Winapi)] private static extern int apiInsertMenu ( IntPtr hMenu, int Position, int Flags, int NewId, String Item ); private IntPtr m_SysMenu = IntPtr.Zero; // Handle to the System Menu public SystemMenu( ) { } // Insert a separator at the given position index starting at zero. public bool InsertSeparator ( int Pos ) { return ( InsertMenu(Pos, ItemFlags.mfSeparator | ItemFlags.mfByPosition, 0, "") ); } // Simplified InsertMenu(), that assumes that Pos is a relative // position index starting at zero public bool InsertMenu ( int Pos, int ID, String Item ) { return ( InsertMenu(Pos, ItemFlags.mfByPosition | ItemFlags.mfString, ID, Item) ); } // Insert a menu at the given position. The value of the position // depends on the value of Flags. See the article for a detailed // description. public bool InsertMenu ( int Pos, ItemFlags Flags, int ID, String Item ) { return ( apiInsertMenu(m_SysMenu, Pos, (Int32)Flags, ID, Item) == 0); } // Appends a seperator public bool AppendSeparator ( ) { return AppendMenu(0, "", ItemFlags.mfSeparator); } // This uses the ItemFlags.mfString as default value public bool AppendMenu ( int ID, String Item ) { return AppendMenu(ID, Item, ItemFlags.mfString); } // Superseded function. public bool AppendMenu ( int ID, String Item, ItemFlags Flags ) { return ( apiAppendMenu(m_SysMenu, (int)Flags, ID, Item) == 0 ); } // Retrieves a new object from a Form object public static SystemMenu FromForm ( Form Frm ) { SystemMenu cSysMenu = new SystemMenu(); cSysMenu.m_SysMenu = apiGetSystemMenu(Frm.Handle, 0); if ( cSysMenu.m_SysMenu == IntPtr.Zero ) { // Throw an exception on failure throw new NoSystemMenuException(); } return cSysMenu; } // Reset's the window menu to it's default public static void ResetSystemMenu ( Form Frm ) { apiGetSystemMenu(Frm.Handle, 1); } // Checks if an ID for a new system menu item is OK or not public static bool VerifyItemID ( int ID ) { return (bool)( ID < 0xF000 && ID > 0 ); } }
You can reset the system menu of a Form to its default state by using the static ResetSystemMenu() class. This is useful if your application runs into errors or does not run properly after modifying the System Menu.
How to Use the SystemMenu Class
If you have read the last section carefully, it should not be difficult to use this class.
// SystemMenu object private SystemMenu m_SystemMenu = null; // ID constants private const int m_AboutID = 0x100; private const int m_ResetID = 0x101; private void frmMain_Load(object sender, System.EventArgs e) { try { m_SystemMenu = SystemMenu.FromForm(this); // Add a separator ... m_SystemMenu.AppendSeparator(); // ... and an "About" entry m_SystemMenu.AppendMenu(m_AboutID, "About this..."); // And a "Reset" item on top m_SystemMenu.InsertSeparator(0); m_SystemMenu.InsertMenu(0, m_ResetID, "Reset Systemmenu"); } catch ( NoSystemMenuException /* err */ ) { // Do some error handling } }
The above code will result in the following System Menu:
Thanks to Mick for providing an English screenshot.
Note: The InsertMenu overload that accepts three parameters and the InsertSeperator method assume that the position you pass is a position, not the ID of the item to be inserted before. Use the superseded InsertMenu method if you want to insert your item before a specified menu item ID, and add mfByCommand using the bitwise or into ItemType.
How to Check When an Event on the Application-Defined Items Has Occurred
This is the most difficult part of this, I guess. For this, you have to override the WndProc member function of your class that should inherit either from Form or from Control. You can accomplish this by writing:
protected override void WndProc ( ref Message msg ) { base.WndProc(ref msg); }
Note: You must call the base class implementation of WndProc; otherwise, it will not work properly. It could work if you implement all message routines on your own, if you want to do so, (good luck). You will not be successful.
But, what should be done in this override? You have to catch the WM_SYSCOMMAND message. You retrieve this message if the user selects an item from the System Menu or chooses the maximize button, minimize button, or the close button. You do not have to look up the constant WM_SYSCOMMAND on your own because I have declared in the WindowMessages enumeration. And the most important thing: The WParam property of the Message object will contain the ID of the item that was pressed.
I assume that you have used the example code above. And, with all the information above, you (hopefully) result in the following code. You will find additional descriptions and information in the comments.
protected override void WndProc ( ref Message msg ) { // Now we should catch the WM_SYSCOMMAND message and process it. // We override the WndProc to catch the WM_SYSCOMMAND message. // You don't have to look this message up in the MSDN; it is // defined in WindowMessages enumeration. // The WParam of the message contains the ID that was pressed. // It is the same value you have passed through InsertMenu() // or AppendMenu() member functions of my class. // Just check for them and do the proper action. // if ( msg.Msg == (int)WindowMessages.wmSysCommand ) { switch ( msg.WParam.ToInt32() ) { case m_ResetID: // ID of the reset item { if ( MessageBox.Show(this, "\tAre you sure?", "Question", MessageBoxButtons.YesNo) == DialogResult.Yes ) { // Reset the Systemmenu SystemMenu.ResetSystemMenu(this); } } break; case m_AboutID: { // Our about id MessageBox.Show(this, "Written by Florian \"nohero\" Stinglmayr.\n" + "EMail: nohero@coding.at", "About"); } break; // TODO: Add more handles, for more menu items } } // Call base class function base.WndProc(ref msg); }
Another possible way to approach this could be by creating an event OnSysCommand and raising it when the WM_SYSCOMMAND message comes in. Then, pass the WParam property into the handler of the event.