PyQt's Graphics View framework provides a powerful way to display and interact with 2D graphics, including SVG images. In this tutorial, we'll build an application where you can drag SVG icons from a panel and drop them onto a canvas — a pattern you'll recognize from diagramming tools, flowchart builders, or visual editors.
We'll start simple, loading and displaying an SVG image, and then gradually add drag-and-drop functionality so you can move items around freely on the scene.
- What we'll be building
- Prerequisites
- Displaying an SVG in QGraphicsScene
- Making items movable on the scene
- Building the drag source: an SVG icon list
- Building the drop target: a custom QGraphicsView
- Putting it all together
- Improving the drag preview
- Adding icons to the list
- Scaling SVG items on drop
- Deleting items from the scene
- Using custom MIME types
- Summary
What we'll be building
By the end of this tutorial, you'll have an application with two panels: a list of SVG icons on the left that you can drag, and a QGraphicsScene on the right where you can drop those icons and then move them around. It's a great foundation for building any kind of visual editor.

Prerequisites
This tutorial assumes you have a basic understanding of PyQt widgets and layouts. You'll also need some SVG files to work with — any simple .svg icons will do. If you don't have any handy, you can download free icons from sites like Material Design Icons or SVG Repo.
We'll be using QGraphicsSvgItem from the QtSvg module (or QtSvgWidgets in PyQt6), so make sure you have SVG support installed. If you installed PyQt via pip, you may need to install PyQt6 along with the SVG module:
pip install PyQt6 PyQt6-QGraphicsSvgItem
Displaying an SVG in QGraphicsScene
Before we add drag-and-drop, let's make sure we can display an SVG file inside a QGraphicsScene. The class we need is QGraphicsSvgItem, which is a graphics item that can render SVG content.
- PyQt6
- PySide6
import sys
from PyQt6.QtWidgets import (
QApplication, QGraphicsScene, QGraphicsView, QMainWindow
)
from PyQt6.QtSvgWidgets import QGraphicsSvgItem
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("SVG on QGraphicsScene")
scene = QGraphicsScene()
item = QGraphicsSvgItem("icon.svg")
scene.addItem(item)
view = QGraphicsView(scene)
self.setCentralWidget(view)
self.resize(600, 400)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
import sys
from PySide6.QtWidgets import (
QApplication, QGraphicsScene, QGraphicsView, QMainWindow
)
from PySide6.QtSvgWidgets import QGraphicsSvgItem
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("SVG on QGraphicsScene")
scene = QGraphicsScene()
item = QGraphicsSvgItem("icon.svg")
scene.addItem(item)
view = QGraphicsView(scene)
self.setCentralWidget(view)
self.resize(600, 400)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Replace "icon.svg" with the path to any SVG file you have available. When you run this, you should see your SVG rendered in the center of the window. The QGraphicsView widget acts as a viewport into the scene, and the QGraphicsSvgItem handles all the SVG rendering for us.
Everything else stays the same.
Making items movable on the scene
One of the nice things about the Graphics View framework is that making items movable is a single flag. Let's enable that so we can drag our SVG around the scene after it's placed.
item = QGraphicsSvgItem("icon.svg")
item.setFlag(QGraphicsSvgItem.GraphicsItemFlag.ItemIsMovable)
item.setFlag(QGraphicsSvgItem.GraphicsItemFlag.ItemIsSelectable)
scene.addItem(item)
Now you can click and drag the SVG item around the scene. The ItemIsSelectable flag is optional but useful — it lets you click to select items, which gives visual feedback and is handy when you have multiple items.
Building the drag source: an SVG icon list
Now we need something to drag from. We'll create a list widget on the left side of our window that shows available SVG icons. When the user drags an icon from this list, we want to carry along enough information to create a new QGraphicsSvgItem on the scene.
QListWidget supports drag operations out of the box. We just need to configure it and attach the right data to each item.
- PyQt6
- PySide6
import os
from PyQt6.QtWidgets import QListWidget, QListWidgetItem
from PyQt6.QtCore import Qt, QMimeData, QByteArray
from PyQt6.QtGui import QDrag, QPixmap
class SvgListWidget(QListWidget):
def __init__(self, svg_files, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
self.setIconSize(QPixmap(48, 48).size())
for filepath in svg_files:
name = os.path.basename(filepath)
item = QListWidgetItem(name)
item.setData(Qt.ItemDataRole.UserRole, filepath)
self.addItem(item)
def startDrag(self, supportedActions):
item = self.currentItem()
if item is None:
return
filepath = item.data(Qt.ItemDataRole.UserRole)
mime_data = QMimeData()
mime_data.setText(filepath)
drag = QDrag(self)
drag.setMimeData(mime_data)
# Create a small pixmap as the drag indicator
pixmap = QPixmap(48, 48)
pixmap.fill(Qt.GlobalColor.transparent)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.CopyAction)
import os
from PySide6.QtWidgets import QListWidget, QListWidgetItem
from PySide6.QtCore import Qt, QMimeData, QByteArray
from PySide6.QtGui import QDrag, QPixmap
class SvgListWidget(QListWidget):
def __init__(self, svg_files, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
self.setIconSize(QPixmap(48, 48).size())
for filepath in svg_files:
name = os.path.basename(filepath)
item = QListWidgetItem(name)
item.setData(Qt.ItemDataRole.UserRole, filepath)
self.addItem(item)
def startDrag(self, supportedActions):
item = self.currentItem()
if item is None:
return
filepath = item.data(Qt.ItemDataRole.UserRole)
mime_data = QMimeData()
mime_data.setText(filepath)
drag = QDrag(self)
drag.setMimeData(mime_data)
# Create a small pixmap as the drag indicator
pixmap = QPixmap(48, 48)
pixmap.fill(Qt.GlobalColor.transparent)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.CopyAction)
This widget takes a list of SVG file paths and displays them. When you start dragging an item, it creates a QDrag object with the file path stored in the MIME data. We're using plain text (setText) to keep things simple — the drop target will read this text to know which SVG file to load.
The startDrag method is called automatically by Qt when the user begins a drag gesture on the list.
Building the drop target: a custom QGraphicsView
Next, we need the QGraphicsView to accept drops. By default, graphics views don't accept drag-and-drop events, so we need to subclass it and handle the relevant events ourselves.
There are three events to handle:
dragEnterEvent— called when a drag enters the view. We check if we can handle the data and accept it.dragMoveEvent— called as the drag moves within the view. We accept the event to allow the drop.dropEvent— called when the user releases the mouse. This is where we create the SVG item.
- PyQt6
- PySide6
from PyQt6.QtWidgets import QGraphicsView
class SvgDropGraphicsView(QGraphicsView):
def __init__(self, scene, parent=None):
super().__init__(scene, parent)
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dragMoveEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dropEvent(self, event):
filepath = event.mimeData().text()
if not filepath:
return
# Map the drop position from the view to the scene
scene_pos = self.mapToScene(event.position().toPoint())
item = QGraphicsSvgItem(filepath)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsMovable
)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsSelectable
)
item.setPos(scene_pos)
self.scene().addItem(item)
event.acceptProposedAction()
from PySide6.QtWidgets import QGraphicsView
class SvgDropGraphicsView(QGraphicsView):
def __init__(self, scene, parent=None):
super().__init__(scene, parent)
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dragMoveEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dropEvent(self, event):
filepath = event.mimeData().text()
if not filepath:
return
# Map the drop position from the view to the scene
scene_pos = self.mapToScene(event.position().toPoint())
item = QGraphicsSvgItem(filepath)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsMovable
)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsSelectable
)
item.setPos(scene_pos)
self.scene().addItem(item)
event.acceptProposedAction()
The important detail here is mapToScene. When the user drops something, the event gives us the position in view coordinates (pixels on the widget). But we need to place the item in scene coordinates, which may be different if the view is scrolled or scaled. The mapToScene method handles that conversion for us.
The event.position() method returns a QPointF (hence the .toPoint() call).
Putting it all together
Now let's assemble everything into a complete application. We'll use a QHBoxLayout with the SVG list on the left and the graphics view on the right.
- PyQt6
- PySide6
import sys
import os
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QHBoxLayout,
QGraphicsScene, QGraphicsView, QListWidget,
QListWidgetItem,
)
from PyQt6.QtSvgWidgets import QGraphicsSvgItem
from PyQt6.QtCore import Qt, QMimeData
from PyQt6.QtGui import QDrag, QPixmap
class SvgListWidget(QListWidget):
"""A list widget that shows SVG files and supports dragging."""
def __init__(self, svg_files, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
for filepath in svg_files:
name = os.path.basename(filepath)
item = QListWidgetItem(name)
item.setData(Qt.ItemDataRole.UserRole, filepath)
self.addItem(item)
def startDrag(self, supportedActions):
item = self.currentItem()
if item is None:
return
filepath = item.data(Qt.ItemDataRole.UserRole)
mime_data = QMimeData()
mime_data.setText(filepath)
drag = QDrag(self)
drag.setMimeData(mime_data)
pixmap = QPixmap(48, 48)
pixmap.fill(Qt.GlobalColor.transparent)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.CopyAction)
class SvgDropGraphicsView(QGraphicsView):
"""A graphics view that accepts SVG file drops."""
def __init__(self, scene, parent=None):
super().__init__(scene, parent)
self.setAcceptDrops(True)
self.setRenderHints(
self.renderHints()
)
def dragEnterEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dragMoveEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dropEvent(self, event):
filepath = event.mimeData().text()
if not filepath:
return
scene_pos = self.mapToScene(
event.position().toPoint()
)
item = QGraphicsSvgItem(filepath)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsMovable
)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsSelectable
)
item.setPos(scene_pos)
self.scene().addItem(item)
event.acceptProposedAction()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("SVG Drag & Drop")
# Find SVG files in the current directory
svg_files = [
f for f in os.listdir(".")
if f.lower().endswith(".svg")
]
# Create the widgets
self.svg_list = SvgListWidget(svg_files)
self.svg_list.setFixedWidth(200)
self.scene = QGraphicsScene()
self.view = SvgDropGraphicsView(self.scene)
# Layout
container = QWidget()
layout = QHBoxLayout(container)
layout.addWidget(self.svg_list)
layout.addWidget(self.view)
self.setCentralWidget(container)
self.resize(800, 600)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
import sys
import os
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QHBoxLayout,
QGraphicsScene, QGraphicsView, QListWidget,
QListWidgetItem,
)
from PySide6.QtSvgWidgets import QGraphicsSvgItem
from PySide6.QtCore import Qt, QMimeData
from PySide6.QtGui import QDrag, QPixmap
class SvgListWidget(QListWidget):
"""A list widget that shows SVG files and supports dragging."""
def __init__(self, svg_files, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
for filepath in svg_files:
name = os.path.basename(filepath)
item = QListWidgetItem(name)
item.setData(Qt.ItemDataRole.UserRole, filepath)
self.addItem(item)
def startDrag(self, supportedActions):
item = self.currentItem()
if item is None:
return
filepath = item.data(Qt.ItemDataRole.UserRole)
mime_data = QMimeData()
mime_data.setText(filepath)
drag = QDrag(self)
drag.setMimeData(mime_data)
pixmap = QPixmap(48, 48)
pixmap.fill(Qt.GlobalColor.transparent)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.CopyAction)
class SvgDropGraphicsView(QGraphicsView):
"""A graphics view that accepts SVG file drops."""
def __init__(self, scene, parent=None):
super().__init__(scene, parent)
self.setAcceptDrops(True)
self.setRenderHints(
self.renderHints()
)
def dragEnterEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dragMoveEvent(self, event):
if event.mimeData().hasText():
event.acceptProposedAction()
def dropEvent(self, event):
filepath = event.mimeData().text()
if not filepath:
return
scene_pos = self.mapToScene(
event.position().toPoint()
)
item = QGraphicsSvgItem(filepath)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsMovable
)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsSelectable
)
item.setPos(scene_pos)
self.scene().addItem(item)
event.acceptProposedAction()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("SVG Drag & Drop")
# Find SVG files in the current directory
svg_files = [
f for f in os.listdir(".")
if f.lower().endswith(".svg")
]
# Create the widgets
self.svg_list = SvgListWidget(svg_files)
self.svg_list.setFixedWidth(200)
self.scene = QGraphicsScene()
self.view = SvgDropGraphicsView(self.scene)
# Layout
container = QWidget()
layout = QHBoxLayout(container)
layout.addWidget(self.svg_list)
layout.addWidget(self.view)
self.setCentralWidget(container)
self.resize(800, 600)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Place a few .svg files in the same directory as your script and run it. You should see the filenames listed on the left. Drag one onto the canvas and the SVG will appear where you drop it. You can then click and drag the placed items to rearrange them on the scene.
Improving the drag preview
Right now, the drag indicator is just a transparent square — functional but not very informative. Let's render a preview of the actual SVG so the user can see what they're about to drop.
We can use Qt's SVG renderer to paint the SVG onto a QPixmap:
- PyQt6
- PySide6
from PyQt6.QtSvg import QSvgRenderer
from PyQt6.QtGui import QPainter
def render_svg_to_pixmap(filepath, size=64):
renderer = QSvgRenderer(filepath)
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return pixmap
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtGui import QPainter
def render_svg_to_pixmap(filepath, size=64):
renderer = QSvgRenderer(filepath)
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return pixmap
Now update the startDrag method to use this function:
def startDrag(self, supportedActions):
item = self.currentItem()
if item is None:
return
filepath = item.data(Qt.ItemDataRole.UserRole)
mime_data = QMimeData()
mime_data.setText(filepath)
drag = QDrag(self)
drag.setMimeData(mime_data)
pixmap = render_svg_to_pixmap(filepath, 48)
drag.setPixmap(pixmap)
drag.setHotSpot(pixmap.rect().center())
drag.exec(Qt.DropAction.CopyAction)
Now update the startDrag method to use this function:
def startDrag(self, supportedActions):
item = self.currentItem()
if item is None:
return
filepath = item.data(Qt.ItemDataRole.UserRole)
mime_data = QMimeData()
mime_data.setText(filepath)
drag = QDrag(self)
drag.setMimeData(mime_data)
pixmap = render_svg_to_pixmap(filepath, 48)
drag.setPixmap(pixmap)
drag.setHotSpot(pixmap.rect().center())
drag.exec(Qt.DropAction.CopyAction)
The setHotSpot call positions the cursor at the center of the preview image, which feels more natural when dragging.
Adding icons to the list
While we're at it, we can also show the SVG preview as an icon in the list itself, making it much easier to identify each item:
- PyQt6
- PySide6
from PyQt6.QtGui import QIcon
class SvgListWidget(QListWidget):
def __init__(self, svg_files, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
self.setIconSize(QPixmap(48, 48).size())
for filepath in svg_files:
name = os.path.basename(filepath)
pixmap = render_svg_to_pixmap(filepath, 48)
item = QListWidgetItem(QIcon(pixmap), name)
item.setData(Qt.ItemDataRole.UserRole, filepath)
self.addItem(item)
from PySide6.QtGui import QIcon
class SvgListWidget(QListWidget):
def __init__(self, svg_files, parent=None):
super().__init__(parent)
self.setDragEnabled(True)
self.setIconSize(QPixmap(48, 48).size())
for filepath in svg_files:
name = os.path.basename(filepath)
pixmap = render_svg_to_pixmap(filepath, 48)
item = QListWidgetItem(QIcon(pixmap), name)
item.setData(Qt.ItemDataRole.UserRole, filepath)
self.addItem(item)
Now each entry in the list shows both the SVG icon and the filename.
Scaling SVG items on drop
SVG files come in all shapes and sizes. Some might be tiny, others might be huge. You might want to normalize items to a consistent size when they're dropped onto the scene. Since SVG is a vector format, scaling is clean and lossless.
You can scale a QGraphicsSvgItem using its setScale method or by calculating a transform based on its bounding rectangle:
def dropEvent(self, event):
filepath = event.mimeData().text()
if not filepath:
return
scene_pos = self.mapToScene(
event.position().toPoint()
)
item = QGraphicsSvgItem(filepath)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsMovable
)
item.setFlag(
QGraphicsSvgItem.GraphicsItemFlag.ItemIsSelectable
)
# Scale to a target size
target_size = 64
bounds = item.boundingRect()
max_dim = max(bounds.width(), bounds.height())
if max_dim > 0:
scale_factor = target_size / max_dim
item.setScale(scale_factor)
item.setPos(scene_pos)
self.scene().addItem(item)
event.acceptProposedAction()
This calculates a scale factor based on the largest dimension of the SVG, so every item gets normalized to roughly the same visual size on the canvas. The aspect ratio is preserved since we're using a uniform scale.
Deleting items from the scene
To make the application more usable, let's add the ability to delete selected items by pressing the Delete key. We can handle this in the graphics view:
- PyQt6
- PySide6
from PyQt6.QtCore import Qt
class SvgDropGraphicsView(QGraphicsView):
# ... existing methods ...
def keyPressEvent(self, event):
if event.key() == Qt.Key.Key_Delete:
for item in self.scene().selectedItems():
self.scene().removeItem(item)
else:
super().keyPressEvent(event)
from PySide6.QtCore import Qt
class SvgDropGraphicsView(QGraphicsView):
# ... existing methods ...
def keyPressEvent(self, event):
if event.key() == Qt.Key.Key_Delete:
for item in self.scene().selectedItems():
self.scene().removeItem(item)
else:
super().keyPressEvent(event)
Click an item to select it (you'll see a selection highlight), then press Delete to remove it from the scene. You can select multiple items by holding Ctrl and clicking.
Using custom MIME types
In our example, we used plain text (mimeData.setText()) to pass the file path during the drag operation. This works fine for a self-contained application, but if you're building something more complex — or if you want to prevent accidental drops from other applications — you can define a custom MIME type instead.
# In startDrag:
mime_data = QMimeData()
mime_data.setData(
"application/x-svg-path",
filepath.encode("utf-8")
)
# In dragEnterEvent / dragMoveEvent:
if event.mimeData().hasFormat("application/x-svg-path"):
event.acceptProposedAction()
# In dropEvent:
filepath = bytes(
event.mimeData().data("application/x-svg-path")
).decode("utf-8")
The custom MIME type application/x-svg-path ensures that only drags originating from your SVG list will be accepted by the view. Any other drag operations (like dragging text from a browser) will be ignored.
Summary
In this tutorial, you built a drag-and-drop SVG editor using PyQt's Graphics View framework. Here's what we covered:
- Displaying SVGs on a
QGraphicsSceneusingQGraphicsSvgItem - Making items movable with the
ItemIsMovableflag - Creating a drag source by subclassing
QListWidgetand overridingstartDrag - Accepting drops in a custom
QGraphicsViewby handlingdragEnterEvent,dragMoveEvent, anddropEvent - Rendering SVG previews for the drag indicator and list icons
- Scaling items to a consistent size on drop
- Deleting items with keyboard shortcuts
- Custom MIME types for more robust drag-and-drop handling
This pattern is flexible enough to serve as the foundation for diagram editors, dashboard builders, visual programming tools, or any application where users need to place and arrange graphical elements on a canvas. From here, you could add features like snapping to a grid, connecting items with lines, or saving and loading the scene layout.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PySide6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!