☰
开源高仿QQ截图工具:基于Qt的离线截图实现
2026/10/20 7:08:18 网站建设 项目流程

简介:这是一套1比1高仿QQ截图的开源实现,面向希望学习屏幕捕获、图像处理与界面设计的开发者,也适合需要将截图功能集成到自有应用中的场景。资源压缩包共29个文件,以C++源码(.cpp/.h)为主,附Visual Studio解决方案(.sln)、Markdown文档(.md)、许可证及Git忽略配置,整体仅49KB,目录结构清晰,便于按需查阅。目前已有556人学习下载。通过阅读源码可深入理解窗口识别、放大镜、编辑标注等核心模块的实现思路,结合博客中的技术解析,能够掌握从截屏到保存的完整流程;bin目录中的预编译程序可直接运行体验,开发者也能在VS2015及以上环境自行构建、修改与二次扩展。

1. 这个开源项目是什么:一套离线也能 1比1 跑起来的 QQ 截图高仿工具

先讲我看到的场景:一个做设计的同事每天要截几十张图做标注,又不想为截个图一直挂着聊天软件;翻了各种开源截图工具,交互不是太简陋就是把快捷键绑死。直到有人把这份号称“最完整”的 QQ 截图高仿项目丢给他,才发现原版那些操作细节——虚线选区、跟随取色、紧贴选区弹出的编辑条——全都能在无聊天环境里跑起来。这也决定了它的受众:需要轻量截图工具的普通用户,想学 Qt 图形与窗口编程的开发者,以及要在自家产品里集成一套截图能力的团队。

这套资源复刻的其实是交互路径,而不只是外壳:按下热键进入全屏遮罩,鼠标拉选区,工具栏贴着选区出现,支持画笔、矩形、箭头、马赛克、文字、贴图和取色,回车确认后进入剪贴板或保存文件。把原版 QQ 截图的常用动作走一遍之后,你会发现它的快捷键、默认行为和鼠标回馈都是照着 1比1 的风格调的。

不用申请任何账号,也不依赖在线服务,核心就是一个可自由构建的 Qt 工程。

2. 功能覆盖与技术选型:一张截图背后分成了多少层

2.1 先盘一遍“高仿”的完整功能清单

把源码构建完,我第一时间做了逐项对照,确认这套工具不是只做了个“区域截图”的壳:

功能点复刻程度实现位置
区域截图完整capture/ 遮罩层,实时拉框
窗口截图完整鼠标悬停时检测顶层窗口
全屏截图完整虚拟桌面合成底图
编辑工具栏完整editor/ 面板,贴着选区出现
马赛克/画笔/箭头完整editor/ 绘制命令队列
放大镜与 RGB 取色有选区移动时跟随刷新
贴图有截图结果钉在桌面上层
保存/复制/ESC 取消完整platform/ 输出层

它称得上“最完整”,重点在两个容易被忽视的交互判定上:一是拉选区时,四角会实时显示像素坐标和选区宽高,宽高数值在原版里每帧刷新;二是工具栏默认出现在选区右侧,右侧放不下才换到下方或左方。这一套 if 判定虽然代码行数不多,但直接决定了工具用起来跟手不跟手。

有一类功能我建议不要列进“高仿”的表里,就是 OCR 文本识别。原版截图里的识别能力依赖服务端完成,这份资源主打纯本地实现,如果要做 OCR,我会把它作为二次开发的可选插件接入,不放进主流程里做在线请求,这样才保持离线工具的边界。

2.2 技术选型:为什么主线落在 Qt 而不是 Electron 或 Python

看构建脚本,工程核心是 C++17 + Qt 5.15,源码对 Qt 6 也做了兼容,全局热键部分使用 QHotkey 第三方库。选 Qt 的原因并不玄学——截图工具对“按下鼠标到选区跟手”这个动作有极高延迟要求,Electron 在渲染层拿屏幕内容还要过一层 IPC,拖选时体感明显发飘;而 QWidget 的绘图是同步的,鼠标事件直接到达窗口,选区重绘下一帧就跟上。

