Qt 实现一个 Windows 启动项查看器

Qt 实现一个 Windows 启动项查看器

flyfish

技术栈:Qt 6.9.3 (msvc2022_64) + C++17

平台:Windows 11

源码下载

Windows 的开机启动项散落的地方:启动文件夹的快捷方式、注册表 Run/RunOnce、系统服务、计划任务、UWP 后台启动权限。

1. 整体架构

刻意做了最薄的两层分离,便于维护:

复制代码
StartupViewer/
├── main.cpp                  // 入口:创建窗口、显示
├── core/
│   ├── startupitem.h         // 启动项数据结构 StartupItem
│   ├── startupmanager.h/.cpp // 采集引擎(纯逻辑,不碰 UI)
└── widget/
    ├── startupviewerwidget.h/.cpp/.ui  // 界面与交互

StartupManager 只负责「扫描 → 返回 QList<StartupItem>」,不依赖任何 Qt 控件。

StartupViewerWidget 负责把数据渲染成表格、处理筛选与双击。

这样的好处是:扫描逻辑可以轻松挪到后台线程,UI 和采集逻辑彻底解耦。

StartupItem 承载一条启动项的全部信息:

cpp 复制代码
struct StartupItem {
    QString      name;           // 显示名称
    QString      command;        // 实际执行命令(含路径 + 参数)
    QString      exePath;        // 主程序路径(解析后)
    QString      location;       // 来源位置(人类可读)
    QString      rawLocation;    // 内部定位标识(注册表路径 / .lnk / 任务名)
    StartupType  type   = ST_Folder;  // 文件夹 / 注册表 / 计划任务 / 服务 / UWP
    StartupScope scope  = SC_CurrentUser;
    bool         enabled = true; // 是否启用(开机自启)
    QString      publisher;      // 厂商(版本信息)
    QString      signature;      // 数字签名:已签名 / 未签名 / 未知
    QString      impact;         // 启动影响:高 / 中 / 低 / 无
    QIcon        icon;           // 程序图标
    bool         systemCritical = false; // 系统关键项(受保护)
};

2. 启动项来源

scanAll() 把五路扫描串行串起来,全都是只读访问:

cpp 复制代码
QList<StartupItem> StartupManager::scanAll() {
    QList<StartupItem> items;
    scanFolders(items);   // 启动文件夹快捷方式
    scanRegistry(items);  // Run / RunOnce
    scanServices(items);  // 系统服务
    scanTasks(items);     // 计划任务(登录触发)
    scanUwp(items);       // UWP 后台启动权限
    return items;
}

2.1 启动文件夹:解析 .lnk 快捷方式

启动文件夹里放的是快捷方式(.lnk),真正的目标要在 .lnk 里读。用 COM 的 IShellLink

cpp 复制代码
QString StartupManager::resolveShortcut(const QString &lnkPath) {
    if (!m_comInit) return QString();                 // 构造函数里 CoInitializeEx 过
    IShellLinkW *psl = nullptr;
    HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL,
                  CLSCTX_INPROC_SERVER, IID_IShellLinkW, (void**)&psl);
    if (FAILED(hr)) return QString();
    IPersistFile *ppf = nullptr;
    psl->QueryInterface(IID_IPersistFile, (void**)&ppf);
    QString target;
    if (SUCCEEDED(ppf->Load((LPCWSTR)lnkPath.utf16(), STGM_READ))) {
        wchar_t buf[MAX_PATH] = {0};
        WIN32_FIND_DATAW wfd;
        if (SUCCEEDED(psl->GetPath(buf, MAX_PATH, &wfd, SLGP_UNCPRIORITY)))
            target = QString::fromWCharArray(buf);
    }
    ppf->Release(); psl->Release();
    return target;
}

IShellLinkW + utf16() 全程宽字符,避免 ANSI 路径在中文系统上丢字。COM 必须先在构造函数 CoInitializeEx,析构里 CoUninitialize

已禁用的文件夹项放在 Startup 同级 的 StartupDisabled 子目录,单独扫一遍即可。

2.2 注册表 Run / RunOnce:禁用标记藏在另一个键

HKCU / HKLM...\CurrentVersion\RunRunOnceHKLM 还有 WOW6432Node 32 位视图)是主力来源。但「禁用」状态不在 Run 键本身 ,而在 ...\Explorer\StartupApproved\Run 里------按名称对应一个 REG_BINARY,首字节为 0x03 表示该键被禁用:

cpp 复制代码
QSettings approved(k.approved, QSettings::NativeFormat);
auto isDisabled = [&](const QString &nm) -> bool {
    QByteArray b = approved.value(nm).toByteArray();
    return !b.isEmpty() && (quint8)b[0] == 0x03;
};

2.3 系统服务:读 Services 注册表的 Start 值

服务不是Run键,而是 HKLM\SYSTEM\CurrentControlSet\Services 下每个服务名的 Start 值:

cpp 复制代码
int startType = s.value("Start", 4).toInt();
bool isAuto = (startType == 2);   // 2=自动, 0=Boot, 1=System, 3=手动, 4=禁用
bool include = isAuto || startType == 0 || startType == 1;

