PyQt - 面向流程布局

发布于 2025-01-11 04:09:59 字数 4665 浏览 0 评论 0原文

我正在尝试调整 FlowLayout 的 PyQt 实现 以允许垂直流动和水平流动。这是我当前的实现:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *


class FlowLayout(QLayout):
    def __init__(self, orientation=Qt.Horizontal, parent=None, margin=0, spacing=-1):
        super().__init__(parent)
        self.orientation = orientation

        if parent is not None:
            self.setContentsMargins(margin, margin, margin, margin)

        self.setSpacing(spacing)

        self.itemList = []

    def __del__(self):
        item = self.takeAt(0)
        while item:
            item = self.takeAt(0)

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

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

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

        return None

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

        return None

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

    def hasHeightForWidth(self):
        return self.orientation == Qt.Horizontal

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

    def hasWidthForHeight(self):
        return self.orientation == Qt.Vertical

    def widthForHeight(self, height):
        return self.doLayout(QRect(0, 0, 0, height), True)

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

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

    def minimumSize(self):
        size = QSize()

        for item in self.itemList:
            size = size.expandedTo(item.minimumSize())

        margin, _, _, _ = self.getContentsMargins()

        size += QSize(2 * margin, 2 * margin)
        return size

    def doLayout(self, rect, testOnly):
        x = rect.x()
        y = rect.y()
        offset = 0
        horizontal = self.orientation == Qt.Horizontal

        for item in self.itemList:
            wid = item.widget()
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)

            if horizontal:
                next = x + item.sizeHint().width() + spaceX
                if next - spaceX > rect.right() and offset > 0:
                    x = rect.x()
                    y += offset + spaceY
                    next = x + item.sizeHint().width() + spaceX
                    offset = 0
            else:
                next = y + item.sizeHint().height() + spaceY
                if next - spaceY > rect.bottom() and offset > 0:
                    x += offset + spaceX
                    y = rect.y()
                    next = y + item.sizeHint().height() + spaceY
                    offset = 0

            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            if horizontal:
                x = next
                offset = max(offset, item.sizeHint().height())
            else:
                y = next
                offset = max(offset, item.sizeHint().width())

        return y + offset - rect.y() if horizontal else x + offset - rect.x()


if __name__ == '__main__':
    class Window(QWidget):
        def __init__(self):
            super().__init__()

            #flowLayout = FlowLayout(orientation=Qt.Horizontal)
            flowLayout = FlowLayout(orientation=Qt.Vertical)
            flowLayout.addWidget(QPushButton("Short"))
            flowLayout.addWidget(QPushButton("Longer"))
            flowLayout.addWidget(QPushButton("Different text"))
            flowLayout.addWidget(QPushButton("More text"))
            flowLayout.addWidget(QPushButton("Even longer button text"))
            self.setLayout(flowLayout)

            self.setWindowTitle("Flow Layout")

    import sys

    app = QApplication(sys.argv)
    mainWin = Window()
    mainWin.show()
    sys.exit(app.exec_())

此实现在处理垂直布局时有 2 个(可能相关)问题:

  1. QLayout 具有 hasHeightForWidthheightForWidth 方法,但是不是它们的倒数 hasWidthForHeightwidthForHeight。不管怎样,我实现了后两种方法,但我怀疑它们是否真的被调用过。
  2. 当使用布局的水平变体时,窗口会自动调整适当的大小以包含所有项目。当使用垂直变体时,情况并非如此。但是,如果您手动调整窗口大小,垂直布局确实可以正常工作。

如何正确实施垂直流布局?

I'm trying to adapt this PyQt implementation of FlowLayout to allow vertical flow as well as horizontal. This is my current implementation:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *


