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

时间:2018-02-19 08:00:14

标签: powershell user-interface filesystemwatcher

我想在创建新文件时更改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的专家(仍然在学习)。任何帮助和建议都表示赞赏。

提前致谢。

2 个答案:

答案 0 :(得分: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()

答案 1 :(得分: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翻译成VB,其中他基于全局变量注册和注销系统的概念以及可能的错误启发了我

这是我的代码:

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

我希望能提供帮助。