I am working on a media player (spotify clone?) and want to recreate the "meatball" menu found on songs and like the way it floats freely next to the meatball button and remains there.
My initial thoughts were to use a dialog, however that doesn't seem similar to the way spotify implemented the menu, as it isn't a standalone window. I then thought to use a "sub-box" widget to hold all my song widgets, then have a button within the song widget to act as an anchor(?) to create the menu.
I have created a small mock-up in a separate file to try and get this to work and have so far gotten the button to create a widget using a custom signal.
class subBox(QWidget):
def __init__(self, widgets: list[QWidget]):
super().__init__()
layout = QVBoxLayout()
for count, wi in enumerate(widgets):
layout.addWidget(wi)
self.setLayout(layout)
@Slot()
def addWid(self, item: testSong):
# TODO
'''
We have the song object that is part of the overall layout.
Now we can create the meatball menu, and slot it over the list of stuff?
'''
layout = self.layout()
idx = layout.indexOf(item)
if idx == -1:
print("Not present.")
return
button: testSongButton = item.findChild(testSongButton)
if button.findChild(testSongMeatball):
print("Meatball spawned.")
return
item.setStyleSheet("background-color: blue")
meatball = testSongMeatball(button)
meatball.setStyleSheet("background-color: red")
meatball.show()
subBox is the centralWidget of mainWindow and contains a testSong widget written as:
class testSong(QWidget):
meatballCreated = Signal(QWidget, name="meatballCreated")
meatballDestroyed = Signal(QWidget, name="meatballDestroyed")
def __init__(self, data={"name": "name", "album": "album"}):
super().__init__()
# song needs a label and a meatball?
# Horizontal
# init layout
layout = QHBoxLayout()
label = QLabel(data['name'])
album = QLabel(data['album'])
meatball = testSongButton("Meatball menu")
# connect clicked signal to relay into a meatballCreate signal, passing this widget
# as a param
meatball.clicked.connect(
lambda checked: self.meatballCreated.emit(self))
layout.addWidget(label)
layout.addWidget(album)
layout.addWidget(meatball)
self.setLayout(layout)
There is a lot of extra stuff to try to mimic my actual app but the main point is: testSong contains two labels and a button, which when clicked will "spawn" a meatball widget. subBox.addwid is a slot that receives a testSong widget and searches that widget for the button child and uses that button as the parent of the newly created meatball menu. I have tried using sizePolicy
, sizeHint
, updateGeometry
and more to get the meatball widget to move around within the button, but I cannot get it to move.
Here is how the window looks initially.
And here (ugly background colors) is after clicking the button to spawn the menu. I have resized the window to show how QLabel
and QPushButton
's sizePolicy
differ, and I have tried making meatball be the child of a QLabel
to see if that would help but it doesn't. I was hoping to resize the meatball widget to be bigger than the button widget and then offset it somehow, but realise now this is probably a dumb way to do it.
I think I will instead try to make the meatball widget a child of subBox and use the geometry of the button to reimplement a resizeEvent()
method of meatball, after looking around a bit. However I still would like to know why I am unable to move or resize the meatball widget as a child? Is there something I need to reimplement??
Any and all help would be welcome, and I hope I have formatted the question correctly and in an easy to read format.
import sys
from PySide6.QtCore import (Qt, QSize, QPoint, QRect,
Signal, SignalInstance, Slot,
)
from PySide6.QtWidgets import (QWidget, QPushButton, QLabel,
QHBoxLayout, QVBoxLayout,
QMainWindow, QApplication,
QSizePolicy)
class TestSong(QWidget):
meatballCreated = Signal(QWidget, name="meatballCreated")
meatballDestroyed = Signal(QWidget, name="meatballDestroyed")
def __init__(self):
super().__init__()
self.setStyleSheet("background-color: blue")
layout = QHBoxLayout(self)
label = QLabel("Label")
meatball = TestSongButton("Meatball button")
# connect clicked signal to relay into a meatballCreate signal, passing this widget
# as a param
meatball.clicked.connect(
lambda checked: self.meatballCreated.emit(self))
layout.addWidget(label)
layout.addWidget(meatball)
class TestSongButton(QPushButton):
def __init__(self, text):
super().__init__(text)
self.setStyleSheet("background-color: purple")
def resizeEvent(self, event):
return super().resizeEvent(event)
def moveEvent(self, event):
return super().moveEvent(event)
class testSongMeatball(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QHBoxLayout(self)
text = QLabel("Meatball Menu")
layout.addWidget(text)
def resizeEvent(self, event):
print("Meatball Resized!") # this never prints
return QWidget.resizeEvent(self, event)
def moveEvent(self, event):
print("Meatball Moved!") # this never prints
self.findPlace()
return super().moveEvent(event)
def findPlace(self):
p: SubBox = self.parent()
if p:
# find song item and store its position
item: TestSong = p.findChild(TestSong)
item_pos = item.pos()
# find button within song item and store its position
button: TestSongButton = item.findChild(TestSongButton)
button_pos = button.pos()
# since item sits inside SubBox, the meatball will have the same geometry starting point (I think?)
# So to find the button we need to add them
x,y = item_pos.x(), item_pos.y()
nx,ny, = button_pos.x(), button_pos.y()
# Add co-ordinates to get button's top left corner
meatball_pos = QPoint(x+nx,y+ny)
# Create a QRect with the new position and size of meatball (self)
meatball_geo = QRect(meatball_pos, self.size())
self.setGeometry(meatball_geo)
def sizePolicy(self):
return QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
def sizeHint(self):
return QSize(70,50)
class SubBox(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet("background-color: green")
layout = QVBoxLayout(self)
self.setLayout(layout)
@Slot()
def addWid(self, item: TestSong):
layout = self.layout()
idx = layout.indexOf(item)
if idx == -1:
print("Not present.")
return
# If meatball already exists
if self.findChild(testSongMeatball):
print("Meatball already present.")
return
# Create meatball widget
meatball = testSongMeatball(self)
meatball.setStyleSheet("background-color: red")
meatball.findPlace() # find geometry
meatball.show() # show meatball
print("Meatball spawned")
class TestWindow(QMainWindow):
def __init__(self):
super().__init__()
window_widget = QWidget()
window_layout = QVBoxLayout(window_widget)
# Give total layout a label
window_layout.addWidget(QLabel("Window"))
# Create sub-box
sub = SubBox()
window_layout.addWidget(sub)
test_widget = TestSong()
sub.layout().addWidget(test_widget)
test_widget.meatballCreated.connect(
lambda item: sub.addWid(item))
# set window as central
self.setCentralWidget(window_widget)
print(self.layout())
def main():
# Create app
app = QApplication(sys.argv)
window = TestWindow()
window.show()
app.exec()
if __name__ == '__main__':
main()
Your first attempt is ineffective, because the visibility of children is always restricted by the geometry of their parent, which acts as a viewport (similarly to a physical window): you cannot show a child outside the bounding rect of the parent widget.
If you want to do the above, you need to make the widget a child of the parent in which it can be fully shown, which in cases like this usually means the top level widget (the "window").
Yet, since the widget is not managed by a layout, you need to properly resize it, which cannot just rely on the widget's size()
.
All new widgets have a default size, unless an explicit minimum or maximum (or fixed) size is set.
When created without a parent, that size is 640x480
, but when created with
a parent, it is 100x30
, which is exactly the size you see, and that's also because you're calling self.size()
to set the new geometry.
Since the widget is not managed by a layout, it's up to you to resize it to the required dimensions. You've already overridden sizeHint()
, so the solution is relatively simple:
meatball_geo = QRect(meatball_pos, self.sizeHint())
self.setGeometry(meatball_geo)
A conceptually identical alternative is to rely on adjustSize()
, which automatically resizes the widget based on its sizeHint()
(if it's valid): so, just call move()
, followed by adjustSize()
.
self.move(meatball_pos)
self.adjustSize()
Note that the above won't solve two important aspects:
The only appropriate way to fix those issues is by making the popup a child of the top level window, and install an event filter on that window, which will then check for Resize
events and eventually update the popup geometry, but the function that sets that geometry must also ensure that the popup rectangle is still within the window bounding rect.
Finally, sizePolicy
is a property, trying to override its function is not only useless, but quite inappropriate.
The only way to set the size policy of a widget is by properly calling setSizePolicy()
. Remember that only functions labeled as "virtual
" in the official C++ docs can be overridden.