powershell:在异步FileSystemWatcher触发时更改GUI元素

问题描述 投票:0回答:2

我想在创建新文件时更改GUI元素(powershell ISE中的windows-form)。因此,我设置了一个表单并在另一个运行空间(MWE)中启动了filesystemwatcher:

# this function should be called when a new file is created
function foobar(){
    $form.BackColor = "black"
}

# set up runspace for async FileSystemWatcher
$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [System.Management.Automation.PowerShell]::Create()
$PowerShell.runspace = $Runspace
$Runspace.Open()

[void]$PowerShell.AddScript({
    $logFile = 'C:\powershell\test.log'  
    $dirName = 'C:\powershell\'

    $hotFolder = New-Object IO.FileSystemWatcher $dirName -Property @{IncludeSubdirectories = $false;NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'}
    Register-ObjectEvent $hotFolder Created -SourceIdentifier FileCreated -Action {
          $name = $Event.SourceEventArgs.Name
          $path = $Event.SourceEventArgs.FullPath 
          $changeType = $Event.SourceEventArgs.ChangeType
          $timeStamp = $Event.TimeGenerated

          Out-File -FilePath $logFile -Append -InputObject "The file '$name' was $changeType at $timeStamp"

      # this call does not work
      foobar
      }  
})

$AsyncObject = $PowerShell.BeginInvoke()

# set up form
$form = New-Object System.Windows.Forms.Form
$form.ShowDialog()

FileSystemWatcher工作(写入日志文件),但“foobar”的调用被忽略/不起作用。

我的第一次尝试是在表单中注册FileSystemWatcher,这不起作用(类似于:FileSystemWatcher and GUI)。我找到了这个线程FileSystemWatcher kommt nicht mit Form zurecht (german only),它建议使用运行空间。

运行空间解决了卡住的GUI问题,但是当fileSystemWatcher注册一个新文件时,我需要一种方法来触发表单中的事件。我怎么能做到这一点?

简而言之:

1)FileSystemWatcher如何触发GUI元素的更改

2)在这种情况下,运行空间是正确的方法

我不是PowerShell的专家(还在学习)。任何帮助和建议表示赞赏。

提前致谢。

powershell user-interface filesystemwatcher
2个回答
0
投票

我做了一些研究并找到了解决方案:

  • 添加了一个同步哈希表,以在运行空间之间共享变量
  • 在哈希表中添加了一个表单按钮(此按钮将隐藏在最终版本drawing.Size(0,0)中)
  • fileSystemWatcher使用performclick()单击按钮
  • 按钮在点击时调用所需的功能

还有一点让人觉得尴尬:

  • 取消注册fileSystemWatcher我将共享变量设置为1并通过生成文件来触发fileSystemWatcher

有没有更优雅的方式来做到这一点?

我是否会错过代码不必要复杂的一些要点?

任何评论都表示赞赏。

这是一个MWE。要使用它,请根据需要设置$ dir变量。 (MWE不适用于Win7附带的未更新的PowerShell)

# set working dir
$dir = 'C:\Downloads'
Write-Host 'working dir' $dir


# create function which is called when new file is created
function showPath($path){
  $label.Text = $path
}

# set up runspace for async FileSystemWatcher
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.Open()

# synchronized hashtable and hashtable elements
$sync = [Hashtable]::Synchronized(@{})
  $sync.path = 1  # random start value
  $sync.exit = 0  # switch: if set to 1 fileSystemWatcher will be unregistert when next event occurs
  $sync.dir = $dir

  $btnNewFile= New-Object System.Windows.Forms.Button
  $btnNewFile.Location = New-Object System.Drawing.Size(220,10)
  $btnNewFile.Size = New-Object System.Drawing.Size(150,23)
  $btnNewFile.Text = "do not click - fake button"
  $btnNewFile.Add_Click({
    $newPath = $sync.path
    $form.text = $newPath
    showPath($newPath)
  })

  $sync.btnNewFile = $btnNewFile

$Runspace.SessionStateProxy.SetVariable("sync", $sync)

$PowerShell = [System.Management.Automation.PowerShell]::Create()
$PowerShell.runspace = $Runspace

