none
バックグランドジョブのフォームの表示更新 RRS feed

  • 質問

  • いつもお世話になります。
    また新しい疑問にぶつかってしまい アドバイスを頂けたらと思います。
    前回のスクリプトの続きですが、 新しく簡単な進行状況
    (例えば経過時間とかループ回数)を フォーム上のラベルなどに表示したいと思っていますが、
    以下のように Function中で Background Jobを実行させた場合、
    メインのスレッドからフォームをリフレッシュする方法はあるのでしょうか?
    (単純なフォームのリフレッシュのサンプルは見つけて試しましたが、
     Function中の Backround Jobの中の $Formや $Label.textは更新できませんでした。)
    実際にはそれほどループする時間は短くないので
    最悪 その度にフォームを削除して新しく表示し直すことを考えていますが
    出来ればもっとスマートな方法を知りたいと思います。

    function Disp_Dialogu ($labeltxt) {
    $job1=Start-Job -ScriptBlock { 
        param($labeltxt)
        Add-Type -AssemblyName System.Windows.Forms | Out-Null
        $Form = New-Object system.Windows.Forms.Form
        $Form.StartPosition =  [System.Windows.Forms.FormStartPosition]::Manual
        $Form.Text = "Sub process"
        $Form.size = "300,150"
        $Form.Location = "0,0"
        $Label = New-Object System.Windows.Forms.Label
        $Label.Text = $labeltxt
        $Label.Location = "10,40"
        $Label.Font = New-Object System.Drawing.Font("MS ゴシック",12)
        $Label.AutoSize = $True
        $Form.Controls.Add($Label)
        $Form.Topmost = $True
        $Form.MaximizeBox = $false
        $Form.MinimizeBox = $true
    
        $ButtonA = New-Object System.Windows.Forms.Button
        $ButtonA.Location = "120,85"
        $ButtonA.size = "60,20"
        $ButtonA.text = "OK"
        $ButtonA.FlatStyle = "popup"
        $form.Controls.Add($ButtonA)
        $ButtonB = New-Object System.Windows.Forms.Button
        $ButtonB.Location = "200,85"
        $ButtonB.size = "60,20"
        $ButtonB.text = "Cancel"
        $ButtonB.FlatStyle = "popup"
        $form.Controls.Add($ButtonB)
        $ButtonA.DialogResult = [System.Windows.Forms.DialogResult]::OK
        $ButtonB.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
    
        $ButtonA.Add_Click({$ret = [System.Windows.Forms.DialogResult]::OK})
        $ButtonB.Add_Click({$ret = [System.Windows.Forms.DialogResult]::Cancel})
    
        $Form.ShowDialog()
        $ret
    
    } -ArgumentList ($labeltxt)
    return $job1
    }
    
    # Parent Process
    $count=1;
    $jobs = Disp_Dialogu("Label text")
    [String]$result =""
    Do {
        Start-Sleep -Seconds 1;
        $results = Receive-Job -Job $jobs
        if ($jobs.State -eq "Completed") {
            $result = $results[0].value }
        Write-Host "${count} Sec    :  " $result
        $count++;
        if ($count -gt 10) {break}
    } While($result -ne "OK")
    Write-Host "Result:  " $results.value
    Get-Job|where {$_.state -eq "Completed" -or $_.state -eq "Stopped"}|Remove-Job
    Write-Host "Backgroundjob Removed"
    # End
    Read-Host -Prompt "Press Enter to continue"
    exit
    

    2016年12月18日 4:30

回答

  • > バックグランドジョブの説明の中にリモートのコンピューターとかの説明も出てきますので、私が漠然と期待していた参照渡> しのような引数の渡し方ができず、「プロセス間通信」が必要になるというご説明に納得しました。

    PowerShellのリモーティング(リモートジョブも含め)では、「実行開始時に引数としてオブジェクトを渡す」ことと、「リモート側の出力オブジェクトを呼び出し元で取得する」ことが可能ですが、これらは.NETオブジェクトの参照をやり取りしているのではなく、一度シリアル化したデータをやり取りしています。受け取り側ではデシリアル化して、元に近いオブジェクトに復元しますが、全く同じものにはなりません。このあたりが問題を厄介にしている一因でもあるんだと思います。

    但しやりたいのはメインのプロセスから文字列を渡して表示させたいので、バックグランドジョブ内のタイマーで書き換えが実行で> きても、参照渡しなどができない限り呼び出し元からの文字列取得は出来ないという理解でいいのでしょうか?

    私の知る限りでは、バックグラウンドジョブの仕組みの範疇では難しいと思います。

    ただ、この点も発想を変えて、フォームのプロセスをメインにして、実際の処理を行うルーチンをジョブとして、フォームから呼び出すアプローチもできるんじゃないかと思っています。

    Windows Formsでは、フォーム要素からワーカースレッドを起動して、そこで処理をやらせて、結果を非同期でフォームに返すという手法が一般的に使われています。要はそれと同じことをPowerShellでできないか、ということです。

    ただし、申し訳ないですが、本当に可能なのか、私もやったことがないので分かりません。

    そうであれば、あまり美しくないのですが変数を書き出したファイルパスを渡してバックグランド側のタイマーで定期的に読むようなことを試してみようと思います。

    正直、この手法が一番低コストだと私も思います。実際私がちょっとしたものを作るときは、ファイル経由でデータを渡すことが多いです。ただしこの手段を取るときにはファイルの排他制御をきちんとする必要があって、System.IO配下のクラスを使うことになると思います。Get-Content/Set-Contentは使えないと考えた方がよいかと思います。

    • 回答としてマーク pai314 2016年12月19日 10:32
    2016年12月19日 0:48
    モデレータ

