I am creating a custom widget that inherits from QLabel, and I would like to have a property on my widget to represent how the data must be formatted when presenting to the user.
For that I am trying to use Q_ENUMS, but I'm not having much success. I can get the property to show in Designer, but the UI file saved shows the enum as PyDMLabel::STRING and not as I would expect DisplayFormat::STRING.
Here is my code for the widget:
class PyDMLabel(QLabel, PyDMWidget):
class DisplayFormat:
DEFAULT = 0
STRING = 1
DECIMAL = 2
EXPONENTIAL = 3
HEX = 4
BINARY = 5
Q_ENUMS(DisplayFormat)
"""
A QLabel with support for Channels and more from PyDM
Parameters
----------
parent : QWidget
The parent widget for the Label
init_channel : str, optional
The channel to be used by the widget.
"""
def __init__(self, parent=None, init_channel=None):
QLabel.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
self.setTextFormat(Qt.PlainText)
self.setTextInteractionFlags(Qt.NoTextInteraction)
self.setText("PyDMLabel")
self._display_format_type = PyDMLabel.DisplayFormat.DEFAULT
#pyqtProperty(DisplayFormat)
def displayFormat(self):
return self._display_format_type
#displayFormat.setter
def displayFormat(self, new_type):
if self._display_format_type != new_type:
self._display_format_type = new_type
What is the correct way to deal with Q_ENUMS and PyQt?
In order for Qt (Designer) to see an enum, PyQt has to add it to the meta-object of the custom class. So it could never be referred to by Qt as DisplayFormat::STRING.
In Qt, enums declared in the class scope expose their constants as members of the class. So for example, the QComboBox class defines an InsertPolicy enum, and the constants can be referred to like this: QComboBox::InsertAtTop. So in that respect, the behaviour of PyQt Q_ENUMS in Qt Designer plugins is exactly as expected, since the ui file shows PyDMLabel::STRING.
However, getting fully equivalent behaviour in Python code requires some extra work. The nearest I could come up with is this:
class DisplayFormat:
DEFAULT = 0
STRING = 1
DECIMAL = 2
EXPONENTIAL = 3
HEX = 4
BINARY = 5
class PyDMLabel(QLabel, PyDMWidget, DisplayFormat):
DisplayFormat = DisplayFormat
Q_ENUMS(DisplayFormat)
This will still result in Qt Designer using PyDMLabel::STRING (as expected). But Python code can now access the constants in any of these ways:
PyDMLabel.STRING
PyDMLabel.DisplayFormat.STRING
DisplayFormat.STRING
And in fact, if you don't mind losing the second of these options, you could simplify things even further to this:
class DisplayFormat:
DEFAULT = 0
...
class PyDMLabel(QLabel, PyDMWidget, DisplayFormat):
Q_ENUMS(DisplayFormat)
Related
I wanted to ask if we could just animate the fontsize of the QLabel in PyQt5 with QPropertyAnimation.
I tried by adding QPropertyAnimation(self.label , b"fontSize") but It was not working and I got the warning
I want you increase the text of the QLabel and then again switch to normal size
From the QPropertyAnimation description:
QPropertyAnimation interpolates over Qt properties. As property values are stored in QVariants, the class inherits QVariantAnimation, and supports animation of the same meta types as its super class.
A class declaring properties must be a QObject. To make it possible to animate a property, it must provide a setter (so that QPropertyAnimation can set the property's value).
All classes that inherit from QObject (including widgets) support Qt properties, and in fact most of them have "builtin" properties, but, obviously, not all properties support animations: they must be based on numeric values. You can animate a numeric change (eg: from 0 to 100), not a text (eg. from 'ABC' to 'PYZ').
For instance, all QWidget have a pos property, and since such property (which is a QPoint) is based on numbers, you can create an animation. You can see the list of supported variant types for animations in the QVariantAnimation docs. Consider that all documentation pages regarding QObject subclasses contain a property section (see the QWidget properties for example), and that properties obviously are inherited from their super classes.
There is no property for the font size, though, so there are two possibilities:
create a custom property (by using the pyqtProperty decorator in PyQt);
use a QVariantAnimation, which is not bound to any property;
Depending on the situation, one might prefer one approach or the other, but for this specific case it doesn't change that much; the QVariantAnimation is certainly simpler, the Qt property approach is more "compliant".
Here you can see how the property is created using the pyqtProperty decorator. Since the animation writes the property, having a setter is mandatory.
class AnimationTest(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.startButton = QtWidgets.QPushButton('Start')
self.label = QtWidgets.QLabel('Hello!')
self.labelFont = self.font()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.startButton)
layout.addWidget(self.label)
self.ani = QtCore.QPropertyAnimation(self, b'labelFontSize')
self.ani.setStartValue(10)
self.ani.setEndValue(80)
self.ani.setDuration(1500)
self.startButton.clicked.connect(self.ani.start)
#QtCore.pyqtProperty(int)
def labelFontSize(self):
return self.labelFont.pointSize()
#labelFontSize.setter
def labelFontSize(self, size):
self.labelFont.setPointSize(size)
self.label.setFont(self.labelFont)
import sys
app = QtWidgets.QApplication(sys.argv)
test = AnimationTest()
test.show()
sys.exit(app.exec_())
The variant animation is certainly shorter:
class AnimationTest(QtWidgets.QWidget):
def __init__(self):
# ...
self.ani = QtCore.QVariantAnimation()
self.ani.setStartValue(10)
self.ani.setEndValue(80)
self.ani.setDuration(1500)
self.ani.valueChanged.connect(self.updateLabelFont)
self.startButton.clicked.connect(self.ani.start)
def updateLabelFont(self, value):
self.labelFont.setPointSize(value)
self.label.setFont(self.labelFont)
Please consider that font size animations are often problematic, as most fonts have slightly different glyphs and spacings depending on the size. In the examples above I used a pretty long animation that will probably show the effect: while the size increases linearly, the change in the font usually is not very smooth.
I'm currently trying to convert an old python program from Python 2 to Python 3, and update from PyQt4 to PyQt5. The application uses the old style signal and slots that are not supported under PyQt5. I have figured out most of what needs to be done, but below are a few lines that I can't seem to get working:
self.emit(SIGNAL('currentChanged'), row, col)
self.emit(SIGNAL("activated(const QString &)"), self.currentText())
self.connect(self,SIGNAL("currentChanged(const QString&)"), self.currentChanged)
The top two lines, I have no idea where to start since they don't seem to be attached to anything. The last example I'm not quite sure what to do with (const QString &).
I'm not entirely sure how to approach these, and I'm still learning python, but any help would be appreciated.
EDIT: The documentation doesn't really seem to go into depth on these cases, at least in a way that I understand.
The exact answer to this will depend on what kind of object self is. If it is a Qt class that already defines those signals, then the new-style syntax would be this:
self.currentChanged[int, int].emit(row, col)
self.activated[str].emit(self.currentText())
self.currentChanged[str].connect(self.handleCurrentChanged)
However, if any of those aren't pre-defined, you would need to define custom signals for them, like this:
class MyClass(QWidget):
# this defines two overloads for currentChanged
currentChanged = QtCore.pyqtSignal([int, int], [str])
activated = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super(MyClass, self).__init__(parent)
self.currentChanged[str].connect(self.handleCurrentChanged)
def handleCurrentChanged(self, text):
print(text)
The old-style syntax allowed custom signals to be emitted dynamically (i.e. without defining them first), but that is not possible any more. With the new-style syntax, custom signals must always be explicitly defined.
Note that, if there is only one overload defined for a signal, the selector can be omitted:
self.activated.emit(self.currentText())
For more information, see these articles in the PyQt Docs:
Support for Signals and Slots (PyQt5)
Old-style Signal and Slot Support (PyQt4)
New-style Signal and Slot Support (PyQt4)
EDIT:
For your actual code, you need to make the following changes for the currentChanged signals:
In Multibar.py (around line 30):
This defines a custom signal (because QWidget does not have it):
class MultiTabBar(QWidget):
# add the following line
currentChanged = pyqtSignal(int, int)
In Multibar.py (around line 133):
This emits the custom signal defined in (1):
# self.emit(SIGNAL('currentChanged'), row, col)
self.currentChanged.emit(row, col)
In ScWindow.py (around line 478):
This connects the signal defined in (1):
# self.connect(self.PieceTab,SIGNAL("currentChanged"),self.pieceTabChanged)
self.PieceTab.currentChanged.connect(self.pieceTabChanged)
In ItemList.py (around line 73):
The QFileDialog class already defines this signal, and there is only one overload of it. But the name of the slot must be changed, because it is shadowing the built-in signal name (which has become an attribute in the new-style syntax). So the connection should be made like this:
# self.connect(self,SIGNAL("currentChanged(const QString&)"),self.currentChanged)
self.currentChanged.connect(self.onCurrentChanged)
In ItemList.py (around line 78):
This renames the slot for the connection made in (4):
# def currentChanged(self, file):
def onCurrentChanged(self, file):
I'm having trouble with getting multiple dynamic inheritance to work. These examples make the most sense to me(here and here), but there's not enough code in one example for me to really understand what's going on and the other example doesn't seem to be working when I change it around for my needs (code below).
I'm creating a universal tool that works with multiple software packages. In one software, I need to inherit from 2 classes: 1 software specific API mixin, and 1 PySide class. In another software I only need to inherit from the 1 PySide class.
The least elegant solution that I can think of is to just create 2 separate classes (with all of the same methods) and call either one based on the software that's running. I have a feeling there's a better solution.
Here's what I'm working with:
## MainWindow.py
import os
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
# Build class
def build_main_window(*arg):
class Build(arg):
def __init__(self):
super( Build, self ).__init__()
# ----- a bunch of methods
# Get software
software = os.getenv('SOFTWARE')
# Run tool
if software == 'maya':
build_main_window(maya_mixin_class, QtGui.QWidget)
if software == 'houdini':
build_main_window(QtGui.QWidget)
I'm currently getting this error:
# class Build(arg):
# TypeError: Error when calling the metaclass bases
# tuple() takes at most 1 argument (3 given) #
Thanks for any help!
EDIT:
## MainWindow.py
import os
# Build class
class BuildMixin():
def __init__(self):
super( BuildMixin, self ).__init__()
# ----- a bunch of methods
def build_main_window(*args):
return type('Build', (BuildMixin, QtGui.QWidget) + args, {})
# Get software
software = os.getenv('SOFTWARE')
# Run tool
if software == 'maya':
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
Build = build_main_window(MayaQWidgetDockableMixin)
if software == 'houdini':
Build = build_main_window()
The error in your original code is caused by failing to use tuple expansion in the class definition. I would suggest simplifying your code to this:
# Get software
software = os.getenv('SOFTWARE')
BaseClasses = [QtGui.QWidget]
if software == 'maya':
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
BaseClasses.insert(0, MayaQWidgetDockableMixin)
class Build(*BaseClasses):
def __init__(self, parent=None):
super(Build, self).__init__(parent)
UPDATE:
The above code will only work with Python 3, so it looks like a solution using type() will be needed for Python 2. From the other comments, it appears that the MayaQWidgetDockableMixin class may be a old-style class, so a solution like this may be necessary:
def BaseClass():
bases = [QtGui.QWidget]
if software == 'maya':
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
class Mixin(MayaQWidgetDockableMixin, object): pass
bases.insert(0, Mixin)
return type('BuildBase', tuple(bases), {})
class Build(BaseClass()):
def __init__(self, parent=None):
super(Build, self).__init__(parent)
arg is a tuple, you can't use a tuple as a base class.
Use type() to create a new class instead; it takes a class name, a tuple of base classes (can be empty) and the class body (a dictionary).
I'd keep the methods for your class in a mix-in method:
class BuildMixin():
def __init__(self):
super(BuildMixin, self).__init__()
# ----- a bunch of methods
def build_main_window(*arg):
return type('Build', (BuildMixin, QtGui.QWidget) + args, {})
if software == 'maya':
Build = build_main_window(maya_mixin_class)
if software == 'houdini':
Build = build_main_window()
Here, args is used as an additional set of classes to inherit from. The BuildMixin class provides all the real methods, so the third argument to type() is left empty (the generated Build class has an empty class body).
Since QtGui.QWidget is common between the two classes, I just moved that into the type() call.
I want to improve my code but currently have not much idea how.
So I used Qt Designer and created a main window plus 3 dialogs which can be opened from main window. Converted .ui files to .py files and created the MainWindow class which manages all.
Everything works fine, but for me this looks wrong:
class MainWindow(QMainWindow, Ui_MainWindow):
# init and else
[...]
def open_add_dialog(self):
self.dialog = AddDialog()
self.dialog.show()
def open_edit_dialog(self):
self.dialog = EditDialog()
self.dialog.show()
def open_about_dialog(self):
self.dialog = AboutDialog()
self.dialog.show()
def assign_widgets(self):
self.actionAdd.triggered.connect(self.open_add_dialog)
self.actionEdit.triggered.connect(self.open_edit_dialog)
self.actionAbout.triggered.connect(self.open_about_dialog)
Code is simplified.. So as you see I've 3 almost equal methods. So the question comes to my mind is it possible to merge all into one? What I want is something like this:
def open_dialog(self):
sender = self.sender()
sender.show()
I think you should never use the sender method of Qt because it makes calling the method from another function impossible, you can then only use it via the signal/slot mechanism. It therefore says in the documentation that: "This function violates the object-oriented principle of modularity". Using it during debugging is fine, of course.
In your case the methods are quite small. You could use lambdas in the connect statement so that you don't have to make separate methods. Or you could create the dialogs in the constructor and only connect to the show methods. Like this:
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
self.add_dialog = AddDialog()
self.edit_dialog = EditDialog()
self.about_dialog = AboutDialog()
def assign_widgets(self):
self.actionAdd.triggered.connect(self.add_dialog.show)
self.actionEdit.triggered.connect(self.edit_dialog.show)
self.actionAbout.triggered.connect(self.about_dialog.show)
I'm making a GUI with pyqt4 and python. Right now I have a QLineEdit and QComboBox, where the QLineEdit displays the values and the QComboBox can be used to change units. I'm using signals and slots to handle real time unit/value feedback for the user but I'm having problems understanding how to programmatically work with the values as I need them all to be in standard units. Here's what I've got so far, the combo_box_line_edit_list is a list of list where I wrap the combo box and line list together
class UnitConverterSignaler(QtCore.QObject):
def __init__(self, combo_box_line_edit_list):
super(QtCore.QObject, self).__init__()
self.combo_box_line_edit_list = combo_box_line_edit_list
self.combo_box_list = [line_edit_combo_box[0] for line_edit_combo_box in combo_box_line_edit_list]
for combo_box, line_edit in self.combo_box_line_edit_list:
combo_box.currentIndexChanged['QString'].connect(line_edit.convert_units)
line_edit.store_unit_state(combo_box.currentText())
line_edit.standard_unit = combo_box.itemText(1)
def convert_to_standard(self):
for combo_box in self.combo_box_list:
combo_box.setCurrentIndex(0)
def convert_to_international(self):
for combo_box in self.combo_box_list:
combo_box.setCurrentIndex(1)
def toggle_unit_conversion(self, hold_line_values_steady):
for combo_box in self.combo_box_list:
if hold_line_values_steady:
combo_box.do_not_convert_units_on_change()
else:
combo_box.convert_units_on_change()
def convert_units_on_change(self):
"""
Changes the value of the line edit each time the combo box is changed
"""
for combo_box, line_edit in self.combo_box_line_edit_list:
combo_box.currentIndexChanged['QString'].connect(line_edit.convert_units)
combo_box.currentIndexChanged['QString'].disconnect(line_edit.store_unit_state)
def do_not_convert_units_on_change(self):
"""
Holds the line edit value constant in spite of combo box changes
"""
for combo_box, line_edit in self.combo_box_line_edit_list:
combo_box.currentIndexChanged['QString'].disconnect(line_edit.convert_units)
combo_box.currentIndexChanged['QString'].connect(line_edit.store_unit_state)
Instantiated & used in another class
self.lockCellCheckBox.toggled.connect(self.unit_converter_signaler.toggle_unit_conversion)
self.internationalPushButton.clicked.connect(self.unit_converter_signaler.convert_to_international)
self.standardPushButton.clicked.connect(self.unit_converter_signaler.convert_to_standard)
I've also monkey patched the QLineEdit instead of subclassing so I can make quick changes with QtDesigner.
# monkey patch slot onto line_edit
def convert_units(line_edit, end_unit):
converted_unit_value = line_edit.unit_registry.convert(float(line_edit.text()), line_edit.stored_unit_state, str(end_unit))
line_edit.setText(str(converted_unit_value))
line_edit.stored_unit_state = str(end_unit)
# monkey patch slot onto line_edit
def store_unit_state(line_edit, unit):
line_edit.stored_unit_state = str(unit)
Would the most generalized way to get the standard units out in my main program be the creation of a signal for each combo box/line edit in the UnitConverter?
From what I understood so far: you have many combo-box/line-edit pairs and the entered values should always be converted to standard units (e.g. displayed on a third QLabel or whatever).
Would the most generalized way to get the standard units out in my main program be the creation of a signal for each combo box/line edit in the UnitConverter?
No, you don't have to. A slot in python (or especially in pyqt) can be any callable object. A callable object is an object with method __call__(self) implemented.
Therefore I would suggest you to create a class which takes the related object(s) as parameter(s) in the contructor and changes them in __call__(self). Something like this:
class ConverterSignal:
def __init__(whatever_you_want_to_refer_to):
self.whatever_you_want_to_refer_to = whatever_you_want_to_refer_to
def __call(self)__:
""" Here you can refer to whatever_you_want_to_refer_to and do whatever you want with it """
The connection is done as following (for the combo box as an example):
self.connect(combo_box, QtCore.SIGNAL('activated(int)'), ConverterSignal(whatever_you_want_to_refer_to))
Here an instance of the class ConverterSignal is created and will be called if the corresponding signal is emitted.