タグ別アーカイブ: デリゲート

クリップボードを簡単に使うスクリプト

Use-Clipboard.ps1 は、クリップボードの内容を取得・設定するスクリプトです。(Invoke-StaThread.ps1New-Delegate.ps1 が必要) 用例1: 文字列「Hello World!」をクリップボードにコピーする

PS> ‘Hello World!’ | Use-Clipboard

用例2: クリップボードの内容を取得する

PS> Use-Clipboard
Hello World!

用例3: ビットマップを作成してクリップボードにコピーし、ペイントを起動してそれを貼り付ける

PS> $bitmap = New-Object Drawing.Bitmap(100, 20)
PS> $graphics = [Drawing.Graphics]::FromImage($bitmap)
PS> $whiteBrush = New-Object Drawing.SolidBrush('White')
PS> $graphics.FillRectangle($whiteBrush, 0, 0, 100, 20)
PS> $font = New-Object Drawing.Font('MS ゴシック', 10)
PS> $blackBrush = New-Object Drawing.SolidBrush('Black')
PS> $point = New-Object Drawing.PointF(5, 5)
PS> $graphics.DrawString('Hello World!', $font, $blackBrush, $point)
PS> $bitmap | Use-Clipboard
PS> $process = [Diagnostics.Process]::Start('mspaint.exe'); [void]$process.WaitForInputIdle(); Start-Sleep 1; [Windows.Forms.SendKeys]::SendWait('^v')

シングル スレッド アパートメント

クリップボードを使うにはどうすればよいでしょう?

PS> [void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
PS> [Windows.Forms.Clipboard]::SetText('Hello World!')
"1" 個の引数を指定して "SetText" を呼び出し中に例外が発生しました: "OLE が呼び出される前に、現在のスレッドが Single Thread Apartment (STA) モードに設定されていなければなりません。Main 関数に STAThreadAttribute が設定されていることを確認してください。" 発生場所 行:1 文字:35 + [Windows.Forms.Clipboard]::SetText( <<<< ‘Hello World!’)

うまくいきません。これは、System.Windows.Forms.Clipboard がシングルスレッドアパートメント(STA)を必要とするのに対し、PowerShell がマルチスレッドアパートメント(MTA)で動いているからです。

したがって、STA のスレッドを新たに開始し、その中でスクリプトを実行することで解決できます。Invoke-StaThread.ps1 は、STA スレッドでスクリプトブロックを実行し、そこに引数や入力を与え、出力を受け取り、主スレッドに出力します。内部で New-Delegate.ps1 を呼び出しますので、こちらも用意してください。

 用例1: クリップボードに「Hello World!」をコピーし、それを貼り付け

PS> [void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
PS> Invoke-StaThread {[Windows.Forms.Clipboard]::SetText('Hello World!')}
PS> Invoke-StaThread {[Windows.Forms.Clipboard]::GetText()}
Hello World!

用例2: 引数の使用

PS> [void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
PS> Invoke-StaThread {[Windows.Forms.Clipboard]::SetText($args[0])} 'Hello'
PS> Invoke-StaThread {[Windows.Forms.Clipboard]::GetText()}
Hello

用例3: パイプラインの使用

PS> [void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
PS> 'World' | Invoke-StaThread {process{[Windows.Forms.Clipboard]::SetText($_)}}
PS> Invoke-StaThread {[Windows.Forms.Clipboard]::GetText()}
World

実装:

Invoke-StaThread は次のような手順で動作します。ハッシュ(連想配列)を用意し、それに引数、パイプラインからの入力を格納します。 New-Delegate を使って新しいスレッドを作成し、STA に設定し、スタートさせます。新スレッド内でハッシュから引数、パイプラインからの入力を取り出し、スクリプトを実行し、その出力をハッシュに格納します。新スレッドが終了するのを待ち、ハッシュ内の出力を取り出します。

Windows PowerShell で delegate

New-Delegate.ps1 は、スクリプトブロックからデリゲートを作るスクリプトです。

用例1: 3 秒後に音が鳴ります。

PS> $delegate = New-Delegate Threading.ThreadStart {Start-Sleep 3; [Media.SystemSounds]::Beep.Play()}
PS> $thread = New-Object Threading.Thread ($delegate)
PS> $thread.Start()

用例2: System.Collections.Generic.List<int> を逆順にソートします。

PS> $list = New-Object Collections.Generic.List``1[System.Int32]
PS> $list.AddRange([int[]](1..3))
PS> $list
1
2
3
PS> $delegate = New-Delegate Comparison``1[System.Int32] {$args[1] - $args[0]}
PS> $list.Sort($delegate)
PS> $list
3
2
1

ご覧のように、第一引数にデリゲート型を、第二引数にスクリプトブロックを指定して呼び出すと、その型のデリゲートを作成し、デリゲートが呼び出された際にスクリプトブロックを実行します。

用例2 のように、デリゲートのパラメータは $args に代入され、スクリプトブロックの出力はデリゲートの戻り値となります。

実装

ソース中で IL を出力しているために、大変読みにくくなっていますが、原理は単純です。

ArrayList に、実行環境となる Runspace と、実行すべき ScriptBlock を格納し、そのArrayList に System.Reflection.Emit.DynamicMethod による新たな動的メソッドを作成してバインドします。

動的メソッド内部では、与えられたパラメータを指定してスクリプトブロックを実行し、その出力をキャストして戻り値に代入しています。

PowerScript は、手軽にコマンドを実行できるシェルであると同時に、非常に強力なスクリプト環境であることは異論がないでしょう。 .NET Framework のオブジェクトをそのまま使用できることが、その強力なパワーの源となっています。 .NET Framework を使えば、プログラムからコンパイルもできます。アセンブルもできます。

つまり、PowerShell にデリゲートを作る手段が無ければ、その手段を作ってしまえば良いのです。

追記

2008年4月17日追記:
このスクリプトは、Windows PowerShell Get-Enjoy コンテストにて選考委員特別賞を受賞しました。