警告

本节包含自动从C++翻译到Python的代码片段,可能包含错误。

拖放#

Qt提供的拖放系统的概述。

拖放提供了一个简单的视觉机制,用户可以使用它在不同应用程序之间以及应用程序内部传输信息。拖放与剪贴板剪切和粘贴机制在功能上相似。

本文件描述了基本的拖放机制,并概述了在自定义控件中启用它的方法。许多Qt的控件也支持拖放操作,例如项目视图和图形视图框架,以及Qt Widgets和Qt Quick的编辑控件。有关项目视图和图形视图的更多信息,请参阅使用项目视图和图形视图框架与拖放。

拖放类#

这些类处理拖放以及所需的MIME类型编码和解码。

PySide6.QtGui.QDrag

QDrag类为基于MIME的数据传输提供了支持。

PySide6.QtGui.QDropEvent

QDropEvent类提供了一个在拖放动作完成后发送的事件。

PySide6.QtGui.QDragEnterEvent

QDragEnterEvent类提供了一个在拖放动作进入控件时发送的事件。

PySide6.QtGui.QDragMoveEvent

QDragMoveEvent类提供了一个在拖放动作进行时发送的事件。

PySide6.QtGui.QDragLeaveEvent

QDragLeaveEvent类提供了一个在拖放动作离开控件时发送的事件。

QUtiMimeConverter

QUtiMimeConverter类在MIME类型和统一类型标识符(UTI)格式之间进行转换。

配置#

QStyleHints对象提供了一些与拖放操作相关的属性

  • startDragTime()描述了用户在开始拖动之前必须按下鼠标按钮在对象上保持的时间(以毫秒为单位)。

  • startDragDistance()表示用户在按下鼠标按钮的同时移动鼠标的距离,移动距离将在被解释为拖动之前。

  • startDragVelocity()表示用户需要以多快的速度(像素/秒)移动鼠标以开始拖动。值为0表示没有这样的限制。

这些数量提供了可接受的基本值,符合底层窗口系统,您可以使用这些值在您的控件中进行拖放支持。

Qt Quick中的拖放#

本文档的其余部分主要关注如何用C++实现拖放。如果您想在Qt Quick场景中使用拖放,请参阅有关Qt Quick Drag、DragEvent和DropArea项的文档,以及Qt Quick拖放示例。

拖放#

要开始拖放,创建一个QDrag对象,并调用其exec()函数。在大多数应用程序中,在鼠标按钮被按下并将光标移动特定距离后才开始拖放操作是一个好主意。但是,从widget启用拖放的最简单方法是重写widget的mousePressEvent()函数并开始一个拖放操作

