我正在开发一个 PyQt6 应用程序,我想在 QScrollArea 中启用小部件的拖放功能。虽然拖放功能最初可以工作,但当小部件太多时,尤其是涉及滚动时,它就会开始表现不正确。拖动的小部件并不总是落在正确的位置。
这是我的代码的相关部分:
from PyQt6.QtWidgets import QVBoxLayout, QApplication, QScrollArea, QToolButton, QWidget, QPushButton
from PyQt6.QtGui import QIcon, QDrag, QPixmap
from PyQt6.QtCore import Qt, QSize, QMimeData
class DragButton(QToolButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
# Main Layout
self.main_layout = QVBoxLayout()
self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Add Button
add_button = QPushButton(parent = self, text ='add button')
add_button.clicked.connect(self.add_button)
self.main_layout.addWidget(add_button)
# button frame/layout
self.button_layout = QVBoxLayout()
self.button_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
self.button_layout.setSpacing(30)
self.button_frame = QWidget()
self.button_frame.setLayout(self.button_layout)
# Scroll area
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll_area.setWidget(self.button_frame)
self.main_layout.addWidget(self.scroll_area)
self.setLayout(self.main_layout)
def add_button(self):
n = self.button_layout.count()
btn = DragButton(parent=self, text=f'{n}')
self.button_layout.addWidget(btn, alignment=Qt.AlignmentFlag.AlignHCenter)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position().toPoint()
widget = e.source()
# Temporarily remove the widget
self.button_layout.removeWidget(widget)
insert_at = self.button_layout.count()
for i in range(self.button_layout.count()):
w = self.button_layout.itemAt(i).widget()
if pos.y() < w.y():
insert_at = i
break
# Insert the widget back to the layout at the new position
self.button_layout.insertWidget(insert_at, widget, alignment=Qt.AlignmentFlag.AlignHCenter)
e.accept()
if __name__ == '__main__':
app = QApplication([])
window = Window()
window.show()
app.exec()
我考虑过dropEvent方法中的滚动偏移,但似乎并不能解决问题。
def dropEvent(self, e):
pos = e.position().toPoint()
widget = e.source()
# Temporarily remove the widget
self.button_layout.removeWidget(widget)
insert_at = self.button_layout.count()
scroll_value = self.scroll_area.verticalScrollBar().value()
for i in range(self.button_layout.count()):
w = self.button_layout.itemAt(i).widget()
widget_pos = w.mapToParent(w.pos())
widget_y = widget_pos.y() - scroll_value
if pos.y() < widget_y:
insert_at = i
break
# Insert the widget back to the layout at the new position
self.button_layout.insertWidget(insert_at, widget, alignment=Qt.AlignmentFlag.AlignHCenter)
e.accept()
大多数 UI 工具包和图形相关软件都基于 相对坐标系 和父/子关系(这在 OOP 中很常见)的概念。
您的第一次尝试无效,因为放置事件的位置是相对于接收它的对象(
Window
实例),而您正在检查的小部件的位置是相对于它们的父级(self.button_frame
) .
请注意,这与您使用滚动区域的事实完全无关,因为如果您刚刚将
self.button_frame
添加到主布局,也会发生同样的情况。滚动区域的使用更加表明了该方法的无效性。
您在第二次尝试中使用
mapToParent()
的尝试也因同样的原因而无效;请参阅 pos
文档:
此属性保存小部件在其父小部件中的位置
w.pos()
在父坐标中已经是,因此将其映射到父坐标是错误的,因为它将给出完全不相关的结果。
事实上,如果您这样做 w.mapToParent(QPoint())
,您将获得与
w.pos()
完全相同的值。这意味着,如果您想将小部件的位置与鼠标位置进行比较,则需要确保两者使用相同的坐标系。
您已经有了小部件的位置(基于其父级),因此您可以将鼠标映射到同一个父级。
由于 widget 结构可能是不确定的,更好的方法是通过一个
common参考系统,通常是 global 位置(相对于屏幕的位置):将本地位置映射到全局,然后映射再次发送给参考父级。
QDropEvent 不像 QMouseEvent 那样提供 globalPosition()
成员,所以我们可以只使用
mapToGlobal()
: def dropEvent(self, e):
globalPos = self.mapToGlobal(e.position())
y = self.button_frame.mapFromGlobal(globalPos).toPoint().y()
widget = e.source()
self.button_layout.removeWidget(widget)
count = self.button_layout.count()
for insert_at in range(count):
w = self.button_layout.itemAt(insert_at).widget()
if y < w.y():
break
else:
insert_at = count
self.button_layout.insertWidget(
insert_at, widget, alignment=Qt.AlignmentFlag.AlignHCenter)
e.accept()
进一步说明。
您通常应该在实际必须处理拖放事件的小部件中实现拖放事件。通常不鼓励在父级中这样做,特别是当使用许多父/子级别时,就像您的情况一样:
Window
->
scroll_area
-> 滚动区域viewport->
button_frame
。未能这样做是需要上述实现的原因,如果在正确的对象中完成拖放实现,则不需要上述实现。至少,
button_frame
dragMoveEvent()
,即使这只是
event.accept()
的问题,除非您完全确定所继承的类的默认行为。在您的情况下,这可能不是必需的(如果拖动输入是,QWidget 默认情况下会接受它),但如果您是从其他类型继承的,则可能是这样。这还涉及在适当的小部件上准确实现拖放处理,如上所述。最后,设置包含扩展小部件(滚动区域的默认大小策略)的布局的对齐方式是没有意义的,尤其是对于窗口的主布局。