Drag and drop widgets onto a MS word-like a4 page
-
Hi,
I'm developing a plotting app, and for reporting purposes, I would like to have the functionality to drag chart types onto a page that looks like an MS Word-like page.
I would not want the page to be a text edit widget, but rather some sort of grid so that the drops snap in place at some given resolution. Any text should come from a draggable text edit widget added to the page.
I know how to implement drag/drop part, the widgets, the rendering, and all.
But I have no clue about how to make an MS Word-like page with A4 proportions, and how to drop widgets at snapping points. I think all the other rearrangement logic etc. will fall into place once I have the page set up.I tried some experiments with a QGridLayout, but obviously, that's the wrong choice because I don't think it's wise to predefine sizes of layouts if it's even possible. I think something with a graphics scene maybe?
Here's some code to at least get a gist of the way I would like to drag widgets in another field:
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGridLayout, QScrollArea from PyQt6 import QtGui, QtCore from PyQt6.QtCore import Qt, QMimeData from PyQt6.QtGui import QDrag, QPixmap class DragButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: # Not sure if the conditional here is needed (yet) if event.buttons() == QtCore.Qt.MouseButton.LeftButton: drag = QDrag(self) mime_data = QMimeData() drag.setMimeData(mime_data) drag.setHotSpot(event.pos()) # Code to show what is being dragged 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) self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() # We want something like a ScrollArea?? self.scrollArea = QScrollArea(self) self.scrollArea.setWidgetResizable(True) self.scrollAreaWidgetContents = QWidget() self.gridLayout = QGridLayout(self.scrollAreaWidgetContents) self.scrollArea.setWidget(self.scrollAreaWidgetContents) for button in ["A", "B", "C", "D"]: _button = DragButton(button) self.button_layout.addWidget(_button) self.main_layout.addLayout(self.button_layout) self.main_layout.addWidget(self.scrollArea) self.setLayout(self.main_layout) # Test self.column = 3 self.row = 3 def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: event.accept() def dropEvent(self, event: QtGui.QDropEvent) -> None: widget = event.source() # Know which widget is triggering print(f"Widget:\t{widget}") event.setDropAction(Qt.DropAction.MoveAction) button = QPushButton("xyz") self.gridLayout.addWidget(button, self.column, self.row) self.column += 1 # self.row += 1 event.accept() app = QApplication([]) w = Window() w.show() app.exec()
Can anyone help me along? Just dropping clues would be welcome as well, the Qt Framework is a bit overwhelming to me.
Regards,
Roddy
-
Hi,
I'm developing a plotting app, and for reporting purposes, I would like to have the functionality to drag chart types onto a page that looks like an MS Word-like page.
I would not want the page to be a text edit widget, but rather some sort of grid so that the drops snap in place at some given resolution. Any text should come from a draggable text edit widget added to the page.
I know how to implement drag/drop part, the widgets, the rendering, and all.
But I have no clue about how to make an MS Word-like page with A4 proportions, and how to drop widgets at snapping points. I think all the other rearrangement logic etc. will fall into place once I have the page set up.I tried some experiments with a QGridLayout, but obviously, that's the wrong choice because I don't think it's wise to predefine sizes of layouts if it's even possible. I think something with a graphics scene maybe?
Here's some code to at least get a gist of the way I would like to drag widgets in another field:
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGridLayout, QScrollArea from PyQt6 import QtGui, QtCore from PyQt6.QtCore import Qt, QMimeData from PyQt6.QtGui import QDrag, QPixmap class DragButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: # Not sure if the conditional here is needed (yet) if event.buttons() == QtCore.Qt.MouseButton.LeftButton: drag = QDrag(self) mime_data = QMimeData() drag.setMimeData(mime_data) drag.setHotSpot(event.pos()) # Code to show what is being dragged 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) self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() # We want something like a ScrollArea?? self.scrollArea = QScrollArea(self) self.scrollArea.setWidgetResizable(True) self.scrollAreaWidgetContents = QWidget() self.gridLayout = QGridLayout(self.scrollAreaWidgetContents) self.scrollArea.setWidget(self.scrollAreaWidgetContents) for button in ["A", "B", "C", "D"]: _button = DragButton(button) self.button_layout.addWidget(_button) self.main_layout.addLayout(self.button_layout) self.main_layout.addWidget(self.scrollArea) self.setLayout(self.main_layout) # Test self.column = 3 self.row = 3 def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: event.accept() def dropEvent(self, event: QtGui.QDropEvent) -> None: widget = event.source() # Know which widget is triggering print(f"Widget:\t{widget}") event.setDropAction(Qt.DropAction.MoveAction) button = QPushButton("xyz") self.gridLayout.addWidget(button, self.column, self.row) self.column += 1 # self.row += 1 event.accept() app = QApplication([]) w = Window() w.show() app.exec()
Can anyone help me along? Just dropping clues would be welcome as well, the Qt Framework is a bit overwhelming to me.
Regards,
Roddy
An update. So I have a graphicsview and graphicsscene.
The peculiar part is that I cannot drop into the scene, only outside of it in the view. It does get added to the scene when dropping.
How to resolve this issue, and basically inverse this behavior (not droppable outside of scene, droppable inside it).?
Code below:
import math from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGridLayout, QScrollArea, QGraphicsGridLayout, QGraphicsView, QGraphicsScene from PyQt6 import QtGui, QtCore from PyQt6.QtCore import Qt, QMimeData from PyQt6.QtGui import QDrag, QPixmap class CustomGraphicsScene(QGraphicsScene): def __init__(self, parent=None): super(QGraphicsScene, self).__init__(parent) # def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: # event.accept() # # def dropEvent(self, event: QtGui.QDropEvent) -> None: # widget = event.source() # Know which widget is triggering # print(f"Widget:\t{widget}") # # event.setDropAction(Qt.DropAction.MoveAction) # # button = QPushButton("xyz") # # # widget.setParent(self.graphics_view) # # self.addWidget(button) # event.accept() class DragButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: # Not sure if the conditional here is needed (yet) if event.buttons() == QtCore.Qt.MouseButton.LeftButton: # Must use buttons(), not button() drag = QDrag(self) mime_data = QMimeData() drag.setMimeData(mime_data) drag.setHotSpot(event.pos()) # Code to show what is being dragged pixmap = QPixmap(self.size()) self.render(pixmap) drag.setPixmap(pixmap) drag.exec(Qt.DropAction.MoveAction) # Event loop blocking the main loop until complete class Window(QWidget): def __init__(self): super().__init__() self.setAcceptDrops(True) self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() self.graphics_scene = CustomGraphicsScene(parent=self) # Make a graphics view and add it to the main_layout as the 2nd widget self.graphics_view = QGraphicsView() self.graphics_view.setScene(self.graphics_scene) self.graphics_view.setAcceptDrops(True) page_size = 300 self.graphics_view.setFixedSize(page_size, int(page_size*(math.sqrt(2)))) # We want something like a ScrollArea?? self.scrollArea = QScrollArea(self) self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidget(self.graphics_view) for button in ["A", "B", "C", "D"]: _button = DragButton(button) self.button_layout.addWidget(_button) self.main_layout.addLayout(self.button_layout) self.main_layout.addWidget(self.scrollArea) self.setLayout(self.main_layout) # Test self.column = 3 self.row = 3 def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: event.accept() def dropEvent(self, event: QtGui.QDropEvent) -> None: widget = event.source() # Know which widget is triggering print(f"Widget:\t{widget}") position = event.position() event.setDropAction(Qt.DropAction.MoveAction) button = QPushButton("xyz") self.graphics_scene.addWidget(button) event.accept() app = QApplication([]) w = Window() w.show() app.exec()
-
An update. So I have a graphicsview and graphicsscene.
The peculiar part is that I cannot drop into the scene, only outside of it in the view. It does get added to the scene when dropping.
How to resolve this issue, and basically inverse this behavior (not droppable outside of scene, droppable inside it).?
Code below:
import math from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGridLayout, QScrollArea, QGraphicsGridLayout, QGraphicsView, QGraphicsScene from PyQt6 import QtGui, QtCore from PyQt6.QtCore import Qt, QMimeData from PyQt6.QtGui import QDrag, QPixmap class CustomGraphicsScene(QGraphicsScene): def __init__(self, parent=None): super(QGraphicsScene, self).__init__(parent) # def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: # event.accept() # # def dropEvent(self, event: QtGui.QDropEvent) -> None: # widget = event.source() # Know which widget is triggering # print(f"Widget:\t{widget}") # # event.setDropAction(Qt.DropAction.MoveAction) # # button = QPushButton("xyz") # # # widget.setParent(self.graphics_view) # # self.addWidget(button) # event.accept() class DragButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: # Not sure if the conditional here is needed (yet) if event.buttons() == QtCore.Qt.MouseButton.LeftButton: # Must use buttons(), not button() drag = QDrag(self) mime_data = QMimeData() drag.setMimeData(mime_data) drag.setHotSpot(event.pos()) # Code to show what is being dragged pixmap = QPixmap(self.size()) self.render(pixmap) drag.setPixmap(pixmap) drag.exec(Qt.DropAction.MoveAction) # Event loop blocking the main loop until complete class Window(QWidget): def __init__(self): super().__init__() self.setAcceptDrops(True) self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() self.graphics_scene = CustomGraphicsScene(parent=self) # Make a graphics view and add it to the main_layout as the 2nd widget self.graphics_view = QGraphicsView() self.graphics_view.setScene(self.graphics_scene) self.graphics_view.setAcceptDrops(True) page_size = 300 self.graphics_view.setFixedSize(page_size, int(page_size*(math.sqrt(2)))) # We want something like a ScrollArea?? self.scrollArea = QScrollArea(self) self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidget(self.graphics_view) for button in ["A", "B", "C", "D"]: _button = DragButton(button) self.button_layout.addWidget(_button) self.main_layout.addLayout(self.button_layout) self.main_layout.addWidget(self.scrollArea) self.setLayout(self.main_layout) # Test self.column = 3 self.row = 3 def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: event.accept() def dropEvent(self, event: QtGui.QDropEvent) -> None: widget = event.source() # Know which widget is triggering print(f"Widget:\t{widget}") position = event.position() event.setDropAction(Qt.DropAction.MoveAction) button = QPushButton("xyz") self.graphics_scene.addWidget(button) event.accept() app = QApplication([]) w = Window() w.show() app.exec()
Hi and welcome to devnet,
You need to implement the dragMoveEvent as well.
-
@SGaist To me it seems like it's because the scene cannot accept drops.
How would the dragMoveEvent suddenly make me be able to drop into the scene, when without it I can drop anywhere but the scene, so long as I set setAcceptDrops to True?
-
@SGaist To me it seems like it's because the scene cannot accept drops.
How would the dragMoveEvent suddenly make me be able to drop into the scene, when without it I can drop anywhere but the scene, so long as I set setAcceptDrops to True?
If you want custom drag and drop you must do it at the view level. The scene handles scene related drag and drop.
dragEnterEvent enables the handling of DnD, dragMoveEvent, handles where it should happen and then dropEvent for the actual drop.
-
If you want custom drag and drop you must do it at the view level. The scene handles scene related drag and drop.
dragEnterEvent enables the handling of DnD, dragMoveEvent, handles where it should happen and then dropEvent for the actual drop.
@SGaist Got it to work. Thanks a lot!
-
@SGaist Got it to work. Thanks a lot!
@MightyDigits you're welcome !
Since you have it working now, please mark the thread as solved using the "Topic Tools" button or the three doted menu beside the answer you deem correct so other forum members may know a solution has been found :-)
-
-
@MightyDigits you're welcome !
Since you have it working now, please mark the thread as solved using the "Topic Tools" button or the three doted menu beside the answer you deem correct so other forum members may know a solution has been found :-)
@SGaist Share the code as well?
-
@SGaist Share the code as well?
@MightyDigits that would be nice as well ! It might help other people having the same issue :-)
-
@MightyDigits that would be nice as well ! It might help other people having the same issue :-)
Sure, below is the code.
Still having trouble with dropping the buttons at the right place. I have experience mapping ROI's from pyqtgraph into views, but PyQt is different. Tried all mapping options and followed some examples, but nothing is giving me what I want. Some of these little functionalities takes hours and hours without success...
The coordinates of the view/scene and buttons do not align, so how to achieve that?
import math from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGridLayout, QScrollArea, QGraphicsGridLayout, QGraphicsView, QGraphicsScene from PyQt6 import QtGui, QtCore from PyQt6.QtCore import Qt, QMimeData from PyQt6.QtGui import QDrag, QPixmap class CustomGraphicsView(QGraphicsView): def __init__(self, parent=None): super(QGraphicsView, self).__init__(parent) self.setAcceptDrops(True) def dragMoveEvent(self, event: QtGui.QDragMoveEvent) -> None: if event.mimeData().hasImage(): event.acceptProposedAction() def dropEvent(self, event): print("In dropEvent of VIEW") if self.scene(): event.accept() button = DragButton("XYZ") point = button.mapFromGlobal(event.position()) try: button.move(point) self.scene().addWidget(button) print("Button moved") except Exception as e: print(e) event.acceptProposedAction() class CustomGraphicsScene(QGraphicsScene): def __init__(self, parent=None): super(QGraphicsScene, self).__init__(parent) class DragButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: print("In mousemovevent of dragbutton") drag = QDrag(self) mime_data = QMimeData() mime_data.setText(f"{event.pos().x()}, {event.pos().y()}") drag.setMimeData(mime_data) drag.setHotSpot(event.pos()) pixmap = QPixmap(self.size()) self.render(pixmap) drag.setPixmap(pixmap) drag.exec(Qt.DropAction.MoveAction) # Event loop blocking the main loop until complete class Window(QWidget): def __init__(self): super().__init__() self.setFixedSize(960, 540) self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() self.graphics_scene = CustomGraphicsScene(parent=self) self.graphics_view = CustomGraphicsView(self) self.graphics_view.setScene(self.graphics_scene) page_size = 300 self.graphics_view.setFixedSize(page_size, int(page_size*(math.sqrt(2)))) self.scrollArea = QScrollArea(self) self.scrollArea.setAcceptDrops(True) self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidget(self.graphics_view) for button in ["A", "B", "C", "D"]: _button = DragButton(button) self.button_layout.addWidget(_button) self.main_layout.addLayout(self.button_layout) self.main_layout.addWidget(self.scrollArea) self.setLayout(self.main_layout) app = QApplication([]) w = Window() w.show() app.exec()
-
Sure, below is the code.
Still having trouble with dropping the buttons at the right place. I have experience mapping ROI's from pyqtgraph into views, but PyQt is different. Tried all mapping options and followed some examples, but nothing is giving me what I want. Some of these little functionalities takes hours and hours without success...
The coordinates of the view/scene and buttons do not align, so how to achieve that?
import math from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget, QPushButton, QGridLayout, QScrollArea, QGraphicsGridLayout, QGraphicsView, QGraphicsScene from PyQt6 import QtGui, QtCore from PyQt6.QtCore import Qt, QMimeData from PyQt6.QtGui import QDrag, QPixmap class CustomGraphicsView(QGraphicsView): def __init__(self, parent=None): super(QGraphicsView, self).__init__(parent) self.setAcceptDrops(True) def dragMoveEvent(self, event: QtGui.QDragMoveEvent) -> None: if event.mimeData().hasImage(): event.acceptProposedAction() def dropEvent(self, event): print("In dropEvent of VIEW") if self.scene(): event.accept() button = DragButton("XYZ") point = button.mapFromGlobal(event.position()) try: button.move(point) self.scene().addWidget(button) print("Button moved") except Exception as e: print(e) event.acceptProposedAction() class CustomGraphicsScene(QGraphicsScene): def __init__(self, parent=None): super(QGraphicsScene, self).__init__(parent) class DragButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None: print("In mousemovevent of dragbutton") drag = QDrag(self) mime_data = QMimeData() mime_data.setText(f"{event.pos().x()}, {event.pos().y()}") drag.setMimeData(mime_data) drag.setHotSpot(event.pos()) pixmap = QPixmap(self.size()) self.render(pixmap) drag.setPixmap(pixmap) drag.exec(Qt.DropAction.MoveAction) # Event loop blocking the main loop until complete class Window(QWidget): def __init__(self): super().__init__() self.setFixedSize(960, 540) self.main_layout = QVBoxLayout() self.button_layout = QHBoxLayout() self.graphics_scene = CustomGraphicsScene(parent=self) self.graphics_view = CustomGraphicsView(self) self.graphics_view.setScene(self.graphics_scene) page_size = 300 self.graphics_view.setFixedSize(page_size, int(page_size*(math.sqrt(2)))) self.scrollArea = QScrollArea(self) self.scrollArea.setAcceptDrops(True) self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidget(self.graphics_view) for button in ["A", "B", "C", "D"]: _button = DragButton(button) self.button_layout.addWidget(_button) self.main_layout.addLayout(self.button_layout) self.main_layout.addWidget(self.scrollArea) self.setLayout(self.main_layout) app = QApplication([]) w = Window() w.show() app.exec()
From the looks of it, you did not mapToScene.
-
From the looks of it, you did not mapToScene.
@SGaist First thing I tried was mapToScene(), but that didn't work.
What worked, is defining the sceneRect for the scene. Without it, there apparently are no native coordinates and they get set on the fly with each button drop, with the first drop being dead center in the scene.
All I had to do is add the following line:
self.graphics_scene = CustomGraphicsScene(parent=self) # ADDED THE FOLLOWING LINE: self.graphics_scene.setSceneRect(0, 0, page_size, int(page_size*math.sqrt(2))) self.graphics_view = CustomGraphicsView(self)
There's still some polishing like scrollarea showing the scrollbars, basically just not slightly showing the full rect. If I increase the size of the scrollarea, it doesn't solve it. But that stuff is for worries later on.
Thanks anyway, you're a great help!