すべての返信

  • バックグラウンドジョブは別プロセスで実行されるので、呼び出し元プロセスからジョブプロセスへの通信が必要になるかと思います。ただ、ジョブ開始時に引数を渡すのではなく、実行中のジョブに呼び出し元から値を送信する簡易な方法は、私の知る限りはありません。

    (よく調べていませんが、一般的なプロセス間通信の手法をとる必要があるかもしれません)

    そこで発想を変えて、経過時間やループ回数表示をフォーム内で行うようにしてみてはいかがでしょうか。具体的にはFormの上にTimerコントロールを付加して、Tickイベントハンドラ内にLabel.Textを変更するコードを書きます。

    2016年12月18日 6:08
    モデレータ
  • 牟田口様
    いつも適切なアドバイスを頂き感謝です。
    何日も悩んでいたことが、(何となくではありますが)理解できました。
    バックグランドジョブの説明の中にリモートのコンピューターとかの説明も出てきますので、私が漠然と期待していた参照渡しのような引数の渡し方ができず、「プロセス間通信」が必要になるというご説明に納得しました。
    TickイベントでLabelを変更するというアドバイスですが、たしかにタイマーコントロールを追加するとラベルはカウントアップの文字列を更新してくれました。
    但しやりたいのはメインのプロセスから文字列を渡して表示させたいので、バックグランドジョブ内のタイマーで書き換えが実行できても、参照渡しなどができない限り呼び出し元からの文字列取得は出来ないという理解でいいのでしょうか? そうであれば、あまり美しくないのですが変数を書き出したファイルパスを渡してバックグランド側のタイマーで定期的に読むようなことを試してみようと思います。

    • 編集済み pai314 2016年12月19日 0:07
    2016年12月19日 0:03
  • > バックグランドジョブの説明の中にリモートのコンピューターとかの説明も出てきますので、私が漠然と期待していた参照渡> しのような引数の渡し方ができず、「プロセス間通信」が必要になるというご説明に納得しました。

    PowerShellのリモーティング(リモートジョブも含め)では、「実行開始時に引数としてオブジェクトを渡す」ことと、「リモート側の出力オブジェクトを呼び出し元で取得する」ことが可能ですが、これらは.NETオブジェクトの参照をやり取りしているのではなく、一度シリアル化したデータをやり取りしています。受け取り側ではデシリアル化して、元に近いオブジェクトに復元しますが、全く同じものにはなりません。このあたりが問題を厄介にしている一因でもあるんだと思います。

    但しやりたいのはメインのプロセスから文字列を渡して表示させたいので、バックグランドジョブ内のタイマーで書き換えが実行で> きても、参照渡しなどができない限り呼び出し元からの文字列取得は出来ないという理解でいいのでしょうか?

    私の知る限りでは、バックグラウンドジョブの仕組みの範疇では難しいと思います。

    ただ、この点も発想を変えて、フォームのプロセスをメインにして、実際の処理を行うルーチンをジョブとして、フォームから呼び出すアプローチもできるんじゃないかと思っています。

    Windows Formsでは、フォーム要素からワーカースレッドを起動して、そこで処理をやらせて、結果を非同期でフォームに返すという手法が一般的に使われています。要はそれと同じことをPowerShellでできないか、ということです。

    ただし、申し訳ないですが、本当に可能なのか、私もやったことがないので分かりません。

    そうであれば、あまり美しくないのですが変数を書き出したファイルパスを渡してバックグランド側のタイマーで定期的に読むようなことを試してみようと思います。

    正直、この手法が一番低コストだと私も思います。実際私がちょっとしたものを作るときは、ファイル経由でデータを渡すことが多いです。ただしこの手段を取るときにはファイルの排他制御をきちんとする必要があって、System.IO配下のクラスを使うことになると思います。Get-Content/Set-Contentは使えないと考えた方がよいかと思います。

    • 回答としてマーク pai314 2016年12月19日 10:32
    2016年12月19日 0:48
    モデレータ
  • 牟田口様

    詳細な説明をいただきありがとうございます。
    シリアライズとか・・・どこかで一度は読んだ記憶があるのですが、正直何のことを言っているのか理解できていませんでしたが、こうして自分の作ったスクリプトに対して説明頂くことで「そうだったのか!」と理解できた気がします。
    取り敢えずファイル渡しの排他制御は考えず、使い慣れた Out-File/Get-Contentで実装してラベルが書き換わるのが確認できました。
    以下が Start-Job のスクリプトの最初の方に追加した $Label.Text を読み取るスクリプトです。

            $Fullpath = "C:\Users\Default\Documents\work\labelname.txt"
            $Watch = New-Object System.Diagnostics.Stopwatch
            $Timer = New-Object System.Windows.Forms.Timer
            $Timer.Interval = 1000
            $Time = {
                $Now = $Watch.Elapsed
                $Label.Text = Get-Content $Fullpath -TotalCount 1
            }
            $Timer.Add_Tick($Time)
    

    試しにメインの書き込み/バックグランドジョブの読み出しループをそれぞれ 10mSec間隔で 数分回してエラーが出なかったので、商用ではありませんし、しばらくこれで使ってみます。

    本当にありがとうございました。

    2016年12月19日 10:32