我有这段代码来创建一个非常简单的隐藏过程,其中包含一个图标托盘和一个上下文菜单。当该功能通过竞赛菜单被激活时,脚本开始发送字母";q〃;每5秒。
[System.Reflection.Assembly]::LoadWithPartialName('presentationframework') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('WindowsFormsIntegration') | out-null
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:WindowsSystem32mmc.exe")
# Part - Add the systray menu
$Main_Tool_Icon = New-Object System.Windows.Forms.NotifyIcon
$Main_Tool_Icon.Text = "WPF Systray tool"
$Main_Tool_Icon.Icon = $icon
$Main_Tool_Icon.Visible = $true
$Menu_Exit = New-Object System.Windows.Forms.MenuItem
$Menu_Exit.Text = "Exit"
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Main_Tool_Icon.ContextMenu = $contextmenu
#by Franco
$Menu_Key = New-Object System.Windows.Forms.MenuItem
$Menu_Key.Text = "Invia pressione tasto ogni 5 secondi"
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($Menu_Exit)
$Main_Tool_Icon.contextMenu.MenuItems.AddRange($Menu_Key)
# When Exit is clicked, close everything and kill the PowerShell process
$Menu_Exit.add_Click({
$Main_Tool_Icon.Visible = $false
Stop-Process $pid
})
#send Key "q" every 5 seconds
$Menu_key.add_Click({
$a = new-object -com "wscript.shell"
while ($true){
sleep 5
$a.sendkeys("q")
}
})
# Make PowerShell Disappear
$windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
$asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
$null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0)
# Force garbage collection just to start slightly lower RAM usage.
[System.GC]::Collect()
# Create an application context for it to all run within.
# This helps with responsiveness, especially when clicking Exit.
$appContext = New-Object System.Windows.Forms.ApplicationContext
[void][System.Windows.Forms.Application]::Run($appContext)
当我想用退出菜单上下文按钮退出脚本时,问题就出现了,直到我用鼠标左键再次单击图标托盘,它才真正退出。只要我用鼠标左键点击图标,图标就会消失;q〃;。当我在没有首先通过上下文菜单激活sendkeys方法的情况下退出scrip wia图标上下文菜单时,问题不会发生。但是如果我这样更改代码:
#send Key "q" every 5 seconds
$Menu_key.add_Click({
$a = new-object -com "wscript.shell"
$c = 0
while ($c -lt 5){
sleep 5
$a.sendkeys("q")
$c++
}
})
它退出,在点击相同的退出按钮后,只有在while循环结束后,有没有办法在循环结束前中断循环?
我也不明白脚本的以下部分是做什么的:
$appContext = New-Object System.Windows.Forms.ApplicationContext
[void][System.Windows.Forms.Application]::Run($appContext)
和
#Force garbage collection just to start slightly lower RAM usage.
[System.GC]::Collect()
我在gitHub上下载模型:Build-PS1-Systray-Tool
感谢的帮助
Franco
我也不明白脚本的以下部分是做什么的:
[System.GC]::Collect()
它强制同步垃圾回收,即回收不再被任何应用程序引用的对象所占用的内存
这样做与您的脚本无关,因为没有明显的理由这样做:不再引用的对象的没有积累,这可能会导致需要缓解的内存压力。
$appContext = New-Object System.Windows.Forms.ApplicationContext
[void][System.Windows.Forms.Application]::Run($appContext)
此代码是通知区域(系统托盘)图标处理(WinForms)GUI事件所必需的,以便与关联的上下文菜单进行交互。
具体而言,它通过[System.Windows.Forms.Application]::Run()
启动阻塞窗口消息循环,该循环一直运行到存储在$appContext
中的应用程序上下文对象([System.Windows.Forms.ApplicationContext]
通过对.ExitThread()
的调用发出退出的信号)
这类似于在更典型的WinForms场景中对.ShowDialog()
的调用,其中表单将以模式显示;在这种情况下,当窗体关闭时,阻塞调用结束。
除非显式创建其他线程,否则脚本将在单个线程中运行,包括作为事件代理的服务器的脚本块。
您提到最终使用Start-ThreadJob
执行定期任务,这实际上是首选的解决方案,因为它使您能够并行执行甚至长时间运行的任务,而不会干扰UI的响应能力。
事实上,正是原始脚本的单线程特性导致了问题:$Menu_key.add_Click()
事件处理程序中的sleep 5
(Start-Sleep -Seconds 5
)调用阻塞了UI事件处理,导致了您看到的症状。
有一些方法可以避免这种情况,但创建一个单独的线程会给您带来更大的灵活性
也就是说,在您的特定情况下——每N秒运行一次快速操作——使用WinForms计时器对象可能是最简单的解决方案。
以下自包含的示例代码演示了在WinForms应用程序上下文中定期运行操作的三种方法:
-
使用带有
Start-ThreadJob
的线程作业在单独的线程中运行操作。 -
使用WinForms计时器对象(
System.Windows.Forms.Timer
)每N秒自动调用一个脚本块。 -
通过
System.Windows.Forms.Application.DoEvents
使用嵌套的手动事件处理循环,调用必要的短时间间隔来处理UI事件,同时执行其间的其他操作
注意:这在单一形式、单一通知图标的场景中效果良好,但在其他情况下应避免。- 避免嵌套
DoEvents()
循环的一种方法是完全放弃使用应用程序上下文和[Application]::Run()
,而是在主线程中实现单个DoEvents()
循环-请参阅以下答案
- 避免嵌套
注意:
-
基于您的代码,在语法方面以及通过使用
System.Windows.Forms.ContextMenuStrip
和System.Windows.Forms.ToolStripMenuItem
来替换过时的System.Windows.Forms.ContextMenu
和System.Windows.Forms.MenuItem
类(这两个类在.NET(Core)3.1+.中都不再可用),对代码进行了简化 -
PowerShell窗口隐藏代码被注释掉,以保持会话可见,因为周期性操作是通过
Write-Verbose
模拟并输出到控制台的。 -
ApplicationContext.ExitThread()
用于退出消息循环,以便能够有序关闭,作为通过Stop-Process
关闭当前进程的更好选择 -
有关详细信息,请参阅源代码注释。
Add-Type -AssemblyName System.Windows.Forms
Write-Verbose -Verbose 'Setting up a notification (system-tray) with a context menu, using PowerShell''s icon...'
# Create the context menu.
$contextMenuStrip = [System.Windows.Forms.ContextMenuStrip]::new()
# Create the notification (systray) icon and attach the context menu to it.
$notifyIcon =
[System.Windows.Forms.NotifyIcon] @{
Text = "WinForms notification-area utility"
# Use PowerShell's icon
Icon = [System.Drawing.Icon]::ExtractAssociatedIcon((Get-Process -Id $PID).Path)
ContextMenuStrip = $contextMenuStrip
Visible = $true
}
# Add menu items to the context menu.
$contextMenuStrip.Items.AddRange(@(
($menuItemExit = [System.Windows.Forms.ToolStripMenuItem] @{ Text = 'Exit' })
($menuItemDoPeriodically = [System.Windows.Forms.ToolStripMenuItem] @{ Text = 'Do something periodically' })
))
# How frequently to take action.
$periodInSeconds = 2
# Set up a WinForms timer that is invoked periodically - to be started later.
$timer = [System.Windows.Forms.Timer] @{ InterVal = $periodInSeconds * 1000 }
$timer.Add_Tick({
# Sample action: Print a verbose message to the console.
# See note re need for the action to run quickly below.
Write-Verbose -vb Tick
})
# Set up the context menu-item event handlers:
$menuItemExit.add_Click({
# Dispose of the timer and the thread job and set the script-scope variable to signal that exiting has been requested.
$timer.Dispose()
if ($script:threadJob) { $threadJob | Remove-Job -Force }
$script:done = $true
# Tell the WinForms application context to exit its message loop.
# This will cause the [System.Windows.Forms.Application]::Run() call below to return.
$appContext.ExitThread()
# Dispose of and thereby implicitly remove the notification icon.
$notifyIcon.Dispose()
})
$menuItemDoPeriodically.add_Click({
Write-Verbose -Verbose "Starting periodic actions, first invocation in $periodInSeconds seconds..."
# Best solution, using a thread job.
# Since it runs in a *separate thread*, there's no concern about blocking the
# main thread with long-running operations, such as extended sleeps.
$script:threadJob = Start-ThreadJob {
while ($true) {
Start-Sleep $using:periodInSeconds
# Sample action: print a message to the console.
# Note: [Console]::WriteLine() writes directly to the display, for demo purposes.
# Otherwise, the output from this thread job would have to be collected on
# demand with Receive-Job
[Console]::WriteLine('Tock')
}
}
# *Single-threaded solutions* that rely on whatever runs periodically
# to run *quickly enough* so as to not block event processing, which
# is necessary to keep the notification icon and its context menu responsive.
# Solution with the previously set up WinForms timer.
$timer.Start()
# Alternative: A manual loop, which allows foreground activity while waiting
# for the next period to elapse.
# To keep the notification icon and its context menu responsive,
# [System.Windows.Forms.Application]::DoEvents() must be called in *short intervals*,
# i.e. whatever action you perform must run *quickly enough* so as to keep the UI responsive.
# This effectively makes *this* loop the UI event loop, which works in this simple case,
# but is generally best avoided.
# Keep looping until the script-level $done variable is set to $true
$sw = [System.Diagnostics.Stopwatch]::StartNew()
while (-not $script:done) {
if ($sw.Elapsed.TotalSeconds -ge $periodInSeconds) { # Period has elapsed.
$sw.Reset(); $sw.Start()
# Sample action: Print a verbose message to the console.
Write-Verbose -vb Tack
}
# Ensure that WinForms GUI events continue to be processed.
[System.Windows.Forms.Application]::DoEvents()
# Sleep a little to lower CPU usage, but short enough to keep the
# context menu responsive.
Start-Sleep -Milliseconds 100
}
})
# Activate this block to hide the current PowerShell window.
# For this demo, the window is kept visible to see event output.
# # Hide this PowerShell window.
# (Add-Type -PassThru -NameSpace NS$PID -Name CHideMe -MemberDefinition '
# [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
# public static void HideMe() { ShowWindow(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle, 0 /* SW_HIDE */); }
# ')::HideMe()
# Initialize the script-level variable that signals whether the script should exit.
$done = $false
Write-Verbose -Verbose @'
Starting indefinite event processing for the notification-area icon (look for the PowerShell icon)...
Use its context menu:
* to start periodic actions
* to exit - do NOT use Ctrl-C.
'@
# Create an application context for processing WinForms GUI events.
$appContext = New-Object System.Windows.Forms.ApplicationContext
# Synchronously start a WinForms event loop for the application context.
# This call will block until the Exit context-menu item is invoked.
$null = [System.Windows.Forms.Application]::Run($appContext)
# The script will now exit.
# Note: If you run the script from an *interactive* session, the session will live on (see below)
# In a PowerShell CLI call, the process will terminate.
Write-Verbose -Verbose 'Exiting...'
# # Activate this statement to *unconditionally* terminate the process,
# # even when running interactively.
# $host.SetShouldExit(0)