Overlay widget partially outside the bounds of it's parent - python

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.

Related

How to change the opacity of a PyQt5 window

I have just been experimenting with some animation in PyQt5 and am looking to animate the opacity of a window. I have had success with animating the opacity of buttons and QWidgets withink the window, however when I try to apply the same concept to the main QWidget class it doesn't seem to work. Below is my code for trying to get the animation to work on the window (I am aware this isn't the best looking code but I'm just trying to experiment, but also feel free to tell me about any big errors unrelated to the problem as well as I am also quite new to PyQt5):
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
class Test(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet("background: green")
self.setGeometry(100, 100, 400, 400)
self.b = QPushButton("Reduce", self, clicked=self.reduce)
self.b.show()
self.b.setStyleSheet("color: yellow")
self.show()
def reduce(self):
self.eff = QGraphicsOpacityEffect(self)
self.eff.setOpacity(1)
self.setGraphicsEffect(self.eff)
self.anim = QPropertyAnimation(self.eff, b"opacity")
self.anim.setDuration(500)
self.anim.setStartValue(1)
self.anim.setEndValue(0)
self.anim.start()
self.t = QTimer()
self.t.timeout.connect(self.loop)
self.t.start(10)
print(self.eff.opacity())
def loop(self):
self.update()
print(self.eff.opacity())
if self.anim.currentValue() == 0:
self.t.stop()
self.update()
if __name__ == "__main__":
window = QApplication(sys.argv)
app = Test()
window.exec_()
I thought maybe I am getting odd results as it isn't possible to do this when the widget is made to be the main widget in the window, however I also don't see why that would be a thing. What seems to happen for me is that the window does nothing and nothing changes, other than the button seems to half freeze, as in I am able to click it and get a result (run the function it is connected too) however no pressing down animation occurs as normally would. However, when I resize the window it will show a change, however this has to be manual as I added a basic loop to constantly resize and it did nothing. And finally when I resize the window, or change the opacity just by changing the opacity of self.eff it changes, but it just makes it all black even at values of 0.8, etc which is what leads me to believe it just isn't possible and some script is defaulting it to black, as the button will work fine while the rest goes black. Any help is appreciated.
Try like this:
import sys
from PyQt5.Qt import *
class Test(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet("background: green;")
self.resize(400, 400)
self.label = QLabel()
self.pixmap = QPixmap('Ok.png')
self.label.setPixmap(self.pixmap)
self.label.setAlignment(Qt.AlignCenter)
self.label.setStyleSheet("background: #CD113B;")
self.b = QPushButton("Reduce", self, clicked=self.reduce)
self.b.setStyleSheet("background: blue; color: yellow;")
layout = QVBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.b)
def reduce(self):
self.anim = QPropertyAnimation(self, b"opacity")
self.anim.setDuration(3000)
self.anim.setLoopCount(3)
self.anim.setStartValue(0.0)
self.anim.setEndValue(1.0)
self.anim.start()
def windowOpacity(self):
return super().windowOpacity()
def setWindowOpacity(self, opacity):
super().setWindowOpacity(opacity)
opacity = pyqtProperty(float, windowOpacity, setWindowOpacity)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = Test()
w.show()
sys.exit(app.exec_())
or so:
import sys
from PyQt5.Qt import *
class Test(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet("background: green;")
self.resize(400, 400)
self.label = QLabel()
self.pixmap = QPixmap('Ok.png')
self.label.setPixmap(self.pixmap)
self.label.setAlignment(Qt.AlignCenter)
self.label.setStyleSheet("background: #CD113B;")
self.b = QPushButton("Reduce", self, clicked=self.reduce)
self.b.setStyleSheet("background: blue; color: yellow;")
layout = QVBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.b)
def reduce(self):
self.eff = QGraphicsOpacityEffect()
self.eff.setOpacity(1.0)
self.label.setGraphicsEffect(self.eff)
self.anim = QPropertyAnimation(self.eff, b"opacity")
self.anim.setDuration(3000)
self.anim.setLoopCount(3)
self.anim.setStartValue(0.0)
self.anim.setEndValue(1.0)
self.anim.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = Test()
w.show()
sys.exit(app.exec_())
Ok.png

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)

PyQt5: setParent on QPushButton does not add it to parent

