【问题标题】:powershell: change GUI element when async FileSystemWatcher firespowershell:异步 FileSystemWatcher 触发时更改 GUI 元素
【发布时间】:2018-02-19 08:00:13
【问题描述】:

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

# 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


    【解决方案1】:

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

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

    还有一点觉得别扭:

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

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

    我是否遗漏了一些代码过于复杂的地方?

    感谢任何cmets。

    这是一个 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()
    

    【讨论】:

      【解决方案2】:

      我真的很喜欢你的解决方案,但正如你所说,这个问题不适用于 PS2.0,包括我需要的 Win7 服务包 1。

      我的 GUI 更新解决方案适用于 PS2(win7) 和 PS3(win10),它基于 Windows Presentation Framework(WPF) 而不是 Windows 窗体,因为有了 WPF,我们可以使用 Data Binding 和 @987654322 @ 界面。我的工作基于 Trevor Jones 网络 How-To,并提供了一些工作技巧。

      关于取消注册系统的问题,我已从 VB 翻译为 PS,Mike Ober 在SpiceWorks 的帖子中,他基于全局变量和可能的错误注册和取消注册系统的概念启发了我。

      这是我的代码:

      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
      

      希望能帮到你。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-19
        • 1970-01-01
        相关资源
        最近更新 更多