9.12.2018

foto Petr Bravenec

Petr Bravenec
+420 777 566 384
petr.bravenec@hobrasoft.cz

Ve spolupráci s firmou PC-DIR školíme programování v Qt a C++. Kurzy jsou dva:

Obsah školení se částečně vyvíjí i podle požadavků a dotazů žáků. Jedním z častých témat dotazů je jazyk QML. Ten slouží k tvorbě GUI a v mnoha případech dokáže velmi podstatným způsobem usnadnit tvorbu aplikace. Na několika posledních školeních jsem QML zmínil a ukázal jeho principy na jednoduchém příkladu.

Každé školení se snažím vést tak, aby si studenti vyzkoušeli probíranou látku na jednoduchých příkladech. Pokud bych jenom mluvil, 90 % látky by studenti zapomenuli. Pokud si probíranou látku okamžitě vyzkoušejí, zapomenou také 90 %, ale v těch zbývajících 10 procentech zůstane alespoň informace, že v Qt takové věci dělat lze. Dalších 10 % si pak připomenou při dalším studiu dokumentace.

Z tohoto způsobu školení pro mne vyplývá jeden důležitý úkol - je nutné vymyslet a připravit smysluplné a jednoduché příklady.

Na posledním školení přímo v Rožnově pod Radhoštěm mě napadla jednoduchá úloha: vypsat zůstatek účtu a několik posledních pohybů na účtu ve FIO bance. Tato úloha má pro mne i praktický význam. Normálně je takový výpis celkem k ničemu, je nepohodlné startovat aplikaci, když můžu rovnou nastartovat celé internetové bankovnictví ve webovém prohlížeči. Na svém počítači však používám prostředí KDE, kde se dá takový výpis zobrazit přímo na pracovní ploše. Tam už je pro mě informace o pohybech na firemním účtu velmi užitečná.

Co je QML

QML je jednoduchý deklarativní jazyk určený k tvorbě grafického uživatelského rozhraní. Pro tvorbu GUI je nad jazykem QML vytvořený framework QtQuick, který obsahuje různé přípravky (widgety) pro uživatelský vstup, obrázky, animace, seznamy (použito v tomto příkladu - třída ListView) a podobně. Velmi přirozeným způsobem je ke QML připojený jazyk JavaScript.

QML je jazyk deklarativní - nepopisuje se v něm "Co a jak se dělá", ale "Jak to vypadá". Velmi vzdáleně lze QML přirovnat k HTML, což je ovšem jazyk pro popis dokumentů (s velmi neobratně naroubovanou podporou pro formuláře). Pro programátory v C++ a v imperativních jazycích obecně jde o velký myšlenkový veletoč. Přesto stojí za to QML zvládnout. Vyjadřovací schopnosti QML při tvorbě GUI jsou totiž mnohem lepší, než u QWidgets a C++. V praxi jsem byl donucen jednou přepisovat velmi jednoduchou aplikaci z QML do C++ (chyběla akcelerovaná grafika) a podobné peklo už nechci nikdy absolvovat znovu.

Velkou výhodou QML je skvělá přenositelnost na jiné platformy. V QML běžně vyrábíme aplikace pro embedded zařízení s dotykovým displejem, stejnou aplikaci je přitom možné velmi jednoduše spustit například na tabletu se systémem Android.

API FIO banky

API umí dva režimy: režim pouze pro čtení a režim umožňující i změny (zadávání platebních příkazů a podobně). Pro výpis pohybů stačí pouze režim pro čtení.

Dokumentace k API je dostupná na internetu:

Pro využití API je nutné API povolit a získat token pro přístup k účtu. Potom je možné získat výpis z účtu (ve zvoleném formátu JSON) jednoduchým dotazem (používám curl a jq, použitý token je opsaný z dokumentace):

curl https://www.fio.cz/ib_api/rest/periods/aGEMQB9Idh35fh1g51h3ekkQwyGlQ/2012-08-25/2012-08-31/transactions.json | jq .

Podrobný popis dat v JSON formátu není v tuto chvíli podstatný, vše důležité je součástí příkladu. Důležité je vědět, že v datech jsou obsažené všechny podstatné údaje: zůstatek, měna, číslo účtu a veškeré pohyby v požadovaném období. Formát JSON je dostatečně jednoduchý a čitelný, aby bylo patrné, jak požadované údaje z datového souboru dostat.

Program v QML