class FlowLayout(QLayout):
    def __init__(self, orientation=Qt.Horizontal, parent=None, margin=0, spacing=-1):
        super().__init__(parent)
        self.orientation = orientation

        if parent is not None:
            self.setContentsMargins(margin, margin, margin, margin)

        self.setSpacing(spacing)

        self.itemList = []

    def __del__(self):
        item = self.takeAt(0)
        while item:
            item = self.takeAt(0)

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

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

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

        return None

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

        return None

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

    def hasHeightForWidth(self):
        return self.orientation == Qt.Horizontal

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

    def hasWidthForHeight(self):
        return self.orientation == Qt.Vertical

    def widthForHeight(self, height):
        return self.doLayout(QRect(0, 0, 0, height), True)

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

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

    def minimumSize(self):
        size = QSize()

        for item in self.itemList:
            size = size.expandedTo(item.minimumSize())

        margin, _, _, _ = self.getContentsMargins()

        size += QSize(2 * margin, 2 * margin)
        return size

    def doLayout(self, rect, testOnly):
        x = rect.x()
        y = rect.y()
        offset = 0
        horizontal = self.orientation == Qt.Horizontal

        for item in self.itemList:
            wid = item.widget()
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)

            if horizontal:
                next = x + item.sizeHint().width() + spaceX
                if next - spaceX > rect.right() and offset > 0:
                    x = rect.x()
                    y += offset + spaceY
                    next = x + item.sizeHint().width() + spaceX
                    offset = 0
            else:
                next = y + item.sizeHint().height() + spaceY
                if next - spaceY > rect.bottom() and offset > 0:
                    x += offset + spaceX
                    y = rect.y()
                    next = y + item.sizeHint().height() + spaceY
                    offset = 0

            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            if horizontal:
                x = next
                offset = max(offset, item.sizeHint().height())
            else:
                y = next
                offset = max(offset, item.sizeHint().width())

        return y + offset - rect.y() if horizontal else x + offset - rect.x()


if __name__ == '__main__':
    class Window(QWidget):
        def __init__(self):
            super().__init__()

            #flowLayout = FlowLayout(orientation=Qt.Horizontal)
            flowLayout = FlowLayout(orientation=Qt.Vertical)
            flowLayout.addWidget(QPushButton("Short"))
            flowLayout.addWidget(QPushButton("Longer"))
            flowLayout.addWidget(QPushButton("Different text"))
            flowLayout.addWidget(QPushButton("More text"))
            flowLayout.addWidget(QPushButton("Even longer button text"))
            self.setLayout(flowLayout)

            self.setWindowTitle("Flow Layout")

    import sys

    app = QApplication(sys.argv)
    mainWin = Window()
    mainWin.show()
    sys.exit(app.exec_())

This implementation has 2 (likely related) problems when handling vertical layouts:

  1. QLayout has the hasHeightForWidth and heightForWidth methods, but not their inverses hasWidthForHeight and widthForHeight. I implemented the latter two methods regardless, but I doubt they're ever actually getting called.
  2. When using the horizontal variant of the layout, the window is automatically appropriately sized to contain all the items. When using the vertical variant, this is not the case. However, the vertical layout does work properly if you manually resize the window.

How do I properly implement a vertical flow layout?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

夜司空 2025-01-18 04:09:59

正如您已经发现的,Qt 布局不支持 widthForHeight,并且通常不鼓励使用此类布局,主要是因为它们在具有嵌套布局和混合小部件大小策略的复杂情况下往往会表现得不稳定。即使非常小心它们的实现,您也可能最终会递归调用大小提示、策略等。

也就是说,部分解决方案仍然返回宽度的高度,但垂直而不是水平放置小部件。

    def doLayout(self, rect, testOnly):
        x = rect.x()
        y = rect.y()
        lineHeight = columnWidth = heightForWidth = 0

        for item in self.itemList:
            wid = item.widget()
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
            if self.orientation == Qt.Horizontal:
                nextX = x + item.sizeHint().width() + spaceX
                if nextX - spaceX > rect.right() and lineHeight > 0:
                    x = rect.x()
                    y = y + lineHeight + spaceY
                    nextX = x + item.sizeHint().width() + spaceX
                    lineHeight = 0

                if not testOnly:
                    item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

                x = nextX
                lineHeight = max(lineHeight, item.sizeHint().height())
            else:
                nextY = y + item.sizeHint().height() + spaceY
                if nextY - spaceY > rect.bottom() and columnWidth > 0:
                    x = x + columnWidth + spaceX
                    y = rect.y()
                    nextY = y + item.sizeHint().height() + spaceY
                    columnWidth = 0

                heightForWidth += item.sizeHint().height() + spaceY
                if not testOnly:
                    item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

                y = nextY
                columnWidth = max(columnWidth, item.sizeHint().width())

        if self.orientation == Qt.Horizontal:
            return y + lineHeight - rect.y()
        else:
            return heightForWidth - rect.y()

这是小部件一显示出来的样子(几乎与水平流相同):

< img src="https://i.sstatic.net/rrEEY.png" alt="first shown">

现在,调整大小以减少垂直空间:

小垂直空间

甚至更小的高度:

没那么高,嗯?

As you already found out, Qt layouts don't support widthForHeight, and, in general, these kinds of layouts are discouraged, mostly because they tend to behave erratically in complex situation with nested layouts and mixed widget size policies. Even when being very careful about their implementation, you might end up in recursive calls to size hints, policies etc.

That said, a partial solution is to still return a height for width, but position the widgets vertically instead of horizontally.

    def doLayout(self, rect, testOnly):
        x = rect.x()
        y = rect.y()
        lineHeight = columnWidth = heightForWidth = 0

        for item in self.itemList:
            wid = item.widget()
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
            if self.orientation == Qt.Horizontal:
                nextX = x + item.sizeHint().width() + spaceX
                if nextX - spaceX > rect.right() and lineHeight > 0:
                    x = rect.x()
                    y = y + lineHeight + spaceY
                    nextX = x + item.sizeHint().width() + spaceX
                    lineHeight = 0

                if not testOnly:
                    item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

                x = nextX
                lineHeight = max(lineHeight, item.sizeHint().height())
            else:
                nextY = y + item.sizeHint().height() + spaceY
                if nextY - spaceY > rect.bottom() and columnWidth > 0:
                    x = x + columnWidth + spaceX
                    y = rect.y()
                    nextY = y + item.sizeHint().height() + spaceY
                    columnWidth = 0

                heightForWidth += item.sizeHint().height() + spaceY
                if not testOnly:
                    item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

                y = nextY
                columnWidth = max(columnWidth, item.sizeHint().width())

        if self.orientation == Qt.Horizontal:
            return y + lineHeight - rect.y()
        else:
            return heightForWidth - rect.y()

This is how the widget appears as soon as it's shown (which is almost the same as the horizontal flow):

first shown

Now, resizing to allow less vertical space:

small vertical space

And even smaller height:

not that tall, huh?

£噩梦荏苒 2025-01-18 04:09:59

虽然答案@提供musicamente 有效,但它是不完整的:

缺少的是 widthForHeight 机制:当项目添加到布局中时,容器小部件的 minimumWidth 未更新。

由于某种原因,Qt 决定应该存在 heightForWidth 机制,而不是 widthForHeight

看起来,当使用 heightForWidth 机制时,父窗口小部件的 minimumHeight 会通过 Qt 框架自动更新(我可能是错的,但我认为情况确实如此) 。

@musicamente 提供的示例中,由于主窗口是可调整大小的,因此这种限制并不容易看出。

然而,当使用 QScrollArea 时,这个限制是显而易见的,因为滚动条不显示并且视图被截断。

因此,我们需要确定 FlowLayout 的哪一行最宽,并相应地设置父窗口小部件的minimumWidth。

我是这样实现的:

当放置项目时,它们被分配 i 和 j 索引,表示它们在 2D 数组中的位置。

然后,一旦放置了所有这些,我们就确定最宽行的宽度(包括项目之间的间距),并使用可以连接到 setMinimumWidth 方法的专用信号让父窗口小部件知道。

我的解决方案可能并不完美,也不是一个很好的实现,但它是迄今为止我找到的实现我想要的目标的最佳替代方案。

下面的代码将提供一个工作版本,虽然我发现我的解决方案不是很优雅,但它有效。

如果您有关于如何优化它的想法,请随时通过在我的 GitHub 上创建 PR 来改进我的实现:https://github.com/azsde/BatchMkvToolbox/tree/main/ui/customLayout

