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:
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
{
}
public enum ItemFlags
{
mfUnchecked = 0x00000000,
mfString = 0x00000000,
mfDisabled = 0x00000002,
mfGrayed = 0x00000001,
mfChecked = 0x00000008,
mfPopup = 0x00000010,
mfBarBreak = 0x00000020,
mfBreak = 0x00000040,
mfByPosition = 0x00000400,
mfByCommand = 0x00000000,
mfSeparator = 0x00000800
}
public enum WindowMessages
{
wmSysCommand = 0x0112
}
public class SystemMenu
{
[DllImport("USER32", EntryPoint="GetSystemMenu", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.Winapi)]
private static extern IntPtr apiGetSystemMenu(IntPtr WindowHandle,
int bReset);
[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 );
[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;
public SystemMenu( )
{
}
public bool InsertSeparator ( int Pos )
{
return ( InsertMenu(Pos, ItemFlags.mfSeparator |
ItemFlags.mfByPosition, 0, "") );
}
public bool InsertMenu ( int Pos, int ID, String Item )
{
return ( InsertMenu(Pos, ItemFlags.mfByPosition |
ItemFlags.mfString, ID, Item) );
}
public bool InsertMenu ( int Pos, ItemFlags Flags, int ID, String Item )
{
return ( apiInsertMenu(m_SysMenu, Pos, (Int32)Flags, ID, Item) == 0);
}
public bool AppendSeparator ( )
{
return AppendMenu(0, "", ItemFlags.mfSeparator);
}
public bool AppendMenu ( int ID, String Item )
{
return AppendMenu(ID, Item, ItemFlags.mfString);
}
public bool AppendMenu ( int ID, String Item, ItemFlags Flags )
{
return ( apiAppendMenu(m_SysMenu, (int)Flags, ID, Item) == 0 );
}
public static SystemMenu FromForm ( Form Frm )
{
SystemMenu cSysMenu = new SystemMenu();
cSysMenu.m_SysMenu = apiGetSystemMenu(Frm.Handle, 0);
if ( cSysMenu.m_SysMenu == IntPtr.Zero )
{
throw new NoSystemMenuException();
}
return cSysMenu;
}
public static void ResetSystemMenu ( Form Frm )
{
apiGetSystemMenu(Frm.Handle, 1);
}
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.
private SystemMenu m_SystemMenu = null;
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);
m_SystemMenu.AppendSeparator();
m_SystemMenu.AppendMenu(m_AboutID, "About this...");
m_SystemMenu.InsertSeparator(0);
m_SystemMenu.InsertMenu(0, m_ResetID, "Reset Systemmenu");
}
catch ( NoSystemMenuException )
{
}
}
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 )
{
if ( msg.Msg == (int)WindowMessages.wmSysCommand )
{
switch ( msg.WParam.ToInt32() )
{
case m_ResetID:
{
if ( MessageBox.Show(this, "\tAre you sure?",
"Question", MessageBoxButtons.YesNo) ==
DialogResult.Yes )
{
SystemMenu.ResetSystemMenu(this);
}
} break;
case m_AboutID:
{
MessageBox.Show(this, "Written by Florian \"nohero\"
Stinglmayr.\n" +
"EMail: nohero@coding.at", "About");
} break;
}
}
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.
Downloads
ManipSysMenu_src_and_demo.zip -