☰
Qt操作Windows服务实战:SCM API封装与ServiceController实现
2026/10/17 16:17:18 网站建设 项目流程

简介:这套项目代码面向使用QT框架在Windows平台开发系统管理工具的工程师,演示了如何对Windows服务执行创建、删除、启动、停止、打开与关闭等操作,并支持设置服务的自动/手动启动类型、查询服务名称、状态与描述等信息,特别提醒了服务控制所需的管理员权限问题,可帮助避开操作时不必要的权限坑。资源包为zip格式,共9个文件、约10KB,压缩包体积较小;其中2个cpp与2个h构成核心服务管理逻辑,1个ui提供操作界面,pro为工程配置,md说明文档帮助快速上手。目前已有50人浏览学习。代码结构清晰,主窗口、ServiceManager类与界面文件分离,可直接在QT环境中编译运行,便于二次扩展为更复杂的服务监控或管理系统,也适合作为学习模板对照修改。对于刚接触Windows服务编程的QT开发者来说,这是一份小而实用的参考实现,也可作为服务管理功能模块嵌入自有QT项目。

1. QT操作Windows服务:先别找QT += service,官方真没有

搜“QT操作Windows服务”的人,大概率是想给Windows服务做一个带界面的管理工具:列出服务、一键启动停止、改启动类型、装新服务卸旧服务。很多人第一反应是在.pro里加一个QT += service之类的模块,然后调某个高级API——Qt官方从来就没有这个模块,不少新手就卡在这第一步。真正干这行的组合拳是:Qt负责界面、线程和事件循环,Win32的SCM(服务控制管理器)API负责和系统服务对话,两者通过信号槽交换结果。下面就从这套组合拳讲起,直接给出可以抄走的C++代码和踩坑记录,适合做桌面运维工具、系统工具或自研服务管理器的开发者。

2. 先弄清Windows服务由谁管理:SCM、Session 0 与三条可走的路

2.1 服务不是“孤儿进程”,是SCM统一管理的系统资源

Windows服务本质上是一个被SCM(服务控制管理器)托管的进程。SCM跑在services.exe里,从系统启动时就开始工作,负责服务的注册、启动、停止、依赖关系维护。每个服务在注册表HKLM\SYSTEM\CurrentControlSet\Services\下都有一个同名键,键里存着ImagePath、Start、DisplayName、ObjectName这些关键配置。想“操作”一个服务,正确的做法是通过advapi32.dll提供的API去和SCM对话,而不是用OpenProcess或TerminateProcess去杀进程——服务进程被强杀后,SCM会把它标记为异常终止,依赖它的服务、之后的重启策略都会连锁出问题。

和SCM对话的枢纽是SERVICE_STATUS结构体。这个结构体记录了服务的当前状态:SERVICE_STOPPED、SERVICE_START_PENDING、SERVICE_RUNNING、SERVICE_STOP_PENDING,还有dwWaitHint、dwControlsAccepted等字段。启动或停止服务,本质上是让服务在当前状态和目标状态之间走一段异步流程:SCM收到StartService请求后,先让服务进入START_PENDING,服务进程初始化完成后自己调用SetServiceStatus改成RUNNING。理解这一点很重要,因为后面很多坑都是“请求发了,状态却没变”造成的。

2.2 三条路线对比:QProcess、Win32 API、第三方库

实际操作Windows服务,常见做法有三条路。

路线优点缺点适用场景
QProcess + net start/stop代码最少,不需要接触结构体每次拉起命令行慢;中文系统下解析输出容易被本地化干扰;UAC弹窗不可控调试脚本、临时工具
Win32 API + Qt 封装速度快,错误码明确,状态控制精细要理解权限和结构体,代码量略大正式项目,推荐
第三方服务封装库接口友好黑匣子,出问题得翻源码,维护不确定性高快速原型