[void]$PowerShell.AddScript({
    $logFile = Join-Path $sync.dir test.log  
    $dirName = $sync.dir

    $hotFolder = New-Object IO.FileSystemWatcher $dirName -Property @{IncludeSubdirectories = $false;NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'}
    Register-ObjectEvent $hotFolder Created -SourceIdentifier FileCreated -Action {
          $path = $Event.SourceEventArgs.FullPath 
          $name = $Event.SourceEventArgs.Name
          $changeType = $Event.SourceEventArgs.ChangeType
          $timeStamp = $Event.TimeGenerated

          # check if exit condition is met
          if($sync.exit -eq 1){
            Out-File -FilePath $logFile -Append -InputObject "Exit file: '$name'"
            Unregister-Event FileCreated
          }
          else{
          Out-File -FilePath $logFile -Append -InputObject "The file '$name' was $changeType at $timeStamp"

          # set path to synchroniszed variable
          $sync.path = $path

          # click Button to trigger function call
          $sync.btnNewFile.PerformClick() 
          }

      }
})

$AsyncObject = $PowerShell.BeginInvoke()

# GUI setup
$labelHeader = New-Object System.Windows.Forms.Label
$labelHeader.Location = New-Object System.Drawing.Size(10,50)
$labelHeader.Size = New-Object System.Drawing.Size(100,23)
$labelHeader.Text = 'path to new file:'

$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Size(110,50)
$label.Size = New-Object System.Drawing.Size(200,23)
$label.Text = 'no file created'


$global:fileCounter = 0
$btnCreateFile = New-Object System.Windows.Forms.Button
$btnCreateFile.Location = New-Object System.Drawing.Size(10,10)
$btnCreateFile.Size = New-Object System.Drawing.Size(100,23)
$btnCreateFile.Text = "New File"
$btnCreateFile.Add_Click({
  $global:fileCounter+=1
  $fileName = "$global:fileCounter.txt"
  $newFile = Join-Path $dir $fileName
  New-Item $newFile -ItemType file
})

$btnExit = New-Object System.Windows.Forms.Button
$btnExit.Location = New-Object System.Drawing.Size(110,10)
$btnExit.Size = New-Object System.Drawing.Size(100,23)
$btnExit.Text = "&Exit"
$btnExit.Add_Click({
  $sync.Exit = 1
  $btnCreateFile.PerformClick() 
  $Powershell.Dispose()
  $form.Close()
})

# set up form
$form = New-Object System.Windows.Forms.Form
$form.Width = 400
$form.Height = 120
$form.Controls.Add($btnCreateFile)
$form.Controls.Add($btnExit)
$form.Controls.Add($labelHeader)
$form.Controls.Add($label)
$form.Controls.Add($sync.btnNewFile)

$form.ShowDialog()

0
投票

我真的很喜欢你的解决方案,但问题就像你说的那样,不适用于PS2.0,包括Win7的Service Pack 1,我需要的地方。

我的GUI更新解决方案适用于PS2(win7)和PS3(win10),它基于Windows Presentation Framework(WPF)而不是Windows Forms,因为使用WPF我们可以使用Data BindingINotifyPropertyChanged界面。我的工作基于Trevor Jones网站How-To,有一些技巧可以开始工作。

关于取消注册我已经翻译的系统的问题,从VB到PS,来自SpiceWorks的Mike Ober的一篇文章,其中他的概念是基于全局变量注册和取消注册系统以及可能的错误激发了我的灵感。

这是我的代码:

Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase

Function Create-WPFWindow {
    Param($Hash)
    # Create a window object
    $Window = New-Object System.Windows.Window
    $Window.Width = '600'
    $Window.Height = '300'
    $Window.Title = 'WPF-CONTROL'
    $window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
    $Window.ResizeMode = [System.Windows.ResizeMode]::NoResize

    # Create a Label object
    $Label = New-Object System.Windows.Controls.Label
    $Label.Height = 40
    $Label.HorizontalContentAlignment = 'Left'
    $Label.VerticalContentAlignment = 'Center'
    $Label.FontSize = 15
    $Label.Content = 'Actividad:'
    $Hash.Label = $Label

    # Create a TextBlock object
    $TextBlock = New-Object System.Windows.Controls.TextBlock
    $TextBlock.Height = 150
    $TextBlock.FontSize = 20
    $TextBlock.TextWrapping = 'Wrap'
    $Hash.TextBlock = $TextBlock

    # Create a Button1 object
    $Button1 = New-Object System.Windows.Controls.Button
    $Button1.Width = 300
    $Button1.Height = 35
    $Button1.HorizontalContentAlignment = 'Center'
    $Button1.VerticalContentAlignment = 'Center'
    $Button1.FontSize = 20
    $Button1.Content = 'Iniciar'
    $Hash.Button1 = $Button1

    # Assemble the window
    $StackPanel1 = New-Object System.Windows.Controls.StackPanel
    $StackPanel1.Margin = '150,20,5,5'
    $StackPanel1.Orientation = 'Horizontal'
    $StackPanel1.Children.Add($Button1)
    $StackPanel2 = New-Object System.Windows.Controls.StackPanel
    $StackPanel2.Margin = '5,5,5,5'
    $StackPanel2.Orientation = 'Vertical'
    $StackPanel2.Children.Add($Label)
    $StackPanel2.Children.Add($TextBlock)
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.Margin = '5,5,5,5'
    $StackPanel.Children.Add($StackPanel1)
    $StackPanel.Children.Add($StackPanel2)
    $Window.Content =  $StackPanel

    # Stop the service and release the resources
    $Window.Add_Closing({
        $Hash.On = $false
        $global:p.BeginInvoke()
        $global:p.Dispose()})
    $Hash.Window = $Window
}

$Hash = [hashtable]::Synchronized(@{})
# Create a WPF window and add it to a Hash table
Create-WPFWindow $Hash | Out-Null

# Create a datacontext for the TextBlock, we add it to the synchronized $Hash to update the GUI from the FileSystemWatcher Event.
$DataContext = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$Text = [string]'Pulse el botón para iniciar el sistema.'
$DataContext.Add($Text)
$Hash.TextBlock.DataContext = $DataContext
$Hash.DataContext = $DataContext
$Hash.path='C:\POWERSHELL_PROJECT\Result'
# These two vars are for my needs, you can obviate them or delete
$Hash.urlLOGIN=''
$Hash.urlLOTE=''
$Hash.fileWatcher = $null
$Hash.LOG='C:\POWERSHELL_PROJECT\Result\LOG.log'
$Hash.firstEvent = $false
$Hash.On=$false
$Hash.msg=''

# Create and set a binding on the TextBlock object
$Binding = New-Object System.Windows.Data.Binding -ArgumentList '[0]'
$Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
[void][System.Windows.Data.BindingOperations]::SetBinding($Hash.TextBlock,[System.Windows.Controls.TextBlock]::TextProperty, $Binding)
# Add an event for the Button1 click to Register FileSystemWatcher and Unregister it
$Hash.Button1.Add_Click({
    if ($Hash.On -eq $true){ 
        $Hash.On = $false
        $Hash.Button1.Background = 'Green'
        $Hash.Button1.Content = 'Iniciar'
    }else{ 
        $Hash.On = $true
        $Hash.Button1.Background = 'Red'
        $Hash.Button1.Content = 'Detener'
    }
    $p.BeginInvoke() | Out-Null
})

# Multithreading runspaces for FileSystemWatcher
$rs_dForm = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$rs_dForm.ApartmentState = 'STA'
$rs_dForm.ThreadOptions = 'ReuseThread'
$rs_dForm.Open()
$rs_dForm.SessionStateProxy.SetVariable('Hash', $Hash)

$p = [PowerShell]::Create().AddScript({
        Function global:OnFileSystemWatcherError {
            FileEventListener -Path $Hash.path
        }
        # With simple function we can refresh the Textbox and Log
        Function global:Refresh-WPF-and-LOG {
            $Hash.DataContext[0] = $Hash.msg
            echo $Hash.msg >> $Hash.LOG
        }
        Function global:FileEventListener ($Path){
            if ($Hash.On){
                $Hash.fileWatcher = New-Object System.IO.FileSystemWatcher
                $Hash.fileWatcher.Path = $Path
                $Hash.fileWatcher.Filter = '*.xml'
                $Hash.fileWatcher.IncludeSubdirectories = $false
                $Hash.fileWatcher.InternalBufferSize = 32768
                $Hash.fileWatcher.EnableRaisingEvents=$true
                Register-ObjectEvent -InputObject $Hash.fileWatcher -EventName Changed -SourceIdentifier File.Changed -Action {
                    $Global:t = $event
                    if (!$Hash.firstEvent){
                        try{
                            # For example you can:
                            $Hash.msg = '[' + $(Get-Date -Format u | foreach {$_ -replace ' ','-'}).ToString() + ']--' + $event.SourceEventArgs.Name
                        }catch{
                            $Hash.msg = '[' + $(Get-Date -Format u | foreach {$_ -replace ' ','-'}).ToString() + ']--' + $_.Exception.Message + ' ' + $_.Exception.ItemName
                        }finally{
                            Refresh-WPF-and-LOG
                        }
                        $Hash.firstEvent=$true
                    }else{
                        $Hash.firstEvent=$false
                    }
                }
                # With this Register we control the errors from the FileSystemWatcher system, and reinit it this case
                Register-ObjectEvent -InputObject $Hash.fileWatcher -EventName Error -SourceIdentifier File.Error -Action {
                    $Global:t = $event
                    $Hash.On = $false
                    OnFileSystemWatcherError
                }
                $Hash.msg = '[' + $(Get-Date -Format u | foreach {$_ -replace ' ','-'}).ToString() + ']--' + 'Servicio INICIADO.'
            }else{
                if ( $Hash.fileWatcher -ne $null ){
                    $Hash.fileWatcher.EnableRaisingEvents=$false
                    Unregister-Event File.Changed
                    Unregister-Event File.Error
                    $Hash.fileWatcher.Dispose()
                    $Hash.fileWatcher=$null
                    $Hash.msg='[' + $(Get-Date -Format u | foreach {$_ -replace ' ','-'}).ToString() + ']--' + 'Sistema DETENIDO.'
                }
            }
            Refresh-WPF-and-LOG
        }
        FileEventListener -Path $Hash.path
})

$p.Runspace = $rs_dForm
# Show the window
$Hash.Window.ShowDialog() | Out-Null

我希望能帮到你。

© www.soinside.com 2019 - 2024. All rights reserved.