How to Drag & Drop SVG Elements into a QGraphicsScene

Dragging and dropping SVG elements into a QGraphicsScene using QGraphicsSvgItem
Heads up! You've already completed this tutorial.

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

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.

Drag and drop SVG demo application

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:

bash
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.

python
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()
python
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.

python
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.

python
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)
python
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.
python
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()
python
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.

python
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()
python
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:

python
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
python
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:

python
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:

python
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:

python
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)
python
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:

python
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:

python
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)
python
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.

python
# 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 QGraphicsScene using QGraphicsSvgItem
  • Making items movable with the ItemIsMovable flag
  • Creating a drag source by subclassing QListWidget and overriding startDrag
  • Accepting drops in a custom QGraphicsView by handling dragEnterEvent, dragMoveEvent, and dropEvent
  • 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.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

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!

More info Get the book

Martin Fitzpatrick

How to Drag & Drop SVG Elements into a QGraphicsScene was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.