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

时间:2015-09-25 02:08:13

标签: qt pyqt pyside

简短版

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

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

详细版本

要加固整个项目很简单:

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

不幸的是,我的用户需要更细粒度的控制,因此不是

  

你好吗?

他们应该能够使用鼠标选择第一个单词并使该项目的文本显示为:

  

你好吗?

我正在考虑的两个选项是:

  1. setIndexWidget

    在我需要此功能的每个单元格中,使用QTextEdit将其显示为setIndexWidget窗口小部件,如下所示: To set widgets on children items on QTreeView。然后我可以使用标准工具在每个单元格中进行富文本编辑。

  2. 自定义代理

    使用自定义委托绘制我需要此功能的每个项目,类似于此处应用的内容: How to make item view render rich (html) text in Qt

  3. 注意与该问题不同,我不只是询问如何渲染富文本,而是如何让用户选择文本并将其呈现为细粒度的富文本。< / p>

    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 个答案:

答案 0 :(得分:3)

唯一合理的方法是使用您的选项2:创建自定义委托。你的几乎就是委托制作的确切情况:使用createEditor创建自定义编辑器(例如旋转框,富文本编辑器等),并实现paint一种方法,可让您精确控制数据输入后的外观。虽然可能还有其他方法可以做到,但几乎可以保证它们比使用代表更糟糕。

因此,要实现这一点,您需要为paint重新实现createEditorQStyledItemDelegate

不幸的是,为了实现createEditor,Qt不提供原生的富文本行编辑器(也就是说,对于富文本没有像QLineEdit这样的内容)。幸运的是,Mark Summerfield实际上在他关于PyQt的书的第13章中写了这样一个函数,所以我在下面的一个完整工作的例子中采用了这个函数,其中包括主窗口中的树视图,能够使用切换文本属性编辑器打开时,工具栏或上下文(右键单击)菜单或键盘快捷键。

enter image description here

相关帖子

我直接在以下主题中帮助实现了许多这些功能:

图标

以下是工具栏中使用的图像:

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

守则

这是代码。我为大小道歉,但它包含了许多可能对学习代表有用的东西(显然是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()