我的建议很直接:正式做“QT操作Windows服务[项目代码]”这个方向,直接用Win32 API,Qt只做界面和线程层。QProcess那条路看着省事,但net start本身要经过cmd.exe,服务多的时候光枚举状态就要反复拉起进程,速度完全没法看;而且net命令的返回值在不同语言版本的系统里有细微差别,解析着解析着就翻车了。第三方库也不是不行,但SCM这套API已经很稳定,自己封装一次,后面所有项目都能复用,比依赖一个别人维护的库踏实。

2.3 权限与UAC:没提权,后面全白干

SCM有自己的一套权限模型。打开SCM句柄至少需要SC_MANAGER_CONNECT,查询服务状态需要SERVICE_QUERY_STATUS,启动服务需要SERVICE_START,停止服务需要SERVICE_STOP,创建服务需要SC_MANAGER_CREATE_SERVICE,删除服务要SERVICE_DELETE,改配置要SERVICE_CHANGE_CONFIG。你写的程序默认在普通用户权限下运行,调用OpenSCManager时可能直接返回空句柄,GetLastError()是5(拒绝访问)。

解决方式分两步。第一步是给程序加UAC清单,让执行时弹出用户账户控制确认。用qmake管理工程时,常见做法是在.pro里指定清单文件,再用编译器把它打进exe资源。清单文件本身长这样,xml里明确写requireAdministrator:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> </assembly>

第二步是代码里按最小权限原则传访问标志:查询就只传SERVICE_QUERY_STATUS,启动再传SERVICE_START,不要图省事传SERVICE_ALL_ACCESS。这样权限不一时能明确知道是哪一步缺权限,而不是被全权限挡住,排查起来更清楚。

提示:qmake工程里可以用QMAKE_MANIFEST += $$PWD/app.manifest把清单文件绑定进exe;CMake工程则在add_executable后调用set_target_properties设置MANIFEST_FILE属性。

3. 用Qt封装一个ServiceController:查询、启动、停止、安装、卸载

3.1 头文件与接口设计:一个QObject,信号槽对外

封装的第一步是把SCM的操作收敛成一个QObject子类。为什么必须是QObject?因为启停服务是耗时操作,结果要跨线程回传到界面,信号槽是Qt里最稳的线程间通信方式。下面这个头文件是常用的最小接口,核心是五个方法:查询状态、启动、停止、安装、卸载,外加两个信号。

// servicecontroller.h #ifndef SERVICECONTROLLER_H #define SERVICECONTROLLER_H #include <QObject> #include <QString> #include <windows.h> class ServiceController : public QObject { Q_OBJECT public: // 对应 SERVICE_STATUS.dwCurrentState 的取值 enum ServiceState { StateStopped = 1, StateStartPending = 2, StateStopPending = 3, StateRunning = 4, StateError = 0 }; Q_ENUM(ServiceState) explicit ServiceController(QObject *parent = nullptr); // 同步查询状态,内部会阻塞调用线程 ServiceState queryStatus(const QString &serviceName); // 异步启停,结果通过信号回传 void startService(const QString &serviceName); void stopService(const QString &serviceName); // 安装/卸载,成功返回true,失败返回false bool installService(const QString &binPath, const QString &serviceName, const QString &displayName, bool autoStart); bool removeService(const QString &serviceName); signals: void statusReady(const QString &serviceName, int state); void startFinished(const QString &serviceName, bool ok, const QString &errMsg); void stopFinished(const QString &serviceName, bool ok, const QString &errMsg); private: bool waitForState(const QString &serviceName, DWORD wantedState, DWORD waitHintMs); }; #endif // SERVICECONTROLLER_H

接口设计上有两个取舍。查询状态我做成了同步函数,因为单次查询只有几十毫秒,调用方自己决定要不要放到工作线程;启停则做成异步,因为启动一个服务可能要等好几秒(服务在START_PENDING里初始化),同步放在界面线程上,界面就卡住了。installService的参数里,binPath是exe的完整路径,serviceName是注册表键名(后面会讲它和显示名的区别),autoStart决定是自动启动还是手动启动。

3.2 查询服务状态:OpenSCManager + OpenService + QueryServiceStatus

