Using message filters in Windows Forms applications
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:
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:
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.
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.
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.
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.
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.
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.
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
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
MessageFilterDemo.zip
|
Sample project for the using message filters in Windows Forms applications blog post |
01/01/2019 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?