Mac平台下如何修改代码捕获全屏幕应用画面并转为QImage?
macOS下CoreGraphics屏幕捕获修复方案
问题背景
开发跨平台FPS测量应用时,Windows端运行正常,但macOS平台下无论是Qt原生API还是CoreGraphics都无法捕获指定区域内的其他应用画面,需修改代码实现指定区域内所有应用画面的捕获并转换为QImage。
核心修改点
1. 处理macOS屏幕录制权限
macOS 10.15+要求应用获取屏幕录制权限才能捕获其他应用画面,需在代码中添加权限检查,未授权时提示用户开启。
2. 修正CoreGraphics捕获参数
- 将
CGWindowListCreateImage的窗口列表选项改为kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,确保捕获所有可见应用窗口(排除桌面背景元素)。 - 转换Qt逻辑坐标为CoreGraphics物理像素坐标,适配Retina屏幕的分辨率差异。
3. 修复CGImage转QImage的图像翻转问题
CoreGraphics坐标系与Qt相反,绘制时需翻转上下文,避免截图上下颠倒。
修改后的完整代码
#include "framecounter.h" #include <QtGui/QGuiApplication> #include <QDir> #include <QMessageBox> #ifdef Q_OS_MAC #include <CoreGraphics/CoreGraphics.h> #include <CoreServices/CoreServices.h> #endif FrameCounter::FrameCounter(QWidget* parent) : QWidget(parent) { connect(&m_captureTimer, &QTimer::timeout, this, &FrameCounter::captureScreen); m_elapsedTimer.start(); } void FrameCounter::captureScreen() { #ifdef Q_OS_MAC // 检查屏幕录制权限 if (!CGPreflightScreenCaptureAccess()) { static bool showedPrompt = false; if (!showedPrompt) { QMessageBox::warning(this, "权限不足", "请在系统设置-安全性与隐私-屏幕录制中开启本应用的权限"); showedPrompt = true; } return; } // 转换Qt逻辑坐标为CoreGraphics物理像素坐标 QScreen* screen = QGuiApplication::primaryScreen(); if (!screen) return; qreal dpr = screen->devicePixelRatio(); CGRect captureRect = CGRectMake(m_x * dpr, m_y * dpr, m_size * dpr, m_size * dpr); // 捕获所有可见应用窗口(排除桌面) CGImageRef cgImage = CGWindowListCreateImage( captureRect, kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID, kCGWindowImageDefault ); if (!cgImage) { qWarning() << "Failed to capture screen image"; return; } QImage screenCapture = CGImageToQImage(cgImage); CGImageRelease(cgImage); // 保存测试截图 static int i = 0; QString fileName = QString("capture_%1.png").arg(i++); saveImage(screenCapture, fileName); #else QScreen* screen = QGuiApplication::primaryScreen(); if (!screen) return; QImage screenCapture = screen->grabWindow(0, m_x, m_y, m_size, m_size).toImage(); #endif if (!m_previousFrame.isNull() && screenCapture != m_previousFrame) { ++m_frameCount; } m_previousFrame = screenCapture; calculateFps(); } void FrameCounter::calculateFps() { qint64 elapsed = m_elapsedTimer.elapsed(); if (elapsed >= 300) { m_currentFps = static_cast<double>(m_frameCount) / (static_cast<double>(elapsed) / 1000.0); m_listOfFps.append(m_currentFps); m_frameCount = 0; m_elapsedTimer.restart(); emit newfpsvalue(); } } void FrameCounter::startMeasurement(int x, int y, int size) { m_x = x; m_y = y; m_size = size; m_captureTimer.start(16); m_elapsedTimer.restart(); } void FrameCounter::stopMeasurement() { m_captureTimer.stop(); m_currentFps = 0; m_listOfFps.clear(); } #ifdef Q_OS_MAC CGBitmapInfo FrameCounter::CGBitmapInfoForQImage(const QImage &image) { CGBitmapInfo bitmapInfo = kCGImageAlphaNone; switch (image.format()) { case QImage::Format_ARGB32: bitmapInfo = kCGImageAlphaFirst | kCGBitmapByteOrder32Host; break; case QImage::Format_RGB32: bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host; break; case QImage::Format_RGBA8888_Premultiplied: bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big; break; case QImage::Format_RGBA8888: bitmapInfo = kCGImageAlphaLast | kCGBitmapByteOrder32Big; break; case QImage::Format_RGBX8888: bitmapInfo = kCGImageAlphaNoneSkipLast | kCGBitmapByteOrder32Big; break; case QImage::Format_ARGB32_Premultiplied: bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; break; default: break; } return bitmapInfo; } QImage FrameCounter::CGImageToQImage(CGImageRef cgImage) { const size_t width = CGImageGetWidth(cgImage); const size_t height = CGImageGetHeight(cgImage); QImage image(static_cast<int>(width), static_cast<int>(height), QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); CGContextRef context = CGBitmapContextCreate( image.bits(), width, height, 8, static_cast<size_t>(image.bytesPerLine()), colorSpace, CGBitmapInfoForQImage(image) ); // 翻转上下文,修正CoreGraphics与Qt的坐标系差异 CGContextTranslateCTM(context, 0, height); CGContextScaleCTM(context, 1.0, -1.0); CGRect rect = CGRectMake(0, 0, width, height); CGContextDrawImage(context, rect, cgImage); CFRelease(colorSpace); CGContextRelease(context); // 转换回逻辑像素适配Qt显示 QScreen* screen = QGuiApplication::primaryScreen(); if (screen) { qreal dpr = screen->devicePixelRatio(); image.setDevicePixelRatio(dpr); } return image; } #endif void FrameCounter::saveImage(const QImage& image, const QString& fileName) { QDir dir; if (!dir.exists("captures")) { dir.mkpath("captures"); } image.save("captures/" + fileName); }
额外说明
- 权限提示仅触发一次,避免重复打扰用户。
- 坐标转换适配Retina屏幕,确保捕获区域与用户选择的逻辑区域完全匹配。
- 修复了CGImage转QImage时的图像翻转问题,保证截图显示正常。
内容的提问来源于stack exchange,提问作者ASM
相关产品推荐
相关产品推荐

