C#实战:Windows进程管理与优雅关闭程序窗口指南
做 Windows 桌面开发的朋友迟早会接到一个听起来很简单的需求用 C# 去关闭其他程序窗口、进程。我第一次深入碰这个是在一套自动化部署脚本里——新版安装包要覆盖旧版但上一版的老进程还占着文件不先关掉安装器永远提示“文件被占用”。当时我脑子里第一反应就是 Process.Kill()等真把工具写出来跑了几轮才发现关进程这件事从“怎么定位目标”到“发消息还是强杀”每一步都有讲究。这篇我把自己从零到一写成一个可用的关闭工具的过程完整梳理出来适合正在做桌面工具箱、自动化运维脚本或者单纯想把卡死应用一键清掉的朋友。1. 先理清关系窗口关了进程不一定结束大多数人提到“关程序”脑子里其实把两件事混在一起了把窗口关掉和把进程终结掉。在 Windows 里这两者高度相关却绝不是一回事。进程是操作系统分配资源的基本单位它本身没有界面。我们看到的窗口是进程内部某个线程创建的 UI 对象。一个进程可以创建多个窗口一个窗口也可能被多个进程共享老式浏览器架构里就很常见。反过来很多进程一个窗口都没有却在后台跑得正欢。如果你只盯着 Process.MainWindowHandle很容易漏掉那些没有主窗口的后台进程。窗口背后是一套消息循环。系统往窗口消息队列里投递一条 WM_CLOSE窗口过程处理完这段逻辑该保存的保存、该弹窗的弹窗然后销毁窗口。如果这是进程里最后一个窗口消息循环退出进程随之结束。但这里有两个例外第一程序完全可以拦截 WM_CLOSE弹一个“是否保存”对话框藏着不关第二主窗口关了进程里还有其他工作线程在跑进程同样不会退。所以“关闭窗口”本质上只是向进程发出一个协作请求最终到底有没有退出必须拿进程状态来判断不能想当然。举个最常见的例子记事本收到 WM_CLOSE 会问你存不存确定后退出但很多带系统托盘的即时通讯软件主窗口收到关闭消息后只隐藏界面进程还挂在后台。你要是以为发条消息就完事等于白做。所以在我自己的工具里“关闭窗口”和“结束进程”被设计成两个独立动作让用户自己决定走到哪一步。拿到需求时我也习惯先把它归到三类模型里只要求界面消失后台继续跑要求程序正常退出并保存现场进程无论如何都得从内存里消失。第一种场景在正规开发里很少见第二种应该优先发 WM_CLOSE第三种才轮到 Kill 或 taskkill。工具如果一开始没厘清这三种后面代码很容易越改越乱。2. 定位目标进程不能关一个找不到的东西写关进程工具第一步永远是定位。Process.GetProcesses() 会列出系统里所有进程返回一个 Process 数组。做“列表型”工具时这么干没问题数据量大点也无所谓但如果目标是某个已知程序先用名称过滤明显更划算。三种匹配习惯我都在不同项目里用过GetProcessesByName(notepad)简单直接但要注意系统允许同名进程开多个实例比如记事本开了五个五个全给你找出来是全关还是挑一个得想清楚。按窗口标题过滤适合做 UI 工具。用户在列表里看到“未命名 - 记事本”这类标题比看到一串 PID 直观得多。按可执行文件路径匹配最精确但代价也最大因为拿别的进程路径这件事本身就有坑。拿路径的坑值得单独说。Process.MainModule.FileName 在某些场景下会直接抛 Win32Exception常见于目标进程权限高于当前进程或者从 32 位进程去访问 64 位进程的模块。这个字段并非总能拿到不能把它当作可靠的定位依据。真要拿路径更稳的方案是用 P/Invoke 调 QueryFullProcessImageName先 OpenProcess 拿到句柄再读完整路径。这一整套我在第五节还会展开。Process 对象有缓存这是一个非常容易被忽略的点。你 new 出一个 Process 对象或者从数组里拿到的 Process里面的 MainWindowTitle、MainWindowHandle 都可能只是一瞬间的快照。每次用之前习惯性调一次 Refresh()看到的数据才接近当前状态。我早期写工具时没加 Refresh窗口标题一直拿到空值排查了半天才发现是缓存没刷新。另外要注意MainWindowHandle 只有在进程拥有可见主窗口时才不等于零。某些程序启动时没有主窗口或者主窗口被隐藏了这个属性就是 IntPtr.Zero。你拿一个 0 句柄去发消息等于什么都没做。所以拿到句柄后先判断不为空再考虑下一步。还有一个更深的坑一个进程的顶层窗口可能不止一个。MainWindowHandle 只代表系统认为的“主窗口”其他的工具窗、启动画面、对话框都不在这里面。要真正把某个进程的所有窗口都找出来得用用户层 API 枚举所有顶层窗口逐个匹配进程 ID。第六节的代码里我会给出这部分的实现。定位阶段的工具体验设计也很重要用名称过滤的结果最好在列表里把 PID、窗口标题、路径都显示出来让用户二次确认。关错进程的代价比慢半秒高得多。3. 给窗口发“请关闭”的消息优雅退出路线很多人以为关窗口就是调用 Process.CloseMainWindow()它确实是 .NET 封装好的“向主窗口发 WM_CLOSE”的方法。简单场景下这行代码就够了但我在实际项目里用过几次之后还是更倾向于直接用 P/Invoke 发 PostMessage。这么取舍下面三个原因都很实在。第一CloseMainWindow 只能作用于 MainWindowHandle遇到多顶层窗口或者主窗口句柄抓不到的情况就失效了。自己用 EnumWindows 枚举出来的句柄列表配合 PostMessage 可以做到“所有窗口都收到关闭请求”。第二.NET 的 CloseMainWindow 调用成功与否只返回 bool你拿不到更细的状态自己发消息则可以把“消息已投递”和“进程已退出”分成两步判断逻辑更清楚。第三我发消息用的是 PostMessage 而不是 SendMessage因为 SendMessage 会等目标窗口处理完才返回目标进程要是卡死你这个调用线程会被一起拖死PostMessage 只是把消息丢进队列立刻返回配合进程退出轮询来控制节奏反而更不容易让工具卡住。WM_CLOSE 本质是“礼貌的请求”。程序如果不理会消息就只是被吞掉窗口照旧。所以在发完消息之后大多数人需要的是这样一个循环发送 WM_CLOSE等待一小段时间检查目标进程是否退出超时再走强杀。等待退出用 WaitForExit(timeoutMs)。这个方法会阻塞当前线程直到进程退出或超时。注意别把它和 HasExited 搞混HasExited 是瞬时状态刚发完消息立刻查基本还是 false没有参考价值。还有一个细节Process.WaitForExit() 不带参数会无限等工具里一定要给超时时间否则遇上无响应的进程你的工具按钮整个跟着卡住。程序拦截 WM_CLOSE 弹“是否保存”对话框时简单方案是把超时设长一点或者干脆在弹窗场景下直接提示用户走强制路线。我不建议去模拟点击“确定”按钮不同程序对话框文案千差万别写死的匹配逻辑很容易在某个版本上失效。4. 暴力路线强制终止进程及进程树先泼一盆冷水Process.Kill() 并不像名字听起来那么干净利落。它本质上是把 TerminateProcess 系统调用包了包调用之后目标进程直接结束不给任何保存、回收、上报的机会。你在记事本里打了一大段文字没保存Kill 一下文字全没。所以只能把它当最后手段。另一个容易被忽略的事实Kill() 是异步的。调用返回时目标进程可能还没从系统里完全消失。实践里经常遇到 Kill 完马上查 HasExited 仍然是 false 的情况。正确姿势是 Kill 之后接一个 WaitForExit()等真正退干净再做后续比如检查文件是否被释放。再顺带说一句Kill 一个无权限操作的进程时异常很常见。你以普通权限去杀某个管理员开的进程通常会抛 Win32Exception拒绝访问。这不是代码写错了是权限模型在起作用处理方式只有提权没有别的玄学第五节我会细说。如果目标程序还带一堆子进程单个 Kill 主进程可能留下孤儿。比如一个正在打包的脚本主进程被你杀了它 spawn 出的子进程可能还在继续写文件。要连根拔起我常用的有两招。第一招最简单直接调用系统自带的 taskkilltaskkill /PID 123 /T /F/T 表示递归结束子进程/F 表示强制。在 C# 里用一个隐藏窗口的 ProcessStartInfo 启动 taskkill 就完事。优势是兼容性好不用引额外库代码量极小缺点是没有子进程时它也照样跑一个外部进程多少有些浪费而且 /F 等于把所有相关进程都强制杀光不够精细。第二招是用 WMI 递归算父子关系SELECT ProcessId FROM Win32_Process WHERE ParentProcessId 123拿到子进程列表之后逐个 Kill再递归查孙进程。这招灵活可以搭配业务逻辑做选择性清理但多写不少代码还要引入 System.Management 包。给个最小递归实现using System.Management; static Listint GetChildProcessIds(int parentPid) { Listint childIds new Listint(); using (var searcher new ManagementObjectSearcher( $SELECT ProcessId FROM Win32_Process WHERE ParentProcessId {parentPid})) { foreach (ManagementObject obj in searcher.Get()) { childIds.Add(Convert.ToInt32(obj[ProcessId])); } } foreach (int childId in childIds) { childIds.AddRange(GetChildProcessIds(childId)); } return childIds; }终结方式对比我做了一个简单的表方便按场景选方式是否等待退出子进程处理风险/复杂度Process.Kill()异步需配合 WaitForExit不处理简单直接数据丢失风险高CloseMainWindow()不等待需自行判断不处理优雅但不响应则无效PostMessage WM_CLOSE不等待需自行判断不处理优雅可控性强taskkill /T /F可同步等待递归结束简单有效但所有目标都会被强杀还有一点任何 Process 对象用完都要 Dispose。进程本身的运行不受你的 Process 对象影响但对象背后的句柄和资源需要释放。长期跑的工具如果每个查询都 new 一批 Process 而不释放句柄数会一路涨上去最后把自己拖死。5. 权限、重定向、误杀防护这堆坑决定了工具能不能用测试环境一切正常一旦拿到别人机器上或者换一个用户运行各种“意想不到”就来了。我按踩过的先后顺序列一下。权限不足是最常见的。Windows 对进程对象的访问是受保护的普通用户默认没有权限去终止另一个用户尤其是管理员启动的进程。调用 Kill 或读取某些属性时异常很直白拒绝访问。解决方式通常是把工具整体提权到管理员运行做法是项目里放一个 app.manifest里面写requestedExecutionLevel levelrequireAdministrator uiAccessfalse /这样程序每次启动都会弹 UAC 确认框。注意提权只能在程序层面做别想着用代码“绕过”某个进程的权限那不是业务工具该碰的领域。有人可能问不弹 UAC 能在运行时自动提权吗可以用“以管理员身份重新启动自己”的方式检测到非管理员权限时用 ProcessStartInfo 把 Verb 设为 runas再启动当前程序一个新实例然后退出旧实例。这种做法在安装器、维护工具里很常见代价是需要处理新旧实例之间的参数传递和短暂的窗口闪烁实际项目里我把原来的命令行参数原封不动拼回去。32 位/64 位混合是第二个坑。从 32 位程序访问 64 位进程时系统会有文件系统重定向和注册表重定向你在 32 位视角下看到的文件路径可能被映射到 SysWOW64导致路径对不上读取目标进程的 MainModule.FileName 在某些调用里也会失败。要拿准确路径我一般用 QueryFullProcessImageName 走 P/Invoke先 OpenProcess 申请 PROCESS_QUERY_LIMITED_INFORMATION 权限再读完整路径最后记得 CloseHandle。代码不算长但比 MainModule 稳定得多。第三个坑是会话隔离。你从普通桌面程序里发的窗口消息到了 Windows 服务进程会话 0里根本找不到窗口句柄。这不代表进程不存在而是服务进程的窗口根本不在你的交互会话里。所以工具如果要去操作别人的服务程序首先要明确期望是通过服务控制管理器去管理服务而不是发窗口消息。窗口那个维度对服务进程不适用。误杀防护必须单独设计。写这种工具的人通常只想关掉自己关心的那批程序但由于定位逻辑太宽一不留神就把重要进程干掉了。我给工具加了两层保险第一层是黑名单System、csrss、smss、wininit 这类系统关键进程一律拒绝操作列表写死第二层是白名单模式默认只允许操作用户手动勾选的进程批量关闭前要出确认清单。日志也一定要有。谁在什么时间关了什么 PID写进一条记录出了问题才好回溯。我见过不少小工具没有日志真误杀了关键进程连定位都无从谈起。6. 拼一个带命令行的小工具关闭清理器原理说得差不多了接下来我把一个最小可用的命令行工具整个走一遍。代码不长但覆盖了定位、优雅关闭、超时强杀、进程树清理四个核心动作。界面方面我用控制台避免为了演示还要起一个窗体项目。命令格式设计成ProcCtl list keyword ProcCtl close keyword [--force] [--tree]list 负责按关键词列出匹配进程close 负责关闭第一个匹配到的进程没带 --force 时先发 WM_CLOSE超时 5 秒再 Kill带 --force 直接强制带 --tree 就把子进程一起处理。核心代码using System.Diagnostics; using System.Runtime.InteropServices; class ProcCtl { const uint WM_CLOSE 0x0010; [DllImport(user32.dll, CharSet CharSet.Unicode)] static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); [DllImport(user32.dll)] static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); [DllImport(user32.dll)] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); static ListProcess FindByKeyword(string keyword) { var matches new ListProcess(); foreach (Process p in Process.GetProcesses()) { p.Refresh(); if (p.ProcessName.Contains(keyword, StringComparison.OrdinalIgnoreCase) || p.MainWindowTitle.Contains(keyword, StringComparison.OrdinalIgnoreCase)) { matches.Add(p); } else { p.Dispose(); } } return matches; } static ListIntPtr GetWindowsOfProcess(int pid) { var handles new ListIntPtr(); EnumWindows((hWnd, lParam) { GetWindowThreadProcessId(hWnd, out uint processId); if (processId pid) { handles.Add(hWnd); } return true; }, IntPtr.Zero); return handles; } static bool CloseGracefully(Process p, int timeoutMs) { foreach (IntPtr hWnd in GetWindowsOfProcess(p.Id)) { PostMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); } if (p.WaitForExit(timeoutMs)) { return true; } p.Kill(); p.WaitForExit(); return false; } static void RunTaskKill(int pid) { ProcessStartInfo psi new ProcessStartInfo(taskkill, $/PID {pid} /T /F); psi.UseShellExecute false; psi.CreateNoWindow true; using Process taskkill Process.Start(psi)!; taskkill.WaitForExit(); } static int Main(string[] args) { if (args.Length 2) { Console.WriteLine(用法: ProcCtl list|close keyword [--force] [--tree]); return 1; } string action args[0]; string keyword args[1]; bool force args.Contains(--force); bool tree args.Contains(--tree); ListProcess candidates FindByKeyword(keyword); if (candidates.Count 0) { Console.WriteLine(没有匹配的进程。); return 0; } if (action list) { foreach (Process p in candidates) { Console.WriteLine($PID{p.Id} 名称{p.ProcessName} 标题{p.MainWindowTitle}); p.Dispose(); } return 0; } foreach (Process p in candidates.Take(1)) { if (force) { if (tree) { RunTaskKill(p.Id); } else { p.Kill(); p.WaitForExit(); } } else { CloseGracefully(p, 5000); } p.Dispose(); } return 0; } }几个实测要点分享先开一个记事本输入文字运行 ProcCtl close notepad会看到记事本弹保存提示说明 WM_CLOSE 确实被它接住了。再开一个 cmd运行 pause进程会一直等着。对它 close发完 WM_CLOSE 之后 5 秒超时就会被强制杀掉这验证了超时兜底逻辑。找一个带托盘的软件试试你会发现主窗口关了但进程还在这正好印证了第一节说的“窗口关闭不等于进程结束”。还有两个目前没处理但心里有数的遗留问题同名进程多开时只关了第一个真实工具应该让用户选另外对无窗口的进程枚举窗口发 WM_CLOSE 这条路走不通直接走强制没有别的选择。把这些边界写在注释里以后维护自己也不会犯错。最后说句实在话能发 WM_CLOSE 就别上 Kill定位一定要准宁可先列出列表再动手任何批量操作都要有确认和日志。这个工具我现在还一直在用每次加新功能的时候都会回到这三个原则上来检查一遍。踩过几次坑之后就会发现真正可靠的关闭工具不是“把进程杀掉”而是懂得在恰当的时刻用恰当的方式退出。

