将PowerShell输出(实时)写入WPF UI控件

3
我一直在阅读关于从不同的runspace向UI编写数据的博客(http://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/)。
我基本上想实现的是,在UI中点击一个按钮并运行一个PowerShell脚本,并捕获该脚本的输出,同时更新WPF UI控件而不冻结UI。
我尝试了一个简单的例子,直接写一些输出,但似乎会挂起UI。我正在使用runspaces和dispatcher,但好像卡在某个地方了。
有什么想法吗?
谢谢。
Add-Type –assemblyName PresentationFramework
Add-Type –assemblyName PresentationCore
Add-Type –assemblyName WindowsBase


$uiHash = [hashtable]::Synchronized(@{})
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open() 
$newRunspace.SessionStateProxy.SetVariable('uiHash',$uiHash)

$psCmd = [PowerShell]::Create().AddScript({
[xml]$xaml = @"
<Window 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="Window" Title="Patcher" Height="350" Width="525" Topmost="True">
    <Grid>
        <Label Content="A Builds" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="88" RenderTransformOrigin="0.191,0.566"/>
        <ListBox HorizontalAlignment="Left" Height="269" Margin="10,41,0,0" VerticalAlignment="Top" Width="88"/>
        <Label Content="New Build" HorizontalAlignment="Left" Margin="387,10,0,0" VerticalAlignment="Top"/>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="387,41,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
        <Label Content="B Builds" HorizontalAlignment="Left" Margin="117,10,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.528,-0.672"/>
        <ListBox HorizontalAlignment="Left" Height="269" Margin="103,41,0,0" VerticalAlignment="Top" Width="88"/>
        <Label Content="C Builds" HorizontalAlignment="Left" Margin="185,10,0,0" VerticalAlignment="Top"/>
        <ListBox HorizontalAlignment="Left" Height="269" Margin="196,41,0,0" VerticalAlignment="Top" Width="88"/>
        <Button x:Name="PatchButton" Content="Patch!" HorizontalAlignment="Left" Margin="426,268,0,0" VerticalAlignment="Top" Width="75"/>
        <RichTextBox x:Name="OutputTextBox" HorizontalAlignment="Left" Height="194" Margin="289,69,0,0" VerticalAlignment="Top" Width="218">
            <FlowDocument>
                <Paragraph>
                    <Run Text=""/>
                </Paragraph>
            </FlowDocument>
        </RichTextBox>

    </Grid>
</Window>
"@

$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$uiHash.Window = [Windows.Markup.XamlReader]::Load($reader)

$uiHash.Button = $uiHash.Window.FindName("PatchButton")
$uiHash.OutputTextBox = $uiHash.Window.FindName("OutputTextBox")

$uiHash.OutputTextBox.Dispatcher.Invoke("Render", [Windows.Input.InputEventHandler]    {$uiHash.OutputTextBox.UpdateLayout()}, $null, $null)

$uiHash.Window.ShowDialog() | Out-Null
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

# The next line fails (null-valued expression)
<#
$uiHash.OutputTextBox.Dispatcher.Invoke("Normal", [action]{
    for ($i = 0; $i -lt 10000; $++) {
        $uiHash.OutputTextBox.AppendText("hi")
    }
})#>
4个回答

1

我遇到了同样的问题,并看到了很多建议,其中有些是涉及C#类或示例,其中UI被放置在工作程序而不是实际的工作任务中。无论如何,经过相当长时间的努力,我终于弄清楚了:

Add-Type -AssemblyName PresentationFramework
$Xaml = New-Object System.Xml.XmlNodeReader([XML]@"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Test" WindowStartupLocation = "CenterScreen" ShowInTaskbar = "True">
    <Grid>
        <TextBox x:Name = "TextBox"/>
    </Grid>
</Window>
"@)

Function Start-Worker {
    $TextBox = $Window.FindName("TextBox")
    $SyncHash = [hashtable]::Synchronized(@{Window = $Window; TextBox = $TextBox})
    $Runspace = [runspacefactory]::CreateRunspace()
    $Runspace.ThreadOptions = "ReuseThread"         
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)          
    $Worker = [PowerShell]::Create().AddScript({
        for($Progress=1; $Progress -le 10; $Progress++){
            $SyncHash.Window.Dispatcher.Invoke([action]{$SyncHash.TextBox.AppendText($Progress)})
            Start-Sleep 1                                                       # Some background work
        }
    })
    $Worker.Runspace = $Runspace
    $Worker.BeginInvoke()
}

$Window = [Windows.Markup.XamlReader]::Load($Xaml)                              # Requires -STA mode
$Window.Add_Loaded({Start-Worker})
[Void]$Window.ShowDialog()

如果想要查看一个Windows控件示例,请参见:PowerShell: Job Event Action with Form not executed


1

是的,我只是出于好奇想看看是否可以用这种方式来做。我并没有特别偏向某一种方法。 - jmcdade
尝试基于这种方法来适应一个WPF解决方案真是一场巨大的头痛。为了我的目的,我只打算在WinForms中托管PowerShell脚本,然后就可以结束了。 - jmcdade

0

我在你的代码中做了一些调整。我不会在调度程序内运行你的 For 循环,而是会在 For 循环内运行调度程序块。我进行了测试,对我来说似乎没有锁定(有一些性能不佳的时刻,但这是预期的),并且持续向窗口写入。

for ($i = 0; $i -lt 10000; $i++) {
    $uiHash.OutputTextBox.Dispatcher.Invoke("Normal", [action]{
        $uiHash.OutputTextBox.AppendText("hi")
    })
}

是的,我是以艰难的方式发现了这个问题。当我尝试在同一个脚本中使用该代码块时,无法访问UI元素,但通过控制台可以正常工作,因为UI正在运行。这是否符合预期?无论如何,从性能角度来看,有没有更好的方法来解决这个问题,或者应该按照Doug的建议改变顺序? - jmcdade
这真的取决于你个人的偏好。有些人会说,你应该用C#而不是PowerShell来编写用户界面(就像Doug建议的那样),但我更喜欢用PowerShell来编写,并确保在编码过程中小心谨慎,以避免对用户界面运行命令时出现任何问题。如果你对C#编码有信心,那么可以尝试一下,否则就继续沿着你当前的路径前进。 - boeprox
UI线程能够处理连续的流水线输出吗,还是会被卡住?我在设想将脚本输出显示在文本框中,这样用户就可以留意输出信息,同时进行其他UI操作。如果脚本的输出会占用所有资源,我需要采取新的方法。 - jmcdade

0
这可能有点晚了,但是尝试下载WPFRunspace,它提供了一个用于WPF和Win Forms GUI脚本的Powershell后台工作器cmdlet,可以在后台执行并在接收到输出或完成时更新您的GUI。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接