You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 06:37:08