I'm trying to make an invalid animation tool. I have a button that when pressed, animates the background color of a field from red to the "normal" field color. This works great but now I want to pass in any arbitrary PyQt widget object (could be QLineEdit, QComboBox, and so on). Here's what my animation handler looks like:
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
Currently it requires that the field object be already named before calling the function to change the background. I would like to be able to dynamically pass in a widget and set the stylesheet on the fly by passing in a parameter, something like this:
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
When I try to do this, the widget is able to be passed but the constant color change from WARNING_COLOR to NORMAL_COLOR doesn't work anymore. I also do not want to put this into a class since it has to be on-the-fly. My goal is being able to call a function to start the animation from anywhere instead of having to press the button. The desired goal is like this:
class VariantAnimation(QtCore.QVariantAnimation):
"""VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
def updateCurrentValue(self, value):
pass
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(widget, color: QtGui.QColor) -> None:
widget.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
def invalid_animation(widget):
return VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=lambda: invalid_animation_handler(widget))
def start_invalid_animation(animation_handler) -> None:
if animation_handler.state() == QtCore.QAbstractAnimation.Running:
animation_handler.stop()
animation_handler.start()
field = QtGui.QLineEdit()
field_animation_handler = invalid_animation(field)
# Goal is to make a generic handler to start the animation
start_invalid_animation(field_animation_handler)
Minimal working example
import sys
from PyQt4 import QtCore, QtGui
class VariantAnimation(QtCore.QVariantAnimation):
"""VariantAnimation: Implement method for QVariantAnimation to fix pure virtual method in PyQt5 -> PyQt4"""
def updateCurrentValue(self, value):
pass
#QtCore.pyqtSlot(QtGui.QColor)
def invalid_animation_handler(color: QtGui.QColor) -> None:
field.setStyleSheet(f"background-color: {QtGui.QColor(color).name()}")
def start_field_invalid_animation() -> None:
if field_invalid_animation.state() == QtCore.QAbstractAnimation.Running:
field_invalid_animation.stop()
field_invalid_animation.start()
NORMAL_COLOR = QtGui.QColor(25,35,45)
SUCCESSFUL_COLOR = QtGui.QColor(95,186,125)
WARNING_COLOR = QtGui.QColor(251,188,5)
ERROR_COLOR = QtGui.QColor(247,131,128)
ANIMATION_DURATION = 1500
if __name__== '__main__':
app = QtGui.QApplication(sys.argv)
button = QtGui.QPushButton('Animate field background')
button.clicked.connect(start_field_invalid_animation)
field = QtGui.QLineEdit()
field_invalid_animation = VariantAnimation(startValue=ERROR_COLOR, endValue=NORMAL_COLOR, duration=ANIMATION_DURATION, valueChanged=invalid_animation_handler)
mw = QtGui.QMainWindow()
layout = QtGui.QHBoxLayout()
layout.addWidget(button)
layout.addWidget(field)
window = QtGui.QWidget()
window.setLayout(layout)
mw.setCentralWidget(window)
mw.show()
sys.exit(app.exec_())
I do not understand exactly why you do not want a class but IMO is the most suitable solution. The logic is to store the callable that allows to change the property and invoke it in updateCurrentValue.
Currently I don't have PyQt4 installed so I implemented the logic with PyQt5 but I don't think it is difficult to change the imports.
import sys
from dataclasses import dataclass
from functools import partial
from typing import Callable
from PyQt5.QtCore import QAbstractAnimation, QObject, QVariant, QVariantAnimation
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QLineEdit,
QMainWindow,
QPushButton,
QWidget,
)
#dataclass
class VariantAnimation(QVariantAnimation):
widget: QWidget
callback: Callable[[QWidget, QVariant], None]
start_value: QVariant
end_value: QVariant
duration: int
parent: QObject = None
def __post_init__(self) -> None:
super().__init__()
self.setStartValue(self.start_value)
self.setEndValue(self.end_value)
self.setDuration(self.duration)
self.setParent(self.parent)
def updateCurrentValue(self, value):
if isinstance(self.widget, QWidget) and callable(self.callback):
self.callback(self.widget, value)
def invalid_animation_handler(widget: QWidget, color: QColor) -> None:
widget.setStyleSheet(f"background-color: {QColor(color).name()}")
def start_field_invalid_animation(animation: QAbstractAnimation) -> None:
if animation.state() == QAbstractAnimation.Running:
animation.stop()
animation.start()
NORMAL_COLOR = QColor(25, 35, 45)
SUCCESSFUL_COLOR = QColor(95, 186, 125)
WARNING_COLOR = QColor(251, 188, 5)
ERROR_COLOR = QColor(247, 131, 128)
ANIMATION_DURATION = 1500
if __name__ == "__main__":
app = QApplication(sys.argv)
button = QPushButton("Animate field background")
field = QLineEdit()
animation = VariantAnimation(
widget=field,
callback=invalid_animation_handler,
start_value=ERROR_COLOR,
end_value=NORMAL_COLOR,
duration=ANIMATION_DURATION,
)
button.clicked.connect(partial(start_field_invalid_animation, animation))
mw = QMainWindow()
layout = QHBoxLayout()
layout.addWidget(button)
layout.addWidget(field)
window = QWidget()
window.setLayout(layout)
mw.setCentralWidget(window)
mw.show()
sys.exit(app.exec_())
Related
I write a PySide6 application where I have a layout with three QListView widgets next to each other. Each displays something with a different list model, and all shall have a context menu. What doesn't work is that the right list model or list view gets resolved. This leads to the context menu appearing in the wrong location, and also the context menu actions working on the wrong thing.
I have done a right-click into the circled area, the context menu shows up in the right panel:
This is the first iteration of an internal tool, so it is not polished, and neither did I separate controller and view of the UI. This is a minimal full example:
import sys
from typing import Any
from typing import List
from typing import Union
from PySide6.QtCore import QAbstractItemModel
from PySide6.QtCore import QAbstractListModel
from PySide6.QtCore import QModelIndex
from PySide6.QtCore import QPersistentModelIndex
from PySide6.QtCore import QPoint
from PySide6.QtGui import Qt
from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QHBoxLayout
from PySide6.QtWidgets import QListView
from PySide6.QtWidgets import QListWidget
from PySide6.QtWidgets import QMainWindow
from PySide6.QtWidgets import QMenu
from PySide6.QtWidgets import QPushButton
from PySide6.QtWidgets import QVBoxLayout
from PySide6.QtWidgets import QWidget
class PostListModel(QAbstractListModel):
def __init__(self, full_list: List[str], prefix: str):
super().__init__()
self.full_list = full_list
self.prefix = prefix
self.endResetModel()
def endResetModel(self) -> None:
super().endResetModel()
self.my_list = [
element for element in self.full_list if element.startswith(self.prefix)
]
def rowCount(self, parent=None) -> int:
return len(self.my_list)
def data(
self, index: Union[QModelIndex, QPersistentModelIndex], role: int = None
) -> Any:
if role == Qt.DisplayRole or role == Qt.ItemDataRole:
return self.my_list[index.row()]
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(1200, 500)
prefixes = ["Left", "Center", "Right"]
self.full_list = [f"{prefix} {i}" for i in range(10) for prefix in prefixes]
central_widget = QWidget(parent=self)
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(parent=central_widget)
central_widget.setLayout(main_layout)
columns_layout = QHBoxLayout(parent=main_layout)
main_layout.addLayout(columns_layout)
self.list_models = {}
self.list_views = {}
for prefix in prefixes:
list_view = QListView(parent=central_widget)
list_model = PostListModel(self.full_list, prefix)
self.list_models[prefix] = list_model
self.list_views[prefix] = list_view
list_view.setModel(list_model)
list_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
list_view.customContextMenuRequested.connect(
lambda pos: self.show_context_menu(list_view, pos)
)
columns_layout.addWidget(list_view)
print("Created:", list_view)
def show_context_menu(self, list_view, pos: QPoint):
print("Context menu on:", list_view)
global_pos = list_view.mapToGlobal(pos)
index_in_model = list_view.indexAt(pos).row()
element = list_view.model().my_list[index_in_model]
menu = QMenu()
menu.addAction("Edit", lambda: print(element))
menu.exec(global_pos)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
retval = app.exec()
sys.exit(retval)
if __name__ == "__main__":
main()
When it is run like that, it gives this output:
Created: <PySide6.QtWidgets.QListView(0x55f2dcc5c980) at 0x7f1d7cc9d780>
Created: <PySide6.QtWidgets.QListView(0x55f2dcc64e10) at 0x7f1d7cc9da40>
Created: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>
Context menu on: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>
I think that the issue is somewhere in the binding of the lambda to the signal. It seems to always take the last value. Since I re-assign list_view, I don't think that the reference within the lambda would change.
Something is wrong with the references, but I cannot see it. Do you see why the lambda connected to the context menu signal always has the last list view as context?
This is happening because lambdas are not immutable objects. All non parameter variables used inside of a lambda will update whenever that variable updates.
So when you connect to the slot:
lambda pos: self.show_context_menu(list_view, pos)
On each iteration of your for loop you are setting the current value of the list_view variable as the first argument of the show_context_menu method. But when the list_view changes for the second and proceeding iterations, it is also updating for all the preceding iterations.
Here is a really simple example:
names = ["alice", "bob", "chris"]
funcs = []
for name in names:
funcs.append(lambda: print(f"Hello {name}"))
for func in funcs:
func()
Output:
Hello chris
Hello chris
Hello chris
Hopefully I am following the guidelines correctly here with my first question. I am trying to create a GUI with the MVC structure. I am having difficulty with understanding why my signals are not always being picked up by the controller. I know that there is just something simple that I'm missing. I'm attaching code from a simple calculator which I used as a guide. I removed most of the features to simplify this as much as possible. It is now only 3 of the original buttons and my own button. For debugging, I just have the value on the button printed out when you press it.
import sys
# Import QApplication and the required widgets from PyQt5.QtWidgets
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import QMainWindow
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QGridLayout
from PySide2.QtWidgets import QLineEdit
from PySide2.QtWidgets import QPushButton
from PySide2.QtWidgets import QVBoxLayout
from functools import partial
ERROR_MSG = 'ERROR'
# Create a subclass of QMainWindow to setup the calculator's GUI
class PyCalcUi(QMainWindow):
"""PyCalc's View (GUI)."""
def __init__(self):
"""View initializer."""
super().__init__()
# Set some main window's properties
self.setWindowTitle('PyCalc')
self.setFixedSize(235, 235)
# Set the central widget and the general layout
self.generalLayout = QVBoxLayout()
self._centralWidget = QWidget(self)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Create the display and the buttons
self._createDisplay()
self._createButtons()
def _createDisplay(self):
"""Create the display."""
# Create the display widget
self.display = QLineEdit()
# Set some display's properties
self.display.setFixedHeight(35)
self.display.setAlignment(Qt.AlignRight)
self.display.setReadOnly(True)
# Add the display to the general layout
self.generalLayout.addWidget(self.display)
def _createButtons(self):
"""Create the buttons."""
self.buttons = {}
buttonsLayout = QGridLayout()
# Button text | position on the QGridLayout
buttons = {'7': (0, 0),
'8': (0, 1),
'9': (0, 2),
}
# Create the buttons and add them to the grid layout
for btnText, pos in buttons.items():
self.buttons[btnText] = QPushButton(btnText)
self.buttons[btnText].setFixedSize(40, 40)
buttonsLayout.addWidget(self.buttons[btnText], pos[0], pos[1])
self.mybutton = QPushButton("5")
buttonsLayout.addWidget(self.mybutton,1,0)
# Add buttonsLayout to the general layout
self.generalLayout.addLayout(buttonsLayout)
# Create a Controller class to connect the GUI and the model
class PyCalcCtrl:
"""PyCalc Controller class."""
def __init__(self, model, view):
"""Controller initializer."""
self._evaluate = model
self._view = view
# Connect signals and slots
self._connectSignals()
def _printthis(self):
print("Hi")
def _printthat(self, buttonvalue):
print(buttonvalue)
def _connectSignals(self):
"""Connect signals and slots."""
self._view.mybutton.clicked.connect(self._printthis)
for btnText, btn in self._view.buttons.items():
btn.clicked.connect(partial(self._printthat, btnText))
# Create a Model to handle the calculator's operation
def evaluateExpression(expression):
"""Evaluate an expression."""
try:
result = str(eval(expression, {}, {}))
except Exception:
result = ERROR_MSG
return result
# Client code
def main():
"""Main function."""
# Create an instance of QApplication if it doesn't exist
pycalc = QApplication.instance()
if pycalc is None:
pycalc = QApplication(sys.argv)
# Show the calculator's GUI
view = PyCalcUi()
view.show()
# Create instances of the model and the controller
model = evaluateExpression
PyCalcCtrl(model=model, view=view)
# Execute the calculator's main loop
sys.exit(pycalc.exec_())
if __name__ == '__main__':
main()
This set of code works, BUT if I comment out the
for btnText, btn in self._view.buttons.items():
btn.clicked.connect(partial(self._printthat, btnText))
The self._view.mybutton.clicked.connect(self._printthis) will no longer work.
What is the btn.clicked.connect(partial(self._printthat, btnText)) line doing which is allowing any other signal I put in def _connectSignals(self): to work. What aspect of that line is achieving something that the mybutton signal isn't doing?
The problem is caused because the PyCalcCtrl object is not assigned to a variable so it will be destroyed and therefore the "_printthis" method will not be accessible. On the other hand, when functools.partial is used then the object of the PyCalcCtrl class is assigned to the scope of that function, that's why it works.
The solution is to assign the PyCalcCtrl object to a variable:
ctrl = PyCalcCtrl(model=model, view=view)
I am coding a GUI program now and I have two separate PyQt5 widget objects that need to communicate with each other. I have something that works now (which I have provided a simplified example of below), but I suspect there is a more robust way of doing this that I am hoping to learn about. I will summarize the functionality below, for those that would like a bit of an intro to the code.
TL;DR: Please help me find a better way to use a button click in object 1 to change a variable in object 2 that sends the coordinates of a mouse click in object 2 to object 1 where those coordinates populate two spin boxes.
This first MainWindow class is where the widget objects are defined. The two objects of interest are MainWindow.plotWidget, an instance of the MplFig class, and MainWindow.linePt1, an instance of the LineEndpoint class. Note here that I am able to pass the self.plotWidget as an argument into the LineEndpoint object, but since MainWindow.plotWidget is defined first, I cannot pass self.linePt1 as an argument there.
The functionality I have achieved with these widgets is a button in LineEndpoint (LineEndpoint.chooseBtn) that, when clicked, changes a variable in MplFig (MplFig.waitingForPt) from None to the value of ptNum which is passed as an argument of LineEndpoint (in the case of linePt1, this value is 1). MplFig has button press events tied to the method MplFig.onClick() which, is MplFig.onClick is not None, passes the coordinates of the mouse click to the two QDoubleSpinBox objects in LineEndpoint.ptXSpin and LineEndpoint.ptYSpin. To achieve this, I pass self as the parent argument when I create the MainWIndow.plotWidget object of MplFig. I set the parent as self.parent which allows me to call the LineEndpoint object as self.parent.linePt1, which from there allows me to access the spin boxes.
This seems like a round-a-bout way of doing things and I'm wondering if anybody could suggest a better way of structuring this functionality? I like the method of passing the MplFig object as an argument to the LineEndpoint class as that makes it clear from the init method in the class definition that the LineEndpoint class communicates with the MplFig class. I know I cannot have both classes depend on each other in the same way, but i would love to learn a way of doing this that still makes it clear in the code that the objects are communicating. I am still open to all suggestions though!
from PyQt5.QtWidgets import (
QMainWindow, QApplication, QLabel, QLineEdit, QPushButton, QFileDialog,
QWidget, QHBoxLayout, QVBoxLayout, QMessageBox, QListWidget,
QAbstractItemView, QDoubleSpinBox
)
from PyQt5.QtCore import Qt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import (
FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
)
import sys # need sys to pass argv to QApplication
class MplFig(FigureCanvasQTAgg):
def __init__(self, parent):
self.fig = Figure()
super().__init__(self.fig)
self.parent = parent
self.waitingForPt = None
self.fig.canvas.mpl_connect('button_press_event', self.onClick)
self.ax = self.figure.add_subplot(111)
def onClick(self, e):
if self.waitingForPt is not None:
if self.waitingForPt == 1:
lineObj = self.parent.linePt1
roundX = round(e.xdata, lineObj.ptPrec)
roundY = round(e.ydata, lineObj.ptPrec)
print(f'x{self.waitingForPt}: {roundX}, '
f'y{self.waitingForPt}: {roundY}'
)
lineObj.ptXSpin.setValue(roundX)
lineObj.ptYSpin.setValue(roundY)
lineObj.chooseBtn.setStyleSheet(
'background-color: light gray'
)
self.waitingForPt = None
class LineEndpoint(QWidget):
def __init__(self, parent, mplObject, ptNum, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.mpl = mplObject
self.layout = QVBoxLayout()
row0Layout = QHBoxLayout()
ptXLabel = QLabel(f'X{ptNum}:')
row0Layout.addWidget(ptXLabel)
ptMin = 0
ptMax = 1000
ptStep = 1
self.ptPrec = 2
self.ptXSpin = QDoubleSpinBox()
self.ptXSpin.setSingleStep(ptStep)
self.ptXSpin.setMinimum(ptMin)
self.ptXSpin.setMaximum(ptMax)
self.ptXSpin.setDecimals(self.ptPrec)
row0Layout.addWidget(self.ptXSpin)
ptYLabel = QLabel(f'Y{ptNum}:')
row0Layout.addWidget(ptYLabel)
self.ptYSpin = QDoubleSpinBox()
self.ptYSpin.setMinimum(ptMin)
self.ptYSpin.setMaximum(ptMax)
self.ptYSpin.setSingleStep(ptStep)
self.ptYSpin.setDecimals(self.ptPrec)
row0Layout.addWidget(self.ptYSpin)
self.layout.addLayout(row0Layout)
row1Layout = QHBoxLayout()
self.chooseBtn = QPushButton('Choose on Plot')
self.chooseBtn.clicked.connect(lambda: self.chooseBtnClicked(ptNum))
row1Layout.addWidget(self.chooseBtn)
self.layout.addLayout(row1Layout)
def chooseBtnClicked(self, endpointNum):
print(f'Choosing point {endpointNum}...')
self.chooseBtn.setStyleSheet('background-color: red')
self.mpl.waitingForPt = endpointNum
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setLayouts()
def setLayouts(self):
self.sideBySideLayout = QHBoxLayout()
self.plotWidget = MplFig(self)
self.sideBySideLayout.addWidget(self.plotWidget)
self.linePt1 = LineEndpoint(self, self.plotWidget, 1)
self.sideBySideLayout.addLayout(self.linePt1.layout)
mainContainer = QWidget()
mainContainer.setLayout(self.sideBySideLayout)
self.setCentralWidget(mainContainer)
QApp = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(QApp.exec_())
If you want to transmit information between objects (remember that classes are only abstractions) then you must use signals:
import sys
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QApplication,
QDoubleSpinBox,
QGridLayout,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QWidget,
)
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
class MplFig(FigureCanvasQTAgg):
clicked = pyqtSignal(float, float)
def __init__(self, parent=None):
super().__init__(Figure())
self.setParent(parent)
self.figure.canvas.mpl_connect("button_press_event", self.onClick)
self.ax = self.figure.add_subplot(111)
def onClick(self, e):
self.clicked.emit(e.xdata, e.ydata)
class LineEndpoint(QWidget):
def __init__(self, ptNum, parent=None):
super().__init__(parent)
ptMin = 0
ptMax = 1000
ptStep = 1
ptPrec = 2
self.ptXSpin = QDoubleSpinBox(
singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
)
self.ptYSpin = QDoubleSpinBox(
singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
)
self.chooseBtn = QPushButton("Choose on Plot", checkable=True)
self.chooseBtn.setStyleSheet(
"""
QPushButton{
background-color: light gray
}
QPushButton:checked{
background-color: red
}"""
)
lay = QGridLayout(self)
lay.addWidget(QLabel(f"X{ptNum}"), 0, 0)
lay.addWidget(self.ptXSpin, 0, 1)
lay.addWidget(QLabel(f"Y{ptNum}"), 0, 2)
lay.addWidget(self.ptYSpin, 0, 3)
lay.addWidget(self.chooseBtn, 1, 0, 1, 4)
lay.setRowStretch(lay.rowCount(), 1)
#pyqtSlot(float, float)
def update_point(self, x, y):
if self.chooseBtn.isChecked():
self.ptXSpin.setValue(x)
self.ptYSpin.setValue(y)
self.chooseBtn.setChecked(False)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setLayouts()
def setLayouts(self):
self.plotWidget = MplFig()
self.linePt1 = LineEndpoint(1)
self.plotWidget.clicked.connect(self.linePt1.update_point)
mainContainer = QWidget()
lay = QHBoxLayout(mainContainer)
lay.addWidget(self.plotWidget)
lay.addWidget(self.linePt1)
self.setCentralWidget(mainContainer)
QApp = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(QApp.exec_())
I wonder if it is possible to change the languages(translations) dynamically without using qt designer to make the UI? That means I don't want to use the function retranslateUi() to update the program interface.
Here is my code, but I'm stuck on lines marked #1 #2 #3. Don't know what I should use to update the interface.
import sys
from PyQt5.QtCore import Qt, QTranslator
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel,
QComboBox, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.button = QPushButton(self.tr('Start'), self)
self.label = QLabel(self.tr('Hello, World'), self)
self.label.setAlignment(Qt.AlignCenter)
self.combo = QComboBox(self)
self.combo.addItem('English')
self.combo.addItem('中文')
self.combo.addItem('français')
self.combo.currentTextChanged.connect(self.change_func)
self.trans = QTranslator(self)
self.v_layout = QVBoxLayout()
self.v_layout.addWidget(self.combo)
self.v_layout.addWidget(self.button)
self.v_layout.addWidget(self.label)
self.setLayout(self.v_layout)
def change_func(self):
print(self.combo.currentText())
if self.combo.currentText() == '中文':
self.trans.load('eng-chs')
_app = QApplication.instance()
_app.installTranslator(self.trans)
# 1
elif self.combo.currentText() == 'français':
self.trans.load('eng-fr')
_app = QApplication.instance()
_app.installTranslator(self.trans)
# 2
else:
_app = QApplication.instance()
_app.removeTranslator(self.trans)
# 3
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
Any help would be appreciated.
TL; DR; It is not necessary to use Qt Designer
You should not use Qt Designer necessarily but you should use the same technique, that is, create a method that could be called retranslateUi() and in it set the texts using translate() instead of tr() (for more details read the docs). Calling that method when you change language for it must use the changeEvent() event. For example in your case the code is as follows:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Demo(QtWidgets.QWidget):
def __init__(self):
super(Demo, self).__init__()
self.button = QtWidgets.QPushButton()
self.label = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
self.combo = QtWidgets.QComboBox(self)
self.combo.currentIndexChanged.connect(self.change_func)
self.trans = QtCore.QTranslator(self)
self.v_layout = QtWidgets.QVBoxLayout(self)
self.v_layout.addWidget(self.combo)
self.v_layout.addWidget(self.button)
self.v_layout.addWidget(self.label)
options = ([('English', ''), ('français', 'eng-fr' ), ('中文', 'eng-chs'), ])
for i, (text, lang) in enumerate(options):
self.combo.addItem(text)
self.combo.setItemData(i, lang)
self.retranslateUi()
#QtCore.pyqtSlot(int)
def change_func(self, index):
data = self.combo.itemData(index)
if data:
self.trans.load(data)
QtWidgets.QApplication.instance().installTranslator(self.trans)
else:
QtWidgets.QApplication.instance().removeTranslator(self.trans)
def changeEvent(self, event):
if event.type() == QtCore.QEvent.LanguageChange:
self.retranslateUi()
super(Demo, self).changeEvent(event)
def retranslateUi(self):
self.button.setText(QtWidgets.QApplication.translate('Demo', 'Start'))
self.label.setText(QtWidgets.QApplication.translate('Demo', 'Hello, World'))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
Then generate the .ts:
pylupdate5 main.py -ts eng-chs.ts
pylupdate5 main.py -ts eng-fr.ts
Then use Qt Linguist to do the translations.
And finally the .qm:
lrelease eng-fr.ts eng-chs.qm
The complete project you find here.
No you will have to use a technique like Qt Designer does with retranslateUi because the Qt widget system does not have a way to redo the translation on it's own (otherwise QT Designer would be using that to).
Building such a system would require a fundamental change to the widgets as for each string property you would need to know that it contains a translatable string (not a data value) and knowing the original string for looking up the new translation would be best (reversing translations could be ambiguous).
What is the best method/practice for emitting a signal upon entering either a QGraphicsWidget or a QGraphicsItem ?
In my MWE I would like to trigger a call to MainWindow.update, from Square.hoverEnterEvent, whenever the user mouse(s) over an item in a QGraphicsScene. The trouble is that QGraphicsItem/Widget is not really designed to emit signals. Instead these classes are setup to handle events passed down to them from QGraphicsScene. QGraphicsScene handles the case that the user has selected an item but does not appear to handle mouse entry events, At least there is no mechanism for entryEvent to percolate up to the parent widget/window.
import sys
from PyQt5.QtWidgets import QWidget, QApplication, qApp, QMainWindow, QGraphicsScene, QGraphicsView, QStatusBar, QGraphicsWidget, QStyle
from PyQt5.QtCore import Qt, QSizeF
class Square(QGraphicsWidget) :
"""
doc string
"""
def __init__(self,*args, name = None, **kvps) :
super().__init__(*args, **kvps)
self.radius = 5
self.name = name
self.setAcceptHoverEvents(True)
def sizeHint(self, hint, size):
size = super().sizeHint(hint, size)
print(size)
return QSizeF(50,50)
def paint(self, painter, options, widget):
self.initStyleOption(options)
ink = options.palette.highLight() if options.state == QStyle.State_Selected else options.palette.button()
painter.setBrush(ink) # ink
painter.drawRoundedRect(self.rect(), self.radius, self.radius)
def hoverEnterEvent(self, event) :
print("Enter Event")
super().hoverEnterEvent(event)
class MainWindow(QMainWindow):
def __init__(self, *args, **kvps) :
super().__init__(*args, **kvps)
# Status bar
self.stat = QStatusBar(self)
self.setStatusBar(self.stat)
self.stat.showMessage("Started")
# Widget(s)
self.data = QGraphicsScene(self)
self.view = QGraphicsView(self.data, self)
item = self.data.addItem(Square())
self.view.ensureVisible(self.data.sceneRect())
self.setCentralWidget(self.view)
# Visibility
self.showMaximized()
def update(self, widget) :
self.stat.showMessage(str(widget.name))
if __name__ == "__main__" :
# Application
app = QApplication(sys.argv)
# Scene Tests
main = MainWindow()
main.show()
# Loop
sys.exit(app.exec_())
The docs state that QGraphicsItem is not designed to emit signals, instead it is meant to respond to the events sent to it by QGraphicsScene. In contrast it seems that QGraphicsWidget is designed to do so but I'm not entirely sure where the entry point ought to be. Personally I feel QGraphicsScene should really be emitting these signals, from what I understand of the design, but am not sure where the entry point ought to be in this case either.
Currently I see the following possible solutions, with #3 being the preferred method. I was wondering if anyone else had a better strategy :
Create a QGraphicsScene subclass, let's call it Scene, to each QGraphicsItem/QGraphicsWidget and call a custom trigger/signal upon the Scene from each widget. Here I would have to subclass any item I intend on including within the scene.
Set Mainwindow up as the event filter for each item in the scene or upon the scene itself and calling MainWindow.update.
Set Mainwindow.data to be a subclass of QGraphicsScene, let's call it Scene, and let it filter it's own events emitting a hoverEntry signal. hoverEntry is then connected to MainWindow.update as necessary.
As Murphy's Law would have it Ekhumoro already provides an answer.
It seems one should subclass QGraphicsScene and add the necessary signal. this is then triggered from the QGraphicsItem/Widget. This requires that all items within a scene be sub-classed to ensure that they call the corresponding emit function but it seems must do this do this anyhow when working with the graphics scene stuff.
I'll not mark this as answered for a bit in case some one has a better suggestion.
import sys
from PyQt5.QtWidgets import QWidget, QApplication, qApp, QMainWindow, QGraphicsScene, QGraphicsView, QStatusBar, QGraphicsWidget, QStyle, QGraphicsItem
from PyQt5.QtCore import Qt, QSizeF, pyqtSignal
class Square(QGraphicsWidget) :
"""
doc string
"""
def __init__(self,*args, name = None, **kvps) :
super().__init__(*args, **kvps)
self.radius = 5
self.name = name
self.setAcceptHoverEvents(True)
self.setFlag(self.ItemIsSelectable)
self.setFlag(self.ItemIsFocusable)
def sizeHint(self, hint, size):
size = super().sizeHint(hint, size)
print(size)
return QSizeF(50,50)
def paint(self, painter, options, widget):
self.initStyleOption(options)
ink = options.palette.highLight() if options.state == QStyle.State_Selected else options.palette.button()
painter.setBrush(ink) # ink
painter.drawRoundedRect(self.rect(), self.radius, self.radius)
def hoverEnterEvent(self, event) :
super().hoverEnterEvent(event)
self.scene().entered.emit(self)
self.update()
class GraphicsScene(QGraphicsScene) :
entered = pyqtSignal([QGraphicsItem],[QGraphicsWidget])
class MainWindow(QMainWindow):
def __init__(self, *args, **kvps) :
super().__init__(*args, **kvps)
# Status bar
self.stat = QStatusBar(self)
self.setStatusBar(self.stat)
self.stat.showMessage("Started")
# Widget(s)
self.data = GraphicsScene(self)
self.data.entered.connect(self.itemInfo)
self.data.focusItemChanged.connect(self.update)
self.view = QGraphicsView(self.data, self)
item = Square(name = "A")
item.setPos( 50,0)
self.data.addItem(item)
item = Square(name = "B")
item.setPos(-50,0)
self.data.addItem(item)
self.view.ensureVisible(self.data.sceneRect())
self.setCentralWidget(self.view)
# Visibility
self.showMaximized()
def itemInfo(self, item):
print("Here it is -> ", item)
if __name__ == "__main__" :
# Application
app = QApplication(sys.argv)
# Scene Tests
main = MainWindow()
main.show()
# Loop
sys.exit(app.exec_())
The magic lines of interest are then the QGrahicsScene subclass.
class GraphicsScene(QGraphicsScene) :
entered = pyqtSignal([QGraphicsItem],[QGraphicsWidget])
The QGraphicsWidget.hoverEnterEvent triggers the entered signal. (This is where I got stuck)
def hoverEnterEvent(self, event) :
...
self.scene().entered.emit(self)
...
And the switcheroo from self.data = QGraphicsScene(...) to self.data = GraphicsScene in the MainWindow's init function.