如何在视图中实现一个富文本编辑器(PyQt/PySide/Qt)?

3

简短版

我有一个 QTreeView,希望用户能够对文本外观进行细粒度控制,提供丰富的富文本格式选项。我已经实现了整个项目可以选择进行格式化(例如,加粗),但我需要更多的灵活性。例如,用户必须能够突出显示项目文本的部分并加粗。

请注意,我正在使用QStandardItemModel(请参见下面的SSCCE)。

详细版

要使整个项目加粗很简单:

itemFont = item.font()
itemFont.setBold(True)
item.setFont(itemFont) 

很不幸,我的用户需要更细粒度的控制,所以他们应该能够使用鼠标仅选择第一个单词,并使该项的文本显示为:
“Hi” how are you?
我正在考虑两个选项:
1. 在需要此功能的每个单元格中,使用 setIndexWidget 将其显示为 QTextEdit 小部件,类似于此处的操作:To set widgets on children items on QTreeView。然后我可以在每个单元格中使用标准的富文本编辑工具。
2. 使用自定义委托来绘制每个需要此功能的项,类似于这里所应用的方法:How to make item view render rich (html) text in Qt

请注意,与那个问题不同,我不仅仅是在问如何呈现富文本,而是如何让用户选择文本并以细粒度的方式呈现为富文本。

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()
1个回答

4
唯一合理的方法是使用您的选项2:创建自定义委托。这几乎就是委托用于的情况:使用createEditor创建自定义编辑器(例如旋转框或富文本编辑器等),并实现一个paint方法,让您完全控制输入数据后的外观。虽然可能有其他方法可以完成此操作,但它们几乎肯定比使用委托更糟糕。
因此,为了使其工作,您需要重新实现paint以及createEditor用于QStyledItemDelegate
不幸的是,对于实现createEditor,Qt没有提供本地的富文本行编辑器(也就是说,没有像富文本那样的QLineEdit)。幸运的是,Mark Summerfield在他关于PyQt的书的第13章中编写了这样的函数,因此我已将其采用到下面的完全工作示例中,其中包括一个树视图在主窗口中,具有使用工具栏或上下文(右键单击)菜单或编辑器打开时的键盘快捷键切换文本属性的功能。

enter image description here


相关文章

以下线程中,我直接得到了许多有关实现这些功能的帮助:


图标

这是工具栏中使用的图片:

boldText.png italicText.png strikeoutText.png underlineText.png


代码

这是代码。很抱歉它有点大,但它包含了许多对于学习代理(正如原帖显然是)可能有用的东西,因此我决定不进行编辑:

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()

我在HtmlPainter委托下使用了与您类似的绘制方法。然而,我从未能够重新获得所选单元格周围的虚线框。您是否已经想出了重新实现它的方法? - RXander
@RXander我从未尝试过这个,但在委托的QtGui.QStyle.State_Selected位中,您应该能够玩弄QRect的填充/轮廓。如果您想查看它们在默认视图中如何执行,请参见以下帖子以了解如何开始探索(请注意,探索默认委托的机制仅适用于勇敢的人,它几乎让我崩溃 :)):https://dev59.com/uZHea4cB1Zd3GeqPq5CN - eric
1
谢谢您的评论。我通过使用Qpen和QBrush painter方法实现了QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_ItemViewItem, option, painter)。你的很多帖子都很有用。是的,我发现浏览pyqt sourceforge信息比原始代码更有用。一本关于在pyqt/pyside中使用样式的全面书籍肯定会对初学者有所帮助,但我还没有找到这样的书。但是《Python和Qt快速GUI编程》也很有帮助。干杯。 - RXander
@RXander,目前为止,我已经放弃了PyQt。我花费的时间更多地是在尝试弄清楚我认为应该是非常简单的事情,而不是真正有生产力的编码。这是我永远无法挽回的一年。 :) 很高兴我的一些帖子能够帮到你,我花了大量时间来写它们。 - eric

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接