Qt.ScrollBarAsNeeded not showing scrollbar when it's actually needed - python

I'm implementing a python application using PyQt5 and I encountered some problems when making use of a QScrollArea. This is the layout of my application:
It's composed of 2 QScrollArea (left and right pane) and a QMdiArea (center widget) arranged into a QHBoxLayout. When I expand the widgets on the left pane by clicking on the controls, and the height of the QWidget of the QScrollArea is bigger than then height of the QScrollArea itself, the scrollbar appears (as expected), but it's overlapping the content of the QScrollArea. To fix this problem I reimplemented the resizeEvent adding the necessary space for the scrollbar (till this point everything works.
Now, when I manually resize the Main Window, the left Pane gets more space and the scrollbar should disappear (but it doesn't) and it overlaps the widgets of the pane:
I also tried to manually toggle the visibility of the scrollbar (when the resizeEvent is received): when I do this, I can successfully hide the scrollbar but then I can't show it again (not matter if I call setVisible(True) on the scrollbar). This results in the space for the scrollbar being added, but the scrollbar is missing and the content of the pane is not scrollable:
Here is the implementation of the pane widget:
class Pane(QScrollArea):
MinWidth = 186
def __init__(self, alignment=0, parent=None):
super().__init__(parent)
self.mainWidget = QWidget(self)
self.mainLayout = QVBoxLayout(self.mainWidget)
self.mainLayout.setAlignment(alignment)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
self.setContentsMargins(0, 0, 0, 0)
self.setFrameStyle(QFrame.NoFrame)
self.setFixedWidth(Pane.MinWidth)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored)
self.setWidgetResizable(True)
self.setWidget(self.mainWidget)
def resizeEvent(self, resizeEvent):
if self.viewport().height() < self.widget().height():
self.setFixedWidth(Pane.MinWidth + 18)
# THIS DOESN'T WORK
#self.verticalScrollBar().show()
else:
self.setFixedWidth(Pane.MinWidth)
#self.verticalScrollBar().hide()
def addWidget(self, widget):
self.mainLayout.addWidget(widget)
def removeWidget(self, widget):
self.mainLayout.removeWidget(widget)
def update(self, *__args):
for item in itemsInLayout(self.mainLayout):
item.widget().update()
super().update(*__args)
What I want to achieve is pretty simple (but practically it seems not as simple): I would like to dynamically show the vertical scrollbar on my left/right pane widgets only when it's needed, and add the necessary space for the scrollbar so it doesn't overlap the widgets in the QScrollArea.
Before someone asks, I already tried to do something like this:
def resizeEvent(self, resizeEvent):
if self.viewport().height() < self.widget().height():
self.setFixedWidth(Pane.MinWidth + 18)
scrollbar = self.verticalScrollbar()
scrollbar.setVisible(True)
self.setVerticalScrollBar(scrollbar) ## APP CRASH
else:
self.setFixedWidth(Pane.MinWidth)
#self.verticalScrollBar().hide()
which results in my application to crash.
I hope that someone already faced this issue and is able to help me.
EDIT: I'm using PyQt5.5 compiled against Qt5.5 under OSX Yosemite 10.10.4 using clang.

Everything seems to work as expected for me without any need for workarounds. However, I strongly suspect there are additional constraints in your real code that you have not revealed in your question.
UPDATE
Below is a simple example that resizes the scrollareas when the scrollbars are shown/hidden:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QMainWindow):
def __init__(self):
super(Window, self).__init__()
widget = QtWidgets.QWidget(self)
layout = QtWidgets.QHBoxLayout(widget)
self.mdi = QtWidgets.QMdiArea(self)
self.leftScroll = Pane(
QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
self.rightScroll = Pane(
QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
layout.addWidget(self.leftScroll)
layout.addWidget(self.mdi)
layout.addWidget(self.rightScroll)
self.setCentralWidget(widget)
for scroll in self.leftScroll, self.rightScroll:
for index in range(4):
widget = QtWidgets.QTextEdit()
widget.setText('one two three four five')
scroll.addWidget(widget)
class Pane(QtWidgets.QScrollArea):
MinWidth = 186
def __init__(self, alignment=0, parent=None):
super().__init__(parent)
self.mainWidget = QtWidgets.QWidget(self)
self.mainLayout = QtWidgets.QVBoxLayout(self.mainWidget)
self.mainLayout.setAlignment(alignment)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
self.setContentsMargins(0, 0, 0, 0)
self.setFrameStyle(QtWidgets.QFrame.NoFrame)
self.setFixedWidth(Pane.MinWidth)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Ignored)
self.setWidgetResizable(True)
self.setWidget(self.mainWidget)
self.verticalScrollBar().installEventFilter(self)
def addWidget(self, widget):
self.mainLayout.addWidget(widget)
def removeWidget(self, widget):
self.mainLayout.removeWidget(widget)
def eventFilter(self, source, event):
if isinstance(source, QtWidgets.QScrollBar):
if event.type() == QtCore.QEvent.Show:
self.setFixedWidth(Pane.MinWidth + source.width())
elif event.type() == QtCore.QEvent.Hide:
self.setFixedWidth(Pane.MinWidth)
return super(Pane, self).eventFilter(source, event)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 300)
window.show()
sys.exit(app.exec_())