相关新闻

MySQL地理坐标数据库:省市区三级行政区划数据包设计与应用

MySQL地理坐标数据库:省市区三级行政区划数据包设计与应用

简介:本资源是一份全国省、市、区三级行政区划的标准化地理坐标数据集,专为GIS应用开发、LBS位置服务、地图可视化及MySQL空间数据建模等场景设计,适用于中初级后端开发者、数据分析工程师与地理信息相关学习者。数据已结构化为可直接导入MyS…

2026/10/18 11:36:07 阅读更多 →
半监督图像分类实战:从FixMatch到PyTorch训练全流程

半监督图像分类实战:从FixMatch到PyTorch训练全流程

1. 为什么我盯上了半监督图像分类训练集标注成本,迟早会成为一个项目的天花板。做计算机视觉的朋友应该都有这种感觉:公开数据集刷精度挺顺手,一到自己的业务场景就开始头疼。业务数据的标注费用高、周期长、隐私问题多,尤其在医疗…

2026/10/18 11:36:07 阅读更多 →
PowerBuilder数据库开发实战:DataWindow与事务管理避坑指南

PowerBuilder数据库开发实战:DataWindow与事务管理避坑指南

简介:本资源是面向PowerBuilder(PB)初学者与中级开发者的数据库应用实战教程,聚焦数据窗口核心机制与企业级数据库开发全流程,帮助开发者快速掌握PB在Oracle、SQL Server等主流数据库环境下的高效开发技巧。压缩包共17…

