如何在视图中实现富文本编辑器(PyQt/PySide/Qt)?
How to implement a rich text editor in a view (PyQt/PySide/Qt)?
短版
我有一个 QTreeView
并希望用户能够精细控制文本的外观,为他们提供富文本格式选项。我已经有了它,所以可以 select 编辑整个项目以进行格式化(例如,粗体),但我需要更大的灵活性。例如,用户必须能够突出显示项目文本的 部分 并加粗它。
请注意,我正在使用 QStandardItemModel
(请参阅下面的 SSCCE)。
详细版
给整个项目加气很简单:
itemFont = item.font()
itemFont.setBold(True)
item.setFont(itemFont)
不幸的是,我的用户需要更细粒度的控制,所以
Hi how are you?
他们应该能够 select 使用鼠标只输入第一个词,并使该项目的文本显示为:
Hi how are you?
我正在考虑的两个选项是:
setIndexWidget
在我需要此功能的每个单元格中,使用 setIndexWidget
将其显示为 QTextEdit
小部件,类似的操作已在此处完成:
To set widgets on children items on QTreeView。然后我可以使用标准工具在每个单元格中进行富文本编辑。
自定义委托
使用自定义委托在我需要此功能的地方绘制每个项目,就像这里应用的一样:
How to make item view render rich (html) text in Qt
请注意,与那个问题不同的是,我不只是问如何呈现富文本,而是如何让用户 select text 并将其呈现为富文本细粒度的文本。
SSCCE
from PySide import QtGui, QtCore
import sys
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.createBoldAction()
self.createToolbar()
def createStatusBar(self):
self.status = self.statusBar()
self.status.setSizeGripEnabled(False)
self.status.showMessage("Ready")
def createToolbar(self):
self.textToolbar = self.addToolBar("Text actions")
self.textToolbar.addAction(self.boldTextAction)
def createBoldAction(self):
self.boldTextAction = QtGui.QAction("Bold", self)
self.boldTextAction.setIcon(QtGui.QIcon("boldText.png"))
self.boldTextAction.triggered.connect(self.emboldenText)
self.boldTextAction.setStatusTip("Make selected text bold")
def emboldenText(self):
print "Make selected text bold...How do I do this?"
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self)
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Title', 'Summary'])
rootItem = model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Title0'), QtGui.QStandardItem('Summary0')]
item00 = [QtGui.QStandardItem('Title00'), QtGui.QStandardItem('Summary00')]
rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.setModel(model)
self.expandAll()
def main():
app = QtGui.QApplication(sys.argv)
myTree = SimpleTree()
#myTree.show()
myMainTree = MainTree(myTree)
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
唯一合理的方法是使用选项 2:创建自定义委托。您的情况几乎就是委托的确切类型:使用 createEditor
创建自定义编辑器(例如旋转框或富文本编辑器等),并实现 paint
使您可以精确控制数据输入后的外观的方法。虽然可能有其他方法可以做到这一点,但几乎可以肯定它们比使用委托更糟糕。
因此,要使其正常工作,您需要为 QStyledItemDelegate
.
重新实现 paint
和 createEditor
不幸的是,为了实现 createEditor
,Qt 没有提供原生的富文本行编辑器(也就是说,没有类似 QLineEdit
的富文本编辑器)。幸运的是,Mark Summerfield 实际上在他关于 PyQt 的书的第 13 章中写了这样一个函数,所以我将其应用到下面的一个完整的示例中,其中包括主 window 中的树视图,具有切换的能力在编辑器打开时使用工具栏或上下文(右键单击)菜单或键盘快捷键的文本属性。
相关帖子
我在以下线程中直接获得了实现其中许多功能的帮助:
图标
工具栏中使用的图像如下:
代码
这是代码。对于大小,我深表歉意,但它包含了很多可能对那些学习代表有用的东西(正如 OP 显然是的那样),所以我决定不对其进行编辑:
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()
短版
我有一个 QTreeView
并希望用户能够精细控制文本的外观,为他们提供富文本格式选项。我已经有了它,所以可以 select 编辑整个项目以进行格式化(例如,粗体),但我需要更大的灵活性。例如,用户必须能够突出显示项目文本的 部分 并加粗它。
请注意,我正在使用 QStandardItemModel
(请参阅下面的 SSCCE)。
详细版
给整个项目加气很简单:
itemFont = item.font()
itemFont.setBold(True)
item.setFont(itemFont)
不幸的是,我的用户需要更细粒度的控制,所以
Hi how are you?
他们应该能够 select 使用鼠标只输入第一个词,并使该项目的文本显示为:
Hi how are you?
我正在考虑的两个选项是:
setIndexWidget
在我需要此功能的每个单元格中,使用
setIndexWidget
将其显示为QTextEdit
小部件,类似的操作已在此处完成: To set widgets on children items on QTreeView。然后我可以使用标准工具在每个单元格中进行富文本编辑。自定义委托
使用自定义委托在我需要此功能的地方绘制每个项目,就像这里应用的一样: How to make item view render rich (html) text in Qt
请注意,与那个问题不同的是,我不只是问如何呈现富文本,而是如何让用户 select text 并将其呈现为富文本细粒度的文本。
SSCCE
from PySide import QtGui, QtCore
import sys
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.createBoldAction()
self.createToolbar()
def createStatusBar(self):
self.status = self.statusBar()
self.status.setSizeGripEnabled(False)
self.status.showMessage("Ready")
def createToolbar(self):
self.textToolbar = self.addToolBar("Text actions")
self.textToolbar.addAction(self.boldTextAction)
def createBoldAction(self):
self.boldTextAction = QtGui.QAction("Bold", self)
self.boldTextAction.setIcon(QtGui.QIcon("boldText.png"))
self.boldTextAction.triggered.connect(self.emboldenText)
self.boldTextAction.setStatusTip("Make selected text bold")
def emboldenText(self):
print "Make selected text bold...How do I do this?"
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self)
model = QtGui.QStandardItemModel()
model.setHorizontalHeaderLabels(['Title', 'Summary'])
rootItem = model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Title0'), QtGui.QStandardItem('Summary0')]
item00 = [QtGui.QStandardItem('Title00'), QtGui.QStandardItem('Summary00')]
rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.setModel(model)
self.expandAll()
def main():
app = QtGui.QApplication(sys.argv)
myTree = SimpleTree()
#myTree.show()
myMainTree = MainTree(myTree)
myMainTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
唯一合理的方法是使用选项 2:创建自定义委托。您的情况几乎就是委托的确切类型:使用 createEditor
创建自定义编辑器(例如旋转框或富文本编辑器等),并实现 paint
使您可以精确控制数据输入后的外观的方法。虽然可能有其他方法可以做到这一点,但几乎可以肯定它们比使用委托更糟糕。
因此,要使其正常工作,您需要为 QStyledItemDelegate
.
paint
和 createEditor
不幸的是,为了实现 createEditor
,Qt 没有提供原生的富文本行编辑器(也就是说,没有类似 QLineEdit
的富文本编辑器)。幸运的是,Mark Summerfield 实际上在他关于 PyQt 的书的第 13 章中写了这样一个函数,所以我将其应用到下面的一个完整的示例中,其中包括主 window 中的树视图,具有切换的能力在编辑器打开时使用工具栏或上下文(右键单击)菜单或键盘快捷键的文本属性。
相关帖子
我在以下线程中直接获得了实现其中许多功能的帮助:
图标
工具栏中使用的图像如下:
代码
这是代码。对于大小,我深表歉意,但它包含了很多可能对那些学习代表有用的东西(正如 OP 显然是的那样),所以我决定不对其进行编辑:
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()