Related

QPaintEvent event rect for scrollable widget

I have a QPaintEvent override for a custom widget that has a fixed size set. This fixed size can change per instance but in this simple example, ive set it. however the PaintEvent doesn't take it into account so when the users scrolls to the right the rectangle shouldn't paint rounded corners since the widget extends past the visible viewport. How do i fix this?
Full widget painted correctly...
When i resize dialog and scroll right, you'll see rounded corners appear on the left side... when it should NOT.
They should look like this...
Code
import os
import sys
from PySide2 import QtGui, QtWidgets, QtCore, QtSvg
class Card(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Card, self).__init__(parent=parent)
self.label = QtWidgets.QLabel('Help This Paint Event Is Broken')
self.label.setFixedHeight(40)
self.label.setFixedWidth(300)
self.mainLayout = QtWidgets.QVBoxLayout(self)
self.mainLayout.addWidget(self.label)
# overrides
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
painter.setOpacity(1.0)
painter.setRenderHints(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QColor(0, 0, 0, 128))
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtGui.QColor('#F44336'))
painter.drawRoundedRect(event.rect(), 12, 12)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.end()
class ListViewExample(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ListViewExample, self).__init__(parent)
self.resize(200,200)
self.listView = QtWidgets.QListWidget()
self.listView.setSpacing(10)
self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.listView.verticalScrollBar().setSingleStep(10)
# layout
self.mainLayout = QtWidgets.QVBoxLayout()
self.mainLayout.setContentsMargins(0,0,0,0)
self.mainLayout.addWidget(self.listView)
self.setLayout(self.mainLayout)
for x in range(50):
wgt = Card()
self.appendItem(wgt)
def appendItem(self, widget):
lwi = QtWidgets.QListWidgetItem()
lwi.setSizeHint(widget.sizeHint())
self.listView.addItem(lwi)
self.listView.setItemWidget(lwi, widget)
################################################################################
# Widgets
################################################################################
def unitTest_CardDelegate():
app = QtWidgets.QApplication(sys.argv)
window = ListViewExample()
window.show()
app.exec_()
if __name__ == '__main__':
pass
unitTest_CardDelegate()
QPaintEvent::rect() returns the visible rectangle, not the rectangle of the widget itself, so you observe this behavior. The solution is:
painter.drawRoundedRect(self.rect(), 12, 12)

Overlay widget partially outside the bounds of it's parent

