PyQt:QScrollArea内部小部件的自动换行布局

5
我正在使用PyQt4开发一款记忆文本的应用程序。我想将所有单词显示在气泡中,以便您可以看到单词的长度。但当我将所有气泡放入我的QScrollArea中时,它们会一个接一个地排列。我希望它们能够并排对齐,但同时也要自动换行。
我已经成功使用具有圆角边框的QLabel来实现气泡效果。但是,由于我将单词放在了QLabel中,PyQt不再将它们视为单词,而是视为小部件。因此,PyQt将一个小部件放在另一个小部件下面。我希望这些小部件能够并排对齐,直到达到行尾,然后它们应该自动换行 - 这意味着QLabel应该像文本文档中的单词一样。
以下是我目前的代码:
f = open(r'myFile.txt')

class Bubble(QtGui.QLabel):
    def __init__(self, text):
        super(Bubble, self).__init__(text)
        self.word = text
        self.setContentsMargins(5, 5, 5, 5)

    def paintEvent(self, e):
        p = QtGui.QPainter(self)
        p.setRenderHint(QtGui.QPainter.Antialiasing,True)
        p.drawRoundedRect(0,0,self.width()-1,self.height()-1,5,5)
        super(Bubble, self).paintEvent(e)


class MainWindow(QtGui.QMainWindow):
    def __init__(self, text, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.MainArea = QtGui.QScrollArea
        self.widget = QtGui.QWidget()
        vbox = QtGui.QVBoxLayout()
        self.words = []
        for t in re.findall(r'\b\w+\b', text):
            label = Bubble(t)
            label.setFont(QtGui.QFont('SblHebrew', 18))
            label.setFixedWidth(label.sizeHint().width())
            self.words.append(label)
            vbox.addWidget(label)
        self.widget.setLayout(vbox)
        self.MainArea.setWidget(self.widget)

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    myWindow = MainWindow(f.read(), None)
    myWindow.show()
    sys.exit(app.exec_())

当我运行这个程序时,会出现以下结果:

picture of my example that I do not like

但是我希望单词(包含单词的Qlabel)彼此相邻,而不是像这样(经过Photoshop编辑后):

enter image description here

我已经进行了很多研究,但没有答案能够帮助我将小部件对齐在一起。

你为什么不使用HTML? - ekhumoro
@eyllanesc,我在编辑中更好地解释了问题。 - Cheyn Shmuel
@CheynShmuel 你可以展示一张你想要的结果图片。 另外,PyQt支持桌面应用程序使用HTML。 - eyllanesc
@eyllanesc 我添加了一张Photoshoped图片,展示我想要它看起来的样子。 - Cheyn Shmuel
我不知道为什么现在我使用了from PyQt5 import QtWidgets,并将QtGui.xxxx的所有小部件替换为QtWidgets.xxx。运行代码后,该框无法显示与我放在myFile.txt中的文本相关的任何内容。 - Raii
显示剩余2条评论
2个回答

9
以下是一个 PyQt5 版本的流式布局演示脚本:
```python from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout
app = QApplication([]) window = QWidget()
# 创建按钮并添加到水平布局中 layout = QHBoxLayout() layout.addWidget(QPushButton('Button 1')) layout.addWidget(QPushButton('Button 2')) layout.addWidget(QPushButton('Button 3'))
# 创建垂直布局,并将水平布局添加到其中 v_layout = QVBoxLayout() v_layout.addLayout(layout)
window.setLayout(v_layout) window.show() app.exec_() ```
该脚本创建了一个窗口和三个按钮,并使用流式布局将按钮放置在窗口中。您可以根据需要调整按钮数量和位置,以满足您的布局需求。
import sys
from PyQt5 import QtCore, QtGui, QtWidgets

class FlowLayout(QtWidgets.QLayout):
    def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1):
        super(FlowLayout, self).__init__(parent)
        self._hspacing = hspacing
        self._vspacing = vspacing
        self._items = []
        self.setContentsMargins(margin, margin, margin, margin)

    def __del__(self):
        del self._items[:]

    def addItem(self, item):
        self._items.append(item)

    def horizontalSpacing(self):
        if self._hspacing >= 0:
            return self._hspacing
        else:
            return self.smartSpacing(
                QtWidgets.QStyle.PM_LayoutHorizontalSpacing)

    def verticalSpacing(self):
        if self._vspacing >= 0:
            return self._vspacing
        else:
            return self.smartSpacing(
                QtWidgets.QStyle.PM_LayoutVerticalSpacing)

    def count(self):
        return len(self._items)

    def itemAt(self, index):
        if 0 <= index < len(self._items):
            return self._items[index]

    def takeAt(self, index):
        if 0 <= index < len(self._items):
            return self._items.pop(index)

    def expandingDirections(self):
        return QtCore.Qt.Orientations(0)

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        return self.doLayout(QtCore.QRect(0, 0, width, 0), True)

    def setGeometry(self, rect):
        super(FlowLayout, self).setGeometry(rect)
        self.doLayout(rect, False)

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

    def minimumSize(self):
        size = QtCore.QSize()
        for item in self._items:
            size = size.expandedTo(item.minimumSize())
        left, top, right, bottom = self.getContentsMargins()
        size += QtCore.QSize(left + right, top + bottom)
        return size

    def doLayout(self, rect, testonly):
        left, top, right, bottom = self.getContentsMargins()
        effective = rect.adjusted(+left, +top, -right, -bottom)
        x = effective.x()
        y = effective.y()
        lineheight = 0
        for item in self._items:
            widget = item.widget()
            hspace = self.horizontalSpacing()
            if hspace == -1:
                hspace = widget.style().layoutSpacing(
                    QtWidgets.QSizePolicy.PushButton,
                    QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Horizontal)
            vspace = self.verticalSpacing()
            if vspace == -1:
                vspace = widget.style().layoutSpacing(
                    QtWidgets.QSizePolicy.PushButton,
                    QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Vertical)
            nextX = x + item.sizeHint().width() + hspace
            if nextX - hspace > effective.right() and lineheight > 0:
                x = effective.x()
                y = y + lineheight + vspace
                nextX = x + item.sizeHint().width() + hspace
                lineheight = 0
            if not testonly:
                item.setGeometry(
                    QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint()))
            x = nextX
            lineheight = max(lineheight, item.sizeHint().height())
        return y + lineheight - rect.y() + bottom

    def smartSpacing(self, pm):
        parent = self.parent()
        if parent is None:
            return -1
        elif parent.isWidgetType():
            return parent.style().pixelMetric(pm, None, parent)
        else:
            return parent.spacing()

class Bubble(QtWidgets.QLabel):
    def __init__(self, text):
        super(Bubble, self).__init__(text)
        self.word = text
        self.setContentsMargins(5, 5, 5, 5)

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
        painter.drawRoundedRect(
            0, 0, self.width() - 1, self.height() - 1, 5, 5)
        super(Bubble, self).paintEvent(event)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, text, parent=None):
        super(MainWindow, self).__init__(parent)
        self.mainArea = QtWidgets.QScrollArea(self)
        self.mainArea.setWidgetResizable(True)
        widget = QtWidgets.QWidget(self.mainArea)
        widget.setMinimumWidth(50)
        layout = FlowLayout(widget)
        self.words = []
        for word in text.split():
            label = Bubble(word)
            label.setFont(QtGui.QFont('SblHebrew', 18))
            label.setFixedWidth(label.sizeHint().width())
            self.words.append(label)
            layout.addWidget(label)
        self.mainArea.setWidget(widget)
        self.setCentralWidget(self.mainArea)

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow('Harry Potter is a series of fantasy literature')
    window.show()
    sys.exit(app.exec_())

4

我认为在 QTextBrowser 插件中使用html可能是可行的,但是Qt的富文本引擎不支持需要用到泡泡标签的 border-radius CSS属性。

因此,看起来您需要一个PyQt版本的Flow Layout example。 这可以将小部件集合“自动换行”放置在容器内,并允许调整边距和水平/垂直间距。

这是一个演示脚本,实现了 FlowLayout 类,并展示了如何在您的示例中使用它:

import sys
from PyQt4 import QtCore, QtGui

