How to apply QScrollerProperties to a QScroller, to get rid of overshoot? - python-3.x

As the title says I'm trying to make a scrollArea that uses QScroller with grabgesture so I can scroll by dragging on the widget. I found some good examples and got it working. Now I want to remove the overshoot that happens when you drag further than there is items in the widget.
But when I try to tweak the Qscroller, I can't seem to figure out how to apply the QScrollerProperties to the QScroller. Which is how I assume you remove the overshoot.
Here is an example of the code:
import sys
from PyQt5.QtWidgets import (
QApplication,
QFormLayout,
QGridLayout,
QLabel,
QScrollArea,
QScroller,
QScrollerProperties,
QWidget,
)
class MainWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
scroll_area = QScrollArea()
layout = QGridLayout(self)
layout.addWidget(scroll_area)
scroll_widget = QWidget()
scroll_layout = QFormLayout(scroll_widget)
for i in range(200):
scroll_layout.addRow(QLabel('Label #{}'.format(i)))
scroll_area.setWidget(scroll_widget)
scroll = QScroller.scroller(scroll_area.viewport())
scroll.grabGesture(scroll_area.viewport(), QScroller.LeftMouseButtonGesture)
scroll.scrollerPropertiesChanged.connect(self.PropsChanged) #Just to see if I could registre a change
props = scroll.scrollerProperties()
props.setScrollMetric(QScrollerProperties.VerticalOvershootPolicy,QScrollerProperties.OvershootAlwaysOff)
props.setScrollMetric(QScrollerProperties.DragStartDistance, 0.01)
#Apply Qscroller properties here somehow?
print(scroll.scrollerProperties().scrollMetric(QScrollerProperties.DragStartDistance))
scroll.scrollerProperties = props #Maybe? Doesn't seem to change the overshoot?
def PropsChanged(self):
print("Something is being changed??")
if __name__ == '__main__':
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
I'm not sure how to proceed from here.
Any help would be appriciated :)

Just call scroll.setScrollerProperties(props) once you've set the new properties.
When you call scrollerProperties() you get "copy" of the current properties: it is not a pointer to the actual properties, so nothing changes unless you apply them back to the scroller.
It's almost like calling self.font():
font = self.font()
font.setPointSize(20)
# at this point, the widget font is still the same...
# unless you do this:
self.setFont(font)
The same applies to almost any property, like text()/setText() for labels, palette()/setPalette(), etc.
To prevent the vertical overshoot, you have to use setScrollMetric with VerticalOvershootPolicy, and set the value to OvershootAlwaysOff:
props.setScrollMetric(QScrollerProperties.VerticalOvershootPolicy,
QScrollerProperties.OvershootAlwaysOff)
scroll.setScrollerProperties(props)

Related

How do I add scroll function to main window in python pyqt5?