I'm trying to create a compound widget similiar to the following:
A Rectangle overlayed with a button that is partially outside the rectangle's bounds.
Here is the code corresponding to that image:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QPoint
from PyQt5.QtGui import QResizeEvent
class MyWidget(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
self.lbl = QLabel()
self.lbl.setStyleSheet('background: #EE6622')
self.lbl.setFixedSize(125, 150)
self.layout.addWidget(self.lbl)
self.btn = QPushButton(parent=self)
self.btn.setStyleSheet('background: #ABCDEF')
self.btn.setFixedSize(25, 25)
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
self.update_btn_pos()
def update_btn_pos(self):
pos = (
self.lbl.pos() +
QPoint(
self.lbl.rect().width() - int(self.btn.width() / 2),
-int(self.btn.height() / 2))
)
self.btn.move(pos)
if __name__ == "__main__":
a = QApplication(sys.argv)
window = MyWidget()
window.show()
a.exec()
My problem is that the widget's behaviour when resizing suggests that the button is not really "part of" that widget - it is cut-off as if it weren't there:
I tried to overwrite the sizeHint()-method to include the button, but that only solves the problem on startup, I can still resize the window manually to cut the button off again.
What must be changed in order to make this work?
I think I might have found a solution myself by adding the following to the __init__ - method:
self.layout.setContentsMargins(
0,
int(self.btn.height() / 2),
int(self.btn.width() / 2),
0
)
By setting the contentsMargin, the size of the big rectangle doesn't change because it is fixed and the parent widget still covers the space under the button:
I'm not sure if this is the *right way* to do it though ...
Alright, thanks to #musicamante this is the final code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QResizeEvent
class MyWidget(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.lbl = QLabel()
self.lbl.setStyleSheet('background: #EE6622')
self.lbl.setFixedSize(125, 150)
self.layout.addWidget(self.lbl)
self.btn = QPushButton(parent=self)
self.btn.setStyleSheet('background: #ABCDEF')
self.btn.setFixedSize(25, 25)
# set contents margin of layout to half the button's size
self.layout.setContentsMargins(
*([int(self.btn.height() / 2), int(self.btn.width() / 2)]*2)
)
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
self.update_btn_pos()
def update_btn_pos(self):
rect = self.btn.rect()
rect.moveCenter(self.lbl.geometry().topRight())
self.btn.move(rect.topLeft())
if __name__ == "__main__":
a = QApplication(sys.argv)
window = MyWidget()
window.show()
a.exec()
Result:
On paint event widget paints itself and all of his children clipped to his bounds. You can try to set button parent to MyWidget's parent, but you'll still have problem of button blocking part of some other widget or clipping on window's client area.
On the other hand there is no much difference between hovering button thats inside parent's widget and hovering button that sticks out, messing with other widgets.

Getting the tops of side-by-side widgets to align using PySide

In the code below, the top of the QTextEdit and QGraphicsView widgets are not aligned when using QHBoxLayout. However, if you comment out QTextEdit and uncomment the other QGraphicsView setup, the top of the widgets align perfectly. Here are my questions:
What causes this alignment issue to occur and how can it be fixed?
Are issues like this best avoided by using Qt Creator?
Is the whole QGraphicsView() --> QGraphicsScene() --> QWidget() necessary to place graphics next to other widgets?
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__()
# Create Widget1
widget1 = QTextEdit()
#widget1 = QWidget()
#view1 = QGraphicsView()
#scene1 = QGraphicsScene(0,0,200,500)
#view1.setScene(scene1)
#layout = QHBoxLayout()
#layout.addWidget(view1)
#widget1.setLayout(layout)
# Create Widget2
widget2 = QWidget()
view2 = QGraphicsView()
scene2 = QGraphicsScene(0,0,200,500)
view2.setScene(scene2)
layout = QHBoxLayout()
layout.addWidget(view2)
widget2.setLayout(layout)
# Layout of Side by Side windows
container = QWidget()
layout = QHBoxLayout()
layout.addWidget(widget1)
layout.addWidget(widget2)
container.setLayout(layout)
# Scroll Area Properties
scroll = QScrollArea()
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setWidgetResizable(False)
scroll.setWidget(container)
# Scroll Area Layer add
vLayout = QVBoxLayout(self)
vLayout.addWidget(scroll)
self.setLayout(vLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
dialog = Widget()
dialog.show()
app.exec_()
The layouts have a default margin. So if one widget is in a layout, and its neighbour is not, they will not be aligned. To remove the default margin, you can do this:
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
However, in your example, the container widget and layout for the QGraphicsView aren't doing anything useful. So you could remove those, and along with some other simplifications, arrive at this:
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__()
widget1 = QTextEdit()
widget2 = QGraphicsView()
widget2.setScene(QGraphicsScene(0, 0, 200, 500, widget2))
container = QWidget()
layout = QHBoxLayout(container)
layout.addWidget(widget1)
layout.addWidget(widget2)
scroll = QScrollArea()
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setWidgetResizable(False)
scroll.setWidget(container)
vLayout = QVBoxLayout(self)
vLayout.addWidget(scroll)
Using Qt Designer is certainly very useful when experimenting with the layouts of a complex application. However, the code it generates is usually quite verbose compared with what you can achieve when coding by hand. For long-term maintainability, though, using Qt Designer seems the best option.

How can I make a PyQt widget resizable by dragging?

I have a QScrollArea containing a widget with a QVBoxLayout. Inside this layout are several other widgets. I want the user to be able to drag the lower borders of those widgets to resize them in the vertical direction. When they are resized, I don't want them to "steal" size from the other widgets in the scrolling area; instead I want the entire scrolled "page" to change its size. So if you enlarge one of the widgets, it should push the other widgets down (out of the viewport of the scroll area); if you shrink it, it should pull the other widgets up. Dragging the border of one widget should not change the size of any of the other widgets in the vertical scroll; it should just move them.
I began by using a QSplitter. If I use that, I can drag to change the size of a widget, but there doesn't seem to be a way to get it to "push/pull" the others as I described above, rather than growing/shrinking them. But I can't find any other way to give a widget a draggable handle that will allow me to change its size. How can I accomplish this?
Here is a simple example of what I'm doing. (In this example I've commented out the splitter, but if you uncomment it you can see what happens with that version.)
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.Qsci import QsciScintilla, QsciLexerPython
class SimplePythonEditor(QsciScintilla):
def __init__(self, parent=None):
super(SimplePythonEditor, self).__init__(parent)
self.setMinimumHeight(50)
class Chunk(QFrame):
def __init__(self, parent=None):
super(Chunk, self).__init__(parent)
layout = QVBoxLayout(self)
sash = QSplitter(self)
layout.addWidget(sash)
sash.setOrientation(Qt.Vertical)
editor = self.editor = SimplePythonEditor()
output = self.output = SimplePythonEditor()
output.setReadOnly(True)
sash.addWidget(editor)
sash.addWidget(output)
self.setLayout(layout)
print(self.sizePolicy())
class Widget(QWidget):
def __init__(self, parent= None):
global inout
super(Widget, self).__init__()
#Container Widget
widget = QWidget()
#Layout of Container Widget
layout = QVBoxLayout(self)
#sash = QSplitter(self)
#layout.addWidget(sash)
#sash.setOrientation(Qt.Vertical)
for num in range(5):
editor = SimplePythonEditor()
editor.setText("Some stuff {}".format(num))
layout.addWidget(editor)
#sash.addWidget(editor)
widget.setLayout(layout)
#Scroll Area Properties
scroll = QScrollArea()
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setWidgetResizable(True)
scroll.setWidget(widget)
scroll.setMaximumHeight(500)
#Scroll Area Layer add
vLayout = QVBoxLayout(self)
vLayout.addWidget(scroll)
self.setLayout(vLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
dialog = Widget()
dialog.show()
app.exec_()
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.setWindowTitle("MainWindow")
MainWindow.resize(500, 500)
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
MainWindow.setCentralWidget(self.centralwidget)
QMetaObject.connectSlotsByName(MainWindow)
class Ewindow(QMainWindow,QApplication):
"""docstring for App"""
resized = pyqtSignal()
def __init__(self,parent):
super(Ewindow,self).__init__(parent=parent)
self.setGeometry(500, 500, 800,800)
self.setWindowTitle('Mocker')
self.setWindowIcon(QIcon('icon.png'))
self.setAttribute(Qt.WA_DeleteOnClose)
ui2 = Ui_MainWindow()
ui2.setupUi(self)
self.resized.connect(self.readjust)
def resizeEvent(self, event):
self.resized.emit()
return super(Ewindow, self).resizeEvent(event)
def readjust(self):
self.examForm.move(self.width()-self.examForm.width(),0)
self.btn_skip.move(self.width()-self.btn_skip.width(),self.height()-100)
self.btn_next.move(self.btn_showAnswers.x()+self.btn_showAnswers.width(),self.height()-100)
self.btn_prev.move(0,self.height()-100)
self.btn_showAnswers.move(self.btn_prev.x()+self.btn_prev.width(),self.height()-100)
self.btn_home.move(self.width()-200,self.height()-150)
self.lbscreen1.resize(self.width()-self.examForm.width(),self.height()-200)
self.examForm.resize(200,self.height()-150)
self.btn_skip.resize(self.examForm.width(),100)
self.btn_next.resize(self.btn_prev.width(),100)
self.btn_prev.resize(self.width()*0.25,100)
self.btn_showAnswers.resize(self.btn_prev.width(),100)
self.btn_home.resize(200,50)
here is an example code of a resizable window it moves and stretches widgets as you resize the window. The idea is to keep widget coordinates and sizes relative to each other.
so i had to make a class Ui_MainWindow and set it for my window class ui2.setupUi(self) and also declare the resized = pyqtSignal() which i'd be using to run the readjust function which resets size and coordinates of the widgets like so self.resized.connect(self.readjust).
i hope this helps!

How to add a second pixmap to an abstract button when pushed in PySide/PyQt

I'm very new to PySide/PyQt environment. I'm trying to make a menu of buttons on top and assign a task to each so that when they are clicked a function draws a painting on the central window. But I also want to make the button change when they are clicked.
I think this might be an straighforward problem to solve if I use QPushButton, but my buttons are images and I'm using the method suggested HERE and use QAbstractButton to create them.
It is mentioned there that
You can add second pixmap and draw it only when the mouse pointer is
hover over button.
And I'm trying to do exactly that. My question is this:
what are possible ways to achieve this? Are the same methods in QPushButtons applicable here? If so, are there any examples of it somewhere?
Here is a snippet of my code:
import sys
from PySide import QtGui, QtCore
BACKGROUND_COLOR = '#808080'
ICON_PATH_ACTIVE = 'icons/activ'
ICON_PATH_PASSIVE = 'icons/pasiv'
class MainWindow(QtGui.QMainWindow):
def __init__(self, app=None):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
dockwidget = QtGui.QWidget()
self.setGeometry(200, 200, 400, 300)
hbox = QtGui.QHBoxLayout()
1_button = PicButton(QtGui.QPixmap("icons/pasiv/1.png"))
2_button = PicButton(QtGui.QPixmap("icons/pasiv/2.png"))
3_button = PicButton(QtGui.QPixmap("icons/pasiv/3.png"))
hbox.addWidget(1_button)
hbox.addWidget(2_button)
hbox.addWidget(3_button)
vbox = QtGui.QVBoxLayout()
vbox.addLayout(hbox)
vbox.setAlignment(hbox, QtCore.Qt.AlignTop)
dockwidget.setLayout(vbox)
self.setCentralWidget(dockwidget)
class PicButton(QtGui.QAbstractButton):
def __init__(self, pixmap, parent=None):
super(PicButton, self).__init__(parent)
self.pixmap = pixmap
self.setFixedSize(100, 100)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(event.rect(), self.pixmap)
def main():
app = QtGui.QApplication(sys.argv)
central = MainWindow()
central.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Thank you.
Use a regular QPushButton with an icon.
iplay = QtGui.QIcon("path/play_icon.png")
ipause = QtGui.QIcon("path/pause_icon.png")
btn = QtGui.QPushButton(ipause, "", None)
def toggle_play():
if btn.icon() == ipause:
btn.setIcon(iplay)
# Do Pause Action
else:
btn.setIcon(ipause)
# Do Play Action
btn.clicked.connect(toggle_play)
btn.show()
If you want hover functionality then you will have to subclass the QPushButton
class MyButton(QtGui.QPushButton):
custom_click_signal = QtCore.Signal()
def enterEvent(self, event):
super().enterEvent(event)
# Change icon hove image here
def leaveEvent(self, event):
super().leaveEvent(event)
# Change icon back to original image here.
def mousePressEvent(self, event):
super().mousePressEvent(event)
self.custom_click_signal.emit()
# connect to signal btn.custom_click_signal.connect(method)
Icons are probably the easiest way instead of manually managing the paint event. There are also mousePressEvent and mouseReleaseEvents if you want the icon to change for someone holding the button down.

Categories

Resources