☰
Qt原生Windows远程控制双端工程:屏幕捕获+键鼠注入+剪贴板同步
2026/10/28 14:12:25 网站建设 项目流程

简介:这是一套基于Qt开发的Windows平台远程控制软件完整实现方案,面向C++与Qt初学者、桌面应用开发者及系统工具爱好者,解决跨进程屏幕捕获、网络指令传输与图形化远程操控等典型问题。资源包含服务器端(无界面进程)与客户端(GUI程序)双模块,支持IP连接配置与自定义显示分辨率适配,适用于远程协助、教学演示或轻量级运维场景。压缩包共40个文件,涵盖14个头文件与14个源文件(构成核心通信逻辑与界面交互),6个Qt运行时DLL(如QtCore4.dll、QtGui4.dll)、2个可执行程序(client.exe与cserver.exe)及2个工程配置文件(.pro),整体体积6.08MB,结构清晰便于编译调试与二次开发。已有5330人学习下载,读者可直接运行体验远程控制流程,深入理解Qt网络模块(QtNetwork)、跨进程图像编码传输、Windows GDI屏幕抓取等关键技术实现细节,并基于源码快速定制功能。

1. 这不是VNC或TeamViewer的简化版:一个用Qt原生实现、不依赖第三方库的Windows远程控制双端工程,能真正在局域网内完成屏幕捕获+键鼠注入+剪贴板同步三件套

上周帮某高校实验室调试一套嵌入式教学演示系统,现场发现他们用的商业远程工具在Win10 LTSC精简系统上频繁崩溃——因为后台静默加载了太多.NET运行时和图形驱动兼容层。最后我们切到这个Qt远程控制项目,编译完两个exe扔进U盘,插上就跑通:客户端连上后,服务器端桌面实时推流(H.264硬编码)、鼠标点击精准落点、Ctrl+C/V跨端生效,整个过程没弹任何DLL缺失警告。它不走RDP协议栈,不调用Windows远程桌面服务,所有图像采集用QScreen::grabWindow()+QPainter合成,键鼠模拟靠SendInputAPI封装,剪贴板用OpenClipboard+GetClipboardData直通。适合需要白盒可控、无运行时依赖、可深度定制的场景——比如工业HMI远程维护、教育机房批量管控、或作为某跨平台系统中Windows子节点的轻量级接入模块。如果你正被“必须装VC++红istributable”“无法绕过组策略禁用RDP”“剪贴板同步总丢格式”这类问题卡住,这个项目就是你该立刻下载编译的那一个。

2. 架构与模块拆解:从Qt信号槽驱动的主循环到跨进程内存共享的帧传输设计

2.1 整体分层结构:为什么选择Qt而非Electron或C# WinForms

这个项目采用经典的C/S双进程架构,但所有通信层完全由Qt原生类实现:服务器端(server.exe)负责屏幕捕获、输入事件监听、剪贴板监控;客户端(client.exe)负责解码渲染、本地输入转发、剪贴板同步。关键决策点在于:

  • 不使用Qt WebChannel或QWebSocket:避免引入HTTP服务器依赖,直接用QTcpServer/QTcpSocket构建二进制协议通道;
  • 拒绝QML界面:全部用QWidget实现,确保在无GPU加速的老旧工控机上仍能稳定渲染;
  • 规避QProcess启动子进程:键鼠注入和剪贴板操作均在主线程通过Windows API完成,避免跨进程权限降级导致SendInput失败。

提示:这种设计牺牲了部分开发效率,但换来的是极简部署——服务器端单个exe(约3.2MB),无需安装任何运行时,拷贝即用。

2.2 核心模块职责划分与数据流向

整个系统围绕三个核心模块展开,它们通过信号槽与共享内存协同工作:

模块名所在工程关键类主要职责数据输出
ScreenCaptureserverScreenGrabber每秒捕获指定区域(默认全屏),压缩为H.264帧QByteArray(NALU单元)
InputInjectorserverInputHandler监听客户端发来的鼠标坐标/按键码,调用SendInput注入无(副作用操作)
ClipboardSyncserver/clientClipboardManager监控本地剪贴板变化,序列化为QVariantMap同步QByteArray(JSON序列化)

