How to add a fixed header to a QScrollArea? - python

I currently have a QScrollArea defined by:
self.results_grid_scrollarea = QScrollArea()
self.results_grid_widget = QWidget()
self.results_grid_layout = QGridLayout()
self.results_grid_layout.setSizeConstraint(QLayout.SetMinAndMaxSize)
self.results_grid_widget.setLayout(self.results_grid_layout)
self.results_grid_scrollarea.setWidgetResizable(True)
self.results_grid_scrollarea.setWidget(self.results_grid_widget)
self.results_grid_scrollarea.setViewportMargins(0,20,0,0)
which sits quite happily nested within other layouts/widgets, resizes as expected, etc.
To provide headings for the grid columns, I'm using another QGridLayout positioned directly above the scroll area - this works... but looks a little odd, even when styled appropriately, especially when the on-demand (vertical) scrollbar appears or disappears as needed and the headers no longer line up correctly with the grid columns. It's an aesthetic thing I know... but I'm kinda picky ;)
Other widgets are added/removed to the self.results_grid_layout programatically elsewhere. The last line above I've just recently added as I thought it would be easy to use the created margin area, the docs for setViewportMargins state:
Sets margins around the scrolling area. This is useful for applications such as spreadsheets with "locked" rows and columns. The marginal space is is left blank; put widgets in the unused area.
But I cannot for the life of me work out how to actually achieve this, and either my GoogleFu has deserted me today, or there's little information/examples out there on how to actually achieve this.
My head is telling me I can assign just one widget, controlled by a layout (containing any number of other widgets) to the scrollarea - as I have done. If I add say a QHeaderview for example to row 0 of the gridlayout, it will just appear below the viewport's margin and scroll with the rest of the layout? Or am I missing something and just can't see the wood for the trees?
I'm just learning Python/Qt, so any help, pointers and/or examples (preferably with Python but not essential) would be appreciated!
Edit: Having followed the advice given so far (I think), I came up with the following little test program to try things out:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setMinimumSize(640, 480)
self.container_widget = QWidget()
self.container_layout = QVBoxLayout()
self.container_widget.setLayout(self.container_layout)
self.setCentralWidget(self.container_widget)
self.info_label = QLabel(
"Here you can see the problem.... I hope!\n"
"Once the window is resized everything behaves itself.")
self.info_label.setWordWrap(True)
self.headings_widget = QWidget()
self.headings_layout = QGridLayout()
self.headings_widget.setLayout(self.headings_layout)
self.headings_layout.setContentsMargins(1,1,0,0)
self.heading_label1 = QLabel("Column 1")
self.heading_label1.setContentsMargins(16,0,0,0)
self.heading_label2 = QLabel("Col 2")
self.heading_label2.setAlignment(Qt.AlignCenter)
self.heading_label2.setMaximumWidth(65)
self.heading_label3 = QLabel("Column 3")
self.heading_label3.setContentsMargins(8,0,0,0)
self.headings_layout.addWidget(self.heading_label1,0,0)
self.headings_layout.addWidget(self.heading_label2,0,1)
self.headings_layout.addWidget(self.heading_label3,0,2)
self.headings_widget.setStyleSheet(
"background: green; border-bottom: 1px solid black;" )
self.grid_scrollarea = QScrollArea()
self.grid_widget = QWidget()
self.grid_layout = QGridLayout()
self.grid_layout.setSizeConstraint(QLayout.SetMinAndMaxSize)
self.grid_widget.setLayout(self.grid_layout)
self.grid_scrollarea.setWidgetResizable(True)
self.grid_scrollarea.setWidget(self.grid_widget)
self.grid_scrollarea.setViewportMargins(0,30,0,0)
self.headings_widget.setParent(self.grid_scrollarea)
### Add some linedits to the scrollarea just to test
rows_to_add = 10
## Setting the above to a value greater than will fit in the initial
## window will cause the lineedits added below to display correctly,
## however - using the 10 above, the lineedits do not expand to fill
## the scrollarea's width until you resize the window horizontally.
## What's the best way to fix this odd initial behaviour?
for i in range(rows_to_add):
col1 = QLineEdit()
col2 = QLineEdit()
col2.setMaximumWidth(65)
col3 = QLineEdit()
row = self.grid_layout.rowCount()
self.grid_layout.addWidget(col1,row,0)
self.grid_layout.addWidget(col2,row,1)
self.grid_layout.addWidget(col3,row,2)
### Define Results group to hold the above sections
self.test_group = QGroupBox("Results")
self.test_layout = QVBoxLayout()
self.test_group.setLayout(self.test_layout)
self.test_layout.addWidget(self.info_label)
self.test_layout.addWidget(self.grid_scrollarea)
### Add everything to the main layout
self.container_layout.addWidget(self.test_group)
def resizeEvent(self, event):
scrollarea_vpsize = self.grid_scrollarea.viewport().size()
scrollarea_visible_size = self.grid_scrollarea.rect()
desired_width = scrollarea_vpsize.width()
desired_height = scrollarea_visible_size.height()
desired_height = desired_height - scrollarea_vpsize.height()
new_geom = QRect(0,0,desired_width+1,desired_height-1)
self.headings_widget.setGeometry(new_geom)
def main():
app = QApplication(sys.argv)
form = MainWindow()
form.show()
app.exec_()
if __name__ == '__main__':
main()
Is something along these lines the method to which you were pointing? Everything works as expected as is exactly what I was after, except for some odd initial behaviour before the window is resized by the user, once it is resized everything lines up and is fine.
I'm probably over-thinking again or at least overlooking something... any thoughts?

