Accessibility and UI Automation
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:
| Property | Value |
|---|---|
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:
| Attributes | TextPattern fields |
|---|---|
| Colors | BackgroundColorAttribute, ForegroundColorAttribute |
| Font | FontNameAttribute, FontSizeAttribute, FontWeightAttribute, IsItalicAttribute, IsSubscriptAttribute, IsSuperscriptAttribute, StrikethroughStyleAttribute, UnderlineStyleAttribute |
| Paragraph | HorizontalTextAlignmentAttribute, IndentationFirstLineAttribute, IndentationLeadingAttribute, IndentationTrailingAttribute, TabsAttribute, TextFlowDirectionsAttribute |
| Page | MarginBottomAttribute, MarginLeadingAttribute, MarginTopAttribute, MarginTrailingAttribute |
| Other | CultureAttribute, 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:
FindFirstwithIsTextPatternAvailablePropertyfinds the first element of the window that exposes its text, which is the editor in a window with oneTextControl.ClassNamedistinguishes the WPFTextControlfrom the one of Windows Forms; the client works with both.DocumentRange.GetText(-1)returns the whole text, andGetAttributeValuethe attribute of a range: 700 for bold text.TextSelectionChangedEventoccurs 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;
[]
internal static extern IntPtr SendMessageTimeout(IntPtr hWnd, int uMsg, IntPtr wParam,
IntPtr lParam, int flags, int uTimeout, out IntPtr result);
[]
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.StringToHGlobalUnicopies the string into a buffer of unmanaged memory, andlParampasses its address. Windows copies the buffer into the process of the editor, becauseEM_REPLACESELis a system message.FreeHGlobalreleases the buffer afterward.SendMessageTimeoutreturnsIntPtr.Zerowhen 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.messageResultis the answer of theTextControl: 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
TextControlsupports the keyboard shortcuts of a word processor. Keyboard Shortcuts lists them.In the ribbon, the WPF
Ribbonshows key tips when the user presses Alt. The items of the predefined tabs of TX Text Control have key tips. ARibbonButtonof 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) };KeyTipis a property of the WPFRibbonButton;GetKeyTipreturns the key tip in the language of the user interface. Custom Tabs and Commands adds such buttons to a tab.
Next Steps
- DPI and Display Scaling: a sharp editor at large scale factors.
- Keyboard Shortcuts: the keys of the editor.
- Commands and Input Bindings: keyboard commands of your own for the window.
- PDF/A: archivable and accessible PDF documents created from the editor's content.