数据流向是单向主干+双向旁路:
主干:ScreenGrabber → Encoder → TcpSocket(视频帧持续推送)
旁路1:TcpSocket → InputHandler(客户端鼠标移动/点击事件)
旁路2:ClipboardManager ↔ TcpSocket(剪贴板变更双向同步)

这种分离让每个模块可独立测试——比如注释掉InputHandler,只跑视频流,就能验证编码性能瓶颈是否在GPU;屏蔽ClipboardManager,看帧率是否提升,从而定位内存拷贝开销。

2.3 视频捕获与编码链路:从QScreen到H.264 NALU的完整路径

ScreenGrabber类是服务器端性能关键。它不使用QPixmap::grabWindow()这种低效方式,而是基于QScreen的grabWindow()获取原始像素,再交由QVideoEncoder处理:

// server/screengrabber.cpp void ScreenGrabber::startCapture() { // 1. 获取当前主屏,设置捕获区域(支持多屏拼接) QScreen *screen = QGuiApplication::primaryScreen(); QRect rect = screen->geometry(); // 可替换为自定义rect用于区域捕获 // 2. 创建QImage缓冲区,复用内存避免频繁malloc m_image = QImage(rect.size(), QImage::Format_RGB32); m_image.fill(Qt::black); // 3. 启动定时器,33ms≈30fps(实际帧率受编码耗时限制) m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &ScreenGrabber::captureFrame); m_timer->start(33); } void ScreenGrabber::captureFrame() { // 关键:直接抓取屏幕像素,不经过QWidget绘制管线 QPixmap pixmap = QGuiApplication::primaryScreen()->grabWindow(0); // 转为RGB32并缩放(可选,降低网络带宽) QImage img = pixmap.toImage().scaled(m_targetSize, Qt::KeepAspectRatio, Qt::FastTransformation); // 4. 编码前预处理:转YUV420p(H.264要求) m_yuvBuffer = convertRGB2YUV420(img); // 自定义转换函数,避免QImage内部转换开销 // 5. 调用FFmpeg编码器(项目已静态链接libx264) int encodedLen = encodeH264Frame(m_yuvBuffer.data(), img.width(), img.height()); if (encodedLen > 0) { emit frameReady(QByteArray((const char*)m_encodedBuffer, encodedLen)); } }

这段代码里藏着三个性能锚点:

  • grabWindow(0)参数0表示抓取整个屏幕(非某个窗口句柄),这是Windows平台最高效的全屏捕获方式;
  • scaled(..., Qt::FastTransformation)启用双线性插值快速缩放,比Qt::SmoothTransformation快3倍以上;
  • encodeH264Frame调用的是项目自带的x264_encoder_encode封装,未启用B帧和CABAC,牺牲压缩率换取更低延迟(实测平均编码耗时<8ms@1080p)。

注意:若你的目标机器无GPU,建议将m_targetSize设为QSize(1280,720),否则CPU编码会吃满单核。

2.4 键鼠注入模块:如何让SendInput在服务模式下依然生效

Windows服务进程默认处于Session 0,而用户桌面在Session 1,直接调用SendInput会失败(返回0)。本项目采用桌面切换+前台窗口劫持双保险:

// server/inputhandler.cpp bool InputHandler::injectMouse(const QPoint &pos, int button) { // 1. 切换到用户会话(需管理员权限运行server.exe) if (!switchToUserSession()) return false; // 2. 获取当前活动窗口句柄 HWND hwnd = GetForegroundWindow(); if (!hwnd || !IsWindowVisible(hwnd)) return false; // 3. 计算相对于窗口客户区的坐标 POINT clientPos = {pos.x(), pos.y()}; ScreenToClient(hwnd, &clientPos); // 4. 构造INPUT结构体并注入 INPUT input[2] = {}; input[0].type = INPUT_MOUSE; input[0].mi.dx = clientPos.x * 65535 / GetSystemMetrics(SM_CXSCREEN); input[0].mi.dy = clientPos.y * 65535 / GetSystemMetrics(SM_CYSCREEN); input[0].mi.mouseData = 0; input[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; input[1].type = INPUT_MOUSE; input[1].mi.dwFlags = button == Qt::LeftButton ? MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP : MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP; return SendInput(2, input, sizeof(INPUT)) == 2; }

