托盘退出包括:托盘定制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

posted on 2025-10-15 10:05  TanZhiWei  阅读(9)  评论(0)    收藏  举报