QLabel设置最小高度后自定义换行Qt.TextWrapAnywhere PyQt5(支持包含/不包含表情的完全响应式布局)

3

我希望在一个布局中的QLabel上使用Qt.TextWrapAnywhere功能。

我按照这个指导进行操作,我的代码也与它相同,以提供最小化的代码。

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle

class SuperQLabel(QLabel):
    def __init__(self, *args, **kwargs):
        super(SuperQLabel, self).__init__(*args, **kwargs)

        self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
        self.isTextLabel = True
        self.align = None

    def paintEvent(self, event):

        opt = QStyleOption()
        opt.initFrom(self)
        painter = QPainter(self)

        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)

        self.style().drawItemText(painter, self.rect(),
                                  self.textalignment, self.palette(), True, self.text())


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.resize(100, 200)

        self.label = QLabel()
        self.label.setWordWrap(True)
        self.label.setText("11111111111111111111\n2222222211111111")

        self.slabel = SuperQLabel()
        self.slabel.setMinimumWidth(10)
        self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")

        self.centralwidget = QWidget()
        self.setCentralWidget(self.centralwidget)

        self.mainlayout = QVBoxLayout()
        self.mainlayout.addWidget(self.label)
        self.mainlayout.addWidget(self.slabel)

        self.centralwidget.setLayout(self.mainlayout)


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

我稍微改了一下这个代码self.slabel.setMinimumWidth(10),否则根据宽度调整标签的大小不起作用。

它完美地根据宽度换行文本。但是问题在于当考虑高度时,self.label = QLabel()普通的QLabel将根据布局自动调整内容的高度。

例如,如果我在文字中添加一个\n,那么QLabel必须显示2行。

但是使用这个新的定制标签,例如self.slabel = SuperQLabel(),只要布局中有足够的空间,换行就很好。我认为我必须使用setminimumHeight(),但不知道如何在自定义换行后获得正确的高度。


1
如果您添加新行,则使用富文本引擎,这会创建已知的布局问题。这使得所提出的解决方案工作不一致。设置最小高度可能有所帮助,但应谨慎进行。 - musicamante
怎么做?你能发一些提示吗,@musicamante? - Hacker6914
很遗憾,您无法实现同时尊重宽度和高度并且在两个维度上设置最小大小的调整大小功能。由于布局的工作方式,这是无法解决的限制。您唯一的选择是覆盖 heightForWidth() 方法,它只会更新提示,但不能强制维度。换句话说,用户始终可以将标签调整为部分隐藏某些文本的宽度/高度。 - musicamante
我正在使用Qpainter来实现WrapAnywhere的功能。那么有没有办法检测传输到下一行的单词长度?我们可以使用QFontmatrics吗? - Hacker6914
你没有抓住重点。问题不在于绘画(只是“绘画”,不控制大小),而在于尺寸限制。如果你减小标签的宽度,理论上会强制要求最小高度,但这会阻止进一步调整,例如,你不能尝试将标签调整为较小的高度,希望它会使标签变宽。无法知道用户(或布局)是否专门要求水平或垂直调整大小,因为两种情况都可能出现,也不可能“决定”哪个维度作为参考。 - musicamante
2个回答

2
只要标签显示在滚动区域中(不会影响顶层布局),更好的解决方案是使用 QTextEdit 子类,并进行以下配置:
  • readOnly 必须为 True;
  • 禁用滚动条;
  • 垂直大小策略必须为 Preferred(而不是 Expanding);
  • both minimumSizeHint()sizeHint() 应该使用内部 QTextDocument 来返回适当的高度,以及一个最小的默认宽度;
  • 任何大小或内容的更改都必须触发 updateGeometry(),以便父布局将知道提示已更改并且几何图形可以再次计算;
  • 提示必须包括滚动区域(QFrame)可能的装饰;