查询是最常用的操作,也是整个封装的地基。逻辑分四步:打开SCM句柄、打开服务句柄、调用QueryServiceStatus、释放两个句柄。错误发生时要逐层释放,并用GetLastError()记录原因。

#include "servicecontroller.h" #include <QDebug> #include <QElapsedTimer> ServiceController::ServiceState ServiceController::queryStatus(const QString &serviceName) { // 打开SCM,只需要CONNECT权限,权限越小越不容易被拒 SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); if (!scm) { qWarning() << "OpenSCManager failed:" << GetLastError(); return StateError; } // 打开服务,查询状态只需要QUERY_STATUS权限 SC_HANDLE service = OpenServiceW( scm, reinterpret_cast<LPCWSTR>(serviceName.utf16()), SERVICE_QUERY_STATUS); if (!service) { qWarning() << "OpenService failed:" << GetLastError(); CloseServiceHandle(scm); return StateError; } SERVICE_STATUS status; if (!QueryServiceStatus(service, &status)) { qWarning() << "QueryServiceStatus failed:" << GetLastError(); CloseServiceHandle(service); CloseServiceHandle(scm); return StateError; } CloseServiceHandle(service); CloseServiceHandle(scm); return static_cast<ServiceState>(status.dwCurrentState); }

这里有个细节:OpenServiceW第二个参数必须传LPCWSTR,我习惯用QString::utf16()转而不是toStdWString().c_str(),因为QString::utf16()返回const ushort*,在Windows上正好和WCHAR兼容,也不会产生临时对象的生命周期问题。句柄一定要用CloseServiceHandle释放,SCM句柄泄漏多了会影响系统服务枚举,这是CI跑久了才会暴露的隐患。查询失败时,GetLastError()常见值是5(权限不足)和1060(服务不存在),排查时先看这两个。另外暂停、继续这类状态(dwCurrentState为5、6、7)在上面的枚举里没展开,实际项目按需补上就行,核心流程不变。

3.3 启动与停止:StartService、ControlService 与等待稳定状态

启动服务的代码比查询多一块:StartServiceW只负责把请求交给SCM,服务进程真正进入RUNNING是异步的。如果调完StartService马上再QueryServiceStatus,拿到的往往还是START_PENDING甚至旧状态,所以必须轮询等待稳定状态。

void ServiceController::startService(const QString &serviceName) { bool ok = false; QString err; SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); if (!scm) { err = QStringLiteral("OpenSCManager失败,错误码%1").arg(GetLastError()); emit startFinished(serviceName, false, err); return; } SC_HANDLE service = OpenServiceW( scm, reinterpret_cast<LPCWSTR>(serviceName.utf16()), SERVICE_START); if (!service) { err = QStringLiteral("OpenService失败,错误码%1(权限不足或服务不存在)") .arg(GetLastError()); CloseServiceHandle(scm); emit startFinished(serviceName, false, err); return; } // StartServiceW的第三四个参数对应服务启动参数,传0/nullptr表示无参数 if (StartServiceW(service, 0, nullptr)) { ok = waitForState(serviceName, SERVICE_RUNNING, 30000); if (!ok) { err = QStringLiteral("服务已受理启动,但未在30秒内进入运行状态"); } } else { err = QStringLiteral("StartService失败,错误码%1").arg(GetLastError()); } CloseServiceHandle(service); CloseServiceHandle(scm); emit startFinished(serviceName, ok, err); }

停止服务的代码结构一样,区别在OpenServiceW传SERVICE_STOP权限,ControlService传SERVICE_CONTROL_STOP,轮询目标从SERVICE_RUNNING换成SERVICE_STOPPED。有一点要记住:ControlService成功只代表停止请求被接受了,服务可能还在STOP_PENDING里清理资源,最终状态要等轮询结果,别拿它当停止完成。

