 C 盤清理工具:Windows 桌面工具實戰(zhàn))
使用游戲引擎寫系統(tǒng)工具聽起來像“混搭”但實際折騰下來非常有意思。最近做 Windows 測試機(jī)時C 盤空間反復(fù)告警又不想為這點(diǎn)小事上個重客戶端于是基于 Godot 做了一個輕量 C 盤清理工具項目代號叫做CleanScope。本文會把從環(huán)境搭建、UI 設(shè)計、目錄掃描到調(diào)用 PowerShell 清理的完整思路整理出來代碼可以直接在 Godot 4.x 中運(yùn)行適合正在學(xué)習(xí) Godot、又對桌面工具開發(fā)感興趣的開發(fā)者。1. 為什么選擇 Godot PowerShell 做 C 盤清理1.1 C 盤爆滿的常見原因日常開發(fā)中C 盤占用上漲通常是幾個固定“兇手”在疊加系統(tǒng)臨時目錄C:\Windows\Temp當(dāng)前用戶臨時目錄%TEMP%Windows Update 下載緩存瀏覽器產(chǎn)生的緩存文件回收站中殘留的舊文件部分軟件生成的日志和崩潰轉(zhuǎn)儲文件單獨(dú)看每一個目錄占用的空間可能并不大但經(jīng)年累月之后里面會堆積大量小文件和舊的壓縮包最終把系統(tǒng)盤塞滿。手工清理也不是不行但需要記住一堆路徑刪除時還要擔(dān)心會不會把正在運(yùn)行的程序文件干掉。正因如此做一個可視化、可勾選、可預(yù)覽大小的清理工具是一個很有價值的練手項目。1.2 為什么不用 WinForms而用 Godot做 Windows 桌面工具傳統(tǒng)方案通常是 WinForms / WPF / Electron。WinForms 足夠熟悉WPF 寫界面也成熟Electron 則依賴 Node.js 運(yùn)行環(huán)境。使用 Godot 做這類工具的出發(fā)點(diǎn)有兩個Godot 的 UI 節(jié)點(diǎn)樹自帶布局能力可以快速做出一個帶進(jìn)度條、列表、按鈕的桌面界面如果后續(xù)愿意擴(kuò)展成游戲中的“系統(tǒng)控制臺”或者做成一個集成開發(fā)小工具Godot 里可以直接復(fù)用那套場景和腳本邏輯。但 Godot 畢竟不是為系統(tǒng)管理設(shè)計的它沒有內(nèi)置的 Windows 磁盤信息查詢接口也沒有現(xiàn)成的“清空臨時目錄”按鈕。所以 CleanScope 采用了一個相對務(wù)實的架構(gòu)Godot 負(fù)責(zé)界面展示、用戶交互、結(jié)果列表PowerShell 負(fù)責(zé)執(zhí)行 Windows 系統(tǒng)級操作GDScript 通過OS.execute()調(diào)用 PowerShell并把返回結(jié)果解析展示。這種方案分工明確既利用了 Godot 的跨平臺 UI 能力又彌補(bǔ)了游戲引擎在系統(tǒng)操作上的短板。1.3 CleanScope 的能力邊界這里需要先講清楚“能做什么”和“不應(yīng)該做什么”否則很容易寫出一個危險工具。CleanScope 主要提供以下功能讀取指定磁盤已經(jīng)使用和剩余的空間對一組經(jīng)過篩選的“安全可清理目錄”進(jìn)行占用掃描在掃描結(jié)果中展示目錄名稱、路徑、占用大小通過復(fù)選框決定是否清理調(diào)用 PowerShell 刪除目錄下的臨時內(nèi)容在界面中輸出清理前后磁盤剩余空間的對比。CleanScope 默認(rèn)不提供對C:\Windows\System32、C:\Windows\WinSxS這類系統(tǒng)關(guān)鍵目錄的刪除操作。它的定位是安全的“臨時文件順手清理工具”不是系統(tǒng)文件分析器。2. 環(huán)境準(zhǔn)備與項目初始化2.1 運(yùn)行環(huán)境下面的示例基于以下環(huán)境整理Windows 10 或 Windows 11Godot 4.x 官方標(biāo)準(zhǔn)版PowerShell 5.1Windows 自帶不需要額外第三方依賴你的電腦上如果安裝的是 Godot 3.x部分 API 名稱會有變化比如DirAccess.open()的用法不同建議直接使用 Godot 4.x。2.2 創(chuàng)建 Godot 工程打開 Godot 項目管理器點(diǎn)擊“新建項目”項目名稱CleanScope項目路徑按自己的目錄習(xí)慣選擇例如D:\Projects\CleanScope渲染器選擇Forward、Mobile或Compatibility都可以因為本項目不涉及 3D 渲染推薦Compatibility以降低導(dǎo)出體積創(chuàng)建完成后在項目目錄下建立以下文件夾CleanScope/ ├── scenes/ # 存放場景文件 ├── scripts/ # 存放 GDScript └── project.godot # Godot 自動生成2.3 UI 節(jié)點(diǎn)結(jié)構(gòu)建議在scenes目錄下新建一個主場景main.tscn根節(jié)點(diǎn)類型選擇Control。CleanScope 的 UI 不需要多復(fù)雜核心節(jié)點(diǎn)結(jié)構(gòu)如下MainScene(Control) ├── VBoxContainer │ ├── Label # 頂部標(biāo)題 │ ├── HBoxContainer # 工具欄 │ │ ├── Button # 掃描按鈕 │ │ ├── Button # 清理按鈕 │ │ └── ProgressBar # 掃描進(jìn)度 │ ├── HBoxContainer # 主體內(nèi)容 │ │ ├── VBoxContainer │ │ │ ├── ProgressBar # 磁盤容量進(jìn)度條 │ │ │ └── Label # 磁盤空間文本 │ │ └── ItemList # 可清理項目列表 │ └── RichTextLabel # 日志區(qū)域在需要引用的節(jié)點(diǎn)上可以開啟“唯一名稱”選項這樣腳本中可以通過%DriveBar、%CleanList的方式快速訪問。組件不必手動擺放只要把每個控件拖到對應(yīng)容器中Godot 會自動完成橫向或縱向排列。3. 核心概念GDScript 目錄遍歷與 PowerShell 調(diào)用3.1 GDScript 遍歷目錄的基本寫法如果只是遍歷某個非系統(tǒng)目錄GDScript 自帶的DirAccess足夠用。下面是一個最簡單的目錄遍歷示例它會遞歸統(tǒng)計某個目錄下所有文件的總大小func calculate_dir_size(path: String) - int: var total: int 0 var dir : DirAccess.open(path) if dir null: return 0 dir.list_dir_begin() var file_name : dir.get_next() while file_name ! : if file_name . or file_name ..: file_name dir.get_next() continue if dir.current_is_dir(): total calculate_dir_size(path.path_join(file_name)) else: var full_path : path.path_join(file_name) var file : FileAccess.open(full_path, FileAccess.READ) if file: total file.get_length() file.close() file_name dir.get_next() dir.list_dir_end() return total這段代碼的思路是打開指定路徑逐項讀取目錄內(nèi)容如果是子目錄遞歸繼續(xù)統(tǒng)計如果是文件通過FileAccess.open()打開后讀取文件長度累加所有文件大小后返回。不過這種遞歸寫法在掃描大型目錄時會比較慢而且遇到?jīng)]有權(quán)限訪問的目錄時返回的錯誤信息不夠直觀。因此 CleanScope 中真正做大目錄掃描時更推薦交給 PowerShell 完成。3.2 為什么把系統(tǒng)操作交給 PowerShellWindows 對中文路徑、長路徑、系統(tǒng)權(quán)限的處理在 PowerShell 中已經(jīng)很成熟。PowerShell 可以使用下面的寫法讓無法訪問的文件自動跳過Get-ChildItem -Path $dir -Recurse -Force -ErrorAction SilentlyContinue-ErrorAction SilentlyContinue會讓腳本在遇到權(quán)限不足或文件被占用時繼續(xù)向下執(zhí)行這是系統(tǒng)清理工具非常關(guān)鍵的能力。GDScript 中調(diào)用外部命令的接口是OS.execute()基礎(chǔ)用法如下var output: Array [] var args : PackedStringArray([-NoProfile, -Command, Get-Date]) var exit_code : OS.execute(powershell.exe, args, output, true) print(output)注意OS.execute()的參數(shù)是PackedStringArray返回值是進(jìn)程退出碼。如果 PowerShell 命令執(zhí)行成功通常返回0。3.3 可清理目錄清單CleanScope 默認(rèn)清理的目錄不是隨意枚舉的它只面向確定安全的臨時和緩存區(qū)域。目錄作用默認(rèn)推薦C:\Windows\Temp系統(tǒng)臨時目錄是%USERPROFILE%\AppData\Local\Temp當(dāng)前用戶臨時目錄是C:\Windows\SoftwareDistribution\DownloadWindows Update 下載緩存可選%USERPROFILE%\AppData\Local\Microsoft\Windows\INetCache系統(tǒng) Web 緩存可選回收站已刪除文件暫存區(qū)可選在真正的工程中不建議把“瀏覽器緩存目錄”寫死到代碼里因為每個人的瀏覽器安裝路徑不同瀏覽器版本升級后也可能改變緩存位置。4. CleanScope 關(guān)鍵代碼實現(xiàn)4.1 獲取磁盤剩余空間獲取 C 盤剩余空間的核心命令是Get-PSDrive -Name C它返回的對象中包含Used和Free兩個屬性。為了在 GDScript 中方便解析可以讓 PowerShell 將其轉(zhuǎn)換為 JSONGet-PSDrive -Name C | Select-Object {nUsed;e{$_.Used}}, {nFree;e{$_.Free}} | ConvertTo-Json -Compress對應(yīng)的 GDScript 代碼如下func get_drive_space(drive_letter: String) - Dictionary: if drive_letter.length() ! 1 or not drive_letter[0] in ABCDEFGHIJKLMNOPQRSTUVWXYZ: return {} var ps_command : (Get-PSDrive -Name drive_letter \ | Select-Object {nUsed;e{$_.Used}}, {nFree;e{$_.Free}}) | ConvertTo-Json -Compress var output: Array [] var args : PackedStringArray([ -NoProfile, -NonInteractive, -ExecutionPolicy, Bypass, -Command, ps_command ]) var exit_code : OS.execute(powershell.exe, args, output, true) if exit_code ! 0 or output.is_empty(): push_warning(無法獲取磁盤信息) return {} var json_text: String output[0].strip_edges() var result JSON.parse_string(json_text) if typeof(result) ! TYPE_DICTIONARY: return {} return result這里對drive_letter做了一個簡單校驗確保只允許單個大寫字母。這是防止外部傳入惡意 PowerShell 命令的第一道防線即便工具只在本機(jī)使用也應(yīng)該保留這種校驗。4.2 讓 PowerShell 腳本文件在運(yùn)行時生成很多人會直接把res://路徑傳給 PowerShell這是不推薦的。Godot 在導(dǎo)出后res://對應(yīng)的是打包資源不一定能直接被 PowerShell 以文件路徑方式讀取。CleanScope 的做法是把需要用到的 PowerShell 腳本內(nèi)容寫到user://數(shù)據(jù)目錄下的一個文件中再由 GDScript 調(diào)用。GDScript 中需要把user://轉(zhuǎn)換成系統(tǒng)絕對路徑使用ProjectSettings.globalize_path()var user_dir : user://cleanscope DirAccess.make_dir_recursive_absolute(user_dir) var script_path : ProjectSettings.globalize_path(user_dir /clean_scope.ps1)這樣做的好處是不修改res://pack內(nèi)部文件程序退出后日志和臨時腳本保留用戶可以打開對應(yīng)目錄檢查腳本內(nèi)容做到行為透明。4.3 生成掃描與清理腳本將下面的 PowerShell 腳本內(nèi)容寫入clean_scope.ps1。腳本接收一個 JSON 數(shù)組作為目標(biāo)列表通過Mode區(qū)分掃描與清理。param( [string]$Mode, [string]$TargetsJson, [string]$OutFile ) $targets $TargetsJson | ConvertFrom-Json $results () foreach ($item in $targets) { $name $item.name $path $item.path if (-not (Test-Path -LiteralPath $path)) { $results [PSCustomObject]{ name $name path $path size 0 exists $false cleaned $false } continue } if ($Mode -eq scan) { $size (Get-ChildItem -LiteralPath $path -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum if ($null -eq $size) { $size 0 } $results [PSCustomObject]{ name $name path $path size [int64]$size exists $true cleaned $false } } if ($Mode -eq clean) { $cleaned $false if (Test-Path -LiteralPath $path) { $items Get-ChildItem -LiteralPath $path -Force -ErrorAction SilentlyContinue if ($items) { $items | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $cleaned $true } } $results [PSCustomObject]{ name $name path $path size 0 exists $true cleaned $cleaned } } } $results | ConvertTo-Json -Compress | Set-Content -LiteralPath $OutFile -Encoding UTF8這里清理的是“目錄下的內(nèi)容”而不是目錄本身這樣不會破壞Temp目錄的系統(tǒng)引用。4.4 構(gòu)造目標(biāo)列表CleanScope 的清理項目定義為一個常量數(shù)組。為了讓用戶名路徑在不同電腦上都能生效使用USERPROFILE環(huán)境變量動態(tài)拼接func build_clean_targets() - Array: var user_profile : OS.get_environment(USERPROFILE) if user_profile.is_empty(): user_profile C:/Users/Default var targets : [ { name: 系統(tǒng)臨時目錄, path: C:/Windows/Temp, checked: true }, { name: 當(dāng)前用戶臨時目錄, path: user_profile /AppData/Local/Temp, checked: true }, { name: Windows 更新緩存, path: C:/Windows/SoftwareDistribution/Download, checked: false }, { name: 系統(tǒng) Web 緩存, path: user_profile /AppData/Local/Microsoft/Windows/INetCache, checked: false } ] return targets實際 UI 中可以把這個數(shù)組渲染到ItemList或Tree上并允許用戶通過復(fù)選框決定是否勾選。4.5 掃描按鈕邏輯點(diǎn)擊掃描按鈕后CleanScope 把勾選的目標(biāo)轉(zhuǎn)成 JSON傳給 PowerShell 腳本并將輸出文件讀取回來。func _on_scan_pressed() - void: var targets: Array build_clean_targets() var json : JSON.stringify(targets) var target_file : user_dir /targets.json var result_file : user_dir /scan_result.json var target_file_abs : ProjectSettings.globalize_path(target_file) var result_file_abs : ProjectSettings.globalize_path(result_file) var f : FileAccess.open(target_file_abs, FileAccess.WRITE) if f: f.store_string(json) f.close() var script_abs : ProjectSettings.globalize_path(ps_script_path) var args : PackedStringArray([ -NoProfile, -NonInteractive, -ExecutionPolicy, Bypass, -File, script_abs, -Mode, scan, -TargetsJson, target_file_abs, -OutFile, result_file_abs ]) var output: Array [] OS.execute(powershell.exe, args, output, true) var rf : FileAccess.open(result_file_abs, FileAccess.READ) if rf null: push_error(掃描結(jié)果文件不存在) return var text : rf.get_as_text() rf.close() var parsed JSON.parse_string(text) if typeof(parsed) ! TYPE_ARRAY: return clean_list.clear() for item in parsed: var path_text: String item.get(path, ) var size_value: int int(item.get(size, 0)) clean_list.add_item( %s | %s | %s % [item.get(name, ), path_text, format_size(size_value)] )這樣設(shè)計有一個好處掃描過程中即使有文件夾無法打開PowerShell 的SilentlyContinue也會保證腳本繼續(xù)運(yùn)行不會因為個別權(quán)限錯誤導(dǎo)致整個掃描中斷。4.6 執(zhí)行清理清理邏輯與掃描類似只是在調(diào)用腳本時把Mode換成clean。為了避免用戶誤點(diǎn)清理前必須再次確認(rèn)。確認(rèn)框中要明確列出會影響的范圍。func _on_clean_pressed() - void: var confirm : ConfirmationDialog.new() confirm.dialog_text 確認(rèn)清理勾選的臨時文件嗎該操作不會刪除臨時目錄本身。 confirm.ok_button_text 確認(rèn)清理 add_child(confirm) confirm.popup_centered() confirm.confirmed.connect(_do_clean)真正執(zhí)行前記錄一下當(dāng)前剩余空間清理后再讀取一次剩余空間把差值顯示到日志區(qū)域。func _do_clean() - void: var before: Dictionary get_drive_space(C) var free_before: int int(before.get(Free, 0)) # 構(gòu)造目標(biāo)、生成腳本并執(zhí)行 PowerShell run_powershell_mode(clean) var after: Dictionary get_drive_space(C) var free_after: int int(after.get(Free, 0)) var released : free_after - free_before log_view.append_text([colorgreen]清理完成預(yù)計釋放空間%s[/color]\n % format_size(released))4.7 字節(jié)大小格式化函數(shù)為了把掃描結(jié)果中的原始字節(jié)轉(zhuǎn)換成更易讀的文本可以添加一個通用格式化函數(shù)func format_size(size: int) - String: if size 1024: return %d B % size elif size 1024 * 1024: return %.2f KB % (size / 1024.0) elif size 1024 * 1024 * 1024: return %.2f MB % (size / (1024.0 * 1024.0)) else: return %.2f GB % (size / (1024.0 * 1024.0 * 1024.0))在 UI 中傳入字節(jié)數(shù)即可顯示成3.56 GB對普通用戶更友好。5. 完整項目腳本示例下面給出一份精簡但完整的clean_scope.gd腳本結(jié)構(gòu)方便直接對照實現(xiàn)。你需要先建立主場景并添加兩個節(jié)點(diǎn)%CleanList類型為ItemList%LogView類型為RichTextLabel腳本內(nèi)容如下extends Control var user_dir : user://cleanscope var ps_script_path : user_dir /clean_scope.ps1 const POWER_SHELL_SCRIPT : { param( [string]$Mode, [string]$TargetsJson, [string]$OutFile ) $targets $TargetsJson | ConvertFrom-Json $results () foreach ($item in $targets) { $name $item.name $path $item.path if (-not (Test-Path -LiteralPath $path)) { $results [PSCustomObject]{ name $name path $path size 0 exists $false cleaned $false } continue } if ($Mode -eq scan) { $size (Get-ChildItem -LiteralPath $path -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum if ($null -eq $size) { $size 0 } $results [PSCustomObject]{ name $name path $path size [int64]$size exists $true cleaned $false } } if ($Mode -eq clean) { $cleaned $false if (Test-Path -LiteralPath $path) { $items Get-ChildItem -LiteralPath $path -Force -ErrorAction SilentlyContinue if ($items) { $items | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $cleaned $true } } $results [PSCustomObject]{ name $name path $path size 0 exists $true cleaned $cleaned } } } $results | ConvertTo-Json -Compress | Set-Content -LiteralPath $OutFile -Encoding UTF8 } onready var clean_list: ItemList %CleanList onready var log_view: RichTextLabel %LogView func _ready() - void: DirAccess.make_dir_recursive_absolute(user_dir) ensure_ps_script() func ensure_ps_script() - void: var abs_path : ProjectSettings.globalize_path(ps_script_path) if FileAccess.file_exists(abs_path): return var f : FileAccess.open(abs_path, FileAccess.WRITE) if f: f.store_string(POWER_SHELL_SCRIPT) f.close() func build_clean_targets() - Array: var user_profile : OS.get_environment(USERPROFILE) if user_profile.is_empty(): user_profile C:/Users/Default var targets : [ { name: 系統(tǒng)臨時目錄, path: C:/Windows/Temp, checked: true }, { name: 當(dāng)前用戶臨時目錄, path: user_profile /AppData/Local/Temp, checked: true }, { name: Windows 更新緩存, path: C:/Windows/SoftwareDistribution/Download, checked: false }, { name: 系統(tǒng) Web 緩存, path: user_profile /AppData/Local/Microsoft/Windows/INetCache, checked: false } ] return targets然后在按鈕信號中調(diào)用掃描與清理方法。核心思路并不復(fù)雜本質(zhì)上就是“用戶確認(rèn)勾選路徑 → 調(diào)用 PowerShell 統(tǒng)計或刪除 → 刷新界面”。6. 常見問題與排查思路在實際運(yùn)行 CleanScope 時你可能會遇到一些問題下面整理成可快速對照的表格。問題現(xiàn)象常見原因解決思路查詢磁盤信息失敗PowerShell 執(zhí)行策略或參數(shù)錯誤檢查是否使用-NoProfile -ExecutionPolicy Bypass確認(rèn)磁盤盤符是單個大寫字母掃描結(jié)果始終為空目標(biāo)目錄不存在或 JSON 解析失敗先手動確認(rèn)C:\Windows\Temp等路徑是否存在再用 Godot 的調(diào)試輸出查看返回文本清理時提示“拒絕訪問”當(dāng)前進(jìn)程沒有管理員權(quán)限右鍵以管理員身份運(yùn)行程序或修改項目導(dǎo)出設(shè)置部分文件無法刪除文件被系統(tǒng)進(jìn)程或軟件鎖定這類文件無法刪除屬正?,F(xiàn)象應(yīng)跳過并繼續(xù)清理其他文件執(zhí)行 PowerShell 后彈出黑窗口外部進(jìn)程執(zhí)行時的控制臺窗口閃爍在導(dǎo)出配置中為 Windows 平臺設(shè)置當(dāng)前進(jìn)程不顯示控制臺窗口或在調(diào)試階段接受窗口閃現(xiàn)清理后剩余空間變化不明顯其他軟件會在清理后重新寫入緩存清理前關(guān)閉瀏覽器、