Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#
Article

Wrapping Win32 Controls in .NET - Horizontal and Vertical Rules

Rate me:
Please Sign up or sign in to vote.
4.85/5 (14 votes)
13 Oct 200318 min read 132K   2.6K   41   27
This article discusses the development of two simple classes that implement horizontal and vertical controls for Windows Forms development by encapsulating the native Win32 vertical and horizontal rule controls.

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,

C#
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.

Figure 1

Figure 2

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.

C#
// NOTE: OBSOLETE first attempt at a HorizRule class.
// A better version is presented below.

using HANDLE = System.IntPtr;

/// <summary>
/// Implements a standard windows horizontal rule.
/// </summary>
[ Description("A horizontal rule.") ] public sealed class HorizRule : Control
{
    private HANDLE m_hwndRule;
    private const int FixedHeight = 2;

    /// <summary>
    /// Constructs a new <see cref="HorizRule"/> object.
    /// </summary>
    public HorizRule()
    {
        // The height of a horizontal rule is a constant.
        Height = FixedHeight;

        // Create the native window that will
        // display as a horizontal rule.
        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
        );
    }

    /// <summary>
    /// Override of <see cref="Control.OnSizeChanged"/>.
    /// </summary>
    protected override void OnSizeChanged(EventArgs args)
    {
        if(m_hwndRule != HANDLE.Zero)
        {
            // The height of a horizontal rule is a constant.
            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
C#
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.

C#
public class HorizRule : Label
{
    public HorizRule()
    {
        // Intentionally Empty
    }

    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.

C#
public class HorizRule : Control
{
    public HorizRule()
    {
        // Intentionally Empty
    }

    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.

C#
/// <summary>
/// Hides the <see cref="Control.Text"/> property in the property
/// browser b/c it doesn't make sense for a rule.
/// </summary>
[ 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.
C#
/// <summary>
/// Implements a standard Windows horizontal rule.
/// </summary>
[ 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;

    /// <summary>
    /// Sets a fixed height for the control.  Set to a value >= 0 to fix
    /// the height of the rule at that value, or -1 to allow the rule to
    /// expand.
    /// </summary>
    [
        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;    // Update height.
        }
    }

    /// <summary>
    /// Override of <see cref="Control.CreateParams"/>.
    /// </summary>
    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;
        }
    }

    /// <summary>
    /// Override of <see cref="Control.SetBoundsCore"/>.
    /// </summary>
    protected override void SetBoundsCore(
        int x, int y, int width, int height, BoundsSpecified specified)
    {
        if(m_fixedHeight != -1)
        {
            // Restrict the height to the defined fixed height.
            height = m_fixedHeight;
        }
        else if(height < DefaultFixedHeight)
        {
            // Never allow the height to get smaller than the default fixed
            // height (to prevent it from disappearing), but otherwise place
            // no restriction on the height.
            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

C#
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralPart of Label Already Pin
shawkins11-Jun-09 10:47
shawkins11-Jun-09 10:47 
GeneralCan't find RuleControlBase Pin
tom.wyckoff21-Nov-06 9:07
tom.wyckoff21-Nov-06 9:07 
AnswerRe: Can't find RuleControlBase Pin
Stephen Quattlebaum21-Nov-06 9:15
Stephen Quattlebaum21-Nov-06 9:15 
GeneralRe: Can't find RuleControlBase Pin
tom.wyckoff21-Nov-06 10:00
tom.wyckoff21-Nov-06 10:00 
GeneralFinnaly Pin
User 226147411-Mar-06 12:04
User 226147411-Mar-06 12:04 
GeneralLicense Pin
Paul Whitehurst2-Mar-06 5:56
Paul Whitehurst2-Mar-06 5:56 
GeneralRe: License Pin
Stephen Quattlebaum2-Mar-06 6:19
Stephen Quattlebaum2-Mar-06 6:19 
GeneralProblems in VS2005 Pin
kobalcz16-Jan-06 4:28
kobalcz16-Jan-06 4:28 
GeneralRe: Problems in VS2005 Pin
Stephen Quattlebaum19-Jan-06 3:46
Stephen Quattlebaum19-Jan-06 3:46 
GeneralSize changes Pin
John Boyce25-Nov-04 3:34
John Boyce25-Nov-04 3:34 
GeneralRe: Size changes Pin
Nathan Baulch10-Dec-04 17:04
Nathan Baulch10-Dec-04 17:04 
GeneralRe: Size changes Pin
Stephen Quattlebaum14-Feb-05 6:31
Stephen Quattlebaum14-Feb-05 6:31 
GeneralRe: Size changes Pin
Jonathan Corwin15-Feb-05 3:51
Jonathan Corwin15-Feb-05 3:51 
GeneralPanels Pin
Chitty25-Aug-04 11:03
Chitty25-Aug-04 11:03 
GeneralRe: Panels Pin
Stephen Quattlebaum25-Aug-04 11:12
Stephen Quattlebaum25-Aug-04 11:12 
GeneralRule Pin
Carlos H. Perez15-Oct-03 15:04
Carlos H. Perez15-Oct-03 15:04 
GeneralRe: Rule Pin
Stephen Quattlebaum15-Oct-03 16:54
Stephen Quattlebaum15-Oct-03 16:54 
GeneralRe: Rule Pin
BillWoodruff21-Oct-03 6:51
professionalBillWoodruff21-Oct-03 6:51 
GeneralArticle Updated Pin
Stephen Quattlebaum14-Oct-03 5:22
Stephen Quattlebaum14-Oct-03 5:22 
GeneralOut of Town Pin
Stephen Quattlebaum2-Oct-03 3:26
Stephen Quattlebaum2-Oct-03 3:26 
GeneralDerive from System.Windows.Forms.Label Pin
Thomas Freudenberg2-Oct-03 1:45
Thomas Freudenberg2-Oct-03 1:45 
GeneralRe: Derive from System.Windows.Forms.Label Pin
Stephen Quattlebaum2-Oct-03 3:22
Stephen Quattlebaum2-Oct-03 3:22 
GeneralRe: Derive from System.Windows.Forms.Label Pin
Thomas Freudenberg2-Oct-03 5:00
Thomas Freudenberg2-Oct-03 5:00 
GeneralRe: Derive from System.Windows.Forms.Label Pin
NormDroid2-Oct-03 8:29
professionalNormDroid2-Oct-03 8:29 
GeneralRe: Derive from System.Windows.Forms.Label Pin
casperOne10-Oct-03 5:21
casperOne10-Oct-03 5:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.