Rust:如何在 Windows 上可靠地获取所有已安装的应用程序及其可执行文件路径(开始菜单 + 注册表方法)?
Rust:如何在 Windows 上可靠地获取所有已安装的应用程序及其可执行文件路径(开始菜单 + 注册表方法)?
核心结论
你当前使用开始菜单快捷方式扫描 + 注册表Uninstall项读取的组合是Windows上枚举已安装应用的基础实用方案,但存在覆盖不全的问题(比如UWP/MSIX应用、未创建快捷方式的程序、仅在用户级注册表注册的应用)。要实现更可靠的枚举,需要结合多源数据+针对性处理不同类型的Windows应用(Win32、UWP、MSIX)。
你的问题逐一解答
1. 这是枚举已安装应用的正确方法吗?
这是常用但不完整的方法:
- 开始菜单快捷方式是用户可见应用的最直接来源,但部分便携应用、后台服务类程序不会创建快捷方式;
- 注册表
Uninstall项仅覆盖通过标准安装程序(如Windows Installer)部署的Win32应用,且存在以下局限:- 部分应用仅在
HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall(用户级安装)而非HKLM注册; - 64位系统上的32位应用会注册到
HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall; - 完全不包含UWP/MSIX应用、便携应用。
- 部分应用仅在
2. 有没有更可靠的方法获取所有可执行文件路径?
需要多源数据组合 + 区分应用类型处理:
- Win32应用:结合开始菜单+HKLM/HKCU的Uninstall项(含Wow6432Node)+ 遍历
Program Files/Program Files (x86)目录(兜底方案); - UWP/MSIX应用:调用Windows原生
PackageManagerAPI获取包安装路径和入口点; - 便携应用:无法完全自动枚举,可通过Windows Search API或
everything-sdkcrate辅助扫描常用便携目录(如Downloads、Documents),但不推荐作为主要来源。
3. 注册表未提供.exe路径时如何处理?
可以通过以下启发式规则自动补全:
- 优先读取
DisplayIcon字段:注册表Uninstall项的DisplayIcon通常会直接指向.exe路径(格式可能为path.exe,0,需截断逗号前的内容); - 遍历
InstallLocation目录:递归扫描该目录下的.exe文件,优先选择:- 文件名与应用名称(
DisplayName)匹配或包含的; - 位于
bin、app等子目录下的; - 带有合法数字签名的程序(可通过Windows API验证);
- 文件名与应用名称(
- 回退标记:若以上都失败,可在结果中标记路径为
"需手动确认",后续由用户补充。
4. 推荐的crate和Windows API
| 场景 | 推荐Crate/API | 说明 |
|---|---|---|
| LNK快捷方式解析 | winlnk、windows::Win32::Shell::IShellLinkW | winlnk比你当前用的lnk更稳定,支持UTF-16编码的完整解析;直接调用Windows API可实现更复杂的快捷方式操作 |
| 注册表操作 | winreg(你已用)、windows::Win32::System::Registry | 支持访问HKCU、Wow6432Node等多路径 |
| UWP/MSIX应用枚举 | windows::Win32::System::PackageManager、msix crate | 直接调用系统API获取UWP/MSIX包的安装信息和入口点 |
| 文件扫描/路径处理 | walkdir(你已用)、path-slash、windows::Win32::Storage::FileSystem | 简化Windows路径的跨版本处理 |
优化后的Rust代码示例
以下代码解决了你的核心痛点:
- 覆盖HKCU、HKLM的32/64位注册表Uninstall路径;
- 自动遍历
InstallLocation查找.exe; - 支持UWP应用枚举;
- 更稳健的LNK解析;
- 去重逻辑优化。
use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use walkdir::WalkDir; use winreg::enums::*; use winreg::RegKey; use serde::Serialize; use windows::Win32::System::PackageManager::{FindPackagesByUserSecurityId, IPackage}; use windows::Win32::Security::{CreateWellKnownSid, WinBuiltinUsersSid}; use winlnk::ShellLink; #[derive(Debug, Serialize, Clone, PartialEq, Eq)] struct App { name: String, path: String, app_type: String, // 新增:标记应用类型,便于后续处理 } impl App { fn new(name: String, path: String, app_type: &str) -> Self { Self { name, path, app_type: app_type.to_string(), } } } fn main() { let mut apps: HashMap<String, App> = HashMap::new(); // 1️⃣ 扫描开始菜单(用户+系统级) scan_start_menu(&mut apps); // 2️⃣ 扫描注册表(HKLM/HKCU + 32/64位) scan_registry(&mut apps); // 3️⃣ 枚举UWP/MSIX应用 scan_uwp_apps(&mut apps); // 4️⃣ 保存结果到JSON let result: Vec<App> = apps.values().cloned().collect(); let json_output = serde_json::to_string_pretty(&result).unwrap(); let output_path = "installed_apps.json"; let mut file = File::create(output_path).expect("无法创建输出文件"); file.write_all(json_output.as_bytes()).expect("无法写入文件"); println!("成功导出{}个应用到{}", result.len(), output_path); } /// 扫描开始菜单快捷方式(用户+系统级) fn scan_start_menu(apps: &mut HashMap<String, App>) { let system_start_path = r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs"; let user_start_path = format!( "{}\\Microsoft\\Windows\\Start Menu\\Programs", std::env::var("APPDATA").unwrap_or_default() ); let start_paths = vec![system_start_path, &user_start_path]; for dir in start_paths { let dir_path = Path::new(dir); if !dir_path.exists() { continue; } for entry in WalkDir::new(dir_path).into_iter().filter_map(Result::ok) { let path = entry.path(); if path.extension().and_then(|s| s.to_str()) != Some("lnk") { continue; } // 使用winlnk解析LNK,支持UTF-16编码 match ShellLink::open(path) { Ok(link) => { if let Some(target) = link.target_path() { let name = path.file_stem() .unwrap_or_default() .to_string_lossy() .to_string(); apps.entry(name.clone()) .or_insert(App::new(name, target.to_string_lossy().to_string(), "Win32 (开始菜单)")); } } Err(e) => eprintln!("解析LNK失败 {}: {}", path.display(), e), } } } } /// 扫描注册表Uninstall项(覆盖HKLM/HKCU、32/64位) fn scan_registry(apps: &mut HashMap<String, App>) { // 要扫描的注册表路径列表 let reg_paths = vec![ (HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"), (HKEY_LOCAL_MACHINE, "SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"), (HKEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"), ]; for (hive, path) in reg_paths { let root_key = RegKey::predef(hive); match root_key.open_subkey(path) { Ok(uninstall_key) => { for subkey_name in uninstall_key.enum_keys().filter_map(Result::ok) { match uninstall_key.open_subkey(&subkey_name) { Ok(subkey) => { // 获取应用名称,跳过空名称 let display_name: Option<String> = subkey.get_value("DisplayName").ok(); let name = match display_name { Some(n) if !n.is_empty() => n, _ => continue, }; // 已存在则跳过 if apps.contains_key(&name) { continue; } // 处理应用路径 let app_path = match subkey.get_value::<String, _>("DisplayIcon") { Ok(icon_path) => { // DisplayIcon格式可能是"path.exe,0",截断逗号前的内容 icon_path.split(',').next().unwrap_or(&icon_path).to_string() } Err(_) => { if let Ok(install_loc) = subkey.get_value::<String, _>("InstallLocation") { // 遍历InstallLocation查找主.exe find_main_exe(&PathBuf::from(install_loc), &name) .map(|p| p.to_string_lossy().to_string()) .unwrap_or(install_loc) } else { continue; // 无有效路径则跳过 } } }; apps.insert(name.clone(), App::new(name, app_path, "Win32 (注册表)")); } Err(e) => eprintln!("打开注册表子键失败 {}: {}", subkey_name, e), } } } Err(e) => eprintln!("打开注册表路径失败 {}: {}", path, e), } } } /// 枚举UWP/MSIX应用 fn scan_uwp_apps(apps: &mut HashMap<String, App>) { // 创建当前用户的SID let mut sid = vec![0u8; 256]; let mut sid_len = sid.len() as u32; unsafe { if CreateWellKnownSid(WinBuiltinUsersSid, None, sid.as_mut_ptr(), &mut sid_len).as_bool() { if let Ok(packages) = FindPackagesByUserSecurityId(sid.as_ptr() as _) {




