While looking at ways of improving the UI of a dialog in an application, I wanted to display some status text in a ListBox control that was empty. The default Windows Forms ListBox (which uses the underlying native Win32 control) doesn't support this, but with a little effort we can extend the control.

A demonstration of the sample project
A demonstration of the sample project

A brief primer on painting in Windows Forms

When a Control receives either the WM_PAINT or WM_ERASEBKGND messages, it will check to see if the ControlStyles.UserPaint style is set. If set then the WM_PAINT message will cause the Paint event to be raised, and for WM_ERASEBKGND the PaintBackground event - but only if the the AllPaintingInWmPaint style is not set.

For both messages, if the UserPaint style is not set, then the control will call the default window procedure allowing that to handle the message.

This is important to note, as for certain controls (such as ListBox which wrap a native window) the UserPaint style is not set, meaning the paint events are never raised. If you try and set the flag yourself, then you will find the paint events work again - but the native control will stop painting correctly due to the default window procedure not being called.

Unfortunately, while you can manually call the default window procedure via the DefWndProc method, you won't have access to the original message data to pass to it.

Capturing WM_PAINT

Based on the above primer, we now know that we can't easily use OnPaint to provide our custom drawing. Instead, we'll intercept the WM_PAINT message when it arrives for our control and initiate painting manually.

Although the Control class offers many events for easily hooking into various actions, it isn't possible to hook into window procedures in this manner. The simplest solution is to create an inherited class and then override the WndProc method.

csharp
const int WM_PAINT = 15;

protected override void WndProc(ref Message m)
{
  // make sure we call existing procedures!
  base.WndProc(ref m);

  if (m.Msg == WM_PAINT)
  {
    // perform some custom painting
  }
}

Painting our custom message

Even though we're very slightly going outside the box to intercept windows messages, we don't need to actually use any Win32 calls. Instead we call CreateGraphics to get a Graphics instance for our control and paint away as we normally would.

csharp
private void DrawText()
{
  if(this.Items.Count == 0 && !string.IsNullOrEmpty(_emptyText) && !this.DesignMode)
  {
    TextFormatFlags flags;

    flags = TextFormatFlags.ExpandTabs | TextFormatFlags.HorizontalCenter | TextFormatFlags.NoPrefix | TextFormatFlags.WordBreak | TextFormatFlags.WordEllipsis | TextFormatFlags.VerticalCenter;

    using (Graphics g = this.CreateGraphics())
    {
      TextRenderer.DrawText(g, _emptyText, this.Font, this.ClientRectangle, this.ForeColor, this.BackColor, flags);
    }
  }
}
Displaying a message in an empty ListBox
Displaying a message in an empty ListBox

In this example it will print the message centred in the middle of the list with word wrapping enabled.

Clearing up after messy resizing

There's just one flaw with the above code - as soon as you resize the control, it will paint the text again without clearing the existing content, which can result in a bit of a mess. As I discussed above, Windows uses the WM_ERASEBKGND to notify a window that it should erase its background and so if we adjust our WndProc to intercept this message we can clean up after ourselves.

Messy output after resizing the window
Messy output after resizing the window
csharp
const int WM_ERASEBKGND = 20;

protected override void WndProc(ref Message m)
{
  base.WndProc(ref m);

  if (m.Msg == WM_PAINT)
  {
    this.OnWmPaint(ref m);
  }
  else if (m.Msg == WM_ERASEBKGND && this.ShouldDrawEmptyText())
  {
    this.ClearBackground();
  }
}

private void ClearBackground()
{
  using (Graphics g = this.CreateGraphics())
  {
    g.Clear(this.BackColor);
  }
}

This time I'm simply instructing the control to draw itself, which will cause the underlying native window to repaint its background ready for our re-positioned text to be drawn.

In the original posting of this article, I'd accidentally defined WM_ERASEBKGND as 14 which is actually WM_GETTEXTLENGTH. So the example managed to work only by chance. Calling Invalidate from WM_ERASEBKGND is the wrong approach as it leads to mass flicker. In the revised version, I just manually erase the background.

And that is pretty much it, short and sweet - the associated download includes an updated fully functional demonstration project.

Adding empty text support to other controls

While this article describes extending the ListBox control, it should be possible to use in other controls too. For example, I use the exact same technique to add empty text support to the ListView control.

Update History

  • 2018-04-28 - 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

# Tomasz

Another great article. I believe that WinForms aren't dead. I use them all the time and Your articles helped me a many times! Looking forward to see more articles about components development, especially design time support and drawing optimization.

Best, Tomasz

Reply

# Richard Moss

Tomasz,

Thanks for the kind words! Although sometimes I look at WPF with envy, I still prefer Windows Forms and don't have any real desire to change. I have been a little lacking on this blog lately, but I still have a few things in the pipeline, including at least one multi-part article for a Windows Forms control.

Regards;
Richard Moss

Reply

# Tomasz

Richard,

I feel the exact same thing. I enjoy creating applications in Windows Forms. I've created couple using WPF, but I still prefer WinForms. Looking forward to new articles about controls :)

Best regards, Tomasz

Reply