In reference to this answer on adding a new tab button to QTabWidget,
I am unsure where the QPushButton is added to the QTabBar.
I assume the setParent method on the pushButton adds it to the tab bar.
But when I try to implement it, the pushButton doesnt seem to appear anywhere on the tab bar even if I add hard values to the move operation.
Here is a minimum reproducible example,
from PyQt5 import QtGui, QtCore, QtWidgets
class TabBarPlus(QtWidgets.QTabBar):
"""Tab bar that has a plus button floating to the right of the tabs."""
plusClicked = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
# Plus Button
self.plusButton = QtWidgets.QPushButton("+")
self.plusButton.setParent(self)
self.plusButton.setFixedSize(20, 20) # Small Fixed size
self.plusButton.clicked.connect(self.plusClicked.emit)
self.movePlusButton() # Move to the correct location
# end Constructor
def sizeHint(self):
"""Return the size of the TabBar with increased width for the plus button."""
sizeHint = QtWidgets.QTabBar.sizeHint(self)
width = sizeHint.width()
height = sizeHint.height()
return QtCore.QSize(width+25, height)
# end tabSizeHint
def resizeEvent(self, event):
"""Resize the widget and make sure the plus button is in the correct location."""
super().resizeEvent(event)
self.movePlusButton()
# end resizeEvent
def tabLayoutChange(self):
"""This virtual handler is called whenever the tab layout changes.
If anything changes make sure the plus button is in the correct location.
"""
super().tabLayoutChange()
self.movePlusButton()
# end tabLayoutChange
def movePlusButton(self):
"""Move the plus button to the correct location."""
# Find the width of all of the tabs
size = sum([self.tabRect(i).width() for i in range(self.count())])
# size = 0
# for i in range(self.count()):
# size += self.tabRect(i).width()
# Set the plus button location in a visible area
h = self.geometry().top()
w = self.width()
if size > w: # Show just to the left of the scroll buttons
self.plusButton.move(w-54, h)
else:
self.plusButton.move(size, h)
# end movePlusButton
# end class MyClass
class CustomTabWidget(QtWidgets.QTabWidget):
"""Tab Widget that that can have new tabs easily added to it."""
def __init__(self, parent=None):
super().__init__(parent)
# Tab Bar
self.tab = TabBarPlus()
self.setTabBar(self.tab)
# Properties
self.setMovable(True)
self.setTabsClosable(True)
# Signals
self.tab.plusClicked.connect(self.addTab)
# self.tab.tabMoved.connect(self.moveTab)
# self.tabCloseRequested.connect(self.removeTab)
# end Constructor
# end class CustomTabWidget
class AppDemo(QtWidgets.QMainWindow):
def __init__(self):
super(AppDemo, self).__init__()
self.centralwidget = QtWidgets.QWidget(self)
self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
self.horizontalLayout.setContentsMargins(0, -1, 0, -1)
self.playlist_manager = CustomTabWidget(self.centralwidget)
self.horizontalLayout.addWidget(self.playlist_manager)
blankWidget = QtWidgets.QWidget(self.playlist_manager)
self.playlist_manager.addTab(blankWidget, "New")
self.setCentralWidget(self.centralwidget)
self.show()
# end class AppDemo
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = AppDemo()
w.setWindowTitle('AppDemo')
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Expected behvaiour is that a "+" button appears at the right of all the tabs,
but no such button appears.
Okay so after some brainstorming, I figured the issue out. Unlike PyQt4. The QTabBar width does not span the entire width of the QTabWidget, and as such the PlusButton when moved to the right of all the tabs will exceed the width of the parent widget and disappear.
The Solution to this is to add the QPushButton in the QTabWidget itself and emit layoutchange from the QTabBar.
Here is a working example, I have modified values to fit my use case
class tabBarPlus(QTabBar):
layoutChanged = pyqtSignal()
def resizeEvent(self, event):
super().resizeEvent(event)
self.layoutChanged.emit()
def tabLayoutChange(self):
super().tabLayoutChange()
self.layoutChanged.emit()
class customTabWidget(QTabWidget):
plusClicked = pyqtSignal()
def __init__(self, parent=None):
super(customTabWidget, self).__init__(parent)
self.tab = tabBarPlus()
self.setTabBar(self.tab)
self.plusButton = QPushButton('+', self)
self.plusButton.setFixedSize(35, 25)
self.plusButton.clicked.connect(self.plusClicked.emit)
self.setMovable(True)
self.setTabsClosable(True)
self.tab.layoutChanged.connect(self.movePlusButton)
def movePlusButton(self):
size = sum([self.tab.tabRect(i).width() for i in range(self.tab.count())])
h = max(self.tab.geometry().bottom() - 24, 0)
w = self.tab.width()
print(size, w, h)
if size > w:
self.plusButton.move(w-self.plusButton.width(), h)
else:
self.plusButton.move(size-2, h)

Prevent clipping the absolute positioned child widget by the parent

I'm trying to create a layout where is existing a row, and this row should contain an absolute positioned button which should be placed outside of this row.
Here is the simple schema
I did it by just pushing a child button into the parent button (I'm not sure that it's a correct solution) and moved it to some absolute coordinates.
It works but, unfortunately, the child button is clipping by the parent. So it's like overflow: hidden in CSS. But in case of QT I couldn't found how to disable this behavior.
Here is the demo of my current QUI
Is there exists any way to solve it? Or should I just use some widget combination with the empty spacer etc.?
btn = QPushButton("button")
test = QPushButton("X")
test.setParent(btn)
test.move(200, 5)
self.layout.addWidget(btn)
Full code of the UI class (minimal reproducible example)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
btn = QPushButton("button")
test = QPushButton("X")
test.setParent(btn)
test.move(200, 5)
self.layout.addWidget(btn)
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,74)
self.layout.setSpacing(0)
# self.layout.addStretch(-1)
self.setMinimumSize(640,400)
self.setWindowFlags(Qt.FramelessWindowHint)
Sorry, but the advice of #Heike is absolutely correct and you should not look for wrong solutions.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setMinimumSize(640,400)
self.setWindowFlags(Qt.FramelessWindowHint)
btn = QPushButton("button")
test = QPushButton("X")
test.setParent(btn)
# test.move(200, 5)
# self.layout = QVBoxLayout()
self.layout = QGridLayout()
self.layout.addWidget(btn, 0, 0, 1, 10)
self.layout.addWidget(test, 0, 11, 1, 1)
self.layout.setContentsMargins(0,0,0,74)
self.layout.setSpacing(0)
self.setLayout(self.layout)
if __name__ == '__main__':
import sys
application = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(application.exec_())

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

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_())

Categories

Resources