powershell powerhell运行问题最小化为图标托盘

mftmpeh8  于 2023-04-30  发布在  Shell
关注(0)|答案(1)|浏览(208)

我有这个代码来创建一个非常简单的隐藏进程与图标托盘的上下文菜单。当通过竞赛菜单激活该功能时,脚本开始每5秒发送字母“q”。

[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:\Windows\System32\mmc.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方法的情况下退出脚本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
谢谢你的帮助
佛朗哥

jdzmm42g

jdzmm42g1#

我也不明白脚本的下面部分是做什么的:
[System.GC]::Collect()
强制 * 同步 * garbage collection,即。即回收由不再被任何应用程序引用的对象所占用的存储器。

这样做与您的脚本无关**,因为没有明显的理由这样做:没有积累不再被引用的对象,这些对象可能会导致需要缓解的内存压力。

$appContext = New-Object System.Windows.Forms.ApplicationContext
[void][System.Windows.Forms.Application]::Run($appContext)
为了让您的通知区域(系统托盘)图标处理 (WinForms)GUI事件**,此代码是必需的,以便与相关上下文菜单的交互按预期工作。
具体来说,它通过[System.Windows.Forms.Application]::Run()启动一个阻塞的windows消息循环,该循环一直运行到$appContext中存储的应用程序上下文对象([System.Windows.Forms.ApplicationContext]通过调用.ExitThread()发出退出的信号)。
这类似于在更典型的WinForms场景中对.ShowDialog()的调用,其中 form 将以模式显示;在这种情况下,阻塞调用在表单 * 关闭 * 时结束。
除非显式创建其他线程,否则脚本将在单个线程中运行,包括作为事件委托服务器的脚本块。
您提到最终使用
Start-ThreadJob来执行周期性任务,这实际上是*更好的 * 解决方案**,因为它使您能够并行执行长时间运行的任务,而不会干扰UI的响应能力。
实际上,正是原始脚本的单线程特性导致了这个问题:您在$Menu_key.add_Click()事件处理程序中的sleep 5Start-Sleep -Seconds 5)调用 * 阻塞 * UI事件处理,导致您看到的症状。
有一些方法可以避免这种情况,但是创建一个单独的线程会给你带来更大的灵活性。
也就是说,在您的特定情况下-每N秒运行一个快速操作-使用WinForms计时器对象可能是最简单的解决方案。
以下自包含的示例代码演示了在WinForms应用程序上下文中定期运行操作的三种方法

注意:这在单表单、单通知图标的场景中效果很好,但在其他情况下应该避免。

  • 避免 nestedDoEvents()循环的一种方法是完全放弃使用应用程序上下文和[Application]::Run(),而是在主线程中实现单个DoEvents()循环(参见this answer)。

注意事项:

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)

相关问题