У меня проблема с QGridLayout
использованием PyQt5
. В моем пользовательском интерфейсе есть два pyqtgraph
PlotWidget(). Один из них содержит 5 разных сюжетов, а другой - только один. Я хочу, чтобы PlotWidget()
с 5 участками был в 5 раз больше, чем другой. Следующий код настраивает все для этой пропорции размера графика:
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1) # self.p1 has 5 plots in it.
grid_layout.addWidget(self.p2, 5, 1, 1, 1) # self.p2 has only 1 plot in it.
Проблема в том, что python делает прямо противоположное. Виджет с одним сюжетом становится в 5 раз больше. Я не знаю почему. Я хотел, чтобы self.p1
был в (row=0, col=1)
и занимал 5 строк и 1 столбец, а self.p2
был в (row=5, col=1)
и занимал только 1 строку и 1 столбец. По какой-то причине Python этого не делает (я использую python 3.11).
Вот минимальный рабочий пример:
NFRAME = 200 * 6
import sys
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QGridLayout, QComboBox, QSlider
from PyQt5.QtGui import QColor
import pyqtgraph as pg
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
grid_layout = QGridLayout()
vlayout = QVBoxLayout()
self.p1 = pg.PlotWidget()
self.p2 = pg.PlotWidget()
self.p1.plotItem.addLegend()
self.p2.plotItem.addLegend()
x = np.linspace(0, 2 * np.pi * 2, NFRAME)
y1 = np.sin(2 * np.pi * 1 * x)
y2 = 10 * np.sin(2 * np.pi * 2 * x) + 50
y3 = 20 * np.sin(2 * np.pi * 3 * x) + 200
y4 = 30 * np.sin(2 * np.pi * 4 * x) + 300
y5 = 40 * np.sin(2 * np.pi * 5 * x) + 400
self.ph1_y1 = self.p1.plot(x, y1, pen=pg.mkPen(QColor(255, 0, 0)), name = "y1")
self.ph1_y2 = self.p1.plot(x, y2, pen=pg.mkPen(QColor(255, 255, 0)), name = "y2")
self.ph1_y3 = self.p1.plot(x, y3, pen=pg.mkPen(QColor(0, 255, 255)), name = "y3")
self.ph1_y4 = self.p1.plot(x, y4, pen=pg.mkPen(QColor(255, 0, 255)), name = "y4")
self.ph1_y5 = self.p1.plot(x, y5, pen=pg.mkPen(QColor(102, 102, 255)), name = "y5")
self.ph2_y1 = self.p2.plot(x, 0 * x + 6)
signal_select = QComboBox(self)
signal_select.addItem("Item1")
signal_select.addItem("Item2")
signal_offset = QSlider(Qt.Orientation.Vertical)
signal_gain = QSlider(Qt.Orientation.Vertical)
hlayout_sliders = QHBoxLayout()
hlayout_sliders.addWidget(signal_offset)
hlayout_sliders.addWidget(signal_gain)
hlayout_sliders_labels = QHBoxLayout()
signal_offset_label = QLabel()
signal_gain_label = QLabel()
signal_offset_label.setText("Offset")
signal_offset_label.setAlignment(Qt.AlignCenter)
signal_gain_label.setText("Gain")
signal_gain_label.setAlignment(Qt.AlignCenter)
hlayout_sliders_labels.addWidget(signal_offset_label)
hlayout_sliders_labels.addWidget(signal_gain_label)
arduino_signal_reset = QPushButton("Reset")
vlayout_signals = QVBoxLayout()
vlayout_signals.addWidget(signal_select)
vlayout_signals.addLayout(hlayout_sliders)
vlayout_signals.addLayout(hlayout_sliders_labels)
vlayout_signals.addWidget(arduino_signal_reset)
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1)
grid_layout.addWidget(self.p2, 5, 1, 1, 1)
widget = QWidget()
widget.setLayout(grid_layout)
vlayout.addWidget(widget)
self.setLayout(grid_layout)
self.setCentralWidget(widget)
self.setWindowTitle('My softwaree')
if __name__ == '__main__':
app = QApplication(sys.argv)
windowExample = MainWindow()
windowExample.show()
sys.exit(app.exec_())
Вы можете попробовать настроить коэффициенты растяжения в строках сетки.
Например, что-то вроде этого может быть тем, что вы ищете.
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1)
grid_layout.addWidget(self.p2, 5, 1, 1, 1)
for row in range(6):
grid_layout.setRowStretch(row, 1)
Приведенное выше решение не будет работать во всех ситуациях. Для лучшего объяснения см. Ответ @musicmante.
Не используйте промежутки для установки пропорций между виджетами, вместо этого используйте setRowStretch() или, что лучше, вложенные макеты.
Распространенное заблуждение о QGridLayout заключается в том, что использование «пробелов» в строках и столбцах приведет к их размещению в разных позициях, и, аналогично, использование больших интервалов строк/столбцов приведет к большему размеру, доступному для виджета. Это не то, как работает макет сетки, поскольку его строки и столбцы не имеют определенного размера и не учитываются напрямую для соотношения размеров.
...По крайней мере, в теории.
То, что происходит в вашем случае, связано со сложным способом поведения и взаимодействия QGridLayout и PlotWidget.
Когда виджет, управляемый макетом, отображается в первый раз (и всякий раз, когда его размер изменяется), он запрашивает его макет, который, в свою очередь, выполняет множество внутренних вычислений для элементов, которыми он управляет, включая рассмотрение их подсказок по размеру, ограничений и политик.
Обратите внимание, что под «элементами» я подразумеваю виджеты, разделители и даже внутренние макеты (см. QLayoutItem).
QGridLayout довольно сложен, и его поведение не всегда интуитивно понятно, особенно при работе со сложными виджетами и макетами.
Элементы в макете сетки размещаются в «ячейках», и при вычислении геометрии для этих ячеек (и элементов, которые они занимают) механизм макета делает примерно следующее:
heightForWidth()
);Хотя это обычно работает, у него есть важный недостаток: если выбранный диапазон не включает другие элементы в тех же строках или столбцах, результирующие значения ширины и высоты могут быть «аннулированы» (это означает, что их возможный размер является минимальным), особенно всякий раз, когда какой-либо виджет имеет политику размера Expanding
, как в случае с PlotWidget, который наследуется от QAbstractScrollArea (поскольку он основан на QGraphicsView), который по умолчанию всегда пытается расширить свое содержимое всякий раз, когда остается место от других виджетов, которые не расширяются себя.
Note that things are actually even more complex, as other widgets that by default have Expanding
size policies might not give the same result (ie. QScrollArea). This answer is already quite extended, and I won't go too much into the darkness of Qt size management... The problem is mainly caused by a wrong usage of spans: if you want to know more, I suggest you to take your time to patiently study the Qt sources.
Возьмем этот базовый пример:
from PyQt5.QtWidgets import *
class Test(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('''
QLabel {
border: 1px solid red;
}
''')
layout = QGridLayout(self)
w1 = QLabel('Row span=2')
w1.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
w2 = QLabel('Row span=1')
w2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
layout.addWidget(w1, 0, 0, 2, 1)
layout.addWidget(w2, 2, 0, 1, 1)
app = QApplication([])
example = Test()
example.show()
sys.exit(app.exec_())
Что будет концептуально похоже на ваш случай:
Такое поведение может показаться неправильным и, конечно, не идеальным, но суть остается неизменной: как упоминалось выше, назначение промежутков не в том, чтобы напрямую предлагать размер элемента; использование чего-либо неправильным образом часто приводит к неожиданному поведению.
Это одна из причин, по которой я обычно не использую (и часто не одобряю) QGridLayout для сложных пользовательских интерфейсов: в большинстве случаев, когда я выбираю QGridLayout, я заканчиваю тем, что разбираю его и вместо этого использую вложенные макеты.
Если вы хотите, чтобы ваши виджеты имели пропорциональные размеры, у вас есть два варианта. Либо правильно используйте QGridLayout (со строками/столбцами на основе элементов и правильными коэффициентами растяжения), либо используйте вложенные макеты.
В вашем случае сохранение макета сетки требует от вас:
setRowStretch()
для обеих строк со значением 5
для первой и 1
для второй; vlayout_signals.addStretch()
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1)
grid_layout.addWidget(self.p2, 1, 1)
grid_layout.setRowStretch(0, 5)
grid_layout.setRowStretch(1, 1)
Вместо этого с вложенным макетом:
stretch
; main_layout = QHBoxLayout(self)
vlayout_signals.addStretch()
main_layout.addLayout(vlayout_signals)
plot_layout = QVBoxLayout()
main_layout.addLayout(plot_layout)
plot_layout.addWidget(self.p1, stretch=5)
plot_layout.addWidget(self.p2, stretch=1)
Note: all QLayouts have an optional QWidget argument that automatically installs the layout on the given widget, thus avoiding calling layout.setLayout(widget)
.
Я настоятельно рекомендую вам это последнее решение, так как оно более совместимо с виджетами, которые, вероятно, не будут иметь отношения к размеру: в тот момент, когда вам нужно добавить больше графиков, вам не нужно заботиться о диапазоне строк панели.
Интересно, что единственный случай, когда макет сетки действительно имел бы смысл, — это пара слайдер/ярлык: вместо добавления двух несвязанных горизонтальных макетов вы должны были использовать для них сетку. Вам «повезло», потому что эти метки относительно маленькие и одинаковые по ширине, но реальность такова, что они не выровнены должным образом с соответствующими ползунками. Попробуйте добавить следующее:
self.setStyleSheet('QSlider, QLabel {border: 1px solid red;}')
И вот результат:
Если бы эти метки имели очень разную длину, их смещение было бы очень запутанным.
Хотя QGridLayout, безусловно, полезен, важно учитывать, что его редко можно использовать и полагаться на него как на «основной макет», который напрямую управляет элементами, которые сильно различаются по своему «визуальному» назначению; его ячейки строк/столбцов являются просто абстрактными ссылками на позиции элементов, а не их результирующие размеры, а диапазоны всегда должны использоваться только в соответствии с другим содержимым в соответствующих строках и столбцах.
Сложные пользовательские интерфейсы почти всегда требуют вложенных менеджеров компоновки, которые группируют свое содержимое на основе иерархии их использования.
Эмпирическое правило заключается в том, что макет должен учитываться для элементов, сгруппированных в одной иерархии. Вы, безусловно, можете использовать QGridLayout в качестве основного макета, но только до тех пор, пока он управляет элементами, которые можно считать родственными элементами. Например, вы можете рассмотреть такой макет сетки:
┌─────────────┬─────────────────────────┐
|┌───────────┐|┌───────────────────────┐|
|| combo ||| ||
||┌────┬────┐||| plot1 ||
||| | |||| ||
||| s1 | s2 |||└───────────────────────┘|
||| | |||┌───────────────────────┐|
||├────┼────┤||| ||
||| l1 | l2 |||| ||
||└─────────┘||| ||
|| btn ||| ||
|| ^ ||| plot2 ||
|| ║ ||| ||
|| stretch ||| ||
|| ║ ||| ||
|| v ||| ||
|└───────────┘|└───────────────────────┘|
├─────────────┴─────────────────────────┤
| something else |
└───────────────────────────────────────┘
Который будет записан следующим образом:
main_layout = QGridLayout()
panel_layout = QVBoxLayout()
main_layout.addLayout(panel_layout)
panel_layout.addWidget(combo)
slider_layout = QGridLayout()
panel_layout.addLayout(slider_layout)
slider_layout.addWidget(s1)
slider_layout.addWidget(s2, 0, 1)
slider_layout.addWidget(l1, 1, 0)
slider_layout.addWidget(l2, 1, 0)
panel_layout.addWidget(btn)
panel_layout.addStretch()
plot_layout = QVBoxLayout()
main_layout.addLayout(plot_layout, 0, 1)
plot_layout.addWidget(plot1)
plot_layout.addWidget(plot2)
main_layout.addWidget(else, 1, 0, 1, 2)
Я предлагаю вам не торопиться, чтобы поэкспериментировать с Qt Designer, чтобы привыкнуть к менеджерам компоновки и различным типам виджетов, политикам или ограничениям.
Обратите внимание, что, хотя ваше решение работает для этой простой ситуации, на самом деле это «ложный обходной путь», основанный на неверных предположениях: строки и столбцы в макетах сетки не должны рассматриваться как объекты, которые напрямую влияют на размеры макета, но рассматриваются ли они на основе их содержание. ОП использует интервалы, полагая, что они повлияют на распределение доступного пространства, но на самом деле все намного сложнее. Если к вышеперечисленному добавить еще один виджет, использующий другую политику размера и динамические подсказки, результат может быть неожиданным.