TX Text Control .NET for WPF

Accessibility and UI Automation

Topic

Assistive technologies such as screen readers and speech recognition, and tools for automated UI tests, work with a WPF window through Microsoft UI Automation. For most WPF controls, WPF provides this interface itself; the WPF TextControl implements its own, which exposes the text of the document with the formatting of each part. Through this interface, an automation client finds the editor and reads and changes its text; your window adds key tips and keyboard support for users who work without a mouse.

What the TextControl Exposes

The WPF TextControl has implemented UI Automation since version 25.0, and since version 27.0 it supports two control patterns, in WPF as in Windows Forms:

  • TextPattern exposes 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.

The blog article Sneak Peek X15: UI Automation Interface for TX Text Control .NET for WPF, written before version 25.0 was released, also names the SynchronizedInputPattern, and describes accessibility and automated UI testing as the two uses of the interface.

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

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

The TextPattern supports DocumentRange, GetSelection, GetVisibleRanges and RangeFromPoint, and raises TextSelectionChangedEvent when the input position changes. Its text ranges support GetText, Select, GetBoundingRectangles, GetEnclosingElement, ScrollIntoView, Clone, Compare, CompareEndpoints, ExpandToEnclosingUnit, Move, MoveEndpointByUnit, MoveEndpointByRange and GetAttributeValue.

Text Attributes a Client Can Read

GetAttributeValue returns these attributes of a text range:

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. A range whose characters differ in an attribute, such as partly bold text, returns TextPattern.MixedAttributeValue for it.

Read the Editor From Another Application

A UI Automation client is a separate application. The following console application finds the TextControl in the running application MyEditor, prints its text, reports whether the selection is bold and writes a line whenever the input position moves. It uses the namespace System.Windows.Automation, which a .NET project references with <UseWPF>true</UseWPF> and a target framework such as net8.0-windows:

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:

  • FindFirst with IsTextPatternAvailableProperty finds the first element of the window that exposes its text, which is the editor in a window with one TextControl.
  • ClassName distinguishes the WPF TextControl from the one of Windows Forms; the client works with both.
  • DocumentRange.GetText(-1) returns the whole text, and GetAttributeValue the attribute of a range: 700 for bold text.
  • TextSelectionChangedEvent occurs whenever the input position in the editor changes.

An automated UI test uses the same calls to compare the document with the expected result.

Change the Text From Outside

The ValuePattern replaces the complete text:

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

To insert text at the input position or replace the selection, a client sends the Win32 edit control messages EM_GETSEL, EM_SETSEL and EM_REPLACESEL, which the TextControl handles. A WPF control has no window handle of its own, but the automation element of the WPF TextControl provides the handle of a wrapper window as its AutomationId:

IntPtr hWnd = new IntPtr(int.Parse(textControl.Current.AutomationId));

EM_REPLACESEL inserts the text at the input position when nothing is selected. In the Windows Forms TextControl, which is a native window, textControl.Current.NativeWindowHandle gives the handle instead.

Send EM_REPLACESEL to the Editor

A .NET client imports the Win32 functions it needs from user32.dll. The following class declares the three message numbers, SendMessageTimeout to send a message with a time limit, and IsWindow to check the handle first:

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 the handle from the AutomationId, the client in the console application above inserts text at the input position of the editor, or in place of its selection:

using System.Runtime.InteropServices;

IntPtr hWnd = new IntPtr(int.Parse(textControl.Current.AutomationId));

if (Win32.IsWindow(hWnd))
{
    // Copy the text into unmanaged memory as a Unicode string
    IntPtr text = Marshal.StringToHGlobalUni("New text");
    try
    {
        // Flag 1 (SMTO_BLOCK), wait at most 10 seconds
        IntPtr timeResult = Win32.SendMessageTimeout(hWnd, Win32.EM_REPLACESEL,
            IntPtr.Zero, text, 1, 10000, out IntPtr messageResult);

        if (timeResult == IntPtr.Zero)
            Console.WriteLine("The editor did not answer in time.");
        else
            Console.WriteLine(messageResult == (IntPtr)1 ? "Text inserted." : "Text not inserted.");
    }
    finally
    {
        Marshal.FreeHGlobal(text);
    }
}

The code works as follows:

  • Marshal.StringToHGlobalUni copies the string into a buffer of unmanaged memory, and lParam passes its address. Windows copies the buffer into the process of the editor, because EM_REPLACESEL is a system message. FreeHGlobal releases the buffer afterward.
  • SendMessageTimeout returns IntPtr.Zero when the message failed or the 10,000 milliseconds passed before the editor processed it. Flag 1, SMTO_BLOCK, keeps the calling thread from processing other requests until the function returns.
  • messageResult is the answer of the TextControl: 1 when the text was inserted and 0 when it was not.

EM_GETSEL and EM_SETSEL, which read and set the selection, are sent with the same function. Microsoft documents SendMessageTimeout, its flags and the edit control messages.

Work Without a Mouse

  • In the document, the TextControl supports the keyboard shortcuts of a word processor. Keyboard Shortcuts lists them.

  • In the ribbon, the WPF Ribbon shows key tips when the user presses Alt. The items of the predefined tabs of TX Text Control have key tips. A RibbonButton of your own gets the key tip of a built-in command from ResourceProvider.GetKeyTip:

    string print = ResourceProvider.FileMenuItem.TXITEM_Print.ToString();
    RibbonButton printButton = new RibbonButton()
    {
        Label = ResourceProvider.GetText(print),
        KeyTip = ResourceProvider.GetKeyTip(print)
    };
    

    KeyTip is a property of the WPF RibbonButton; GetKeyTip returns the key tip in the language of the user interface. Custom Tabs and Commands adds such buttons to a tab.

Next Steps