2026/10/18 11:36:07 阅读更多 →

最新新闻

FP8量化加速Qwen图像编辑模型推理与显存优化实战

FP8量化加速Qwen图像编辑模型推理与显存优化实战

1. 项目概述:FP8量化、Qwen图像编辑与推理加速看到标题里那个“[特殊字符]”先别皱眉,它不是乱码,也不是标题复制错了,而是很多内部项目里常见的占位标记。实际落地时,这一格会被替换成模型版本号、分支代号或者某次量…

2026/10/18 12:42:33 阅读更多 →
集成、验证与面试题精讲:从模块拼装到工程化思维

集成、验证与面试题精讲:从模块拼装到工程化思维

这一讲我们聊的主题是集成、验证与面试题精讲。很多课程习惯把这几个话题拆成独立章节讲,但实际工作中它们根本不是割裂的:你的模块写得再漂亮,集成不到一起去就是废代码;功能自测觉得没问题,一联调就露馅,…

2026/10/18 12:42:33 阅读更多 →
FOCAS 4.2函数库解析:FANUC数控机床数据采集与连接实战

FOCAS 4.2函数库解析:FANUC数控机床数据采集与连接实战

简介:这是一份面向FANUC数控系统开发者的函数库说明文档,内容整理自FOCAS1/2 4.2版本的官方函数库,重点介绍FOCAS通信库中常用接口、数据结构与调用方法,兼顾数据读写、程序传输、报警状态获取等典型应用场景。FOCAS1/2 4.2支持Wi…

