Search code examples
pythonhtmlpyqt5qtableviewqtextedit

Adjust HTML editor appearance and behaviour in QTableView


What I want to do: have HTML cells in both columns of my table, both when rendering and when editing. Col 1 should be fully editable. When cells in Col 0 are edited, it should be possible to select text, including using keyboard "editor kit" keys, such as Ctrl-Shift-Right (advance selection by one word), etc., but it should not be possible to edit the text.

In both columns I want the editor to be as indistinguishable from the renderer as possible.

Here's an MRE:

class TableViewDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self):
        super().__init__()
        self.editor_for_col_0 = None 
        self.editor_for_other_cols = None 
        self.doc = QtGui.QTextDocument()
    
    def createEditor(self, parent, option, index):
        if index.column() == 0:
            if self.editor_for_col_0 == None:
                self.editor_for_col_0 = QtWidgets.QTextEdit(parent)
                self.editor_for_col_0.setReadOnly(True)
            return self.editor_for_col_0
        else:
            if self.editor_for_other_cols == None:
                self.editor_for_other_cols = QtWidgets.QTextEdit(parent)
            return self.editor_for_other_cols
     
    def setEditorData(self, editor, index):
        cell_text = index.model().data(index, QtCore.Qt.DisplayRole)
        editor.setHtml(cell_text)            
 
    def destroyEditor(self, editor, index):
        editor.clear()
        
    def paint(self, painter, option, index):
        options = QtWidgets.QStyleOptionViewItem(option)
        self.doc.setDefaultFont(options.font)
        self.initStyleOption(options, index)
         
        painter.save()

        self.doc.setTextWidth(options.rect.width())                
        self.doc.setHtml(options.text)
        options.text = ""
        options.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
        painter.translate(options.rect.left(), options.rect.top())
        clip = QtCore.QRectF(0, 0, options.rect.width(), options.rect.height())
        painter.setClipRect(clip)
        ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
        ctx.clip = clip
        self.doc.documentLayout().draw(painter, ctx)
        
        painter.restore()
        
    def sizeHint(self, option, index):
        self.initStyleOption(option, index)
        if option.text == '':
            return super().sizeHint(option, index)
        self.doc.setHtml(option.text)
        self.doc.setTextWidth(option.rect.width())
        self.doc.setDefaultFont(option.font)
        self.doc.setDocumentMargin(0)
        return QtCore.QSize(int(self.doc.idealWidth()), int(self.doc.size().height()))

class TableViewModel(QtCore.QAbstractTableModel):
    def __init__(self):
        super().__init__()
        self._data = [
            ['Lorem <em>ipsum dolor</em> sit amet, <strong>consectetur adipiscing</strong> elit.', 
             'Curabitur eu <strong>nulla</strong> ut <em>lacus bibendum</em> interdum.'],
            ['Vivamus <em>ultricies <strong>eleifend nulla</strong></em> eget pharetra.', 
             'Fusce <strong>aliquam magna</strong> a <em>sem placerat consectetur</em>.',],
       ]
    
    def rowCount(self, *args):
        return len(self._data)
    
    def columnCount(self, *args):
        return 2
    
    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            return self._data[index.row()][index.column()]

    def flags(self, index):
        result = super().flags(index)
        return QtCore.Qt.ItemFlag.ItemIsEditable | result
    
class TableView(QtWidgets.QTableView):
    def __init__(self):
        super().__init__()
        self.setItemDelegate(TableViewDelegate()) 
        self.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)
        
class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Frozen HTML editor MRE')
        layout = QtWidgets.QVBoxLayout()
        self.table_view = TableView()
        layout.addWidget(self.table_view)
        self.table_view.setModel(TableViewModel())
        
        self.table_view.setColumnWidth(0, 200)
        self.table_view.setColumnWidth(1, 200)
        
        centralwidget = QtWidgets.QWidget(self)
        centralwidget.setLayout(layout)
        self.setCentralWidget(centralwidget)
        self.setGeometry(QtCore.QRect(400, 400, 600, 400))
        self.table_view.resizeRowsToContents()
                
app = QtWidgets.QApplication([])
main_window = MainWindow()
main_window.show()
sys.exit(app.exec())    

There are 2 problems:

  1. When entering edit in both columns the editor acquires a thin blue frame and, worse, some sort of edging, padding or margin which causes a scrollbar to appear. I tried setContentMargins on the editor but this didn't seem to have any effect.

  2. Although it is possible to select text using the mouse in the Col 0 editor, because of setReadOnly(True), the normal "editor kit" keys to extend selection don't work any more. I wonder if there's another way to achieve a "read-only-but-selectable-with-editor-kit" editor?

PS OS is W10.

Edit

ekhumoro pretty much solved this.

I found a way to get rid of the blue border:

self.editor[...].setFrameStyle(QtWidgets.QFrame.NoFrame)

If setDocumentMargin() is then set with the same value, e.g. 0, for the editor and in sizeHint, it is mostly rock-solid: when the edit starts the text doesn't budge even by 1 pixel.

(NB once in the editor on col 1, you can press Escape to stop editing, and then Ctrl-DownArrow to move to the cell below).

BUT... with a long text things go nasty. If you add the following third row to the table you'll see what I mean:

['Nam fermentum a velit vel euismod. Quisque ut mollis nibh. Nulla aliquam placerat tortor, eget mattis eros tincidunt id. Nunc auctor eros feugiat, molestie tellus a, congue eros. ', 
         'Suspendisse tempor finibus tempus. Morbi fermentum rutrum lectus, in malesuada mauris tincidunt maximus. Fusce id elit rutrum, congue diam a, imperdiet ex. Vivamus sagittis purus eleifend, feugiat urna sit amet, feugiat erat. Nullam imperdiet ligula elit. Pellentesque lobortis efficitur metus, ornare ullamcorper magna interdum imperdiet. Praesent fermentum feugiat erat, dignissim molestie sem convallis eget. Proin varius nisi quis enim convallis elementum. Mauris ac vulputate leo. Cras ac nisi eu velit mattis maximus. In interdum interdum dui, et blandit turpis tristique non. Integer molestie bibendum turpis non feugiat.',],

(and you might want to change the setGeometry like so: self.setGeometry(QtCore.QRect(400, 400, 600, 1000)))

... then you can see that a word wrap occurs in the editor where it doesn't in the rendered cell. This in turn causes a scrollbar to appear, which makes everything rubbish.

I'm trying to track down the source of this problem. To me it looks like a difference of 1 pixel.

Stranger still: if you go to the bottom right-hand cell, with my setEditTriggers in the QTreeView constructor, this will start editing immediately. But if you stop editing by pressing Escape, and then start editing again by pressing F2, the word-wrap does not occur!

Hmmmm...


Solution

  • The keyboard behaviour can be controlled via the text interaction flags:

    self.editor_for_col_0.setTextInteractionFlags(
        QtCore.Qt.TextSelectableByMouse | QtCore.Qt.TextSelectableByKeyboard)
    

    The internal padding can be set via the document margin:

    self.editor_for_col_0.document().setDocumentMargin(0)
    

    The issue with the blue frame appears to be platform-specific, since I can't reproduce it on my Linux system. There are a few solutions suggested in these questions:

    The most promising for your purposes seems to be adjusting the style-option state flags in the delegate. You may need to experiment a little with various focus/selection flag combinations to get the desired behaviour for your system.