class FlowLayout(QLayout):

    widthChanged = pyqtSignal(int)

    def __init__(self, parent=None, margin=0, spacing=-1, orientation=Qt.Horizontal):
        super(FlowLayout, self).__init__(parent)

        if parent is not None:
            self.setContentsMargins(margin, margin, margin, margin)

        self.setSpacing(spacing)
        self.itemList = []
        self.orientation = orientation

    def __del__(self):
        item = self.takeAt(0)
        while item:
            item = self.takeAt(0)

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

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

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

        return None

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

        return None

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

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        if (self.orientation == Qt.Horizontal):
            return self.doLayoutHorizontal(QRect(0, 0, width, 0), True)
        elif (self.orientation == Qt.Vertical):
            return self.doLayoutVertical(QRect(0, 0, width, 0), True)

    def setGeometry(self, rect):
        super(FlowLayout, self).setGeometry(rect)
        if (self.orientation == Qt.Horizontal):
            self.doLayoutHorizontal(rect, False)
        elif (self.orientation == Qt.Vertical):
            self.doLayoutVertical(rect, False)

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

    def minimumSize(self):
        size = QSize()

        for item in self.itemList:
            size = size.expandedTo(item.minimumSize())

        margin, _, _, _ = self.getContentsMargins()

        size += QSize(2 * margin, 2 * margin)
        return size

    def doLayoutHorizontal(self, rect, testOnly):
        # Get initial coordinates of the drawing region (should be 0, 0)
        x = rect.x()
        y = rect.y()
        lineHeight = 0
        i = 0
        for item in self.itemList:
            wid = item.widget()
            # Space X and Y is item spacing horizontally and vertically
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
            # Determine the coordinate we want to place the item at
            # It should be placed at : initial coordinate of the rect + width of the item + spacing
            nextX = x + item.sizeHint().width() + spaceX
            # If the calculated nextX is greater than the outer bound...
            if nextX - spaceX > rect.right() and lineHeight > 0:
                x = rect.x() # Reset X coordinate to origin of drawing region
                y = y + lineHeight + spaceY # Move Y coordinate to the next line
                nextX = x + item.sizeHint().width() + spaceX # Recalculate nextX based on the new X coordinate
                lineHeight = 0

            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            x = nextX # Store the next starting X coordinate for next item
            lineHeight = max(lineHeight, item.sizeHint().height())
            i = i + 1

        return y + lineHeight - rect.y()

    def doLayoutVertical(self, rect, testOnly):
        # Get initial coordinates of the drawing region (should be 0, 0)
        x = rect.x()
        y = rect.y()
        # Initalize column width and line height
        columnWidth = 0
        lineHeight = 0

        # Space between items
        spaceX = 0
        spaceY = 0

        # Variables that will represent the position of the widgets in a 2D Array
        i = 0
        j = 0
        for item in self.itemList:
            wid = item.widget()
            # Space X and Y is item spacing horizontally and vertically
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
            # Determine the coordinate we want to place the item at
            # It should be placed at : initial coordinate of the rect + width of the item + spacing
            nextY = y + item.sizeHint().height() + spaceY
            # If the calculated nextY is greater than the outer bound, move to the next column
            if nextY - spaceY > rect.bottom() and columnWidth > 0:
                y = rect.y() # Reset y coordinate to origin of drawing region
                x = x + columnWidth + spaceX # Move X coordinate to the next column
                nextY = y + item.sizeHint().height() + spaceY # Recalculate nextX based on the new X coordinate
                # Reset the column width
                columnWidth = 0

                # Set indexes of the item for the 2D array
                j += 1
                i = 0

            # Assign 2D array indexes
            item.x_index = i
            item.y_index = j

            # Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
            # For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            y = nextY # Store the next starting Y coordinate for next item
            columnWidth = max(columnWidth, item.sizeHint().width()) # Update the width of the column
            lineHeight = max(lineHeight, item.sizeHint().height()) # Update the height of the line

            i += 1 # Increment i

        # Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
        # For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
        if not testOnly:
            self.calculateMaxWidth(i)
            self.widthChanged.emit(self.totalMaxWidth + spaceX * self.itemsOnWidestRow)
        return lineHeight

    # Method to calculate the maximum width among each "row" of the flow layout
    # This will be useful to let the UI know the total width of the flow layout
    def calculateMaxWidth(self, numberOfRows):
        # Init variables
        self.totalMaxWidth = 0
        self.itemsOnWidestRow = 0

        # For each "row", calculate the total width by adding the width of each item
        # and then update the totalMaxWidth if the calculated width is greater than the current value
        # Also update the number of items on the widest row
        for i in range(numberOfRows):
            rowWidth = 0
            itemsOnWidestRow = 0
            for item in self.itemList:
                # Only compare items from the same row
                if (item.x_index == i):
                    rowWidth += item.sizeHint().width()
                    itemsOnWidestRow += 1
                if (rowWidth > self.totalMaxWidth):
                    self.totalMaxWidth = rowWidth
                    self.itemsOnWidestRow = itemsOnWidestRow

