Introduction
The .NET Windows Forms library provided by Microsoft contains a number of
controls that wrap standard Windows controls. In fact, most of the basic
Windows control types are represented - Button
,
Label
,
TextBox
, etc. Presently, however,
there are no controls that implement horizontal and vertical rules, though such
functionality does exist in the base Win32 library.
In this article, I will discuss the development of two simple classes that
implement horizontal and vertical controls for Windows Forms development. The
goal of these classes was to encapsulate the native Win32 vertical and
horizontal rule controls, rather than create new ones from scratch. Two
versions of the controls are presented - my original attempt, and a better
version that is the result of comments posted to the original article.
The benefit of wrapping the standard horizontal and vertical rule controls,
rather than simply overriding OnPaint
in a Control
subclass and drawing a few lines, is standards conformance. Horizontal and
vertical rules are pretty simple controls and don't appear to have changed much
(or at all) in appearance since Windows 95. Nonetheless, it remains that if we
allow Windows to draw a control instead of doing so ourselves, we are able to
reflect changing UI conventions when new themes and new versions of Windows
come out, with little or no effort on our part.
Background
Probably the most common use of these controls is to divide a complex form into
many areas. The standard GroupBox
control does the same
thing, but it takes up more real-estate on the form and is inappropriate when
your logical areas don't lend themselves to naming (or when you don't want to
name them). The pictures below show how the Windows Explorer in Windows XP uses
rules and groups on some of its property pages.
How does Windows do it?
To begin, I knew that the Windows UI library included the ability to draw
horizontal rules. These rules can be seen in various places in Windows,
including the first page of a file properties dialog in Windows Explorer. I
also knew that I'd been able to access this horizontal rule capability in MFC
(starting I think with Visual Studio 2002) by dropping a Picture
control
onto my form in the dialog designer (the Win32 one used for MFC, ATL, etc.; not
the .NET one) and setting its type
to Etched Horz
or
Etched Vert
. This led me to believe that whatever Win32 control
implements, "picture" functionality also implements horizontal and vertical
rule functionality. This turns out to be about half true.
In reality, the Picture
control available in the Visual Studio
dialog designer is a psuedo-control. Depending on the value you assign to its
Type
property in the property window, different resource lines will
be added to the
.rc file that the designer is working with. I was dimly
aware of this but hadn't worked with it for a long time. For a picture with
type
Icon
the line might look like this.
ICON "",IDC_STATIC,33,30,20,20
For a picture control with type set to Etched Horiz
, the following
line is emitted.
CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,31,19,131,1
The CONTROL
declaration can be thought of as a kind of generic
request to create a window of a given type. According to the MSDN
documentation, it creates a user-defined control, which is really just a way of
saying that it can create a window of any given window class. At runtime, a CONTROL
declaration translates into a call to CreateWindowEx
to create a
window with the given set of parameters. For this case, the two important
arguments are the 3rd and 4th, Static
and SS_ETCHEDHORZ
.
When Win32 windows are created using CreateWindowEx
, they must be
created from a registered window class. A full discussion of window classes is
beyond the scope of this article, but all you really need to know about them is
that they define a basic set of properties for windows that are created from
that class. (Win32 API programmers may be groaning at this point, but in
essentials the statement is correct). Windows predefines a set of window
classes to implement standard windows, such as buttons, list boxes, combo boxes
and scroll bars. When you get right down to it, it is the window class that
defines what the behavior of a new window will be.
One of the standard window classes available is the Static
class.
There is wide variability in the appearance of static controls, but the one
thing that all static controls have in common is that they accept no user
input. They are, from the user's point of view, static. When a window of class
Static
is created, its behavior is controlled by the set of
SS_*
flags included in the window style parameter passed to
CreateWindowEx
.
The two
SS_*
flags that we care about here are
SS_ETCHEDHORZ
and
SS_ETCHEDVERT
. These create static controls that look like
horizontal and vertical rules, respectively.
Armed with this information about static controls, I began to wonder whether the
best solution would be to write a single control with a property that would
allow the user to select the SS_*
flags of their control, similar
to how they would when calling CreateWindowEx
. This would allow a
single .NET control to morph into any kind of static control. I can imagine how
such a control might be useful. I opted not to do this, however, because I much
prefer the idea of controls with different appearances and behavior being
represented as different controls in the toolbar. Also, differences in behavior
of static controls require differences in the .NET code that wraps them, and
given the choice of handling differences by using an if
statement
or by creating a new class to represent the difference, I usually opt for the
class-based approach.
Creating the .NET Wrappers - First Attempt
Now that we know how to create horizontal and vertical rule windows, wrapping
those windows into .NET control classes requires very little code. My first
attempt at doing so resulted in the code below. Note that it uses Interop to
create a window in the constructor and to manually change the size of that
window as needed. The following is the implementation of the HorizRule
control; the VertRule
control is almost identical.
using HANDLE = System.IntPtr;
[ Description("A horizontal rule.") ] public sealed class HorizRule : Control
{
private HANDLE m_hwndRule;
private const int FixedHeight = 2;
public HorizRule()
{
Height = FixedHeight;
m_hwndRule = (IntPtr) User.CreateWindowEx(
0, "static", "",
User.WS_CHILD | User.WS_VISIBLE | User.SS_ETCHEDHORZ,
0, 0, Width, Height, Handle,
HANDLE.Zero, HANDLE.Zero, HANDLE.Zero
);
}
protected override void OnSizeChanged(EventArgs args)
{
if(m_hwndRule != HANDLE.Zero)
{
Height = FixedHeight;
User.SetWindowPos(
m_hwndRule, HANDLE.Zero, 0, 0, Width, Height,
User.SWP_NOCOPYBITS | User.SWP_DRAWFRAME
);
}
base.OnSizeChanged(args);
}
}
First off, the User
class that you see used here to
access the Win32 API is included with the source code download. It is a subset
of Wesner Moise's Win32
Library for .NET.
As you can see, the class derives from System.Windows.Forms.Control
,
and includes two methods. The constructor sets the height of the .NET control
to a constant value defined by FixedHeight
and then uses
CreateWindowEx
to create the native window, which is stored for
later reference. The new window is given the window handle of the .NET control
(accessed through the
Handle
property) as its parent.
Its location (which is relative to its parent, the .NET control) is set to
(0,0), and its width and height are set to match that of the .NET parent
control. In this way, the native window exactly overlays the .NET parent. The
OnSizeChanged
handler simply uses
SetWindowPos
to maintain this relationship as the .NET control's size changes (for instance
by being sized manually in a form designer or resized along with its container
when docked or anchored).
OnSizeChanged
also prevents
the form's height from changing, by always setting it back to the constant
fixed size.
Why, you may ask, should the control restrict itself to a fixed height? This is
one of those behavioral differences I mentioned above that would make
development of a single class that exposed all the Windows static control
styles difficult. It only makes sense to restrict the height of a horizontal
rule control, since any height beyond the space used to draw the rule would be
useless - nothing would be drawn in that space. Even if that hadn't been the
case, however, another consideration forced me to implement the size
restriction. If the size isn't restricted, parts of the control remain undrawn,
resulting in an ugly hall-of-mirrors effect. (Try removing the override of
OnSizeChanged
and anchoring the control to a resizable parent to see what I
mean). My original explanation for why this happens, which is that the parent
of the static control is not being notified to draw its background, is probably
not correct, but I must admit that I really don't know why the problem occurs.
Through experimentation, I discovered that restricting the height of the
control to 2 pixels allows the rule to be drawn and leaves no extra space to be
left un-drawn. This is not a perfect solution - it may malfunction in some
cases that I haven't discovered yet, and it means that the code will have to be
modified if the size of rules changes in future versions of Windows. Luckily,
my second attempt does not suffer from this problem.
Creating the .NET Wrappers - Second Attempt
So, aside from the drawing problem that forced the control to be fixed-sized,
what else is wrong with this implementation? Plenty. I'll proceed
point-by-point through to a better version.
The Child Control
The approach taken in the first attempt was to use the Win32
CreateWindowEx
function to create a window with the appropriate window class and properties
to become a horizontal or vertical rule. This native window was then
overlaid over the window that was automatically created by the base class,
Control, and made to exactly cover it. The base class' window was actually
never used. A much better solution, pointed out to me by
Thomas Freudenberg, is to modify the window that Control is going to create
anyway, and forget all about using
CreateWindowEx
directly.
The way to do this is to use the CreateParams
property of Control. This
property is read by Control whenever it is about to create the native window
that the control encapsulates. It gives us a chance to specify many of the
arguments that will eventually be passed to CreateWindowEx
, which is called
somewhere deep inside the framework's NativeWindow
class. NativeWindow
is a
framework class that encapsulates a native Win32 window object and provides
utility methods for working with native Win32 windows.
Thomas' original suggestion was to override the CreateParams
property and to
derive from Label instead of from Control. Label is, of course, a subclass of
Control itself, and the window that Label creates has the "STATIC" window
class, as described above. A simple test showed that the following code worked
fine.
public class HorizRule : Label
{
public HorizRule()
{
}
protected override CreateParams CreateParams
{
get
{
CreateParams createParams = base.CreateParams;
createParams.Style = User.WS_CHILD | User.WS_VISIBLE |
User.SS_ETCHEDHORZ;
return createParams;
}
}
}
That's simple enough! I didn't want to derive from Label
, however, because a
horizontal rule is not really a label. That's a purist argument, but it is not
good design in my opinion to
choose a base class based on convenience rather
than correctness. (Sure, sometimes it may be necessary or desirable, but that's
a whole set of additional arguments and doesn't apply here). Additionally, I
wasn't even sure at first why it would make a difference; it seemed to me that
as long as I was controlling the properties of the window that got created
using CreateParams
, it shouldn't matter that I was deriving directly from
Control
instead of from Label
. So, I tried the following. Note that I'm now
deriving from Control
directly, instead of through Label, and that I've added
an additional line to the CreateParams
override to set the desired window class
name, since it won't be set automatically by virtue of having been a subclass
of Label
.
public class HorizRule : Control
{
public HorizRule()
{
}
protected override CreateParams CreateParams
{
get
{
CreateParams createParams = base.CreateParams;
createParams.ClassName = "static";
createParams.Style = User.WS_CHILD | User.WS_VISIBLE |
User.SS_ETCHEDHORZ;
return createParams;
}
}
}
To my surprise, this didn't work. At runtime, a Win32Exception
would be thrown
with the error text "Class already registered." The error was being thrown from
within the NativeWindow.RegisterClass
method. That method is not public and not
documented, but its purpose is pretty clear - it's a wrapper for the Win32
RegisterClassEx
function, which registers custom window classes with Windows.
It didn't make any sense for that method to be called, however, because
"static" is a predefined window class. Of course any attempt to register it as
a custom class will fail!
Chagrined, I inspected the CreateParams
object returned from CreateParams
in
the version of the class that derives from Label, and compared it to the one
that derives from Control. I checked the ClassName
property of CreateParams
in
the Label version and saw that it was "STATIC", as expected. Well, almost as
expected... I wish I could say that my eureka moment occurred immediately, but
in fact it took me about 10 minutes of running down other roads to realize the
significance of the fact that I had seen "STATIC" instead of "static" or
"Static" in the ClassName
property of the CreateParams
obtained from Label
. I
was misled by a belief that case shouldn't matter. It really shouldn't in this
case, but because of an apparent bug in the framework it does.
As soon as the thought hit me I changed "static" to "STATIC", and suddenly it
started working. From the clues I've gathered, I surmise that something like
the following must be taking place within NativeWindow
. Whenever NativeWindow
is asked to create a new window, it looks at the requested window class for the
new window and decides whether that class should be registered by checking an
internal list of already-registered classes. This prevents clients from having
to know anything about registering window types - they can just specify the
type name, whether custom or predefined, and the name will be registered
automatically when needed. Presumably this list is seeded initially with the
names of the predefined window classes ("STATIC", "BUTTON", etc.), to prevent
NativeWindow
from ever trying to incorrectly register them. It appears, though,
that the list is case-sensitive, even though the Win32 API treats window class
names in a case-insensitive manner. So if I specify "static" as a class name,
NativeWindow
won't recognize that the "static" class is already registered
under the name "STATIC", and will attempt to register it. RegisterClassEx
then
fails because "static" really is registered already.
The theory above could be wrong, but it fits with the evidence I've gathered.
If someone knows better, I'd certainly like to hear the real story.
With that out of the way, the basic shape of our new HorizRule
class comes into
focus. It will derive from Control
rather than Label
and use CreateParams
to
modify the window that Control
creates.
Sizing
The second problem with the original control is the way that it handled
sizing. As mentioned above, the original version suffered from a drawing
problem that mandated a fixed width or height (depending on whether the
control was a horizontal or vertical rule). Now that we're modifying
the control created by our parent instead of creating an overlay child
control, that problem no longer exists. However, I still think it's a
good idea to restrict the size of the rule - rules by nature should
expand in only one direction.
The problem with the implementation of sizing in the first attempt was that it
was too complicated - a much simpler solution exists. This solution was pointed
out to me by David Kean. Instead of overriding OnSizeChanged
and continually
setting the height to a fixed value, we can override SetBoundsCore
instead.
This is more efficient and straightforward.
When you do give a static control with the SS_ETCHEDHORZ
style a height larger
than 2, it does have a sane behavior. It simply draws the dark and light parts
of the rule etching at the top and bottom of the sized control, and includes
side markings linking the two as well. See the Test project in the source
downloads to see what I mean. The result doesn't really look like a rule
anymore - it looks like a label control with a static edge. It could also be
interpreted as a thick rule, however. Because some people may want this "thick
rule" capability, I have added a new property to the control, FixedHeight
. This
property has a default value of 2. Any value you assign to this property
greater than 2 will cause the height of the control to be fixed to that value.
Values less than 2 are illegal, except for -1, which causes the height of the
control to not be fixed. In that case, the control's height will expand or
contract normally just like any other control, for instance when it is anchored
to a parent whose size changes.
Unwanted Properties
The Control class contains a property called
TabStop
. You may notice that
TabStop
does not show up in the property list for Labels that are dropped
onto a form in the form designer, however. How can this be, if Label
inherits everything that Control does? It makes sense that it would be
missing - a label can't be a tab stop so there's no need for a property
that would allow the user to say that it is one. It would just be
confusing for a
TabStop
property to show up for a Label object. There are
a few other properties of Control that don't show up for Labels as well.
Just as you would expect, Label really does have a TabStop
property. Label
inherits it from Control
and there's no way for it to get rid of the property.
It can, however, hide it or override it. I won't get into the details here
because a C# reference or tutorial would be a better source, but there are ways
to hide properties and methods inherited from base classes, both at the code
level (preventing properties or methods of a parent class from being
accessible
through references to a derived class) and in the form designer's property
list.
The first version of my HorizRule
and VertRule
classes did no property hiding.
As a result, several properties that were inherited from Control
were visible
in the form designer, even though setting these properties had no visible
affect on the behavior of the control. To rectify this problem, I have added
several new properties along the lines of the following.
[ Browsable(false) ] public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
This property defines a new Text property that hides the Text property
inherited from Control. In code, the new Text property is visible and
it is implemented in terms of
Control.Text
, so clients notice no difference
in using it. The property is not visible in the form designer's property
list, however, because it has the
Browsable
attribute applied and set to
false
. The form designer sees this new Text, notes that it is not visible,
and does not include it in the property list for the control. Because the
new Text hides the parent's one, it also does not include the parent's
Text in the property list. The result is that the
Text
property is not
visible in the form designer for objects of type
HorizRule
or
VertRule
. This is exactly what we want - these objects do not display
the contents of their Text properties so it doesn't make sense to set
those properties in the designer. Several other properties are hidden
in the same way. The full list is
Text
,
TabStop
,
BackColor
,
BackgroundImage
,
RightToLeft
,
Enabled
and
ImeMode
.
Because this list of hidden properties is the same for both HorizRule
and
VertRule
, and because adding them adds about 70 lines to the implementation
(including blank lines, bracket-only lines and comments - it's only a few new
source lines really), I opted to create an abstract base class for the rule
controls called RuleControlBase
and move these properties out to it. That way,
they only have to be specified once. Otherwise, the exact same 70 lines of code
would be duplicated in both classes.
Interop
The final problem with the original control is that it uses Interop to
access the Win32 API functions. This is not too
terrible, since we're trying to access a capability that is native to
Windows here anyway, but nevertheless if I can choose between using
the framework or using Interop to accomplish a task, I usually try to use the
framework. It just so happens that the answers to the other problems listed
here eliminate the need for Interop, which is an added bonus.
The Final Result
The final
HorizRule
class looks like the following. Its base class,
RuleControlBase
, can be found in the source archive. It just subclasses from
Control
and hides unwanted properties inherited from that class, as
described above. Just as before,
VertRule
is almost identical to
HorizRule
,
so I won't list it here either.
[ Description("A horizontal rule.") ] public sealed class HorizRule : RuleControlBase
{
private const int DefaultFixedHeight = 2;
private const string FixedHeightDesc =
"Set to a fixed height >= 2, or -1 to allow rule to expand.";
private int m_fixedHeight = DefaultFixedHeight;
[
Description(FixedHeightDesc),
Category("Layout"),
DefaultValue(DefaultFixedHeight)
]
public int FixedHeight
{
get { return m_fixedHeight; }
set
{
if(value < DefaultFixedHeight && value != -1)
{
throw new InvalidOperationException(
"Value for FixedHeight must be >= "
+ DefaultFixedHeight + "."
);
}
m_fixedHeight = value;
if(m_fixedHeight != -1)
Height = m_fixedHeight;
}
}
protected override CreateParams CreateParams
{
get
{
CreateParams p = base.CreateParams;
p.ClassName = "STATIC";
p.Style = User.WS_CHILD | User.WS_VISIBLE | User.SS_ETCHEDHORZ;
return p;
}
}
protected override void SetBoundsCore(
int x, int y, int width, int height, BoundsSpecified specified)
{
if(m_fixedHeight != -1)
{
height = m_fixedHeight;
}
else if(height < DefaultFixedHeight)
{
if(height < DefaultFixedHeight)
height = DefaultFixedHeight;
}
base.SetBoundsCore(x, y, width, height, specified);
}
}
Conclusion
In the source archive, you will find a project that contains the HorizRule
and VertRule
classes. These classes are defined in the
Covidimus.Forms
namespace. The project files are in Visual Studio
2002 format. Also included is a small demo project that just shows a few
horizontal and vertical controls on a resizable form.
History
-
October 1, 2003 - Initial posting
- October 13, 2003 - Updated with new versions of the
HorizRule
and VertRule
classes; substantial content added to the body of the article.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.