Layouting Issue with QScrollBar
-
Hi
I'm trying to replicate an indicator like the one found in GoogleFotos. (Basically you have a scrollbar which has an indicator widget that points to the current location of the scroll bar and gives a readout of the date at the point.
Now I was able to replicate that mostly within Qt. However, I was trying to force the
size
of theslider
in theQScrollBar
to remain constant regardless of therange
it has. That also was a bit tricky, since thesetStyleStheet
function was behaving a bit erratic:
Passing it:QScrollBar::handle:vertical { min-height: 50px; max-height: 50px }
lead to no observable change.
But passing it:QScrollBar:vertical { border: 1px dashed black; width: 15px; } QScrollBar::handle:vertical { min-height: 50px; max-height: 50px }
locked the size of the slider to 50px. It might be possible that I've missed something in the docs but I wasn't able to find any mention of that behavior in the Qt 6.6 docs.
I'd like to add that for example the docs on the properties
min-width
andmax-width
aren't that precise. Before I discovered that I was in fact able to style the slider if I passed that extra block of styling, I assumed thatQScrollBar::handle
didn't support stylesheets with Python.However, testing with copy-pasting different sections from Customizing Scrollbar, I was able to discover the mentioned behavior.
This revealed a new problem. Once I got the slider to a fixed size, it would not stop in front of the buttons anymore but now move over top of them.
Here's the Python Code for that Window:
import sys from PyQt6.QtWidgets import ( QApplication, QFormLayout, QGridLayout, QLabel, QScrollArea, QScroller, QWidget, QMainWindow, QScrollBar, QFrame, QHBoxLayout, QVBoxLayout, QListWidget, ) from PyQt6 import QtGui from PyQt6.QtCore import Qt import random class RootWindow(QMainWindow): def __init__(self): super().__init__() self.dummy_widget = QWidget() self.dummy_widget.setMinimumWidth(100) self.dummy_widget.setMinimumHeight(500) self.placeholder = QFrame() self.placeholder.setFrameStyle(QFrame.Shape.Box) self.indicator = QLabel("Indicator", self) self.indicator.setVisible(False) self.indicator.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.sc = QScrollBar(Qt.Orientation.Vertical) self.sc.setMaximum(1000) self.sc.valueChanged.connect(self.value_reader) self.sc.sliderReleased.connect(self.hide_indicator) self.sc.sliderPressed.connect(self.show_indicator) self.sc.setFixedWidth(15) self.sc.setPageStep(10) style_sheet = """ QScrollBar:vertical { border: 1px dashed black; width: 15px; } QScrollBar::handle:vertical { min-height: 50px; max-height: 50px } """ self.sc.setStyleSheet(style_sheet) self.l = QHBoxLayout() self.dummy_widget.setLayout(self.l) self.l.addWidget(self.placeholder) self.l.addWidget(self.sc) self.setCentralWidget(self.dummy_widget) def show_indicator(self): self.indicator.setVisible(True) self.value_reader() def hide_indicator(self): self.indicator.setVisible(False) def value_reader(self): no_arrows = self.sc.height() - self.sc.width() * 2 relative = self.sc.value() / (self.sc.maximum() - self.sc.minimum()) # should be # movement_range = no_arrows - 50 # is self.indicator.setText(str(self.sc.value())) movement_range = self.sc.height() - 50 self.indicator.move(self.width() - 15 - self.indicator.width(), int(relative * movement_range + 25 + 10 - self.indicator.height() / 2)) if __name__ == '__main__': app = QApplication(sys.argv) ex = RootWindow() ex.show() sys.exit(app.exec())
I'm running
Python3.8
andPyQt6.6
I'm admittedly quite new to Qt and I haven't mastered all of it's concepts yet. It might very well be that there's some hidden trick to get that to work I've just not been able to track it down.
Thank you for your help
AliSot2000
-
Hi
I managed to replicate the GooglePhotos ScrollBar with the following code snippet. Sadly I wasn't able to get the position of the handle of the scrollbar using any of the available methods offered by Qt. I ended up calculating the position myself in python and then moving the widget to the associated position.
That is to say. None of the Methods like
subControlRect()
provided me with any information about the handle of the scrollbar. They always returned a QSomething() (Like QRect or QPoint) which was empty. That's why I ended up computing the position in python using the QPixelMetric, which was the only thing I could access in python.Here's the code snippet that ended up working for me:
import math import sys from PyQt6.QtCore import Qt from PyQt6.QtGui import QPixmapCache from PyQt6.QtWidgets import ( QApplication, QLabel, QWidget, QMainWindow, QScrollBar, QHBoxLayout, QTextEdit, QGridLayout, QPushButton, QSlider, QStyle ) class RootWindow(QMainWindow): def __init__(self): super().__init__() self.sc = QScrollBar(Qt.Orientation.Vertical) self.dummy_widget = QWidget() self.dummy_widget.setMinimumWidth(100) self.dummy_widget.setMinimumHeight(500) self.placeholder = QWidget() self.placeholder_layout = QGridLayout() self.placeholder_layout.setSpacing(10) self.placeholder.setLayout(self.placeholder_layout) self.placeholder_layout.setContentsMargins(0, 0, 0, 0) self.style_sheet_input = QTextEdit() self.submit_btn = QPushButton("Submit") self.submit_btn.clicked.connect(self.set_style_sheet) self.scroll_max = QSlider(Qt.Orientation.Horizontal) self.scroll_max.valueChanged.connect(self.max_set) self.scroll_max.setMaximum(1000) self.scroll_max.setMinimum(50) self.page_size = QSlider(Qt.Orientation.Horizontal) self.page_size.valueChanged.connect(self.page_set) self.page_size.setMaximum(50) self.page_size.setMinimum(10) self.do_shit_btn = QPushButton("Do-Shit") self.do_shit_btn.clicked.connect(self.do_shit) self.placeholder_layout.addWidget(self.style_sheet_input, 0, 0, 1, 4) self.placeholder_layout.addWidget(self.submit_btn, 1, 2) self.placeholder_layout.addWidget(self.scroll_max, 1, 0) self.placeholder_layout.addWidget(self.page_size, 1, 1) self.placeholder_layout.addWidget(self.do_shit_btn, 1, 3) # self.sc = QScrollBar(Qt.Orientation.Horizontal) self.sc.setMaximum(20) self.sc.valueChanged.connect(self.value_reader) self.sc.sliderReleased.connect(self.hide_indicator) self.sc.sliderPressed.connect(self.show_indicator) self.l = QHBoxLayout() self.l.setContentsMargins(10, 10, 10, 10) self.l.setSpacing(10) # self.l = QVBoxLayout() self.dummy_widget.setLayout(self.l) self.l.addWidget(self.placeholder) self.l.addWidget(self.sc) self.setCentralWidget(self.dummy_widget) # Spacing self.upper_pad = QLabel("", self) self.upper_pad.setVisible(False) self.upper_pad.setStyleSheet("QLabel {background: rgba(255, 128, 0, 128);}") self.lower_pad = QLabel("", self) self.lower_pad.setVisible(False) self.lower_pad.setStyleSheet("QLabel {background: rgba(255, 128, 0, 128);}") # Arrows self.upper_arrow = QLabel("", self) self.upper_arrow.setVisible(False) self.upper_arrow.setStyleSheet("QLabel {background: rgba(128, 255, 0, 128);}") self.lower_arrow = QLabel("", self) self.lower_arrow.setVisible(False) self.lower_arrow.setStyleSheet("QLabel {background: rgba(128, 255, 0, 128);}") # Page self.upper_page = QLabel("", self) self.upper_page.setVisible(False) self.upper_page.setStyleSheet("QLabel {background: rgba(0, 255, 128, 128);}") self.lower_page = QLabel("", self) self.lower_page.setVisible(False) self.lower_page.setStyleSheet("QLabel {background: rgba(0, 255, 128, 128);}") # Scrollbar self.scrollbar = QLabel("", self) self.scrollbar.setVisible(False) self.scrollbar.setStyleSheet("QLabel {background: rgba(0, 128, 255, 128);}") self.indicator = QLabel("Indicator", self) self.indicator.setStyleSheet("QLabel {background: red;}") self.indicator.setVisible(True) self.indicator.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.indicator.setFixedHeight(26) self.indicator.setFixedWidth(100) def do_shit(self, *args, **kwargs): """ Function shows all overlays for debugging purposes (i.e. take a screenshot to make sure the calculations in python are correct """ print(f"Args {args}") print(f"Kwargs {kwargs}") self.indicator.setVisible(True) self.upper_pad.setVisible(True) self.lower_pad.setVisible(True) self.upper_arrow.setVisible(True) self.lower_arrow.setVisible(True) self.upper_page.setVisible(True) self.lower_page.setVisible(True) self.scrollbar.setVisible(True) def max_set(self, v: int): self.sc.setMaximum(v) self.sc.setValue(0) print(f"Max: {v}") def page_set(self, v: int): self.sc.setPageStep(v) self.sc.setValue(0) print(f"Page: {v}") def set_style_sheet(self): style_sheet = self.style_sheet_input.toPlainText() print(style_sheet) self.sc.setStyleSheet(style_sheet) def show_indicator(self): self.indicator.setVisible(True) self.upper_pad.setVisible(True) self.lower_pad.setVisible(True) self.upper_arrow.setVisible(True) self.lower_arrow.setVisible(True) self.upper_page.setVisible(True) self.lower_page.setVisible(True) self.scrollbar.setVisible(True) self.value_reader() def hide_indicator(self): self.indicator.setVisible(False) self.upper_pad.setVisible(False) self.lower_pad.setVisible(False) self.upper_arrow.setVisible(False) self.lower_arrow.setVisible(False) self.upper_page.setVisible(False) self.lower_page.setVisible(False) self.scrollbar.setVisible(False) def value_reader(self): """ This function is an implementation in python to get the QScrollBar().subControlRect() of the handle of the scrollbar. It then moves a QLabel to the position of the scrollbar such that the middle of the scrollbar and the middle of QLabel are on the same y-coordinate. Sadly, I couldn't find a way to make this work using the methods of Qt. Last I checked, subControlRect returns an empty QRect() for the Scrollbar handle. """ print(f"Scrollbar Size:", self.sc.size()) min_handle_height = self.sc.style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarSliderMin) no_arrows = self.sc.height() - self.sc.width() * 2 relative = self.sc.value() / (self.sc.maximum() - self.sc.minimum()) # Height should be page step / document length bar_height_rel = self.sc.pageStep() / (self.sc.maximum() - self.sc.minimum() + self.sc.pageStep()) bar_height_px = math.floor(bar_height_rel * no_arrows) handle_height = max(min_handle_height, bar_height_px) print(handle_height) self.indicator.setText(str(self.sc.value())) movement_range = no_arrows - handle_height self.indicator.move(self.width() - self.sc.width() - 10 - self.indicator.width(), int(relative * movement_range + (handle_height / 2) + 10 + self.sc.width() - self.indicator.height() / 2)) # Spacing self.lower_pad.setFixedWidth(self.width()) self.lower_pad.setFixedHeight(10) self.lower_pad.move(0, self.height() - 10) self.upper_pad.setFixedWidth(self.width()) self.upper_pad.setFixedHeight(10) self.upper_pad.move(0, 0) # Arrows self.lower_arrow.setFixedHeight(self.sc.width()) self.lower_arrow.setFixedWidth(self.width()) self.lower_arrow.move(0, self.height() - 10 - self.sc.width()) self.upper_arrow.setFixedHeight(self.sc.width()) self.upper_arrow.setFixedWidth(self.width()) self.upper_arrow.move(0, 10) # Page self.lower_page.setFixedWidth(self.width()) self.lower_page.setFixedHeight(int(math.ceil(movement_range * (1 - relative)))) self.lower_page.move(0, 10 + self.sc.width() + int(movement_range * relative + handle_height)) self.upper_page.setFixedWidth(self.width()) self.upper_page.setFixedHeight(int(movement_range * relative)) self.upper_page.move(0, 10 + self.sc.width()) # Handle self.scrollbar.setFixedWidth(self.width()) self.scrollbar.setFixedHeight(handle_height) self.scrollbar.move(0, 10 + self.sc.width() + int(relative * movement_range)) # Getting position of # stl = self.style() # Doesn't work: v is empty QRect() # v = stl.subControlRect(stl.ComplexControl.CC_ScrollBar, None, stl.SubControl.SC_ScrollBarSlider, self.sc) # stl.subElement doesn't work because it has no scrollbar stuff if self.sc.isSliderDown(): print(f"Slider is down") else: print(f"Slider is up") if __name__ == '__main__': app = QApplication(sys.argv) x = app.style().pixelMetric(QApplication.style().PixelMetric.PM_ScrollBarExtent) print(x) print(QPixmapCache.cacheLimit()) QPixmapCache.setCacheLimit(1024) print(QPixmapCache.cacheLimit()) ex = RootWindow() ex.show() sys.exit(app.exec())
-