Making Drawn Text Clickable and Editable on a QPixmap in PySide6

How to interact with text and other elements drawn onto a pixmap using QPainter
Heads up! You've already completed this tutorial.

I'm drawing text onto a QPixmap using QPainter in PySide6. Is it possible to make this text clickable so I can modify it afterwards? There could be several items drawn, but I don't see how to select them.

When you draw text (or anything else) onto a QPixmap using QPainter, you're painting pixels directly onto a bitmap. Once the painter finishes, the text becomes part of the image — it no longer exists as a separate object you can click or edit. It's like writing on paper with a pen: the ink is now part of the paper.

That said, there are several ways to work around this and build interactive, editable graphics in PySide6. We'll look at three approaches, starting from the simplest and moving toward the most powerful.

The Starting Point

Here's a typical example of drawing text onto a QPixmap:

python
def draw_something(self):
    painter = QtGui.QPainter(self.canvas)
    pen = QtGui.QPen()
    pen.setWidth(1)
    pen.setColor(QtGui.QColor("green"))
    painter.setPen(pen)
    font = QtGui.QFont()
    font.setFamily("Times")
    font.setPointSize(40)
    painter.setFont(font)
    painter.drawText(50, 100, "Hello World")
    painter.end()
    self.label.setPixmap(self.canvas)

After painter.end() is called, the text "Hello World" is just green pixels in the pixmap. There's no text object to click on. So how do we make it interactive?

Option 1: Track Elements and Redraw the Scene

The most straightforward approach is to keep a record of everything you've drawn — position, text content, font, color — and redraw the entire pixmap from scratch whenever something changes. Each drawn element is stored as data in a list, and the pixmap is just a visual representation of that data.

When the user clicks on the pixmap, you check whether the click position falls within the bounding rectangle of any stored element. If it does, you can modify that element's data and redraw.

Here's a complete working example:

python
import sys
from PySide6 import QtCore, QtGui, QtWidgets


class TextItem:
    """Stores data about a single text element."""

    def __init__(self, x, y, text, font_family="Times", font_size=40, color="green"):
        self.x = x
        self.y = y
        self.text = text
        self.font_family = font_family
        self.font_size = font_size
        self.color = color

    def font(self):
        font = QtGui.QFont()
        font.setFamily(self.font_family)
        font.setPointSize(self.font_size)
        return font

    def bounding_rect(self):
        metrics = QtGui.QFontMetrics(self.font())
        rect = metrics.boundingRect(self.text)
        # Move the rect to the draw position. drawText draws
        # with y as the baseline, so we offset accordingly.
        rect.moveLeft(self.x)
        rect.moveBottom(self.y)
        return rect


class Canvas(QtWidgets.QLabel):
    def __init__(self, width=600, height=400):
        super().__init__()
        self.setFixedSize(width, height)
        self.items = []
        self.selected_item = None
        self._redraw()

    def add_text(self, x, y, text, **kwargs):
        item = TextItem(x, y, text, **kwargs)
        self.items.append(item)
        self._redraw()

    def _redraw(self):
        """Rebuild the pixmap from scratch using stored items."""
        canvas = QtGui.QPixmap(self.size())
        canvas.fill(QtGui.QColor("white"))

        painter = QtGui.QPainter(canvas)
        for item in self.items:
            pen = QtGui.QPen()
            pen.setWidth(1)
            pen.setColor(QtGui.QColor(item.color))
            painter.setPen(pen)
            painter.setFont(item.font())
            painter.drawText(item.x, item.y, item.text)

            # Draw a highlight around the selected item.
            if item is self.selected_item:
                highlight_pen = QtGui.QPen(
                    QtGui.QColor("blue"), 1, QtCore.Qt.PenStyle.DashLine
                )
                painter.setPen(highlight_pen)
                painter.drawRect(item.bounding_rect())

        painter.end()
        self.setPixmap(canvas)

    def mousePressEvent(self, event):
        """Detect clicks on text items."""
        pos = event.position().toPoint()
        self.selected_item = None

        # Check items in reverse order so top-most items are selected first.
        for item in reversed(self.items):
            if item.bounding_rect().contains(pos):
                self.selected_item = item
                break

        self._redraw()

    def mouseDoubleClickEvent(self, event):
        """Double-click to edit the selected item's text."""
        pos = event.position().toPoint()

        for item in reversed(self.items):
            if item.bounding_rect().contains(pos):
                new_text, ok = QtWidgets.QInputDialog.getText(
                    self,
                    "Edit Text",
                    "Enter new text:",
                    QtWidgets.QLineEdit.EchoMode.Normal,
                    item.text,
                )
                if ok and new_text:
                    item.text = new_text
                    self._redraw()
                return


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Clickable Text on QPixmap")

        self.canvas = Canvas()
        self.setCentralWidget(self.canvas)

        # Add some text items.
        self.canvas.add_text(50, 100, "Hello World")
        self.canvas.add_text(50, 200, "Click me!", color="red")
        self.canvas.add_text(50, 300, "Double-click to edit", color="purple")


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

