qt 您所在的位置:网站首页 剪贴板原理 qt

qt

2024-01-26 13:23| 来源: 网络整理| 查看: 265

一、复制粘贴的简单原理

复制粘贴大概可以分为以下两种场景:

在同一个进程中复制并粘贴在进程A(源窗口)中复制,在进程B(目的窗口)中粘贴

场景1好理解,数据都在一个进程中,直接在内存中设置和读取就可以了,不涉及跨进程之间的问题

场景2数据会跨进程传递,那就应该用了某一种进程间通信技术,而在wayland平台使用的就是匿名管道(pipe)

但是,他们是直接进程A和进程B之间进行通信吗?还是怎样?具体下面会给出分析

二、几个关于复制粘贴相关的类

QClipboard: 提供了对窗口系统剪贴板的访问

该类提供了一些方便的接口来访问普通数据类型,更灵活的数据可以通过mimeData()接口获取

在应用程序中只有一个全局的QClipboard,你可以使用QApplication::clipboard()来获取

你可以通过调用clear()清空剪贴板

QClipboard和QDragObject支持一样的数据类型,并且使用类似的机制

拓展:不同平台下的剪切板实现不同

X窗口系统有一个选择的概念—当文本被选择,它立即被复制到全局鼠标选择系统中,此时可用鼠标中键来粘贴全局鼠标选择系统中的内容;X窗口系统还有一个所有权的概念,如果你在一个窗口中改变了选择,X11仅仅通知变化的拥有者和前任拥有者

Windows仅仅在文本被显示地复制或者剪切的时候才被复制到剪贴板,是一个完完全全的全局资源,所以所有应用程序都会被通知变化

macOS支持一个单独的查找缓冲区,该缓冲区在“查找”操作中保存当前的搜索字符串。可以通过指定FindBuffer模式来访问此查找剪贴板。

通过以上拓展可以了解,QClipboard几个Mode {Clipboard, Selection, FindBuffer}的用处:

Clipboard 全局剪切板系统,所有平台都支持,包括wayland平台Selection 全局鼠标选择系统,X11平台支持FindBuffer 单独的查找缓冲区,只有macOS平台支持

QMimedata: 为数据提供一个容器,用来记录关于MIME类型数据的信息

常用来描述保存在剪切板里信息,或者拖拽信息

QMimeData对象把它所保存的信息和正确的MIME类型连接起来,来保证信息可以被安全的在应用程序之间转移,或者在同一个应用程序之间拷贝

对于最常见的MIME类型,QMimeData提供了方便的功能来访问数据:

TesterGetterSetterMIME TypeshasText()text()setText()text/plainhasHtml()html()setHtml()text/htmlhasUrls()urls()setUrls()text/uri-listhasImage()imageData()setImageData()image/ *hasColor()colorData()setColorData()application/x-color

以下是平台接口类

QInternalMimeData: 该类是一个接口类,由不同平台获取数据的类继承使用,其继承自QMimeData

QPlatformClipboard: 系统剪切板平台接口类,QClipboard数据设置和获取最终都是调用这个类

以下是wayland平台下和剪切板相关类:

QWaylandClipboard: wayland平台剪切板类,继承自QPlatformClipboard

QWaylandDataDevice: 该类是和窗口管理器交互的类,主要是通知和接受是否有剪切信息

QWaylandDataDeviceManager: 该类是管理QWaylandDataDevice的类,通过此类获取device对象

QWaylandDataSource: 该类是复制或剪切的源窗口需要构造的数据类

QWaylandDataOffer: 该类是粘贴的目的窗口需要构造的数据类

QWaylandMimeData: wayland平台真正接受剪切数据的类,继承自QInternalMimeData

总结了以上相关类的uml类图如下,可以更便于了解各个类之间的关系

在这里插入图片描述

三、wayland平台复制粘贴的内部实现

上面我们已经提前知道了,在wayland平台复制粘贴跨进程数据传递用的是匿名管道pipe,但这种通信是怎么进行的呢?带着上面的问题,再结合一下源代码详细梳理一下内部原理

首先我们通过调试代码知道,当我们在进程A中进行复制或剪切,会调用到代码:

void QClipboard::setMimeData(QMimeData* src, Mode mode) { QPlatformClipboard *clipboard = QGuiApplicationPrivate::platformIntegration()->clipboard(); if (!clipboard->supportsMode(mode)) { if (src != nullptr) { qDebug("Data set on unsupported clipboard mode. QMimeData object will be deleted."); src->deleteLater(); } } else { clipboard->setMimeData(src,mode); } }

此时会调用的wayland平台下的类中,即QWaylandClipboard类:

void QWaylandClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) { auto *seat = mDisplay->currentInputDevice(); if (!seat) { qCWarning(lcQpaWayland) hasFormat(plain) && !data->hasFormat(utf8)) data->setData(utf8, data->data(plain)); switch (mode) { case QClipboard::Clipboard: if (auto *dataDevice = seat->dataDevice()) { //构造QWaylandDataSource类,并设置给dataDevice dataDevice->setSelectionSource(data ? new QWaylandDataSource(mDisplay->dndSelectionHandler(), data) : nullptr); emitChanged(mode); } break; ..... } }

上面的关键函数为setSelectionSource,我们看下setSelectionSource的实现:

void QWaylandDataDevice::setSelectionSource(QWaylandDataSource *source) { if (source) connect(source, &QWaylandDataSource::cancelled, this, &QWaylandDataDevice::selectionSourceCancelled); // wayland协议接口,通知窗口管理器此时有复制的数据 set_selection(source ? source->object() : nullptr, m_inputDevice->serial()); m_selectionSource.reset(source); }

此时我们可以看出,进程A最终只是跟窗口管理器进行了通信,并不是直接跟进程B进行交互,这里也是回答了一开始提出的那个问题。

那窗口管理器又是如果通知到进程B的呢?当然我们猜测也是通过wayland协议接口,具体是哪几个接口呢?

这里先简单介绍一下wayland相关的内容,我们应该知道在wayland平台中所有窗口都是由窗口管理器进行统一管理的,而窗口和窗口管理器之间的交互是基于wayland协议。前面我们介绍了三个和窗口管理器通信的类,而这三个类就是继承了wayland的通信协议并实现了协议中的接口:

QWaylandDataDevice 继承自 wl_data_device

QWaylandDataSource 继承自 wl_data_source

QWaylandDataOffer 继承自 wl_data_offer

从第二章节我们了解到,QWaylandDataSource和QWaylandDataOffer是真正保存数据的类,并且前者是源窗口保存数据的类,后者是目的窗口保存数据的类,而QWaylandDataDevice就是管理和构造以上两个类,并且主要负责和窗口管理器进行同步的类。

从上面的复制过程的代码中,我们看到了QWaylandDataSource构造的过程,那目的窗口中的QWaylandDataOffer是如何构造的还不清楚,通过查看wayland_debug的日志可以看到,当进程B窗口刚激活时会有如下信号:

在这里插入图片描述

结合以上信号继续调试代码可以发现,会调到qtwayland的如下接口:

void QWaylandDataDevice::data_device_data_offer(struct ::wl_data_offer *id) { // 这里创建QWaylandDataOffer对象 new QWaylandDataOffer(m_display, id); } void QWaylandDataOffer::data_offer_offer(const QString &mime_type) { // 同步format信息 m_mimeData->appendFormat(mime_type); }

最后调用selection接口,在这里发送了一个信号给到上层客户端,客户端可以通过绑定dataChanged()信号来开始获取剪切板的数据,但真正的数据还没获取到

void QWaylandDataDevice::data_device_selection(wl_data_offer *id) { if (id) // 这里将创建的QWaylandDataOffer对象给到dataDevice保管 m_selectionOffer.reset(static_cast(wl_data_offer_get_user_data(id))); else m_selectionOffer.reset(); #if QT_CONFIG(clipboard) QGuiApplicationPrivate::platformIntegration()->clipboard()->emitChanged(QClipboard::Clipboard); #endif }

以下画了个交互图,方面理解wayland交互的流程: 在这里插入图片描述

对于上图的交互流程需要特别注意两点:

wayland平台给客户端同步剪切板信息,只会在客户端激活的情况才会通知,没有激活是不会通知的以上只是激活目的窗口的流程,激活后就会收到窗管的信号,并收到了相关的format信息,但此时真正的复制或剪切的数据还没有发送到目的窗口中。

此时在进程B中做粘贴的操作,继续调试代码,发现调用mimeData()接口取数据:

const QMimeData* QClipboard::mimeData(Mode mode) const { QPlatformClipboard *clipboard = QGuiApplicationPrivate::platformIntegration()->clipboard(); if (!clipboard->supportsMode(mode)) return nullptr; return clipboard->mimeData(mode); }

查看对应wayland平台的类:

QMimeData *QWaylandClipboard::mimeData(QClipboard::Mode mode) { auto *seat = mDisplay->currentInputDevice(); if (!seat) return &m_emptyData; switch (mode) { case QClipboard::Clipboard: if (auto *dataDevice = seat->dataDevice()) { // 如果在同一进程复制粘贴,那source不为空,直接取source里面的mimedata if (auto *source = dataDevice->selectionSource()) { return source->mimeData(); } // 如果是跨进程粘贴,那source为空,offer不为空,就取offer里面的mimedata if (auto *offer = dataDevice->selectionOffer()) { return offer->mimeData(); } } return &m_emptyData; ... } }

从上面的代码中就可以看出,两种不同场景数据获取的来源。

获取了mimedata之后,需要取出里面的数据,例如text(),那看看怎么取text数据

QString QMimeData::text() const { Q_D(const QMimeData); QVariant utf8Text = d->retrieveTypedData(textPlainUtf8Literal(), QMetaType::QString); if (!utf8Text.isNull()) return utf8Text.toString(); QVariant data = d->retrieveTypedData(textPlainLiteral(), QMetaType::QString); return data.toString(); } QVariant QMimeDataPrivate::retrieveTypedData(const QString &format, QMetaType::Type type) const { Q_Q(const QMimeData); QVariant data = q->retrieveData(format, QVariant::Type(type)); ... }

这里看出,retrieveData接口是虚函数,就要看看子类的实现:

QVariant QInternalMimeData::retrieveData(const QString &mimeType, QVariant::Type type) const { QVariant data = retrieveData_sys(mimeType, type); // 以下是对不同数据类型进行解析 ... return data; }

接着看retrieveData_sys的实现:

QVariant QWaylandMimeData::retrieveData_sys(const QString &mimeType, QVariant::Type type) const { Q_UNUSED(type); if (m_data.contains(mimeType)) return m_data.value(mimeType); QString mime = mimeType; if (!m_types.contains(mimeType)) { if (mimeType == QStringLiteral("text/plain") && m_types.contains(utf8Text())) mime = utf8Text(); else return QVariant(); } // 创建管道,pipefd[0]表示r端口,pipefd[1]表示w端口 int pipefd[2]; if (qt_safe_pipe(pipefd) == -1) { qWarning("QWaylandMimeData: pipe2() failed"); return QVariant(); } // 这里是发消息给窗管,通知窗管进程B准备接受数据,窗管会通知进程A写数据 m_dataOffer->startReceiving(mime, pipefd[1]); close(pipefd[1]); QByteArray content; // 从管道中读数据 if (readData(pipefd[0], content) != 0) { qWarning("QWaylandDataOffer: error reading data for mimeType %s", qPrintable(mimeType)); content = QByteArray(); } close(pipefd[0]); m_data.insert(mimeType, content); return content; }

看到这里已经可以看出真相了,这里是开了一个管道,通过startReceiving接口通知窗管,再由窗管通知进程A写数据,然后在进程B中通过readData函数读取。

而进程A中写管道的代码如下:

void QWaylandDataSource::data_source_send(const QString &mime_type, int32_t fd) { QByteArray content = QWaylandMimeHelper::getByteArray(m_mime_data, mime_type); if (!content.isEmpty()) { // Create a sigpipe handler that does nothing, or clients may be forced to terminate // if the pipe is closed in the other end. struct sigaction action, oldAction; action.sa_handler = SIG_IGN; sigemptyset (&action.sa_mask); action.sa_flags = 0; sigaction(SIGPIPE, &action, &oldAction); write(fd, content.constData(), content.size()); sigaction(SIGPIPE, &oldAction, nullptr); } close(fd); }

到此,关于wayland平台中复制粘贴的过程基本都和清楚了。以上的几个问题,也都得到了解答

总结一下:

同一个进程的复制粘贴数据就保存在自己的内存中,直接设置和获取;

不同进程之间复制粘贴的交互是通过窗口管理器进行间接交互的,数据是通过管道传输的。

四、目前wayland平台复制粘贴存在的问题

问题一:当在进程A中复制后,如果关闭了进程A,再在进程B中粘贴,发现没有数据?

原因:是因为wayland平台的数据传输是通过管道传输的,如果管道的一端关闭了,数据就传输不成功,如果要解决该问题,可能要修改wayland平台下的数据传输的方式,例如可以改成消息队列

问题二:大数据量(>70M)的复制和拷贝文本,在此过程中如果不停做点击鼠标和移动鼠标等动作,最后会导致进程崩溃?

原因:该问题的是flushrequest的问题,最终会打印:“The wayland connectting broken, …”,这个在qt官网有类似问题,具体是因为拷贝复制过程中数据量过大,导致主线程卡死,此时不停点击或移动鼠标会使socket数据不停积累最后溢出,导致wayland连接管道破裂。该问题暂时没找到好解决方案。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有