How to change languages(translations) dynamically on PyQt5? - python

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).

Related

Qaction Shortcut in extended contextmenu not triggered

I try to extend the contextmenu of a QLineEdit with an additional entry for replacing text. I can extend the contextmenu with .createStandardContextMenu(), which works fine. But when I try to add a shortcut with .setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R)) it will not react on the key. Same with different keys, which I tried. In addition the shortcut made with QAction('&Replace', self) doesn't work too.
Some examples here in SO and other sources are constructed in the same way, so I'm wondering that nobody else has got the same problem. Seems that I'm missing anything. But what? I can't figure out, checked the docs multiple times.
Working Example:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
print("my_contextMenuEvent")
menu = self.lineEdit.createStandardContextMenu()
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
menu.addAction(action)
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()
Based on musicamante's comment I came to the following solution:
Extract from the docs:
If you want to extend the standard context menu, reimplement this
function, call createStandardContextMenu() and extend the menu
returned.
The default use of the QAction list (as returned by actions()) is to
create a context QMenu.
It's not totally logically to me, not for the 1st time ;-)
Final code:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class ECM(QWidget):
def __init__(self):
super(ECM, self).__init__()
self.setWindowTitle("Extended Context Menu")
self.lineEdit = QLineEdit()
self.lineEdit.setContextMenuPolicy(Qt.CustomContextMenu)
self.lineEdit.customContextMenuRequested.connect(self.my_contextMenuEvent)
layout = QVBoxLayout()
layout.addWidget(self.lineEdit)
self.setLayout(layout)
self.setFixedSize(800,200)
action = QAction('&Replace', self)
action.setStatusTip('Replace values')
action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))
action.triggered.connect(self.replace)
self.lineEdit.addAction(action)
self.show()
def replace(self):
print("replace")
def my_contextMenuEvent(self):
menu = self.lineEdit.createStandardContextMenu()
menu.addActions(self.lineEdit.actions())
menu.exec_()
if __name__ == '__main__':
app = QApplication(sys.argv)
sender = ECM()
app.exec_()

C++ type is not supported as a pyqtSignal() type argument type

I'm trying to implement a state machine in my GUI (using python3.8 and Qt5 (using PyQt5 and not PySides!)). The problem I encounter with this is when adding a transistion based on an event.
Using the code below, the interpreter complains at line 26:
s1.addTransition(self.btn_start, pyqtSignal("clicked()"), s2)
TypeError: C++ type 'clicked()' is not supported as a pyqtSignal() type argument type
import sys
import logging
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QApplication
from PyQt5.QtCore import QStateMachine, QState, QFinalState, pyqtSignal
class TabTest(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.logger = logging.getLogger("TABTEST")
self.btn_start = QPushButton("START")
# self.btn_start.clicked.connect(self.btn_start_clicked)
layout = QVBoxLayout()
layout.addWidget(self.btn_start)
self.setLayout(layout)
machine = QStateMachine()
s1 = QState()
s1.assignProperty(self.btn_start, "TEXT", "CLICK ME")
s2 = QFinalState()
s1.addTransition(self.btn_start, pyqtSignal("clicked()"), s2)
machine.addState(s1)
machine.addState(s2)
machine.setInitialState(s1)
machine.start()
# def btn_start_clicked(self):
# self.logger.info("klikked")
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = TabTest()
main_window.show()
sys.exit(app.exec_())
I googled before and read on stackoverflow but all answers are for Qt4 or are using PySides. Lots of changes between Qt5 and Qt4.
And PySides has PySides.QtCore.SIGNAL, I found the equivalent in PyQt5 is PyQt5.QtCore.pyqtSignal, but can't seem to have it working.
Your code has 3 errors:
QStateMachine is a local variable that will be destroyed instantly preventing the desired behavior from being observed, given that there are 2 solutions: set a parent(change to QStateMachine(self)) or make it a member of the class(change all machine to self.machine).
If you want to assign a property then the name must be respected, in this case the property is "text" and not "TEXT".
In PyQt5 you must pass the signal, that is, obj.foo_signal, in your case self.btn_start.clicked.
Considering the above, the solution is:
class TabTest(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.btn_start = QPushButton("START")
layout = QVBoxLayout(self)
layout.addWidget(self.btn_start)
machine = QStateMachine(self)
s1 = QState()
s1.assignProperty(self.btn_start, "text", "CLICK ME")
s2 = QFinalState()
s1.addTransition(self.btn_start.clicked, s2)
# Check if QFinalState was entered
machine.finished.connect(lambda: print("finished"))
machine.addState(s1)
machine.addState(s2)
machine.setInitialState(s1)
machine.start()

Using a for loop to assign methods to the valueChanged signals of QScrollbars does not seem to work

I am trying to use a for loop to assign methods to the valueChanged signals of QScrollbars to make my code cleaner. However, this does not seem to work correctly. What am I doing wrong?
import sys
from PyQt5.QtWidgets import QScrollBar, QDialog, QVBoxLayout, QApplication
class MainWindow(QDialog):
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.scrollbar1 = QScrollBar(self)
self.scrollbar2 = QScrollBar(self)
self.scrollbars = [self.scrollbar1, self.scrollbar2]
self.names = ['scrollbar 1', 'scrollbar 2']
self.layout.addWidget(self.scrollbar1)
self.layout.addWidget(self.scrollbar2)
for scrollbar, name in zip(self.scrollbars, self.names):
print(scrollbar, name)
scrollbar.valueChanged.connect(lambda: self.test(name))
# self.scrollbar1.valueChanged.connect(
# lambda: self.test(self.names[0])
# )
# self.scrollbar2.valueChanged.connect(
# lambda: self.test(self.names[1])
# )
self.show()
def test(self, scrollbar):
print(scrollbar)
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
sys.exit(app.exec_())
Assigning the methods "manually" works as expected, i.e. different names are passed on. When using the for loop however, for both scrollbars the same name is printed on a value change.
EDIT: Here is my snap_slider method
old:
def snap_slider(scrollbar):
x = np.modf(scrollbar.value() / scrollbar.singleStep())
if x[0] < 0.5:
scrollbar.setSliderPosition(
int(x[1] * scrollbar.singleStep()))
else:
scrollbar.setSliderPosition(
int((x[1]+1) * scrollbar.singleStep()))
new:
def snap_slider(self):
x = np.modf(self.sender().value() / self.sender().singleStep())
if x[0] < 0.5:
self.sender().setSliderPosition(
int(x[1] * self.sender().singleStep()))
else:
self.sender().setSliderPosition(
int((x[1] + 1) * self.sender().singleStep()))
A few things here, since you are trying to make your code cleaner:
You should prefer the use of the sender() method to a lambda function
It is a good practice to isolate the UI setup in a separate function that you could call elsewhere
Same thing about the show() method: avoid to put it in the __init__ method, since you may need to instanciate a MainWindow without showing it (eg: for testing purposes)
Which would give something like:
import sys
from PyQt5.QtWidgets import QScrollBar, QDialog, QVBoxLayout, QApplication
class MainWindow(QDialog):
def __init__(self):
super().__init__()
self.createWidgets()
def createWidgets(self):
self.layout = QVBoxLayout(self)
self.scrollbar1 = QScrollBar(self)
self.scrollbar2 = QScrollBar(self)
for widget in [self.scrollbar1, self.scrollbar2]:
widget.valueChanged.connect(self.test)
self.layout.addWidget(widget)
def test(self, event):
print(self.sender())
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())

I want to know how to use pyqt5 to load the created widget at once

Sorry. I will modify the contents. I would like to load a widget inside def test by pressing Qbutton. Can not you use QStackedWidget to load the widget's configured functions? I've compiled the class and called it, but only a = QLineEdit ('Qline', self). I wonder what should be done to switch widgets.
You can also create a table like html using pyqt5.
import sys
from PyQt5.QtWidgets import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.stacked = QStackedWidget(self)
self.FirstpUI()
def FirstpUI(self):
self.btn1 = QPushButton('test1', self)
self.btn1.move(50,50)
self.btn1.clicked.connect(self.btn1_click)
def test(self):
a = QLineEdit('Qline', self)
b = QLineEdit('Qline2', self)
c = QPushButton('button', self)
a.move(0, 0)
b.move(100, 0)
c.move(50,50)
c.clicked.connect(self.btn2_click)
def btn1_click(self):
self.btn1.deleteLater()
self.stacked.addWidget(self.test())
self.stacked.setCurrentIndex(self.stacked.currentIndex()+1)
def btn2_click(self):
QMessageBox.about(self,'hello','hello2')
if __name__ == "__main__":
app = QApplication(sys.argv)
fream = MainWindow()
fream.show()
app.exec_()
May be I don't know what you real want,because I know that little, I think You can use QtDesigner,it's very useful

How do I make a shortcut using arrow key with PySide2?

I want to add a keyboard shortcut to a button with Qt5 + Python (Pyside2). Code for making a shortcut with a regular key:
import sys
import random
from PySide2 import QtCore, QtWidgets, QtGui
class MyWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.hello = ["Hallo Welt", "你好,世界", "Hei maailma",\
"Hola Mundo", "Привет мир"]
self.button = QtWidgets.QPushButton("Click me!")
self.text = QtWidgets.QLabel("Hello World")
self.text.setAlignment(QtCore.Qt.AlignCenter)
self.text.setFont(QtGui.QFont("Titillium", 30))
self.button.setFont(QtGui.QFont("Titillium", 20))
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.text)
self.layout.addWidget(self.button)
self.setLayout(self.layout)
shortcut = QtWidgets.QShortcut(QtGui.QKeySequence('o'), self.button)
shortcut.activated.connect(self.magic)
self.button.clicked.connect(self.magic)
def magic(self):
self.text.setText(random.choice(self.hello))
if __name__ == "__main__":
app = QtWidgets.QApplication([])
widget = MyWidget()
widget.resize(800, 600)
widget.show()
sys.exit(app.exec_())
If I replace that shortcut = ... line with this:
shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.LeftArrow), self.button)
Nothing happens. What am I missing?
I also tried converting the QtCore.Qt.LeftArrow value to string (nothing happens), and tried making the QShortcut directly with the Qt.LeftArrow (complains about nullptr). Using QtCore.Qt.Key_Left as parameter for QShortcut gave me nullptr too.
I found out:
shortcut = QtWidgets.QShortcut(QtGui.QKeySequence.MoveToPreviousChar, self.button)
The list of actions are here:
http://doc.qt.io/qt-5/qkeysequence.html#StandardKey-enum
With PySide2 something like the following should works:
QtWidgets.QShortcut(QtGui.QKeySequence("right"), self.button, self.magic)
which will directly connect the button with callback function.

Categories