'Adding drag and drop to a scroll area
I'm trying to create a scroll area that will allow drag and drop in QtPy5. I want to be able to drag one label (label 1) to another label (Label 3) and have the label's switch places. (Ie 1, 2, 3, 4 ... becomes 3, 2, 1, 4 .....) So far I can get the scroll part working but I'm having trouble getting the drag and drop functional. I think the issue is I need to use setAcceptDrop(True) somewhere but I can't think where. Any help would be appreciated! Thanks.
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QMainWindow, QGridLayout, QScrollArea, QHBoxLayout, QPushButton
from PyQt5.QtCore import Qt, QMimeData, pyqtSignal
from PyQt5.QtGui import QDrag, QPixmap
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
self.setAcceptDrops(True)
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec_(Qt.MoveAction)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.scrollArea = QScrollArea()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setAcceptDrops(True)
self.setAcceptDrops(True)
self.contents = QWidget()
self.contents.setAcceptDrops(True)
self.scrollArea.setWidget(self.contents)
layout = QHBoxLayout(self.contents)
for row in range(20):
button = DragItem(str(row))
layout.addWidget(button)
def dropEvent(self, e):
pos = e.pos()
widget = e.source()
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x():
# We didn't drag past this widget.
# insert to the left of it.
self.blayout.insertWidget(n-1, widget)
break
e.accept()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
Solution 1:[1]
The reason your current code is not allowing you to drop anything is because you have not implemented the dragEnterEvent
and dragMoveEvent
. Both of them need to be accepted by the widget in order for them to accept drops. Also the drop event you have in your mainwindow
isn't actually doing anything because it is nested in your __init__
function.
This is one way you could do it:
I created a signal on the main window with two parameters identifying the indices of the widgets you want to swap. When the signal is emitted it triggers a function to swap those widgets. Then I implemented dragEnter
and dragMove
events for each of the labels and adjusted the drop event to emit the MainWindow
signal with the widgets positions in the layout.
I adjusted a few other things to make it more obvious what is happening when you run it.
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.original = int(self.text())
self.current = self.original
self.setText(f"Original: {self.original}\n Current: {self.current}")
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.setAcceptDrops(True)
def set_data(self, number):
self.current = number
self.setText(f"Original: {self.original}\n Current: {self.current}")
def dragEnterEvent(self, e):
if e.mimeData().hasImage(): e.accept()
else: e.ignore()
def dragMoveEvent(self, e):
if e.mimeData().hasImage(): e.accept()
else: e.ignore()
def dropEvent(self, e):
source_pos = e.source().current
current_pos = self.current
self.window().swap.emit(*sorted([source_pos, current_pos]))
# or instead you can extract the pixmap from the event mime data
# pixmap = e.mimeData().imageData()
# ... do something with pixmap
def mouseMoveEvent(self, e):
if e.buttons() == Qt.LeftButton:
drag = QDrag(self)
mime = QMimeData()
pixmap = QPixmap(self.size())
mime.setImageData(pixmap)
drag.setMimeData(mime)
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec_(Qt.MoveAction)
class MainWindow(QMainWindow):
swap = pyqtSignal([int,int])
def __init__(self):
super().__init__()
self.scrollArea = QScrollArea()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setAcceptDrops(True)
self.setAcceptDrops(True)
self.contents = QWidget()
self.contents.setAcceptDrops(True)
self.scrollArea.setWidget(self.contents)
self.layout = QHBoxLayout(self.contents)
for row in range(20):
button = DragItem(str(row))
self.layout.addWidget(button)
self.swap.connect(self.swap_widgets)
def swap_widgets(self, pos1, pos2):
widget1 = self.layout.itemAt(pos1).widget()
widget2 = self.layout.itemAt(pos2).widget()
self.layout.removeWidget(widget2)
self.layout.removeWidget(widget1)
self.layout.insertWidget(pos1, widget2)
self.layout.insertWidget(pos2, widget1)
widget1.set_data(pos2)
widget2.set_data(pos1)
For more details on drag and drop in qt
check out the official documentation. https://doc.qt.io/qt-5/dnd.html
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |