Ausgangspunkt für diesen Artikel ist ein Skript, welches ich zur automatischen Steuerung einer Branchensoftware gebastelt habe. Da dies auf einem Industrie-PC läuft, auf dem ein vom Softwarehersteller modifiziertes Windows werkelt, wollte ich möglichst nur mit Bordmitteln ans Ziel. Auch wenn ich mich gerne mal wieder mit AutoIt beschäftigen würde, was ich in einer frühen Form vor Jahren mal angetestet habe, bin ich deshalb lieber bei der bereits vorhandenen Powershell geblieben.
Nur mit der Powershell habe ich es nicht hinbekommen etwas an die Anwendungen weiterzugeben und diese aktiv in den Vordergrund zu holen, deshalb habe ich folgendes aus VisualBasic eingebunden:
1 2 3 4 5 |
# VisualBasic-Klasse für App-Auswahl einbinden [void] [System.Reflection.Assembly]::LoadWithPartialName("'Microsoft.VisualBasic") # VisualBasic-Klasse für SendWait einbinden [void] [System.Reflection.Assembly]::LoadWithPartialName("'System.Windows.Forms") |
Mit folgendem Code kann man nun das Fenster einer offenen Anwendung anhand derer Prozess-ID auswählen und aktivieren und im Anschluss sendet man eine Tastatureingabe:
1 2 3 4 5 6 7 |
$process = "Notepad" # Das Fenster der Anwendung auswählen $a = Get-Process | Where-Object {$_.Name -eq $process} [Microsoft.VisualBasic.Interaction]::AppActivate($a.ID) [System.Windows.Forms.SendKeys]::SendWait("A") |
Das Ergebnis sieht dann wie folgt aus. In das in den Vordergrund geholte Editor-Fenster wurde der Buchstabe A geschrieben:
Aus irgendeinem Grund funktioniert der Wechsel zum gewünschten Fenster nur nicht, wenn vorher das Fenster der Powershell ISE als letztes aktives Fenster ausgewählt war.
Es gibt einige Berichte vom instabilen Arbeiten des „AppActivate“-Befehls, allerdings habe ich dies nur im beschriebenen eben Fall feststellen können. Da das Skript nachher sowieso als geplanter Task ausgeführt wird und nicht in der ISE, ist dies zumindest für mein Skript zu vernachlässigen.
Nun gibt es allerdings ein Problem, wenn eine Meldung bestätigt werden muss und dafür der Titel des Fensters der Meldung erkannt werden muss. Mit den Powershell-Befehlen dafür funktionierte dies leider nicht sehr zuverlässig.
Hierfür habe ich einen wirklich astreinen C#-Code gefunden, den man im Powershell-Skript ebenfalls einbinden kann:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; using System.Runtime.InteropServices; public static class User32 { private class unmanaged { // FindWindowByCaption [DllImport("user32.dll", EntryPoint="FindWindow", SetLastError = true)] internal static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName); } // FindWindowByCaption public static IntPtr FindWindowByCaption(string Title) { return unmanaged.FindWindowByCaption(IntPtr.Zero, Title); } } |
Leider finde ich grade den Blog-/Forumseintrag, wo ich diesen her habe, nicht mehr…
Nun steht der Verwendung von Schleifen und Abfragen nichts mehr im Weg, um komplexere Szenarien zu automatisieren. Für meinen Zweck muss eine Anwendung wieder zurück ins Start-Fenster, wenn die Benutzer vergessen haben eine Eingabemaske zu verlassen, weil nur dadurch alle eingegebenen Daten an ein anderes System zurückgegeben werden.
Da das Startfenster ein eigener Prozess ist, der beim Verlassen der Eingabemaske wieder gestartet wird, bietet es sich an mit einer Schleife zu arbeiten, die solange läuft bis der Prozess des Start-Fensters gestartet wurde. Denn dann wurde die Eingabemaske auf jeden Fall verlassen.
Welches Fenster bzw. welche Meldung im Vordergrund ist, lässt sich mit If-Abfragen in Kombination mit der C#-Funktion einfach herausfinden und kann dann mit entsprechend simulierten Tastenbefehlen bedient werden.
Das fertige Skript sieht bei mir nun 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 |
# VisualBasic-Klasse für App-Auswahl einbinden [void] [System.Reflection.Assembly]::LoadWithPartialName("'Microsoft.VisualBasic") # VisualBasic-Klasse für SendWait einbinden [void] [System.Reflection.Assembly]::LoadWithPartialName("'System.Windows.Forms") # C#-Code für das Auffinden aktiver Fenster mit bestimmten Fenster-Titeln einbinden $User32 = @" using System; using System.Runtime.InteropServices; public static class User32 { private class unmanaged { // FindWindowByCaption [DllImport("user32.dll", EntryPoint="FindWindow", SetLastError = true)] internal static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName); } // FindWindowByCaption public static IntPtr FindWindowByCaption(string Title) { return unmanaged.FindWindowByCaption(IntPtr.Zero, Title); } } "@ Add-Type -TypeDefinition $User32 $process = "Anwendung1" $process2 = "Anwendung2" do { # Herausfinden ob Anwendung1 offen ist $ProcessActive = Get-Process $process -ErrorAction SilentlyContinue # Wenn die Anwendung offen ist die Tasteneingaben emulieren if($ProcessActive -ne $null) { # Das Fenster der Anwendung auswählen $a = Get-Process | Where-Object {$_.Name -eq $process} [Microsoft.VisualBasic.Interaction]::AppActivate($a.ID) if ([User32]::FindWindowByCaption("Fenstertitel1") -ne 0){ # Die Tasteneingaben an die Anwendung schicken [System.Windows.Forms.SendKeys]::SendWait("{F2}") } elseif ([User32]::FindWindowByCaption("Fenstertitel2") -ne 0){ # Die Tasteneingaben an die Anwendung schicken [System.Windows.Forms.SendKeys]::SendWait("{F5}") } elseif ([User32]::FindWindowByCaption("Fenstertitel3") -ne 0){ # Die Tasteneingaben an die Anwendung schicken [System.Windows.Forms.SendKeys]::SendWait("{F5}") } } Start-Sleep 5 $ProcessActive2 = Get-Process $process2 -ErrorAction SilentlyContinue Start-Sleep 5 } until ($ProcessActive2 -ne $null) |
Als Alternative habe ich noch ein Snapin für Powershell gefunden, das sogenannte „Windows Automation Snapin for PowerShell“, kurz WASP. Damit habe ich mich aber nicht weiter beschäftigt, da es bei meinen ersten Tests nicht auf Anhieb funktionierte und ich schon mit den anderen Lösungsvorschlägen gut voran kam.