Setting tab stops in a Windows Forms TextBox control

I was adding a Wizard to one of my applications, and the final screen of this Wizard was a summary of the user's choices. I wanted the user to be able to copy this to the Clipboard if required, and so I'd used a TextBox rather than the ListView I might have otherwise used. However, this presented a minor issue as I'd chosen to use tabs to delimit the information, and the varying length of text meant that this wasn't aligned as expected.

Previously I have "dealt" with this issue by cheating - I'd just add extra tabs to force everything to line up. However, I do plan on fully localising this application at some point, not to mention that even using a different font could potentially trigger the text to misalign once more. And so I decided to do it properly this time.

I knew that Win32 Edit controls (of which the Windows Forms TextBox wraps) supported customising tab stops, but this was functionality the Framework developers did not expose. Similarly, the RichTextBox offers the ability to customise tab stops, but at a selection level and I really couldn't be fussed with that approach. So I decided to directly invoke the Win32 API to set the tab stops in a TextBox control. How hard could it be?

Introducing EM_SETTABSTOPS

I already knew in principle how to do this, by using the SendMessage API call and the EM_SETTABSTOPS message, although I didn't know the specifics. On checking the documentation for the message, it stated that when calling SendMessage with this message, wParam is used to define how the tab stops are set, whilst lParam has the tab stop values. The following operations are available

wParam Value lParam Value Description
0 0 Resets tab stops to default, which is every 32 dialog units
1 integer Sets all tab stops to be every integer dialog units
2 or more integer[] Sets custom tab stops (in dialog units) using each value in _integer[]_. Although not explicitly documented, the last value in the array is used for any additional tabs the user enters into the control

Incidentally, there's an odd omission. Almost invariably with the Win32 API, if there's a SET message, there's usually a corresponding GET as well, e.g. WM_SETTEXT and WM_GETTEXT. For some reason though, there is no EM_GETTABSTOPS - as far as I can tell, there isn't a way of getting tab stop information.

Getting Started

As the EM_SETTABSTOPS can be called several ways, I'm going to define 3 different overloads of SendMessage.

internal static class NativeMethods
{
  public const int EM_SETTABSTOPS = 0x00CB;

  [DllImport("user32", CharSet = CharSet.Auto)]
  public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

  [DllImport("user32", CharSet = CharSet.Auto)]
  public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, int[] lParam);

  [DllImport("user32", CharSet = CharSet.Auto)]
  public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref int lParam);
}

The documentation for the message states that the EM_SETTABSTOPS message doesn't cause the Edit control to be refreshed, although in my testing this didn't seem to be the case - the control was always repainted. My assumption is the TextBox control is refreshing itself when receiving certain messages, however I have chosen to also explicitly request a repaint by calling Invalidate.

Setting all tab stops to be the same fixed value

If you want all tab stops to be the same fixed value with no variations, you send EM_SETTABSTOPS with wParam set to 1 and lParam to the new tab size.

NativeMethods.SendMessage(textBox.Handle, NativeMethods.EM_SETTABSTOPS, 1, tabSize);

Setting custom tab stops

To specify tab stops of different sizes, you send EM_SETTABSTOPS with wParam with a value greater than one, and lParam with an array of tab stop positions.

Although the documentation states that this should be greater than one, I observed (on Windows 10 1809) that unless wParam was equal to the length of the array I was passing in, the control didn't behave exactly as expected.

NativeMethods.SendMessage(textBox.Handle, NativeMethods.EM_SETTABSTOPS, tabStops.Length, tabStops);

Important! The array of tab stops is specified as absolute positions, not the size of each stop, e.g each item in the array should be the sum of all previous entries plus the size of the tab stop. So for example, if you wanted tab sizes of 100, 60 and 60 dialog units, the values to send would be 100, 160 and 220.

Resetting tab stops

To reset tab stops, we send the EM_SETTABSTOPS message to our TextBox with a wParam value of 0. lParam is unsed in this case so I send 0 as well.

NativeMethods.SendMessage(textBox.Handle, NativeMethods.EM_SETTABSTOPS, 0, 0);

Introducing Dialog Units

I mentioned above that tab stops are expressed in terms of dialog units. But what are these? I certainly wasn't familiar with them, pretty much all API calls I've ever used express these types of values in plain pixels.

