Unter Fernsteuerung eines Programms verstehe ich, dieses Programm, die „GUI“, von einem anderen Programm aus so zu bedienen, als würde der Benutzer sie mit Maus und Tastatur selbst bedienen.
Ich verstehe nicht darunter, eine Seite in einem Webbrowser zu bedienen.
"Fenster" ist im folgenden ein Oberbegriff für alle möglichen Controls, das, was in C/C++ ein "Fensterhandle" (Handle) hat.
Was wollen wir alles bei der Fernsteuerung tun?
- Fenstertexte lesen
- Fenstertexte schreiben
- Button klicken
- CheckBoxen checken und unchecken
- Child-Dialoge identifizieren
Damit ist im Prinzip alles erfasst, alles weitere lässt sich darauf zurückführen.
Zunächst benötigen wir das Fenster-Handle des zu steuernden GUI selbst, dies bekommen wir als Property
Fangen wir an mit dem Identifizieren aller Controls (Child-Fenster) auf der anzusteuernden GUI.
Der Witz dabei ist der, dass beim Durchklickern die Reihenfolge dieser Controls immer (bei jedem Neustart) dieselbe ist. Man muss sich also nur ein Mal die Arbeit machen, alle Fenster zuzuordnen, dann genügt es, aus der Liste der Controls per Index das richtige / die richtigen herauszupicken.
Dazu verwenden wir die API-Funktion
Diese erwartet als Parameter zunächst das Fensterhandle der GUI.
Der 2. Parameter ist ein Delegate auf eine Call-Back-Funktion, in der wir die uns vom System übergebene Information für jedes einzelne Fenster behandeln können.
Der 3. Parameter ist der Pointer auf das Handle einer Liste, in der die Call-Back-Funktion die gesuchte Information ablegt.
Resultat ist eine Liste mit den Window-Handles aller Controls. Hier werden native Methoden und managed IEnumerables gehörig verwoben, so sieht der Code auch aus.
Aufgerufen wird diese Funktion wie folgt, ich hab das ganze mal als eine
Der Ablauf ist folgender:
Wir rufen auf:
Die Prozedur
Falls was passiert oder auch nicht, werden die unmanaged Überreste im
Das Identifizieren aller Controls erfolgt nun in 2 Schritten, einmal der "Design-Time", einmal der "Runtime".
Bei der "Design-Time" ermitteln wir die Indizes der Controls, die wir benötigen, bei "Runtime" erfolgt nur noch der entsprechende Array-Index-Zugriff.
Bei der Design-Time können z.B. der Window-Text aller gefundener Controls durch einen eigenen Text ersetzt werden (z.B. durch eine laufende Nummer), so können die Controls ganz easy identifiziert werden.
Dazu verwenden wir die Prozedur
Zur Demonstration habe ich ein kleines C++-Programm geschrieben, dessen Controls wollen wir nun enumerieren.
Zuerst wird die "GUI" gestartet, dann enumerieren wir die Controls und weisen ihnen einen anderen signifikanten Text zu:
Und das passiert, die Reihenfolge ist bei jedem Start der GUI dieselbe:
vorher:
==> nachher: 
Fortsetzung folgt ...
Ich verstehe nicht darunter, eine Seite in einem Webbrowser zu bedienen.
"Fenster" ist im folgenden ein Oberbegriff für alle möglichen Controls, das, was in C/C++ ein "Fensterhandle" (Handle) hat.
Was wollen wir alles bei der Fernsteuerung tun?
- Fenstertexte lesen
- Fenstertexte schreiben
- Button klicken
- CheckBoxen checken und unchecken
- Child-Dialoge identifizieren
Damit ist im Prinzip alles erfasst, alles weitere lässt sich darauf zurückführen.
Zunächst benötigen wir das Fenster-Handle des zu steuernden GUI selbst, dies bekommen wir als Property
MainWindowHandle
der kommunizierenden Process-Instanz:Fangen wir an mit dem Identifizieren aller Controls (Child-Fenster) auf der anzusteuernden GUI.
Der Witz dabei ist der, dass beim Durchklickern die Reihenfolge dieser Controls immer (bei jedem Neustart) dieselbe ist. Man muss sich also nur ein Mal die Arbeit machen, alle Fenster zuzuordnen, dann genügt es, aus der Liste der Controls per Index das richtige / die richtigen herauszupicken.
Dazu verwenden wir die API-Funktion
EnumChildWindows()
. Der 2. Parameter ist ein Delegate auf eine Call-Back-Funktion, in der wir die uns vom System übergebene Information für jedes einzelne Fenster behandeln können.
Der 3. Parameter ist der Pointer auf das Handle einer Liste, in der die Call-Back-Funktion die gesuchte Information ablegt.
Resultat ist eine Liste mit den Window-Handles aller Controls. Hier werden native Methoden und managed IEnumerables gehörig verwoben, so sieht der Code auch aus.
Aufgerufen wird diese Funktion wie folgt, ich hab das ganze mal als eine
List<IntPtr>
implementiert:C#-Quellcode
- public static List<IntPtr> GetChildWindows(IntPtr parent)
- {
- List<IntPtr> result = new List<IntPtr>();
- GCHandle listHandle = GCHandle.Alloc(result);
- try
- {
- EnumWindowProc childProc = new EnumWindowProc(EnumWindow);
- EnumChildWindows(parent, childProc, GCHandle.ToIntPtr(listHandle));
- }
- finally // ordentlich aufräumen, falls was passiert, denn das sind unmanaged Ressourcen
- {
- if (listHandle.IsAllocated)
- {
- listHandle.Free();
- }
- }
- return result;
- }
- private static bool EnumWindow(IntPtr handle, IntPtr pointer)
- {
- GCHandle gch = GCHandle.FromIntPtr(pointer);
- List<IntPtr> list = gch.Target as List<IntPtr>;
- if (list == null)
- {
- throw new InvalidCastException("GCHandle Target could not be cast as List<IntPtr>");
- }
- list.Add(handle);
- // You can modify this to check to see if you want to cancel the operation, then return a null here
- return true;
- }
Wir rufen auf:
Die Prozedur
EnumChildWindows()
klickert alle Child-Fenster einer GUI durch und für jedes gefundene Fenster wird die Callback-Funktion EnumWindow
genau ein al aufgerufen und legt das übergebene Fensterhandle in der List<IntPtr>
ab. Da der Aufruf aus dem unmanaged Bereich kommt, wird der Pointer auf diese List mit "durchgetunnelt".Falls was passiert oder auch nicht, werden die unmanaged Überreste im
finally
-Block aufgeräumt.Das Identifizieren aller Controls erfolgt nun in 2 Schritten, einmal der "Design-Time", einmal der "Runtime".
Bei der "Design-Time" ermitteln wir die Indizes der Controls, die wir benötigen, bei "Runtime" erfolgt nur noch der entsprechende Array-Index-Zugriff.
Bei der Design-Time können z.B. der Window-Text aller gefundener Controls durch einen eigenen Text ersetzt werden (z.B. durch eine laufende Nummer), so können die Controls ganz easy identifiziert werden.
Dazu verwenden wir die Prozedur
SendMessage
in ihrer .NET-Ausprägung mit dem letzten Parameter (LPARAM) als string (später kommt noch eine Ausprägung mit LPARAM als int):Zur Demonstration habe ich ein kleines C++-Programm geschrieben, dessen Controls wollen wir nun enumerieren.
Zuerst wird die "GUI" gestartet, dann enumerieren wir die Controls und weisen ihnen einen anderen signifikanten Text zu:
C#-Quellcode
- private void btnStart_Click(object sender, EventArgs e)
- {
- this._Process = new Process();
- this._Process.Exited += this.TestGui_Exited;
- this._Process.StartInfo.FileName = "FernsteuerungTestGui.exe";
- this._Process.Start();
- this._Process.WaitForInputIdle();
- this._MainWnd = this._Process.MainWindowHandle;
- }
vorher:
Fortsetzung folgt ...
Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch
Ein guter .NET-Snippetkonverter (der ist verfügbar).
Programmierfragen über PN / Konversation werden ignoriert!
Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch

Ein guter .NET-Snippetkonverter (der ist verfügbar).
Programmierfragen über PN / Konversation werden ignoriert!