def mousePressEvent(self, event):

    if (event.button() == Qt.LeftButton
        and iconLabel.geometry().contains(event.pos())) {
        drag = QDrag(self)
        mimeData = QMimeData()
        mimeData.setText(commentEdit.toPlainText())
        drag.setMimeData(mimeData)
        drag.setPixmap(iconPixmap)
        Qt.DropAction dropAction = drag.exec()                ...

尽管用户可能需要一些时间来完成任务,但对于应用程序而言,exec()函数是一个阻塞函数,它会返回几个值之一。这些值显示了操作如何结束,以下是更详细的说明。

请注意,exec()函数不会阻塞主事件循环。

对于需要区分鼠标点击和拖放的widget,重写widget的mousePressEvent()函数以记录拖放的起始位置是有用的

def mousePressEvent(self, event):

    if event.button() == Qt.LeftButton:
        dragStartPosition = event.pos()

稍后,在mouseMoveEvent()中,我们可以确定是否应该开始拖放,并构建拖放对象来处理该操作

def mouseMoveEvent(self, event):

    if not (event.buttons()  Qt.LeftButton):
        return
    if ((event.pos() - dragStartPosition).manhattanLength()
         < QApplication.startDragDistance())
        return
    drag = QDrag(self)
    mimeData = QMimeData()
    mimeData.setData(mimeType, data)
    drag.setMimeData(mimeData)
    Qt.DropAction dropAction = drag.exec(Qt.CopyAction | Qt.MoveAction)            ...

此特定方法使用qPoint::manhattanLength()函数获取鼠标点击位置与当前光标位置之间的大致距离。此函数将准确性换取速度,通常适用于此目的。

放下#

要能够接收 widget 上投放的媒体,为 widget 调用setAcceptDrops(true),并重写dragEnterEvent()和dropEvent()事件处理函数。

例如,以下代码在 QWidget 子类的构造函数中启用放下事件,使您可以有效地实现放下事件的处理器

def __init__(self, parent):
    super().__init__(parent)            ...

setAcceptDrops(True)

dragEnterEvent()函数通常用于告知Qt该widget接受的数据类型。如果您想在重新实现的dragMoveEvent()和dropEvent()中接收QDragMoveEventQDropEvent,则必须重新实现此函数。

以下代码演示了如何重新实现dragEnterEvent()来告知拖放系统我们只能处理纯文本

def dragEnterEvent(self, event):

    if event.mimeData().hasFormat("text/plain"):
        event.acceptProposedAction()

dropEvent()用于解包放下数据,并按照适用于您的应用程序的方式处理。

以下代码将事件中提供的内容传递到QTextBrowser,并将QComboBox填充用于描述数据所使用的MIME类型列表

def dropEvent(self, event):

    textBrowser.setPlainText(event.mimeData().text())
    mimeTypeCombo.clear()
    mimeTypeCombo.addItems(event.mimeData().formats())
    event.acceptProposedAction()

在这种情况下,我们接受建议的操作而不检查它。在实际应用中,如果操作不相关,可能需要从dropEvent()函数返回而不接受建议的操作或处理数据。例如,如果我们不支持外部源链接,我们可能选择忽略Qt::LinkAction操作。

覆盖建议的操作#

我们还可以忽略建议的操作,对数据进行其他一些操作。为此,我们可以在调用accept()之前,利用事件对象的setDropAction()方法,并传入Qt::DropAction中的首选操作。这确保替换的拖放操作被用来代替建议的操作。

对于更复杂的应用,重新实现dragMoveEvent()dragLeaveEvent()将允许你的小部件的部分区域对拖放事件做出响应,并让你对你的应用程序中的拖放行为有更多控制。

复杂小部件的子类化#

一些标准的Qt小部件为其拖放功能提供支持。当你子类化这些小部件时,可能需要除了实现dragEnterEvent()dropEvent()之外,还要重新实现dragMoveEvent(),以防止基类提供默认的拖放处理,以及处理任何你感兴趣的特殊情况。

拖放操作#

在最简单的情况下,拖放操作的目标会收到正在拖拽数据的副本,而数据源决定是否删除原始数据。这由CopyAction操作描述。目标也可以选择执行其他操作,具体是MoveActionLinkAction操作。如果数据源调用exec(),且它返回MoveAction,则数据源负责根据自己的意愿删除任何原始数据。而由数据源小部件创建的QMimeData和QDrag对象不应被删除——它们将由Qt销毁。目标负责接收在拖放操作中发送的数据的所有权;这通常通过保留数据的引用来完成。

如果目标理解LinkAction操作,它应该存储对原始信息的自己的引用;数据源不需要对数据进行任何进一步的处理。拖放操作的常见用途之一是在同一小部件内执行移动;有关此功能的更多信息,请参阅关于Drop Actions的部分。

拖放操作的其他主要用途是与引用类型(例如文本/uri-list)一起使用,这里的拖拽数据实际上是文件的引用或对象的引用。

添加新的拖放类型#

拖放不仅限于文本和图像。任何类型的信息都可以通过拖放操作进行传输。要在应用程序之间拖放信息,这些应用程序必须能够相互指示它们可以接受和生成哪些数据格式。这是通过使用MIME类型来实现的。由源应用程序构建的QDrag对象包含一个列表,其中包含用来表示数据的MIME类型(从最合适的到最不合适的顺序),而拖放目标是使用这些类型之一来访问数据。对于常见的数据类型,便利函数会透明地处理所使用的MIME类型,但对于自定义数据类型,则需要明确指定。

要实现未由QDrag便利函数覆盖的信息类型的拖放操作,首先和最重要的步骤是寻找现有合适的格式:互联网编号分配机构(IANA)在信息科学研究所(ISI)提供了一个MIME媒体类型的分层列表。使用标准MIME类型将最大限度地提高您现在和将来应用程序与其他软件的互操作性。

要支持新的媒体类型,只需使用setData()函数将数据 设置在QMimeData对象中,提供完整的MIME类型以及在适当格式中包含数据的QByteArray。以下代码从一个标签中获取位图,并将其作为可移植网络图形(PNG)文件存储在QMimeData对象中

output = QByteArray()
outputBuffer = QBuffer(output)
outputBuffer.open(QIODevice.WriteOnly)
imageLabel.pixmap().toImage().save(outputBuffer, "PNG")
mimeData.setData("image/png", output)

当然,对于这种情况,我们可以简单地使用setImageData()来提供各种格式的图像数据

mimeData.setImageData(QVariant(imageLabel.pixmap()))

在这个例子中,使用QByteArray方法仍然是有用的,因为它提供了对存储在QMimeData对象中的数据量的更大控制。

请注意,用于项视图的自定义数据类型必须声明为元对象,并且必须实现它们的流操作符。

拖放动作#

在剪切板模型中,用户可以剪切或复制源信息,然后稍后粘贴它。同样,在拖放模型中,用户可以拖动信息的副本,或者将信息本身拖放到新位置(移动它)。对于程序员来说,拖放模型有额外的复杂性:程序不知道用户是想剪切还是复制信息,直到操作完成。在应用程序之间拖放信息时,这通常没有区别,但在应用程序内部,检查使用了哪种拖放动作是很重要的。

我们可以重新实现小部件的mouseMoveEvent(),并用可能的拖放动作的组合开始一个拖放操作。例如,我们可能希望确保在小部件内拖放始终移动对象

def mouseMoveEvent(self, event):

    if not (event.buttons()  Qt.LeftButton):
        return
    if ((event.pos() - dragStartPosition).manhattanLength()
         < QApplication.startDragDistance())
        return
    drag = QDrag(self)
    mimeData = QMimeData()
    mimeData.setData(mimeType, data)
    drag.setMimeData(mimeData)
    Qt.DropAction dropAction = drag.exec(Qt.CopyAction | Qt.MoveAction)            ...

由exec()函数返回的操作默认为CopyAction,如果信息被放入其他应用程序,但如果它被放入同一应用程序的另一个小部件中,我们可能会获得不同的拖放动作。

可以在小部件的dragMoveEvent()函数中过滤所提议的拖放动作。然而,在dragEnterEvent()中接受所有提议的操作,并让用户稍后决定他们想要接受哪种操作是很可能的。

def dragEnterEvent(self, event):

    event.acceptProposedAction()

当小部件发生拖放时,调用dropEvent()处理函数,我们可以依次处理每个可能的行为。首先,我们处理同一个小部件内的拖放操作

def dropEvent(self, event):

    if event.source() == self and event.possibleActions()  Qt.MoveAction:
        return

在这种情况下,我们拒绝处理移动操作。接受的每种类型的拖放动作都被检查和处理。

if event.proposedAction() == Qt.MoveAction:
    event.acceptProposedAction()
    # Process the data from the event.
elif event.proposedAction() == Qt.CopyAction:
   event.acceptProposedAction()
   # Process the data from the event.
else:
    # Ignore the drop.
    return            ...

注意,在上述代码中我们已经检查了单个下落操作。如上述覆写推荐的动作章节中提到的,有时需要覆写推荐的下落动作,并从可能的下落动作中选择不同的一个。要实现这一点,需要通过事件possibleActions() 提供的价值检查每个动作的存在,使用 setDropAction() 设置下落动作,并调用 accept()

下落矩形#

可以使用小部件的 dragMoveEvent() 方法通过仅在这些区域内接受推荐的箭头动作来限制下落只发生在小部件的部分。例如,以下代码在鼠标光标在子小部件 dropFrame 上时接受任何推荐的箭头动作。

def dragMoveEvent(self, event):

    if (event.mimeData().hasFormat("text/plain")
        and event.answerRect().intersects(dropFrame.geometry()))
        event.acceptProposedAction()

如果你需要在拖放操作期间提供视觉反馈、滚动窗口或进行其他合适的操作,也可以使用 dragMoveEvent()

剪切板#

应用程序也可以通过在剪切板上放置数据相互通信。为此,需要从 QClipboard 对象从应用程序对象中获取。

使用 QMimeData 类表示从剪切板传输和接收的数据。要将数据放入剪切板,可以使用常见的文本、图像和位图数据的实用函数,例如 setText()、setImage() 和 setPixmap()。这些函数与 QMimeData 类中的函数类似,但它们还接受一个额外的参数来控制数据存储的位置:如果指定了 Clipboard,数据将被放置在剪切板上;如果指定了 Selection,数据将被放置在鼠标选择中(仅在 X11 上)。默认情况下,数据放置在剪切板上。

例如,我们可以使用以下代码将 QLineEdit 的内容复制到剪切板

QGuiApplication::clipboard()->setText(lineEdit->text(), QClipboard::Clipboard);

也可以将不同 MIME 类型的数据放入剪切板。构建一个 QMimeData 对象,并使用与上一节描述的 setData() 函数设置数据;然后可以使用 setMimeData() 函数将该对象放入剪切板。

QClipboard 类可以通过其 dataChanged() 信号通知应用程序其包含的数据发生更改。例如,我们可以通过将此信号连接到小部件中的一个槽来监视剪切板

clipboard.dataChanged.connect(
        self.updateClipboard)

与该信号连接的槽可以读取剪切板上的数据,使用可以表示它的 MIME 类型之一

def updateClipboard(self):

    mimeTypeCombo.clear()
    formats = clipboard.mimeData().formats()
    if formats.isEmpty():
        return
    for format in formats:
        data = clipboard.mimeData().data(format)
        # ...

在X11上,可以使用selectionChanged()信号来监视鼠标选择。

示例

  • 可拖动图标

  • 可拖动文本

  • 放置点

与其他应用程序协作#

在X11上,使用公开的XDND协议,而在Windows上Qt使用OLE标准,Qt for macOS使用Cocoa拖拽管理器。在X11上,XDND使用MIME,因此无需翻译。Qt API在不同平台上都是相同的。在Windows上,使用MIME的应用程序可以通过使用MIME类型作为剪贴板格式名称来进行通信。一些Windows应用程序已经使用MIME命名约定为其剪贴板格式命名。

可以通过在Windows上重新实现QWindowsMimeConverter或在macOS上重新实现QUtiMimeConverter来注册自定义类以转换专有剪贴板格式。