I am looking for a way to create a grid of graphs that can be dragged/dropped to rearrange the order. My first try was using QDockWidgets as they allow for drag/drop, however they were limited in a lot of other ways. Would it be possible to implement this function in a QGridLayout?
For now I have a QGridLayout with 3x3 matplotlib widgets.
Here is an example of the desired layout outcome.
Sample code:
import sys
from PyQt5 import QtWidgets
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
import random
from PyQt5.QtWidgets import QGridLayout, QVBoxLayout, QHBoxLayout, QScrollArea, QWidget, QDialog, QApplication, QFrame
class IndicSelectWindow(QDialog):
def __init__(self, parent=None):
super(IndicSelectWindow, self).__init__(parent=parent)
self.resize(1000, 800)
self.layout = QtWidgets.QHBoxLayout(self)
self.scrollArea = QScrollArea(self)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.gridLayout = QGridLayout(self.scrollAreaWidgetContents)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.layout.addWidget(self.scrollArea)
for i in range(3):
for j in range(3):
self.Frame = QFrame(self)
self.Frame.setStyleSheet("background-color: white;")
self.Frame.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.Frame.setLineWidth(2)
self.layout = QHBoxLayout(self.Frame)
self.figure = Figure() # a figure to plot on
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111) # create an axis
data = [random.random() for i in range(10)]
self.ax.plot(data, '*-') # plot data
self.canvas.draw() # refresh canvas
self.layout.addWidget(self.canvas)
Box = QVBoxLayout()
Box.addWidget(self.Frame)
self.gridLayout.addLayout(Box, i, j)
self.gridLayout.setColumnStretch(i % 3, 1)
self.gridLayout.setRowStretch(j, 1)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = IndicSelectWindow()
w.show()
sys.exit(app.exec_())
Here is an implementation that will swap the positions of the items involved in a drag/drop. The 3 main steps are:
(1) Reimplement mousePressEvent
to get the index of the LayoutItem
based on mouse coordinates.
(2) Reimplement mouseMoveEvent
to set up a QDrag
of the FigureCanvas.
(3) Reimplement dropEvent
to swap the target items in the layout.
Since the matplotlib widgets absorb mouse events you also need to reimplement eventFilter
to detect them.
import sys, random from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * from matplotlib.figure import Figure from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas class IndicSelectWindow(QDialog): def __init__(self, parent=None): super(IndicSelectWindow, self).__init__(parent=parent) self.resize(1000, 800) self.target = None self.setAcceptDrops(True) self.layout = QHBoxLayout(self) self.scrollArea = QScrollArea(self) self.scrollArea.setWidgetResizable(True) self.scrollAreaWidgetContents = QWidget() self.gridLayout = QGridLayout(self.scrollAreaWidgetContents) self.scrollArea.setWidget(self.scrollAreaWidgetContents) self.layout.addWidget(self.scrollArea) for i in range(3): for j in range(3): self.Frame = QFrame(self) self.Frame.setStyleSheet("background-color: white;") self.Frame.setFrameStyle(QFrame.Panel | QFrame.Raised) self.Frame.setLineWidth(2) self.layout = QHBoxLayout(self.Frame) self.figure = Figure() # a figure to plot on self.canvas = FigureCanvas(self.figure) self.ax = self.figure.add_subplot(111) # create an axis data = [random.random() for i in range(10)] self.ax.plot(data, '*-') # plot data self.canvas.draw() # refresh canvas self.canvas.installEventFilter(self) self.layout.addWidget(self.canvas) Box = QVBoxLayout() Box.addWidget(self.Frame) self.gridLayout.addLayout(Box, i, j) self.gridLayout.setColumnStretch(i % 3, 1) self.gridLayout.setRowStretch(j, 1) def eventFilter(self, watched, event): if event.type() == QEvent.MouseButtonPress: self.mousePressEvent(event) elif event.type() == QEvent.MouseMove: self.mouseMoveEvent(event) elif event.type() == QEvent.MouseButtonRelease: self.mouseReleaseEvent(event) return super().eventFilter(watched, event) def get_index(self, pos): for i in range(self.gridLayout.count()): if self.gridLayout.itemAt(i).geometry().contains(pos) and i != self.target: return i def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.target = self.get_index(event.windowPos().toPoint()) else: self.target = None def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton and self.target is not None: drag = QDrag(self.gridLayout.itemAt(self.target)) pix = self.gridLayout.itemAt(self.target).itemAt(0).widget().grab() mimedata = QMimeData() mimedata.setImageData(pix) drag.setMimeData(mimedata) drag.setPixmap(pix) drag.setHotSpot(event.pos()) drag.exec_() def mouseReleaseEvent(self, event): self.target = None def dragEnterEvent(self, event): if event.mimeData().hasImage(): event.accept() else: event.ignore() def dropEvent(self, event): if not event.source().geometry().contains(event.pos()): source = self.get_index(event.pos()) if source is None: return i, j = max(self.target, source), min(self.target, source) p1, p2 = self.gridLayout.getItemPosition(i), self.gridLayout.getItemPosition(j) self.gridLayout.addItem(self.gridLayout.takeAt(i), *p2) self.gridLayout.addItem(self.gridLayout.takeAt(j), *p1) if __name__ == '__main__': app = QApplication(sys.argv) w = IndicSelectWindow() w.show() sys.exit(app.exec_())