首页 / 资讯中心 / 文章详情

开源高仿QQ截图工具:基于Qt的离线截图实现

开源高仿QQ截图工具:基于Qt的离线截图实现 ★ FEATURED ARTICLE
简介这是一套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看构建脚本工程核心是 C17 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 或 ShiftF1因为 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_TYPERelease \ -DCMAKE_PREFIX_PATH/path/to/Qt/5.15/gcc_64 cmake --build build -j8 ./build/QQScreenShotCMake 找到 Qt 的路径是关键。Windows 上如果装了 Qt 官方安装包不需要指定 CMAKE_PREFIX_PATH会自动探测Linux 和 macOS 建议显式指定避免系统里多版本 Qt 时选错。构建期依赖清单大致这样系统依赖WindowsQt 5.15 或 Qt 6MSVC 或 MinGW 套件Ubuntu/Debianqtbase5-dev、libqt5x11extras5-dev、libx11-devmacOSQt 5.15 或 Qt 6Xcode 命令行工具Qt 版本之间有个坑要提前讲工程在 Qt 5.15 和 Qt 6 之间做了兼容但 Qt 6 移除了 QDesktopWidget所有屏幕枚举统一走 QGuiApplication。拿到源码做 Qt 6 迁移时不要再引入 QDesktopWidget否则编译直接报错。QHotkey 在 Qt 6 下要求 C17所以 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 做逐像素 setPixelColor4K 区域下开销过大。解决一是在预览阶段把图像缩小到 40% 再做马赛克二是使用先缩后放的方法替代逐格填充颗粒感差别不大但帧率提升明显。坑五Linux 下截图复制后在另一个程序粘贴不出来。现象是保存在剪贴板里的图片过几秒再打开别的软件粘贴却是一片空白。原因是 X11 剪贴板的图片数据在源窗口关闭后不可读而截图工具每次截图后都把窗口销毁了。解决截图确认后不仅写剪贴板还要让进程常驻托盘持有这份 QMimeData 对象程序退出前保留数据请求不要关闭。6. 二次开发实录给现有源码加上滚动截屏与自动识别6.1 滚动截屏的玩法当前版本不支持滚动拼接想扩展的思路很简单先用遮罩选目标区域保存第一帧底图然后对区域窗口发送滚轮事件屏幕滚动后再抓一屏两帧之间找出最小交叠行数完成无缝拼接。最麻烦的是算交叠高度我一般会缩小灰度图后用像素差匹配而不是直接比整帧——这是在这个工程架构下不用动原抓屏层就能加上的模块。交叠匹配的阈值建议设在 85% 以上的像素差低于 10 才算对齐太低会把相似区域误判成重叠。6.2 自动识别窗口第二个实用扩展是窗口截图增强。原版 QQ 截图鼠标悬停时会自动检测窗口这个工程里用的是原生 API 拿鼠标下的窗口句柄再用窗口矩形绘制高亮。验证方法很简单把鼠标移动到任意窗口边缘看高亮矩形是否正确扣掉了阴影边距那种多出 8 像素左右黑色区域的偏差通常就是没扣掉窗口不可见边框导致的。6.3 验收习惯我给自己定的验证步骤是在 1080p 和 2K 两个显示器上分别跑“按热键、拉选区、加标注、贴图、复制”五连击截图后立刻把图片在像素级放大核对边界并确认剪贴板数据在关掉工具后仍可粘贴。从那以后我每次拿到这种 Qt 截图项目都强制走一遍这条链路再动代码省了很多后面翻车的血泪时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站