05-30-click-to-edit-in-qt.md 7.0 KB


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 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 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[^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:

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

    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:

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

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.