PyQt5的拖放动作
1. 什么是拖放动作
拖放动作是指用户希望怎样处理拖放的数据,比如移动、复制、还是创建由目标到源的链
接等。 拖放动作由 Qt::DropAction 枚举描述:
2. 可能的拖放动作,实际的拖放动作,建议的拖放动作
-
可能的拖放动作:
是指用户在拖放时可能会执行的拖放动作,用户在拖放时通常可由用户选择,比如可以选择移动、复制或链接等动作中的一种,这些动作都是可能的拖放动作。可能的拖放动作在 QDrag::exe()函数的第 1 个参数中指定,同时该函数的返回值是最终的实际拖放动作。 -
实际的拖放动作:
是指拖放到最终放置时实际执行的动作,实际拖放动作在 drogEvent()函数中使用 QDropEvent::setDropAction()函数(还需在之后调用 accept()函数)设置,该函数会影响 QDrag::exec()函数的返回值。 -
建议的拖放动作:
是指当用户执行拖动而不使用修饰键时的默认动作, 建议拖放动作可在 QDrag::exec()函数的第 2 个参数中指定,该参数的设置会影响拖动时鼠标光标右下角的外观,另外 QDropEvent::acceptProposedAction()函数表示设置执行操作为建议操作并接受该事件。
以上三种拖放动作常常相互关联,比如用户在拖动时通常可以执行移动、复制或链接等动作(可能的拖放动作)中的一种,然而应用程序在拖放到最终放置时并不知道用户到底需要执行哪种操作,若用户未指定需要执行的可能的拖放动作中的哪一种动作时,应用程序可以使用设置的建议动作,作为需要执行的动作。
3. 各拖放动作之间的关系
-
QDrag::exec()函数的规则
- 若 QDrag::exec()未指定建议拖放动作,则依顺序移动、复制、链接进行选择
- 若 QDrag::exec()函数在第 2 个参数上指定了建议拖放动作,但该动作不在可能的拖放动作组合之中,则使用默认的复制拖放动作。
-
QDropEvent::setDropAction()函数的规则
- 使用 setDropAction()函数设置拖放动作之后应使用 accept()函数,而不应使用QDropEvent::acceptProposedAction()函数(因为该函数会重置拖放动作为建议拖放动作)
- 若 QDropEvent::setDropAction()函数设置的拖放动作不在可能的拖放动作组合之中,则使用建议拖放动作。
-
dropEvent()函数的规则,该函数是否接受事件直接影响到 QDrag::exec()函数的返回值,其规则如下
- 若在该函数内调用 ignore(),则 exec()函数返回 Qt::IgnoreAction
- 若在该函数内调用 accept(),则 exec()函数返回在该函数中使用 setDropAction()函数设置的拖放动作。
4. 拖放动作及拖放程序设计原则
-
若在 mouseMoveEvent()函数中启动拖放,则可以编写避免用户因为手握鼠标抖动而产生的拖动,这比在 mousePressEvent()函数中启动拖放效果更好。
-
在 QDrag::exec()函数的参数中指定可能的拖放动作, 比如在其中同时指定移动、复制、链接等; 但最终是否接受这些动作,由后续的事件处理函数进行判断,详见后文。 另外需要注意的是 QDrag::exec()函数虽是阻塞函数,但在执行完该函数(比如释放鼠标按钮完成拖放时)后程序会返回该函数,然后接着执行之后的语句, exec()函数返回的是用户实际执行的动作。
-
dragEnterEvent()函数通常根据该部件或实际使用情况进行筛选, 比如若该部件不接受图片数据,则忽略对该动作的接受,从而阻止事件被进一步传递。
-
若重新实现了 dragMoveEvent()函数,则还可以在该函数内进行进一步的设置, 比如默认为复制动作,若用户拖动的同时按下了 Shift 键,则设置为移动动作,若按下了Ctrl 键,则为复制动作,若按下了 Alt 键则为链接动作等,在该函数中的设置可以影响鼠标光标在外观上的显示,比如复制会在光标右下角显示一个“+”符号等。另外,在该函数内还可以设置用户拖动到该部件中的范围,比如拖动到某矩形范围内时,该部件才接受拖放,否则被忽略等。注意:若在 dragEnterEvent()函数内也设置了拖放动作,同样会改变光标的外观但只会改变进入部件时的外观,光标最终的外观以dragMoveEvent()函数设置的为准(因为该函数位于 dragEnterEvent()之后执行)。
-
在 dropEvent()函数内最终决定对拖放的数据的处理,以及用户实际执行的拖放动作,因此该函数决定着 QDrag::exe()函数的返回值,这里要注意的是,对于移动动作,通常原始数据应由源部件(启动拖放的部件)进行删除,因此当 drogEvent()处理完数据之后,应把拖放动作设置为移动, QDrag::exec()函数会返回在 drogEvent()函数中设置的动作,然后源部件根据 QDrag::exe()的返回值是否是移动动作,而作出是否删除原始数据的决定。注: dragEnterEvent()和 dragMoveEvent()对拖放动作的设置不会影响QDrag::exe()的返回值。
-
注意:在源部件中创建的 QMimeData 和 QDrag 对象不应由程序员销毁,因为 Qt 会自动销毁,若程序员销毁了,则可能会出现多次 delete 同一个指针的错误。
-
以上只是通常在各函数中的做法,当然你也可以不按照这些步骤来实现,从之前的拖放示例可以看到,对拖放的处理完全是任意的。
-
注意: Qt 为某些部件提供了一些标准的拖放支持,在继承这些部件实现拖放时需要重新实现 dragEnterEvent()、 dropEvent(),另外还可能需要重新实现 dragMoveEvent()函数,以避免与标准实现的拖放支持相冲突或产生预料之外的结果。
5. 拖放动作示例程序:
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout
from PyQt5 import QtGui
from PyQt5.QtGui import QDrag
from PyQt5.QtCore import QPoint, QMimeData, Qt, qDebug, QRect
import sys
class MyButton(QPushButton):
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.point = QPoint() # 用于保存第一次点击鼠标的位置
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
self.point = e.pos()
def mouseMoveEvent(self, e: QtGui.QMouseEvent) -> None:
# 若移动的距离大于5个像素,则启动拖放(避免抖动)
if abs(e.pos().x() - self.point.x()) >= 5 or abs(e.pos().y() - self.point.y()) >= 5:
# 拖放数据准备
my_drag = QDrag(self)
my_mime = QMimeData()
my_mime.setText('hello')
my_drag.setMimeData(my_mime)
qDebug('mimedata already set')
# 启动拖放,该数据可复制、移动、链接,具体是否接受这些动作,需要由后续程序决定
# 若dropEvent函数把拖放动作设置为移动,则需要对原始数据做进一步处理,注意:在完成拖放后会返回exec()函数,然后继续执行其后的语句
supportedActions = Qt.DropActions()
supportedActions = supportedActions | Qt.DropAction.CopyAction | Qt.DropAction.MoveAction | Qt.DropAction.LinkAction
reurn_action = my_drag.exec_(supportedActions)
if reurn_action == Qt.DropAction.MoveAction:
# 本例中没有可以删除的数据,只简单输出字符串
qDebug('move action executed done')
else:
qDebug(f'my_drag.exec_ return: {reurn_action}')
def dragEnterEvent(self, e: QtGui.QDragEnterEvent) -> None:
# 若拖动的数据中不包含文本hello,则忽略该事件,否则接受该事件
qDebug('dragEnterEvent')
if e.mimeData().text() != 'hello':
e.ignore()
e.accept()
def dragMoveEvent(self, e: QtGui.QDragMoveEvent) -> None:
qDebug('dragMoveEvent')
# 以下设置会改变鼠标光标的外观。若拖动的同时按下了CTRL/ALT/SHIFT键,则把拖放动作设置为复制、移动、链接,否则为复制
if e.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier:
qDebug('set CopyAction')
e.setDropAction(Qt.DropAction.CopyAction)
elif e.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
qDebug('set MoveAction')
e.setDropAction(Qt.DropAction.MoveAction)
elif e.keyboardModifiers() == Qt.KeyboardModifier.AltModifier:
qDebug('set LinkAction')
e.setDropAction(Qt.DropAction.LinkAction)
else:
qDebug('set default CopyAction')
e.setDropAction(Qt.DropAction.CopyAction)
# 若光标位于button自身矩形r内,则接受该事件,否则忽略该事件
r = QRect(0, 0, self.rect().width(), self.rect().height())
qDebug(f'r.x={r.x()}, r.y={r.y()}, r.width={r.width()}, r.height={r.height()}')
qDebug(f'e.pos: x={e.pos().x()}, y={e.pos().y()}')
if r.contains(e.pos()):
qDebug('inside rect, accept')
e.accept()
else:
qDebug('outside rect, ignore')
e.ignore()
def dropEvent(self, e: QtGui.QDropEvent) -> None:
qDebug('dropEvent')
# 若拖动的源和目标在同一个部件,则什么也不做。注意,应该使用return跳出函数,若使用ignore或accept,程序还会继续执行之后的语句
if e.source() == self:
return
self.setText(e.mimeData().text())
qDebug('dropEvent done. will return move action')
# 把拖放动作设为移动
e.setDropAction(Qt.DropAction.MoveAction)
# 执行accept会把QDrag.exec()函数的返回值置为上面设置的移动动作
e.accept()
class MyWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
btn_a = MyButton('AAA')
btn_b = MyButton('BBB')
btn_a.setAcceptDrops(False)
btn_b.setAcceptDrops(True)
layout = QHBoxLayout()
layout.addWidget(btn_a)
layout.addWidget(btn_b)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
my_widget = MyWidget()
my_widget.show()
sys.exit(app.exec_())
初始界面
不按任何键直接拖动,或者按住ctrl键进行拖动
按住shift键进行拖动
按住alt键进行拖动
小手一抖,点个赞再走哦