---
title: Click-to-Edit in PyQt6
tags: coding, python, Qt, PyQt
description: A small example of setting up a "click-to-edit" text box with markdown input and formatting, in PyQt6
category: coding
date: 2024-05-30
---

[Qt](https://www.qt.io/) is a pretty powerful toolkit for building desktop apps. I'm not a desktop app builder, generally, so it seemed reasonably to start by using something that includes a great deal of functionality packed in, and has a long history of commentary and examples[^1]. Related, you can use Qt through the lovely [PyQt](https://riverbankcomputing.com/software/pyqt/) libraries, and I'm exremely well-versed in Python.

One of the features of Qt is a pretty robust set of text editing and display capabilities. Notably, it can display a fairly solid html subset (including limited css support), and markdown (well, its dialect thereof).

My goal? A panel which displays formatted text, and, when clicked, allows you to edit the markdown source for that text. Honestly, I'm mostly writing this blog post to capture some of what I've figured out for my future self to reference.

Let's dive in.

I'll start out by saying: I'm using subclassing for my widgets[^2]. To start off, I'm going to subclass [QStackedWidget](https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QStackedWidget.html)[^3].

What is QStackedWidget? It's a pretty simple widget container that holds widgets "on top" of each other---not vertically, but depthwise ("same spot but different z index" if that makes sense). Only one will be visible at any given time. It can be used to implement tabbed interfaces. The tabs would be their own widgets, sending signals to the QStackedWidget that it uses to decide what in its container is visible. I'm using it a bit differently, but the concept holds.

I'm going to create and configure two widgets: one which is an editor that holds plaintext (into which I'll put markdown), and the other which will display the formatted result.

Here's the class and constructor:

~~~{.python}
class CustomTextToggle(QStackedWidget):
    def __init__(self, parent):
        QStackedWidget.__init__(self, parent)

        self.editbox = CustomPlainTextEdit(self)
        self.doc = QTextDocument(self)
        self.doc.setDocumentLayout(QPlainTextDocumentLayout(self.doc))
        self.editbox.setDocument(self.doc)

        self.displaybox = QTextEdit(self)
        self.displaybox.setReadOnly(True)
        self.displaybox.mouseReleaseEvent=self.to_edit

        self.addWidget(self.editbox)
        self.addWidget(self.displaybox)

        self.to_view(None)
~~~

First thing to note is that `editbox` is another subclassed widget I'll discuss later. Nothing I'm doing to it here is reliant on the subclass's behavior, though.

I create a [QTextDocument](https://doc.qt.io/qtforpython-6/PySide6/QtGui/QTextDocument.html), which is the data class that backs a QTextEdit widget. I want it to hold markdown, though, not formatted text, so I set it to only ever hold plain text. I then say "this will be the document behind the `editbox` widget".

Next, I create `displaybox`, a [QTextEdit](https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTextEdit.html), which is a full-featured text editing widget. I'll only be using it for display purposes, though, hence the name. And hence the `setReadOnly` call.

I also add an event handler to `displaybox`. When the mouse button is released (i.e. when a click happens) on the `displaybox`, I want to call my `to_edit()` method (I'll get to that in a moment).

Finally, I add both widgets to the new `CustomTextToggle` object. Because only one will be visible at a time, they both will take up all available space (and be the same size).

Finally finally, I call my own `to_display()` method.

Fairly straightforward, and hopefully you can guess from the naming of `to_edit` and `to_display` that they are intended to bring up the `editbox` and `displaybox` widgets, respectively.

If you can't tell, let's look at that code:

~~~{.python}
    def to_edit(self, event):
        self.setCurrentIndex(0)

    def to_display(self, event):
        self.setCurrentIndex(1)
        self.displaybox.setMarkdown(self.doc.toPlainText())
~~~

Both methods take an `event` argument (that they both ignore, but which are necessary to be an event handler). And they both call `setCurrentIndex` which, you may have inferred, is the QStackedWidget method to change which widget is visible. It takes an array index, so it's important to know what order you added your widgets in.

`to_display` then calls `setMarkdown` on `displaybox`, which is the `QTextEdit` method to receive markdown input. It also understands `setPlainText`, `setText`, and `setHtml`. All of those methods replace the current contents entirely--there are other methods for inserts and other changes. And it passes in the plaintext of the document we set up in the constructor (the one backing `editbox`).

These are pretty simple methods, but I wanted to encapsulate the widget order, and attach the special behavior when switching to display.

That's it![^4] When the text display is clicked, the text edit is brought up (so you can no longer click the display). And when we switch back, the display is updated with the contents of the edit box.

Oh, right, that last bit. Let's look quickly at the subclass for the editor:

~~~{.python}
class CustomPlainTextEdit(QPlainTextEdit):
    def focusOutEvent(self, event):
        super().focusOutEvent(event)
        if event.lostFocus():
            self.parentWidget().to_display(event)
~~~

Not much to this subclass, not even a constructor. The [QPlainTextEdit](https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QPlainTextEdit.html) is itself a subclass of QTextEdit that restricts input to, well, plain text---no pasting formatting here. I'm just overriding its `focusOutEvent` handler, and when focus is lost, I'm calling `to_display` on the container widget.

That does limit the reusability of this subclass, but at three lines of code, I think it's a reasonable tradeoff.

And *that*'s it!

I've put that code, and some scaffolding to run it as an example, [in this repo on Codeberg](https://codeberg.org/jmelesky/click-to-edit-in-pyqt).

Hopefully this example is helpful to you, even if you are me from the future.


[^1]: Honestly, much of modern front-end programming is overwhelming to me. I've been on the back-end for so long, and my formative front-end experience involved using bitwise functions to flip individual pixels, and timing speaker clicks to simulate tones.

[^2]: There's apparently some debate on whether subclassing is appropriate in all cases, and I honestly don't have a strong opinion on the matter. I'm a Qt newb, and subclassing seems to do what I need.

[^3]: Astute readers will notice that this seems to be documentation for "PySide6", not PyQt6. The PyQt6 documentation doesn't have a class reference, and PySide6 seems to be entirely compatible (and is hosted alongside the C++ documentation for Qt). Should I be using PySide6 instead? That's probably another community debate that I'm unqualified to weigh in on.

[^4]: Obviously not, but thanks for reading this footnote anyway.


