在 PyQt6 中的 QScrollArea 中拖放小部件无法正常工作

问题描述 投票:0回答:1

我正在开发一个 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()
python user-interface drag-and-drop pyqt6
1个回答
0
投票

大多数 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 默认情况下会接受它),但如果您是从其他类型继承的,则可能是这样。这还涉及在适当的小部件上准确实现拖放处理,如上所述。
最后,设置包含

扩展小部件

(滚动区域的默认大小策略)的布局的对齐方式是没有意义的,尤其是对于窗口的主布局。

© www.soinside.com 2019 - 2024. All rights reserved.