PyQt - 面向流程布局
我正在尝试调整 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 个(可能相关)问题:
QLayout
具有hasHeightForWidth
和heightForWidth
方法,但是不是它们的倒数hasWidthForHeight
和widthForHeight
。不管怎样,我实现了后两种方法,但我怀疑它们是否真的被调用过。- 当使用布局的水平变体时,窗口会自动调整适当的大小以包含所有项目。当使用垂直变体时,情况并非如此。但是,如果您手动调整窗口大小,垂直布局确实可以正常工作。
如何正确实施垂直流布局?
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:
QLayout
has thehasHeightForWidth
andheightForWidth
methods, but not their inverseshasWidthForHeight
andwidthForHeight
. I implemented the latter two methods regardless, but I doubt they're ever actually getting called.- 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
正如您已经发现的,Qt 布局不支持 widthForHeight,并且通常不鼓励使用此类布局,主要是因为它们在具有嵌套布局和混合小部件大小策略的复杂情况下往往会表现得不稳定。即使非常小心它们的实现,您也可能最终会递归调用大小提示、策略等。
也就是说,部分解决方案仍然返回宽度的高度,但垂直而不是水平放置小部件。
这是小部件一显示出来的样子(几乎与水平流相同):
< 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.
This is how the widget appears as soon as it's shown (which is almost the same as the horizontal flow):
Now, resizing to allow less vertical space:
And even smaller height:
虽然答案由@提供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
要使用它,请执行以下操作:
在声明 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
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)