Qt / Gtk comparison (python bindings)
A comparison of Qt and Gtk illustrated by a simple (but complete) example. Source code for the examples are available here.
About Qt and Gtk
- Qt is pushed by The Qt Compagny. Al lot of work. A wonderful code ide, QtCreator. Efforts on embeded devices. A non-exhaustive list of Qt-powered software can be found here.
- Gtk is pushed by RedHat.
Summary
This project was prompted by my experience of implementing a program for multiple OS (MacOS, Linux and Windows) with the inheritance of parts including both libraries (and more).
The example proposed implements the same GUI (an image, a button and a text entry) in Python: once using the Qt binding and a second time using the Gtk binding.
Python was chosen for its rich ecosystem and the possibility to integrate it into a closed source software (The LGPL allows the integration in a proprietary software as long as the binding between the two is dynamic).
Example used : a very simple form
- a text entry
- a button
- a svg image displayed
It looks like that :
Comparison summary
Gtk | Qt | Comment | |
---|---|---|---|
Gui layout creation tool | Glade (unstable on some platforms) | QtDesigner (part of Qt creator) | Quite similar actually. You configure your view, and it generates an xml, later loaded by your application. |
Connecting the view | Events handlers are passed as a dictionary that is shorter to write | Events handlers have to be connected one by one. Longer than Gtk but possibly healthier | |
Styling | css-like is possible (not perfectly documented) | qss files (css-like also) | Equivalent. QT is probably better documented. |
Internationalisation (i18n) | Handled by gettext | Handled by Qt Translation | Qt has a good tool for translation, called Qt Linguist. It allows to easily work with a translator |
Packaging | pyinstaller works well | pyinstaller works well | Equivalent, in both cases I used the development branch) |
Qt have my preference for the IDE, documentation and robustness, but if some reason makes you prefer Gtk, it is absolutely usable and provides satisfying results - just do not rely too much on the official documentation.
Note : there are a lot of jobs offer for QT/QML developpers, because they are used inside a lot of embeded devices, such as internet boxes, or cars infotainement system. If you plan to work further on HMI professionnaly, it is probably better to choose Qt.
Install and run
Gtk
Follow the getting started guide to install gtk and pygobgect.
Usefull links:
- python GObject introspection : Nice documentation of all Gtk classes and objects in python.
- pygobject getting started guide : follow the install instruction from here, and not from Gtk official site
- python gtk3 tutorial : perfect place to begin learning gtk, and provides some nice overview of widgets, and good examples.
Qt
Follow PySide2 installation instructions.
Minimal Application
Qt
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QPushButton, QLineEdit
from PySide2.QtCore import QFile, QIODevice
def text_changed(text):
print("Received text : ", text)
def button_clicked(button):
print("Button Clicked !")
if __name__ == "__main__":
app = QApplication(sys.argv)
ui_file_name = "form.ui"
ui_file = QFile(ui_file_name)
if not ui_file.open(QIODevice.ReadOnly):
print("Cannot open {}: {}".format(ui_file_name, ui_file.errorString()))
sys.exit(-1)
loader = QUiLoader()
window = loader.load(ui_file)
ui_file.close()
if not window:
print(loader.errorString())
sys.exit(-1)
line = window.findChild(QLineEdit, 'lineEdit')
line.textChanged.connect(text_changed)
btn = window.findChild(QPushButton, 'pushButton')
btn.clicked.connect(button_clicked)
window.show()
sys.exit(app.exec_())
Gtk
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
def text_changed(widget):
print("Received text : ", widget.get_text())
def button_clicked(button):
print("Button Clicked !")
if __name__ == "__main__":
handlers = {"text_changed":text_changed, "button_clicked":button_clicked}
builder = Gtk.Builder()
builder.add_from_file("form.glade")
builder.connect_signals(handlers)
window = builder.get_object("main_window")
window.show_all()
Gtk.main()
Styling
Both Qt and Gtk offer a css-like styling, easy to apply. Not being a perfect css expert myself, I’ll let you judge by looking examples below, where I demonstrate how to style a simple push button.
Styling Qt
def load_css(app, css_path):
with open(css_path, "r") as f:
_style = f.read()
app.setStyleSheet(_style)
QPushButton {
background-color: transparent;
border-radius:0px;
border-width: 0px;
}
QPushButton:hover {
background-color: blue;
color: white;
}
Styling Gtk
def load_css(css_path):
'''loads a css file globally'''
provider = Gtk.CssProvider()
display = Gdk.Display.get_default()
screen = Gdk.Display.get_default_screen(display)
Gtk.StyleContext.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
provider.load_from_path(css_path)
button {
background-color: transparent;
border-width: 0px;
border-radius: 0px;
}
button:hover {
background-image: none;
background-color: blue;
color: white;
}
Translation
Translating Qt
Install pyside2-tools
sudo dnf install pyside2-tools
modify python code
When you want a translatable string:
- A function called
QCoreApplication.translate(context, "Hello")
do the job. You can put any string in context (well you know what context is supposed to mean), since it is copied in .ts file for translation. It if usefull to know which class or file you are translating. - If inside a class, it is probably better to inherit from QObject then call
self.tr("your text")
Create a .pro file
This will allow you to list source and destination files. In our example (myapp.pro):
SOURCES = main.py
FORMS = form.ui
TRANSLATIONS = i18n/en_US.ts i18n/fr_FR.ts
generate your translation files
pyside2-lupdate myapp.pro
Edit generated ts files to create translations
Those are xml. You can edit them manually, or use Qt Linguist (installed via a separated package on my fedora).
compile translations
In Qt Linguist, do File>Release. There should be a lrelease util somewhere, like lrelease-qt5
. But in my case it was with the qt5-linguist package, so I used the gui.
load the needed translation
translator = QTranslator()
translator.load('i18n/fr_FR')
app = QApplication(sys.argv)
app.installTranslator(translator)
Notes
- You could use some mix of qt translation and gettext proposed below, if for a reason you prefer gettext.
- gettext utility msgfmt seem to be able to produce some .qm files if needed, but I don’t have a clue as of how to create the original .po files.
Translating Gtk
Gtk translation is based on gettext. We use two utilities to compile strings used in program, and their translation.
1/ Collect strings that needs translation
xgettext <file to generate po template for>
note : for glade ui files, some additional options are needed:
xgettext -o form.po -L Glade form.glade
2/ Translate collected strings
Open your .po file, and fill fields that needs it :
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-03 12:12+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
I think that the only mandatory field is to set CHARSET to UTF-8.
Then you’ll have to make a copy of this file for each language you want to have in your translation list. You should name your copies using language codes
3/ Compile previous list so gettext understand it
msgfmt <.po to compile>
. You can compute multiple pos into one mo if you dispatched your app translation data.
4/ create a proper gettext arborescence
At least in python, gettext, available lang are found using the find method, which looks into : localedir/language/LC_MESSAGES/domain.mo
.
localedir
, language
and domain
are arguments to the function translation
used to load all translations.
So if you want to translate into english and french, and that you are storing your translation file into a folder named locale, you’ll have something like :
├── locale
│ ├── en
│ │ └── LC_MESSAGES
│ │ └── myapp.mo
│ └── fr
│ └── LC_MESSAGES
│ └── myapp.mo
5/ load correct language
def load_lang(localedir, lang):
'''localedir : absolute path of directory where your language files are, ex /usr/share/locale'''
# This part for Gtk.Builder .glade files translation
lang_to_locale={
'fr':'fr_FR.utf8',
'en':'en_US.utf8'
}
locale.bindtextdomain('myapp', localedir)
locale.setlocale(locale.LC_ALL, lang_to_locale[lang])
# This part for python strings translation
tr_en = gettext.translation('myapp', localedir=localedir, languages=[lang])
tr_en.install()
For Gtk Builder to use the correct translation file you have to add the following line : locale.bindtextdomain('myapp', localedir)
. This is because gettext module does not sets the text domain at libc level, but locale does. (stack overflow answer).
Another way to contourn the problem, is to follow this method, which acually parse the whole xml and translate everything tagged as translatable in it, using python gettext (not recommended).
Conclusion :translation in gtk is messy, because we have to handle both python translation and libc related translations, leading to the use of both locale and gettext modules.
Packaging
Using Pyinstaller
Pyinstaller is a really cool tool to create cross-platfomr python app.
Create a spec template
pyinstaller main.py
Qt
Same process as Gtk, but there is an hidden import to add. Hidden import is just a module dependency not detected by pyinstaller.
Gtk
modify main.spec file with the following :
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
embedded_data=[ ( 'locale', 'locale' ),
('*.css', '.'),
('*.glade', '.')
]
a = Analysis(['main.py'],
pathex=['/home/bf/dev/qt-versus-gtk/gtk'],
binaries=[],
datas=embedded_data,
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='main')
Version 4.0 has a bug in macos / linux that collect all /share folder. Use the dev version or you’ll create an app with a GB size !.
Important note : your generated app, according to the pyinstaller documentation, is dynamically linked to the host os libc. Which is not retro compatible. Meaning that your app won’t run natively on OS havaing an older version of libc than yours.
Conclusion about pyinstaller
packages size
- gtk generated folder is about 200MB large
- qt generated folder is about 400MB large
This seems of course too big four our dummy application. One should exclude some packages. For example in qt we see that some big dll used for Web rendering are included, we of course does not need them.
Going further with pyinstaller
- The pyinstaller onefile option allows to generate a single executable, but it didn’t worked out of the box for me.
- One could generate some installer or package.
Other options
There are of course a lot of other options to be discussed. I won’t speak of it there, but I’ll try to write another article with a more global view.