Aligning Windows Forms custom controls to text baselines using C#

One of the nice things about the Visual Studio WinForms designers are the guidelines it draws onto design surfaces, aiding you in perfectly positioning your controls. These guidelines are known internally as snap lines, and by default each visual component inheriting from Control gets four of these, representing the values of the control's Margin property.

A problem arises when you have multiple controls that have different heights, and contain a display string - in this case aligning along one edge isn't going to work and will probably look pretty ugly. Instead, you more than likely want to align the different controls so that the text appears on the same line.

Fortunately for us developers, the designers do include this functionality - just not by default. After all, while all controls have a Text property, not all of them use it, and how could the default designers know where your owner-draw control is going to paint text?

The image above shows a Label, ComboBox and Button control all aligned along the text baseline (the magenta line). We can achieve the same thing by creating a custom designer.

Creating the designer

The first thing therefore is to create a new class and inherit from System.Windows.Forms.Design.ControlDesigner. You may also need to add a reference to System.Design to your project (which rules out Client Profile targets).

.NET conventions generally recommend that you put these types of classes in a sub-namespace called Design.

So, assuming I had a control named BetterTextBox, then the associated designer would probably look similar to the following.

using System.Windows.Forms.Design;

namespace DesignerSnapLinesDemo.Design
{
  public class BetterTextBoxDesigner : ControlDesigner
  {
  }
}

If you use a tool such as Resharper to fill in namespaces, note that by default it will try and use System.Web.UI.Design.ControlDesigner which unsurprisingly won't work for WinForms controls.

Adding a snap line

To add or remove snap lines, we override the SnapLines property and manipulate the list it returns. There are only a few snap lines available, the one we want to add is Baseline

For the baseline, you'll need to calculate where the control will draw the text, taking into consideration padding, borders, text alignments and of course the font. My previous article retrieving font and text metrics using C# describes how to do this.

public override IList SnapLines
{
  get
  {
    IList snapLines;
    int textBaseline;
    SnapLine snapLine;

    snapLines = base.SnapLines;
    
    textBaseline = this.GetTextBaseline(); // Font ascent

    // TODO: Increase textBaseline by anything else that affects where your text is rendered, such as
    // * The value of the Padding.Top property
    // * If your control has a BorderStyle
    // * If you reposition the text vertically for centering etc
    
    snapLine = new SnapLine(SnapLineType.Baseline, textBaseline, SnapLinePriority.Medium);

    snapLines.Add(snapLine);

    return snapLines;
  }
}

Note: Resharper seems to think the SnapLines property can return a null object. At least for the base WinForms ControlDesigner, this is not true and it will always return a list containing every possible snapline except for BaseLine

Linking the designer to your control

You can link your custom control to your designer by decorating your class with the System.ComponentModel.DesignerAttribute. If your designer type is in the same assembly as the control (or is referenced), then you can call it with the direct type as with the following example.

[Designer(typeof(BetterTextBoxDesigner))]
public class BetterTextBox : Control
{
}

However, if the designer isn't directly available to your control, all is not lost - the DesignerAttribute can also take a string value that contains the assembly qualified designer type name. Visual Studio will then figure out how to load the type if it can.

[Designer("DesignerSnapLinesDemo.Design.BetterTextBoxDesigner, DesignerSnapLinesDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
public class BetterTextBox : Control
{
}

After rebuilding the project, you'll find that your control now uses your designer rather than the default.

I seem to recall that when using older versions of Visual Studio once the IDE had loaded my custom designer contained in a source code project it seemed to cache it. This meant that if I then changed the designer code and recompiled, it wouldn't be picked up unless I restarted Visual Studio. I haven't noticed that happening in VS2015, so either I'm imagining the whole thing, or it was fixed. Regardless, if you get odd behaviour in older versions of VS, a restart of the IDE might be just what you need.

The following image shows a zoomed version of the BetterTextbox (which is just a garishly painted demo control and so is several lies for the price of one) showing all three controls are perfectly aligned to the magenta BaseLine guideline.

Bonus Chatter: Locking down how the control is sized

The default ControlDesigner allows controls to be resized along any edge at will. If your control automatically sets its height or width to fit its contents, then this behaviour can be undesirable. By overriding the SelectionRules property, you can define how the control can be processed. The following code snippet shows an example which prevents the control from being resized vertically, useful for single-line text box style controls.

public override SelectionRules SelectionRules
{
  get { return SelectionRules.Visible | SelectionRules.Moveable | SelectionRules.LeftSizeable | SelectionRules.RightSizeable; }
}

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

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?

Styling with Markdown is supported