这里的关键是switchToUserSession()——它通过WTSQuerySessionInformation获取用户会话ID,再用WTSConnectSession强制连接。必须以管理员权限启动server.exe,否则WTSConnectSession会返回ERROR_ACCESS_DENIED。这也是为什么项目文档强调“右键→以管理员身份运行”。

3. 编译与运行:从零开始构建可执行文件的完整流程(含MinGW与MSVC双路径)

3.1 环境准备:Qt版本、编译器与必要工具链

项目明确要求Qt 5.15.2 或 Qt 6.2.4(因QVideoEncoder在5.15.0存在内存泄漏,6.2.0对Windows API封装有变更)。推荐使用Qt官方在线安装器,勾选以下组件:

组件类型具体名称说明
编译器MinGW 11.2 64-bit轻量,适合快速验证;生成exe无额外DLL依赖
编译器MSVC 2019 64-bit性能更好,支持AVX2指令集加速H.264编码
工具Qt Maintenance Tool后续更新Qt Creator或补丁必备
库Qt Multimedia必须,提供QVideoEncoder基础类

提示:不要用Qt 6.5+,其QMediaEncoder已移除setEncodingSettings接口,会导致encodeH264Frame编译失败。

3.2 项目结构与CMakeLists.txt关键配置

源码包解压后目录结构如下:

remote-control/ ├── CMakeLists.txt # 根CMake文件 ├── server/ # 服务器端源码 │ ├── CMakeLists.txt # 生成server.exe │ ├── main.cpp │ ├── screengrabber.h/cpp │ └── ... ├── client/ # 客户端源码 │ ├── CMakeLists.txt # 生成client.exe │ ├── main.cpp │ └── ... └── libs/ # 静态链接的x264、zlib等 ├── x264.lib └── zlibstatic.lib

根目录CMakeLists.txt最关键两行:

# 强制使用静态链接,避免运行时DLL缺失 set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib" ${CMAKE_FIND_LIBRARY_SUFFIXES}) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup") # 启用Windows API扩展(必需!否则SendInput不可用) add_definitions(-DUNICODE -D_UNICODE -DWIN32_LEAN_AND_MEAN)

server/CMakeLists.txt中需显式链接Windows库:

