运行时CustomApplicationContext类内ApplicationContext为何为空?
问题分析与解决方案
核心问题
- 事件处理程序中
$this失效:PowerShell类的事件处理脚本块无法直接继承类实例的$this上下文,导致计时器Tick事件无法调用TimerProcess()方法,同时相关属性访问失效。 - 数据结构与返回类型不匹配:
UpdateProcesses方法声明返回Hashtable,但实际添加的是PSCustomObject;且传递进程名称字符串而非完整进程对象,导致后续日志格式化时无法访问进程属性(如StartTime)。 - 类型错误与参数不匹配:无参构造函数中
$this.intervalInSec被赋值为字符串类型,且调用Init方法时参数数量不足。
修复步骤
1. 修正事件处理程序的实例引用
在所有事件处理脚本块中,先将类实例捕获到临时变量(如$self),再在脚本块内使用该变量替代$this。例如:
在Init方法中创建计时器时:
$self = $this $this.timer = New-Object System.Windows.Forms.Timer $this.timer.Interval = $this.intervalInSec * 1000 $this.timer.Add_Tick({ $self.TimerProcess() })
同理修改以下事件处理代码:
$this.form.Add_FormClosing$this.form.Add_KeyDown$this.notifyIcon.Add_Click- 所有菜单项的
Add_Click事件
2. 修正UpdateProcesses方法的数据结构
将返回类型改为array,并传递完整进程对象而非名称:
[array] UpdateProcesses() { $processStatuses = @() $currentProcesses = Get-Process -Name $this.processNameToMonitor -ErrorAction SilentlyContinue if ($currentProcesses -eq $null -and $this.previousProcesses.Count -eq 0) { return $processStatuses } if ($currentProcesses -ne $null) { $newProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "=>" } $endedProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "<=" } $this.previousProcesses = $currentProcesses } else { $endedProcesses = $this.previousProcesses $newProcesses = @() $this.previousProcesses = @() } foreach ($newProcess in $newProcesses) { $processStatuses += [PSCustomObject]@{ Status = "Started" Process = $newProcess } } foreach ($endedProcess in $endedProcesses) { $processStatuses += [PSCustomObject]@{ Status = "Ended" Process = $endedProcess } } return $processStatuses }
3. 修正无参构造函数的类型与参数
将$this.intervalInSec改为整数类型,并确保调用Init方法时参数完整:
CustomApplicationContext() { $this.processNameToMonitor = "explorer" $this.intervalInSec = 10 # 改为整数类型 $this.logFilePath = "$env:TEMP\ProcessMonitor_$($this.processNameToMonitor).log" $this.Init($this.processNameToMonitor, $this.intervalInSec, $this.logFilePath) }
完整修复后的CustomApplicationContext类代码
# Define your custom ApplicationContext class CustomApplicationContext : System.Windows.Forms.ApplicationContext { [System.Windows.Forms.Form] $form [System.Windows.Forms.NotifyIcon] $notifyIcon [System.Windows.Forms.ListBox] $logListBox [System.Windows.Forms.ContextMenu] $contextMenu [System.Windows.Forms.MenuItem] $menuItemChangeProcessName [System.Windows.Forms.MenuItem] $menuItemChangeCheckInterval [System.Windows.Forms.MenuItem] $menuItemExit [System.Windows.Forms.Timer] $timer [System.Array] $previousProcesses [System.Collections.Hashtable] $processStartTime [string] $logFilePath [string] $processNameToMonitor [string] $caption [bool] $menuAskClosing [int] $intervalInSec CustomApplicationContext() { $this.processNameToMonitor = "explorer" $this.intervalInSec = 10 $this.logFilePath = "$env:TEMP\ProcessMonitor_$($this.processNameToMonitor).log" $this.Init($this.processNameToMonitor, $this.intervalInSec, $this.logFilePath) } CustomApplicationContext([string] $processNameToMonitor, [int] $intervalInSec) { $this.logFilePath = "$env:TEMP\ProcessMonitor_$processNameToMonitor.log" $this.Init($processNameToMonitor, $intervalInSec, $this.logFilePath) } CustomApplicationContext([string] $processNameToMonitor, [int] $intervalInSec, [string] $logFilePath) { $this.Init($processNameToMonitor, $intervalInSec, $logFilePath) } [void] Init([string] $processNameToMonitor, [int] $intervalInSec, [string] $logFilePath) { # Store parameters $this.processNameToMonitor = $processNameToMonitor $this.intervalInSec = $intervalInSec $this.caption = "Process Monitor - {0} - {1} s" -f $processNameToMonitor, $intervalInSec $this.menuAskClosing = $false $this.previousProcesses = @() $this.processStartTime = @{} $this.logFilePath = $logFilePath # Create a form $this.form = New-Object System.Windows.Forms.Form $this.form.Text = $this.caption $this.form.Size = New-Object System.Drawing.Size(800, 300) $this.form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen $this.form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable # Create a list box to display the log $this.logListBox = New-Object System.Windows.Forms.ListBox $this.logListBox.Dock = [System.Windows.Forms.DockStyle]::Fill $this.logListBox.SelectionMode = [System.Windows.Forms.SelectionMode]::MultiExtended $this.logListBox.FormattingEnabled = $true $this.logListBox.Font = "Courier" $this.form.Controls.Add($this.logListBox) # Create a NotifyIcon for the system tray $this.notifyIcon = New-Object System.Windows.Forms.NotifyIcon $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$env:SystemRoot\System32\SHELL32.dll") $this.notifyIcon.Icon = $icon $this.notifyIcon.Text = $this.caption $this.notifyIcon.Visible = $true # Create a context menu to the NotifyIcon $this.contextMenu = New-Object Windows.Forms.ContextMenu # MenuItem to change the searched process name $this.menuItemChangeProcessName = New-Object Windows.Forms.MenuItem $this.menuItemChangeProcessName.Text = "Change Process Name" # MenuItem to exit the application $this.menuItemExit = New-Object Windows.Forms.MenuItem $this.menuItemExit.Text = "Exit" # MenuItem to change perdiode between 2 checks $this.menuItemChangeCheckInterval = New-Object Windows.Forms.MenuItem $this.menuItemChangeCheckInterval.Text = "Change Checking Interval" # Complete context menu $this.contextMenu.MenuItems.Add($this.menuItemChangeProcessName) $this.contextMenu.MenuItems.Add($this.menuItemChangeCheckInterval) $this.contextMenu.MenuItems.Add($this.menuItemExit) # Add contexte menu to Tray Icon and Form $this.notifyIcon.ContextMenu = $this.contextMenu $this.form.ContextMenu = $this.contextMenu # Create a timer to periodically check for processes $self = $this $this.timer = New-Object System.Windows.Forms.Timer $this.timer.Interval = $this.intervalInSec * 1000 $this.timer.Add_Tick({ $self.TimerProcess() }) # Handle the FormClosing event to dispose of resources $this.form.Add_FormClosing({ if ($self.menuAskClosing) { $self.timer.Stop() $self.timer.Dispose() $self.notifyIcon.Dispose() $self.form.Dispose() $self.ExitThread() } else { $self.form.Hide() $self.notifyIcon.ShowBalloonTip(2000, "Process Monitor", "The application has been minimized to the system tray.", [System.Windows.Forms.ToolTipIcon]::Info) $_.Cancel = $true # Cancel the default close action } }) # Handle the FormKeyPress event to check for the shortcuts $this.form.KeyPreview = $true $this.form.Add_KeyDown({ if ($_.Control -and $_.KeyValue -eq 80) { # Check for Ctrl+P (ASCII code 80) $self.menuItemChangeProcessName.PerformClick() # Trigger the menu item click event } if ($_.Control -and $_.KeyValue -eq 73) { # Check for Ctrl+I (ASCII code 73) $self.menuItemChangeCheckInterval.PerformClick() # Trigger the menu item click event } if ($_.Control -and $_.KeyValue -eq 67) { # Check for Ctrl+C (ASCII code 67) $self.CopySelectedItemsToClipboard() } if ($_.Control -and $_.KeyValue -eq 65) { # Check for Ctrl+A (ASCII code 65) $self.SelectAllListItems() } }) # Handle the NotifyIcon Click event to show/hide the form $this.notifyIcon.Add_Click({ if ($_.Button -ne [System.Windows.Forms.MouseButtons]::Left) { return } if ($self.form.Visible) { $self.form.Hide() } else { $self.form.Show() $self.form.Activate() } }) # Handle the Change Process Name menu button click event $this.menuItemChangeProcessName.Add_Click({ $self.ChangeProcessName($self.AskNewProcessName()) $self.ChangeCaption() }) # Handle the Change Interval menu button click event $this.menuItemChangeCheckInterval.Add_Click({ $self.ChangeInterval($self.AskNewInterval()) $self.ChangeCaption() }) $this.menuItemExit.Add_Click({ $self.menuAskClosing = $true $self.form.Close() }) # Start the timer $this.timer.Start() } ########################################################################### # Constructor - End # ########################################################################### # Core process of the timer [void] TimerProcess() { $changedProcesses = $this.UpdateProcesses() foreach ($changedProcess in $changedProcesses) { $logEntry = $this.PrintLog($changedProcess) $this.PrintInForm($logEntry) $this.PrintInFile($logEntry) } } # List the latest created and stoped processes [array] UpdateProcesses() { $processStatuses = @() $currentProcesses = Get-Process -Name $this.processNameToMonitor -ErrorAction SilentlyContinue if ($currentProcesses -eq $null -and $this.previousProcesses.Count -eq 0) { return $processStatuses } if ($currentProcesses -ne $null) { $newProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "=>" } $endedProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "<=" } # Update the previous Xxxx processes $this.previousProcesses = $currentProcesses } else { $endedProcesses = $this.previousProcesses $newProcesses = @() $this.previousProcesses = @() } foreach ($newProcess in $newProcesses) { $processStatuses += [PSCustomObject]@{ Status = "Started" Process = $newProcess } } foreach ($endedProcess in $endedProcesses) { $processStatuses += [PSCustomObject]@{ Status = "Ended" Process = $endedProcess } } return $processStatuses } # Format the process info to log string # Then send to printer with form or file [string] PrintLog([PSCustomObject] $processInfo) { $process = $processInfo.Process $status = $processInfo.Status $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $startTime = $process.StartTime if (-not $process.HasExited) { $this.processStartTime[$process.Id] = $process.StartTime } else { $startTime = $this.processStartTime[$process.Id] $this.processStartTime.Remove($process.Id) } $processTime = ((Get-Date) - $startTime).ToString("hh\:mm\:ss") $privateBytes = [int]($process.PrivateMemorySize64 / 1MB) $logEntry = "[{0}] {1} | PID: {2,-6}| Status: {3,-8}| Run Time: {4} | Private Bytes: {5,4} MB" -f $timestamp, $process.Name, $process.Id, $status, $processTime, $privateBytes return $logEntry } # Print log entry in form [void] PrintInForm([string] $logEntry) { $this.logListBox.Items.Add($logEntry) $this.logListBox.ClearSelected() $this.logListBox.SelectedIndex = $this.logListBox.Items.Count - 1 # Focus on the last element } # Print log entry in file [void] PrintInFile([string] $logEntry) { if (-not (Test-Path($this.logFilePath)) -and $this.logFilePath -ne '') { New-Item -Path $this.logFilePath -ItemType File } # Append the log entry to a file $logEntry | Out-File -Append -FilePath $this.logFilePath } # Show dialog to input new process name to monitor [string] AskNewProcessName(){ # Show a dialog to input the new process name $title = "Change Process Name" $msg = "Enter the new process name:" $newProcessName = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title, $this.processNameToMonitor) if (-not [string]::IsNullOrWhiteSpace($newProcessName)) { return $newProcessName } return $this.processNameToMonitor } # Show dialog to input new check interval [int] AskNewInterval(){ # Show a dialog to input the new periode $title = "Change Checking Interval" $msg = "Enter the new periode in secondes:" $newInterval = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title, $this.intervalInSec) if (![string]::IsNullOrEmpty($newInterval) -and [decimal]::TryParse($newInterval, [ref]$null) -and $newInterval -gt 0) { return $newInterval } return $this.intervalInSec } # Set process name property and clear the previous processes pile [void] ChangeProcessName([string] $newProcessName) { if ($newProcessName -ne $this.processNameToMonitor){ $this.processNameToMonitor = $newProcessName $this.previousProcesses = @() } } # Set interval property and update timer interval [void] ChangeInterval([int] $newInterval) { if ($newInterval -ne $this.intervalInSec){ $this.intervalInSec = [int]$newInterval $this.timer.Interval = $this.intervalInSec *1000 } } # Set caption of tray icon and form according to process name and interval properties [void] ChangeCaption() { $this.caption = "Process Monitor - $this.processNameToMonitor - $this.intervalInSec s" $this.notifyIcon.Text = $this.form.Text = $this.caption } # Set clipboard with seleted lines of listbox [void] CopySelectedItemsToClipboard() { # Get the selected
相关产品推荐
相关产品推荐

