警告
本节包含从C++自动翻译到Python的代码片段,可能包含错误。
拖放¶
Qt提供的拖放系统概述。
拖放提供了一种简单的视觉机制,用户可以使用它在应用程序之间和内部传输信息。拖放的功能类似于剪贴板的剪切和粘贴机制。
本文档描述了基本的拖放机制,并概述了在自定义控件中启用该机制的方法。许多Qt控件也支持拖放操作,例如项目视图和图形视图框架,以及Qt Widgets和Qt Quick的编辑控件。有关项目视图和图形视图的更多信息,请参阅《使用项目视图和图形视图框架进行拖放》。
拖放类¶
这些类处理拖放以及必要的MIME类型编码和解码。
QDrag 类提供了基于 MIME 的拖放数据传输支持。
QDropEvent 类提供了一个事件,该事件在拖放操作完成时发送。
QDragEnterEvent 类提供了一个事件,当拖放操作进入小部件时,该事件会被发送到小部件。
QDragMoveEvent 类提供了一个事件,该事件在拖放操作进行时发送。
QDragLeaveEvent 类提供了一个事件,当拖放操作离开小部件时,该事件会被发送到小部件。
QUtiMimeConverterQUtiMimeConverter 类在 MIME 类型和统一类型标识符 (UTI) 格式之间进行转换。
配置¶
QStyleHints 对象提供了一些与拖放操作相关的属性:
startDragTime()描述了用户必须在对象上按住鼠标按钮的时间(以毫秒为单位),然后拖动才会开始。
startDragDistance()表示用户在按住鼠标按钮的同时需要移动鼠标多远,才会被解释为拖动。
startDragVelocity()表示用户需要以多快的速度(以像素/秒为单位)移动鼠标才能开始拖动。值为0表示没有这样的限制。
这些数量提供了合理的默认值,符合底层窗口系统的要求,如果您在控件中提供拖放支持,可以使用这些默认值。
Qt Quick中的拖放功能¶
文档的其余部分主要关注如何在C++中实现拖放功能。要在Qt Quick场景中使用拖放,请阅读Qt Quick Drag、DragEvent和DropArea项目的文档,以及Qt Quick拖放示例。
拖动¶
要开始拖动,创建一个QDrag对象,并调用其exec()函数。在大多数应用程序中,只有在按下鼠标按钮并且光标移动了一定距离后,才开始拖放操作是一个好主意。然而,启用从部件拖动的简单方法是重新实现部件的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() 函数不会阻塞主事件循环。
对于需要区分鼠标点击和拖动的小部件,重新实现小部件的 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()函数来粗略估计鼠标点击发生位置与当前光标位置之间的距离。该函数以速度为代价换取准确性,通常适用于此目的。
删除¶
为了能够接收拖放到小部件上的媒体,需要为小部件调用setAcceptDrops(true),并重新实现dragEnterEvent()和dropEvent()事件处理函数。
例如,以下代码在QWidget子类的构造函数中启用了拖放事件,使得可以有效地实现拖放事件处理程序:
def __init__(self, parent): super().__init__(parent) ... setAcceptDrops(True)
dragEnterEvent() 函数通常用于通知 Qt 小部件接受的数据类型。如果你想在重新实现的 dragMoveEvent() 和 dropEvent() 中接收 QDragMoveEvent 或 QDropEvent,则必须重新实现此函数。
以下代码展示了如何重新实现 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()之前,使用Qt::DropAction中的首选操作调用事件对象的setDropAction()。这确保了使用替换的拖放操作而不是建议的操作。
对于更复杂的应用程序,重新实现dragMoveEvent()和dragLeaveEvent()将使您能够使小部件的某些部分对放置事件敏感,并为您提供对应用程序中拖放的更多控制。
复杂小部件的子类化¶
某些标准的Qt小部件提供了它们自己对拖放的支持。当子类化这些小部件时,除了dragEnterEvent()和dropEvent()之外,可能还需要重新实现dragMoveEvent(),以防止基类提供默认的拖放处理,并处理您感兴趣的任何特殊情况。
拖放操作¶
在最简单的情况下,拖放操作的目标接收被拖动数据的副本,源决定是否删除原始数据。这由CopyAction操作描述。目标还可以选择处理其他操作,特别是MoveAction和LinkAction操作。如果源调用exec(),并且它返回MoveAction,源负责删除任何原始数据(如果它选择这样做)。由源小部件创建的QMimeData和QDrag对象不应被删除 - 它们将由Qt销毁。目标负责获取拖放操作中发送的数据的所有权;这通常通过保持对数据的引用来完成。
如果目标理解LinkAction操作,它应该存储对原始信息的自身引用;源不需要对数据执行任何进一步的处理。拖放操作最常见的用途是在同一小部件内执行移动;有关此功能的更多信息,请参阅Drop Actions部分。
拖拽操作的另一个主要用途是使用引用类型,如text/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()。
剪贴板¶
应用程序也可以通过将数据放在剪贴板上来相互通信。要访问此功能,您需要从QApplication对象获取一个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) # ...
selectionChanged() 信号可以在 X11 上用于监视鼠标选择。
示例¶
可拖动的图标
可拖动的文本
投放站点
与其他应用程序的互操作性¶
在X11上,使用公共的XDND协议,而在Windows上,Qt使用OLE标准,macOS上的Qt使用Cocoa拖放管理器。在X11上,XDND使用MIME,因此不需要翻译。无论平台如何,Qt API都是相同的。在Windows上,支持MIME的应用程序可以通过使用MIME类型的剪贴板格式名称进行通信。一些Windows应用程序已经在其剪贴板格式中使用MIME命名约定。
可以通过在Windows上重新实现QWindowsMimeConverter或在macOS上重新实现QUtiMimeConverter来注册用于转换专有剪贴板格式的自定义类。