I'm trying to learn pyqt5 in python by creating a small application. For one of the windows, I need to add a vertical scroll bar to the window. Now, this window has a table made using QLabel and QLineEdit. Check the picture to get exactly how it looks like.
As you can see there are a lot of chemicals, which goes below the window screen. I have tried numerous approaches but somehow couldn't get the result. If I am able to get the scroll, all the elements get aligned one under another (QVBoxLayout) which is not the way I want the elements to be aligned.
Here's the code I'm using
class ChemicalWindow(QWidget):
def __init__(self,chemicals,data):
super().__init__()
self.layout = QVBoxLayout()
self.setWindowTitle("Chemicals")
self.setMinimumSize(QSize(600,600))
self.setStyleSheet("background-color:#eaf4f4;")
self.chemicals = chemicals
self.data = data
self.createBody()
self.createButtons()
def createBody(self):
headerLabel = QLabel('Chemicals',scroll_widget)
headerLabel.move(265,10)
headerLabel.resize(70,40)
headerLabel.setStyleSheet("color:#000;")
tcLabel = QLabel('Tc',scroll_widget)
tcLabel.move(200,50)
tcLabel.resize(60,30)
tcLabel.setStyleSheet("color:#000;")
pcLabel = QLabel('Pc',scroll_widget)
pcLabel.move(280,50)
pcLabel.resize(60,30)
pcLabel.setStyleSheet("color:#000;")
cpLabel = QLabel('Cp',scroll_widget)
cpLabel.move(360,50)
cpLabel.resize(60,30)
cpLabel.setStyleSheet("color:#000;")
self.chemical_names = self.chemicals.keys()
y_position = 90
# List for keeping chemical inputs variables in form of dict of list -> {A:[chemical_a_tc,chemical_a_pc,chemical_a_cp],
# B:[chemical_b_tc,chemical_b_pc,...],...}
self.chemical_inputs = dict()
# Creating labels for the chemical names
for name in self.chemical_names:
chemicalLabel = QLabel(name,scroll_widget)
chemicalLabel.move(70,y_position)
chemicalLabel.resize(75,30)
chemicalLabel.setStyleSheet("color:#000;")
chemicalLabel.setToolTip(name)
y_position += 40
current_chemical_inputs = dict()
for chemical_input in self.chemicals[name]:
current_chemical_inputs[chemical_input] = QLineEdit(scroll_widget)
self.chemical_inputs[name] = current_chemical_inputs
position_y = 90
for individual_chemical in self.chemical_inputs:
position_x = 160
for chemical_input in self.chemical_inputs[individual_chemical]:
self.chemical_inputs[individual_chemical][chemical_input].setText(str(self.data['chemicals'][individual_chemical][chemical_input]))
self.chemical_inputs[individual_chemical][chemical_input].move(position_x,position_y)
self.chemical_inputs[individual_chemical][chemical_input].resize(80,30)
self.chemical_inputs[individual_chemical][chemical_input].setStyleSheet("color:#000;background-color:#a9d6e5;padding:2px;")
position_x += 90
position_y += 40
def createButtons(self):
close_button = QPushButton('Close',self)
close_button.move(510,550)
close_button.resize(70,30)
close_button.setStyleSheet("background-color:#00509d;color:#fff;")
close_button.clicked.connect(self.closeButton)
def closeButton(self):
self.close()
What am I doing wrong?
Firstly, instead of using .move() to manually place your widgets, you should be using a QLayout (ex. QHBoxLayout or QVBoxLayout). This will automatically space your labels, and you can modify it by adjusting stretch and adding spacers (QSpacerItem). For more complex layouts, you can either nest multiple box layouts, or use a QGridLayout.
Now to address the scrolling:
First, you want to create your scroll area. Make this widget the central widget. Remember to set setWidgetResizable to True.
scroller = QScrollArea()
scroller.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroller.resize(self.width(),self.height())
scroller.setWidgetResizable(True)
self.setCentralWidget(scroller)
Next, create your container and add it to the scroll area. All your layout elements (labels, buttons, etc.) should be placed in this container.
self.container = QWidget()
scroller.setWidget(self.container)
Here's the full sample program I created:
import sys
from PyQt5.QtWidgets import QMainWindow, QWidget, QScrollArea, QVBoxLayout, QLabel, QApplication
from PyQt5.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(1100, 800)
scroller = QScrollArea()
scroller.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.container = QWidget()
scroller.setWidget(self.container)
scroller.resize(self.width(),self.height())
scroller.setWidgetResizable(True)
self.setCentralWidget(scroller)
self.holderColumn=QVBoxLayout()
txtList=["apple","banana","orange","triangle","circle","square","moon","star","sun","delta"]
objs=list()
for i in txtList:
tempLabel=QLabel()
tempLabel.setText(i)
tempLabel.setFixedSize(300,300)
objs.append(tempLabel)
self.holderColumn.addWidget(tempLabel)
self.container.setLayout(self.holderColumn)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

QListWidget doesn't show custom widget lables

