十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用C# WinForms开发定时自动清理过期文件的小工具

用C# WinForms开发定时自动清理过期文件的小工具 简介这是一份面向C#初学者与Windows桌面应用开发者的实用工具型资源解决日常文件管理中临时文件、日志等周期性清理难题。用户无需编程基础双击exe即可启动定时删除任务开发者可基于完整源码快速定制路径、条件与日志策略。压缩包共49个文件含8个核心C#源码.cs、6个可执行程序.exe、11个缓存及调试文件.cache/.pdb、3个配置文件.config/.ini/.settings等结构清晰便于理解WinForm界面逻辑、System.Threading.Timer定时机制与System.IO文件扫描删除全流程。资源包仅155KB轻量易部署。目前已有197人学习下载附带可直接运行的ClearLogs.exe、Visual Studio解决方案.sln及详细日志记录功能支持自定义天数阈值、安全删除确认与操作结果实时反馈兼顾实用性与教学参考价值。 我手里正好有几个项目的临时文件堆积成灾截图目录、日志目录、下载文件夹动辄几十个G手动清理隔几天就忘真等磁盘满了再动手来来回回要折腾半天。所以我就花了一个周末用C# WinForms做了一个小工具配置一个目录设置文件保留天数程序自动按周期扫描到期文件直接删。做好后顺手用自带的发布功能打成了exe双击就能跑源码也一起整理了出来。这篇文章就把整个开发过程、核心逻辑和一些排坑经验从头到尾讲一遍希望对同样被文件自动清理问题困扰的朋友有点参考价值。1. 需求拆解做定时删除工具之前先把边界想清楚1.1 这个工具到底解决什么问题很多场景下我们的文件夹里会持续堆积过期就没用的文件每日定时备份的数据库压缩包保留近7天就够了更早的全是垃圾程序运行日志Log文件按天滚动半年下来几百个文件占据大量磁盘空间临时下载目录安装包、压缩包下载完就没用了但没人去删视频监控或截图工具生成的抓拍图片保留近30天是最常见的合规要求测试环境里的build产物、缓存目录构建一次就产生一批磁盘吃紧时才想起手动清需求本身不难但自动删除文件这件事有几个隐含边界必须在一开始就定下来否则后面很容易返工删除依据是什么——按文件的最后写入时间、创建时间还是修改时间删除范围是什么——只删指定目录下的文件还是包括子目录是否要处理子目录本身的清理执行周期是什么——每隔多少天检查一次是严格按小时跑还是仅在程序启动时检查删除前要不要确认——是否有必要放到回收站而不是直接物理删除这几个问题看起来简单实际写代码时每一项都影响逻辑设计。我最终的方案是默认按最后写入时间LastWriteTime判断支持递归子目录扫描执行周期由用户配置默认不开启删除前确认因为是自动任务建议提示日志替代手动确认。这个思路在后面编码中帮我省了不少事。1.2 为什么不做成服务而是做成WinForms程序一开始我考虑过Windows服务方案毕竟定时任务听起来服务更合适。但仔细一想Windows服务有几个明显短板开发调试麻烦、配置不够直观、目标用户未必有权限注册服务。而WinForms程序加一个最小化到托盘的功能几乎可以达到同样的效果——开机启动、后台运行、托盘驻留用户双击exe就能看到配置界面完全不依赖命令行。所以我最终选择了C# WinForms。对于这类工具来说看得见的界面本身就是最好的说明书用户能直观看到删除哪些目录、保留几天、下次执行时间比任何文档都有效。1.3 和市面上的清理工具对比我为什么还要自研你可能会说Windows自带存储感知或者直接装个CCleaner不就行了。但实际用下来市面通用工具在按指定周期删除指定目录下的过期文件这个细分场景下表现并不理想工具方案优点缺点Windows存储感知系统内置、无需安装可控性差只能清理系统认定的临时文件不能自行指定业务目录CCleaner等清理工具功能全、界面友好偏重于系统垃圾清理自定义目录过期删除能力有限部分功能收费手写批处理计划任务轻量、零依赖配置和调试不直观备份/误删处理逻辑写起来麻烦自研WinForms工具完全可控、配置直观、可打包分发需要开发成本但一次开发长期受益我的核心诉求是指定目录、指定保留天数、定时执行、能自动跑自研工具是最贴合需求的。开发成本大约是半个工作日加一个晚上的调试写完之后不仅自己用发给同事对方双击就能配置好不需要装任何运行环境前提是目标机器有对应版本的.NET这个后面详细说。2. 技术选型复盘WinForms Timer 组合的正确打开方式2.1 为什么用WinForms而不是WPF或控制台应用控制台应用写定时任务确实更纯正但控制台程序对普通用户不够友好配置参数只能靠命令行或配置文件缺少可视化的目录选择和实时状态展示。WPF虽然界面更现代但它的学习成本和打包体积都比WinForms大对于这种单窗口小工具WinForms的轻量反而是最大优势。WinForms是.NET生态里最成熟、资料最多的UI框架网上随便搜都有大量案例。我的诉求很简单一个主窗口、几个Label和Button、一个NumericUpDown、一个Timer控件WinForms几个小时就能搞定WPF反而有点杀鸡用牛刀。2.2 Timer选型System.Windows.Forms.Timer为什么是首选.NET里有三种常用的Timer很多人一开始分不清我直接说结论和适用场景System.Windows.Forms.Timer依赖UI线程Tick事件在消息循环里触发。它的优点是不会跨线程操作UI缺点是精度一般约几十毫秒级别但对于多少天删一次这种分钟级任务精度完全够。这是WinForms里最省心的选择。System.Timers.Timer基于线程池触发不在UI线程操作UI控件时需要Invoke。适合后台计算场景不适合WinForms直接配合。System.Threading.Timer最底层的线程定时器也是ThreadPool线程执行回调。精度最高但用法最繁琐需要自己处理线程同步。在本项目里我只需要做到每天/每几天检查一次文件精度要求极低所以直接使用System.Windows.Forms.Timer。它有一个额外的好处当窗口最小化或隐藏到托盘后只要程序进程还在Timer就继续跑不用担心被GC回收。2.3 Framework版本选择.NET Framework 4.7.2 还是 .NET 6/8这是个很现实的问题。如果用的是Visual Studio 2022新建WinForms项目默认是.NET 6/8Windows Forms是.NET Core/.NET 5才跨平台支持不过Windows下很好用。但考虑到用户机器上不一定安装了对应运行库我做了点取舍面向.NET Framework 4.7.2Windows 10/11自带Win7 SP1及以上可以通过安装补丁支持。目标exe只依赖系统自带组件发布时体积小兼容性极好基本是双击就能用。面向.NET 6/8性能更好但需要目标机器安装对应桌面运行时或者发布时选择自包含模式体积会到几十MB甚至上百MB。对于这种工具型小软件我的选择是面向.NET Framework 4.7.2配合Release AnyCPU 不依赖额外NuGet包的发布方式生成一个不到100KB的exe随便拷到哪台Windows机器上都能跑。如果你的目标机器都是较新的Windows 11改用.NET 8也没问题打包时选一下目标运行时即可。3. 界面与交互设计用户只关心删哪个目录、保留几天3.1 主界面布局去掉多余的东西只留核心配置这个工具的使用者是普通用户轻度技术用户界面设计不需要花哨但信息层级要清楚。我的主窗口布局是这样安排的-------------------------------------------------------------- | 待清理目录: [文本框显示当前目录][浏览...] | | 文件保留天数: [NumericUpDown: 天] | | 检查周期: [NumericUpDown: 小时] (默认24小时) | | [ ] 包含子目录 [ ] 开机自动启动 | | [ ] 删除前放入回收站 | | ------------------------------------------------------ | | | 日志区域显示扫描时间、删除文件数、释放空间大小 | | | | 2025-xx-xx 14:00:00 扫描完成删除 12 个文件 | | | | 2025-xx-xx 14:00:00 释放空间 1.2 GB | | | ------------------------------------------------------ | | [保存设置] [立即执行一次] [启动定时任务] | --------------------------------------------------------------这里我刻意没有做配置向导或多标签页因为工具的使用链路非常短选目录、设天数、保存、启动。多一步操作都显得多余。日志区域是必须保留的因为自动删除很容易让用户产生到底删了没删了哪些的焦虑有一份日志至少能让人放心。3.2 目录选择逻辑FolderBrowserDialog还是自定义选择器WinForms里选择目录很多人第一个想到的是FolderBrowserDialog。这个控件虽然原生但体验确实一般——窗口旧、不能直接输入路径、层级浏览效率低。我最终用的是FolderBrowserDialog的增强版思路在真正调用系统弹窗前先允许用户在文本框里直接粘贴路径并且做一次路径有效性校验。这样技术用户可以直接粘贴普通用户可以用弹窗选择两个入口都保留。代码大概这样private void btnBrowse_Click(object sender, EventArgs e) { using (var fbd new FolderBrowserDialog()) { fbd.Description 请选择需要清理的目录; fbd.ShowNewFolderButton false; if (fbd.ShowDialog() DialogResult.OK) { txtDirectory.Text fbd.SelectedPath; } } } private bool ValidatePath(string path) { try { return Directory.Exists(path); } catch (Exception ex) { MessageBox.Show(路径校验失败 ex.Message, 提示, MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } }3.3 删除依据的默认选择LastWriteTime是最符合直觉的判断一个文件是否过期最直观的标准是它多久没被修改了。在.NET中FileInfo提供了三个时间属性CreationTime创建时间。问题在于文件复制、解压时部分工具会改写创建时间参考价值低。LastWriteTime最后一次写入时间。最稳定也最符合这个文件最近还在用吗的判断。LastAccessTime最后访问时间。但NTFS对文件的读取访问有时不会实时刷新而且频繁访问会导致判断不稳定不推荐用于删除逻辑。所以我默认使用LastWriteTime。在代码中就是一句话(DateTime.Now - fileInfo.LastWriteTime).TotalDays daysToKeep。注意NTFS文件系统在极端情况下比如电源故障可能出现时间戳异常这里不深究正常业务场景下LastWriteTime足够可靠。4. 核心定时逻辑与删除流程别让删除这一步出事故4.1 Timer的启动、停止与周期设置System.Windows.Forms.Timer的用法非常简单。考虑到用户可能设置每12小时检查一次或每3天检查一次我直接用TimeSpan.FromHours换算成毫秒赋值给Intervalprivate System.Windows.Forms.Timer timer; private void InitTimer() { timer new System.Windows.Forms.Timer(); timer.Tick Timer_Tick; timer.Interval (int)TimeSpan.FromHours( (double)nudIntervalHours.Value).TotalMilliseconds; timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { // 防止上一次扫描未完成时再次触发 if (isScanning) return; ExecuteCleanup(); }这里有一个很容易被忽略的点Timer的Interval按小时换算成毫秒后如果是12小时就是43200000毫秒完全在int范围内没问题但如果用户填了超过24天的周期数值就可能溢出intint最大约24.8天。为了避免这种隐藏bug我用long接住毫秒数再强制判断一下范围。通常这个工具的使用周期不会超过7天但作为开发者的严谨性还是要有的。4.2 删除流程的防重入与防误删设计自动删除工具最怕的就是逻辑写错把不该删的文件删了。我在删除流程里做了几重保护第一重扫描标记防重入。上面代码里的isScanning字段确保上次扫描还没跑完时定时器不会再触发一次。因为删除大量文件时IO操作耗时不可忽略如果没有这个开关Timer可能在文件删除过程中叠加上来导致异常。第二重扩展名白名单/黑名单。我的工具允许用户填写只删除这些扩展名逗号分隔留空则删除所有文件。这个设计在实测中非常实用——比如备份目录里既有.bak备份文件又有.txt说明文件用户可能只想删.bak不想动.txt。如果我没有这个选项用户只能被迫整目录全删风险就大了。第三重删除前放入回收站。这不是默认开启的但我在界面上加了删除前放入回收站开关。通过Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile这个API就可以方便地实现回收站删除// 引用 Microsoft.VisualBasic 程序集 using Microsoft.VisualBasic.FileIO; private void DeleteFileToRecycleBin(string filePath) { FileSystem.DeleteFile(filePath, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); }这样做的好处是即使用户误删也能从回收站捞回来心里踏实很多。代价是删除速度稍慢回收站写入也是IO但相比数据安全这点性能损失完全值得。第四重只删文件不删目录默认。在包含子目录的处理上我默认递归扫描子目录里的过期文件但不删除空目录。为什么因为很多软件比如日志框架会持续写入固定目录结构如果把目录本身删了程序运行时会报目录不存在需要重新创建反而引入风险。让空目录留着下次扫描继续遍历就行多不了多少IO开销。如果你确实想连空目录一起清再加一个选项也不难。4.3 扫描与删除的完整代码流程我整理了一个简化的核心逻辑便于理解整个流程public void ExecuteCleanup() { isScanning true; try { string targetDir txtDirectory.Text.Trim(); if (!Directory.Exists(targetDir)) { AppendLog(目录不存在跳过本次扫描 targetDir); return; } double daysToKeep (double)nudRetentionDays.Value; DateTime threshold DateTime.Now.AddDays(-daysToKeep); SearchOption searchOption chkIncludeSubdirectories.Checked ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; string[] files Directory.GetFiles(targetDir, *.*, searchOption); int deletedCount 0; long freedBytes 0; foreach (string filePath in files) { try { var fi new FileInfo(filePath); if (fi.LastWriteTime threshold) { if (!IsExtensionAllowed(fi.Extension)) continue; long size fi.Length; if (chkRecycleBin.Checked) DeleteFileToRecycleBin(filePath); else File.Delete(filePath); deletedCount; freedBytes size; } } catch (Exception ex) { // 单个文件失败不影响其他文件处理 AppendLog(删除失败 filePath 原因 ex.Message); } } AppendLog($扫描完成删除 {deletedCount} 个文件释放空间 {FormatSize(freedBytes)}); } finally { isScanning false; } }这段代码里有两个细节值得展开第一个是IsExtensionAllowed方法。如果用户配置了只删除指定扩展名我会解析出一个HashSet检查当前文件的扩展名是否在集合里如果用户没有配置扩展名过滤则返回true。第二个是FormatSize方法。文件释放空间动辄几百MB如果用字节数直接显示用户很难快速感知所以我实现了一个简单的大小格式化函数自动选择B/KB/MB/GB单位private string FormatSize(long bytes) { string[] units { B, KB, MB, GB, TB }; double size bytes; int unitIndex 0; while (size 1024 unitIndex units.Length - 1) { size / 1024; unitIndex; } return ${size:0.##} {units[unitIndex]}; }5. 配置持久化与开机自启让工具真正跑起来不用管5.1 配置文件选型JSON比App.config更清晰WinForms本身支持Properties.Settings或者App.config但这类配置在用户修改后保存有点别扭。我更倾向于使用一个独立的JSON配置文件放在exe同目录下名字就叫config.json。用户改动后保存时写回文件下次启动自动加载。采用JSON的好处很直接结构清晰可读性强用户可以直接用记事本改使用System.Text.Json或Newtonsoft.Json序列化代码简洁和exe放在一起备份、迁移、多机部署都很方便我的配置类长这样public class AppConfig { public string TargetDirectory { get; set; } public int RetentionDays { get; set; } 7; public int IntervalHours { get; set; } 24; public bool IncludeSubdirectories { get; set; } true; public bool StartWithWindows { get; set; } public bool UseRecycleBin { get; set; } public string ExtensionFilter { get; set; } ; }加载和保存的代码就两段加载时用File.ReadAllTextJsonConvert.DeserializeObject保存时用JsonConvert.SerializeObjectFile.WriteAllText。5.2 开机自启的两种实现方式开机自启是这个工具自动运行的重要一环。Windows下常用的做法有两种方式一写注册表Run键。简单可靠适合当前用户。代码using Microsoft.Win32; private void SetAutoStart(bool enable) { const string runKey Software\Microsoft\Windows\CurrentVersion\Run; using (RegistryKey key Registry.CurrentUser.OpenSubKey(runKey, true)) { if (enable) { string exePath $\{Application.ExecutablePath}\; key.SetValue(AutoFileCleaner, exePath); } else { key.DeleteValue(AutoFileCleaner, false); } } }方式二计划任务。通过schtasks命令或任务计划程序API能做到更精细的登录时启动或定时启动。但计划任务配合WinForms有一点麻烦——界面程序放在开机启动的计划任务里运行时可能不带界面用户反而不知道程序在跑。所以我最终选择了注册表Run键方案让程序在用户登录后自动运行托盘图标常驻用户能看到。提示注册表Run键启动的程序默认工作目录可能不是exe所在目录。如果程序里需要读取同目录下的配置文件建议用AppDomain.CurrentDomain.BaseDirectory拼路径而不是直接用相对路径。5.3 托盘显示关闭窗口不等于退出程序做这类常驻工具最常用的交互模式是关闭窗口后最小化到托盘不退出进程。WinForms原生没有托盘控件需要添加NotifyIcon然后处理FormClosing事件拦截窗口关闭动作private void FormMain_FormClosing(object sender, FormClosingEventArgs e) { // 如果是用户点右上角X则最小化到托盘而不是退出 if (!_isExiting) { e.Cancel true; this.Hide(); notifyIcon.ShowBalloonTip(3000, 自动删除工具, 程序已最小化到托盘后台定时任务继续运行, ToolTipIcon.Info); } }同时在托盘图标的右键菜单里提供显示主界面立即清理退出三个选项。这个设计很关键因为很多用户第一次使用时会觉得我关掉窗口了程序应该没跑了吧——有了托盘通知和图标至少能明确告诉用户程序还在继续工作。5.4 保存配置的时机我的处理方式是用户点击保存设置按钮时把当前界面的值全部写入config.json并立即使定时器的Interval更新。这样避免了改了设置但没重启不生效的困惑。有一个小坑是如果用户改了目录路径但没点保存就直接点立即执行程序会使用界面上当前显示的值而不是配置文件里的旧值。为了让直觉更一致我在立即执行按钮的Click事件里也顺手调用了保存方法保证执行前配置已落盘。6. 打包发布为exe从开发机到用户电脑的距离6.1 发布方式对比框架依赖还是自包含我可以选择两种发布方式框架依赖Framework-Dependentexe体积小几百KB但用户机器上需要安装.NET Framework 4.7.2或更高版本。对于Windows 10/114.7.2本身已内置所以大多数情况没问题Win7 SP1需要额外安装补丁。自包含Self-Contained把.NET运行库和exe一起打包体积大约60~150MB但好处是目标机器什么都不用装。考虑到双击即可使用的定位我选择框架依赖。为什么因为Windows 10和11覆盖了绝大多数目标用户而系统自带的.NET Framework版本往往高于4.7.2Win10 1809以上基本是4.7.2Win11是4.8。如果我已经基于4.7.2开发那在支持.NET Framework 4.x的Windows版本上都能直接运行。如果担心Win7用户可以再往前退一步用.NET Framework 4.0或4.5编译但这会牺牲一部分语言新特性。我自己权衡下来4.7.2是一个合适的折中。6.2 Visual Studio 2022发布步骤在VS 2022里的发布流程非常简单右键项目 → 选择发布选择发布目标文件夹点击配置目标运行时选择win-x64或win-x86取决于目标机器部署模式选择框架依赖文件系统选项默认即可点击发布生成一个包含exe和依赖文件可能有config、dll的文件夹如果你希望生成单个exe文件不附带一堆dll可以勾选启用ReadyToRun旁边的相关选项或者在项目文件里加PropertyGroup PublishSingleFiletrue/PublishSingleFile SelfContainedfalse/SelfContained /PropertyGroup但要注意.NET Framework 4.7.2的WinForms项目对单文件发布支持不太好如果你用的是.NET 6/8单文件发布会顺畅很多。我的实际做法是不追求单文件发布出的文件夹拷给别人时整个文件夹一起发。虽然多几个文件但稳定优先。6.3 用绿色小巧的exe需要额外注意的杀软误报问题自己编译的exe在没有代码签名的情况下偶尔会被部分杀毒软件误报或拦截。这几乎是所有自研工具的痛点。我的经验是尽量用Visual Studio默认的编译设置不要加一些加密壳或混淆器加了更容易触发误报发布后自己先用Windows Defender扫描一下确认没有报毒代码本身不做任何危险系统调用不操作其他进程、不注入、不监听键盘误报概率会大大降低如果发给同事被SmartScreen拦截让对方点仍要运行即可这是正常弹窗6.4 测试环境与真实环境的差异开发机上跑得好好的不代表换台电脑没问题。我遇到过最典型的问题有两个第一个是缺少.NET运行时。我在自己机器上开发运行库自然是齐全的但有一台老旧的Win7电脑连.NET Framework 4.7.2都没有程序双击后报错不是有效的Win32应用程序或者直接不响应。这时候必须改用.NET Framework 4.0或下载安装运行库才能解决。第二个是权限问题。程序需要写入config.json如果放在Program Files目录下普通用户没有写权限保存配置时会报Access Denied。所以我的程序有一个隐藏逻辑启动时检测自己是否位于受保护目录如果是则把配置文件自动转向用户目录。更好的做法是强制用户把exe放在非系统盘运行比如桌面或D盘工具目录。7. 排坑实录从开发到交付遇到的5个实际问题7.1 Timer的Interval被系统休眠打断怎么补救Windows系统休眠或睡眠后System.Windows.Forms.Timer的Tick事件不会在睡眠期间触发唤醒后才会补上。这意味着原本设定每天10点检查一次如果电脑在10点时处于睡眠状态任务会延迟到唤醒后才执行。这个问题说大不大说小不小。对我这种清理临时文件的场景晚几个小时完全无所谓。但如果你做的是凌晨准时执行删除之类的任务就需要在启动时加一个补偿检查逻辑比较当前时间和上次执行时间如果间隔超过一个周期就立即执行一次。我在程序启动时做了一次这样的补偿保证长期睡眠后回到电脑工具会第一时间补跑清理任务。7.2 文件被占用删不掉怎么办删除文件时偶尔会遇到文件正在被另一个进程使用无法删除的异常。在抓拍目录或日志目录中这种情况尤其常见——日志文件正被某个服务持续写入。我用了一个简单而有效的策略单文件失败不影响整体流程。每个文件的删除包在独立的try-catch里失败就记录日志继续下一个文件。这样即使个别文件被占用其余脏文件还是能正常清除。下一个周期再扫描时占用的文件如果被释放了依然会被删除。如果遇到权限不足UnauthorizedAccessException可能是文件有只读属性或者当前用户无访问权限。我处理了只读删除前先把属性改为Normal。至于完全无权限的受保护文件那就让日志如实记录让用户手动处理。7.3 防止误删正在使用或需要保留的目录做工具的人最怕代码里逻辑多绕一圈结果把不该删的删了。我在扫描前加了一个目录白名单校验如果用户选择的目录是系统关键目录比如C:\Windows、Program Files程序直接拒绝并红字提示。这个校验在工具分发给其他人时尤其重要——我发给同事时不可能一个个解释千万别选C盘最好的保护就是程序自己拦住。private bool IsDangerousDirectory(string path) { string systemDir Environment.GetFolderPath(Environment.SpecialFolder.Windows); string programFiles Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string programFilesX86 Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); string fullPath Path.GetFullPath(path).TrimEnd(\\); string[] dangerousPaths { systemDir.TrimEnd(\\), programFiles.TrimEnd(\\), programFilesX86.TrimEnd(\\) }; foreach (string dangerous in dangerousPaths) { if (string.IsNullOrEmpty(dangerous)) continue; if (fullPath.Equals(dangerous, StringComparison.OrdinalIgnoreCase) || fullPath.StartsWith(dangerous \\, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; }这里没有处理C盘根目录这种极端情况因为有些用户确实有把临时目录放在C盘根目录的习惯。但按我自己的使用经验把系统关键目录、Program Files目录拦截掉已经能挡住99%的误操作。7.4 config.json被误删除后的恢复逻辑程序启动时如果发现config.json不存在不应该直接抛异常而是用默认配置创建一份新的。我的默认值故意设置得比较保守默认不开启自动执行等用户在界面上点了启动定时任务后才开始扫描。这样即使配置丢失也不会发生程序意外启动并自动删除文件的危险情况。7.5 日志文件的滚动与清理如果程序每天都记录日志时间久了日志本身也会堆积。我给日志模块做了简单的滚动只保留最近30天的log文件。这既是工具能力的自证也避免了清理工具把自己撑死的尴尬。日志内容尽量精简每行记录时间—执行结果—删除数量—释放空间不记录具体文件名如果需要审计可以加一个开关。8. 实测效果与后续扩展思路8.1 我实际跑了一周效果怎么样我本机配置了两个任务一个指向E:\Backup保留7天一个指向D:\Downloads保留30天。检查周期都是24小时。跑了一周下来日志显示每天稳定扫描一次累计删除过期文件200多个释放空间约4GB。磁盘占用曲线明显平缓了再也不用眼巴巴盯着一堆压缩包手动清理。上传给朋友后他反馈在Windows 11上直接双击exe就能用界面配置直观没有遇到缺少运行库的问题。唯一需要提醒的是第一次加载目录选择弹窗可能偏慢这是系统FolderBrowserDialog的固有体验不是程序卡死等一两秒就好。8.2 我还想加什么功能程序做到这里已经能稳定解决我的问题但后续有空的话我计划在几个方向上做进一步优化多目录配置目前一个程序只管理一个目录改成任务列表可以一次配置多个目录各自独立设置保留天数文件匹配模式高级化目前只支持按扩展名白名单后续可以增加通配符比如temp_*.tmp、文件大小过滤只删超过10MB的文件邮件或企业微信通知每次执行完清理任务后发送一条通知。对于跑在服务器上的场景这个需求不少软链接/符号链接目录的处理如果指定目录里包含软链接扫描时是否跟进到目标目录需要仔细定义避免无意中删除链接目标里的文件8.3 源码怎么组织给想要扩展的朋友一点指引如果你打算拿这套源码做二次开发我的建议是把UI逻辑、文件清理逻辑、配置逻辑三个层分得清楚一点不要全塞在Form1.cs里。我的项目结构大致是AutoFileCleaner/ ├── Program.cs // 程序入口处理单实例和异常捕获 ├── FormMain.cs // 主窗体UI逻辑 ├── FileCleaner.cs // 核心删除与扫描逻辑 ├── AppConfig.cs // 配置模型与加载/保存 ├── LogHelper.cs // 日志工具 └── config.json // 运行后生成的配置文件这样一旦你想增加新功能比如多目录任务只需要在AppConfig里加一个列表在FormMain里加一个ListView在FileCleaner里改成循环处理每个目录即可各层互不干扰。考虑到原文章的实用性补充三段可用的核心代码片段扫描过期文件的主逻辑、配置持久化、定时器启动代码。示例代码块1核心扫描与删除文件清理器public class FileCleaner { private readonly string _targetDirectory; private readonly double _retentionDays; private readonly bool _includeSubdirectories; private readonly bool _useRecycleBin; private readonly HashSetstring _extensionFilter; public FileCleaner(string targetDirectory, double retentionDays, bool includeSubdirectories, bool useRecycleBin, string extensionFilter) { _targetDirectory targetDirectory; _retentionDays retentionDays; _includeSubdirectories includeSubdirectories; _useRecycleBin useRecycleBin; _extensionFilter ParseExtensionFilter(extensionFilter); } public CleanResult Execute() { var result new CleanResult(); DateTime threshold DateTime.Now.AddDays(-_retentionDays); SearchOption option _includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; string[] files Directory.GetFiles(_targetDirectory, *.*, option); foreach (string file in files) { try { var fi new FileInfo(file); if (fi.LastWriteTime threshold) continue; string ext fi.Extension.ToLowerInvariant(); if (_extensionFilter.Count 0 !_extensionFilter.Contains(ext)) continue; long size fi.Length; if (_useRecycleBin) { FileSystem.DeleteFile(file, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); } else { if (fi.IsReadOnly) fi.Attributes FileAttributes.Normal; File.Delete(file); } result.DeletedCount; result.FreedBytes size; } catch (Exception ex) { result.Errors.Add(${file} - {ex.Message}); } } return result; } private static HashSetstring ParseExtensionFilter(string filter) { var set new HashSetstring(StringComparer.OrdinalIgnoreCase); if (!string.IsNullOrWhiteSpace(filter)) { foreach (var item in filter.Split(,, ;, )) { string ext item.Trim(); if (string.IsNullOrEmpty(ext)) continue; if (!ext.StartsWith(.)) ext . ext; set.Add(ext.ToLowerInvariant()); } } return set; } } public class CleanResult { public int DeletedCount { get; set; } public long FreedBytes { get; set; } public Liststring Errors { get; } new Liststring(); }示例代码块2配置读写public static class ConfigManager { private static readonly string ConfigPath Path.Combine( AppDomain.CurrentDomain.BaseDirectory, config.json); public static AppConfig Load() { try { if (File.Exists(ConfigPath)) { string json File.ReadAllText(ConfigPath); return JsonConvert.DeserializeObjectAppConfig(json) ?? new AppConfig(); } } catch (Exception ex) { MessageBox.Show(加载配置文件失败已使用默认配置。\n ex.Message); } return new AppConfig(); } public static void Save(AppConfig config) { try { string json JsonConvert.SerializeObject(config, Formatting.Indented); File.WriteAllText(ConfigPath, json); } catch (Exception ex) { MessageBox.Show(保存配置文件失败。\n ex.Message); } } }示例代码块3定时器的完整生命周期public partial class FormMain : Form { private System.Windows.Forms.Timer _timer; private bool _isScanning false; private bool _isExiting false; private AppConfig _config; private void StartTimer() { if (_timer ! null) StopTimer(); _timer new System.Windows.Forms.Timer(); _timer.Interval (int)TimeSpan.FromHours(_config.IntervalHours).TotalMilliseconds; _timer.Tick OnTimerTick; _timer.Start(); AppendLog($定时任务已启动检查周期{_config.IntervalHours} 小时); } private void StopTimer() { if (_timer ! null) { _timer.Stop(); _timer.Tick - OnTimerTick; _timer.Dispose(); _timer null; } } private void OnTimerTick(object sender, EventArgs e) { if (_isScanning) return; ExecuteCleanup(); } private void ExecuteCleanup() { if (_isScanning) return; _isScanning true; try { string dir _config.TargetDirectory; if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) { AppendLog(目录无效跳过本次扫描); return; } var cleaner new FileCleaner( dir, _config.RetentionDays, _config.IncludeSubdirectories, _config.UseRecycleBin, _config.ExtensionFilter); AppendLog($开始扫描{dir}保留 {_config.RetentionDays} 天); CleanResult result cleaner.Execute(); AppendLog($扫描完成删除 {result.DeletedCount} 个文件释放 {FormatSize(result.FreedBytes)}); foreach (string error in result.Errors) { AppendLog(错误 error); } } finally { _isScanning false; } } }最后聊一个我实际使用中总结的小技巧不要把删除周期和检查周期混为一谈。检查周期可以比保留周期短很多比如你设置文件保留7天但可以每小时检查一次目录。这样做的好处是一旦有新的过期文件产生最多1小时内就能被清掉不用等到第7天的那个时间点才触发。这种频繁检查延迟删除的组合在文件产生不规律的目录里特别实用。如果你也想解决自己电脑或服务器上的文件自动清理问题可以直接按这套思路自己写一个二三十行核心代码加一个简单界面就能完成。当然我的完整源码也打包好了exe可以直接双击使用适合想省事的朋友。有需要的话可以从我的项目页面下载自己改配置跑起来看看效果有问题欢迎随时交流。本文还有配套的精品资源点击获取
返回列表