TX Text Control .NET for Windows Forms

Accessibility and UI Automation

Topic

Screen readers, speech recognition software and automated UI tests don't see your application the way a user does: they ask Windows for its elements through Microsoft UI Automation. The TextControl answers these requests with the text of its document and the formatting of each part, and it can be named, reached by keyboard and changed from outside. Here is what TX Text Control .NET for Windows Forms exposes, how an accessibility tool or a test finds and reads the editor, and what your application adds on its side.

What the TextControl Exposes

Since version 27.0, the TextControl implements two control patterns of UI Automation, in Windows Forms and in WPF:

  • TextPattern gives access to the text of the document in ranges, and each range reports its format and style attributes, such as the font, the size, bold and italic, the alignment and the indents.
  • ValuePattern represents the complete text as one string, which a client can read and replace.

A UI Automation client recognizes the TextControl by these properties of its automation element:

PropertyValue
LocalizedControlType"document"
ClassName"TXTextControl.TextControl"
FrameworkId"WinForm"

The TextPattern of the TextControl supports DocumentRange, GetSelection, GetVisibleRanges and RangeFromPoint for getting text ranges, and raises TextSelectionChangedEvent when the input position changes. On a range, it supports GetText, Select, GetBoundingRectangles, GetEnclosingElement, ScrollIntoView, Clone, Compare, CompareEndpoints, ExpandToEnclosingUnit, Move, MoveEndpointByUnit, MoveEndpointByRange and GetAttributeValue.

Text Attributes a Client Can Read

GetAttributeValue of a text range returns these attributes of the text in the TextControl:

AttributesTextPattern fields
ColorsBackgroundColorAttribute, ForegroundColorAttribute
FontFontNameAttribute, FontSizeAttribute, FontWeightAttribute, IsItalicAttribute, IsSubscriptAttribute, IsSuperscriptAttribute, StrikethroughStyleAttribute, UnderlineStyleAttribute
ParagraphHorizontalTextAlignmentAttribute, IndentationFirstLineAttribute, IndentationLeadingAttribute, IndentationTrailingAttribute, TabsAttribute, TextFlowDirectionsAttribute
PageMarginBottomAttribute, MarginLeadingAttribute, MarginTopAttribute, MarginTrailingAttribute
OtherCultureAttribute, IsReadOnlyAttribute

StyleNameAttribute is not supported. For a range whose characters differ in an attribute, such as partly bold text, the value is TextPattern.MixedAttributeValue.

Read the Editor From Another Application

The following console application reads the TextControl of a running application named MyEditor: it prints the text of the document, reports whether the selected text is bold and writes a line whenever the user moves the input position. It uses the UI Automation client of .NET, in the namespace System.Windows.Automation; in a .NET project, set <UseWPF>true</UseWPF> and a target framework such as net8.0-windows in the project file to reference it.

using System;
using System.Diagnostics;
using System.Windows.Automation;
using System.Windows.Automation.Text;

// Find the main window of the running editor application
Process editor = Process.GetProcessesByName("MyEditor")[0];
AutomationElement window = AutomationElement.FromHandle(editor.MainWindowHandle);

// Find the first element that exposes its text through the TextPattern
AutomationElement textControl = window.FindFirst(TreeScope.Descendants,
    new PropertyCondition(AutomationElement.IsTextPatternAvailableProperty, true));

// Identify the element as a TextControl
string className = textControl.Current.ClassName;
Console.WriteLine($"{className}, {textControl.Current.FrameworkId}, {textControl.Current.LocalizedControlType}");

if (className == "TXTextControl.TextControl" || className == "TXTextControl.WPF.TextControl")
{
    TextPattern textPattern = (TextPattern)textControl.GetCurrentPattern(TextPattern.Pattern);

    // The complete text of the document
    Console.WriteLine(textPattern.DocumentRange.GetText(-1));

    // Is the selected text bold?
    TextPatternRange selection = textPattern.GetSelection()[0];
    object fontWeight = selection.GetAttributeValue(TextPattern.FontWeightAttribute);
    bool isBold = fontWeight != TextPattern.MixedAttributeValue && (int)fontWeight == 700;
    Console.WriteLine($"Selection is bold: {isBold}");

    // Report every move of the input position
    Automation.AddAutomationEventHandler(TextPattern.TextSelectionChangedEvent,
        textControl, TreeScope.Element,
        (sender, e) => Console.WriteLine("The selection has changed."));

    Console.ReadLine();
}