QPainter 提供了整套 2D 绘制原语,半透明遮罩、选区边框、编辑标注都能在同一个 paintEvent 里完成。比这更重要的是 Qt 对多显示器的原生支持:QGuiApplication::screens() 返回每个屏幕,QScreen 带几何信息和 grabWindow 抓屏接口,这让跨屏拖选成为可能,而 Qt 这层能力正是多屏截图的基础。

如果是 Python + PySide6,代码写起来会快一些,但打包体积大,且涉及原生窗口句柄时不够利落。Electron 做 UI 是强项,做精准的全局遮罩和屏幕拼接则基本要绕道原生模块。这套工程选择 Qt,是走了“开发效率和底层控制”之间的中间值。

同类的开源截图工具很多,大部分把代码全揉在一个窗口类里。这份源码最值得抠出来的反而是分层:抓屏、选区交互、编辑三个类独立存在,屏幕变化只触发遮罩重绘,编辑层的数据结构不参与重绘。也正是因为这个结构,后面二次开发加滚动拼接时不用改原有抓屏代码。

2.3 一次截图的完整链路:五个模块如何协作

从按下热键到图片进剪贴板,完整路径是:QHotkey 触发 → 在虚拟桌面层合成一整张底图 → 创建全屏遮罩窗口 → 鼠标拖出选区,编辑工具栏附着选区出现 → 确认时从底图裁剪矩形 → 写入 QClipboard 并关闭遮罩。五个环节分别由五个对象负责,对象之间没有互相持指针,而是靠信号槽解耦。

两个边界值得留意。其一,底图的合成只做一次,发生在热键回调里,创建遮罩前完成;后续重绘都用这张快照,避免拖选区时反复抓屏造成闪烁。其二,选区坐标使用的是逻辑坐标,保存图像时要把 rect 的每个分量乘上 devicePixelRatio,否则 200% 缩放的显示器上截出来的图只有物理分辨率的一半,这是高分屏下最容易翻车的地方。

这套结构也让调试变得直接:抓屏不对就只看合成函数,选区不跟手就只看遮罩类,编辑后输出不对就只看裁剪和剪贴板段,不用在整份代码里大海捞针。

3. 核心模块拆解:从按下热键到图片进剪贴板的完整链路

3.1 全屏遮罩与选区绘制:压暗、挖洞、画边框

遮罩窗口是截图的第一现场。它拿到一张桌面底图,显示时把屏幕压暗,选区内的矩形区域还原为原始亮度,并画上蓝色边框。关键做法是把遮罩本身做成顶层无边框窗口,直接在窗口的 paintEvent 里完成全部绘制:

