I have the following
class MyView(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QVBoxLayout()
layout.addWidget(QLabel('Hello World'))
self.setLayout(layout)
class NavigationMenu(QWidget):
pass
# Renders a bar of full width and 15 px height
What is the easiest way to add the NavigationMenu to MyView?
In the future, I would have to also add the NavigationMenu to all other Views, so I am looking for something scalable from a typing and maintainability stand point.
I tried decorators (just #NavigationMenuDecorator on top of the class), but I either cannot bind them or they get initialized at parse time and error QWidget: Must construct a QApplication before a QWidget.
I tried just adding it into MyView, but there is a lot of boilerplate
class MyWidget(Widget.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = Widget.QVBoxLayout()
layout.addWidget(QLabel('Hello World'))
topLayout = Widget.QVBoxLayout()
topLayout.setContentsMargins(0, 0, 0, 0)
topLayout.addWidget(NavigationMenu())
topLayout.addLayout(layout)
self.setLayout(topLayout)
A possible solution is to use metaclass:
from PyQt5 import QtCore, QtWidgets
class NavigationMenu(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(QtWidgets.QLabel("NavigationMenu"))
class MetaNavigationMenu(type(QtWidgets.QWidget), type):
def __call__(cls, *args, **kw):
obj = super().__call__(*args, **kw)
lay = obj.layout()
if lay is not None:
lay.addWidget(NavigationMenu())
return obj
class View(QtWidgets.QWidget, metaclass=MetaNavigationMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(QtWidgets.QLabel('Hello World'))
self.setLayout(layout)
if __name__=="__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = View()
w.show()
sys.exit(app.exec_())
Update:
With the following method you can inject the view and the additional arguments that the view requires:
from PyQt5 import QtCore, QtWidgets
class NavigationMenu(QtWidgets.QWidget):
def __init__(self, value, text="", parent=None):
super().__init__(parent)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(QtWidgets.QLabel(text))
print(value)
class MetaMenu(type(QtWidgets.QWidget), type):
def __new__(cls, class_name, parents, attrs, **kwargs):
cls._view = kwargs.pop('view', None)
cls._args = kwargs.pop('args', tuple())
cls._kwargs = kwargs.pop('kwargs', dict())
return type.__new__(cls, class_name, parents, attrs)
def __call__(cls, *args, **kw):
obj = super().__call__(*args, **kw)
layout = getattr(obj, 'layout', None)
if callable(layout) and View is not None:
layout().addWidget(cls._view(*cls._args, **cls._kwargs))
return obj
class View(QtWidgets.QWidget, metaclass=MetaMenu, view=NavigationMenu, args=(10, ), kwargs={"text": "NavigationMenu"}):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(QtWidgets.QLabel('Hello World'))
self.setLayout(layout)
if __name__=="__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = View()
w.show()
sys.exit(app.exec_())
The other solution here is amazing and I learned a lot about metaclasses. However, it is quite hard to read and adds unnecessary complexity. I settled for a composition-based approach, where I just extracted the boilerplate to a separate function.
The add_navigation() function wraps the old layout in a widget, creates a QVBoxLayout with the NavigationMenu and the old layout, and finally swaps the layouts.
def add_navigation(widget, title)
main = QWidget()
main.setLayout(widget.layout())
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(NavigationBar(title))
layout.addWidget(main)
widget.setLayout(layout)
We then have just a 1-liner of boilerplate and the code then becomes.
class MyView(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QVBoxLayout()
layout.addWidget(QLabel('Hello World'))
self.setLayout(layout)
add_navigation(self, 'Navigation Title')
class NavigationMenu(QWidget):
pass
# Renders a bar of full width and 15 px height
Related
I have a Main window that has a settings window that pops out. I want to be able to change one of the settings and it affects the Main window
So far I have
mainwindow.py (the function I want to run to make a frame visible)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.button_open_settings.clicked.connect
(self.open_settings)
def open_settings(self):
self.settings_window = settings.MainWindow_settings(self)
self.settings_window.show()
def set_toggle_vis(self, toggle):
self.ui.frame_toggle_list.setVisible(toggle)
settings window.py
class MainWindow_settings(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow_settings, self).__init__(*args,**kwargs)
self.ui = settings_ui.Ui_Form()
self.ui.setupUi(self)
def change_toggle(self):
toggle_enabled = 1
from mainwindow import MainWindow
MWind = MainWindow()
MWind.set_toggle_vis(toggle_enabled)
This kinda works, but it doesn't set the visibility of the frame to 1 as it would if I ran it from the mainwindow, it created a whole new main window, so now 2 are open.
How do I get it to refresh the mainwindow rather than opening a new one?
Answering my own question (with the help of musicamante)
Instead of calling the function I use pyqtSignals to send a signal
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.button_open_settings.clicked.connect
(self.open_settings)
def open_settings(self):
self.settings_window = settings.MainWindow_settings(self)
self.settings_window.toggle_submitter.connect(self.set_toggle_vis)
self.settings_window.show()
def set_toggle_vis(self, toggle):
self.ui.frame_toggle_list.setVisible(toggle)
settings window.py
class MainWindow_settings(QMainWindow):
toggle_submitter = pyqtSignal(int)
def __init__(self, *args, **kwargs):
super(MainWindow_settings, self).__init__(*args,**kwargs)
self.ui = settings_ui.Ui_Form()
self.ui.setupUi(self)
def change_toggle(self)
toggle_enabled = 1
self.toggle_submitter.emit(toggle_enabled)
I'm currently trying myself at PyQT5 and tried to create a custom widget which contains a nested layout with some labels.
However, when I try to run the code there are no errors thrown but the window stays blank.
What could be the problem here?
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setWindowTitle("This is a test")
devicewidget = DeviceWidget()
self.setCentralWidget(devicewidget)
class DeviceWidget(QWidget):
def __init__(self, *args, **kwargs):
super(DeviceWidget, self).__init__(*args, **kwargs)
layout = QVBoxLayout()
save_image_btn = QPushButton("Save Image")
restore_image_btn = QPushButton("Install Image")
device_size_layout = QHBoxLayout()
device_size_desc_lbl = QLabel("Space:")
device_size_lbl = QLabel("69420MB")
device_size_layout.addWidget(device_size_desc_lbl)
device_size_layout.addWidget(device_size_lbl)
layout.addWidget(save_image_btn)
layout.addWidget(save_image_btn)
layout.addLayout(device_size_layout)
#Initialization
app = QApplication([])
window = MainWindow()
window.show()
app.exec_()
Just to be clear, this is what I am currently trying to accomplish:
The solution was that I forgot to set the layout in the DeviceWidget class.
self.setLayout(layout) or layout= QVBoxLayout(self)
helped.
Using PyQt5, there are certain event handlers which I can bind to my object, and others that only work if I implement them as methods. changeEvent and event are examples of the later type.
You can see in my example below that I can add a keyPressEvent handler to my widget programmatically, but I can not do the same thing for changeEvent.
from PyQt5 import QtGui, QtWidgets, QtCore
import types
def keyPressEvent(self, key: QtGui.QKeyEvent) -> None:
#works
print(key.isAutoRepeat())
def changeEvent(self, a0: QtCore.QEvent) -> None:
#doesn't work
print("bound change event", a0.type())
bindable = [keyPressEvent, changeEvent]
def bind_key_functions(target):
for bound in bindable:
setattr(target, bound.__name__, types.MethodType(bound, target))
class my_widget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle("w1")
bind_key_functions(self)
class my_widget2(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle("w2")
def changeEvent(self, a0: QtCore.QEvent) -> None:
#this does work
print("derived change event", a0.type())
app = QtWidgets.QApplication([])
mw1 = my_widget()
mw1.show()
mw2 = my_widget2()
mw2.show()
app.exec_()
What makes the changeEvent different? how can I force it to behave as I want?
Using setattr to override methods is a bad choice as it is not very elegant and if you want to listen to the events of another QWidget then it is better to use an event filter.
from PyQt5 import QtGui, QtWidgets, QtCore
class Binder(QtCore.QObject):
def __init__(self, qobject):
super().__init__(qobject)
self._qobject = qobject
self.qobject.installEventFilter(self)
#property
def qobject(self):
return self._qobject
def eventFilter(self, obj, event):
if self.qobject is obj:
print(event.type(), event)
return super().eventFilter(obj, event)
class My_Widget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle("w1")
Binder(self)
class My_Widget2(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle("w2")
app = QtWidgets.QApplication([])
mw1 = My_Widget()
mw1.show()
mw2 = My_Widget2()
mw2.show()
app.exec_()
On the other hand, it is not documented which methods can be assigned or not, so if you want to find the reason you must analyze the source code of sip and pyqt5. What little is pointed out is that PyQt5 creates a cache of the methods (it is not known when or what methods are stored in the cache).
I want to trigger a function of a different class on press of a button. Something like this example How to emit custom Events to the Event Loop in PyQt.
But I also want to pass a parameter to that function everytime the button is clicked. How do I achieve that?
If you want to add additional arguments you can use functools.partial:
main.py
from PyQt5 import QtCore, QtWidgets
from globalobject import GlobalObject
import functools
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
button = QtWidgets.QPushButton(text="Press me", clicked=self.on_clicked)
self.setCentralWidget(button)
#QtCore.pyqtSlot()
def on_clicked(self):
GlobalObject().dispatchEvent("hello")
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
wrapper = functools.partial(self.foo, "foo", bar="baz")
GlobalObject().addEventListener("hello", wrapper)
self._label = QtWidgets.QLabel()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._label)
def foo(self, foo, bar=None):
print(foo, bar)
self._label.setText(foo)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w1 = MainWindow()
w2 = Widget()
w1.show()
w2.show()
sys.exit(app.exec_())
That logic can also be implemented in the library:
globalobject.py
from PyQt5 import QtCore
import functools
#functools.lru_cache()
class GlobalObject(QtCore.QObject):
def __init__(self):
super().__init__()
self._events = {}
def addEventListener(self, name, func, *, args=(), kwargs=None):
kwargs = kwargs or {}
if name not in self._events:
self._events[name] = []
self._events[name].append((func, args, kwargs))
def dispatchEvent(self, name):
functions = self._events.get(name, [])
for func, args, kwargs in functions:
wrapper = func
wrapper = functools.partial(func, *args, **kwargs)
QtCore.QTimer.singleShot(0, wrapper)
main.py
from PyQt5 import QtCore, QtWidgets
from globalobject import GlobalObject
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
button = QtWidgets.QPushButton(text="Press me", clicked=self.on_clicked)
self.setCentralWidget(button)
#QtCore.pyqtSlot()
def on_clicked(self):
GlobalObject().dispatchEvent("hello")
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
GlobalObject().addEventListener(
"hello", self.foo, args=("foo",), kwargs={"bar": "baz"}
)
self._label = QtWidgets.QLabel()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._label)
def foo(self, foo, bar=None):
print(foo, bar)
self._label.setText(foo)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w1 = MainWindow()
w2 = Widget()
w1.show()
w2.show()
sys.exit(app.exec_())
Update:
If you want to send arguments through the dispatchEvent method then you should use the following:
globalobject.py
from PyQt5 import QtCore
import functools
#functools.lru_cache()
class GlobalObject(QtCore.QObject):
def __init__(self):
super().__init__()
self._events = {}
def addEventListener(self, name, func):
if name not in self._events:
self._events[name] = []
self._events[name].append(func)
def dispatchEvent(self, name, *, args=(), kwargs=None):
kwargs = kwargs or {}
functions = self._events.get(name, [])
for func in functions:
wrapper = func
wrapper = functools.partial(func, *args, **kwargs)
QtCore.QTimer.singleShot(0, wrapper)
main.py
from PyQt5 import QtCore, QtWidgets
from globalobject import GlobalObject
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
button = QtWidgets.QPushButton(text="Press me", clicked=self.on_clicked)
self.setCentralWidget(button)
self.counter = 0
#QtCore.pyqtSlot()
def on_clicked(self):
self.counter += 1
GlobalObject().dispatchEvent("hello", args=(self.counter,))
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
GlobalObject().addEventListener("hello", self.foo)
self._label = QtWidgets.QLabel()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._label)
def foo(self, x):
print(x)
self._label.setNum(x)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w1 = MainWindow()
w2 = Widget()
w1.show()
w2.show()
sys.exit(app.exec_())
How can I subclass the QPushbutton in Pyside but require an arg. In my example I need the arg to be a list of ints [0,0,0].
My goal is to make it so i can create MyButton like this:
# GOAL
MyButton([0,255,0])
When the arguement containing the list of values is passed in, it should set the value self._data. I'm not sure if i have this setup correctly, so any corrections are appreciated.
class MyButton(QtGui.QPushButton):
def __init__(self, *args, **kwargs):
super(MyButton, self).__init__(*args, **kwargs)
self._data = stuff
#property
def data(self):
return self._data
#data.setter
def data(self, value):
self._data = value
MyButton([0,255,0])
Updated: I noticed though when i pass that value into my Init it doesn't appear to trigger the setter for that property?? Why is that? If you test the code below, you'll see the color of the button isn't set when it's instantiated. How do i fix that?
import os
import sys
import json
from PySide import QtCore, QtGui
class QColorSwatch(QtGui.QPushButton):
colorClicked = QtCore.Signal(list)
colorChanged = QtCore.Signal(list)
def __init__(self, stuff, *args, **kwargs):
super(QColorSwatch, self).__init__(*args, **kwargs)
self._color = stuff
self.setMaximumWidth(22)
self.setMaximumHeight(22)
self.setAutoFillBackground(True)
self.pressed.connect(self.color_clicked)
#property
def color(self):
return self._color
#color.setter
def color(self, value):
self._color = value
pixmap = QtGui.QPixmap(self.size())
pixmap.fill(QtGui.QColor(value[0], value[1], value[2]))
self.setIcon(pixmap)
self.colorChanged.emit(value)
# swatch.setIconSize(pixmap.size() - QtCore.QSize(6,6))
def color_clicked(self):
self.colorClicked.emit(self.color)
class ExampleWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(ExampleWindow, self).__init__(parent)
self.resize(300, 200)
self.ui_swatch = QColorSwatch([255,0,0])
# main layout
main_layout = QtGui.QVBoxLayout()
main_layout.setContentsMargins(5,5,5,5)
main_layout.setSpacing(5)
main_layout.addWidget(self.ui_swatch)
main_widget = QtGui.QWidget()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
# Signals
self.ui_swatch.colorClicked.connect(self.color_clicked)
self.ui_swatch.colorChanged.connect(self.color_changed)
def color_clicked(self, col):
print 'CLICKED:', col
self.ui_swatch.color = [255,0,0]
def color_changed(self, col):
print 'CHANGED:', col
def main():
app = QtGui.QApplication(sys.argv)
ex = ExampleWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You have to put stuff as a parameter.
class MyButton(QtGui.QPushButton):
def __init__(self, stuff, *args, **kwargs):
super(MyButton, self).__init__(*args, **kwargs)
self._data = stuff
#property
def data(self):
return self._data
#data.setter
def data(self, value):
self._data = value
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
mainWin = MyButton([0,255,0])
print(mainWin.data)
mainWin.show()
sys.exit(app.exec_())
Update:
You have to use self.color to use the setter.
class QColorSwatch(QtGui.QPushButton):
colorClicked = QtCore.Signal(list)
colorChanged = QtCore.Signal(list)
def __init__(self, stuff, *args, **kwargs):
super(QColorSwatch, self).__init__(*args, **kwargs)
self._color = None
self.color = stuff
self.setMaximumWidth(22)
self.setMaximumHeight(22)
self.setAutoFillBackground(True)
self.pressed.connect(self.color_clicked)
#property
def color(self):
return self._color
#color.setter
def color(self, value):
self._color = value
pixmap = QtGui.QPixmap(self.size())
pixmap.fill(QtGui.QColor(value[0], value[1], value[2]))
self.setIcon(pixmap)
self.colorChanged.emit(value)
# swatch.setIconSize(pixmap.size() - QtCore.QSize(6,6))
def color_clicked(self):
self.colorClicked.emit(self.color)