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\Run 和 RunOnce(HKLM 还有 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(finishScan 里 applyFilter() + 刷新表格 + 更新状态栏统计)。
7. 额外信息:厂商 / 签名 / 图标的获取
StartupManager 还顺手给每条项补充了安全相关信息,全部基于 Win32 API:
厂商 :GetFileVersionInfoW + VerQueryValueW 读版本资源的 CompanyName。
数字签名 :WinVerifyTrust 配 WINTRUST_ACTION_GENERIC_VERIFY_V2 策略 GUID 校验。
图标 :SHGetFileInfoW 取系统关联图标,QPixmap::fromImage(QImage::fromHICON(...)) 转成 QIcon。
系统关键项 :维护一份系统服务 / 注册表白名单(isSystemCritical),界面里用绿色标出,提示用户勿误删。
这些都是只读判断。