2026/10/18 12:42:33 阅读更多 →
Hermes引擎入门:字节码预编译、内存优化与调试的4个关键步骤

Hermes引擎入门:字节码预编译、内存优化与调试的4个关键步骤

自己翻来覆去折腾技术栈这些年,最怕的不是新东西学不会,而是花了大把时间学了一堆“看起来有用、实际上用不上”的边角料。Hermes 我用了整整三个月,从最初对着文档一头雾水,到后来能顺畅定位问题、调性能,回头再看&am…

2026/10/18 12:42:33 阅读更多 →
BERT与图卷积结合的多任务谣言检测源码解析

BERT与图卷积结合的多任务谣言检测源码解析

简介:这是一份面向深度学习与自然语言处理学习者的多任务谣言检测完整实现,源自2022届本科毕业论文的后续优化代码,适合毕业设计、期末大作业及图神经网络入门研究参考。方案以BERT为编码器提取词向量与句向量,接入BiGCN或Self-At…

2026/10/18 12:42:33 阅读更多 →
GESP五级筛法精讲:埃氏筛与线性筛的原理、选择与考场模板

GESP五级筛法精讲:埃氏筛与线性筛的原理、选择与考场模板

GESP C五级的知识点里,埃氏筛和线性筛是公认的“第一次让你真正感受到算法效率”的内容。很多同学刚学到这里都会有个共同的困惑:代码看起来差不多,名字又很像,到底学哪个?考试默写哪个?为什么明明暴力也能…

2026/10/18 12:41:33 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →