现象
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。在业务场景中需要一次性写入多种数据,这个时候怎么办呢?结合OleSetClipboard 和 OleFlushClipboard 两个系统 API,放弃OleSetClipboard本身延迟渲染的机制,采用立即渲染的模式。