For a utility application, I wanted to add an item in the system menu. It's been quite a long time since I last did this (and was in VB6), so I decided to find some ready made source code. This class provides a nice little helper for wrapping the system menu to add new commands to it, but it requires the owner form to hook into its window procedure and forward messages on which I felt was an awkward design.

The code snippets below should illustrate my point - first we initialise the instance of the SystemMenu class, but in order for custom commands to be processed we have to override to override the form's WndProc and pass any messages received into the SystemMenu class.

Form:

csharp
private SystemMenu _menu;

protected override void OnLoad(EventArgs e)
{
  base.OnLoad(e);

  _menu = new SystemMenu(this);
  _menu.AddCommand("&Defaults...", this.ShowDefaultsDialog, true);
  _menu.AddCommand("&Properties...", this.ShowPropertiesDialog, false);
  _menu.AddCommand("&About...", this.ShowAboutDialog, true);
}

protected override void WndProc(ref Message m)
{
  if (_menu != null)
  {
    _menu.HandleMessage(ref m);
  }

  base.WndProc(ref m);
}

SystemMenu class:

csharp
public void HandleMessage(ref Message msg)
{
  if (msg.Msg == WM_SYSCOMMAND)
  {
    this.OnSysCommandMessage(ref msg);
  }
}

private void OnSysCommandMessage(ref Message msg)
{
  if ((long)msg.WParam > 0 && (long)msg.WParam <= lastId)
  {
    actions[(int)msg.WParam - 1]();
  }
}

This definitely isn't an ideal situation! As WndProc is protected and there is no equivalent event, perhaps the original author of this code thought this was the only solution. Fortunately there is a (little used?) feature of Windows Forms that can inspect and manipulate source messages at an application level.

Introducing Message Filters

The static Application class has the AddMessageFilter and RemoveMessageFilter methods, both of which accept a single parameter - an object implementing the IMessageFilter interface. This interface has a single method, PreFilterMessage, allowing you to inspect a message and then either allow it to be dispatched or blocked ("eaten").

Warning! Caution is advised when dealing with message filters. The MSDN documentation notes that adding filters can degrade application performance but you can also adversely effect your application if you accidentally block messages you shouldn't.

Implementing a message filter

Lets take the SystemMenu class from the start of the article. I've stripped out as much as the code as possible to try and focus only on message filtering.

csharp
internal sealed class SystemMenu
{
  private const int WM_SYSCOMMAND = 0x112;
  private const int MF_STRING = 0x0;
  private const int MF_SEPARATOR = 0x800;

  [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

  [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  private static extern bool AppendMenu(IntPtr hMenu, int uFlags, int uIDNewItem, string lpNewItem);

  private Form _owner;
  private int _lastId = 0;
  private List<Action> _actions = new List<Action>();

  public SystemMenu(Form owner)
  {
    if (owner == null)
    {
      throw new ArgumentNullException(nameof(owner));
    }

    _owner = owner;
  }

  public void AddCommand(string text, Action action, bool separatorBeforeCommand)
  {
    IntPtr systemMenuHandle;
    int id;

    systemMenuHandle = GetSystemMenu(_owner.Handle, false);

    id = ++_lastId;

    if (separatorBeforeCommand)
    {
      AppendMenu(systemMenuHandle, MF_SEPARATOR, 0, string.Empty);
    }

    AppendMenu(systemMenuHandle, MF_STRING, id, text);

    _actions.Add(action);
  }

  public void HandleMessage(ref Message msg)
  {
    if (msg.Msg == WM_SYSCOMMAND)
    {
      this.OnSysCommandMessage(ref msg);
    }
  }
  
  private void OnSysCommandMessage(ref Message msg)
  {
    if ((long)msg.WParam > 0 && (long)msg.WParam <= lastId)
    {
      actions[(int)msg.WParam - 1]();
    }
  }
}

Implementing IMessageFilter

First of all, we need to implement the interface. I'm choosing to explicitly implement it as it doesn't need a public surface.

csharp
internal sealed class SystemMenu : IMessageFilter
{
  bool IMessageFilter.PreFilterMessage(ref Message m)
  {
    return false; // allow the message to be dispatched
  }

This empty implementation returns false to ensure we don't eat any messages. If instead we returned true then our application would be completely broken - it wouldn't paint and you wouldn't be able to interact with anything on it.

With that said, a message filter that doesn't do anything is a bit of a waste, so I'll remove the public HandleMessage method and wrap its code into PreFilterMessage. I also need to adjust OnSysCommandMessage to return a result code as well - again, if you swallow all WM_SYSCOMMAND messages then your custom actions might work but the default ones like Close, Maximize, etc won't - and as this is an application filter it will break all of your application windows.

Also, I added a check to make sure that the WM_SYSCOMMAND message is destined for our owner form - there's no point intercepting it for other forms in our application.

csharp
private bool OnSysCommandMessage(ref Message msg)
{
  bool result;
  int commandId;

  commandId = msg.WParam.ToInt32();
  result = commandId > 0 && commandId <= _lastId;

  if (result)
  {
    _actions[commandId - 1].Invoke();
  }

  return result;
}

bool IMessageFilter.PreFilterMessage(ref Message m)
{
  bool result;

  if (m.Msg == WM_SYSCOMMAND && m.HWnd == _owner.Handle)
  {
    result = this.OnSysCommandMessage(ref m);
  }
  else
  {
    result = false; // allow the message to continue being processed
  }

  return result;
}

The message filter is now complete.

Installing the filter

To install the filter we call Application.AddMessageFilter and pass in our class instance. I choose to do this from the constructor.

csharp
public SystemMenu(Form owner)
{
  if (owner == null)
  {
    throw new ArgumentNullException(nameof(owner));
  }

  _owner = owner;

  Application.AddMessageFilter(this);
}

If we now run the application, we'll find that our SystemMenu class is now self contained and working perfectly.

An example of system menu modifications using a message filter to intercept the WM_SYSCOMMAND message
An example of system menu modifications using a message filter to intercept the WM_SYSCOMMAND message

Removing the filter

Once we've finished with the filter we should remove it - in this example, once the form is closed there isn't much point in waiting for messages that will never arrive. We could make the class disposable via the IDisposable interface but as the Form object has a FormClosed event we can use that and free the caller of having to anything except "fire and forget".

Once again, I modify the constructor, this time to wire up the event, and then supply the event handler itself.

csharp
public SystemMenu(Form owner)
{
  if (owner == null)
  {
    throw new ArgumentNullException(nameof(owner));
  }

  _owner = owner;

  owner.FormClosed += this.FormClosedHandler;

  Application.AddMessageFilter(this);
}

private void FormClosedHandler(object sender, FormClosedEventArgs e)
{
  Application.RemoveMessageFilter(this);

  _actions = null;

  _owner.FormClosed -= this.FormClosedHandler;
  _owner = null;
}

As well as removing the handler, I detach the event and free up objects as well, on the assumption that nothing else is going to be done with the class.

Wrapping up

Although you probably won't have much call to use them, message filters can be useful when dealing with certain windows messages. I originally became aware of them when I needed to have mouse wheel scrolling working on a control without focus and now again for this generic system menu class.

Update History

  • 2019-01-01 - First published
  • 2020-11-22 - Updated formatting

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


Comments