这样可以避免覆盖 paintEvent(),并由于 QTextDocument 提供的功能,提供更好、更易于实现的大小机制,同时将递归的可能性降到可接受的水平。
class WrapLabel(QtWidgets.QTextEdit):
    def __init__(self, text=''):
        super().__init__(text)
        self.setStyleSheet('''
            WrapLabel {
                border: 1px outset palette(dark);
                border-radius: 8px;
                background: palette(light);
            }
        ''')
        self.setReadOnly(True)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, 
            QtWidgets.QSizePolicy.Maximum)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.textChanged.connect(self.updateGeometry)

    def minimumSizeHint(self):
        doc = self.document().clone()
        doc.setTextWidth(self.viewport().width())
        height = doc.size().height()
        height += self.frameWidth() * 2
        return QtCore.QSize(50, height)

    def sizeHint(self):
        return self.minimumSizeHint()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.updateGeometry()


class ChatTest(QtWidgets.QScrollArea):
    def __init__(self):
        super().__init__()
        self.messages = []

        container = QtWidgets.QWidget()
        self.setWidget(container)
        self.setWidgetResizable(True)

        layout = QtWidgets.QVBoxLayout(container)
        layout.addStretch()
        self.resize(480, 360)

        for i in range(1, 11):
            QtCore.QTimer.singleShot(1000 * i, lambda:
                self.addMessage('1' * randrange(100, 250)))

    def addMessage(self, text):
        self.widget().layout().addWidget(WrapLabel(text))
        QtCore.QTimer.singleShot(0, self.scrollToBottom)

    def scrollToBottom(self):
        QtWidgets.QApplication.processEvents()
        self.verticalScrollBar().setValue(
            self.verticalScrollBar().maximum())

更新:HTML和QTextDocument
当使用setHtml()setDocument()时,源代码可能有不允许换行的pre格式化文本。为了避免这种情况,需要迭代文档中的所有 QTextBlocks,获取它们的 QTextBlockFormat,检查nonBreakableLines()属性,并最终将其设置为False,然后使用 QTextCursor将格式设置回来。
class WrapLabel(QtWidgets.QTextEdit):
    def __init__(self, text=None):
        super().__init__()
        if isinstance(text, str):
            if Qt.mightBeRichText(text):
                self.setHtml(text)
            else:
                self.setPlainText(text)
        elif isinstance(text, QtGui.QTextDocument):
            self.setDocument(text)
        # ...

    def setHtml(self, html):
        doc = QtGui.QTextDocument()
        doc.setHtml(html)
        self.setDocument(doc)

    def setDocument(self, doc):
        doc = doc.clone()
        tb = doc.begin() # start a QTextBlock iterator
        while tb.isValid():
            fmt = tb.blockFormat()
            if fmt.nonBreakableLines():
                fmt.setNonBreakableLines(False)
                # create a QTextCursor for the current text block,
                # then set the updated format to override the wrap
                tc = QtGui.QTextCursor(tb)
                tc.setBlockFormat(fmt)
            tb = tb.next()
        super().setDocument(doc)

请注意,当使用具有预定义或最小宽度的对象时(如图像和表格),这可能不足够。结果将是,如果对象大于可用空间,则会在其右侧裁剪它(对于从右到左的文本布局则在其左侧)。

经过一些测试,我尝试使用setPlainText()方法来代替在构造函数中传递文本,它有效了。但是如果我想设置另一个Qtextedit的文档呢?因为文档通常会采用复制文本的颜色,这非常吸引人。 - Hacker6914
不,使用包装无法设置另一个QTextEdit的文档。 - Hacker6914
1
@Hacker6914,它“应该”可以工作,我刚测试了一下,它按预期换行。你能提供一个实际的不换行的文档示例吗? - musicamante
抱歉我很忙所以没能为您发布示例。现在我已经在我的问题中发布了示例,请检查是否成功换行!还请手动输入一些长文本并查看差异。 - Hacker6914
@Hacker6914,请不要编辑问题,包括代码和答案,因为这会使问题变得非常混乱。另外,您不需要发布所有内容,您只需指出问题是选择预格式化文本(我没有考虑到,我正在尝试理解如何解决它),因为该问题与回答有关,而不是您的原始帖子。 - musicamante
显示剩余3条评论

