托盘退出包括:托盘定制UI、托盘显示位置、关闭托盘菜单、窗体关闭、资源释放、应用退出
1托盘定制UI(Styles.ContextMenu.xaml)
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 菜单样式 -->
<Style x:Key="Style.ContextMenu" TargetType="ContextMenu">
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#E7E7E7" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border
x:Name="MainBorder"
Padding="10,6"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8">
<ItemsPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 菜单项样式 -->
<Style x:Key="Style.MenuItem" TargetType="MenuItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border
x:Name="Border"
Padding="12,6"
Background="Transparent"
Cursor="Hand">
<ContentPresenter VerticalAlignment="Center" ContentSource="Header" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Border" Property="Background" Value="#F5F5F5" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="#181818" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Icon" Value="{x:Null}" />
</Style>
</ResourceDictionary>
2 退出结束进程任务树
using Microsoft.Management.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace LinseerCopilot.Service
{
public class ProcessHelper
{
/// <summary>
/// 通过WMI获取进程的路径
/// </summary>
/// <param name="processId"></param>
/// <returns></returns>
private static string GetProcessPathWmi(int processId)
{
string query = $"SELECT ExecutablePath FROM Win32_Process WHERE ProcessId = {processId}";
string Namespace = @"root\cimv2";
CimSession mySession = CimSession.Create(null);
IEnumerable<CimInstance> queryInstance = mySession.QueryInstances(Namespace, "WQL", query);
return queryInstance.Select(instance => instance.CimInstanceProperties["ExecutablePath"].Value?.ToString()).FirstOrDefault();
}
/// <summary>
/// 获取进程路径
/// </summary>
/// <param name="process"></param>
/// <returns></returns>
public static string GetProcessPath(Process process)
{
try
{
// 使用 MainModule 属性获取路径
var path = process.MainModule?.FileName;
return !string.IsNullOrWhiteSpace(path) ? path : GetProcessPathWmi(process.Id);
}
catch (Exception ex)
{
CommService.Logger.Error($"获取进程路径失败: {ex}");
return null;
}
}
/// <summary>
/// 删除进程树
/// </summary>
/// <param name="rootPid"></param>
public static void KillProcessTree(int rootPid)
{
try
{
// 获取所有子进程PID
var allPids = GetChildProcesses(rootPid);
allPids.Add(rootPid); // 包含根进程
// 从最底层子进程开始终止
foreach (var pid in allPids)
{
try
{
using var process = Process.GetProcessById(pid);
process.Kill();
}
catch (Exception)
{
//ignore
}
}
}
catch (Exception ex)
{
CommService.Logger.Error($"删除进程树异常:{ex}");
}
}
public static List<int> GetChildProcesses(int parentPid)
{
var children = new List<int>();
try
{
string Namespace = @"root\cimv2";
string OSQuery = $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {parentPid}";
CimSession mySession = CimSession.Create(null);
IEnumerable<CimInstance> queryInstance = mySession.QueryInstances(Namespace, "WQL", OSQuery);
foreach (var instance in queryInstance)
{
int childPid = Convert.ToInt32(instance.CimInstanceProperties["ProcessId"].Value);
children.Add(childPid);
children.AddRange(GetChildProcesses(childPid)); // 递归获取
}
}
catch (Exception e)
{
CommService.Logger.Error($"获取子进程失败: {e}");
}
return children;
}
/// <summary>
/// 启动进程开启更新器
/// </summary>
/// <returns></returns>
public static void StartProcessByAdmin(string fileName, string arg)
{
var startInfo = new ProcessStartInfo
{
FileName = fileName, // 要运行的程序
Arguments = arg, // 参数(示例为执行命令)
UseShellExecute = true, // 必须为 true,否则 Verb 设置无效
Verb = "runas", // 以管理员权限运行
CreateNoWindow = true,
RedirectStandardError = false,
RedirectStandardInput = false,
RedirectStandardOutput = false,
};
using var process = Process.Start(startInfo);
}
}
}
3 退出整体方案
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using Application = System.Windows.Application;
using ContextMenu = System.Windows.Controls.ContextMenu;
using MenuItem = System.Windows.Controls.MenuItem;
namespace LinseerCopilot.Service
{
internal class NotifyIconManager
{
#region Fields
private readonly NotifyIcon _notifyIcon;
private ContextMenu _contextMenu;
#endregion
#region Constructors
public NotifyIconManager()
{
_notifyIcon = new NotifyIcon();
InitNotifyIcon();
HookExUtils.MouseLeftButtonUp += HookExUtils_MouseLeftButtonUp;
HookExUtils.MouseRightButtonUp += HookExUtils_MouseRightButtonUp;
}
private void HookExUtils_MouseRightButtonUp(object sender, MouseEventArgs e)
{
_contextMenu.IsOpen = false;
}
private void HookExUtils_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
_contextMenu.IsOpen = false;
}
#endregion
#region Private Methods
private void InitNotifyIcon()
{
UpdateNotifyIconSource();
_notifyIcon.Text = "灵犀助手";
_notifyIcon.Click += NotifyIcon_Click;
var contextMenuStyle = (Style)Application.Current.FindResource("Style.ContextMenu");
var menuItemStyle = (Style)Application.Current.FindResource("Style.MenuItem");
// 创建WPF右键菜单
_contextMenu = new ContextMenu
{
Style = contextMenuStyle,
Items =
{
new MenuItem {Style = menuItemStyle,Header = "打开灵犀助手", Command = new Microsoft.Xaml.Behaviors.Core.ActionCommand(OpenMainWindow)},
new MenuItem {Style = menuItemStyle, Header = "退出", Command = new Microsoft.Xaml.Behaviors.Core.ActionCommand(ExitApp) }
}
};
}
private async void ExitApp()
{
if (_notifyIcon != null)
{
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
}
CloseAllWindows();
CommService.Logger.Info("【开始退出】退出后台Ai服务");
var stopAiTask = Task.Run(StopAiServiceProcesses);
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
var completedTask = await Task.WhenAny(stopAiTask, timeoutTask);
if (completedTask == timeoutTask)
{
CommService.Logger.Info("【退出超时】强制终止Ai服务清理流程");
}
CommService.Logger.Info("【开始退出】调用Shutdown");
Application.Current.Shutdown();
}
/// <summary>
/// 强制关闭所有打开的窗口
/// </summary>
private void CloseAllWindows()
{
foreach (Window window in Application.Current.Windows)
{
try
{
window.Close(); // 触发窗口关闭事件
// 如果窗口未响应Close,可以强制隐藏
if (window.IsVisible)
{
window.Hide();
}
}
catch (Exception ex)
{
CommService.Logger.Error($"关闭窗口失败: {ex.Message}");
}
}
}
/// <summary>
/// 停止Ai微服务
/// </summary>
public void StopAiServiceProcesses()
{
foreach (var aiServiceProcess in CustomText.AiServiceProcessNames)
{
var processes = Process.GetProcessesByName(aiServiceProcess);
foreach (var process in processes)
{
try
{
var processPath = ProcessHelper.GetProcessPath(process);
var isContain = IsFileInFolder(processPath, CustomText.DefaultInstallPath);
if (isContain)
{
ProcessHelper.KillProcessTree(process.Id);
}
}
catch (Exception)
{
//ignore
}
}
}
}
/// <summary>
/// 判断文件是否在文件夹内
/// </summary>
/// <param name="filePath"></param>
/// <param name="folderPath"></param>
/// <returns></returns>
private bool IsFileInFolder(string filePath, string folderPath)
{
if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(folderPath))
return false;
try
{
// 转换为绝对路径
string fullFilePath = Path.GetFullPath(filePath);
string fullFolderPath = Path.GetFullPath(folderPath);
// 处理文件夹路径,确保以目录分隔符结尾
fullFolderPath = fullFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (fullFolderPath.Length == 0)
fullFolderPath = Path.DirectorySeparatorChar.ToString();
fullFolderPath += Path.DirectorySeparatorChar;
// 比较路径
return fullFilePath.StartsWith(fullFolderPath, StringComparison.OrdinalIgnoreCase)
&& fullFilePath.Length > fullFolderPath.Length;
}
catch (ArgumentException)
{
return false; // 处理无效路径
}
catch (NotSupportedException)
{
return false; // 处理包含冒号等无效字符的情况
}
}
private void OpenMainWindow()
{
_contextMenu.IsOpen = false;
AppService.WindowsManager.CloseSimpleMainWindow();
AppService.WindowsManager.ShowMainWindow();
}
private void UpdateNotifyIconSource()
{
string iconAbsolutePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "icon.ico");
_notifyIcon.Icon = new Icon(iconAbsolutePath);
}
private void NotifyIcon_Click(object sender, EventArgs e)
{
var mouseArgs = e as MouseEventArgs;
if (mouseArgs == null || mouseArgs.Button != MouseButtons.Right)
{
AppService.WindowsManager.ShowMainWindow();
return;
}
if (mouseArgs.Button == MouseButtons.Right)
{
// 获取鼠标位置并显示菜单
var mousePos = Control.MousePosition;
var dpiScale = ScreenHelper.GetDpiScaleAtMousePosition();
Debug.WriteLine($"应用退出 dpiScale :{dpiScale}");
_contextMenu.IsOpen = true;
_contextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.AbsolutePoint;
_contextMenu.HorizontalOffset = mousePos.X / dpiScale;
_contextMenu.VerticalOffset = mousePos.Y / dpiScale;
}
}
#endregion
#region Public Methods
public void ShowNotifyIon()
{
_notifyIcon.Visible = true;
}
#endregion
}
}
备注:
HookExUtils.MouseLeftButtonUp 参考:https://www.cnblogs.com/terryK/p/19034372
ScreenHelper.GetDpiScaleAtMousePosition 参考:https://www.cnblogs.com/terryK/p/19034288
浙公网安备 33010602011771号