Benutzer mit den meisten Antworten
PS Forms - Laufenden Prozess ueber Cancel-Button beenden

Frage
-
Hallo,
ich habe eine kleine GUI gebastelt.
Die GUI enthält einen Copy und einen Cancel Button.
Durch "Klick" auf den Button Copy sollen logischerweise Daten kopiert werden.
Durch "Klick" auf den Button Cancel, soll der Kopiervorgang ggf. abgebrochen werden können.
Jetzt ist es ja so, dass während des Kopiervorganges die GUI eingefroren ist und der Cancel-Button
gar nicht betätigt werden kann bzw. keine Funktion hat.
Ich habe durch recherchieren öfters den Hinweis auf Multithreading, Runspaces und Jobs gefunden, allerdings
sahen mir die Beispiele meistens so aus, als wenn ich sie für die Lösung meines Problems nicht wirklich
gebrauchen könnte bzw. wüsste ich nicht, wie ich es sinnvoll anwenden könnte.
Hat jemand vielleicht einen Beispielcode oder Codeschnipsel, der meine beschriebene Problematik löst bzw. ansehnlicher
veranschaulicht. Ich erwarte keinen fertigen Code, aber vielleicht einen Ansatz in die richtige Richtung. Bitte keine
Schlagwörter wie Runspaces, Jobs oder Multithreading, denn das hat mich bisher leider auch noch nicht weiter gebracht.
Vielen Dank im voraus!
Habe den Code mal mit angehängt:
clear # Load Assemblies [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | out-null [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | out-null # Init Form $Form = New-Object System.Windows.Forms.Form $Form.width = 400 $Form.height = 600 $Form.Backcolor=“white“ $Form.Text = "Test" # Init ProgressBar $pbrTest = New-Object System.Windows.Forms.ProgressBar $pbrTest.Maximum = 100 $pbrTest.Minimum = 0 $pbrTest.Location = new-object System.Drawing.Size(10,10) $pbrTest.size = new-object System.Drawing.Size(200,50) $i = 0 $Form.Controls.Add($pbrTest) # Button $btnConfirm = new-object System.Windows.Forms.Button $btnConfirm.Location = new-object System.Drawing.Size(250,10) $btnConfirm.Size = new-object System.Drawing.Size(100,30) $btnConfirm.Text = "Start Progress" $Form.Controls.Add($btnConfirm) ### Cancel $ButtonCancel = New-Object System.Windows.Forms.Button $ButtonCancel.Location = New-Object System.Drawing.Size(250,50) $ButtonCancel.Size = New-Object System.Drawing.Size(100,30) $ButtonCancel.Font = New-Object System.Drawing.Font("Consolas", 8 ,[System.Drawing.FontStyle]::Bold) $ButtonCancel.BackColor = [System.Drawing.Color]::Red $ButtonCancel.ForeColor = [System.Drawing.Color]::Yellow $ButtonCancel.Add_MouseHover({$ButtonCancel.backcolor = [System.Drawing.Color]::Green}) $ButtonCancel.Add_MouseLeave({$ButtonCancel.backcolor = [System.Drawing.Color]::Red}) $ButtonCancel.Text = "Cancel" $ButtonCancel.Add_Click({ Write-Host "BREAK !!!" }) $Form.Controls.Add($ButtonCancel) ### Output box $LabelOutput = New-Object System.Windows.Forms.Label $LabelOutput.Location = New-Object System.Drawing.Size(10,90) $LabelOutput.Size = New-Object System.Drawing.Size(80,25) $LabelOutput.Font = New-Object System.Drawing.Font("Consolas", 8 ,[System.Drawing.FontStyle]::Bold) $LabelOutput.ForeColor = [System.Drawing.Color]::Green $LabelOutput.Text = "Result(s): " $Form.Controls.Add($LabelOutput) $outputBox = New-Object System.Windows.Forms.TextBox $outputBox.Location = New-Object System.Drawing.Size(10,115) $outputBox.Size = New-Object System.Drawing.Size(350,400) $outputBox.MultiLine = $True $outputBox.ScrollBars = "Vertical" $Form.Controls.Add($outputBox) ################################################################################ ### Statusbar ################################################################################ $statusBar1 = New-Object System.Windows.Forms.StatusBar $statusBar1.Name = "statusBar1" $statusBar1.Text = "Status: ... Waiting ..." $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Width = 284 $System_Drawing_Size.Height = 22 $statusBar1.Size = $System_Drawing_Size $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 0 $System_Drawing_Point.Y = 240 $statusBar1.Font = New-Object System.Drawing.Font("Consolas", 8 ,[System.Drawing.FontStyle]::Bold) $statusBar1.Location = $System_Drawing_Point $statusBar1.DataBindings.DefaultDataSourceUpdateMode = 0 $statusBar1.TabIndex = 1 $Form.Controls.Add($statusBar1) # Button Click Event to Run ProgressBar $dest_path = "C:\TEST1 $data_path = "C:\TEST2 $robo_log = "C:\TEST\robo.log" $top_dirs = Get-ChildItem -Path $data_path -Directory $top_dirs = $top_dirs.Name $btnConfirm.Add_Click({ $i=0 foreach ($folder in $top_dirs) { Write-Host "folder: $($folder)" ### calculate percentage $i++ $pbrTest.Value = ($i/$top_dirs.count)*100 ### update the progress bar Start-Sleep -m 1 $folder_content = (gci -recurse $data_path\$folder) # $folder_content = $folder_content.Name Write-Host "folder_content: $folder_content" robocopy $data_path\$folder $dest_path\$folder /s /ndl /njh /njs /np /ns /nc /mir /log:$robo_log $robo_log_content = Get-Content "$robo_log" foreach ($log_entry in $robo_log_content){ $outputBox.AppendText("$($log_entry) `r`n") } ### show folder_content in outputBox # $outputBox.AppendText("$($folder_content) `r`n") } }) <# $btnConfirm.Add_Click({ While ($i -le 100) { Write-Host "i: $($i)" $pbrTest.Value = $i Start-Sleep -m 1 "VALLUE EQ" $i $i += 1 robocopy $data_path\$folder $dest_path\$folder /s /ndl /njh /njs /np /ns /nc /mir /Tee } }) #> # Show Form $Form.Add_Shown({$Form.Activate()}) $Form.ShowDialog()
- Bearbeitet yelw0rc Dienstag, 1. Oktober 2019 10:14
Antworten
-
Ich hab dir mal ein kleines Beispiel gebastelt:
Add-Type -AssemblyName System.Windows.Forms $form = New-Object System.Windows.Forms.Form $form.ClientSize = '300,300' $form.topmost = $true $BnStart = New-Object System.Windows.Forms.Button $BnStart.Text = "Start" $BnStart.size = "200,20" $BnStart.Location = "50,50" $form.Controls.Add($BnStart) $BnCancel = New-Object System.Windows.Forms.Button $BnCancel.Text = "Cancel" $BnCancel.size = "200,20" $BnCancel.Location = "50,100" $BnCancel.Enabled = $false $form.Controls.Add($BnCancel) $LbStatus = New-Object System.Windows.Forms.Label $LbStatus.Text = "......" $LbStatus.size = "200,20" $LbStatus.Location = "50,200" $form.Controls.Add($LbStatus) $jobScript = { # der copy job! Start-Sleep 10 Write-Output "Copy job ist erledigt." } $BnCancel.add_click({ Stop-Job -Job $job $LbStatus.Text = $job.state $BnCancel.Enabled = $false $BnStart.Enabled = $false }) $BnStart.add_click({ $BnCancel.Enabled = $true $BnStart.Enabled = $false #starte job $LbStatus.Text = "Kopiere..." $job = Start-Job -ScriptBlock $jobScript
# warte auf das Ende des jobs, aber reagiere weiter auf events Do {[System.Windows.Forms.Application]::DoEvents()} Until ($job.State -ne "Running") $LbStatus.Text = $job.state $BnCancel.Enabled = $false $BnStart.Enabled = $true }) $form.ShowDialog()
Grüße, Denniver
Blog: http://www.bytecookie.de
Powershell Code Manager: Link
(u.a. Codesnippets verwalten + komplexe Scripte graphisch darstellen)
Hilf mit und markiere hilfreiche Beiträge mit dem "Abstimmen"-Button (links) und Beiträge die eine Frage von dir beantwortet haben, als "Antwort" (unten).
Warum das Ganze? Hier gibts die Antwort.
- Bearbeitet Denniver ReiningMVP, Moderator Dienstag, 8. Oktober 2019 11:14
- Als Antwort vorgeschlagen Denniver ReiningMVP, Moderator Donnerstag, 10. Oktober 2019 10:32
- Als Antwort markiert Denniver ReiningMVP, Moderator Montag, 14. Oktober 2019 10:40
-
Statt der Schleife
Do {[System.Windows.Forms.Application]::DoEvents()} Until ($job.State -ne "Running")
empfehle ich einen Timerevent, denn die Schleife bringt dir sofort 100% Last auf einer CPU.
Der DoEvents unterbricht den Prozess nicht. Es wird nur kurz abgefragt, ob Windowsmessages vorliegen.https://docs.microsoft.com/de-de/dotnet/api/system.windows.forms.timer?view=netframework-4.8
Zu empfehlen ist auch hier mindestens ein Timer von 20ms, da kürzere Timer wiederum durch Prozessorlast auffallen.
- Als Antwort vorgeschlagen Denniver ReiningMVP, Moderator Donnerstag, 10. Oktober 2019 10:32
- Als Antwort markiert Denniver ReiningMVP, Moderator Montag, 14. Oktober 2019 10:40
Alle Antworten
-
Powershell unterstützt direkt kein Threading, man kann mittls "Start-Job" eine Aufgabe asynchron durchführen.
Der Returnwert gibt dir Zugriff auf den Job.https://www.msxfaq.de/code/powershell/psparallel.htm
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/start-job?view=powershell-6Im Abbruchfall kann man per Remove-Job den Batchjob canceln.
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/remove-job?view=powershell-6- Als Antwort vorgeschlagen Denniver ReiningMVP, Moderator Sonntag, 6. Oktober 2019 22:26
- Nicht als Antwort vorgeschlagen Denniver ReiningMVP, Moderator Dienstag, 8. Oktober 2019 11:15
-
Hallo,
danke für die Antwort. Ist ja alles schön und gut, aber mir leuchtet trotzdem nicht ein,
wie das funktionieren soll. Vielleicht ist PowerShell für so eine Art vorhaben auch nicht wirklich geeignet.
Meine erste Idee war:
start-job -scriptblock { ### Cancel $ButtonCancel = New-Object System.Windows.Forms.Button $ButtonCancel.Location = New-Object System.Drawing.Size250,50) $ButtonCancel.Size = New-Object System.Drawing.Size(100,30) $ButtonCancel.Font = New-Object System.Drawing.Font("Consolas", 8 ,[System.Drawing.FontStyle]::Bold) $ButtonCancel.BackColor = [System.Drawing.Color]::Red $ButtonCancel.ForeColor = [System.Drawing.Color]::Yellow $ButtonCancel.Add_MouseHover({$ButtonCancel.backcolor = [System.Drawing.Color]::Green}) $ButtonCancel.Add_MouseLeave({$ButtonCancel.backcolor = [System.Drawing.Color]::Red}) $ButtonCancel.Text = "Cancel" $ButtonCancel.Add_Click({ kill robocopy }) $Form.Controls.Add($ButtonCancel) }
Wenn das so funktionieren würde, wäre das wahrscheinlich schon zu viel Magie.
So wird der "Cancel-Button" natürlich gar nicht mehr angezeigt. Ist er aber, wie in meinem vorherigen Post,
von Anfang an deklariert, ist er nicht anklickbar, da
der robocopy Kopierprozess die GUI blockiert.
Mir scheint es, als wäre das so gar nicht wirklich in PowerShell umsetzbar, oder kann mich jemand eines besseren belehren?
Gruß,
yelw0rc
-
Start-Job startet einen Subprozess und wird daher nicht blockiert.
Nun kommt das Event-Handling dazu.
Per ShowDialog() wird der Hauptthread blockiert und wartet auf Ereignisse der Form.
Zusätzlich kann man nun einen Timer-Event starten, der z.B. alle 1000ms den Status der/des Jobs prüft um ggf. den nächsten zu starten.
Wenn der Cancel-Button gedrückt ist, kannst du den Batchjob dann killen und den Dialog z.B. mit Hide() beenden. -
Ich hab dir mal ein kleines Beispiel gebastelt:
Add-Type -AssemblyName System.Windows.Forms $form = New-Object System.Windows.Forms.Form $form.ClientSize = '300,300' $form.topmost = $true $BnStart = New-Object System.Windows.Forms.Button $BnStart.Text = "Start" $BnStart.size = "200,20" $BnStart.Location = "50,50" $form.Controls.Add($BnStart) $BnCancel = New-Object System.Windows.Forms.Button $BnCancel.Text = "Cancel" $BnCancel.size = "200,20" $BnCancel.Location = "50,100" $BnCancel.Enabled = $false $form.Controls.Add($BnCancel) $LbStatus = New-Object System.Windows.Forms.Label $LbStatus.Text = "......" $LbStatus.size = "200,20" $LbStatus.Location = "50,200" $form.Controls.Add($LbStatus) $jobScript = { # der copy job! Start-Sleep 10 Write-Output "Copy job ist erledigt." } $BnCancel.add_click({ Stop-Job -Job $job $LbStatus.Text = $job.state $BnCancel.Enabled = $false $BnStart.Enabled = $false }) $BnStart.add_click({ $BnCancel.Enabled = $true $BnStart.Enabled = $false #starte job $LbStatus.Text = "Kopiere..." $job = Start-Job -ScriptBlock $jobScript
# warte auf das Ende des jobs, aber reagiere weiter auf events Do {[System.Windows.Forms.Application]::DoEvents()} Until ($job.State -ne "Running") $LbStatus.Text = $job.state $BnCancel.Enabled = $false $BnStart.Enabled = $true }) $form.ShowDialog()
Grüße, Denniver
Blog: http://www.bytecookie.de
Powershell Code Manager: Link
(u.a. Codesnippets verwalten + komplexe Scripte graphisch darstellen)
Hilf mit und markiere hilfreiche Beiträge mit dem "Abstimmen"-Button (links) und Beiträge die eine Frage von dir beantwortet haben, als "Antwort" (unten).
Warum das Ganze? Hier gibts die Antwort.
- Bearbeitet Denniver ReiningMVP, Moderator Dienstag, 8. Oktober 2019 11:14
- Als Antwort vorgeschlagen Denniver ReiningMVP, Moderator Donnerstag, 10. Oktober 2019 10:32
- Als Antwort markiert Denniver ReiningMVP, Moderator Montag, 14. Oktober 2019 10:40
-
Statt der Schleife
Do {[System.Windows.Forms.Application]::DoEvents()} Until ($job.State -ne "Running")
empfehle ich einen Timerevent, denn die Schleife bringt dir sofort 100% Last auf einer CPU.
Der DoEvents unterbricht den Prozess nicht. Es wird nur kurz abgefragt, ob Windowsmessages vorliegen.https://docs.microsoft.com/de-de/dotnet/api/system.windows.forms.timer?view=netframework-4.8
Zu empfehlen ist auch hier mindestens ein Timer von 20ms, da kürzere Timer wiederum durch Prozessorlast auffallen.
- Als Antwort vorgeschlagen Denniver ReiningMVP, Moderator Donnerstag, 10. Oktober 2019 10:32
- Als Antwort markiert Denniver ReiningMVP, Moderator Montag, 14. Oktober 2019 10:40
-
Hallo,
nur noch mal zum Verständnis.
Sollte das jobScript den robocopy Befehl nicht ausführen?
$jobScript = { # der copy job! # Start-Sleep 10
robocopy $src_path $dst_path /s
Write-Output "Copy job ist erledigt." }
Passiert aber irgendwie nicht.
Gruß
yelw0rc
-
Hallo,
hätte doch noch mal eine Frage.
Ist es auch möglich die Progressbar irgendwie unter zu kriegen.
In dem Job an sich scheint das nicht zu funktionieren.
### Init ProgressBar $script:Progress = New-Object System.Windows.Forms.ProgressBar $script:Progress.Maximum = 100 $script:Progress.Minimum = 0 $script:Progress.Location = new-object System.Drawing.Size(10,10) $script:Progress.size = new-object System.Drawing.Size(200,50) $Form.Controls.Add($script:Progress) $jobScript = { $loop_count=1 while ($loop_count -ne 500){ ### calculate percentage $script:Progress.Value = ($loop_count/500)*100 Write-Host $script:Progress.Value ### update the progress bar Start-Sleep -m 1 $loop_count++ } Write-Output "Copy job ist erledigt." }
Vielen Dank.
Gruß
yelw0rc
-
Der Batchjob aus dem Hinterggrund kann nicht auf den Hauptjob zugreifen.
Du kannst aber Ergebnisse abfragen:https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/receive-job?view=powershell-6
die du im Batch geschrieben hast:
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-host?view=powershell-6Im oben genannten Timerevent kanst du nach dem Status
$job.State -ne "Running"
dann noch einen Recevie-Job hinterherjagen.
-
Verstehe ich leider nicht so recht.
So wie ich es verstehe, müssen also 2 Jobs laufen. Diese natürlich parallel - also einmal der Copy-Job und als zweites der der Job für die ProgressBar, oder?
Ich verstehe jetzt nicht so ganz, was ich mit dem Receive-Job anfangen soll. Oder den Receive-Job so lange laufen lassen, bis der Status des Copy-Jobs noch "Running" zurück meldet. Wo baue ich den Receive-Job dann mit ein? Im jobScript oder im $BntStart.add_click?
Tut mir leid für meine Verständnisprobleme. Beschäftige mich noch nicht ganz lange damit und so manches ist mir irgendwie noch nicht so ganz klar.
Danke trotzdem und Gruß
-
Deine Form läuft im Hauptprozess und dieser wird angehalten durch die ShowDialog()-Methode der Form.
Mittels "$JVar = Start-Job ..." startest du einen Hintergrundprozess und hast in $JVar den Zugriff darauf.
Der Batchprozess kann per write-host auf "stdout", also die Standarduasgabe, Daten schreiben.
Bei "Receive-Job $JVar" liest du das aus, was der Batschjob geschrieben hat.Nun erstellst du noch ein Forms-Timer und per Tick-Event kannst du nun den Status auslesen, ob der Job fertig ist als auch die Ausgabe des Batchjobs auswerten.