1

经过一些研究,我成功解决了这个问题。

这是完全响应式的,可以带或不带表情符号。

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter,QFontMetrics,QFont
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QStyleOption, QVBoxLayout, QWidget, QStyle
import math
class SuperQLabel(QLabel):
    def __init__(self, *args, **kwargs):
        super(SuperQLabel, self).__init__(*args, **kwargs)

        self.textalignment = Qt.AlignLeft | Qt.TextWrapAnywhere
        self.isTextLabel = True
        self.align = None

    def paintEvent(self, event):

        opt = QStyleOption()
        opt.initFrom(self)
        painter = QPainter(self)

        self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)

        self.style().drawItemText(painter, self.rect(),
                                  self.textalignment, self.palette(), True, self.text())
        
        fm=QFontMetrics(self.font())
        #To get unicode in Text if using Emoji(Optional)
        string_unicode = self.text().encode("unicode_escape").decode()
        ##To remove emoji/unicode from text while calculating
        string_encode = self.text().encode("ascii", "ignore")
        string_decode = string_encode.decode()
        #If Unicode/Emoji is Used
        if string_unicode.count("\\U0001") > 0:
            height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()+1
            # +1 is varrying according to Different font .SO set different value and test.
        else:
            height=fm.boundingRect(self.rect(),Qt.TextWordWrap,string_decode).height()
        row=math.ceil(fm.horizontalAdvance(self.text())/self.width())
        self.setMinimumHeight(row*height)
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.resize(100, 200)

        self.label = QLabel()
        self.label.setWordWrap(True)
        self.label.setStyleSheet("background:red;")
        self.label.setText("11111111111111111111\n2222222211111111")
        self.emoji_font = QFont("Segoe UI Emoji",15,0,False)
        self.emoji_font.setBold(True)
        self.slabel = SuperQLabel()
        self.slabel.setMinimumWidth(10)
        self.slabel.setStyleSheet("background:green;")
        self.slabel.setFont(self.emoji_font)
        ########### Plain Text ######################
        # self.slabel.setText("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
        ####################    Or Using Emoji ############
        self.slabel.setText("111111111111111111111ABCDDWAEQQ1111111111111111111111111111wqewqgdfgdfhyhtyhy11111111111111111111111111111111111111")
        
        self.centralwidget = QWidget()
        self.setCentralWidget(self.centralwidget)

        self.mainlayout = QVBoxLayout()
        self.mainlayout.addWidget(self.label)
        self.mainlayout.addWidget(self.slabel)

        self.centralwidget.setLayout(self.mainlayout)


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

是的,我遇到了一些问题,正在努力修复。但在我的情况下并不涉及递归,我正在开发一个聊天应用程序。你有尝试过调整大小后输出的效果吗? - Hacker6914
你永远无法确定,它不会在你的计算机当前使用当前测试用例当前配置中发生。至少会调用2到3个递归循环,这是因为调用setMinimumHeight会导致一个resizeEvent,自动安排了一次repaint,然后会强制进行进一步的计算,可能会触发整个布局(然后是进一步的repainting等)。你应该考虑使用更适合你要求的对象方法。像你这样的应用程序通常使用图形框架,而不是小部件。 - musicamante
我想QTextEdit可能适合这个。 - Hacker6914
QTextEdit可能是更好的解决方案,因为它已经本地提供了换行选项,所以您不需要覆盖绘画;如果您总是在滚动区域内使用它,那么更新小部件的最小大小提示可能是安全的,因为它将最小化递归的可能性(特别是如果使用基本框布局)。 - musicamante
但是问题在于当我复制和粘贴一些内容时,水平滚动条会出现。如何将文档的宽度调整为小部件的宽度?垂直包装应该完成,因为我想固定QTextEdit的宽度。 - Hacker6914
显示剩余2条评论

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