The code works as follows:

  • AutomationElement.FromHandle gets the element of the main window, and FindFirst searches its descendants for the first element that supports the TextPattern.
  • ClassName tells a TextControl of Windows Forms from one of WPF, so the same client works with both.
  • DocumentRange.GetText(-1) returns the whole text; -1 means no length limit.
  • GetAttributeValue(TextPattern.FontWeightAttribute) returns 700 for bold text and MixedAttributeValue for a selection that is partly bold.
  • TextSelectionChangedEvent is raised by the TextControl whenever the input position changes.

The same calls serve automated UI tests: a test starts the application, finds the editor this way and compares its text and attributes with the expected result.

Change the Text From Outside

The ValuePattern replaces the complete text of the document:

// The ValuePattern replaces the whole text of the document
if (textControl.TryGetCurrentPattern(ValuePattern.Pattern, out object pattern))
{
    ((ValuePattern)pattern).SetValue("New text");
}

The ValuePattern has no method to insert text at the input position or to replace the selection. For that, the TextControl handles the standard edit control messages of Win32, EM_GETSEL, EM_SETSEL and EM_REPLACESEL, which a client sends to the window of the editor. In Windows Forms, the TextControl is a native window, and its handle is the NativeWindowHandle property of the automation element:

IntPtr hWnd = new IntPtr(textControl.Current.NativeWindowHandle);

EM_REPLACESEL inserts the text at the input position when nothing is selected. Microsoft documents the messages and their parameters.

To send a message, the client imports two functions of user32.dll, SendMessageTimeout and IsWindow, and defines the numbers of the three messages:

using System;
using System.Runtime.InteropServices;

internal static class Win32
{
    internal const int EM_GETSEL = 0x00B0;
    internal const int EM_SETSEL = 0x00B1;
    internal const int EM_REPLACESEL = 0x00C2;

    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    internal static extern IntPtr SendMessageTimeout(IntPtr hwnd, int uMsg, IntPtr wParam,
        IntPtr lParam, int flags, int uTimeout, out IntPtr result);

    [DllImport("user32.dll", ExactSpelling = true)]
    internal static extern bool IsWindow(IntPtr hWnd);
}

With this class, the client inserts text at the input position of the TextControl, or replaces the selected text, with the handle hWnd from above:

if (Win32.IsWindow(hWnd))
{
    // Copy the text into an unmanaged buffer
    IntPtr text = Marshal.StringToHGlobalUni("New text");

    // Wait at most 10 seconds for the editor to process the message
    IntPtr timeResult = Win32.SendMessageTimeout(hWnd, Win32.EM_REPLACESEL,
        IntPtr.Zero, text, 1, 10000, out IntPtr messageResult);

    Marshal.FreeHGlobal(text);

    bool inserted = timeResult != IntPtr.Zero && messageResult.ToInt32() == 1;
}

The code works as follows:

  • IsWindow checks that the handle still belongs to a window, for example after the application of the editor has been closed.
  • Marshal.StringToHGlobalUni copies the string into unmanaged memory. The system passes this buffer on to the process of the editor, so the client needs no memory of its own in that process. Marshal.FreeHGlobal releases the buffer afterward.
  • SendMessageTimeout returns IntPtr.Zero when the 10,000 milliseconds passed before the text was inserted. messageResult is 1 when the text was inserted and 0 when it was not.

Name the Editor for Screen Readers

The TextControl, the ButtonBar, the RulerBar and the StatusBar derive from System.Windows.Forms.Control and inherit its accessibility properties. Give an editor a name and a description when a form has more than one, so that a screen reader can tell them apart:

// Name and describe the editor for screen readers
textControl1.AccessibleName = "Letter";
textControl1.AccessibleDescription = "The text of the letter to the customer";

AccessibleName, AccessibleDescription and AccessibleRole are also in the Properties window, in the Accessibility category.

Work Without a Mouse

Users who can't use a mouse work with the keyboard alone:

  • In the document, the TextControl supports the keyboard shortcuts of a word processor for moving, selecting, formatting and editing. Keyboard Shortcuts lists them.
  • In the ribbon, every item of the predefined tabs has a key tip, the letter the user presses after Alt to reach it. Give buttons of your own a key tip with KeyTip, or take the one of a built-in command from ResourceProvider.GetKeyTip. Tabs and groups have a KeyTip property as well.

Next Steps