Tuesday, September 25, 2007

PanelBackgroundPainter sample / A better message filter

Say you want to get an event and the only way to do it is to inherit from the control. Lots of times, that’s either inconvenient or impossible. Sometimes the only answer is to use NativeWindow, but it requires a lot of background knowledge to get it right. I’ve solved that by wrapping it in a class called MessageFilter.

The article below walks you through a sample problem and how to use MessageFilter to solve it.

Real world example : Faster transparency for panels
Ted has an image on his form, with several panels of stuff they want to show. Ted wants the image from the form to show through. His first try was to panel.BackColor to Transparent, but he found that performance wasn’t so great when dragging the window around.

Ted decides the solution is to paint what he wants directly onto the panel. This means that he has to draw the part of the Form.BackgroundImage that overlaps with the panel onto the panel itself.

It would seem like a pretty simple matter of hooking the Paint event for the panel and calling DrawImage with a few extra arguments to paint a part of the image in the right spot.

Alas, he finds that this solution flickers. The grey BackColor of the panel gets painted in the background paint cycle (OnPaintBackground), followed by his image drawing in the foreground paint cycle (Paint event). For flicker free painting, you need to paint all of the background stuff together. He needs to somehow use OnPaintBackground instead.

Unfortunately there is no event corresponding to OnPaintBackground. But there is a Win32 message: WM_ERASEBACKGND. Ted can choose one of three ways to try to get a hold of this message:

1. Override the WndProc method
2. Install an IMessageFilter in Application.AddMessageFilter
3. Subclass the control using NativeWindow

Option 1 is usually the best option, but in this case, Ted has lots of kinds of panels and he doesn’t want to inherit.

Option 2 is a no-go. IMessageFilter only works on messages that are posted, and gets all the window messages for the thread. Since WM_ERASEBKGND is usually sent through SendMessage, not PostMessage, WM_ERASEBKGND would never show up in the message filter.

Option 3 is our best option. We can create a class that derives from NativeWindow and get all the window messages that are intended for the control before they go to the control.WndProc method. This is what the MessageFilter class does for you.

Simple message filter
Here’s how the PanelBackgroundPainter would use it to trap WM_ERASEBACKGND and WM_PAINT messages.


public class PanelBackgroundPainter : IDisposable {
private MessageFilter _messageFilter;
private Control _control;

public PanelBackgroundPainter(Control control) {
_control = control;

_messageFilter = new MessageFilter(control,
NativeMethods.WM_ERASEBKGND,
NativeMethods.WM_PAINT);

_messageFilter.FilterMessage += WndProc;

}
void WndProc(object sender, MessageFilter.MessageEventArgs e) {

switch (e.m) {
case NativeMethods.WM_ERASEBKGND:
break;
case NativeMethods.WM_PAINT:
break;
}

}

Handling messages in the message filter
The other advantage over IMessageFilter is that you can handle the message too. That is, just like overriding the WndProc, you can either call DefWndProc, the base.WndProc, or handle the message entirely.

  • If you want to directly call the DefWndProc, just call e.DefWndProc()
  • If you want to directly call base WndProc, just call e.WndProc()
  • If you want to handle the message, set e.Result and e.Handled = true.
  • If you do nothing to the event args, WndProc will be called as usual.

Full source [ MessageFilter.cs PanelBackgroundPainter.cs Form1.cs ]
More about NativeWindow, subclassing.

2 comments:

Anonymous said...

The message filter seems to be a variation of the WndProcHooker from MSDN

http://msdn2.microsoft.com/en-us/library/ms229658.aspx

JFo said...

Cool, it looks like there's a new article out there. It's a nice API to do a message map.

I think my only worry about it is that it doesn't use NativeWindow, which has been thouroughly tested for stuff in the finalizer.

The other thing is that it calls SetWindowLong instead of SetWindowLongPtr (or I assume that because the p/invoke class is not included in the sample) - which will not work on 64 bit platforms?