I can't actually find a dedicated topic in Microsoft's documentation, but essentially a dialog unit is a device independent way of specifying position and size information for dialog controls. I believe they are mostly used in resource templates, but as these are generally the province of C++ applications they aren't actually something I've used before.

As to the actual values of a dialog unit, they are equal to the average width, in pixels, of the characters in the font used by the dialog; the vertical base unit is equal to the height, in pixels, of the font.

There are also a set of base units, which are the same calculations but for the system font. I wonder how many modern Windows developers have seen the system font, given it hasn't been in widespread used since Windows 3.0!

Converting from Dialog Unit to Pixels

In order to convert from dialog units to pixels, you are supposed to be able to use the MapDialogRect function, by passing in the dialog handle and a RECT structure populated with the values to convert. The function will modify the RECT with the converted values, at least in theory. Unfortunately this is where things get complicated as the documentation states this function accepts only handles returned by one of the dialog box creation functions; handles for other windows are not valid. However, as Windows Forms doesn't use the dialog functions for creating windows, I was not able to get this API call to perform a conversion.

The documentation for GetDialogBaseUnits notes that you can use GetTextMetrics to calculate the values for a given font. I'd already written about this some time ago and so I did test this but the results were inconclusive, so I wonder if MapDialogRect is doing additional actions rather than just returning the average.

(In a move that doesn't surprise me in the slightest given my experiences with this functionality so far, there isn't a function to take pixel values and convert them into dialog units.)

Fortunately, it doesn't really matter1 as I suppose you don't really want pixel perfect tab stops. I certainly didn't, I just wanted rough values so "columns" would line up, and we can do a naive characters to dialog units conversion by multiplying the number of characters by 4. Not perfect, but good enough.

Auto detecting tab stops

I mentioned in the article preamble that I wanted to reformat summary text so that columns would align. Below is a function that will calculate rough tab stop positions based on a text string.

Note: This is rough and ready code and is doing a lot of string manipulation via string.Split so isn't very efficient at all. Originally I was going to count characters from tabs to avoid any type of string processing at all, but I've more than ran out of the time I'd allocated for this article.

public static int[] AutoDetectTabStops(string text)
{
  int[] result;

  if (!string.IsNullOrEmpty(text) && text.IndexOf('\t') != -1)
  {
    List<int> tabStops;
    string[] lines;

    tabStops = new List<int>();

    lines = text.Split(_lineSeparators, StringSplitOptions.RemoveEmptyEntries);

    for (int i = 0; i < lines.Length; i++)
    {
      string[] cells;

      cells = lines[i].Split(_cellSeparators);

      for (int j = 0; j < cells.Length; j++)
      {
        int estimatedDialogUnits;

        estimatedDialogUnits = cells[j].Length * 4;

        if (tabStops.Count <= j)
        {
          tabStops.Add(estimatedDialogUnits);
        }
        else if (estimatedDialogUnits > tabStops[j])
        {
          tabStops[j] = estimatedDialogUnits;
        }
      }
    }

    for (int i = 1; i < tabStops.Count; i++)
    {
      tabStops[i] += tabStops[i - 1];
    }

    result = tabStops.ToArray();
  }
  else
  {
    result = new[] { 32 };
  }

  return result;
}

public static bool AutoDetectTabStops(this TextBoxBase textBox, string text)
{
  if (textBox == null)
  {
    throw new ArgumentNullException(nameof(textBox));
  }

  return textBox.SetTabStops(AutoDetectTabStops(text));
}

public static bool AutoDetectTabStops(this TextBoxBase textBox)
{
  return textBox.AutoDetectTabStops(textBox.Text);
}

Demonstration project

As usual, a demonstration program is available for the link below, including a helper class for adding some useful tab stop related extension methods to the TextBox and RichTextBox controls.


1. Also, I lied. I suppose if you were displaying a ruler for a word processor then you'd very much want pixel perfect tab stops. I did try doing a basic ruler for this demonstration, but ended up having to cheat the calculations as I could get MapDialogRect to work, I didn't have time to do things like add scrolling support and at the end of the day it didn't really add much to the demo.

Downloads

Filename Description Version Release Date
TextBoxTabStops.zip
  • sha256: d3b220e4c5e4753d7b761ee0eb978600bdc14f6a9bbee50117e66a1a052d642c

Sample project for the Setting tab stops in a Windows Forms TextBox control blog post.

25/05/2019 Download

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