只有自动 / 引导 / 系统三类才算开机启动项。

2.4 计划任务:调 schtasks 解析 CSV

没有现成注册表键能直接列出登录触发的启动任务,最稳的办法是调系统自带 schtasks

cpp 复制代码
p.start("schtasks", QStringList() << "/query" << "/fo" << "CSV" << "/v");

再写一个简单的 CSV 解析器(处理引号内逗号),按表头定位列,只保留触发器含「At logon / 登录时」的任务:

cpp 复制代码
if (!triggers.contains("At logon", Qt::CaseInsensitive) &&
    !triggers.contains("登录时", Qt::CaseInsensitive))
    continue;   // 不是登录触发,跳过

2.5 UWP 后台启动权限

UWP 应用的「后台启动」权限在 HKCU\...\BackgroundAccessApplications 下,逐组读 Disabled 值即可。这类项没有本地 exe 路径,双击时只弹信息框。

3. 扫描动画:火箭图标

cpp 复制代码
class CircleProgress : public QWidget {
public:
    explicit CircleProgress(QWidget *parent = nullptr, int size = 48)
        : QWidget(parent), m_angle(0) { setFixedSize(size, size); }
    void setAngle(int a) { m_angle = a; update(); }
protected:
    void paintEvent(QPaintEvent *) override {
        QPainter p(this);
        p.setRenderHint(QPainter::Antialiasing);
        p.scale(width() / 140.0, width() / 140.0);  // 基准 140 → 任意尺寸
        int cx = 70, cy = 70, r = 64;
        // ... 画浅绿底圆、绿色弧、白色火箭头/机翼/尾焰 ...
    }
    int m_angle;
};

构造时把它塞进工具栏布局右侧,默认隐藏:

cpp 复制代码
m_scanCircle = new CircleProgress(this, 44);
m_scanCircle->hide();
ui->toolbarLayout->addWidget(m_scanCircle);

扫描时启动一个 30ms 定时器递增 m_angle 让火箭旋转,扫描结束 hide() 掉。

6. 让扫描不卡 UI:QtConcurrent 后台线程

采集里 schtasks 可能耗时数秒,必须放到后台,否则 UI 冻住、旋转动画也停。用 QtConcurrent::run + QFutureWatcher

cpp 复制代码
void StartupViewerWidget::onScanClicked() {
    if (m_scanning) return;
    m_scanning = true;
    ui->btnScan->setEnabled(false);
    showScanningUI();                           // 先显示动画

    QTimer::singleShot(80, this, [this]() {    // 让动画先画一帧
        QFuture<QList<StartupItem>> future = QtConcurrent::run([this] {
            return m_mgr->scanAll();            // 后台线程执行
        });
        auto *watcher = new QFutureWatcher<QList<StartupItem>>(this);
        connect(watcher, &QFutureWatcher<QList<StartupItem>>::finished,
                this, [this, watcher]() {
            m_items = watcher->result();
            watcher->deleteLater();
            finishScan();                        // 回到主线程刷新表格
        });
        watcher->setFuture(future);
    });
}

QFutureWatcher::finished 信号在主线程 触发,所以在回调里安全地操作 UI(finishScanapplyFilter() + 刷新表格 + 更新状态栏统计)。

7. 额外信息:厂商 / 签名 / 图标的获取

StartupManager 还顺手给每条项补充了安全相关信息,全部基于 Win32 API:

厂商GetFileVersionInfoW + VerQueryValueW 读版本资源的 CompanyName

数字签名WinVerifyTrustWINTRUST_ACTION_GENERIC_VERIFY_V2 策略 GUID 校验。

图标SHGetFileInfoW 取系统关联图标,QPixmap::fromImage(QImage::fromHICON(...)) 转成 QIcon

系统关键项 :维护一份系统服务 / 注册表白名单(isSystemCritical),界面里用绿色标出,提示用户勿误删。

这些都是只读判断。

相关推荐
FfHUCisI1 小时前
Golang 函数调用的瞬间:栈帧、拷贝栈与逃逸分析
开发语言·后端·golang
万邦科技Lafite1 小时前
淘宝海外采购指南,taoworld资格申请对接流程讲解
开发语言·电商开放平台·淘宝开放平台·京东开放平台·比价
ThornArmor1 小时前
奔腾的呼吸:被驱逐者的灵感
开发语言·程序人生·架构
软件黑马王子2 小时前
12.缓存池优化:对象上限
开发语言·前端框架·c#
阿里嘎多学长2 小时前
2026-09-07 GitHub 热点项目精选
开发语言·程序员·github·代码托管
流浪0012 小时前
大模型技术全景(六):AI 的“逐字接龙“——Token 与自回归生成背后的真相
开发语言·人工智能·llm
Raas1003 小时前
AI网关和OpenRouter区别?MAI Gateway(魔芋企业级AI网关)企业级方案对比指南
大数据·开发语言·人工智能·gateway·php·ai网关·mai gateway
点心的游戏开发世界10 小时前
GDScript 入门笔记(十一):Lambda 与匿名函数
开发语言·笔记·游戏引擎·godot
事圆则缓12 小时前
Kotlin 协程完全指南
开发语言·kotlin