I had a similar problem and solved it a little differently. Instead of using one QScrollArea I use two and forward a movement of the lower scroll area to the top one. What the code below does is
It creates two QScrollArea widgets in a QVBoxLayout.
It disables the visibility of the scroll bars of the top QScrollArea and assigns it a fixed height.
Using the valueChanged signal of the horizontal scroll bar of the lower QScrollArea it is possible to "forward" the horizontal scroll bar value from the lower QScrollArea to the top one resulting a fixed header at the top of the window.
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
widget = QWidget()
self.setCentralWidget(widget)
vLayout = QVBoxLayout()
widget.setLayout(vLayout)
# TOP
scrollAreaTop = QScrollArea()
scrollAreaTop.setWidgetResizable(True)
scrollAreaTop.setFixedHeight(30)
scrollAreaTop.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scrollAreaTop.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scrollAreaTop.setWidget(QLabel(" ".join([str(i) for i in range(100)])))
# BOTTOM
scrollAreaBottom = QScrollArea()
scrollAreaBottom.setWidgetResizable(True)
scrollAreaBottom.setWidget(QLabel("\n".join([" ".join([str(i) for i in range(100)]) for _ in range(10)])))
scrollAreaBottom.horizontalScrollBar().valueChanged.connect(lambda value: scrollAreaTop.horizontalScrollBar().setValue(value))
vLayout.addWidget(scrollAreaTop)
vLayout.addWidget(scrollAreaBottom)