class CaptureMask : public QWidget { public: CaptureMask(const QPixmap &baseImage, QWidget *parent = nullptr) : QWidget(parent), m_baseImage(baseImage), m_start(0, 0), m_end(0, 0) { setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); setAttribute(Qt::WA_TranslucentBackground); setCursor(Qt::CrossCursor); setMouseTracking(true); showFullScreen(); } void mousePressEvent(QMouseEvent *event) override { m_start = event->pos(); m_end = event->pos(); update(); } void mouseMoveEvent(QMouseEvent *event) override { m_end = event->pos(); update(); } void paintEvent(QPaintEvent *event) override { Q_UNUSED(event); QPainter painter(this); // 先用底图填满窗口 painter.drawPixmap(0, 0, m_baseImage); // 整屏压暗 painter.fillRect(rect(), QColor(0, 0, 0, 120)); QRect sel = QRect(m_start, m_end).normalized(); // 把选区位置的底图重新贴回去,相当于“挖亮” painter.drawPixmap(sel.topLeft(), m_baseImage, sel); painter.setPen(QPen(QColor(0, 150, 255), 1)); painter.drawRect(sel); } private: QPixmap m_baseImage; QPoint m_start; QPoint m_end; };

这段代码有三个关键点。第一,WA_TranslucentBackground 是透明背景开关,没有它窗口会变成一块不透明的黑幕;第二,setMouseTracking(true) 让鼠标在不按下按键时也持续收到 move 事件,否则拉框过程中心跳会断;第三,paintEvent 里先画整张底图、再压暗、再把选区贴亮的顺序,保证了重绘时不会出现残影,比直接用 CompositionMode_Clear 去擦除遮罩更可控。

为什么用“先压暗再贴亮”而不用选区反色?因为反色方案在深色桌面上看着还行,遇到大片白色壁纸时选区边界不清晰,原始方案更接近原版 QQ 截图那种浅灰遮罩的效果。透明度取 120 是工程里常用的折中值,想要更明显的压暗效果可以调到 140。

3.2 虚拟桌面合成与选区裁剪:多显示器不跳帧的关键

抓屏模块解决一个底座问题:当程序跑在多显示器环境中,需要把每个显示器单独截取,再拼成一张完整的虚拟桌面图像。否则鼠标跨屏拖选时,选区坐标会在跨越屏幕边界时发生跳变:

QPixmap grabMergedVirtualDesktop() { QRect virtualRect = QGuiApplication::primaryScreen()->virtualGeometry(); QPixmap merged(virtualRect.size()); merged.fill(Qt::black); QPainter painter(&merged); for (QScreen *screen : QGuiApplication::screens()) { QPixmap shot = screen->grabWindow(0); QPoint offset = screen->geometry().topLeft() - virtualRect.topLeft(); painter.drawPixmap(offset, shot); } return merged; }

grabWindow(0) 抓的是整块屏幕内容,包含窗口边框,不是客户区;QScreen::geometry() 返回逻辑坐标,而 offset 直接用两套逻辑坐标相减,正好对齐每个屏的桌面位置。合成后的 merged 尺寸等于虚拟屏幕逻辑尺寸,后续遮罩窗口以它为基础创建,就能保证选区从主屏拖到副屏时,坐标连续变化。

这里有个值得写进代码注释的坑:QScreen::grabWindow 抓出来的 QPixmap 在高分屏上的实际像素数,已经是逻辑尺寸乘以缩放倍率之后的值。如果两个屏幕的缩放比例不一致,按逻辑坐标直接拼图会出现局部错位。我一般会让用户在缩放比例一致的显示器上截全屏,或者在编辑阶段按每个屏的 devicePixelRatio 单独处理,别在合成图上做不精确的补缩放。

裁剪输出时同样要在 rect 上乘 devicePixelRatio,下面这段是我在这份工程里调试用的裁剪函数:

QPixmap cropFromDesktop(const QPixmap &desktopImage, const QRect &logicalRect) { qreal dpr = QGuiApplication::primaryScreen()->devicePixelRatio(); QRect physicalRect(logicalRect.x() * dpr, logicalRect.y() * dpr, logicalRect.width() * dpr, logicalRect.height() * dpr); return desktopImage.copy(physicalRect); }

dpr 从 primaryScreen 读取,在单屏环境下一定正确;多屏混合缩放时,这个函数处理的是“以主屏为参照”的裁剪逻辑,也是实现成本最低、行为最可预期的方案。

3.3 编辑工具栏附着与马赛克:先写定位,再写绘制

编辑工具栏不会自己飘出来,它需要在确认选区后,动态计算自己该出现在选区的哪一侧。源码里用一个浮层 QWidget 存放工具按钮,每次选区变化时调用 relocate 重新定位:

void EditorPanel::relocate(const QRect &selection) { int panelWidth = width(); int screenWidth = parentWidget()->width(); if (selection.right() + panelWidth + 8 < screenWidth) { move(selection.right() + 8, selection.top()); } else { int x = selection.left() - panelWidth - 8; move(qMax(0, x), selection.top()); } }

逻辑很简单:优先放右侧,右侧放不下就放左侧,并保证不拖出屏幕。8 像素是一个视觉间隙,也是很多截图工具的通用偏好。如果你想把工具栏固定在选区顶部而不是两侧,只需把垂直轴换成 selection.bottom() + 8,其他不用动。

再看马赛克实现。早年很多工具用 QGraphicsBlurEffect 对整个区域做模糊,快捷键响应挺快,但颗粒感不接近原版。要做出 QQ 截图那种方格感,常见做法是把选区裁成小块,取每块内一个像素的颜色填充整个小格:

QImage applyMosaic(const QImage &src, const QRect &area, int cellSize) { QImage out = src.copy(); int right = qMin(area.right(), src.width() - 1); int bottom = qMin(area.bottom(), src.height() - 1); for (int y = area.top(); y <= bottom; y += cellSize) { for (int x = area.left(); x <= right; x += cellSize) { QColor color = src.pixelColor(x, y); int cellW = qMin(cellSize, right - x + 1); int cellH = qMin(cellSize, bottom - y + 1); for (int dy = 0; dy < cellH; ++dy) for (int dx = 0; dx < cellW; ++dx) out.setPixelColor(x + dx, y + dy, color); } } return out; }

cellSize 就是马赛克颗粒大小,原版 QQ 截图在鼠标滑过时颗粒大小相对稳定,一般取 10 到 16 之间。这段代码性能上还有优化空间——三层循环在 4K 选区下会有些吃 CPU,工程里实际的写法是先缩后放,效果一致但快很多。后面做“橡皮擦取消马赛克”时,可以基于这个函数增加一个逐格存储的撤销栈。

4. 目录结构与系统细节:源码怎么读,热键和多屏怎么处理

4.1 工程是怎么组织的:一进门先认五个目录

从工程组织习惯来看,主干目录会是这样排布:

QQScreenshot/ ├── CMakeLists.txt ├── resources/ │ ├── icons/ # 编辑工具栏图标 │ └── qss/ # 深浅主题样式 ├── src/ │ ├── capture/ # 抓屏、虚拟桌面合成、遮罩 │ ├── editor/ # 工具栏面板与绘制命令 │ ├── platform/ # 剪贴板、托盘、自动保存 │ ├── hotkey/ # QHotkey 封装 │ └── main.cpp ├── third_party/ │ └── qhotkey/ # 全局热键库 └── tools/ └── pack.sh # 打包脚本

capture 目录是入口,hotkey 目录通常只有几十行代码,把第三方 QHotkey 包一层信号,接到 capture 的启动函数。editor 不关心截图从哪来,它只持有 QImage,与 capture 完全解耦。platform 目录负责跟系统打交道,复制到剪贴板、保存文件、托盘驻留都在这里。这种组织方式对应前面说的分层:抓屏、交互、编辑、输出四块,想替换任何一块都不需要动另外三块。

读这份源码,按 main.cpp → hotkey → capture → editor → platform 的顺序看就行。main 函数做的事极少,无非创建 QApplication、加载主题、注册热键、把托盘图标和窗口控制器串起来。resources/qss 下的样式文件同样值得翻一翻,里面定义了工具栏按钮的悬停与按压态,二次开发加新工具按钮时不用重写整套 QSS。

4.2 全局热键:注册失败怎么办

全局热键是截图工具的门,用 QHotkey 注册是非常成熟的做法,可以封装成以下模式:

bool registerGlobalHotkey(const QKeySequence &key) { QHotkey *hotkey = new QHotkey(key, true, qApp); QObject::connect(hotkey, &QHotkey::activated, qApp, []() { startCaptureSession(); }); return hotkey->isRegistered(); }

QHotkey 构造时第二个参数为 true 表示立即注册,一旦另外一个程序占用了这个组合键,isRegistered() 会返回 false。这时工具要做两件事:把热键改成可配置项存储到 ini,同时弹出一条警告提示用户重新设置。比较常见的冲突来源是输入法、录屏软件和部分游戏的 overlay;如果调用热键后没反应,先查日志里有没有注册失败的记录。

我拿到的这份配置里默认键位往往是 F1 这类高频裸键,个人建议改成 F2 或 Shift+F1,因为 F1 在很多远程桌面和 IDE 里都有语义。注册发生冲突时,QHotkey 不会抛异常,只是注册失败,日志是唯一的排查入口。

4.3 高分屏与多屏坐标:一张表讲清楚的换算关系

截图项目里最容易出现“相同代码、不同屏幕结果不一样”的,就是多屏坐标换算。这里把三类坐标列清楚:

坐标类型来源用途
逻辑坐标QScreen::geometry()遮罩窗口布局、选区范围
物理像素逻辑坐标 × devicePixelRatio真正从 QPixmap 里裁剪的区域
虚拟桌面坐标primaryScreen()->virtualGeometry()多屏拼接的统一坐标系

遮罩窗口的坐标系统属于第一类,窗口本身是全屏的,鼠标事件给出的 pos 属于逻辑坐标;抓屏后的 QPixmap 物理尺寸则属于第二类。在拼接函数里,用逻辑坐标做 offset 对齐,再从合成图里按 physicalRect 裁剪,这样两端的换算就闭环了。

这里有一种我常写错的场景:直接把 QCursor::pos() 的全局坐标拿来当光标在遮罩里的位置。如果遮罩窗口不是从原点开始的,这个值就会整体偏移。正确做法是用 event->pos(),或者把全局坐标先减掉遮罩在虚拟桌面里的偏移。另外,对 QPixmap 直接 copy 时如果不注意 Windows 上某些格式带的 alpha 通道,保存成 PNG 后可能有透明底;裁剪后主动转成 QImage::Format_ARGB32 再保存,可以避免这类疑似玄学的色块问题。写坐标换算时最好在入口打一行日志,把两个坐标系的矩形都打印出来,出问题立刻能定位到是哪一步换算错了。

5. 运行、编译与踩坑:依赖清单和五个高频坑的排查顺序

5.1 从拉到源码到跑起来:构建步骤

这份源码是标准 CMake 工程,构建命令不复杂:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/path/to/Qt/5.15/gcc_64 cmake --build build -j8 ./build/QQScreenShot

CMake 找到 Qt 的路径是关键。Windows 上如果装了 Qt 官方安装包,不需要指定 CMAKE_PREFIX_PATH,会自动探测;Linux 和 macOS 建议显式指定,避免系统里多版本 Qt 时选错。构建期依赖清单大致这样:

系统依赖
WindowsQt 5.15 或 Qt 6,MSVC 或 MinGW 套件
Ubuntu/Debianqtbase5-dev、libqt5x11extras5-dev、libx11-dev
macOSQt 5.15 或 Qt 6,Xcode 命令行工具

Qt 版本之间有个坑要提前讲:工程在 Qt 5.15 和 Qt 6 之间做了兼容,但 Qt 6 移除了 QDesktopWidget,所有屏幕枚举统一走 QGuiApplication。拿到源码做 Qt 6 迁移时,不要再引入 QDesktopWidget,否则编译直接报错。QHotkey 在 Qt 6 下要求 C++17,所以 CMake 里需要设置 CXX_STANDARD 17,这份工程已经在 CMakeLists 里做了这个设置。

5.2 运行前的准备:快捷键、主题、托盘

构建产物直接运行即可,首次运行会读取一个简单的 ini 配置文件,内容通常包括热键键位、默认保存路径、自动保存开关。托盘图标默认开启,方便关闭遮罩后继续打开工具;如果开发环境没有桌面托盘,代码也会在几秒后自动退出,不会让进程卡在后台。

配置文件可以由用户手动编辑,也可以在界面上触发重新加载。建议把 QSettings 的存储路径先打出来看一眼,这样命令行里也能用 config --hotkey= 这样的参数临时覆盖,排查问题时少开一个界面。Linux 下如果跑在 Wayland 环境,遮罩窗口的透明层级表现会有差异,常见做法是在 platform 层做降级:不加透明属性,改用半透明 QSS 背景,牺牲一点视觉但保证可用。

5.3 五个实测踩坑记录

坑一:遮罩窗口直接黑屏不透明。现象是按下热键后全屏变黑,选区看不到桌面底图。原因是 Qt 顶层窗口默认不带透明通道,WA_TranslucentBackground 属性没有设置。解决:在构造函数里加 setAttribute(Qt::WA_TranslucentBackground),并且不要在 paintEvent 里用 CompositionMode_Clear 清底色,用“先画底图再压暗再贴亮选区”的顺序。

坑二:鼠标拖出窗口边缘,选区停住不更新。现象是拉框过程中鼠标快速移到屏幕边缘,选区尺寸冻结。原因是构造函数里少了 setMouseTracking(true),Qt 默认只在按下状态才有 move,悬停时不发事件。解决:在 CaptureMask 构造时打开 mouseTracking,并在 mouseMoveEvent 里直接取 event->pos(),不用 globalPos。

坑三:高分屏保存的图只有一半大小。现象是 200% 缩放的 Windows 笔记本上,拉一个 1000×500 的选区,保存出来变成 500×250。原因是没有乘 devicePixelRatio。解决:按 3.2 节 cropFromDesktop 的方式,把逻辑 rect 四个值分别乘 dpr 再 copy;如果读取的 dpr 不是整数,用四舍五入保证 copy 区域不出界。

坑四:马赛克拖动时工具条掉帧。现象是编辑页面移动马赛克区域时,界面卡顿明显。原因是 applyMosaic 的循环实现每帧都对 QImage 做逐像素 setPixelColor,4K 区域下开销过大。解决:一是在预览阶段把图像缩小到 40% 再做马赛克,二是使用先缩后放的方法替代逐格填充,颗粒感差别不大,但帧率提升明显。

坑五:Linux 下截图复制后,在另一个程序粘贴不出来。现象是保存在剪贴板里的图片,过几秒再打开别的软件粘贴却是一片空白。原因是 X11 剪贴板的图片数据在源窗口关闭后不可读,而截图工具每次截图后都把窗口销毁了。解决:截图确认后不仅写剪贴板,还要让进程常驻托盘,持有这份 QMimeData 对象;程序退出前保留数据请求,不要关闭。

6. 二次开发实录:给现有源码加上滚动截屏与自动识别

6.1 滚动截屏的玩法

当前版本不支持滚动拼接,想扩展的思路很简单:先用遮罩选目标区域,保存第一帧底图;然后对区域窗口发送滚轮事件,屏幕滚动后再抓一屏,两帧之间找出最小交叠行数完成无缝拼接。最麻烦的是算交叠高度,我一般会缩小灰度图后用像素差匹配,而不是直接比整帧——这是在这个工程架构下不用动原抓屏层就能加上的模块。交叠匹配的阈值建议设在 85% 以上的像素差低于 10 才算对齐,太低会把相似区域误判成重叠。

6.2 自动识别窗口

第二个实用扩展是窗口截图增强。原版 QQ 截图鼠标悬停时会自动检测窗口,这个工程里用的是原生 API 拿鼠标下的窗口句柄,再用窗口矩形绘制高亮。验证方法很简单:把鼠标移动到任意窗口边缘,看高亮矩形是否正确扣掉了阴影边距,那种多出 8 像素左右黑色区域的偏差,通常就是没扣掉窗口不可见边框导致的。

6.3 验收习惯

我给自己定的验证步骤是:在 1080p 和 2K 两个显示器上分别跑“按热键、拉选区、加标注、贴图、复制”五连击,截图后立刻把图片在像素级放大核对边界,并确认剪贴板数据在关掉工具后仍可粘贴。从那以后我每次拿到这种 Qt 截图项目,都强制走一遍这条链路再动代码,省了很多后面翻车的血泪时间。希望帮到你。

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

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

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

立即咨询