tags: python qt widget title: Simple Number Spinner 1 A **warning**: adding signals and slots to this was my first experience of why you shouldn't do the lazy `from X import *` thing: it causes a very obscure error when you try to connect. In particular, if I put the `NumberSpinner` class below in its own file `number_spinner.py` in which I do the `import *`, and then in another file I also do `from PySide6.... import *` and then `import number_spinner`, then trying to connect the number spinner to something causes an exception, which doesn't happen with the `NumberSpinner` class is declared in the same file. If instead we do the proper thing of not `import *`ing, the problem goes away. That said, I failed to reproduce the exception when I went back to check the above, so that might be the problem, or it might be something else. For reference, the exception looked like: ``` shibokensupport/signature/parser.py:270: RuntimeWarning: pyside_type_init:_resolve_value UNRECOGNIZED: 'PySide6.QtCore.QMetaObject.Connection' OFFENDING LINE: '6:PySide6.QtCore.QObject.connect(self,sender:PySide6.QtCore.QObject,signal:char*,member:char*,type:PySide6.QtCore.Qt.ConnectionType=Qt.AutoConnection)->PySide6.QtCore.QMetaObject.Connection' ... Traceback (most recent call last): File "/home/john/qt/widgets/signals/a.py", line 42, in a = A() ^^^ File "/home/john/qt/widgets/signals/a.py", line 35, in __init__ self.spinner.boing.connect(self.spinnerChanged) TypeError: 'PySide6.QtCore.QObject.connect' called with wrong argument types: PySide6.QtCore.QObject.connect(NumberSpinner, str, method) Supported signatures: PySide6.QtCore.QObject.connect(PySide6.QtCore.QObject, Union[bytes, bytearray, memoryview], Union[bytes, bytearray, memoryview], PySide6.QtCore.Qt.ConnectionType = Instance(Qt.AutoConnection)) PySide6.QtCore.QObject.connect(Union[bytes, bytearray, memoryview], Callable, PySide6.QtCore.Qt.ConnectionType = Instance(Qt.AutoConnection)) PySide6.QtCore.QObject.connect(Union[bytes, bytearray, memoryview], PySide6.QtCore.QObject, Union[bytes, bytearray, memoryview], PySide6.QtCore.Qt.ConnectionType = Instance(Qt.AutoConnection)) PySide6.QtCore.QObject.connect(PySide6.QtCore.QObject, PySide6.QtCore.QMetaMethod, PySide6.QtCore.QObject, PySide6.QtCore.QMetaMethod, PySide6.QtCore.Qt.ConnectionType = Instance(Qt.AutoConnection)) PySide6.QtCore.QObject.connect(PySide6.QtCore.QObject, Union[bytes, bytearray, memoryview], PySide6.QtCore.QObject, Callable, PySide6.QtCore.Qt.ConnectionType = Instance(Qt.AutoConnection)) PySide6.QtCore.QObject.connect(PySide6.QtCore.QObject, Union[bytes, bytearray, memoryview], Callable, PySide6.QtCore.Qt.ConnectionType = Instance(Qt.AutoConnection)) PySide6.QtCore.QObject.connect(PySide6.QtCore.QObject, Union[bytes, bytearray, memoryview], PySide6.QtCore.QObject, Union[bytes, bytearray, memoryview], PySide6.QtCore.Qt.ConnectionType = Instance(Qt.AutoConnection)) ``` Thus, instead of this: ```py #!/usr/bin/env python from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * from PySide6.QtNetwork import * app = QApplication() # this allows us to remove keyword arguments that exist that we are interested in # and return defaults for those not specified, so that the resulting dictionary # can be passed to super() with those keyword arguments removed. def gd(dictionary,key,default,delete=True): if key in dictionary: v = dictionary[key] if delete: del(dictionary[key]) return v else: return default class NumberSpinner(QLabel): def __init__(self,*xs,**kw): gran = int(gd(kw,"gran",10)) gran = max(1,gran) vmax = int(gd(kw,"vmax",100)) vmin = int(gd(kw,"vmin",0)) value = int(gd(kw,"init",vmin)) if vmax <= vmin: raise ValueError(f"Invalid range {vmin}..{vmax}") roll = int(gd(kw,"roll",False)) super().__init__(*xs,**kw) self.value = value self.gran = gran self.vmax = vmax self.vmin = vmin self.roll = roll self.drag = False self.x0 = 0 self.y0 = 0 self.v0 = 0 self.update() def update(self): self.setText(str(self.value)) return super().update() def mousePressEvent(self,e): pos = e.position() self.x0 = pos.x() self.y0 = pos.y() self.v0 = self.value def mouseMoveEvent(self,e): pos = e.position() x = pos.x() y = pos.y() dy = y - self.y0 dv = int(-dy/self.gran) v = self.v0 + dv vmx = self.vmax + 1 # so that we can attain the maximum bound if self.roll: v = self.vmin + ( (v - self.vmin) % (vmx - self.vmin) ) else: v = max(self.vmin,min(self.vmax,v)) self.value = v self.update() ns = NumberSpinner(vmin=1,vmax=16,roll=False) ns.resize(100,50) ns.setStyleSheet("font-size: 24px") ns.setAlignment(Qt.AlignCenter) ns.show() app.exec() ``` if we want a reusable spinner widget, do this ```py import PySide6.QtWidgets import PySide6.QtGui import PySide6.QtCore from jdautil import gd class NumberSpinner(PySide6.QtWidgets.QLabel): changed = PySide6.QtCore.Signal(int) def __init__(self,*xs,**kw): gran = int(gd(kw,"gran",10)) gran = max(1,gran) vmax = int(gd(kw,"vmax",100)) vmin = int(gd(kw,"vmin",0)) value = int(gd(kw,"init",vmin)) if vmax <= vmin: raise ValueError(f"Invalid range {vmin}..{vmax}") roll = int(gd(kw,"roll",False)) super().__init__(*xs,**kw) self.value = value self.gran = gran self.vmax = vmax self.vmin = vmin self.roll = roll self.drag = False self.x0 = 0 self.y0 = 0 self.v0 = 0 self.update() def update(self): self.setText(str(self.value)) return super().update() def mousePressEvent(self,e): pos = e.position() self.x0 = pos.x() self.y0 = pos.y() self.v0 = self.value def mouseMoveEvent(self,e): pos = e.position() x = pos.x() y = pos.y() dy = y - self.y0 dv = int(-dy/self.gran) v = self.v0 + dv vmx = self.vmax + 1 # so that we can attain the maximum bound if self.roll: v = self.vmin + ( (v - self.vmin) % (vmx - self.vmin) ) else: v = max(self.vmin,min(self.vmax,v)) self.value = v self.changed.emit(v) self.update() ``` It's probably also a good idea not to be lazy and terse with variable names, but old habits die hard.