8  Časově náročné operace v GUI

Jedním ze základních problémů aplikací s uživatelským rozhraním, je responzivita, což je schopnost reagovat na akce uživatele v reálném čase (Ramos 2021). Pakliže nebude aplikace reagovat na akce uživatele v reálném čase, pak nebude příliš uživatelsky přívětivá a ani použitelná (Fitzpatrick 2021). Tuto situaci v praxi znají všichni uživatelé, aplikace občas “zamrzne, zatuhne”, přičemž v některých případech dojde po chvíli k obnovení responzivity a někdy je nutné aplikaci nestandardně ukončit (např. použitím Task manageru). Důvody proč k tomuto dochází mohou být různé, od chyb až po náročnost některých operací.

V této kapitole se specificky zaměříme na operace, které trvají delší dobu, a z hlediska uživatele by způsobovaly “zamrznutí” aplikace. V této terminologii lze za dlouho běžící operaci označit jakoukoliv operaci, jejíž zpracování překročí cca 0.5 vteřiny. Tento čas už uživatel zvládne vnímat a rozpozná, že v aplikaci “něco nefunguje”. Běžné změny v GUI, vykreslení prvků GUI atd. obvykle zaberou řádově pouze setiny vteřiny, proto je vhodné cokoliv delšího spouštět tak, aby to neomezovalo uživatele v interakci s GUI, zároveň je ale nezbytné dát uživateli nějakým způsobem vědět, že operace probíhá na pozadí a pokud je to možné, tak ho informovat o stavu této operace (např. zpracovaná procenta).

QGIS

V QGIS jsou takto řešeny Nástroje zpracování, které se spouští tak, aby neblokovaly uživatelovu interakci s GUI. Zároveň je ve spodní liště zobrazen ukazatel zpracování, který uživatele informuje, kolik procent dané úlohy je zpracováno.

8.1 Spouštení procesu bez blokace GUI

Základním krokem je vytvoření objektu, který odvodíme z třídy QRunnable. Tato třída vytváří objekty, obsahující komplexní úlohy, které je možné snadno spouštět. Klíčovou roli hraje funkce run třídy QRunnable, která se spouští a měla by obsahovat dlouho běžící úlohu.

V příkladu pod odstavcem vytvoříme třídu Worker, pro kterou vytvoříme konstruktor, abychom demonstrovali, jak je možné do úlohy předat data. Mimo to v této třídě vytvoříme tři signály, s ohledem na omezení kombinace Pythonu a PyQt nelze signály vytvořit přímo na objektu QRunnable. Je nutné pro signály vytvořit vlastní třídu a tu následně použít jako proměnnou ve třídě odvozené od QRunnable (Fitzpatrick 2021). Jeden informuje o ukončení této úlohy, druhý předává výslednou hodnotu a třetí slouží pro informování kolik procent úlohy už je zpracováno a název Worker, který běží. V tomto případě používáme jako vstup i výstup jednoduché texty, ale lze takto předávat i komplexnější data či celé objekty. Worker v ukázkovém příkladu nic specifického nevykonává, pouze 10 krát jednu sekundu vyčkává, abychom demonstrovali dlouho běžící process.

Zdrojový kód
import time

from PyQt5.QtCore import QRunnable, pyqtSignal, QObject

class WorkerSignals(QObject):
    finished = pyqtSignal()
    percentDone = pyqtSignal(str, float)
    result = pyqtSignal(str)

class Worker(QRunnable):

    def __init__(self, job_name: str):
        super(Worker, self).__init__()
        self.signal = WorkerSignals()
        self.job_name = job_name

    def run(self):
        for i in range(10):
            time.sleep(1)
            self.signal.percentDone.emit(self.job_name, (i / 10) * 100)
        self.signal.finished.emit()
        self.signal.result.emit("Job `{}` is done.".format(self.job_name))

Tuto vytvořenou třídu může následně využít. Lze ji použít přímo z hlavního okna aplikace, či jakékoliv jiné dílčí komponenty. Je pouze nutné při inicializaci této komponenty vytvořit proměnnou třídy QThreadPool, která se postará o spouštění tříd odvozených z QRunnable. Tvorba této proměnné vypadá následovně.

self.threadpool = QThreadPool()

Mimo je nutné vytvořit v této třídě funkci, která bude instance třídy Worker vytvářet a předávat je ke spuštění do třídy QThreadPool. To je nezbytné minimum, nad rámec toho je možné k signálům třídy Worker připojit sloty (funkce), které budou na vyvolání signálů reagovat. V tomto ukázkovém případě pouze tiskem informačních zpráv, nicméně funkce mohou být libovolně komplexní.

V kódu používáme uuid, pomocí nějž generujeme náhodné unikátní označení uuid.uuid4(), respektive jeho část, pro každou instanci třídy Worker.

Výsledné funkce, které použijeme v kódu příslušné třídy, pak vypadají následovně:

import uuid

def run_worker(self):
    worker_id = str(uuid.uuid4()).split("-")[0]
    worker = Worker(worker_id)

    worker.signal.result.connect(self.print_worker_output)
    worker.signal.finished.connect(self.worker_finished)
    worker.signal.percentDone.connect(self.worker_percent_done)

    self.threadpool.start(worker)

def print_worker_output(self, output: str):
    print(output)

def worker_finished(self):
    print("Worker finished!")

def worker_percent_done(self, worker_name: str, percent: float):
    msg = "Done {}%. ({})".format(percent, worker_name)
    print(msg)

Tento postup funguje v momentech kdy jednotlivé instance třídy Worker neupravují stejná data a ani si jinak vzájemně nekonkurují, co se týká zdrojů. Pakliže by bylo nutné řešit konkurenci úloh, jsou nutné pokročilejší postupy a přístupy k managementu dat a procesů Ramos (2021).

8.2 Praktická ukázka

Ukázkou aplikace, na níž je možné demonstrovat výše zmíněné koncepty a úskalí práce s GUI je ukázka long_running_tasks.py. Tato aplikace umožňuje vyzkoušení jak se chová aplikace při spuštění naivní úlohy (tlačítko Create Long Running Taks) v porovnání s úlohou spouštěnou skrze Threadpool (tlačítko Create Worker) (viz Obr. 8.1). Během toho, co jsou tyto úlohy aktivní, lze otestovat zda-li je aplikace responzivní pomocí tlačítka Test Interactivity, které by mělo okamžitě vypsat text Clicked at: aktuální datum a čas do QPlainTextEdit umístěného pod tímto tlačítkem.

Obr. 8.1: Aplikace pro testování interaktivity pro déle běžící úlohy

V této aplikaci pro jednoduchost existuje pouze jeden QProgressBar a jedna QLabel, které ukazují stav řešení i pro vícero současně spuštěných úloh. Takže se zprávy a procenta dokončení místy poměrně dynamicky střídají. V tomto případě je cílem jednoduchost UI, ne komplexnost řešení.

QGIS

V QGIS existuje komponenta, která zobrazuje procenta dokončení spuštěných Nástrojů zpracování. Tento widget je výrazně komplexnější, neboť zobrazuje pro vícero procesů souhrn procent dokončení a při kliknutí zobrazí okno s výpisem všech úloh a stavu jejich dokončení.

Komponenta se v GUI QGIS zobrazuje pouze pokud je aktivní ve stavovém řádku, před komponentou Souřadnice.