Displaying text in an empty ListBox
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 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 theWndProc
method.
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.
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);
}
}
}
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.
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
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
ListBoxEmptyTextDemo.zip
|
Sample project for the displaying text in an empty ListBox blog post. |
1.0.0.1 | 29/04/2018 | 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?
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
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
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