The only reasonable way to do this is using your option 2: create a custom delegate. Yours is pretty much the exact kind of situation delegates are made for: use createEditor
to create a custom editor (such as a spin box, or rich text editor, or whatever), and also implement a paint
method that lets you control exactly how the data will look once it is entered. While there might be other ways to do it, they are almost guaranteed to be worse than using a delegate.
Hence, to get this to work, you'll need to reimplement paint
as well as createEditor
for a QStyledItemDelegate
.
Unfortunately, for implementing createEditor
, Qt doesn't provide a native rich text line editor (that is, there is nothing like QLineEdit
for rich text). Luckily, Mark Summerfield actually wrote such a function in Chapter 13 of his book on PyQt, so I've adopted that into a fully working example below, which includes a tree view in a main window, with the ability to toggle the text properties using the toolbar or a context (right-click) menu or keyboard shortcuts when the editor is open.

Relevant posts
I directly got help implementing many of these features in the following threads:
The icons
Here are the images used in the toolbar:

The Code
Here is the code. I apologize for the size, but it includes so many things that might be useful for those learning delegates (as the OP obviously is) that I decided not to edit it down:
import sys
from xml.sax.saxutils import escape as escape
from PySide import QtGui, QtCore
class MainTree(QtGui.QMainWindow):
def __init__(self, tree, parent = None):
QtGui.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setCentralWidget(tree)
self.createStatusBar()
self.createActions()
self.createToolbar()
self.tree = tree
self.setGeometry(500,150,400,300)
def createStatusBar(self):
self.status = self.statusBar()
self.status.setSizeGripEnabled(False)
self.status.showMessage("Ready")
def createActions(self):
'''Create all actions to be used in toolbars/menus: calls createAction()'''
self.boldTextAction = self.createAction("&Bold",
shortcut = QtGui.QKeySequence.Bold, iconName = "boldText", tip = "Embolden",
status = "Toggle bold", disabled = True)
self.italicTextAction = self.createAction("&Italic",
shortcut = QtGui.QKeySequence.Italic, iconName = "italicText", tip = "Italicize",
status = "Toggle italics", disabled = True)
self.underlineTextAction = self.createAction("&Underline",
shortcut = QtGui.QKeySequence.Underline, iconName = "underlineText", tip = "Underline",
status = "Toggle underline", disabled = True)
self.strikeoutTextAction = self.createAction("Stri&keout",
shortcut = QtGui.QKeySequence("Ctrl+K"), iconName = "strikeoutText", tip = "Strikeout",
status = "Toggle strikeout", disabled = True)
def createAction(self, text, slot = None, shortcut = None, iconName = None,
tip = None, status = None, disabled = False):
'''Creates each individual action'''
action = QtGui.QAction(text, self)
if iconName is not None:
action.setIcon(QtGui.QIcon("{0}.png".format(iconName)))
if shortcut is not None:
action.setShortcut(shortcut)
if tip is not None:
action.setToolTip(tip)
if status is not None:
action.setStatusTip(status)
if slot is not None:
action.triggered.connect(slot)
if disabled:
action.setDisabled(True)
return action
def createToolbar(self):
self.textToolbar = self.addToolBar("Text actions")
self.textToolbar.addAction(self.boldTextAction)
self.textToolbar.addAction(self.underlineTextAction)
self.textToolbar.addAction(self.italicTextAction)
self.textToolbar.addAction(self.strikeoutTextAction)
class HtmlTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self)
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Task', 'Description'])
self.rootItem = model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('Get a job'), QtGui.QStandardItem('Do not blow it')]
item01 = [QtGui.QStandardItem('Call temp agency'), QtGui.QStandardItem('Maybe they will be kind')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
item1[0].appendRow(item01)
self.setModel(model)
self.expandAll()
self.setItemDelegate(HtmlPainter(self))
self.resizeColumnToContents(0)
self.resizeColumnToContents(1)
#print "unoiform row heights? ", self.uniformRowHeights()
class HtmlPainter(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
print "delegate parent: ", parent, parent.metaObject().className()
QtGui.QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
if index.column() == 1 or index.column() == 0:
text = index.model().data(index)
palette = QtGui.QApplication.palette()
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
#Set text (color depends on whether selected)
if option.state & QtGui.QStyle.State_Selected:
displayString = "<font color={0}>{1}</font>".format(palette.highlightedText().color().name(), text)
document.setHtml(displayString)
else:
document.setHtml(text)
#Set background color
bgColor = palette.highlight().color() if (option.state & QtGui.QStyle.State_Selected)\
else palette.base().color()
painter.save()
painter.fillRect(option.rect, bgColor)
document.setTextWidth(option.rect.width())
offset_y = (option.rect.height() - document.size().height())/2
painter.translate(option.rect.x(), option.rect.y() + offset_y)
document.drawContents(painter)
painter.restore()
else:
QtGui.QStyledItemDelegate.paint(self, painter, option, index)
def sizeHint(self, option, index):
rowHeight = 18
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
document.setHtml(text)
return QtCore.QSize(document.idealWidth() + 5, rowHeight) #fm.height())
def createEditor(self, parent, option, index):
if index.column() == 1:
editor = RichTextLineEdit(option, parent)
editor.returnPressed.connect(self.commitAndCloseEditor)
editor.mainWindow = parent.window()
self.setConnections(editor.mainWindow, editor)
self.enableActions(editor.mainWindow)
return editor
else:
return QtGui.QStyledItemDelegate.createEditor(self, parent, option,
index)
def setConnections(self, mainWindow, editor):
'''Create connections for font toggle actions when editor is created'''
mainWindow.boldTextAction.triggered.connect(editor.toggleBold)
mainWindow.underlineTextAction.triggered.connect(editor.toggleUnderline)
mainWindow.italicTextAction.triggered.connect(editor.toggleItalic)
mainWindow.strikeoutTextAction.triggered.connect(editor.toggleStrikeout)
def enableActions(self, mainWindow):
mainWindow.boldTextAction.setEnabled(True)
mainWindow.underlineTextAction.setEnabled(True)
mainWindow.italicTextAction.setEnabled(True)
mainWindow.strikeoutTextAction.setEnabled(True)
def disableActions(self, mainWindow):
mainWindow.boldTextAction.setDisabled(True)
mainWindow.underlineTextAction.setDisabled(True)
mainWindow.italicTextAction.setDisabled(True)
mainWindow.strikeoutTextAction.setDisabled(True)
def commitAndCloseEditor(self):
editor = self.sender()
if isinstance(editor, (QtGui.QTextEdit, QtGui.QLineEdit)):
self.commitData.emit(editor)
self.closeEditor.emit(editor, QtGui.QAbstractItemDelegate.NoHint)
def setModelData(self, editor, model, index):
if index.column() == 1:
self.disableActions(editor.mainWindow)
model.setData(index, editor.toSimpleHtml())
else:
QtGui.QStyledItemDelegate.setModelData(self, editor, model, index)
class RichTextLineEdit(QtGui.QTextEdit):
'''Single line editor invoked by delegate'''
(Bold, Italic, Underline, StrikeOut) = range(4)
returnPressed = QtCore.Signal()
def __init__(self, option, parent=None):
QtGui.QTextEdit.__init__(self, parent)
self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.setTabChangesFocus(True)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
#Following lines set it so text is centered in editor
fontMetrics = QtGui.QFontMetrics(self.font())
margin = 2
self.document().setDocumentMargin(margin)
height = fontMetrics.height() + (margin + self.frameWidth()) * 2
self.setFixedHeight(height)
self.setToolTip("Right click for text effect menu.")
def toggleBold(self):
self.setFontWeight(QtGui.QFont.Normal
if self.fontWeight() > QtGui.QFont.Normal else QtGui.QFont.Bold)
def toggleItalic(self):
self.setFontItalic(not self.fontItalic())
def toggleUnderline(self):
self.setFontUnderline(not self.fontUnderline())
def toggleStrikeout(self):
#Adapted from: https://www.binpress.com/tutorial/developing-a-pyqt-text-editor-part-2/145
#https://srinikom.github.io/pyside-docs/PySide/QtGui/QTextCharFormat.html
# Grab the text's format
textFormat = self.currentCharFormat()
# Change the fontStrikeOut property to its opposite
textFormat.setFontStrikeOut(not textFormat.fontStrikeOut())
# Apply the new format
self.setCurrentCharFormat(textFormat)
def contextMenuEvent(self, event):
'''
Context menu for controlling text
'''
textFormat = self.currentCharFormat()
menu = QtGui.QMenu("Text Effects")
for text, shortcut, data, checked in (
("&Bold", "Ctrl+B", RichTextLineEdit.Bold,
self.fontWeight() > QtGui.QFont.Normal),
("&Italic", "Ctrl+I", RichTextLineEdit.Italic,
self.fontItalic()),
("Stri&keout", "Ctrl+K", RichTextLineEdit.StrikeOut,
textFormat.fontStrikeOut()),
("&Underline", "Ctrl+U", RichTextLineEdit.Underline,
self.fontUnderline())):
action = menu.addAction(text, self.setTextEffect)
if shortcut is not None:
action.setShortcut(QtGui.QKeySequence(shortcut))
action.setData(data)
action.setCheckable(True)
action.setChecked(checked)
self.ensureCursorVisible()
menu.exec_(self.viewport().mapToGlobal(
self.cursorRect().center()))
def setTextEffect(self):
'''Called by context menu'''
action = self.sender()
if action is not None and isinstance(action, QtGui.QAction):
what = int(action.data())
if what == RichTextLineEdit.Bold:
self.toggleBold()
return
if what == RichTextLineEdit.Italic:
self.toggleItalic()
return
if what == RichTextLineEdit.Underline:
self.toggleUnderline()
return
format = self.currentCharFormat()
if what == RichTextLineEdit.StrikeOut:
format.setFontStrikeOut(not format.fontStrikeOut())
self.mergeCurrentCharFormat(format)
def keyPressEvent(self, event):
'''
Handles all keyboard shortcuts, and stops retun from returning newline
'''
if event.modifiers() & QtCore.Qt.ControlModifier:
handled = False
if event.key() == QtCore.Qt.Key_B:
self.toggleBold()
handled = True
elif event.key() == QtCore.Qt.Key_I:
self.toggleItalic()
handled = True
elif event.key() == QtCore.Qt.Key_U:
self.toggleUnderline()
handled = True
if handled:
event.accept()
return
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self.returnPressed.emit()
event.accept()
else:
QtGui.QTextEdit.keyPressEvent(self, event)
def toSimpleHtml(self):
html = ""
block = self.document().begin()
while block.isValid():
iterator = block.begin()
while iterator != block.end():
fragment = iterator.fragment()
if fragment.isValid():
format = fragment.charFormat()
text = escape(fragment.text())
if format.fontUnderline():
text = "<u>{}</u>".format(text)
if format.fontItalic():
text = "<i>{}</i>".format(text)
if format.fontWeight() > QtGui.QFont.Normal:
text = "<b>{}</b>".format(text)
if format.fontStrikeOut():
text = "<s>{}</s>".format(text)
html += text
iterator += 1
block = block.next()
return html
def main():
app = QtGui.QApplication(sys.argv)
myTree = HtmlTree() #myTree.show()
myMainTree = MainTree(myTree)
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()