Issue
I'm trying to overlay two QWidgets and have them both resize automatically when the window size is altered.
The background QWidget shows a grid with co-ordinates, the foreground QWidget contains other widgets (buttons etc). These are both set as children to the same QWidget. If I don't use a layout I can get them to overlap, but then I lose the useful auto resizing of the child widgets when the parent widget is resized.
My overall goal is creating an editor which will let the user design their own widgets for a dashboard. The grid background will be the gridlayout with their widgets in the foreground.
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QWidget, \
QGridLayout
class Grid(QWidget):
def __init__(self, rows, columns, *args, **kwargs):
super().__init__(*args, **kwargs)
self.columns = columns
self.rows = rows
self.layout = QGridLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.setSpacing(0)
for ii in range(self.rows):
for jj in range(self.columns):
label = QLabel('{}, {}'.format(ii,jj))
label.setStyleSheet("border: 1px solid grey; color: grey")
self.layout.addWidget(label, ii, jj, 1, 1)
self.setLayout(self.layout)
class Overlay(QWidget):
def __init__(self, rows, columns, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rows = rows
self.columns = columns
self.layout = QGridLayout()
# self.layout.setContentsMargins(0,0,0,0)
# self.layout.setSpacing(0)
button_1 = QPushButton('yep')
button_2 = QPushButton('nope')
top_left_label = QLabel('tl')
bottom_right_label = QLabel('br')
self.layout.addWidget(button_1, 1, 1, 1, 4)
self.layout.addWidget(button_2, 4, 4, 2, 2)
self.layout.addWidget(top_left_label, 0, 0, 1, 1)
self.layout.addWidget(bottom_right_label, self.rows, self.columns, 1, 1)
self.setLayout(self.layout)
class Parent_Dashboard_Template(QWidget):
def __init__(self, rows, columns, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rows = rows
self.columns = columns
self.setMinimumSize(400, 200)
self.layout = QGridLayout()
self.layout_2 = QGridLayout()
self.grid = Grid(self.rows, self.columns, parent=self)
# self.layout.addWidget(self.grid)
# self.setLayout(self.layout)
# self.grid.showMaximized()
self.overlay = Overlay(self.rows, self.columns, parent=self)
# self.layout_2.addWidget(self.overlay)
# self.setLayout(self.layout_2)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Parent_Dashboard_Template(10, 10)
window.show()
app.exec_()
Solution
QGridLayout has the "hidden" feature of being able to overlap items that occupy the same "cells", so you can add those two widgets in the same row/column:
class Parent_Dashboard_Template(QWidget):
def __init__(self, rows, columns, *args, **kwargs):
# ...
layout = QGridLayout(self)
self.grid = Grid(self.rows, self.columns)
self.overlay = Overlay(self.rows, self.columns)
layout.addWidget(self.grid, 0, 0)
layout.addWidget(self.overlay, 0, 0)
Since those overlayed widgets need to be correctly aligned to the grid, a better possibility is to create a grid layout for the main widget and add everything there, then use the above "trick" to add the overlayed widgets. Consider that, since Qt buttons have a minimum size hint and a fixed vertical policy, you should also explicitly override the size hint based on the smallest size possible, and set a Preferred
vertical size policy.
class Parent_Dashboard_Template(QWidget):
def __init__(self, rows, columns, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rows = rows
self.columns = columns
self.setMinimumSize(400, 200)
layout = QGridLayout(self)
for ii in range(self.rows):
for jj in range(self.columns):
label = QLabel('{}, {}'.format(ii,jj))
label.setStyleSheet("border: 1px solid grey; color: grey")
layout.addWidget(label, ii, jj)
button_1 = QPushButton('yep')
button_2 = QPushButton('nope')
# just a reference based on the last created label
minSize = label.sizeHint()
button_1.sizeHint = lambda: minSize
button_2.sizeHint = lambda: minSize
button_1.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred)
button_2.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred)
top_left_label = QLabel('tl')
bottom_right_label = QLabel('br')
layout.addWidget(button_1, 1, 1, 1, 4)
layout.addWidget(button_2, 4, 4, 2, 2)
layout.addWidget(top_left_label, 0, 0)
layout.addWidget(
bottom_right_label, self.rows - 1, self.columns - 1)
Finally, considering that the grid will probably need to be in a separate class anyway, you can override the resize event of the main widget, and then set the geometries of the overlayed widgets based on the cell rectangle of their spanning, mapped to the top level widget.
class Parent_Dashboard_Template(QWidget):
def __init__(self, rows, columns, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rows = rows
self.columns = columns
self.setMinimumSize(400, 200)
layout = QVBoxLayout(self)
self.grid = Grid(self.rows, self.columns)
layout.addWidget(self.grid)
self.button_1 = QPushButton('yep', self)
self.button_2 = QPushButton('nope', self)
self.top_left_label = QLabel('tl', self)
self.bottom_right_label = QLabel('br', self)
self.widgetCells = {
self.button_1: (1, 1, 1, 4),
self.button_2: (4, 4, 2, 2),
self.top_left_label: (0, 0, 1, 1),
self.bottom_right_label: (
self.rows - 1, self.columns - 1, 1, 1),
}
def resizeEvent(self, event):
super().resizeEvent(event)
layout = self.grid.layout
# required for first show
layout.activate()
for w, (row, column, rSpan, cSpan) in self.widgetCells.items():
geo = layout.cellRect(row, column)
geo |= layout.cellRect(
row + rSpan - 1, column + cSpan - 1)
geo.moveTopLeft(self.grid.mapTo(self, geo.topLeft()))
w.setGeometry(geo)
Note that:
layout()
is an existing dynamic property of all QWidget classes, and you should not overwrite it;- you don't need to explicitly add the row/column span if they are just
1
, as that's the default value; - you can automatically "install" a QLayout on a widget by specifying it in the layout constructor (as I did with
QGridLayout(self)
);
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.