You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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原生PackageManager API获取包安装路径和入口点;
  • 便携应用:无法完全自动枚举,可通过Windows Search API或everything-sdk crate辅助扫描常用便携目录(如DownloadsDocuments),但不推荐作为主要来源。

3. 注册表未提供.exe路径时如何处理?

可以通过以下启发式规则自动补全:

  1. 优先读取DisplayIcon字段:注册表Uninstall项的DisplayIcon通常会直接指向.exe路径(格式可能为path.exe,0,需截断逗号前的内容);
  2. 遍历InstallLocation目录:递归扫描该目录下的.exe文件,优先选择:
    • 文件名与应用名称(DisplayName)匹配或包含的;
    • 位于binapp等子目录下的;
    • 带有合法数字签名的程序(可通过Windows API验证);
  3. 回退标记:若以上都失败,可在结果中标记路径为"需手动确认",后续由用户补充。

4. 推荐的crate和Windows API

场景推荐Crate/API说明
LNK快捷方式解析winlnkwindows::Win32::Shell::IShellLinkWwinlnk比你当前用的lnk更稳定,支持UTF-16编码的完整解析;直接调用Windows API可实现更复杂的快捷方式操作
注册表操作winreg(你已用)、windows::Win32::System::Registry支持访问HKCU、Wow6432Node等多路径
UWP/MSIX应用枚举windows::Win32::System::PackageManagermsix crate直接调用系统API获取UWP/MSIX包的安装信息和入口点
文件扫描/路径处理walkdir(你已用)、path-slashwindows::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 _) {

火山引擎 最新活动