要使用它,请执行以下操作:

  • 在声明 FlowLayout 时,指定其方向:

    myFlowLayout = FlowLayout(containerWidget,orientation=Qt.Vertical)

  • 将 FlowLayout 的 widthChanged 信号连接到容器的 setMinimumWidth 方法:

    myFlowLayout.widthChanged.connect(containerWidget.setMinimumWidth)

While the answer provided by @musicamente works, it is incomplete:

What is missing is the widthForHeight mecanism: as items are added into the layout, the minimumWidth of the container widget is not updated.

For some reason, Qt decided that heightForWidth mecanism should exist but not widthForHeight.

It would seem that when using the heightForWidth mecanism, the minimumHeight of the parent widget is automatically updated via the Qt framework (I may be wrong but I think it is the case).

In the example provided by @musicamente, as the main window is resizable this limitation is not easilly seen.

However when using a QScrollArea, this limitation is cleary observable as the scrollbar doesn't show up and the view is truncated.

So we need to determine which row of the FlowLayout is the widest and set the minimumWidth of the parent widget accordingly.

I've implemented it like so:

As the items are placed, that they are assigned i and j indexes which represent their position in a 2D array.

Then once all of them are placed, we determine the width of the widest row (including spacing between items) and let the parent widget know using a dedicated signal which can be connected to the setMinimumWidth method.

My solution might not be perfect nor a great implementation, but it is the best alternative I found so far to achieve what I wanted.

The following code will provide a working version, while I don't find my solution very elegant, it works.

If you have ideas on how to optimize it feel free to improve my implementation by making a PR on my GitHub : https://github.com/azsde/BatchMkvToolbox/tree/main/ui/customLayout

