Requirements:
QScrollArea containing several widgets.
Each widget should be individually resizable by the user (in either horizontal, or vertical, but not both directions).
User resizing of a widget should not change the size of other widgets. It should increase/decrease the area available in the QScrollArea.
Using a QSplitter doesn't help, because the QSplitter remains of fixed width, and resizing any of its splits causes other splits to shrink.
[1] [2] [3]
Surely it can be done by creating a custom widget, adding a visual bar for indicating the draggable area, and listening to a drag event to resize the widget via code. Is there a simpler solution?
I had the same problem. Came up with a nasty hack:
put a QSplitter inside the QScrollArea
store the old sizes of all QSplitter child widgets
when a QSplitterHandle moves (i.e. on SIGNAL splitterMoved() )
Calculate how much the changed child widget has grown/shrunk
Change the min size of the whole QSplitter by that amount
Update my stored size for the changed child widget only
Set the sizes of the QSplitter child widgets to my stored sizes.
It works for me (for now). But it's kludgy, and there are some yucky magic numbers in it to make it work.
So if anyone comes up with a better solution, that would be great!
Anyway - in case anyone finds it useful, Code (in Python3 & PySide2)
import sys
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QWidget, QScrollArea, QSplitter
from PySide2.QtWidgets import QApplication, QMainWindow, QLabel, QFrame
class ScrollSplitter(QScrollArea):
def __init__(self, orientation, parent=None):
super().__init__(parent)
# Orientation = Qt.Horizontal or Qt.Vertical
self.orientation = orientation
# Keep track of all the sizes of all the QSplitter's child widgets BEFORE the latest resizing,
# so that we can reinstate all of them (except the widget that we wanted to resize)
self.old_sizes = []
self._splitter = QSplitter(orientation, self)
# TODO - remove magic number. This is required to avoid zero size on first viewing.
if orientation == Qt.Horizontal :
self._splitter.setMinimumWidth(500)
else :
self._splitter.setMinimumHeight(500)
# In a default QSplitter, the bottom widget doesn't have a drag handle below it.
# So create an empty widget which will always sit at the bottom of the splitter,
# so that all of the user widgets have a handle below them
#
# I tried playing with the max width/height of this bottom widget - but the results were crummy. So gave up.
bottom_widget = QWidget(self)
self._splitter.addWidget(bottom_widget)
# Use the QSplitter.splitterMoved(pos, index) signal, emitted every time the splitter's handle is moved.
# When this signal is emitted, the splitter has already resized all child widgets to keep its total size constant.
self._splitter.splitterMoved.connect(self.resize_splitter)
# Configure the scroll area.
if orientation == Qt.Horizontal :
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
else :
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setWidgetResizable(True)
self.setWidget(self._splitter)
# Called every time a splitter handle is moved
# We basically undo the QSplitter resizing of all the other children,
# and resize the QSplitter (using setMinimumHeight() or setMinimumWidth() ) instead.
def resize_splitter(self, pos, index):
# NOTE: index refs the child widget AFTER the moved splitter handle.
# pos is position relative to the top of the splitter, not top of the widget.
# TODO - find a better way to initialise the old_sizes list.
# Ideally whenever we add/remove a widget.
if not self.old_sizes :
self.old_sizes = self._splitter.sizes()
# The 'index' arg references the QWidget below the moved splitter handle.
# We want to change the QWidget above the moved splitter handle, so...
index_above = index - 1
# Careful with the current sizes - QSplitter has already mucked about with the sizes of all other child widgets
current_sizes = self._splitter.sizes()
# The only change in size we are interested in is the size of the widget above the splitter
size_change = current_sizes[index_above] - self.old_sizes[index_above]
# We want to keep the old sizes of all other widgets, and just resize the QWidget above the splitter.
# Update our old_list to hold the sizes we want for all child widgets
self.old_sizes[index_above] = current_sizes[index_above]
# Increase/decrease the(minimum) size of the QSplitter object to accommodate the total new, desired size of all of its child widgets (without resizing most of them)
if self.orientation == Qt.Horizontal :
self._splitter.setMinimumWidth(max(self._splitter.minimumWidth() + size_change, 0))
else :
self._splitter.setMinimumHeight(max(self._splitter.minimumHeight() + size_change, 0))
# and set the sizes of all the child widgets back to their old sizes, now that the QSplitter has grown/shrunk to accommodate them without resizing them
self._splitter.setSizes(self.old_sizes)
#print(self.old_sizes)
# Add a widget at the bottom of the user widgets
def addWidget(self, widget):
self._splitter.insertWidget(self._splitter.count()-1, widget)
# Insert a widget at 'index' in the splitter.
# If the widget is already in the splitter, it will be moved.
# If the index is invalid, widget will be appended to the bottom of the (user) widgets
def insertWidget(self, index, widget):
if index >= 0 and index < (self._splitter.count() - 1) :
self._splitter.insertWidget(index, widget)
self.addWidget(widget)
# Replace a the user widget at 'index' with this widget. Returns the replaced widget
def replaceWidget(self, index, widget):
if index >= 0 and index < (self._splitter.count() - 1) :
return self._splitter.replaceWidget(index, widget)
# Return the number of (user) widgets
def count(self):
return self._splitter.count() - 1
# Return the index of a user widget, or -1 if not found.
def indexOf(self, widget):
return self._splitter.indexOf(widget)
# Return the (user) widget as a given index, or None if index out of range.
def widget(self, index):
if index >= 0 and index < (self._splitter.count() - 1) :
return self._splitter.widget(index)
return None
# Save the splitter's state into a ByteArray.
def saveState(self):
return self._splitter.saveState()
# Restore the splitter's state from a ByteArray
def restoreState(self, s):
return self._splitter.restoreState(s)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("ScrollSplitter Test")
self.resize(640, 400)
self.splitter = ScrollSplitter(Qt.Vertical, self)
self.setCentralWidget(self.splitter)
for color in ["Widget 0", "Widget 1", "Widget 2", "Some other Widget"]:
widget = QLabel(color)
widget.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.splitter.addWidget(widget)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Related
A Qt packing layout, such as QVBoxLayout, can pack widgets inside it, such as buttons. In this case, they will be packed vertically as shown in image below:
When we pack too many widgets inside such a layout, and since scrolling is not added by default, the buttons will eventually get squeezed onto each other up to a point that they will overlap, as shown below:
My questions are:
How to tell Qt to not show/pack widgets beyond the available viewing space in the non-scrolling layout?
How to handle the case when the window is resized? I.e. Qt should add/remove widgets accordingly. E.g. if there is extra space available, then perhaps Qt should add some extra widgets that it couldn't add previously.
To be specific: "too many packed widgets" is when the widgets start invading spaces of other widgets, including their inter-widget spacings or margins.
Appendix
Images above are generated by this code below as run in a tile in i3, which is a modified version of this.
from PyQt5 import QtCore, QtWidgets
app = QtWidgets.QApplication([])
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
for i in range(40):
layout.addWidget(QtWidgets.QPushButton(str(i + 1)))
widget.show()
app.exec_()
When too many widgets are packed:
If the window is tiled, you see them overcrowded as in in the image.
If the window is floating, the window will keep growing until it is no longer fully visible in the monitor.
None of these outcomes are acceptable in my case. My goal is to have Qt only pack as much as will be visible, and add/remove/hide/show dynamically as the window gets resized.
Try this code. It does not rely on QVBoxLayout but it basically does the same as this layout. It hides the child widgets which are outside of the area. There are no partially visible widgets.
from PyQt5 import QtWidgets
class Container(QtWidgets.QWidget):
_spacing = 5
def __init__(self, parent=None):
super().__init__(parent)
y = self._spacing
for i in range(40):
button = QtWidgets.QPushButton("Button" + str(i + 1), self)
button.move(self._spacing, y)
y += button.sizeHint().height() + self._spacing
def resizeEvent(self, event):
super().resizeEvent(event)
for child in self.children():
if isinstance(child, QtWidgets.QWidget):
child.resize(self.width() - 2 * self._spacing, child.height())
child.setVisible(child.geometry().bottom() < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
Note that is in fact does not add nor remove widgets dynamically, this would be much more code and it would probably be very depending on your specific use case. Moreover it feels as a premature optimization. Unless you really need it, do not do it.
UPDATE:
I experimented with the code above and proposed some improvements. I especially wanted to make it responsive to changes in child widgets. The problem is that if the child widget changes it size, the parent container must be re-layouted. The code above does not react in any way. To make it responsive, we need to react to LayoutRequest event. Note that in the code below, I have created three types of buttons - one add a line to itself, other increases font size, and yet another decreases font size.
from PyQt5 import QtCore, QtWidgets
def changeFontSize(increment):
font = QtWidgets.QApplication.font()
font.setPointSize(font.pointSize() + increment)
QtWidgets.QApplication.setFont(font)
class Container(QtWidgets.QWidget):
_spacing = 5
_children = [] # maintains the order of creation unlike children()
def __init__(self, parent=None):
super().__init__(parent)
for i in range(100):
child = QtWidgets.QPushButton(self)
child.installEventFilter(self)
# these are just to test various changes in child widget itself to force relayout
r = i % 3
if r == 0:
text = "New line"
onClicked = lambda state, w=child: w.setText(w.text() + "\nclicked")
elif r == 1:
text = "Bigger font"
onClicked = lambda: changeFontSize(1)
elif r == 2:
text = "Smaller font"
onClicked = lambda: changeFontSize(-1)
child.setText(text)
child.clicked.connect(onClicked)
self._children.append(child)
def resizeEvent(self, event):
super().resizeEvent(event)
self._relayout()
def event(self, event):
if event.type() == QtCore.QEvent.LayoutRequest:
self._relayout()
return super().event(event)
def _relayout(self):
y = self._spacing
for child in self._children:
h = child.sizeHint().height()
child.move(self._spacing, y)
child.resize(self.width() - 2 * self._spacing, h)
y += h + self._spacing
child.setVisible(y < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
This code is satisfactory, however it is not perfect. I have observed that when the container is being re-layouted and some of the child widgets will change its visibility state, re-layouting is called again. This is not needed but I have not discovered how to prevent it.
Maybe there is some better way...
I'm coding a simple image viewer and would like for the window to resize based on the image that I open.
The window I'm using is a QMainWindow and has a toolbar. The only widget I have is a QLabel which is set as the central widget. When I open the image I use self.resize(self.label.sizeHint()), but the window size doesn't take into account the size of the title bar and the toolbar, so for example if I open a 400x400 image the window will be of the correct width, but a little bit too short.
What would be the correct way to calculate the correct window size so that it resizes correctly on every platform? (Windows, macOS, Linux)
EDIT: the minimal code is:
import PyQt5.QtWidgets as w
import PyQt5.QtGui as g
import PyQt5.QtCore as c
import sys
class ImageViewerWindow(w.QMainWindow):
def __init__(self):
super().__init__()
self.loadedImagePaths = []
self.imageIndex = 0
self.scrollArea = w.QScrollArea()
self.label = w.QLabel()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidgetResizable(True)
self.label.setAlignment(c.Qt.AlignCenter)
self.label.setMinimumSize(1,1)
# Actions
self.openAction = w.QAction("Open...", self)
self.openAction.setShortcut(g.QKeySequence.Open)
self.openAction.triggered.connect(self.openMenuDialog)
# Toolbar elements
toolbar = w.QToolBar("Top toolbar")
toolbar.setMovable(False)
toolbar.setContextMenuPolicy(c.Qt.PreventContextMenu)
self.addToolBar(toolbar)
# Status bar elements
self.setStatusBar(w.QStatusBar(self))
# Add actions to toolbar and menu
toolbar.addAction(self.openAction)
def showImageAtIndex(self, index):
image = g.QPixmap(self.loadedImagePaths[index])
self.label.setPixmap(image)
self.scrollArea.setWidget(self.label)
self.imageIndex = index
self.angle = 0
self.label.adjustSize()
self.resize(self.label.sizeHint())
def openMenuDialog(self, firstStart = False):
self.loadedImagePaths, _ = w.QFileDialog.getOpenFileNames(parent=self, caption="Select one or more JPEG files to open:", filter="JPEG Image(*.jpg *.jpeg)")
if self.loadedImagePaths:
if firstStart:
self.show()
self.imageIndex = 0
self.showImageAtIndex(self.imageIndex)
elif firstStart:
sys.exit()
a = w.QApplication([])
ivw = ImageViewerWindow()
ivw.openMenuDialog(firstStart = True)
a.exec()
If you try and open an image and then resize the window you will notice that some of the image is covered by the title bar and the status bar.
The main problem is that using setWidgetResizable():
the scroll area will automatically resize the widget in order to avoid scroll bars where they can be avoided
So you have to remove that line, or use setFixedSize() on the label using the image size.
Then, calling adjustSize() on the label is not enough, as you actually need to call adjustSize() against the top level window: this is because calling resize() with the image size won't consider all other widgets in the window (in your case, the toolbar and status bar).
Unfortunately, that won't be enough, as QScrollArea caches the size hint of the widget, and calling again setWidget() with the same widget is useless.
The easiest solution is to use a subclass of QScrollArea and reimplement the sizeHint().
Finally, the alignment only has effect on the label contents, but when the widget is added to a container the alignment has to be set for the widget.
class ScrollAreaAdjust(w.QScrollArea):
def sizeHint(self):
if not self.widget():
return super().sizeHint()
frame = self.frameWidth() * 2
return self.widget().sizeHint() + c.QSize(frame, frame)
class ImageViewerWindow(w.QMainWindow):
def __init__(self):
super().__init__()
self.loadedImagePaths = []
self.imageIndex = 0
self.scrollArea = ScrollAreaAdjust()
self.label = w.QLabel()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidget(self.label)
self.scrollArea.setAlignment(c.Qt.AlignCenter)
# ...
def showImageAtIndex(self, index):
image = g.QPixmap(self.loadedImagePaths[index])
self.label.setPixmap(image)
self.label.setFixedSize(image.size())
self.imageIndex = index
self.angle = 0
self.scrollArea.updateGeometry()
self.adjustSize()
Note that the size hint of a top level window will only be respected until the size doesn't exceed 2/3 of the screen size. This means that if the image will force the window to a slightly bigger size, at least one scroll bar will be shown, even if it's not strictly necessary.
There is no obvious nor universal solution for that, and you need to find your own way. For instance, you can check if the scroll bars are visible after adjusting the size and eventually compare the size of the image and that of the scroll area's viewport, then if one of the image dimensions is just smaller by the size of the opposite scroll bar, force a resizing of the top level window by that scroll bar size.
The Qt desktop app I'm writing contains a QCombobox in the UI (made with Designer). After I select the QCombobox, I can change the selected item by scrolling the mouse wheel, or by pressing the up/down arrows on the keyboard. That all works fine.
When navigating using the keyboard down arrow, for example, when I reach the bottom item in the list, the down arrow no longer changes the selected item. I understand that this is the expected behavior.
But for this particular QComboBox I'd like to be able to keep pressing the down arrow after reaching the final item in the list, and "wrap" back to the first item, so I can continue to cycle through the items. I have studied the documentation for QComboBox at https://doc.qt.io/qt-5/qcombobox.html and for QAbstractItemModel at https://doc.qt.io/qt-5/qabstractitemmodel.html, but I could not discover any way to achieve what I want here.
Ideally I'd prefer a solution that works for keyboard arrow navigation, for mouse scroll wheel navigation, and for any other UI gesture that might try to activate the "next" or "previous" item in the QComboBox.
I didn't try this solution, but I'm guessing that it's right by intuition.
I think you need to do:
Override keyPressEvent(QKeyEvent *e) to detect up and down arrows.
If the down arrow is pressed, check if it's the last index using currentIndex() const function, compared to the size of the combo box itself.
If so, change the current index to the first one using setCurrentIndex(int index).
Do the same for up arrow if you reached the first index.
P.S. As currentIndex() returned the index after pressing, this might make it jump from the penultimate index to the first one. Thus, I suggest using a private boolean member to be toggled when the condition is met for the first time.
I hope this solution helps you.
The full solution to this problem has a few different aspects.
When the QComboBox is expanded to show all the items, an elegant semantic solution is to override the QAbstractItemView::moveCursor() method. This part of the solution does not require low level event handlers because moveCursor() encapsulates the concept of "next" and "previous". Sadly this only works when the QComboBox is expanded. Note that the items are not actually activated during navigation in this case, until another gesture like a click or enter occurs.
When the QComboBox is collapsed to show one item at a time (the usual case), we have to resort to the low level approach of capturing each relevant gesture, as sketched in the answer by Mohammed Deifallah. I wish Qt had a similar abstraction here analogous to QAbstractItemView::moveCursor(), but it does not. In the code below we capture key press and mouse wheel events, which are the only gestures I'm aware of at the moment. If other gestures are also needed, we would need to independently implement each one. Because the Qt architects did not generalize the concepts of "next" and "previous" for these cases the way they did for QAbstractItemView::moveCursor().
The following code defines a replacement class for QComboBox that implements these principles.
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt
# CircularListView allows circular navigation when the ComboBox is expanded to show all items
class CircularListView(QtWidgets.QListView):
"""
CircularListView allows circular navigation.
So moving down from the bottom item selects the top item,
and moving up from the top item selects the bottom item.
"""
def moveCursor(
self,
cursor_action: QtWidgets.QAbstractItemView.CursorAction,
modifiers: Qt.KeyboardModifiers,
) -> QtCore.QModelIndex:
selected = self.selectedIndexes()
if len(selected) != 1:
return super().moveCursor(cursor_action, modifiers)
index: QtCore.QModelIndex = selected[0]
top = 0
bottom = self.model().rowCount() - 1
ca = QtWidgets.QAbstractItemView.CursorAction
# When trying to move up from the top item, wrap to the bottom item
if index.row() == top and cursor_action == ca.MoveUp:
return self.model().index(bottom, index.column(), index.parent())
# When trying to move down from the bottom item, wrap to the top item
elif index.row() == bottom and cursor_action == ca.MoveDown:
return self.model().index(top, index.column(), index.parent())
else:
return super().moveCursor(cursor_action, modifiers)
class CircularCombobox(QtWidgets.QComboBox):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
view = CircularListView(self.view().parent())
self.setView(view)
def _activate_next(self) -> None:
index = (self.currentIndex() + 1) % self.count()
self.setCurrentIndex(index)
def _activate_previous(self):
index = (self.currentIndex() - 1) % self.count()
self.setCurrentIndex(index)
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
if event.key() == Qt.Key_Down:
self._activate_next()
elif event.key() == Qt.Key_Up:
self._activate_previous()
else:
super().keyPressEvent(event)
def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
delta = event.angleDelta().y()
if delta < 0:
self._activate_next()
elif delta > 0:
self._activate_previous()
I'm trying to build a topBar to put in other widgets layout but I don't know why the `QPixmap is not rescaling as we change the application window size. Here's the code:
QPixmap is within QLabel within a QHBoxLayout of a QWidget that is the centralWidget of a QMainWindow
QT 5.8 - Python 3.6
I've updated this code and deleted the previous version on March 24, 2017.
0 - Dependencies
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
1 - Main Window
class MainWindow(QMainWindow):
def __init__(self):
print("I've been in main window")
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("I'm here, the main window")
2 - Top Bar
class topBar(QWidget):
def __init__(self):
print("I've been in topBar")
super().__init__()
self.initUI()
def initUI(self):
self.setObjectName("topBar")
self.setStyleSheet("""
QWidget {background-color: pink;}
QLabel {background-color: green; }""")
def resizeEvent(self,event):
resizeHandler(self,event) # You'll see this little dude right next
3 - The resizeEvent Handler, that's were I believe the issue is
def resizeHandler(self,event):
print(" I've been in resizeHandler")
if self.objectName() == "topBar":
print("I've been resizing the topBar")
logo = QPixmap('some_Image')
# Debug
# You'll be able to see that it does re-scale, but it's not updating the Pixmap.
logo.scaled(event.size(),Qt.KeepAspectRatio).save("pixmap.png")
# ...
logoContainer = QLabel()
logoContainer.setPixmap(logo.scaled(event.size(),Qt.KeepAspectRatio,Qt.FastTransformation))
logoContainer.setMaximumSize(logo.width(),logo.height())
containerLayout = QHBoxLayout()
containerLayout.addWidget(logoContainer,0)
container = QWidget()
container.setLayout(containerLayout)
# Layout
layout = QHBoxLayout()
layout.addWidget(container,0)
self.setLayout(layout)
main.setCentralWidget(self)
4 - Testing
if __name__ == '__main__':
print("I've been in __main__")
app = 0
app = QApplication(sys.argv)
app.aboutToQuit.connect(app.deleteLater)
app.setWindowIcon(QIcon('someIcon'))
main = MainWindow()
main.layout().setSizeConstraint(QLayout.SetNoConstraint)
bar = topBar()
main.setCentralWidget(bar)
main.show()
app.exec_()
If it's possible I'd also like to limit topBar itself to not exceed 20% of the current screen size vertically (setMaximumHeight? But based on what?) but I'm not sure how.
Thanks!
To get a widget to fill out the container, you would want to set the vertical and horizontal size policy to either minimum, expanding, minimum expanding, or ignored. http://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
As far as the second question, it's not a built-in feature of Qt widgets. You might get better luck with QML or Web Engine. You could create a sub-class of QWidget that uses setGeometry() and some window calculation to constrain its size.
I think you're looking at the wrong thing here when trying to diagnose the problem.
Your last comment states...
the real problem is that QPixmap is not updating its size
That's because the QLabel that's displaying it isn't being resized. Going back to your original code I think all you need to do is insert a layout between container and the QLabel logo...
class topBar(QWidget):
def __init__(self,parent):
super().__init__()
container = QWidget()
container.setMaximumSize(587,208)
container.setMinimumSize(0,0)
## Logo
logo = QLabel(container)
#logo = QLabel(container_layout)
logo.setPixmap(QPixmap('.some_image_in_the_current_working_dir.png'))
logo.setScaledContents(1)
# G.M.
container_layout = QHBoxLayout(container)
container_layout.addWidget(logo)
# Layout
## to center content horizontally in wrapper w/o preventing rescale
layout = QHBoxLayout(self)
layout.addWidget(container)
self.setStyleSheet("""
QWidget {background-color: red;}
QLabel {background-color: green; Qt::KeepAspectRatio;}""")
if __name__ == '__main__':
app = 0
app = QApplication(sys.argv)
app.aboutToQuit.connect(app.deleteLater)
test = topBar(None)
test.show()
app.exec_()
(Look for the G.M. comment)
The code above simply creates a layout container_layout for container and makes logo a child of it. That appears to solve the problem I think you're describing.
After a lot of debugging and reading here and there I came up with the following solution (using numpy to help with the rescaling):
def resizeHandler(self,event):
if self.objectName() == "topBar":
# Wiping the old layout
temp = QWidget()
temp.setLayout(self.layout())
# Pixmap
logoPixmap = QPixmap('./img/exampleImage.png')
# Label
logoLabel = QLabel()
logoLabel.setPixmap(logoPixmap)
logoLabel.setScaledContents(True)
## Label Container Layout
containerLayout = QHBoxLayout()
containerLayout.addWidget(logoLabel,0)
# Label Container
logoContainer = QWidget()
logoContainer.setLayout(containerLayout)
# Finding the width and height of the scaled box
# Image unit vectors
imageSize = np.array((logoPixmap.width(),logoPixmap.height()))
screenSize = np.array((event.size().width(),event.size().height()))
# Proportion of each dimension in relation to the smallest side
# Note that one will always be the unit vector and the other greater than a unit
screenUVec = screenSize / screenSize.min()
imageUVec = imageSize / imageSize.min()
# minBorder 11 is the distance between the Application vertical border and the central widget
# 22 is the minimum height where the CentralWidget begins to appear
# Which should vary according to the height of menubar and statsbar of the QMainWindow
minBorder = np.array([11,22]) *2
screenSize -= minBorder
for axis,size in enumerate(screenSize):
if size < 0:
screenSize[axis] = 0
maxSize = np.zeros(2)
# Ideal ratio based on the maxSide
ratio = int(round(screenSize[imageUVec.argmax()] / imageUVec.max() - 0.49999999))
if ratio >=1 and 0 not in screenSize: # Image is scalable on the current screen
maxSize[imageUVec.argmin()] = min(screenSize[imageUVec.argmin()],ratio) # We should add imageSize[imageUVec.argmin()] if we want to limit the maxSize to the maximum size of the image
maxSize[imageUVec.argmax()] = maxSize[imageUVec.argmin()] * imageUVec.max()
sizeUVec = maxSize / maxSize.min()
# Layout
layout = QHBoxLayout()
layout.addWidget(logoContainer,0)
logoLabel.setMaximumSize(QSize(maxSize[0],maxSize[1]))
self.setLayout(layout)
A special thanks to #alexisdm, he showed me HERE that we should first wipe the old layout. When I started watching the globals I saw that several layouts were stacked.
As for the rescaling part, I still went through an unconventional path, but it's behaving the way I want.
I have a couple of questions regarding the sizes of QItemDelegates in a QListView:
I have a QListView using a QItemDelegate which renders a widget in the delegate's custom paint() method like so:
self.thumbnail = MyCustomWidget()
self.thumbnail.render(painter, QtCore.QPoint(option.rect.x(), option.rect.y()))
This, however, shows the item with a 250x260 image in a QListView, even though the MyCustomWidget().sizeHint() is 250x250 and it's maximumSize() returns 250x250 as well.
I found that the culprit is the QListView's spacing, which I had set to 10. If I set the spacing to 100, I still get the QItemDelegates size of 250x260, but if I just don't use setSpacing() at all it renders as expected at 250x250.
The spacing seems to alter the option.rect that is passed into the paint method, causing the incorrect size.
I do need that spacing, so I'm a bit confused why the QListView's spacing alters the QItemDelegates's size? Is this a bug?
I can work around this by rendering a QPixmap first, then have the painter draw the QPixmap instead of rendering to the painter directly:
self.thumbnail = MyCustomWidget()
pixmap = QtGui.QPixmap(self.thumbnail.size())
self.thumbnail.render(pixmap)
painter.drawPixmap(option.rect.topLeft(), pixmap)
This yields 250x250 images which is what I need, but I don't understand why the first method doesn't render the correct size when I use setSpacing?!
Now, the bigger challenge is how to dynamically scale the size of the QItemDelegate's via a QSlider:
I have a QSlider in the QListView that is supposed to scale the items so the user can chose to see smaller but more items in the current view. I tested the resizing of a standalone instance of MyCustomWidget() and it works just fine.
However, the delegates won't scale as expected. This is my delegate code:
class Delegate(QtGui.QItemDelegate):
def __init__(self, parent = None):
super(Delegate, self).__init__(parent)
self.scaleValue = 100 # size in percent (as returned by QSlider)
def paint(self, painter, option, index):
proxyModel = index.model()
item = proxyModel.sourceModel().itemFromIndex(proxyModel.mapToSource(index))
self.thumbnail = ElementThumbnail(item)
self.thumbnail.scale(self.scaleValue)
pixmap = QtGui.QPixmap(self.thumbnail.size())
self.thumbnail.render(pixmap)
painter.drawPixmap(option.rect.topLeft() * self.scaleValue / 100.0, pixmap)
super(Delegate, self).paint(painter, option, index)
def setScaleValue(self, value):
self.scaleValue = value
def sizeHint(self, option, index):
return ElementThumbnail.thumbSize * self.scaleValue / 100.0
and in the QListView I am using this slot connected to the slider's valueChanges signal:
def scaleThumbnails(self, value):
self.itemDelegate().setScaleValue(value)
self.update()
The result is that the QSlider will crop the QItemDelegates but not scale them, because the QItemDelegate's sizeHint() is only called when the QListView is first shown.
Additionally, I need to make sure that when the widgets are (eventually) scaled down, the layout of QListView is recalculated and more items are fit inside the visible area.
So in a nutshell my questions are:
How can I scale QItemDelegates dynamically inside a QListView?
How can I force the QListView to recalculate it's layout after the delegate size has been changed?
edit: as for issue 2: QAbstractItemView.doItemsLayout seems to do the trick. Still wondering about issue 1 though
Thanks,
frank
Turns out it was the context of my code that was the issue, not the delegate.
I was scaling the widget before rendering it to a pixmap, which of course made the editor scale properly, but not the item when it wasn't in edit state.
So the solution is simply to scale the pixmap after rendering it from the widget, e.g.:
scaledPixmap = pixmap.scaled(pixmap.size() * self.scaleValue / 100.0)
painter.drawPixmap(option.rect.topLeft(), scaledPixmap)