class FlowLayout(QtGui.QLayout):
    def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1):
        super(FlowLayout, self).__init__(parent)
        self._hspacing = hspacing
        self._vspacing = vspacing
        self._items = []
        self.setContentsMargins(margin, margin, margin, margin)

    def __del__(self):
        del self._items[:]

    def addItem(self, item):
        self._items.append(item)

    def horizontalSpacing(self):
        if self._hspacing >= 0:
            return self._hspacing
        else:
            return self.smartSpacing(
                QtGui.QStyle.PM_LayoutHorizontalSpacing)

    def verticalSpacing(self):
        if self._vspacing >= 0:
            return self._vspacing
        else:
            return self.smartSpacing(
                QtGui.QStyle.PM_LayoutVerticalSpacing)

    def count(self):
        return len(self._items)

    def itemAt(self, index):
        if 0 <= index < len(self._items):
            return self._items[index]

    def takeAt(self, index):
        if 0 <= index < len(self._items):
            return self._items.pop(index)

    def expandingDirections(self):
        return QtCore.Qt.Orientations(0)

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        return self.doLayout(QtCore.QRect(0, 0, width, 0), True)

    def setGeometry(self, rect):
        super(FlowLayout, self).setGeometry(rect)
        self.doLayout(rect, False)

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

    def minimumSize(self):
        size = QtCore.QSize()
        for item in self._items:
            size = size.expandedTo(item.minimumSize())
        left, top, right, bottom = self.getContentsMargins()
        size += QtCore.QSize(left + right, top + bottom)
        return size

    def doLayout(self, rect, testonly):
        left, top, right, bottom = self.getContentsMargins()
        effective = rect.adjusted(+left, +top, -right, -bottom)
        x = effective.x()
        y = effective.y()
        lineheight = 0
        for item in self._items:
            widget = item.widget()
            hspace = self.horizontalSpacing()
            if hspace == -1:
                hspace = widget.style().layoutSpacing(
                    QtGui.QSizePolicy.PushButton,
                    QtGui.QSizePolicy.PushButton, QtCore.Qt.Horizontal)
            vspace = self.verticalSpacing()
            if vspace == -1:
                vspace = widget.style().layoutSpacing(
                    QtGui.QSizePolicy.PushButton,
                    QtGui.QSizePolicy.PushButton, QtCore.Qt.Vertical)
            nextX = x + item.sizeHint().width() + hspace
            if nextX - hspace > effective.right() and lineheight > 0:
                x = effective.x()
                y = y + lineheight + vspace
                nextX = x + item.sizeHint().width() + hspace
                lineheight = 0
            if not testonly:
                item.setGeometry(
                    QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint()))
            x = nextX
            lineheight = max(lineheight, item.sizeHint().height())
        return y + lineheight - rect.y() + bottom

    def smartSpacing(self, pm):
        parent = self.parent()
        if parent is None:
            return -1
        elif parent.isWidgetType():
            return parent.style().pixelMetric(pm, None, parent)
        else:
            return parent.spacing()

class Bubble(QtGui.QLabel):
    def __init__(self, text):
        super(Bubble, self).__init__(text)
        self.word = text
        self.setContentsMargins(5, 5, 5, 5)

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
        painter.drawRoundedRect(
            0, 0, self.width() - 1, self.height() - 1, 5, 5)
        super(Bubble, self).paintEvent(event)

class MainWindow(QtGui.QMainWindow):
    def __init__(self, text, parent=None):
        super(MainWindow, self).__init__(parent)
        self.mainArea = QtGui.QScrollArea(self)
        self.mainArea.setWidgetResizable(True)
        widget = QtGui.QWidget(self.mainArea)
        widget.setMinimumWidth(50)
        layout = FlowLayout(widget)
        self.words = []
        for word in text.split():
            label = Bubble(word)
            label.setFont(QtGui.QFont('SblHebrew', 18))
            label.setFixedWidth(label.sizeHint().width())
            self.words.append(label)
            layout.addWidget(label)
        self.mainArea.setWidget(widget)
        self.setCentralWidget(self.mainArea)

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    window = MainWindow('Harry Potter is a series of fantasy literature')
    window.show()
    sys.exit(app.exec_())

非常感谢!这正是我想要的!虽然我很遗憾地说我不明白为什么。两个问题:(1)有没有办法将小部件从右到左显示?(2)有没有办法将小部件对齐到任何方向,或者使它们对齐,也许? - Cheyn Shmuel
@CheynShmuel。也许可以,但需要进行大量的额外工作。这就是为什么我建议早些时候使用html——它解决了排版文档的所有困难,并且可以轻松地以各种不同的方式格式化单词。不幸的是,“QTextEdit”和“QTextBrowser”类不支持现代html/css的全部功能集,因此无法获得像圆角之类的花哨效果。但从右到左和对齐是受支持的。 - ekhumoro
我知道这可能很难,但至少是有可能的。你能至少指点我一下方向吗?我知道这可能很难,所以你能至少帮我一点吗? - Cheyn Shmuel
谢谢。我会尝试去做。 - Cheyn Shmuel
这是C++,Qt编写的语言。请参见我上面的答案,其中有一个用Python编写的自定义布局管理器的示例(我从C++移植过来的)。我不知道还有其他的。 - ekhumoro
显示剩余3条评论

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