August 4, 2023

Написание приложений на Python с использованием фреймворка GTK

Написание десктопных приложений подразумевает разветвлённую многопоточную систему управления логикой. Причина проста: эвентлуп GTK требует минимальное число операций в функциях по отрисовке интерфейса по причине его подвисания в противном случае. Поэтому подход простой:

1. Написание функций по отрисовке интерфейса

Для разработки требуется установленная в системе библиотека PyGObject. Её предлагается использовать следующим образом:

import gi

gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")

from gi.repository import Gdk, GLib, Gio, Gtk, GObject

Для быстрого прототипирования предлагается отрисовывать формы в Glade и подключать через декоратор:

@Gtk.Template.from_file("main_window.glade")
class MainWindow(Gtk.ApplicationWindow):
  __gtype_name__ = "main_window"
  # подключение к необходимым объектам в прототипе
  rootcont = Gtk.Template.Child("rootcont")

  def __init__(self, **kwargs):
    super().__init__(**kwargs)
    ...

Стилевое оформление прописывается в каскадных таблицах:

provider = Gtk.CssProvider()
provider.load_from_path("./style.css")
screen = Gdk.Screen.get_default()
Gtk.StyleContext.add_provider_for_screen(
  screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)

Использование системы сигналов и слотов на примере с кнопками:

self.get_info_btn.connect("clicked", self.show_info_screen)

Документация по PyGObject: https://lazka.github.io/pgi-docs/Gtk-3.0/index.html

2. Добавление логики в приложение

def threaded_logic():
  # some logic
t = Thread(target=threaded_logic)
t.daemon = True
t.start()

Коммуникацию предлагается обеспечивать через сигналы и очереди:

class MySignals(GObject.GObject):
  __gsignals__ = {
    'get': (GObject.SignalFlags.RUN_FIRST, None, ()),
    'pass': (GObject.SignalFlags.RUN_FIRST, None, ()),
    'default_screen': (GObject.SignalFlags.RUN_FIRST, None, ()),
    'show_error': (GObject.SignalFlags.RUN_FIRST, None, (str,)),
    'show_critical': (GObject.SignalFlags.RUN_FIRST, None, (str,))
  }

signals = MySignals()
some_objects = queue.Queue()

Из потоков напрямую запрещается производить вызов отрисовки интерфейса, равно как и вызывать сигналы (в противном случае - segfault). Предлагается использовать безопасный способ:

Gdk.threads_add_idle(
  GLib.PRIORITY_HIGH_IDLE, signals.emit, 'default_screen'
)
Gdk.threads_add_idle(
  GLib.PRIORITY_HIGH_IDLE, lambda: self.curr_sequencer.prev()
)

3. Последовательность запуска

Сначала запускаются необходимые фоновые потоки, затем - эвентлуп GTK:

class Application(Gtk.Application):
  def __init__(self, *args, **kwargs):
    super().__init__(
      *args,
      application_id="org.comp.app",
      flags=Gio.ApplicationFlags.FLAGS_NONE,
      **kwargs,
    )

  def do_activate(self):
    self.win = MainWindow(application=self)
    self.win.present()
    self.win.show_all()


if __name__ == "__main__":
  Application().run(sys.argv)

Вуаля!