尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用Godot引擎开发C盘清理工具:扫描、权限与安全删除实战

用Godot引擎开发C盘清理工具:扫描、权限与安全删除实战 很多开发者的 C 盘都是被“看不见的缓存”慢慢撑爆的。Windows 自带的磁盘清理工具能扫出一点系统更新缓存但对浏览器缓存、临时文件、开发工具生成的中间产物往往无能为力第三方全家桶的清理功能又太重广告和推荐让你防不胜防。这时如果把“清理 C 盘”这件事做成一个轻量的桌面工具你会选什么技术栈有人会选 C# WPF有人会选 Electron也有人会用 Python Tkinter。而我的选择可能让很多人意外用游戏引擎 Godot。这篇文章要拆解的是一个名为Godot-CleanScope的 Windows C 盘清理工具。它用 Godot 4 开发把“扫描 C 盘可清理项、按大小排序展示、勾选后再删除”这一整套流程做成了可视化 GUI。它不是什么商业级产品但它非常能说明一个趋势Godot 早已不只适合做游戏同样适合做面向特定场景的桌面效率工具。读这篇文章你会得到四样东西一是清楚 Godot 做非游戏桌面应用的边界在哪里二是学会用 GDScript 实现目录扫描和文件大小统计三是掌握“扫描 预览 勾选 确认删除”的安全清理流程四是搞懂 Windows 下清理工具的权限、线程和异常处理。1. 为什么值得关心“用 Godot 写清理工具”先讲一个真实场景。系统盘飘红以后很多人第一反应是下载各种“电脑管家”结果装完发现内存占用反而多了几百 MB后台服务比要清理的垃圾还积极。另一部分人选择手动清理于是打开资源管理器从用户目录一层一层往下点找到 AppData、Local、Temp全选删除然后看着“正在删除 30000 个文件”的进度条卡了十分钟。这个痛点的本质是清理 C 盘的动作本身很简单难的是人机交互和信息展示。你不仅要告诉用户哪些文件可以删还要告诉他每个文件夹有多大、占了多少比例、删除后能不能恢复。文件小了不值得删文件大了又怕误删。如果你只提供一个命令行脚本用户根本没耐心去看输出如果你用 WPF 或 WinForms 写UI 布局和实时刷新逻辑并不轻松。Godot 的优势恰好在这里。第一Godot 的编辑器本身就是一套可拖拽的 UI 设计工具。Control 节点体系适合做列表、按钮、进度条、勾选框这类工具界面比手写 XAML 更直观。第二GDScript 语法简单写文件遍历、字符串拼接、信号通知比 C 和 Java 顺手得多。第三Godot 打包后的程序体积不大不依赖运行时框架拿到一台普通 Windows 电脑上解压就能跑。当然它不是没有代价。最大的代价在于Godot 没有内置的 Windows 系统对话框也没有管理员权限获取的原生接口。部分系统集成能力需要你自己通过 OS.execute 调用 PowerShell 或第三方命令行工具来补足。因此用 Godot 写工具类应用本质是“Godot 做界面和交互Windows 系统工具做脏活累活”。2. 先搞清楚Godot-CleanScope 的清理范围和安全边界很多初学者一听“C 盘清理工具”就激动恨不得把所有目录都扫一遍把没见过的文件都删干净。这是非常危险的想法。一个负责任的清理工具必须在设计的第一天就定义清楚一个问题哪些东西属于可清理范围哪些东西绝对不碰。Godot-CleanScope 的清理策略可以概括为三句话只清理用户目录下的临时文件和缓存文件。不在没有预览的情况下执行删除。默认不对系统目录和 Windows 更新目录做任何操作。下面这组目录是 Windows 上常见的“可安全清理项”。目录位置内容说明清理风险C:/Users/用户名/AppData/Local/Temp当前用户的临时文件低绝大多数文件可删C:/Windows/Temp系统临时文件中部分文件被系统占用C:/Users/用户名/AppData/Local/Microsoft/Windows/INetCacheIE 和系统组件缓存低C:/Users/用户名/AppData/Local/Google/Chrome/User Data/Default/CacheChrome 浏览器缓存低C:/Users/用户名/AppData/Local/Microsoft/Edge/User Data/Default/CacheEdge 浏览器缓存低C:/Users/用户名/Downloads下载目录高不作为默认清理项这里要特别强调两点。第一像 Downloads 这类目录绝对不应该出现在清理工具的默认清单里因为用户可能用几天时间下载了重要安装包一旦误删就找不回来了。第二Windows 的 Temp 目录里有很多文件虽然扩展名是 .tmp但可能正被某个正在运行的应用程序占用。删除这些文件时操作系统会返回“文件正在使用”程序要做的是记录到日志并跳过而不是反复重试或直接报错崩溃。CleanScope 这个名字来自 “Clean Scope”意思是“清理作用域”。它想表达的产品理念是我只清理我明确确认过的内容不搞系统级扫雷。3. Godot 桌面工具项目的环境准备如果你还没有安装 Godot第一步是去 Godot 官网下载 Windows 版本的编辑器。这里建议使用 Godot 4.x 的最新稳定版本。Godot 4 的底层渲染、文件系统和线程 API 都比 3.x 完善教程中涉及的接口需要 4.x 才能运行。下载后将压缩包解压到任意目录双击Godot_v4.x-stable_win64.exe即可启动。启动后点击项目管理器右侧的“新建”按钮创建项目。项目名称建议命名为CleanScope渲染器选择GL Compatibility。这个选项适合做 2D 界面工具因为它不依赖 Vulkan在集成显卡和远程桌面环境下兼容性更好。创建完成后项目根目录会生成一个project.godot文件。我们需要手动调整一部分配置。用文本编辑器打开它把内容改成下面这样。; 文件路径project.godot config_version5 [application] config/nameCleanScope run/main_sceneres://ui/main.tscn config/featuresPackedStringArray(4.2, GL Compatibility) [display] window/size/viewport_width960 window/size/viewport_height640 window/stretch/modedisabled window/dpi/allow_hidpitrue [rendering] renderer/rendering_methodgl_compatibility renderer/rendering_method.mobilegl_compatibility这里面最值得关注的是window/stretch/modedisabled。在游戏项目中拉伸模式通常设为canvas_items让画面适应不同屏幕分辨率。但在工具类应用中我们不希望 UI 元素因为系统分辨率变化而被拉伸变形。关闭拉伸模式让窗口按设计像素显示再配合allow_hidpitrue在高分屏上才能保持清晰。配置完成以后在项目根目录创建ui、scripts、tools三个文件夹。ui放场景文件scripts放 GDScript 脚本tools放后续可能用到的辅助命令文件。4. 核心模块设计扫描线程、UI 信号与删除策略在开始写代码前先把工具的内部结构拆清楚。Godot-CleanScope 计划由四个模块组成范围管理模块负责生成待扫描目录列表过滤不存在的路径。扫描模块在工作线程中递归遍历目录统计每个文件的大小。界面模块使用 ItemList 控件展示文件清单提供勾选、全选、排序功能。清理模块接收用户勾选的文件路径执行删除并记录日志。这四个模块之间的数据流非常简单扫描模块把结果发给界面模块界面模块把用户指令发给清理模块。难点在于文件扫描如果放在 UI 主线程执行程序会卡死。用户在扫描过程中不能点击按钮也不能拖动窗口。Godot 的解决方案是Thread类。我们可以把扫描函数放到独立线程里执行一旦扫描完成或有新文件被发现就通过信号通知 UI 线程刷新。Godot 允许信号跨线程发出但直接在线程里发射信号并连接 UI 的槽函数仍然有风险稳妥的做法是使用call_deferred在主线程里触发信号避免 Godot 报“Cant change state while flushing queries”之类的错误。# 文件路径scripts/scanner.gd class_name CleanScanner extends RefCounted signal file_found(path: String, size: int) signal scan_finished(total_size: int, file_count: int) var _total_size: int 0 var _file_count: int 0 var _current_path: String func scan_directories(dirs: Array[String]) - void: _total_size 0 _file_count 0 for dir_path in dirs: if DirAccess.dir_exists_absolute(dir_path): _scan_folder(dir_path) scan_finished.emit(_total_size, _file_count) func _scan_folder(dir_path: String, depth: int 0) - void: # 递归深度限制防止因目录嵌套过深导致栈溢出 if depth 12: return if not DirAccess.dir_exists_absolute(dir_path): return # 逐文件统计大小 var files : DirAccess.get_files_at(dir_path) for file_name in files: var full_path : dir_path.path_join(file_name) var size : _safe_file_size(full_path) _total_size size _file_count 1 _notify_file(full_path, size) # 递归子目录 var sub_dirs : DirAccess.get_directories_at(dir_path) for sub_name in sub_dirs: # 显式跳过常见的系统链接目录防止重复扫描 if sub_name in [AppData, Application Data, Temporary Internet Files]: continue _scan_folder(dir_path.path_join(sub_name), depth 1) func _notify_file(full_path: String, size: int) - void: # call_deferred 保证发射过程回到主线程执行 call_deferred(_emit_file_signal, full_path, size) func _emit_file_signal(full_path: String, size: int) - void: file_found.emit(full_path, size) func _safe_file_size(path: String) - int: var file : FileAccess.open(path, FileAccess.READ) if file null: return 0 var size: int file.get_length() file.close() return size这个脚本中有一个细节对新手很友好DirAccess.get_files_at()和DirAccess.get_directories_at()是 Godot 4 提供的开箱即用方法。老教程里常见的模式是list_dir_begin()配合get_next()循环那是 Godot 3 时代的写法代码更啰嗦。理解这份扫描代码的关键不是递归本身而是要看到它的两个安全设计深度限制、目录跳过。递归深度限制为什么需要绝大多数正常目录嵌套不会超过十二层就算真的遇到深层目录也说明里面有极端异常结构。深度限制能让程序在异常环境下不至于把线程栈消耗干净。目录跳过为什么需要AppData是一个隐藏目录但在用户目录下我们可控的清理范围其实只包括AppData/Local/Temp如果从AppData这个层级开始递归会把Local、Roaming、LocalLow全扫一遍不仅慢而且会把不该展示给用户的软件配置和身份信息暴露出来。所以正确的做法是在扫描入口只把精确的临时目录路径传进来不要从用户主目录这个根节点开始扫。5. 用 Godot 搭建主界面和交互逻辑接下来处理界面。推荐先手动创建ui/main.tscn场景再通过代码挂载节点。为了结构清晰界面由至少这些节点组成。Control场景根节点。VBoxContainer垂直布局容器。ToolbarHBoxContainer顶部工具栏放置“开始扫描”和“退出清理”按钮。ItemList展示扫描文件结果。StatusBarHBoxContainer展示扫描状态、总大小和清理按钮。Label用于显示正在扫描的目录。GDScript 的界面信号绑定非常简单。场景根节点挂在main.gd上然后通过%节点名语法获取子节点。# 文件路径ui/main.gd extends Control onready var file_list: ItemList %FileList onready var status_label: Label %StatusLabel onready var total_label: Label %TotalLabel onready var scan_button: Button %ScanButton onready var clean_button: Button %CleanButton var _scanner: CleanScanner var _scan_thread: Thread var _is_scanning: bool false # 保存文件路径到 ItemList item 的元数据中 const META_PATH : full_path const META_SIZE : file_size func _ready() - void: clean_button.disabled true total_label.text 待扫描... _scanner CleanScanner.new() _scanner.file_found.connect(_on_file_found) _scanner.scan_finished.connect(_on_scan_finished) func _start_scan() - void: if _is_scanning: return _is_scanning true clean_button.disabled true scan_button.text 扫描中... file_list.clear() total_label.text 正在获取临时目录... # 构造扫描目录 var user : OS.get_environment(USERPROFILE) var dirs : PackedStringArray([ OS.get_environment(TEMP), user \\AppData\\Local\\Temp, user \\AppData\\Local\\Microsoft\\Windows\\INetCache, user \\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cache, user \\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Cache, ]) # 将实际存在的目录过滤出来 var valid_dirs: Array[String] [] for dir_path in dirs: if DirAccess.dir_exists_absolute(dir_path): valid_dirs.append(dir_path) else: print([skip] 目录不存在: , dir_path) status_label.text 目录检查完成开始扫描... _scan_thread Thread.new() _scan_thread.start(_scanner.scan_directories.bind(valid_dirs)) func _on_file_found(path: String, size: int) - void: # 为了避免大量文件瞬间涌入拖垮 UI只展示大于 1MB 的文件 # 小文件在后台统计数量用总大小表达即可 if size 1024 * 1024: var idx : file_list.add_item(%s (%s) % [path, format_size(size)]) file_list.set_item_metadata(idx, META_PATH, path) file_list.set_item_metadata(idx, META_SIZE, size) func _on_scan_finished(total_size: int, file_count: int) - void: _is_scanning false scan_button.text 重新扫描 clean_button.disabled false status_label.text 扫描完成 total_label.text 文件总数: %d, 可清理空间: %s % [file_count, format_size(total_size)] func _on_clean_button_pressed() - void: if _is_scanning: return # 界面只是展示文件最终删除动作统一由 tools/cleaner.gd 执行 # 这里通过弹窗让用户二次确认 var dialog : ConfirmationDialog.new() dialog.dialog_text 确认删除列表中勾选的文件该操作不可恢复请先确认没有被占用的程序。 dialog.ok_button_text 删除 dialog.cancel_button_text 取消 add_child(dialog) dialog.confirmed.connect(_do_clean_selected) dialog.popup_centered() func _do_clean_selected() - void: var selected : file_list.get_selected_items() if selected.size() 0: status_label.text 请先勾选要删除的文件 return var success_count : 0 var fail_count : 0 for index in selected: var path: String file_list.get_item_metadata(index) if _try_delete(path): success_count 1 file_list.remove_item(index) else: fail_count 1 status_label.text 删除完成成功 %d 个失败 %d 个 % [success_count, fail_count]对应的场景文件不必从零手写。在 Godot 编辑器里添加控件会更简便。不过作为文档这里给出一个最小化的main.tscn节点树样例方便读者理解各控件之间的父子关系。# 文件路径ui/main.tscn节点树结构示例 Control (main.gd) ├── VBoxContainer │ ├── Toolbar (HBoxContainer) │ │ ├── ScanButton (%ScanButton) │ │ ├── CleanButton (%CleanButton) │ │ └── StatusLabel (%StatusLabel) │ ├── FileList (ItemList, %FileList) │ └── BottomBar (HBoxContainer) │ └── TotalLabel (%TotalLabel)在 Godot 编辑器里要给一个节点设置唯一名称只需在检查器面板里勾选Unique Name In Scene这样代码里就能用%FileList访问到它。这个特性非常适合工具类项目可以省掉一长串通过字符串路径查找节点的代码。在这个交互流程里我特别不想让用户“一键清理”。一键清理看起来很爽但它有三个致命问题无法确认错误文件、无法理解清理逻辑、无法逐项判断删除风险。CleanScope 的数据项在界面展示时会附带文件路径和大小。用户要先在列表里勾选再点击删除最后会弹出一个不可恢复的警告弹窗。这个路径虽然多了一步但它能把“工具自动删除”这个噩梦变成“我在工具辅助下手动删除”。6. 真正的文件删除与回收站切换删除文件是整个项目里最容易出错的地方。很多第一次用 Godot 做桌面工具的人第一反应是调DirAccess.remove_absolute(path)。这个方法确实能删文件而且一旦执行成功文件会从磁盘上彻底消失。它的最大问题是没有统一错误处理机制。如果文件被占用、没有权限、路径过长Godot 会向控制台输出错误但不一定会返回让人满意的失败信息。所以在 CleanScope 里我建议把删除动作封装成独立脚本并为两种删除模式提供支持移动到回收站和永久删除。# 文件路径tools/cleaner.gd class_name Cleaner extends RefCounted enum DeleteMode { RECYCLE, PERMANENT } static func delete_file(path: String, mode: DeleteMode) - bool: if not FileAccess.file_exists(path): return false if mode DeleteMode.RECYCLE: # 利用 PowerShell 调用 VisualBasic 把文件移动到回收站 var safe_path : path.replace(, ) var ps_cmd : Add-Type -AssemblyName Microsoft.VisualBasic; \ [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile(%s, OnlyErrorDialogs, SendToRecycleBin) \ % safe_path var output : [] var exit_code : OS.execute(powershell.exe, [-NoProfile, -Command, ps_cmd], output, true) return exit_code 0 # 永久删除先尝试 Godot 原生方法 var err : DirAccess.remove_absolute(path) if err OK: return true # 失败时再用 cmd 的 del 命令尝试并重新获取错误码 var cmd_out : [] var code : OS.execute(cmd.exe, [/c, del, /f, /q, path], cmd_out, true) return code 0为什么要做两种模式在实际清理场景里永久删除往往才能腾出空间。因为移动到回收站的文件依然占据磁盘空间如果用户没有“删除后清空回收站”的习惯C 盘空间不会明显回升。但永久删除又非常危险。所以我给出的建议是默认用永久删除模式但把清理范围严格限制在临时文件和缓存目录内对用户手工下载的非缓存文件提供回收站模式。使用这个脚本的时候界面层的_do_clean_selected里的_try_delete直接调用Cleaner.delete_file(path, Cleaner.DeleteMode.PERMANENT)即可。另外要注意OS.execute的返回值和运行方式。最后一个布尔参数为true时Godot 会阻塞当前线程等待命令执行完成并返回退出码。如果你不想让界面卡住可以把参数改为false但那样拿到退出码就需要额外设计回调复杂度会上升。对于删除单个文件这样的短任务阻塞同步调用完全没有问题。7. 在 CMD 和 PowerShell 环境下跑通验证在编写代码时第一步是验证核心的目录扫描和文件删除逻辑应该在 Godot 编辑器里直接按 F5 运行项目观察控制台输出。为了降低风险先不要直接扫描 C 盘的用户目录而是先创建一个测试目录在目录里造几个文件更安全。更合理的验证路径分三步。第一步先用 PowerShell 查看系统临时目录当前占用空间作为对照组Get-ChildItem $env:TEMP -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum | Select-Object Count, Sum第二步在 Godot 里运行 CleanScope点击“开始扫描”记录界面显示的文件数和总大小。正常情况下两者应当基本接近。第三步勾选 100 个最小文件点击删除。随后回到 PowerShell重新执行上面这条命令观察空间是否减少。如果删除失败不要在 UI 层反复点击。优先查看 Godot 输出控制台里的错误信息再检查对应文件是否正被程序占用。确定文件被占用时关闭占用程序后重试比强制删除更稳妥。8. Windows 清理工具的常见问题与排查清单Godot 桌面工具最容易出问题的地方往往不是 GDScript 语法而是与 Windows API 交互时的差异。下面这张表整理了几个在实践中常遇到的场景可以作为排错索引。问题现象可能原因排查方式解决方案DirAccess.get_directories_at返回为空目录名包含中文或特殊字符或目录权限不足打印完整路径确认字符串被正确转义使用OS.get_environment(USERPROFILE)拼路径避免硬编码用户名扫描过程 UI 卡死扫描逻辑写在了_on_button_pressed里没有使用独立线程查看调试器是否有长时间阻塞的栈将递归扫描放进Thread通过信号刷新 UI文件明明存在却“删除成功”后仍占空间文件被系统或杀毒软件占用remove_absolute失败却被忽略检查DirAccess.remove_absolute返回值删除后再次调用FileAccess.file_exists确认清理后 C 盘空间没有明显增加文件被移入回收站而非永久删除查看回收站清理项确认DeleteMode.PERMANENT分支生效或手动清空回收站高分屏下文字边缘发虚Godot 默认关闭 HiDPI或者启用了拉伸模式检查project.godot的display配置设置window/dpi/allow_hidpitrue并关闭画布拉伸列表刷新太频繁导致界面卡顿每扫到一个文件就执行一次 UI 的add_item打印每个批次的时间戳观察主线程负载只展示大于 1MB 的文件或用“每 100 个文件刷新一次”的节流策略弹出 ConfirmationDialog 后窗口没有响应在弹窗显示时后台仍在扫描UI 线程被占用查看是否出现线程死锁确保扫描完成后才允许执行删除操作这里要重点提醒一个反直觉的点“扫描完成后可清理空间”与实际“预期可释放空间”并不是同一个数字。因为 Temp 目录里总有几个文件会被正在运行的进程锁定。Word、Chrome、微信、企业微信几乎每时每刻都在占用临时文件。一个足够成熟的清理工具应当在统计结果中标注“存在占用风险”的文件并提供“仅显示可删除文件”的过滤选项。9. Godot 桌面工具项目的工程化建议如果你想把这个项目从“能跑”推进到“真的敢在别人的电脑上用”可以再考虑几点工程建议。第一扫描器和清理器要保持解耦。扫描器只需要告诉 UI“找到了哪个文件、有多大”它不应该知道删除逻辑。未来如果要增加“按扩展名过滤”“按访问时间筛选”等功能不会影响到清理模块。第二记录日志是清理工具的生命线。每次删除至少要记录以下字段文件完整路径、文件大小、删除时间、删除结果、错误码。GDScript 里可以用FileAccess.open(user://cleanscope.log, FileAccess.WRITE)简单记录但如果做正式版本建议把日志写到程序所在的目录方便用户反馈问题。第三权限问题要提前做判断。CleanScope 如果在普通用户权限下运行访问C:/Windows/Temp可能失败如果以管理员权限运行又可能误操作系统保护文件。稳妥做法是保留普通用户权限待用户需要清理系统级目录时再提示使用管理员权限重启程序。这遵循“最小权限原则”。第四不要直接遍历C:/Windows/SoftwareDistribution/Download这类系统更新缓存目录。虽然删除后可以临时释放几个 GB 空间但它可能导致后续 Windows 更新异常或者让系统进入一个“看起来正常但更新组件损坏”的危险状态。专业清理工具如 Dism) 之所以提供了深层的系统清理项是因为它实现了系统 API 级的安全校验。Godot 这类轻量工具不应该越界。第五UI 层面给用户明确的“扫描进度”反馈。当前_on_file_found只处理大于 1MB 的文件是有意的设计这样能大幅降低 UI 压力。但扫描期间仍然需要一个不断变化的提示比如显示“正在扫描文件夹xxx”避免用户以为程序卡住。# 在 scanner.gd 的 _scan_folder 中补充一个当前目录信号 signal current_dir_changed(dir_path: String) func _scan_folder(dir_path: String, depth: int 0) - void: current_dir_changed.emit(dir_path) # ...继续后续递归接到这个信号后main.gd只需要更新status_label.text。这个微小更新能显著改善工具类软件的体感让用户知道程序确实在干活而不是黑屏等待。10. 总结游戏引擎做工具真正的门槛不在界面写到这里Godot-CleanScope 的核心实现已经完整铺开了。回顾整个项目你会发现用 Godot 做 Windows C 盘清理工具最顺利的部分恰恰是 UI 和交互逻辑因为 Godot 的节点体系、信号机制和运行性能对这类应用太友好了。真正的难点分布在三个不起眼的位置一是 Windows 路径和权限规则二是文件占用的异常处理三是安全删除的边界控制。这也是我想通过这个项目传达的判断不要把 Godot 简单理解成“做游戏的引擎”。它的开发链路短、跨平台打包方便、内置 UI 控件和渲染逻辑完善非常适合做体量小、交互明确、需要快速交付的桌面效率工具。当然它不是万能的。如果你的目标用户是 Windows 大企业客户需要兼容 AD 域策略、系统组策略、企业证书等还是优先考虑成熟的桌面技术栈。如果你正想把 Godot 学到手不妨用“给朋友写一个临时文件清理器”这样的真实需求来练手。它能帮你把 GDScript、节点布局、线程、文件访问、外部进程调用全部串起来比单纯跟着教程做几款小游戏更能建立项目工程感。你可以先从单目录扫描开始跑通后再增加多个目录。等确认回收站删除和永久删除两条路径都安全后再考虑把项目做成带导出功能的小工具。最后任何清理动作都建议在虚拟机或自己的备用电脑上测试确认无误后再真正用于日常主力机。C 盘清理虽小敬畏数据的心不能少。
返回列表