pyqt5qtextedit

How do I add a full-width horizontal line in QTextEdit?


I am trying to add a full-width horizontal line in QTextEdit from PyQt5.QtWidgets.

My function looks like this:

def print_results(self, results):
    self.result_text_box.clear()
    self.cursor = self.result_text_box.textCursor()
    self.cursor.insertText(results[0])
    for result in results[1:]:
        # how do I add a horizontal line here?
        self.cursor.insertText(result)

results is a list of strings (possibly with newline characters) and self.result_text_box is an instance of QTextEdit.

When I tried self.cursor.insertHtml("<hr/>") I got weird results. Starting with the second string, a horizontal line was added whenever there was a newline character. For whatever reason, self.cursor.insertHtml("<p/><hr/><p/>") solved that problem, but only on macOS, while on Windows it didn't. I tried using just insertHtml, for the text too, but that didn't do the trick.

I would appreciate your help with this. Thanks!


Solution

  • The difference between support and compliance of HTML

    An important thing that should always be kept in mind is that, while Qt "supports" HTML, it is NOT a web browser [see note].

    Whenever a QtGui/QtWidget class or function provides HTML functionalities, it always follows a limited HTML subset support (see the foot note).

    Whenever a function like setHtml() (or setText() for optional rich-text objects like QLabel) is called, the given HTML is parsed and converted into a QTextDocument: if the text engine parser doesn't support a certain HTML tag/attribute or CSS syntax, it will be completely ignored and will revert to the most possible way to show the given content considering the context and the HTML object tree.

    The <hr> (aka, "Horizontal Rule") element is a peculiar one, even in the HTML world.
    In Qt, it is an element that is always part of the current QTextBlock (a "paragraph", as in <p>...</p>) and shown at its bottom.

    I'll repeat this, as it is extremely important: the <hr> line is always part of a previous block, it is not a separate element.

    Add lines as separators between elements

    Now, while we could go extremely into deep of the QTextDocument structure, it seems clear that you just want to show plain text entries that are separated by a horizontal line.

    So, let's keep this simple.

    If you only want lines between elements, it's quite easy:

    def print_results(self, results):
        self.result_text_box.setHtml(
            '<hr>'.join(results))
    

    Note that I removed self.result_text_box.clear(), which is completely pointless when using setHtml(), because that function always overwrites the current contents.

    A further line, allowing user insertion after it

    In case you want to add a further line at the end of the last entry and also allow insertion of further elements, things get a bit trickier.

    As said above, the horizontal line is always part of a QTextBlock. This means that if you try to do insertHtml() the insertion will always be within the current text block of the text cursor, and since insertion of new block always inherits the block format of the current block, you'll end up with two or more consecutive lines.

    The proper way to add a line at the end and allow insertion of new text after that is the following:

    1. do the above, but also add a further <hr> element;
    2. get the QTextCursor of the document;
    3. move it to the end;
    4. add a further text block;
    5. remove the line of the new text block (which is inherited by the previous);

    Here is a possible implementation of the above:

    def print_results(self, results):
        self.result_text_box.setHtml(
            '<hr>'.join(results) + '<hr>') # <-- note the added <hr> element
    
        tc = self.result_text_box.textCursor()
        # move the cursor to the end of the document
        tc.movePosition(tc.End)
        # insert an arbitrary QTextBlock that will inherit the previous format
        tc.insertBlock()
        # get the block format
        fmt = tc.blockFormat()
        # remove the horizontal ruler property from the block
        fmt.clearProperty(fmt.BlockTrailingHorizontalRulerWidth)
        # set (not merge!) the block format
        tc.setBlockFormat(fmt)
        # eventually, apply the cursor so that editing actually starts at the end
        self.result_text_box.setTextCursor(tc)
    

    Note that I just used a local variable for the QTextCursor; creating an instance attribute for it is not only pointless (you're probably going to call that function more than once, so you will also be overwriting it every time), but wrong, since cursor() is an existing property/function of all QWidgets and, as such, should never be overwritten, especially with something that is completely unrelated to the original purpose.

    Be aware that the differences you've seen between macOS and Window might be related to two different aspects:


    If you want proper HTML compliance, the only way to do so in Qt is through the Qt WebEngine API; in PyQt5 most of the important classes are within the QtWebEngine, QtWebEngineWidgets and QtWebEngineCore modules, while the QtWebEngine module classes have all been moved to the QtWebEngineCore module in PyQt6.