You may be over-thinking things slightly.
All you need to do is use the geometry of the scrollarea's viewport and the current margins to calculate the geometry of any widgets you want to place in the margins.
The geometry of these widgets would also need to be updated in the resizeEvent of the scrollarea.
If you look at the source code for QTableView, I think you'll find it uses this method to manage its header-views (or something very similar).
EDIT
To deal with the minor resizing problems in your test case, I would advise you to read the Coordinates section in the docs for QRect (in particular, the third paragraph onwards).
I was able to get more accurate resizing by rewriting your test case like this:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setMinimumSize(640, 480)
self.container_widget = QWidget()
self.container_layout = QVBoxLayout()
self.container_widget.setLayout(self.container_layout)
self.setCentralWidget(self.container_widget)
self.grid_scrollarea = ScrollArea(self)
self.test_group = QGroupBox("Results")
self.test_layout = QVBoxLayout()
self.test_group.setLayout(self.test_layout)
self.test_layout.addWidget(self.grid_scrollarea)
self.container_layout.addWidget(self.test_group)
class ScrollArea(QScrollArea):
def __init__(self, parent=None):
QScrollArea.__init__(self, parent)
self.grid_widget = QWidget()
self.grid_layout = QGridLayout()
self.grid_widget.setLayout(self.grid_layout)
self.setWidgetResizable(True)
self.setWidget(self.grid_widget)
# save the margin values
self.margins = QMargins(0, 30, 0, 0)
self.setViewportMargins(self.margins)
self.headings_widget = QWidget(self)
self.headings_layout = QGridLayout()
self.headings_widget.setLayout(self.headings_layout)
self.headings_layout.setContentsMargins(1,1,0,0)
self.heading_label1 = QLabel("Column 1")
self.heading_label1.setContentsMargins(16,0,0,0)
self.heading_label2 = QLabel("Col 2")
self.heading_label2.setAlignment(Qt.AlignCenter)
self.heading_label2.setMaximumWidth(65)
self.heading_label3 = QLabel("Column 3")
self.heading_label3.setContentsMargins(8,0,0,0)
self.headings_layout.addWidget(self.heading_label1,0,0)
self.headings_layout.addWidget(self.heading_label2,0,1)
self.headings_layout.addWidget(self.heading_label3,0,2)
self.headings_widget.setStyleSheet(
"background: green; border-bottom: 1px solid black;" )
rows_to_add = 10
for i in range(rows_to_add):
col1 = QLineEdit()
col2 = QLineEdit()
col2.setMaximumWidth(65)
col3 = QLineEdit()
row = self.grid_layout.rowCount()
self.grid_layout.addWidget(col1,row,0)
self.grid_layout.addWidget(col2,row,1)
self.grid_layout.addWidget(col3,row,2)
def resizeEvent(self, event):
rect = self.viewport().geometry()
self.headings_widget.setGeometry(
rect.x(), rect.y() - self.margins.top(),
rect.width() - 1, self.margins.top())
QScrollArea.resizeEvent(self, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
form = MainWindow()
form.show()
sys.exit(app.exec_())

Related

Problem with GridLayout widget stretching/sizing

I'm trying to make a simple image editor GUI using the GridLayout. However, I am coming across a problem where the ratios between the image and the side panels are not what I want them to be. Currently, my code is:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Window(QMainWindow):
def __init__(self, parent = None):
super().__init__(parent)
self.setWindowTitle("PyEditor")
self.setGeometry(100, 100, 500, 300)
self.centralWidget = QLabel()
self.setCentralWidget(self.centralWidget)
self.gridLayout = QGridLayout(self.centralWidget)
self.createImagePanel()
self.createDrawPanel()
self.createLayerPanel()
def createImagePanel(self):
imageLabel = QLabel(self)
pixmap = QPixmap('amongus.png')
imageLabel.setPixmap(pixmap)
self.gridLayout.addWidget(imageLabel, 0, 0, 3, 4)
def createDrawPanel(self):
drawPanel = QLabel(self)
drawLayout = QVBoxLayout()
drawPanel.setLayout(drawLayout)
tabs = QTabWidget()
filterTab = QWidget()
drawTab = QWidget()
tabs.addTab(filterTab, "Filter")
tabs.addTab(drawTab, "Draw")
drawLayout.addWidget(tabs)
self.gridLayout.addWidget(drawPanel, 0, 4, 1, 1)
def createLayerPanel(self):
layerPanel = QLabel(self)
layerLayout = QVBoxLayout()
layerPanel.setLayout(layerLayout)
tab = QTabWidget()
layerTab = QWidget()
tab.addTab(layerTab, "Layers")
layerLayout.addWidget(tab)
self.gridLayout.addWidget(layerPanel, 1, 4, 1, 1)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
This gives me the following window:
When I resize the window, only the filter/draw and layer panels are stretching, and not the image panel. I want to image panel to stretch as well and take up the majority of the window instead.
While theoretically every Qt widget could be used as a container, some widgets should not be used for such a purpose, as their size hints, size policies and resizing have different and specific behavior depending on their nature.
QLabel is intended as a display widget, not as a container. Everything related to its size is based on the content (text, image or animation), so the possible layout set for it will have no result in size related matters and will also create some inconsistencies in displaying the widgets added to that layout.
If a basic container is required, then basic QWidget is the most logical choice.
Then, if stretching is also a requirement, that should be applied using the widget or layout stretch factors. For QGridLayout, this is achieved by using setColumnStretch() or setRowStretch().
Trying to use the row or column span is not correct for this purpose, as the spanning only indicates how many grid "cells" a certain layout item will use, which only makes sense whenever there are widgets that should occupy more than one "cell", exactly like the spanning of a table.
So, the following changes are required to achieve the wanted behavior:
change all QLabel to QWidget (except for the label that shows the image, obviously);
use the proper row/column spans; the imageLabel should be added with only one column span (unless otherwise required):
self.gridLayout.addWidget(imageLabel, 0, 0, 3, 1, alignment=Qt.AlignCenter)
set a column stretch of (at least) 1 for the first column:
self.gridLayout.setColumnStretch(0, 1)
if you want the image to be center aligned in the available space, set the alignment on the widget (not when adding it to the layout):
imageLabel = QLabel(self, alignment=Qt.AlignCenter)
Note that all the above will not scale the image whenever the available size is greater than that of the image. While you can set the scaledContents to True, the result will be that the image will be stretched to fill the whole available space, and unfortunately QLabel doesn't provide the ability to keep the aspect ratio. If you need that, then it's usually easier to subclass QWidget and provide proper implementation for size hint and paint event.
class ImageViewer(QWidget):
_pixmap = None
def __init__(self, pixmap=None, parent=None):
super().__init__(parent)
self.setPixmap(pixmap)
def setPixmap(self, pixmap):
if self._pixmap != pixmap:
self._pixmap = pixmap
self.updateGeometry()
def sizeHint(self):
if self._pixmap and not self._pixmap.isNull():
return self._pixmap.size()
return super().sizeHint()
def paintEvent(self, event):
if self._pixmap and not self._pixmap.isNull():
qp = QPainter(self)
scaled = self._pixmap.scaled(self.width(), self.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation)
rect = scaled.rect()
rect.moveCenter(self.rect().center())
qp.drawPixmap(rect, scaled)

Using QScrollArea collapses children widgets

I am trying to create a dynamic GUI with multiple Groupbox objects in a QVBoxLayout. As there are a lot of them, I will be needing a scroll area to make them available to the end user.
So I tried to change to top widget of this tab from a QWidget to a QScrollArea.
Before the change:
This is the kind of result I want but with a scroll bar because the window is too high.
After the change to QScrollArea:
My GroupBoxs are now "collapsed" and there is not scrollbar. I tried setting their size but it is not adequate because they are not fixed. I searched the documentation and tried to use WidgetResizable or I tried to set a fixed height or the sizehint but nothing worked as I wanted.
After creating the the Groupbox, the sizeHint for my QScrollArea is already very low (around 150px of height) so I think I'm missing a parameter.
It would be complicated to provide code as it is intricate. If necessary I could recreate the problem in a simpler way.
How to reproduce:
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import *
import sys
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
v_layout = QVBoxLayout()
scroll_area = QScrollArea()
self.layout().addWidget(scroll_area)
scroll_area.setLayout(v_layout)
# v_layout.setSizeConstraint(QLayout.SetMinimumSize)
for i in range(50):
box = QGroupBox()
grid = QGridLayout()
box.setLayout(grid)
grid.addWidget(QLabel("totototo"), 0, 0)
grid.addWidget(QLineEdit(), 1, 0)
grid.addWidget(QPushButton(), 2, 0)
v_layout.addWidget(box)
self.show()
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Uncommenting # v_layout.setSizeConstraint(QLayout.SetMinimumSize) allows the content of the group boxes to deploy and fixes the first part of the issue. But there is still not scroll bar.
You have 2 errors:
A widget should not be added to the layout of a QMainWindow, but the setCentralWidget method should be used.
You should not add the layout to the QScrollArea but use a widget as a container for the other widgets, also if you use layouts then you have to activate the widgetResizable property.
Considering the above, the solution is:
def initUI(self):
scroll_area = QScrollArea(widgetResizable=True)
self.setCentralWidget(scroll_area)
container = QWidget()
scroll_area.setWidget(container)
v_layout = QVBoxLayout(container)
for i in range(50):
box = QGroupBox()
grid = QGridLayout()
box.setLayout(grid)
grid.addWidget(QLabel("totototo"), 0, 0)
grid.addWidget(QLineEdit(), 1, 0)
grid.addWidget(QPushButton(), 2, 0)
v_layout.addWidget(box)
self.show()

How to align widgets in PyQt5

I am trying to use PyQt5 for one of my GUI application. I could be able to add widgets as I want, but couldn't align them properly. I want to align my widgets as below:
But, My code is working something like this,
Following is my code, can anyone help me please?
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QDesktopWidget, QLabel
class GroupBox(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setGeometry(QtCore.QRect(20, 20, 900, 700))
self.setWindowTitle("InvoiceMee - Split Documents")
layout = QtWidgets.QGridLayout(self)
groupbox = QtWidgets.QGroupBox("Files to Convert", checkable=False)
layout.addWidget(groupbox)
hbox = QtWidgets.QHBoxLayout()
groupbox.setLayout(hbox)
label = QLabel()
pixmap = QPixmap('images.jpg')
label.setPixmap(pixmap)
label.resize(pixmap.width(), pixmap.height())
pathBox = QtWidgets.QLineEdit(self)
pathBox.setPlaceholderText("Enter the Path Here")
pathBox.setGeometry(QRect(160, 150, 201, 20))
selectFileBtn = QtWidgets.QPushButton("Select")
convertButton = QtWidgets.QPushButton("Convert")
good_radiobutton = QtWidgets.QRadioButton("Invoices")
naive_radiobutton = QtWidgets.QRadioButton("Credit Notes")
hbox.addWidget(pathBox, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(selectFileBtn, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(convertButton, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(good_radiobutton, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(naive_radiobutton, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(label,alignment=QtCore.Qt.AlignCenter)
hbox.addStretch()
self.center()
def center(self):
# geometry of the main window
qr = self.frameGeometry()
# center point of screen
cp = QDesktopWidget().availableGeometry().center()
# move rectangle's center point to screen's center point
qr.moveCenter(cp)
# top left of rectangle becomes top left of window centering it
self.move(qr.topLeft())
Use QGridLayout instead of QHBoxLayout. Grid Layout gives you the option to layout your widgets in a grid like struture. Here's the official documentation for QGridLayout.
You can change your layout like this:
grid = QtWidgets.QGridLayout()
groupbox.setLayout(grid)
grid.addWidget(label,0,0,1,0,QtCore.Qt.AlignCenter)
grid.addWidget(pathBox,1,0,QtCore.Qt.AlignRight)
grid.addWidget(selectFileBtn,1,1,QtCore.Qt.AlignLeft)
grid.addWidget(good_radiobutton,2,0,QtCore.Qt.AlignRight)
grid.addWidget(naive_radiobutton,2,1,QtCore.Qt.AlignLeft)
grid.addWidget(convertButton,3,0,1,0,QtCore.Qt.AlignCenter)
Add a vertical spacer item if you want to remove space between your widgets:
verticalSpacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
grid.addItem(verticalSpacer, 6, 0, QtCore.Qt.AlignTop)
You are using a QHBoxLayout (which stands for horizontal box layout). This means that all widgets that you added will be always displayed side by side, horizontally, according to the order of insertion.
You should use a layout that allows vertical orientation, instead.
You're using more than a widget per row, so you could use a QGridLayout, but, since some of those widgets have different horizontal sizes, the result might not be what you showed us.
The solution is to use nested layouts, with a main grid layout with stretch sets for first/third row and column and a "central" layout added to the second row/column of the grid. Then, whenever you need more than one widget in a row, add a nested QHBoxLayout.
class GroupBox(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setGeometry(QtCore.QRect(20, 20, 900, 700))
self.setWindowTitle("InvoiceMee - Split Documents")
layout = QtWidgets.QGridLayout(self)
groupbox = QtWidgets.QGroupBox("Files to Convert", checkable=False)
layout.addWidget(groupbox)
# the "main" layout, used to ensure that the actual layout containing
# all widgets stays in the center
groupLayout = QtWidgets.QGridLayout()
groupbox.setLayout(groupLayout)
groupLayout.setColumnStretch(0, 1)
groupLayout.setColumnStretch(2, 1)
groupLayout.setRowStretch(0, 1)
groupLayout.setRowStretch(2, 1)
# this is the actual layout used to add widgets
centerLayout = QtWidgets.QVBoxLayout()
groupLayout.addLayout(centerLayout, 1, 1)
label = QLabel()
pixmap = QPixmap('images.jpg')
label.setPixmap(pixmap)
# this won't work
# label.resize(pixmap.width(), pixmap.height())
pathBox = QtWidgets.QLineEdit(self)
pathBox.setPlaceholderText("Enter the Path Here")
# this won't work either, the layout will try to move and resize it anyway
# pathBox.setGeometry(QRect(160, 150, 201, 20))
# use minimum width instead
pathBox.setMinimumWidth(200)
selectFileBtn = QtWidgets.QPushButton("Select")
convertButton = QtWidgets.QPushButton("Convert")
good_radiobutton = QtWidgets.QRadioButton("Invoices")
naive_radiobutton = QtWidgets.QRadioButton("Credit Notes")
centerLayout.addWidget(label, alignment=QtCore.Qt.AlignCenter)
# the second row has more than one widget, use a nested horizontal layout
inputLayout = QtWidgets.QHBoxLayout()
centerLayout.addLayout(inputLayout)
inputLayout.addWidget(pathBox)
inputLayout.addWidget(selectFileBtn)
# the same for the radio buttons
radioLayout = QtWidgets.QHBoxLayout()
centerLayout.addLayout(radioLayout)
# use horizontal alignment to keep buttons closer, otherwise the layout
# will try to expand them as much as possible (depending on the other
# widgets in the centerLayout)
radioLayout.addWidget(good_radiobutton, alignment=QtCore.Qt.AlignRight)
radioLayout.addWidget(naive_radiobutton, alignment=QtCore.Qt.AlignLeft)
# use center alignment so that the button doesn't expand
centerLayout.addWidget(convertButton, alignment=QtCore.Qt.AlignCenter)
I'd suggest you to carefully study how layout work and behave, make some experiments and also use Qt Designer to easily see how nested layout can work.
Also, consider that in some cases it might be necessary to set a specific size policy to avoid widgets expanding too much, and use a QWidget "container" can make things easier.
For example, instead of using the horizontal alignment when you add the radio buttons, you can use a QWidget container:
# ...
radioContainer = QtWidgets.QWidget()
centerLayout.addWidget(radioContainer, alignment=QtCore.Qt.AlignCenter)
radioContainer.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Preferred)
radioLayout = QtWidgets.QHBoxLayout(radioContainer)
radioLayout.addWidget(good_radiobutton)
radioLayout.addWidget(naive_radiobutton)
# ...

PyQt - Toggle between two widgets withtout resizing qsplitter

I try to toggle a splitter container between two widgets keeping the actual size of the splitter.
For this I use QSplitter.sizes() to read the actual size and QSplitter.setSizes() after I toggle my widgets.
The problem is that I have a QToolButton which I resize with setFixedSize() in a resizeEvent(), and because of this when I set the new size, it often doesn't work.
I write a little script to reproduce this :
In the left part of the splitter, I have a button to toggle the right part of the splitter between two classes (which are QWidgets).
A little precision : I want to keep my QToolbutton in a 1:1 aspect ratio.
Here a demo :
https://webmshare.com/play/5Bmvn
So here the script :
from PyQt4 import QtGui, QtCore
minSize = 50
maxSize = 350
class mainWindow(QtGui.QWidget):
def __init__(self):
super(mainWindow, self).__init__()
self.layout = QtGui.QVBoxLayout(self)
self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
self.splitter.setHandleWidth(20)
self.layout.addWidget(self.splitter)
wgt_left = QtGui.QWidget()
lyt_left = QtGui.QVBoxLayout(wgt_left)
self.btn_toggleSplitter = QtGui.QPushButton('Toggle Button')
self.btn_toggleSplitter.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
self.btn_toggleSplitter.setCheckable(True)
lyt_left.addWidget(self.btn_toggleSplitter)
self.splitter.addWidget(wgt_left)
self.first = panel('1')
self.second = panel('2')
self.splitter.addWidget(self.first)
self.width = self.first.size()
self.btn_toggleSplitter.clicked.connect(self.ToggleParent)
def ToggleParent(self):
self.sizes = self.splitter.sizes()
if self.btn_toggleSplitter.isChecked() == True:
self.first.setParent(None)
self.splitter.addWidget(self.second)
else :
self.second.setParent(None)
self.splitter.addWidget(self.first)
self.splitter.setSizes(self.sizes)
class panel(QtGui.QWidget):
def __init__(self, text):
super(panel, self).__init__()
lyt_main = QtGui.QVBoxLayout(self)
lyt_icon = QtGui.QHBoxLayout()
self.tbtn_icon = QtGui.QToolButton()
self.tbtn_icon.setText(text)
self.tbtn_icon.setMinimumSize(QtCore.QSize(minSize,minSize))
self.tbtn_icon.setMaximumSize(QtCore.QSize(maxSize,maxSize))
lyt_icon.addWidget(self.tbtn_icon)
lyt_horizontal = QtGui.QHBoxLayout()
lyt_horizontal.addWidget(QtGui.QPushButton('3'))
lyt_horizontal.addWidget(QtGui.QPushButton('4'))
lyt_main.addWidget(QtGui.QLabel('Below me is the QToolButton'))
lyt_main.addLayout(lyt_icon)
lyt_main.addLayout(lyt_horizontal)
lyt_main.addWidget(QtGui.QPlainTextEdit())
def resizeEvent(self, event):
w = panel.size(self).width()
h = panel.size(self).height()
size = min(h, w)-22
if size >= maxSize:
size = maxSize
elif size <= minSize:
size = minSize
self.tbtn_icon.setFixedSize(size, size)
app = QtGui.QApplication([])
window = mainWindow()
window.resize(600,300)
window.show()
app.exec_()
Thanks
You are looking for QtGui.QStackedWidget. Adding the widgets to this on the right side of your splitter will change the code around self.first and self.second's construction to this:
self.stack_right = QtGui.QStackedWidget()
self.splitter.addWidget(self.stack_right)
self.first = panel('1')
self.second = panel('2')
self.stack_right.addWidet(self.first)
self.stack_right.addWidget(self.second)
Then your ToggleParent method:
def ToggleParent(self):
if self.btn_toggleSplitter.isChecked() == True:
self.stack_right.setCurrentWidget(self.second)
else:
self.stack_right.setCurrentWidget(self.first)
This will avoid the awkwardness of caching and manually resizing your widgets.
Addendum:
The tool button scaling is really a separate question, but here's a tip:
Have a look at the heightForWidth layout setting for lyt_left. This will help you keep a 1:1 ratio for the QToolButton. You currently have a size policy of Preferred/Expanding, which doesn't make sense if you need a 1:1 aspect ratio. I highly recommend this over manually resizing the tool button while handling an event. Generally, calling setFixedSize more than once on a widget should be considered a last resort. Let the layouts do the work.
Addendum to addendum: doing a little poking (it's been awhile), you may need to inherit from QToolButton and reimplement the hasHeightForWidth() and heightForWidth() methods. There are a plethora of questions addressing the subject here. Just search for heightForWidth.

Set widget width based on its height

I am trying to find a way to have one of my widgets maintain a width based on its height. I have other widgets working fine reimplementing the heightForWidth method. That was easy because that method is standard. I know there is no included widthForHeight method so I have tried many options the internet has suggested but have not gotten anything to work all the way. What I currently have almost gets me there.
I first reimplement my widget's sizeHint to get its width to be a ratio of its parent's (QHBoxLayout) height.
The sizeHint makes MyCustomLabel show with the right size at first show but did not update during times the user resized the window. I don't know if this the best way but to fix that I am reimplementing resizeEvent and calling adjustSize to force the sizeHint recalculation.
With those two reimplemented methods MyCustomLabel shows with the right size. I placed this custom widget in a QHBoxLayout with a few other standard widgets. The problem is the other widgets in the layout don't respect the new size of my MyCustomLabel when the user resizes the window. What I end up with is the other widgets in the layout either overlapping or being placed too far from MyCustomLabel. I kind of get it, I am brute forcing my widget to a size and not letting the layout do the work. However I thought updating the sizeHint would inform the layout of MyCustomLabel's new size and adjust everything accordingly. How do I fix this layout problem or am I going about this widthForHeight problem all the wrong way?
Edit:
I tried #AlexanderVX suggestion of setting the SizePolicy to Minimum and while it does prevent the other widgets from overlapping it also locked MyCustomLabel to a fixed size. I need the widget to expand and shrink with the layout. I also tried Preferred, Expanding, MinimumExpanding policies just to see if they would do anything but with no luck.
from __future__ import division
from PySide import QtCore
from PySide import QtGui
import sys
class MyCustomLabel(QtGui.QLabel):
clicked = QtCore.Signal(int)
dblClicked = QtCore.Signal(int)
def __init__(self, leadSide='height', parent=None):
super(MyCustomLabel, self).__init__()
self.leadSide = leadSide
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
# QtGui.QSizePolicy.Preferred)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
# QtGui.QSizePolicy.Expanding)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding,
# QtGui.QSizePolicy.MinimumExpanding)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
# QtGui.QSizePolicy.Minimum)
# self.setSizePolicy(sizePolicy)
def resizeEvent(self, event):
super(MyCustomLabel, self).resizeEvent(event)
self.adjustSize()
def sizeHint(self):
super(MyCustomLabel, self).sizeHint()
parentSize = self.parent().size().toTuple()
if self.leadSide.lower() == 'height':
new_size = QtCore.QSize(parentSize[1] * (16 / 9), parentSize[1]) * .9
if self.leadSide.lower() == 'width':
new_size = QtCore.QSize(parentSize[0], parentSize[0] / (16 / 9)) * .9
return new_size
class __Test__(QtGui.QWidget):
def __init__(self):
super(__Test__, self).__init__()
self.initUI()
def initUI(self):
customLabel = MyCustomLabel(leadSide='height')
# customLabel.setScaledContents(True)
customLabel.setStyleSheet('background-color: blue;'
'border:2px solid red;')
btn01 = QtGui.QPushButton('button')
btn01.setFixedHeight(80)
textEdit = QtGui.QTextEdit()
textEdit.setFixedSize(150, 150)
layout01 = QtGui.QHBoxLayout()
layout01.setContentsMargins(0,0,0,0)
layout01.setSpacing(0)
layout01.addWidget(customLabel)
layout01.addWidget(btn01)
layout01.addWidget(textEdit)
self.setLayout(layout01)
self.setGeometry(300, 300, 600, 300)
self.setWindowTitle('Testing')
self.show()
def main():
app = QtGui.QApplication(sys.argv)
ex = __Test__()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Categories