Run this and you'll see three text items drawn on a white pixmap. Click on any one to select it (a dashed blue rectangle appears around it). Double-click to edit the text in a dialog.

The underlying idea is simple: the pixmap is treated as a rendering of your data, not as the data itself. Every time something changes, you clear the pixmap and redraw everything. This keeps things clean and avoids the problem of old pixels lingering underneath.

This approach works well when you have a modest number of elements. If your scene grows to hundreds or thousands of objects, the constant full redraws can become slow — which brings us to a better option.

Option 2: Use QGraphicsScene for Fully Interactive Elements

Qt provides a purpose-built system for exactly this kind of problem: QGraphicsScene and QGraphicsView. Instead of painting onto a bitmap, you add items to a scene. Each item remains a live object that can be clicked, moved, rotated, scaled, or edited at any time. The view handles all the rendering automatically.

This is the recommended approach if you need interactive, editable graphics.

Here's a complete working example using QGraphicsScene:

python
import sys
from PySide6 import QtCore, QtGui, QtWidgets


class EditableTextItem(QtWidgets.QGraphicsTextItem):
    """A text item that highlights on selection and allows editing."""

    def __init__(self, text, color="green", font_family="Times", font_size=40):
        super().__init__(text)

        self.setDefaultTextColor(QtGui.QColor(color))
        font = QtGui.QFont(font_family, font_size)
        self.setFont(font)

        # Allow the item to be selected and moved.
        self.setFlag(
            QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True
        )
        self.setFlag(
            QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True
        )

    def mouseDoubleClickEvent(self, event):
        """Enable inline text editing on double-click."""
        self.setTextInteractionFlags(
            QtCore.Qt.TextInteractionFlag.TextEditorInteraction
        )
        self.setFocus()
        super().mouseDoubleClickEvent(event)

    def focusOutEvent(self, event):
        """Disable editing when the item loses focus."""
        self.setTextInteractionFlags(
            QtCore.Qt.TextInteractionFlag.NoTextInteraction
        )
        # Clear the text cursor selection.
        cursor = self.textCursor()
        cursor.clearSelection()
        self.setTextCursor(cursor)
        super().focusOutEvent(event)

    def paint(self, painter, option, widget):
        """Draw a highlight rectangle when selected."""
        super().paint(painter, option, widget)
        if self.isSelected():
            pen = QtGui.QPen(
                QtGui.QColor("blue"), 1, QtCore.Qt.PenStyle.DashLine
            )
            painter.setPen(pen)
            painter.drawRect(self.boundingRect())


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Interactive Text with QGraphicsScene")

        # Create the scene and view.
        self.scene = QtWidgets.QGraphicsScene(0, 0, 600, 400)
        self.view = QtWidgets.QGraphicsView(self.scene)
        self.view.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
        self.setCentralWidget(self.view)

        # Add some text items to the scene.
        item1 = EditableTextItem("Hello World", color="green")
        item1.setPos(50, 50)
        self.scene.addItem(item1)

        item2 = EditableTextItem("Drag me!", color="red")
        item2.setPos(50, 150)
        self.scene.addItem(item2)

        item3 = EditableTextItem("Double-click to edit", color="purple")
        item3.setPos(50, 250)
        self.scene.addItem(item3)


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

Run this and you can:

  • Click on any text to select it (a dashed rectangle appears).
  • Drag any text to reposition it.
  • Double-click on any text to edit it inline — just start typing.
  • Click away to deselect and finish editing.

Each text item is a real object in the scene. You can add as many as you like, and Qt handles hit detection, rendering, and updates for you. You can also add other types of items — rectangles, ellipses, images, lines — and they all work the same way.

Which Approach Should You Use?

If you're building something where you just need to display a few labeled elements on an image and occasionally update them, the redraw approach (Approach 1) is simple and effective.

If you need rich interactivity — dragging, resizing, rotating, layering, or editing multiple types of objects — QGraphicsScene (Approach 2) is the way to go. It's what Qt designed for this purpose, and it scales well to complex scenes with many items.

For more on working with bitmap graphics and QPainter, see our PySide6 bitmap graphics tutorial. For a deeper dive into QGraphicsScene and vector graphics, check out the PySide6 QGraphicsScene tutorial.

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

Packaging Python Applications with PyInstaller by Martin Fitzpatrick

This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

Martin Fitzpatrick

Making Drawn Text Clickable and Editable on a QPixmap in PySide6 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.