none
PS Forms - Laufenden Prozess ueber Cancel-Button beenden RRS feed

  • 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
    Dienstag, 1. Oktober 2019 10:13

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.


    Dienstag, 8. Oktober 2019 11:12
    Moderator
  • 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.

    Dienstag, 8. Oktober 2019 11:28

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-6

    Im 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

    Dienstag, 1. Oktober 2019 14:39
  • 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

    Montag, 7. Oktober 2019 08:50
  • 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.

    Montag, 7. Oktober 2019 10:52
  • 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.


    Dienstag, 8. Oktober 2019 11:12
    Moderator
  • 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.

    Dienstag, 8. Oktober 2019 11:28
  • Hallo,

    vielen Dank für das Beispiel. Sehr lehr- und hilfreich.

    Gruß

    Montag, 14. Oktober 2019 10:30
  • 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

    Dienstag, 15. Oktober 2019 06:58
  • Hallo,

    hat sich erledigt.

    Ich muss die Pfade mit in den Teil jobScript nehmen bzw. $script:src_path / script:dst_path nutzen.

    Gruß

    yelw0rc

    Dienstag, 15. Oktober 2019 07:44
  • 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

    Dienstag, 15. Oktober 2019 10:25
  • 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-6

    Im oben genannten Timerevent kanst du nach dem Status

    $job.State -ne "Running"

    dann noch einen Recevie-Job hinterherjagen.

    Dienstag, 15. Oktober 2019 10:36
  • 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ß

    Dienstag, 15. Oktober 2019 12:23
  • 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.

    Dienstag, 15. Oktober 2019 14:20