I'm trying to add custom RadioWidgets as QListWidgetItem to a QListWidget. The Mainwindow is shown and it seems radiowidget items are added but the labels are not shown. This is the code I'm using:
from typing import Optional
from PyQt5.QtWidgets import QLabel, QWidget
class RadioWidget(QWidget):
def __init__(self, parent: Optional[QWidget], radioTitle: str) -> None:
super().__init__(parent=parent)
self.radioTitleLbl = QLabel(radioTitle)
import sys
from typing import Optional
from PyQt5.QtWidgets import (
QApplication,
QListWidget,
QListWidgetItem,
QMainWindow,
QWidget,
)
from vagh.radiowidget import RadioWidget
class Vagh(QMainWindow):
def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent=parent)
self.radiosList = QListWidget(self)
self.radios = ("radio1", "radio2", "radio3")
self.loadRadios()
self.setCentralWidget(self.radiosList)
self.show()
def loadRadios(self):
for radio in self.radios:
radioItem = QListWidgetItem(self.radiosList)
radioWidget = RadioWidget(self.radiosList, radio)
radioItem.setSizeHint(radioWidget.sizeHint())
self.radiosList.addItem(radioItem)
self.radiosList.setItemWidget(radioItem, radioWidget)
if __name__ == "__main__":
app = QApplication(sys.argv)
v = Vagh()
app.exec_()
A widget that is created without any parent is considered a top level window (until it's reparented, for example when added to a layout).
Theoretically, the QLabel could be correctly added as a child by adding the RadioWidget instance as a parent in the constructor:
self.radioTitleLbl = QLabel(radioTitle, self)
But this won't work well: the label will be created on the top left of the radio widget, but that widget will know nothing about the label.
The sizeHint of a widget is returned based on its contents only when a layout manager is set for the widget (or when reimplemented in some way).
Since no layout manager is set for that widget, the result is that RadioWidget considers itself empty, returning an invalid hint (QSize(-1, -1)), and you will not see the label (nor the item) because the item is resized to a null size.
As usual, layout managers should always be used.
Not only: adding the label to a layout ensures that it correctly resizes itself whenever its text changes.
With the following modification you don't even need to set the sizeHint on the QListWidgetItem, as it will automatically use the hint returned by the widget:
class RadioWidget(QWidget):
def __init__(self, parent: Optional[QWidget], radioTitle: str) -> None:
super().__init__(parent=parent)
layout = QVBoxLayout(self)
self.radioTitleLbl = QLabel(radioTitle)
layout.addWidget(self.radioTitleLbl)

How to modify this PyQt5 current setup to enable drag resize between layouts

How to modify this current setup to enable resizing(horizontally and vertically) between the layouts shown below? Let's say I want to resize the lists in the right toward the left by dragging them using the mouse, I want the image to shrink and the lists to expand and same applies for in between the 2 lists.
Here's the code:
from PyQt5.QtWidgets import (QMainWindow, QApplication, QDesktopWidget, QHBoxLayout, QVBoxLayout, QWidget,
QLabel, QListWidget)
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt
import sys
class TestWindow(QMainWindow):
def __init__(self, left_ratio, right_ratio, window_title):
super().__init__()
self.left_ratio = left_ratio
self.right_ratio = right_ratio
self.current_image = None
self.window_title = window_title
self.setWindowTitle(self.window_title)
win_rectangle = self.frameGeometry()
center_point = QDesktopWidget().availableGeometry().center()
win_rectangle.moveCenter(center_point)
self.move(win_rectangle.topLeft())
self.tools = self.addToolBar('Tools')
self.left_widgets = {'Image': QLabel()}
self.right_widgets = {'List1t': QLabel('List1'), 'List1l': QListWidget(),
'List2t': QLabel('List2'), 'List2l': QListWidget()}
self.central_widget = QWidget(self)
self.main_layout = QHBoxLayout()
self.left_layout = QVBoxLayout()
self.right_layout = QVBoxLayout()
self.adjust_widgets()
self.adjust_layouts()
self.show()
def adjust_layouts(self):
self.main_layout.addLayout(self.left_layout, self.left_ratio)
self.main_layout.addLayout(self.right_layout, self.right_ratio)
self.central_widget.setLayout(self.main_layout)
self.setCentralWidget(self.central_widget)
def adjust_widgets(self):
self.left_layout.addWidget(self.left_widgets['Image'])
self.left_widgets['Image'].setPixmap(QPixmap('test.jpg').scaled(500, 400, Qt.IgnoreAspectRatio,
Qt.SmoothTransformation))
for widget in self.right_widgets.values():
self.right_layout.addWidget(widget)
if __name__ == '__main__':
test = QApplication(sys.argv)
test_window = TestWindow(6, 4, 'Test')
sys.exit(test.exec_())
One way to rescale the image to an arbitrary size while maintaining its aspect ratio is to subclass QWidget and override sizeHint and paintEvent and use that instead of a QLabel for displaying the image, e.g.
class PixmapWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._pixmap = None
def sizeHint(self):
if self._pixmap:
return self._pixmap.size()
else:
return QSize()
def setPixmap(self, pixmap):
self._pixmap = pixmap
self.update()
def paintEvent(self, event):
painter = QPainter(self)
super().paintEvent(event)
if self._pixmap:
size = self._pixmap.size().scaled(self.size(), Qt.KeepAspectRatio)
offset = (self.size() - size)/2
rect = QRect(offset.width(), offset.height(), size.width(), size.height())
painter.drawPixmap(rect, self._pixmap)
Since you are subclassing QMainWindow you could use DockWidgets to display the lists instead of adding them to the layout of the central widget, e.g.
class TestWindow(QMainWindow):
def __init__(self, left_ratio, right_ratio, window_title):
super().__init__()
#self.left_ratio = left_ratio <--- not needed since image and lists
#self.right_ratio = right_ratio <--- are not sharing a layout anymore
...
# use PixmapWidget instead of QLabel for showing image
# refactor dictionary for storing lists to make adding DockWidgets easier
self.left_widgets = {'Image': PixmapWidget()}
self.right_widgets = {'List1': QListWidget(),
'List2': QListWidget()}
self.central_widget = QWidget(self)
# self.main_layout = QHBoxLayout() <-- not needed anymore
self.left_layout = QVBoxLayout()
self.adjust_widgets()
self.adjust_layouts()
self.show()
def adjust_layouts(self):
self.central_widget.setLayout(self.left_layout)
self.setCentralWidget(self.central_widget)
def adjust_widgets(self):
self.left_layout.addWidget(self.left_widgets['Image'])
self.left_widgets['Image'].setPixmap(QPixmap('test.jpg').scaled(500, 400, Qt.IgnoreAspectRatio, Qt.SmoothTransformation))
self.dock_widgets = []
for text, widget in self.right_widgets.items():
dock_widget = QDockWidget(text)
dock_widget.setFeatures(QDockWidget.NoDockWidgetFeatures)
dock_widget.setWidget(widget)
self.addDockWidget(Qt.RightDockWidgetArea, dock_widget)
self.dock_widgets.append(dock_widget)
Screenshots
You need to use QSplitter.
It acts almost like a box layout, but has handles that allow the resizing of each item.
Be aware that you can only add widgets to a QSplitter, not layouts, so if you need to add a "section" (a label and a widget) that can resize its contents, you'll have to create a container widget with its own layout.
Also note that using dictionaries for these kind of things is highly discouraged. For versions of Python older than 3.7, dictionary order is completely arbitrary, and while sometimes it might be consistent (for example, when keys are integers), it usually isn't: with your code some times the labels were put all together, sometimes the widgets were inverted, etc., so if somebody is using your program with <=3.6 your interface won't be consistent. Consider that, while python 3.6 will reach end of life in 2022, it's possible that even after that a lot of people will still be using previous versions.
If you need a way to group objects, it's better to use a list or a tuple, as I did in the following example.
If you really "need" to use a key based group, then you can use OrderedDict, but it's most likely that there's just something wrong with the logic behind that need to begin with.
class TestWindow(QMainWindow):
def __init__(self, left_ratio, right_ratio, window_title):
super().__init__()
self.left_ratio = left_ratio
self.right_ratio = right_ratio
self.current_image = None
self.window_title = window_title
self.setWindowTitle(self.window_title)
win_rectangle = self.frameGeometry()
center_point = QDesktopWidget().availableGeometry().center()
win_rectangle.moveCenter(center_point)
self.move(win_rectangle.topLeft())
self.tools = self.addToolBar('Tools')
self.left_widgets = {'Image': QLabel()}
self.right_widgets = [(QLabel('List1'), QListWidget()),
(QLabel('List2'), QListWidget())]
self.central_widget = QSplitter(Qt.Horizontal, self)
self.setCentralWidget(self.central_widget)
self.right_splitter = QSplitter(Qt.Vertical, self)
self.adjust_widgets()
self.central_widget.setStretchFactor(0, left_ratio)
self.central_widget.setStretchFactor(1, right_ratio)
self.show()
def adjust_widgets(self):
self.central_widget.addWidget(self.left_widgets['Image'])
self.left_widgets['Image'].setPixmap(QPixmap('test.jpg').scaled(500, 400, Qt.IgnoreAspectRatio,
Qt.SmoothTransformation))
self.left_widgets['Image'].setScaledContents(True)
self.central_widget.addWidget(self.right_splitter)
for label, widget in self.right_widgets:
container = QWidget()
layout = QVBoxLayout(container)
layout.addWidget(label)
layout.addWidget(widget)
self.right_splitter.addWidget(container)

Is there a way to put the text of a QCheckBox above the icon?

I have a gridlayout that holds a bunch of check boxes. I wanted to add an image to the check boxes as well as some text. The problem I am having is that the layout of a check box is left to right (check box, icon, text).
Is there a way to put the text above the icon? Not sure if using a style sheet would work for this or not or even how that would look.
Thank you.
Answer : In PyQt4. No, your can't do it.
Why ? I read source code of QCheckBox Qt4 (C++) here and here. I saw it use default QStyleOptionButton to show check box, text and icon. It's use drawControl to draw all element in QStyleOptionButton by specified config in QStyleOptionButton. Also it have LayoutDirection. And layout direction in QStyleOptionButton. I don't know in Qt4 C++ and inheritance it and swap direction icon. But in PyQt4, It's impossible to do it.
Another way ? : Yes, It have another way to solve but not directly. Your just create your own widget just like QCheckBox and disable icon in QCheckBox and make your own QLabel ot show your icon and set it with same QLayout.
Example;
import sys
from PyQt4 import QtGui, QtCore
class QCustomCheckBox (QtGui.QWidget):
stateChanged = QtCore.pyqtSignal(int)
def __init__ (self, text, parentQWidget = None):
super(QCustomCheckBox, self).__init__(parentQWidget)
self.customQCheckBox = QtGui.QCheckBox(text)
self.iconQLabel = QtGui.QLabel()
allQHBoxLayout = QtGui.QHBoxLayout()
allQHBoxLayout.addWidget(self.customQCheckBox)
allQHBoxLayout.addWidget(self.iconQLabel)
allQHBoxLayout.addStretch(1)
self.setLayout(allQHBoxLayout)
self.customQCheckBox.stateChanged.connect(self.stateChanged.emit)
def setPixmap (self, newQPixmap, width = 48, height = 48):
self.iconQLabel.setPixmap(newQPixmap.scaled(width, height, QtCore.Qt.KeepAspectRatio))
def pixmap (self):
return self.iconQLabel.pixmap()
class QCustomWidget (QtGui.QWidget):
def __init__ (self, parent = None):
super(QCustomWidget, self).__init__(parent)
allQVBoxLayout = QtGui.QVBoxLayout()
firstQCustomCheckBox = QCustomCheckBox('First Check Box')
firstQCustomCheckBox.setPixmap(QtGui.QPixmap('1.jpg'))
allQVBoxLayout.addWidget(firstQCustomCheckBox)
secondQCustomCheckBox = QCustomCheckBox('Second Check Box')
secondQCustomCheckBox.setPixmap(QtGui.QPixmap('2.jpg'))
allQVBoxLayout.addWidget(secondQCustomCheckBox)
self.setLayout(allQVBoxLayout)
if __name__ == '__main__':
myQApplication = QtGui.QApplication(sys.argv)
myQCustomWidget = QCustomWidget()
myQCustomWidget.show()
sys.exit(myQApplication.exec_())

Using QAction with QLabel and creating accordion using QStackedWidget

I am trying to add QAction object to QLabel object using QLabel.addAction() method, but it doesn't seem to work. Is it not supposed to work or am I doing something wrong?
I am trying to make an accordion using QStackedWidget.
For this I need a section title which will either hide or show the title's section when user presses on that title. I could use mouseReleasedEvent, but I would prefer proper QAction toggle() implementation. Maybe I could use something else than QLabel for this matter?
The addAction functionality of QWidget is used for providing context menus and does not directly relate to an action that is triggered when the mouse is clicked on the label.
You therefore must use some kind of mousexxxevent.
If you prefer signals instead, this is also quite easy:
from PySide.QtGui import *
from PySide.QtCore import *
class ClickableLabel(QLabel):
"""
A Label that emits a signal when clicked.
"""
clicked = Signal()
def __init__(self, *args):
super().__init__(*args)
def mousePressEvent(self, event):
self.clicked.emit()
# example
app = QApplication([])
window = QWidget()
layout = QVBoxLayout(window)
labelA = ClickableLabel('Click on me for more.')
layout.addWidget(labelA)
labelB = QLabel('Here I am.')
layout.addWidget(labelB)
labelB.hide()
labelA.clicked.connect(labelB.show)
window.show()
app.exec_()
Or if you want an action instead, make it like this:
from PySide.QtGui import *
from PySide.QtCore import *
class ClickableLabel(QLabel):
"""
A Label that emits a signal when clicked.
"""
def __init__(self, *args):
super().__init__(*args)
def mousePressEvent(self, event):
self.action.triggered.emit()
# example
app = QApplication([])
window = QWidget()
layout = QVBoxLayout(window)
labelA = ClickableLabel('Click on me for more.')
layout.addWidget(labelA)
labelB = QLabel('Here I am.')
layout.addWidget(labelB)
labelB.hide()
action = QAction('Action', labelA)
labelA.action = action
action.triggered.connect(labelB.show)
window.show()
app.exec_()
The example are in Python 3.X notation and for PySide but translation to Python 2.X or PyQt is probably quite simple.

Resources