bool ServiceController::waitForState(const QString &serviceName, DWORD wantedState, DWORD waitHintMs) { // 轮询间隔取200ms,太短会白白消耗SCM句柄资源 QElapsedTimer timer; timer.start(); ServiceState st = StateError; do { st = queryStatus(serviceName); if (st == static_cast<ServiceState>(wantedState)) { return true; } if (st == StateError) { return false; } Sleep(200); } while (timer.elapsed() < static_cast<qint64>(waitHintMs)); return false; }

waitForState里我固定Sleep(200)轮询一次。更讲究的做法是先调QueryServiceStatus拿到SERVICE_STATUS里的dwWaitHint,用它做轮询间隔参考,但200毫秒在绝大多数场景下已经够用。30秒超时是给普通服务的默认值,如果你的服务初始化特别慢(比如启动时要加载模型或连数据库),把这个参数提到60秒或更长。注意Sleep在非界面线程用没问题,在界面线程里千万别这么写——这会直接造成界面无响应,下面第4章专门说这个。轮询过程中如果状态跳回STOPPED,说明服务进程初始化失败,通常事件日志里会有对应的DLL加载错误或超时记录,别傻等30秒。

3.4 安装与卸载:CreateService、DeleteService 与启动类型

安装服务是CreateServiceW的活,参数比前几个多。最容易错的是binPath:路径带空格时,CreateServiceW会把空格当成参数分隔符,所以路径里的空格要么用引号包住,要么先转义。下面这段代码把路径用双引号包了一层再传入。

bool ServiceController::installService(const QString &binPath, const QString &serviceName, const QString &displayName, bool autoStart) { SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CREATE_SERVICE); if (!scm) { qWarning() << "OpenSCManager failed:" << GetLastError(); return false; } // 带空格路径要包引号,否则CreateServiceW解析会出错 QString quoted = QStringLiteral("\"%1\"").arg(binPath); SC_HANDLE service = CreateServiceW( scm, reinterpret_cast<LPCWSTR>(serviceName.utf16()), reinterpret_cast<LPCWSTR>(displayName.utf16()), DELETE | SERVICE_START | SERVICE_STOP | SERVICE_QUERY_STATUS, SERVICE_WIN32_OWN_PROCESS, // 独占进程的服务 autoStart ? SERVICE_AUTO_START : SERVICE_DEMAND_START, // 自动启动还是手动启动 SERVICE_ERROR_NORMAL, // 启动失败时记日志即可 reinterpret_cast<LPCWSTR>(quoted.utf16()), nullptr, nullptr, nullptr, // LoadOrderGroup、TagId、Dependencies nullptr, nullptr // ServiceStartName、Password ); if (!service) { qWarning() << "CreateService failed:" << GetLastError(); CloseServiceHandle(scm); return false; } CloseServiceHandle(service); CloseServiceHandle(scm); return true; }

CreateServiceW最后两个参数是服务启动账户名和密码,传nullptr表示用LocalSystem账户,这是最省事的默认值。如果你要跑在指定域账户下,再往这里填对象名。SERVICE_ERROR_NORMAL表示服务启动失败时只写日志不弹提示,适合后台服务。卸载的逻辑对称:先OpenServiceW拿到SERVICE_STOP和SERVICE_DELETE权限,如果是运行状态先送停止请求,再DeleteServiceW,最后CloseServiceHandle。生产环境里我一般还会在卸载前调用ChangeServiceConfigW确认启动类型不是AUTO_START,防止卸载失败后服务又自动复活,这是血泪经验。

4. 把服务操作接进界面:异步刷新、状态展示与命令行小工具

4.1 为什么必须在工作线程里操作服务:从“界面卡顿”到“看着像qt崩溃”

把ServiceController直接塞进按钮槽函数是新手最常见的翻车现场。Windows服务枚举在物理机上通常不慢,一两百个服务全量查一次大概几百毫秒;但到了远程桌面、虚拟机或装了各种安全软件的机器上,单次QueryServiceStatus可能卡到几百毫秒,全量刷新就是几十秒。界面线程卡住后,Windows会给窗口标题加“未响应”,用户的第一感觉就是qt崩溃了。

正确做法是把耗时操作丢到工作线程,结果通过信号槽回传。这个模式本质上是生产者-消费者:界面是生产者,把“查状态”这个任务发出去;工作线程是消费者,执行任务后把结果塞回信号;界面槽函数拿到结果只做表格更新。Qt里最省事的写法是QtConcurrent::run结合lambda,注意lambda里不能直接碰任何UI对象,只能发信号。

// widget.cpp 中的刷新按钮槽 #include <QtConcurrent> void ServiceWidget::onRefreshClicked() { ui->btnRefresh->setEnabled(false); ui->lblStatus->setText(QStringLiteral("正在枚举服务...")); // 后台线程执行枚举,UI线程继续响应 QtConcurrent::run([this]() { QStringList names = m_enumerator.allServiceNames(); for (const QString &name : names) { int state = m_controller.queryStatus(name); // 信号跨线程回传到主线程的槽,自动进入事件循环 emit serviceStateFetched(name, state); } emit serviceEnumFinished(); }); }

这里emit的信号是跨线程的,Qt会自动用QueuedConnection把调用排到主线程事件循环里,UI不会被阻塞。m_enumerator.allServiceNames()是另一个枚举类,内部用EnumServicesStatusExW把系统里所有服务的键名、显示名、状态一次性拉回来,比逐个OpenService快一个数量级。如果你不想用QtConcurrent,也可以用QThread+moveToThread把ServiceController挪到子线程里,逻辑一样,只是样板代码更多。

4.2 用 QTableView 展示服务列表:状态列、刷新进度与大数据量处理

服务数量少时用QTableWidget最省事,行数几十个随便插。但真实机器上Windows服务加驱动服务动辄一两百个,QTableWidget每个单元格都是独立对象,逐行插入时界面会明显掉帧。我之前在工具里直接用QTableWidget,用户反馈滚动和刷新都卡,后来换成了QTableView+QAbstractTableModel,数据攒好以后一次reset模型,刷新过程顺滑很多。

// 继承QAbstractTableModel实现自定义模型,只示意关键部分 class ServiceTableModel : public QAbstractTableModel { Q_OBJECT public: struct RowData { QString name; QString displayName; int state = 0; }; void setRows(const QList<RowData> &rows) { beginResetModel(); m_rows = rows; endResetModel(); } int rowCount(const QModelIndex &parent) const override { return parent.isValid() ? 0 : m_rows.size(); } int columnCount(const QModelIndex &) const override { return 3; } QVariant data(const QModelIndex &index, int role) const override { if (role != Qt::DisplayRole) return QVariant(); const RowData &row = m_rows.at(index.row()); switch (index.column()) { case 0: return row.name; case 1: return row.displayName; case 2: return stateToText(row.state); default: return QVariant(); } } private: static QString stateToText(int state) { switch (state) { case 1: return QStringLiteral("已停止"); case 2: return QStringLiteral("启动中"); case 3: return QStringLiteral("停止中"); case 4: return QStringLiteral("运行中"); default: return QStringLiteral("未知"); } } QList<RowData> m_rows; };

模型把状态列翻译成中文文字,界面层只负责调用setRows刷新。这里有个实践经验:刷新频率要控制。服务状态不会每秒都变,我一般给刷新按钮加一个3秒的最小间隔,并在状态栏显示“上次刷新时间”。如果做成实时监控,用QTimer每隔几秒拉一次全量数据,也走同样的异步通道,别直接在主线程里Sleep。

4.3 顺手做成命令行工具:QCoreApplication + QCommandLineParser 复用同一个类

界面工具做出来以后,运维场景往往还想要一个命令行版:脚本里批处理服务启停,CI里检查服务状态。好在ServiceController不依赖任何UI,QCoreApplication环境下直接用,连界面那套异步都可以不要。这里用QCommandLineParser解析参数,做成和sc命令风格接近的小工具。

// main_cli.cpp #include <QCoreApplication> #include <QCommandLineParser> #include "servicecontroller.h" #include <cstdio> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName("svctl"); QCommandLineParser parser; parser.setApplicationDescription("Qt Windows Service Controller"); parser.addHelpOption(); parser.addPositionalArgument("action", "start | stop | status | install | remove"); parser.addPositionalArgument("service", "服务名"); parser.process(app); const QStringList args = parser.positionalArguments(); if (args.size() < 2) { parser.showHelp(1); return 1; } const QString action = args.at(0); const QString serviceName = args.at(1); ServiceController ctrl; if (action == QLatin1String("status")) { int state = ctrl.queryStatus(serviceName); printf("state=%d\n", state); return state == 4 ? 0 : 1; } if (action == QLatin1String("start")) { QObject::connect(&ctrl, &ServiceController::startFinished, [&app](const QString &name, bool ok, const QString &err) { printf("%s: %s\n", qPrintable(name), ok ? "started" : qPrintable(err)); app.quit(); }); ctrl.startService(serviceName); // 异步信号需要事件循环才能派发,quit后才会真正退出 return app.exec(); } // install/remove 同理,略 return 0; }

注意start分支最后要app.exec(),因为startFinished是异步信号,没有事件循环信号就不会派发,控制台程序容易在这翻车。命令行版和界面版共用同一个ServiceController类,服务操作逻辑只写一遍,两边行为完全一致。这样一个项目就同时覆盖了“有界面的服务管理器”和“脚本里能跑的服务控制命令”,正好对应搜索里经常一起出现的QT命令行工具需求。

5. 操作Windows服务最容易翻车的地方:权限、状态轮询与错误码排查

5.1 提权和编译链接的坑

现象:程序编译通过,运行时OpenSCManager返回空句柄,GetLastError()是5。原因:exe没有UAC清单,进程在普通用户令牌下运行,SCM拒绝了连接。解决:按2.3节的manifest加requireAdministrator,重新编译后右键属性里能看到“以管理员身份运行”默认勾选。

现象:编译时报一串依赖错误,比如报错信息里带着qt/5.15.2/msvc2019_64/include/qtwidgets这类路径,或者链接时一堆未解析的外部符号。原因:pro文件里QT模块没写全。比如类里用了QApplication,pro里却只写了QT += core gui,少写了widgets;也有人手滑在pro里重定义了QT变量,导致Qt头文件路径全乱。解决:按实际用到的模块写准:

QT += core gui widgets CONFIG += c++17

如果还报这个错,clean之后重新qmake一次,再不行检查编译器套件是不是和Qt库的位数一致(msvc2019_64就用64位编译器,别用32位的)。

5.2 服务名、状态时机和启动失败错误码的坑

现象:界面上明明看到服务叫“Windows Update”,OpenServiceW却返回错误码1060,服务不存在。原因:OpenServiceW第二参数是服务名,也就是注册表HKLM\SYSTEM\CurrentControlSet\Services\下面的键名。Windows Update的键名是wuauserv,显示名才是“Windows Update”。SCM API里绝大多数函数认的都是键名。解决:枚举的时候把键名和DisplayName一起拉回来,在界面上显示显示名,操作时传键名;或者提供搜索框,用户输入显示名也能匹配到键名。

现象:StartServiceW明明返回了成功,马上queryStatus却还是SERVICE_STOPPED,再等两秒就变成RUNNING。原因:服务启动是异步两段式的,服务进程要先被SCM拉起来,进入入口点,再自己调用SetServiceStatus(SERVICE_RUNNING)。StartServiceW成功只代表“SCM受理了请求”,不代表服务已经跑起来。解决:用3.3节的waitForState轮询,别相信StartService的返回值。界面里最好把按钮状态设成禁用,等最终信号回来再恢复,不然用户会以为点了没反应。

现象:服务启动失败,错误码1053(服务没有及时响应启动请求)或1079(为服务指定的账户与运行在其他服务上的账户不同)。这两个错误码在Windows日志里非常常见。原因:1053多数是服务程序在入口点没及时调用StartServiceCtrlDispatcher,或者初始化动作太重,SCM等不到SERVICE_START_PENDING更新就判定超时;1079则是服务属性的“登录”页里账户配置冲突,比如原本LocalSystem的服务被改成了某个用户账户,而这个账户和别的服务不一致。解决:1053的根治办法是服务入口第一时间注册Control Handler并SetServiceStatus(SERVICE_START_PENDING, 8000),把重活放到工作线程里做;1079在服务属性-登录里把登录身份改回Local System,如果确实要用指定账户,保证所有相关服务都填同一个账户名。

6. 把Qt程序本身做成Windows服务:Session 0、ServiceMain 与验证手段

6.1 QApplication 在服务里是坑,QCoreApplication 才是正路

有一段代码把我自己坑过:把一个带QWidget界面的程序做成Windows服务,以为服务启动后能弹个窗口出来,结果服务跑在Session 0里,和用户桌面完全隔离,界面根本看不见。Windows服务不该有UI,Qt侧也早有对应方案——服务端程序用QCoreApplication就够了,界面丢给一个配套的客户端工具去操作。

6.2 ServiceMain 回调 + QCoreApplication 的最小拼法

服务程序入口不是main直接跑逻辑,而是把一组SERVICE_TABLE_ENTRY交给StartServiceCtrlDispatcherW,SCM为每个服务启动一个线程回调ServiceMain。在ServiceMain里把QCoreApplication和定时器拉起来,就是一个能响应SCM生命周期的最小服务。

#include <QCoreApplication> #include <QTimer> #include <windows.h> #include <atomic> #include <thread> std::atomic<bool> g_stopRequested{false}; void WINAPI ServiceCtrlHandler(DWORD control) { if (control == SERVICE_CONTROL_STOP) { g_stopRequested = true; // 通知业务线程退出 } } void WINAPI ServiceMain(DWORD, LPWSTR*) { g_stopRequested = false; SERVICE_STATUS status{}; status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; status.dwCurrentState = SERVICE_START_PENDING; status.dwWaitHint = 8000; SERVICE_STATUS_HANDLE h = RegisterServiceCtrlHandlerW(L"QtServiceDemo", ServiceCtrlHandler); SetServiceStatus(h, &status); // 业务逻辑放到独立线程,QCoreApplication 在线程里跑事件循环 std::thread worker([]() { int argc = 1; char arg0[] = "qt-service"; char* argv[] = { arg0, nullptr }; QCoreApplication app(argc, argv); QTimer timer; timer.setInterval(5000); QObject::connect(&timer, &QTimer::timeout, []() { if (g_stopRequested) QCoreApplication::quit(); }); timer.start(); app.exec(); }); status.dwCurrentState = SERVICE_RUNNING; SetServiceStatus(h, &status); worker.join(); status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus(h, &status); } int main(int argc, char *argv[]) { SERVICE_TABLE_ENTRYW table[] = { { L"QtServiceDemo", ServiceMain }, { nullptr, nullptr } }; // 直接从命令行启动时StartServiceCtrlDispatcher会失败,用于调试 return StartServiceCtrlDispatcherW(table) ? 0 : -1; }

关键参数:SERVICE_TABLE_ENTRYW里的服务名必须和安装时CreateServiceW传的名字一致,不一致SCM会拒绝;SERVICE_START_PENDING的dwWaitHint设为8000毫秒,给QCoreApplication和定时器初始化留时间;业务用QTimer轮询g_stopRequested,收到SERVICE_CONTROL_STOP后自动退事件循环。调试时从命令行直接跑这个程序会返回-1,因为StartServiceCtrlDispatcher只能被SCM调用,这时候不是代码有bug,是运行方式不对。

6.3 验证手段与收尾

部署完成后用sc命令验证最直接:sc query QtServiceDemo看状态是不是RUNNING,sc stop然后sc start走一遍启停,注册表里的ImagePath确认没有空格转义问题。日志别用qDebug,服务没有控制台,写文件最靠谱;我会在启动和停止的地方各写一行带时间戳的记录,排错时能立刻定位到是SCM没拉起进程,还是事件循环自己退了。

这套封装我前前后后改了三个项目才稳定下来,最大的教训是别信懒人写法:QProcess临时用可以,做正式工具必须走SCM API;状态一定要轮询,显示名一定要和服务名分清。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询