windows Qt 剪贴板失效问题

41 阅读3分钟

现象

Qt 的应用进行 ctrl+c 后使用 setMimeData 写入数据,但是 ctrl+v 后并没有数据。只有极少数出现这样的 case。

QClipboard

在 Qt 中,QClipboard 类用来访问系统的剪贴板。它可以让你:

  • 读取剪贴板中的数据。
  • 把应用的数据放到剪贴板中。
  • 监听剪贴板内容变化。

使用方式如下,QClipboard 是一个应用级别的单例:

QClipboard *clipboard = QGuiApplication::clipboard();
//写入自定义数据
clipboard->setMimeData(data);
//获取自定义数据
auto data = clipboard->mimeData();

在问题排查过程中通过 ownsClipboard (判断当前应用是否为剪贴板的拥有者)来发现问题。在异常 case 中,使用 setMimeData 后,通过 ownsClipboard 发现当前应用并不是剪贴板的拥有者,推断可能是部分用户电脑上的一些软件导致

  • QClipboard windows 关键实现

Qt 中的源码如下,核心是使用了 OleSetClipboard 系统 API 实现了剪贴板的功能。剪贴板的使用在 windows 上,还可以用SetClipboardData 系统 API。

void QWindowsClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode)
{
    qCDebug(lcQpaMime) << __FUNCTION__ <<  mode << mimeData;
    if (mode != QClipboard::Clipboard)
        return;

    const bool newData = !m_data || m_data->mimeData() != mimeData;
    if (newData) {
        releaseIData();
        if (mimeData)
            m_data = new QWindowsOleDataObject(mimeData);
    }

    HRESULT src = S_FALSE;
    int attempts = 0;
    for (; attempts < 3; ++attempts) {
        src = OleSetClipboard(m_data);
        if (src != CLIPBRD_E_CANT_OPEN || QWindowsContext::isSessionLocked())
            break;
        QThread::msleep(100);
    }

    if (src != S_OK) {
        QString mimeDataFormats = mimeData ?
            mimeData->formats().join(u", ") : QString(QStringLiteral("NULL"));
        qErrnoWarning("OleSetClipboard: Failed to set mime data (%s) on clipboard: %s",
                      qPrintable(mimeDataFormats),
                      QWindowsContext::comErrorString(src).constData());
        releaseIData();
        return;
    }
}

SetClipboardData vs OleSetClipboard

  • OleSetClipboard
    • 用途:把一个实现了 IDataObject 接口的 COM 对象 注册到系统剪贴板中。
    • 核心特点:
      • 支持延迟渲染(Lazy Rendering)。
      • 可以同时声明支持多种数据格式(例如 CF_UNICODETEXT、自定义 MIME 等)。
      • 引用一个接口指针到剪贴板,而不是直接拷贝数据。
      • 目标应用通过 OleGetClipboard 获取数据对象。
    • 优势:适合复杂对象、多格式、延迟渲染、跨进程拖放。
    • 劣势:需要 COM 编程,依赖拥有者进程不能退出,否则剪贴板失效。
  • SetClipboardData
    • 用途:将某个具体格式的数据立即放入剪贴板。
    • 核心特点:
      • 必须指定具体的 UINT format(例如 CF_UNICODETEXT, CF_BITMAP)。
      • 直接拷贝或句柄传递数据到系统,数据立即可由其他应用读取。
      • 不涉及 COM/OLE 接口,没有延迟渲染能力。
      • 只能一次注册单个格式;要支持多格式,需要多次调用。
    • 优势:简单易用,数据持久化到系统,无需 COM 环境。
    • 劣势:一次只写单一格式,多格式需多次调用;没有延迟渲染能力,拷贝大数据会变慢。

通过对比发现两个系统 API 在机制上存在一定的不同,特别是 OleSetClipboard支持延迟渲染。通过翻阅 chrome 源码发现 OleSetClipboard 这个系统 API 并没有使用。

实现增强版本的 SetClipboardData

通过前面的一些分析,发现 SetClipboardData 系统 API 在一些极端(万分之一的概率跟设备环境有关)情况下可能会出现剪贴板失效的问题,像 chrome 这样的客户端并没有使用这个 API。在业务场景中需要一次性写入多种数据,这个时候怎么办呢?结合OleSetClipboardOleFlushClipboard 两个系统 API,放弃OleSetClipboard本身延迟渲染的机制,采用立即渲染的模式。