Ačkoli je QML určené především pro tvorbu GUI, je možné použít i jazyk JavaScript. Toho se využívá v příkladu pro získání dat z FIO banky (pomocí XMLHttpRequest - weboví programátoři třídu dobře znají). Získaná data jsou zkonvertovaná do datového modelu a ten je předložen třídě ListView pro zobrazení. JavaScript je nejsložitější částí celého programu. V Qt a C++ s použitím lambda funkcí a tříd pro práci s datumy by byla komunikace s bankou asi o cosi jednodušší. Komplikovanost je daná slabou vyjadřovací schopností jazyka JavaScript. Pro komunikaci s bankou slouží funkce getFioData(). Ta je volaná periodicky pomocí QML třídy Timer.

import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Layouts 1.2
import QtQuick.Controls 1.4

Window {
    id: root;
    visible: true
    width: 640
    height: 480
    title: accountname;

    // Sem doplňte vlastní název účtu
    property string accountname: "Běžný účet FIO";

    // Sem doplňte vlastní token pro přístup k bance
    property string token: "aGEMQB9Idh35fh1g51h3ekkQwyGlQ";

    // Počet dní na výpisu, může změnit na vlastní hodnotu
    property int days: 30;

    // Obdelník zajišťuje mezeru mezi okrajem okna a vypsanými hodnotami
    Rectangle {
        anchors.fill: parent;
        anchors.leftMargin: 9;
        anchors.rightMargin: 9;
        anchors.bottomMargin: 5;

        // Text s názvem účtu
        Text  {
            id: label1;
            text: root.accountname;
            font.pixelSize: 24;
            height: font.pixelSize*1.9;
            horizontalAlignment: Text.AlignLeft;
            verticalAlignment: Text.AlignVCenter;
            anchors.top: parent.top;
            anchors.left: parent.left;
            }

        // Text se zůstatkem na účtu
        Text  {
            id: zustatek;
            text: "unknown";
            font.pixelSize: label1.font.pixelSize;
            height: label1.height;
            horizontalAlignment: Text.AlignRight;
            verticalAlignment: Text.AlignVCenter;
            anchors.top: parent.top;
            anchors.right: parent.right;
            anchors.left: label1.right;
            }

        // Seznam s výpisem transakcí 
        ListView {
            id: listview;
            anchors.top: label1.bottom;
            anchors.left: parent.left;
            anchors.right: parent.right;
            anchors.bottom: parent.bottom;
            spacing: 5;
            clip: true;

            // Nejdůležitější část třídy - zobrazení jednoho řádku výpisu
            // Řádek lze libovolně formátovat, ve skutečnosti je možné
            // zformátovat jeden datový řádek do několika řádků textových
            // a přizpůsobit jeho grafickou podobu
            delegate: Rectangle {
                width: parent.width;
                height: childrenRect.height * 1.3;
                color: "lightgray";
                radius: 5;

                // Datum transakce a číslo protiúčtu
                Text {
                    id: text1;
                    text: modelData.datum +
                         ( (modelData.protiucet !== '')
                                ? (" – " + modelData.protiucet + " " + modelData.nazev)
                                : ""
                          );
                    anchors.left: parent.left;
                    font.pixelSize: 18;
                    height: font.pixelSize * 1.3;
                    }

                // Popis transakce (poznámka) nebo zpráva pro příjemce
                Text {
                    id: text2;
                    text: modelData.zprava;
                    font.pixelSize: 16;
                    anchors.top: text1.bottom;
                    anchors.left: parent.left;
                    height: font.pixelSize * 1.3;
                    }

                // Částka transakce
                // Podle znaménka se zvolí barva ve výpisu:
                // - záporné částky červeně
                // - kladné částky černě
                Text {
                    id: textmena;
                    anchors.right: parent.right;
                    text: modelData.objem + " " + modelData.mena;
                    font.pixelSize:  18;
                    height: font.pixelSize * 1.6;
                    color: (modelData.objem > 0) ? "black" : "red";
                    }

                }
            }
        }

    // Časovač volá periodicky funkci pro získání dat z banky
    // Při prvním spuštění se volá takřka okamžitě (30 ms)
    // Při dalších spuštěních se volá jednou za deset minut.
    Timer {
        id: timer;
        interval: 50;
        running: true;
        repeat: true;
        onTriggered: {
            getFioData();
            timer.interval = 300000
            }
        }

    // Získání výpisu z FIO banky
    // Používají se postupy běžné z webových prohlížečů
    function getFioData() {

        // Rozšíření třídy Number o formát s úvodními nulami
        // Převede číslo na string s požadovanou šířkou a úvodními nulami, například:
        // 1 -> 01
        Number.prototype.pad = function(size) {
            var s = String(this);
            while (s.length < (size || 2)) { s = "0" + s; }
            return s;
            };

        // Rošíření třídy Date o pričítání/odečítání dní k datumu
        Date.prototype.addDays = function(days) {
            var result = new Date(this);
            result.setDate(result.getDate() + days);
            return result;
            };

        // Převede datum do podoby YYYY-MM-DD
        Date.prototype.toMyString = function() {
            var y = this.getFullYear();
            var m = this.getMonth() + 1;
            var d = this.getDate();
            var s = y.pad(4) + '-' + m.pad(2) + '-' + d.pad(2);
            return s;
            };

        // Vytvoří se URL pro komunikaci s bankou
        // Součástí URL je token pro přístup k účtu (bere se z properties hlavní třídy),
        // dále počáteční datum a koncový datum výpisu
        var date = new Date();
        var datefrom = date.addDays(-root.days).toMyString();
        var dateto = date.toMyString();
        var url = "https://www.fio.cz/ib_api/rest/periods/"
                + root.token + "/"
                + datefrom + "/" + dateto + "/transactions.json";

        // Pro získání dat se používá třída XMLHttpRequest
        var rq = new XMLHttpRequest();
        rq.open("GET", url, true);
        rq.send();

        // V případě chyby se vypíše varování na konzolu
        // Skutečná aplikace by měla informovat uživatele mnohem srozumitelněji
        rq.onerror = function() {
            console.log("*** Chyba při tahání dat z FIO ***");
            }

        // Pokud se změní stav požadavku, například díky načteným datům,
        // provede se tento kus kódu. Zajímá nás pouze úspěšné získání dat.
        // Přijatý JSON se překonvertuje do jednodušší struktury, která
        // se předhodí k zobrazení třídě ListView. Bylo by samozřejmě
        // možné v ListView zobrazit i přímo data poslaná bankou, ale konverze
        // by se pak prováděla na různých místech kódu. Takto se o přípravu dat
        // stará pouze tato část kódu a zobrazovací část je díky tomu zjednodušená.
        // Ve složitější aplikaci by byla celá část získání dat a jejich konverze
        // do podoby vhodné pro GUI vytvořená v C++ částu programu. QML by v
        // takovém případě zůstalo prakticky beze změny.
        rq.onreadystatechange = function() {
            if (rq.readyState === XMLHttpRequest.DONE && rq.status === 200) {
                var model = [];
                var data = JSON.parse(rq.responseText);
                zustatek.text = data.accountStatement.info.closingBalance + ' '
                              + data.accountStatement.info.currency;
                for (var i=data.accountStatement.transactionList.transaction.length-1; i>=0 ; i--) {
                    var linedata = data.accountStatement.transactionList.transaction[i];
                    var line = new Object;
                    if (linedata === null) { continue; }
                    line.datum     = linedata.column0.value.substring(8,10) + '.'
                                   + linedata.column0.value.substring(5,7) + '.'
                                   + linedata.column0.value.substring(0,4);
                    line.objem     = linedata.column1.value;
                    line.protiucet = (linedata.column2 === null || linedata.column3 === null)
                                    ? ""
                                    : (linedata.column2.value + "/" + linedata.column3.value);
                    line.nazev     = (linedata.column10 === null) ? "" : linedata.column10.value;
                    line.zprava    = (linedata.column16 === null) ? "" : linedata.column16.value;
                    line.komentar  = (linedata.column25 === null) ? "" : linedata.column25.value;
                    line.mena      = (linedata.column14 === null) ? "" : linedata.column14.value;
                    model.push(line);
                    }
                listview.model = model;
                }
            }

        }

}

Závěr

Kompletní aplikaci si můžete stáhnout zde:

FioBanka.zip

Příklad je přiměřeně jednoduchý, aby se dal použít při školení. Při skutečném školení bych takový příklad asi nepoužil. Ne každý má účet ve FIO bance a zpřístupnit vlastní účet pro účely školení se mi samozřejmě nechce. I tak může být tento příklad vhodný pro domácí samostudium.

Hobrasoft s.r.o. | Kontakt