You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

运行时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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 00:01:01