Wie man beliebige Automation​Properties (HelpText, Name, AutomationId u. a.) in WinForms setzt

Wie man beliebige Automation​Properties (HelpText, Name, AutomationId u. a.) in WinForms setzt

An sich ist es recht einfach, die 1-2 vorgegebenen Properties für jedes WinForms Control zu setzen. Wem das nicht reicht, der muß etwas weiter ausholen.

Wer ein WinForms Control instanziiert, kann direkt auf die Eigenschaften control.Name, control.AccessibleName (beide nützlich) sowie control.AccessibleDefaultActionDescription und controlAccessibilityObject (beide bislang ohne beobachteten Nutzen) zugreifen.

Wer sich einmal in einem Spy-Programm (Inspect z. B., siehe auch StackOverflow) alle vorhandenen Properties angeschaut hat, findet dort u. a. auch den „HelpText“. Nachdem es mich lange gewurmt hat, wie man diesen Wert denn nun setzen kann, bin ich schließlich auf den passenden Seiten bei MSDN gelandet. Und nicht nur das, es gibt sogar ein funktionierendes Beispiel dort. Freude über Freude.

Im Wesentlichen muß man folgende Schritte durchführen:

  1. für das Control IRawElementProviderSimple implementieren, inkl. der Funktionen, die eigentlich nicht relevant sind – wie das bei einem Interface so ist
  2. dem Control eine eigene WndProc mitgeben, die das entsprechende Interface zurückgibt
  3. die für uns spannende Funktion IRawElementProviderSimple.GetPropertyValue implementieren

.

Das sieht dann ungefähr so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// http://msdn.microsoft.com/en-us/library/ms771658(v=vs.90).aspx
class MyButton : Button, IRawElementProviderSimple
{
    #region IRawElementProviderSimple
 
    /// <summary>
    /// Handles WM_GETOBJECT message; others are passed to base handler.
    /// </summary>
    /// <param name="m">Windows message.</param>
    /// <remarks>This method provides the link with UI Automation.</remarks>
    [PermissionSetAttribute(SecurityAction.Demand, Unrestricted = true)]
    protected override void WndProc(ref Message m)
    {
        // 0x3D == WM_GETOBJECT
        if ((m.Msg == 0x3D) && (m.LParam.ToInt32() == AutomationInteropProvider.RootObjectId))
        {
            m.Result = AutomationInteropProvider.ReturnRawElementProvider(
                Handle, m.WParam, m.LParam, (IRawElementProviderSimple)this);
            return;
        }
        base.WndProc(ref m);
    }
 
    /// <summary>
    /// Returns the object that supports the specified pattern.
    /// </summary>
    /// <param name="patternId">ID of the pattern.</param>
    /// <returns>Object that implements IInvokeProvider.</returns>
    object IRawElementProviderSimple.GetPatternProvider(int patternId)
    {
        if (patternId == InvokePatternIdentifiers.Pattern.Id)
        {
            return this;
        }
        else
        {
            return null;
        }
    }
 
    /// <summary>
    /// Returns property values.
    /// </summary>
    /// <param name="propId">Property identifier.</param>
    /// <returns>Property value.</returns>
    object IRawElementProviderSimple.GetPropertyValue(int propId)
    {
        if (propId == AutomationElementIdentifiers.ClassNameProperty.Id)
        {
            return "Super Great Test Button";
        }
        else if (propId == AutomationElementIdentifiers.ControlTypeProperty.Id)
        {
            return ControlType.Button.Id;
        }
        if (propId == AutomationElementIdentifiers.HelpTextProperty.Id)
        {
            return "This is a crazy help text.";
        }
        if (propId == AutomationElementIdentifiers.IsEnabledProperty.Id)
        {
            return true;
        }
        else
        {
            return null;
        }
    }
 
    /// <summary>
    /// Tells UI Automation that this control is hosted in an HWND, which has its own
    /// provider.
    /// </summary>
    IRawElementProviderSimple IRawElementProviderSimple.HostRawElementProvider
    {
        get
        {
            return AutomationInteropProvider.HostProviderFromHandle(Handle);
        }
    }
 
    /// <summary>
    /// Retrieves provider options.
    /// </summary>
    ProviderOptions IRawElementProviderSimple.ProviderOptions
    {
        get
        {
            return ProviderOptions.ServerSideProvider;
        }
    }
    #endregion IRawElementProviderSimple
}

und kann dann ganz normal benutzt werden. Aus meinem Button wird dann ein MyButton.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Stack<myButton> buttons = new Stack<myButton>();
int butCnt = 0;
private void CreateOneButton(Control parent)
{
    MyButton but = new MyButton();
    but.Location = new Point(100, 25);
    but.Size = new Size(80, 30);
    but.Name = "same";
    but.AccessibleName = "sameId";
    but.AccessibleDefaultActionDescription = "You can click me.";
    but.AccessibilityObject.Value = "My cool value.";
    but.Text = "my button";
    int myNumber = butCnt; // each button should have its own number/id
    but.Click += (s, ea) =>
    {
        labelResultButton.Text = myNumber.ToString();
    };
    parent.Controls.Add(but);
    but.BringToFront();
    buttons.Push(but);
    ++butCnt;
}

Solche Buttons lassen sich natürlich auch prächtig dynamisch in unsere GUI einfügen. Um sie nachher noch unterscheiden zu können, aber per UI möglichst schwer unterscheidbar zu machen (meine Anforderung – natürlich gibt man seinen Elementen sonst brauchbare Eigenschaften mit!) füge ich jeweils eine ClickMethode hinzu, die einen anderen Wert in der GUI updatet.

Click-Event mit Lambda

Besonders erwähnenswert ist hier nur, daß ich die statische Variable zuvor einer lokalen Variable zuweise. Würde ich direkt die statische benutzen, so würde der Lambda-Ausdruck immer auf den aktuellen (veränderten) Wert zugreifen. (Das will ich nicht, ich möchte den Wert zum Zeitpunkt der Erstellung haben.) So wird der Wert wohl als Konstante übergeben. Immerhin wäre die lokale Variable ja zum Ausführungszeitpunkt – typischweise – nicht mehr vorhanden.

Damit gibt es ein sehr simples Programm, das als Automation Provider (Server) herhalten kann. Nebenbei werden noch dynamisch Controls (in diesem Fall ein Button) erzeugt, wobei hier auch nicht allzuviel dabei ist – wichtig ist, daß der Button im Parent-Control hinzugefügt wird, sonst wird er auch nicht korrekt verwaltet und angezeigt.

Aufgrund der Einfachheit der Anwendung habe ich keine Fehlerfälle abgefangen (leeren Stack poppen). Ich bin mein einziger Benutzer (und alle anderen lesen den Artikel hier) und weiß um die Unzulänglichkeit.


Das Ganze gibt es auch noch als fertige Solution zum Download (WindowsFormsDynamic) (Visual Studio 2008).

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.