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 :

Gtk Windows

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:

Qt

Follow PySide2 installation instructions.

Minimal Application

Qt

Application windows image


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

Application windows image

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.