class FlowLayout(QLayout):

    widthChanged = pyqtSignal(int)

    def __init__(self, parent=None, margin=0, spacing=-1, orientation=Qt.Horizontal):
        super(FlowLayout, self).__init__(parent)

        if parent is not None:
            self.setContentsMargins(margin, margin, margin, margin)

        self.setSpacing(spacing)
        self.itemList = []
        self.orientation = orientation

    def __del__(self):
        item = self.takeAt(0)
        while item:
            item = self.takeAt(0)

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

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

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

        return None

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

        return None

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

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        if (self.orientation == Qt.Horizontal):
            return self.doLayoutHorizontal(QRect(0, 0, width, 0), True)
        elif (self.orientation == Qt.Vertical):
            return self.doLayoutVertical(QRect(0, 0, width, 0), True)

    def setGeometry(self, rect):
        super(FlowLayout, self).setGeometry(rect)
        if (self.orientation == Qt.Horizontal):
            self.doLayoutHorizontal(rect, False)
        elif (self.orientation == Qt.Vertical):
            self.doLayoutVertical(rect, False)

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

    def minimumSize(self):
        size = QSize()

        for item in self.itemList:
            size = size.expandedTo(item.minimumSize())

        margin, _, _, _ = self.getContentsMargins()

        size += QSize(2 * margin, 2 * margin)
        return size

    def doLayoutHorizontal(self, rect, testOnly):
        # Get initial coordinates of the drawing region (should be 0, 0)
        x = rect.x()
        y = rect.y()
        lineHeight = 0
        i = 0
        for item in self.itemList:
            wid = item.widget()
            # Space X and Y is item spacing horizontally and vertically
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
            # Determine the coordinate we want to place the item at
            # It should be placed at : initial coordinate of the rect + width of the item + spacing
            nextX = x + item.sizeHint().width() + spaceX
            # If the calculated nextX is greater than the outer bound...
            if nextX - spaceX > rect.right() and lineHeight > 0:
                x = rect.x() # Reset X coordinate to origin of drawing region
                y = y + lineHeight + spaceY # Move Y coordinate to the next line
                nextX = x + item.sizeHint().width() + spaceX # Recalculate nextX based on the new X coordinate
                lineHeight = 0

            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            x = nextX # Store the next starting X coordinate for next item
            lineHeight = max(lineHeight, item.sizeHint().height())
            i = i + 1

        return y + lineHeight - rect.y()

    def doLayoutVertical(self, rect, testOnly):
        # Get initial coordinates of the drawing region (should be 0, 0)
        x = rect.x()
        y = rect.y()
        # Initalize column width and line height
        columnWidth = 0
        lineHeight = 0

        # Space between items
        spaceX = 0
        spaceY = 0

        # Variables that will represent the position of the widgets in a 2D Array
        i = 0
        j = 0
        for item in self.itemList:
            wid = item.widget()
            # Space X and Y is item spacing horizontally and vertically
            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
            # Determine the coordinate we want to place the item at
            # It should be placed at : initial coordinate of the rect + width of the item + spacing
            nextY = y + item.sizeHint().height() + spaceY
            # If the calculated nextY is greater than the outer bound, move to the next column
            if nextY - spaceY > rect.bottom() and columnWidth > 0:
                y = rect.y() # Reset y coordinate to origin of drawing region
                x = x + columnWidth + spaceX # Move X coordinate to the next column
                nextY = y + item.sizeHint().height() + spaceY # Recalculate nextX based on the new X coordinate
                # Reset the column width
                columnWidth = 0

                # Set indexes of the item for the 2D array
                j += 1
                i = 0

            # Assign 2D array indexes
            item.x_index = i
            item.y_index = j

            # Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
            # For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            y = nextY # Store the next starting Y coordinate for next item
            columnWidth = max(columnWidth, item.sizeHint().width()) # Update the width of the column
            lineHeight = max(lineHeight, item.sizeHint().height()) # Update the height of the line

            i += 1 # Increment i

        # Only call setGeometry (which place the actual widget using coordinates) if testOnly is false
        # For some reason, Qt framework calls the doLayout methods with testOnly set to true (WTF ??)
        if not testOnly:
            self.calculateMaxWidth(i)
            self.widthChanged.emit(self.totalMaxWidth + spaceX * self.itemsOnWidestRow)
        return lineHeight

    # Method to calculate the maximum width among each "row" of the flow layout
    # This will be useful to let the UI know the total width of the flow layout
    def calculateMaxWidth(self, numberOfRows):
        # Init variables
        self.totalMaxWidth = 0
        self.itemsOnWidestRow = 0

        # For each "row", calculate the total width by adding the width of each item
        # and then update the totalMaxWidth if the calculated width is greater than the current value
        # Also update the number of items on the widest row
        for i in range(numberOfRows):
            rowWidth = 0
            itemsOnWidestRow = 0
            for item in self.itemList:
                # Only compare items from the same row
                if (item.x_index == i):
                    rowWidth += item.sizeHint().width()
                    itemsOnWidestRow += 1
                if (rowWidth > self.totalMaxWidth):
                    self.totalMaxWidth = rowWidth
                    self.itemsOnWidestRow = itemsOnWidestRow

To use it do the following:

  • When declaring a FlowLayout, specify its orientation :

    myFlowLayout = FlowLayout(containerWidget, orientation=Qt.Vertical)

  • Connect the FlowLayout's widthChanged signal to the setMinimumWidth method of the container:

    myFlowLayout.widthChanged.connect(containerWidget.setMinimumWidth)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文