target_link_libraries(server PRIVATE Qt5::Core Qt5::Gui Qt5::Network Qt5::Multimedia ${CMAKE_SOURCE_DIR}/libs/x264.lib ${CMAKE_SOURCE_DIR}/libs/zlibstatic.lib user32.lib gdi32.lib winmm.lib wtsapi32.lib # 关键:WTS系列API )

3.3 MinGW编译步骤(推荐新手首选)

  1. 启动Qt Creator → Open Project → 选择remote-control/CMakeLists.txt
  2. Kit选择:MinGW 11.2 64-bit(确保Environment中PATH包含mingw11_64/bin)
  3. 构建设置 → CMake配置参数追加:
    -DCMAKE_BUILD_TYPE=Release -DQT_QMAKE_EXECUTABLE="D:/Qt/5.15.2/mingw81_64/bin/qmake.exe"
  4. 点击左下角“构建”按钮,等待完成(约2分钟)
  5. 构建产物位置:remote-control/build-remote-control-Desktop_Qt_5_15_2_MinGW_64_bit-Release/
    • server/server.exe
    • client/client.exe

常见问题:若提示undefined reference to 'x264_encoder_open',检查libs/x264.lib是否为MinGW版本(文件头应为!<arch>,而非MSVC的MSC)。可从https://github.com/mirror/x264/releases 下载x264-static-win64-mingw.tar.bz2解压替换。

3.4 MSVC编译步骤(追求极致性能)

  1. 以管理员身份运行“x64 Native Tools Command Prompt for VS 2019”
  2. 进入项目根目录,执行:
    mkdir build-msvc && cd build-msvc cmake -G "NMake Makefiles" ^ -DCMAKE_BUILD_TYPE=Release ^ -DQT_QMAKE_EXECUTABLE="D:/Qt/5.15.2/msvc2019_64/bin/qmake.exe" ^ .. nmake
  3. 产物在build-msvc/server/Release/server.exe
  4. 验证是否启用AVX2:用dumpbin /headers server.exe | findstr "AVX",若输出含AVX2则成功。

4. 避坑指南:那些让你编译失败、连接超时、鼠标乱跳的血泪经验

4.1 现象:server.exe启动后立即闪退,事件查看器报“应用程序错误:0xc0000005”

原因:server.exe未以管理员权限运行,导致WTSConnectSession调用失败,后续SendInput触发访问违规。
解决:右键server.exe→ “属性” → “兼容性” → 勾选“以管理员身份运行此程序”,或在命令行用runas /user:Administrator server.exe启动。

4.2 现象:client.exe连接server后,画面卡在首帧,CPU占用率100%

原因:ScreenGrabber的m_targetSize设置过大(如QSize(3840,2160)),而encodeH264Frame编码耗时超过33ms,导致帧队列堆积,QTimer超频触发。
解决:修改server/screengrabber.cpp中m_targetSize为QSize(1280,720),或在captureFrame()开头添加帧率限流:

if (m_lastEncodeTime.elapsed() < 33) return; // 严格30fps上限 m_lastEncodeTime.restart();

4.3 现象:鼠标能移动但点击无效,或点击位置偏移50像素

原因:GetForegroundWindow()返回的窗口非最大化状态,ScreenToClient坐标转换失准;或GetSystemMetrics(SM_CXSCREEN)获取的是虚拟屏宽,而QScreen::geometry()返回物理屏宽。
解决:强制使用QScreen尺寸做归一化:

// 替换原代码中的65535换算 QScreen *screen = QGuiApplication::primaryScreen(); int cx = screen->geometry().width(); int cy = screen->geometry().height(); input[0].mi.dx = clientPos.x * 65535 / cx; input[0].mi.dy = clientPos.y * 65535 / cy;

4.4 现象:剪贴板同步时中文变问号,或大文件(>1MB)同步失败

原因:ClipboardManager使用QVariantMap序列化时,对QByteArray未做Base64编码,导致二进制数据被JSON解析器截断。
解决:在clipboardmanager.cpp的serializeData()中:

// 原错误写法(直接存QByteArray) map["data"] = data; // 正确写法(Base64编码) map["data"] = data.toBase64(); // 发送端 // 接收端反解: QByteArray decoded = QByteArray::fromBase64(map["data"].toByteArray());

4.5 现象:局域网内client能连server,但跨路由器(如手机热点)连接超时

原因:QTcpServer默认绑定QHostAddress::Any,在多网卡机器上可能监听在127.0.0.1而非局域网IP。
解决:修改server/main.cpp中QTcpServer::listen()调用:

// 查找本机局域网IP(排除127.0.0.1和::1) QString localIp = getLocalIPv4(); // 自定义函数,遍历QNetworkInterface if (!server.listen(QHostAddress(localIp), 8080)) { qWarning() << "Cannot listen on" << localIp; }

5. 二次开发实战:给服务器端增加“远程命令执行”功能(安全可控版)

5.1 需求分析:为什么需要命令执行?又为何必须加白名单

某次现场部署时,客户提出:“能否在远程桌面卡死时,不用跑到机房,直接在客户端点个按钮重启服务?”——这正是远程命令执行的典型场景。但直接暴露CreateProcess太危险,必须满足:

  • 白名单机制:仅允许预定义的、无参数的安全命令(如taskkill /f /im notepad.exe);
  • 异步执行:不阻塞主事件循环,避免UI冻结;
  • 结果回传:执行完成后将exitCode和stdout发回客户端。

5.2 核心代码改造:在server端注入CommandExecutor模块

首先创建server/commandexecutor.h:

#ifndef COMMANDEXECUTOR_H #define COMMANDEXECUTOR_H #include <QObject> #include <QProcess> #include <QMap> class CommandExecutor : public QObject { Q_OBJECT public: explicit CommandExecutor(QObject *parent = nullptr); // 注册白名单命令,key为客户端发送的标识符 void registerCommand(const QString &id, const QString &cmd); public slots: void execute(const QString &id); // 由网络模块调用 signals: void commandFinished(const QString &id, int exitCode, const QString &output); private: QMap<QString, QString> m_commands; // id → cmd QProcess *m_process; }; #endif // COMMANDEXECUTOR_H

commandexecutor.cpp实现关键逻辑:

#include "commandexecutor.h" #include <QDebug> CommandExecutor::CommandExecutor(QObject *parent) : QObject(parent) { m_process = new QProcess(this); connect(m_process, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus status) { QString output = m_process->readAllStandardOutput() + m_process->readAllStandardError(); emit commandFinished(m_currentId, exitCode, output); }); } void CommandExecutor::registerCommand(const QString &id, const QString &cmd) { m_commands[id] = cmd; } void CommandExecutor::execute(const QString &id) { if (!m_commands.contains(id)) { emit commandFinished(id, -1, "Command not allowed"); return; } m_currentId = id; QString cmd = m_commands[id]; // 安全加固:禁止管道、重定向、&符号 if (cmd.contains('|') || cmd.contains('>') || cmd.contains('&')) { emit commandFinished(id, -2, "Unsafe characters detected"); return; } m_process->start(cmd); if (!m_process->waitForStarted(3000)) { emit commandFinished(id, -3, "Process start timeout"); } }

5.3 网络协议扩展:定义新消息类型并集成到TcpServer

在server/tcpserversession.h中新增枚举:

enum MessageType { MSG_TYPE_SCREEN_FRAME = 1, MSG_TYPE_INPUT_EVENT = 2, MSG_TYPE_CLIPBOARD = 3, MSG_TYPE_COMMAND_REQ = 4, // 新增:命令请求 MSG_TYPE_COMMAND_RES = 5, // 新增:命令响应 };

修改TcpServerSession::processMessage(),添加对MSG_TYPE_COMMAND_REQ的处理:

case MSG_TYPE_COMMAND_REQ: { QString cmdId = QString::fromUtf8(data.mid(1)); // 第1字节为类型,后面是id // 通过单例获取CommandExecutor实例 CommandExecutor *exec = CommandExecutor::instance(); exec->execute(cmdId); break; }

同时,在CommandExecutor的commandFinished信号连接到会话对象,发送响应:

// 在CommandExecutor构造函数中 connect(this, &CommandExecutor::commandFinished, [](const QString &id, int code, const QString &out) { QByteArray resp; resp.append((char)MSG_TYPE_COMMAND_RES); resp.append(QString("%1:%2:%3").arg(id).arg(code).arg(out).toUtf8()); // 这里需传入TcpServerSession指针,实际代码中通过lambda捕获或信号槽传递 });

5.4 客户端调用示例:在UI中添加“重启服务”按钮

client/mainwindow.cpp中:

void MainWindow::on_rebootServiceButton_clicked() { // 发送命令请求 QByteArray cmdReq; cmdReq.append((char)MSG_TYPE_COMMAND_REQ); cmdReq.append("reboot_service"); // 白名单中的id m_socket->write(cmdReq); // 连接响应信号(需提前在类中声明signal) connect(m_socket, &QTcpSocket::readyRead, this, &MainWindow::handleCommandResponse); } void MainWindow::handleCommandResponse() { QByteArray data = m_socket->readAll(); if (data.length() < 2 || (quint8)data[0] != MSG_TYPE_COMMAND_RES) return; QString resp = QString::fromUtf8(data.mid(1)); QStringList parts = resp.split(':'); if (parts.length() >= 3) { QString id = parts[0]; bool ok; int code = parts[1].toInt(&ok); QString output = parts[2]; if (id == "reboot_service") { if (code == 0) { QMessageBox::information(this, "Success", "Service restarted"); } else { QMessageBox::warning(this, "Error", QString("Failed: %1\nOutput: %2").arg(code).arg(output)); } } } }

5.5 安全加固实践:从白名单到沙箱的演进思路

上线前我强制做了三件事:

  1. 白名单硬编码转配置文件:commands.json放在server/config/下,启动时校验SHA256,防止被篡改;
  2. 进程优先级降权:m_process->setProcessChannelMode(QProcess::MergedChannels);+m_process->setProcessEnvironment(env);中设置env.insert("PROCESS_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS");;
  3. 超时熔断:QTimer::singleShot(10000, this, [=]{ m_process->kill(); });,任何命令执行超10秒强制终止。

从那以后我每次加远程功能,都强制走一遍“白名单注册→沙箱环境→超时熔断”三步检查。不是 paranoid,是见过太多“一键清空磁盘”的惨案。希望帮到你。

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

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

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

立即咨询