Hlavní navigace

Reakce na události v GUI deklarované v jazyku QML a PySide 2

19. 6. 2018
Doba čtení: 24 minut

Sdílet

V dnešní části seriálu o tvorbě rozhraní s Pythonem a frameworkem PySide (přesněji PySide 2) se seznámíme s dalšími možnostmi reakcí na události, které vzniknou v okně či dialogu deklarovaném v jazyku QML.

Obsah

1. Reakce na události v GUI v jazyku QML a PySide 2

2. Reakce na stisk tlačítka myši kdekoli na ploše hlavního okna

3. Úprava příkladu – omezení plochy reagující na stisk tlačítka myši

4. Zjištění souřadnic kurzoru myši

5. Lokální souřadnicový systém každého objektu MouseArea

6. Zdrojový kód třetího demonstračního příkladu

7. Reakce na otočení kolečkem myši

8. Zdrojový kód čtvrtého demonstračního příkladu

9. Událost, která vznikne po úplné inicializaci komponenty

10. Použití pojmenovaných (neanonymních) funkcí při programování reakcí na události

11. Zdrojový kód šestého demonstračního příkladu

12. Refaktoring předchozího kódu a použití „univerzálních“ callback funkcí

13. Zdrojový kód sedmého demonstračního příkladu

14. Tažení (drag) objektů s využitím myši

15. Demonstrační příklad, v němž je možné objekty přemisťovat

16. Deklarativní omezení plochy, po níž se jednotlivé objekty mohou přemisťovat

17. Alternativní způsob zápisu atributů, které se vztahují k operaci „drag“

18. Nastavení prahové hodnoty pro operaci tažení

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Reakce na události v GUI v jazyku QML a PySide 2

V pořadí již dvacáté třetí části seriálu o tvorbě grafického uživatelského rozhraní v Pythonu s využitím frameworku PySide (2) se opět seznámíme s některými důležitými vlastnostmi jazyka QML (Qt Modeling Language). Tentokrát se bude jednat o popis některých možností, které vývojáři mají při programování reakcí na události, jež vznikají v grafickém uživatelském rozhraní činností uživatele (použití myši, klávesnice nebo dalšího vstupního zařízení, například dotykového displeje). Zajímavé a užitečné přitom je, že některé reakce je možné naprogramovat přímo v jazyku QML, takže není nutné pro každou podúlohu zajišťovat kooperaci mezi QML a Pythonem (což je ovšem většinou dobře, protože nám to umožní oddělit logiku aplikace od vzhledu a chování GUI). Samozřejmě je možné zkombinovat naprogramované reakce na GUI události s animacemi, o nichž jsme se ve stručnosti zmínili minule.

Poznámka: i v dnešním článku v prakticky všech dále popsaných demonstračních příkladech využijeme „univerzální“ skript určený pro načtení souboru QML, inicializaci grafického uživatelského rozhraní na základě obsahu QML a pro zobrazení hlavního okna aplikace. Úplný zdrojový kód výše popsaného modulu naleznete na adrese https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/QmlViewer­.py. Tento modul by měl být umístěn ve stejném adresáři jako QML soubory a skripty s demonstračními příklady:
# vim: set fileencoding=utf-8
 
# univerzální prohlížeč QML souborů
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide2 import QtCore
from PySide2 import QtGui
 
# modul pro práci s QML
from PySide2 import QtQuick
 
 
# nový widget bude odvozen od QDeclarativeView
class MainWindow(QtQuick.QQuickView):
 
    def __init__(self, qml_file, parent=None):
        super(MainWindow, self).__init__(parent)
        # nastavení titulku hlavního okna aplikace
        self.setTitle("QML Example @ PySide2: " + qml_file)
        # načtení souboru QML
        self.setSource(QtCore.QUrl.fromLocalFile(qml_file))
        # necháme QML změnit velikost okna
        self.setResizeMode(QtQuick.QQuickView.SizeRootObjectToView)
 
 
def main(qml_file):
    # vytvoření Qt aplikace
    app = QtGui.QGuiApplication(sys.argv)
 
    # vytvoření hlavního okna
    window = MainWindow(qml_file)
 
    # zobrazení hlavního okna na desktopu
    window.show()
 
    # spuštění aplikace
    sys.exit(app.exec_())

2. Reakce na stisk tlačítka myši kdekoli na ploše hlavního okna

Dnešní první demonstrační příklad bude velmi jednoduchý a bude vlastně pouze shrnovat znalosti, které již o jazyku QML máme. V příkladu je deklarován obdélník představující plochu okna aplikace, do kterého jsou vloženy tři čtverce s různobarevnou výplní:

Obrázek 1: Výchozí nastavení okna dnešního prvního demonstračního příkladu.

Navíc je však přes celé okno vytvořena plocha reagující na operace prováděné myší. Tato plocha je neviditelná a pokud uživatel stiskne kdekoli v okně tlačítko myši, změní se barva výplně prostředního čtverce. Toto chování zajišťuje následující kód:

MouseArea {
    anchors.fill: parent
    onClicked: {
        r2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
    }
}

Povšimněte si, že se vlastně jedná o deklaraci hodnoty přiřazené k atributu onClicked. Hodnotou je ovšem (anonymní) funkce, což je zcela korektní, protože QML využívá podmnožinu JavaScriptu, v němž jsou funkce plnohodnotným datovým typem.

Obrázek 2: Změna barvy prostředního čtverce kliknutím tlačítkem myši.

Úplný zdrojový kód dnešního prvního demonstračního příkladu vypadá následovně:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        z: 1
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        rotation: 45
        anchors.right: parent.right
        anchors.bottom: parent.bottom
    }
 
    MouseArea {
        anchors.fill: parent
        onClicked: {
            r2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
        }
    }
}

Zdrojový kód demonstračního příkladu využívá modul QmlViewer popsaný v první kapitole:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from QmlViewer import *
 
QML_FILE = "13_mouse_click.qml"
 
if __name__ == '__main__':
    main(QML_FILE)

3. Úprava příkladu – omezení plochy reagující na stisk tlačítka myši

Pokud budeme vyžadovat, aby se barva druhého čtverce změnila pouze ve chvíli, kdy uživatel klikne do plochy tohoto čtverce a nikoli na libovolné místo v hlavním okně aplikace, je úprava velmi snadná – prostě přesuneme deklaraci MouseArea dovnitř deklarace příslušného čtverce (význam anchors.fill: parent se tedy změní, protože rodičem MouseArea je nyní čtverec s id=r2):

Rectangle {
    id: r2
    width: 160
    height: 160
    color: "yellow"
    opacity: 0.5
    z: 1
    anchors.horizontalCenter: parent.horizontalCenter
    anchors.top: parent.top
 
    MouseArea {
        anchors.fill: parent
        onClicked: {
            r2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
        }
    }
}

Jen pro úplnost si uveďme úplný zdrojový kód druhé varianty předchozího demonstračního příkladu:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        z: 1
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
 
        MouseArea {
            anchors.fill: parent
            onClicked: {
                r2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
            }
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        rotation: 45
        anchors.right: parent.right
        anchors.bottom: parent.bottom
    }
 
}

4. Zjištění souřadnic kurzoru myši

V některých případech nám nestačí pouze reagovat na samotný stisk tlačítka myši, ale budeme potřebovat vědět, na kterém místě se nachází kurzor myši. To lze zjistit (v handleru příslušné události) velmi snadno s využitím mouse.x a mouse.y:

MouseArea {
    anchors.fill: parent
    onClicked: {
        r2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
        console.log("mouse coordinates:", mouse.x, mouse.y);
    }
}

Příklad výstupu (na konzoli/terminál) s využitím console.log():

qml: mouse coordinates: 9 10
qml: mouse coordinates: 4 5
qml: mouse coordinates: 314 236
qml: mouse coordinates: 14 233
qml: mouse coordinates: 281 25
qml: mouse coordinates: 276 75
qml: mouse coordinates: 240 85

V handleru události onClicked totiž máme k dispozici instanci třídy QQuickMouseEvent, která nám nabízí mj. i tyto atributy:

Atribut Význam
x horizontální souřadnice kurzoru myši
y vertikální souřadnice kurzoru myši
button tlačítko myši, které bylo stisknuto (levé, pravé, prostřední) a vyvolalo událost
buttons bitové pole s kombinacemi právě stisknutých tlačítek
flags v současnosti obsahuje pouze příznak, zda stisk vyvolá událost typu doubleclick
modifiers bitové pole obsahující příznaky modifikátorů (Shift, Control, Alt) stisknutých během události
source rozlišení mezi reálnou myší a jiným zařízením (touchscreen atd.)

Opět si ukažme úplný zdrojový kód demonstračního příkladu, který dokáže vypsat souřadnice kurzoru myši:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        z: 1
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        rotation: 45
        anchors.right: parent.right
        anchors.bottom: parent.bottom
    }
 
    MouseArea {
        anchors.fill: parent
        onClicked: {
            r2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
            console.log("mouse coordinates:", mouse.x, mouse.y);
        }
    }
}
#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from QmlViewer import *
 
QML_FILE = "14_mouse_click_coordinates.qml"
 
if __name__ == '__main__':
    main(QML_FILE)

5. Lokální souřadnicový systém každého objektu MouseArea

V předchozí kapitole jsme se zmínili o použití souřadnice kurzoru myši ve chvíli, kdy vznikla nějaká událost vyvolaná myší. Tyto souřadnice však nejsou absolutní (ani v rámci okna a samozřejmě ani v rámci celého desktopu), ale jsou vztaženy k objektu typu MouseArea. Záleží tedy na tom, jak přesně je tato plocha definována: pokud bude vytvořena v rámci menšího objektu, popř. když bude celý objekt otočen, změní se příslušným způsobem i lokální souřadnicový systém, k němuž jsou vztaženy souřadnice kurzoru myši. To je ve většině případů přesně takové chování, které dává smysl (představme si například kreslicí plochu umístěnou do libovolného místa okna aplikace).

Například následující čtverec má rozměry 160×160 délkových jednotek a je natočen o 45°:

Rectangle {
    id: r1
    width: 160
    height: 160
    color: "red"
    opacity: 0.5
    rotation: 45
    anchors.left: parent.left
    anchors.bottom: parent.bottom
 
    MouseArea {
        anchors.fill: parent
        onClicked: {
            console.log("mouse coordinates:", mouse.x, mouse.y);
        }
    }
}

Souřadnice [0, 0] a [160, 0] budou umístěny do dvou bodů naznačených na dalším obrázku:

Obrázek 3: Umístění souřadnic [0, 0] a [160, 0] pro první čtverec.

6. Zdrojový kód třetího demonstračního příkladu

V dnešním třetím demonstračním příkladu jsou použity tři samostatné plochy reagující na stisk tlačítka myši. Vyzkoušejte si, že se tyto plochy překrývají (v jedné oblasti dokonce všechny tři) a příslušná událost je vždy zachycena pouze jednou plochou, konkrétně tou plochou, která virtuálně leží nad ostatními dvěma plochami (to lze ovlivnit atributem z):

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
 
        MouseArea {
            anchors.fill: parent
            onClicked: {
                parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
                console.log("mouse coordinates:", mouse.x, mouse.y);
            }
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        z: 1
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
 
        MouseArea {
            anchors.fill: parent
            onClicked: {
                parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
                console.log("mouse coordinates:", mouse.x, mouse.y);
            }
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        rotation: 45
        anchors.right: parent.right
        anchors.bottom: parent.bottom
 
        MouseArea {
            anchors.fill: parent
            onClicked: {
                parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
                console.log("mouse coordinates:", mouse.x, mouse.y);
            }
        }
    }
}

Skript pro spuštění příkladu:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from QmlViewer import *
 
QML_FILE = "15_more_mouse_areas.qml"
 
if __name__ == '__main__':
    main(QML_FILE)

7. Reakce na otočení kolečkem myši

V mnoha aplikacích můžeme využít i kolečko myši. Ve chvíli, kdy uživatel kolečkem otočí, se zavolá callback funkce pojmenovaná onWheel, které se předá jak aktuální souřadnice kurzoru myši, tak i relativní hodnota otočení. Ve skutečnosti tato callback funkce podporuje dvě relativní hodnoty – x-ovou a y-ovou (angleDelta.x/angleDelta.y). Záleží na konkrétním provedení myši, jaké informace (zda vůbec nějaké) se přenesou v parametru odpovídajícím x-ové ose; typicky je tato hodnota využitelná u myší, které namísto standardního kolečka obsahují buď malý touchpad nebo tzv. scroll ball (malý trackball). Naproti tomu náklon kolečka se většinou považuje za stisk čtvrtého, resp. pátého tlačítka myši. Příklad naprogramování reakce na otočení kolečkem myši:

MouseArea {
    anchors.fill: parent
    onClicked: {
        ...
        ...
        ...
    }
    onWheel: {
        console.log("mouse wheel:", wheel.angleDelta.y);
    }
}

Typicky se setkáme s tím, že je otočení kolečkem myši o jeden „zub“ vráceno jako hodnota +120 nebo –120, v závislosti na směru otáčení. Tomuto chování je možné přizpůsobit aplikaci. Například v následujícím kódu se otáčením kolečkem myši mění natočení čtverce nebo jakéhokoli jiného předka objektu MouseArea:

Rectangle {
    id: r1
    width: 160
    height: 160
    color: "red"
    opacity: 0.5
    rotation: 45
    anchors.left: parent.left
    anchors.bottom: parent.bottom
 
    MouseArea {
        anchors.fill: parent
        onClicked: {
            parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
            console.log("mouse coordinates:", mouse.x, mouse.y);
        }
        onWheel: {
            console.log("mouse wheel:", wheel.angleDelta.y);
            parent.rotation += wheel.angleDelta.y / 30;
        }
    }
}
Poznámka: první událost otočení kolečkem může vracet dosti náhodnou změnu úhlu, která se může i o řád odlišovat od očekávané hodnoty ±120.

8. Zdrojový kód čtvrtého demonstračního příkladu

Ve čtvrtém příkladu je ukázáno, jak lze naprogramovat otáčení libovolným čtvercem v okně tím nejjednodušším způsobem – kolečkem myši. K tomuto účelu se používají tři objekty MouseArea s prakticky totožnými handlery událostí:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
 
        MouseArea {
            anchors.fill: parent
            onClicked: {
                parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
                console.log("mouse coordinates:", mouse.x, mouse.y);
            }
            onWheel: {
                console.log("mouse wheel:", wheel.angleDelta.y);
                parent.rotation += wheel.angleDelta.y / 30;
            }
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        z: 1
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
 
        MouseArea {
            anchors.fill: parent
            onClicked: {
                parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
                console.log("mouse coordinates:", mouse.x, mouse.y);
            }
            onWheel: {
                console.log("mouse wheel:", wheel.angleDelta.y);
                parent.rotation += wheel.angleDelta.y / 30;
            }
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        rotation: 45
        anchors.right: parent.right
        anchors.bottom: parent.bottom
 
        MouseArea {
            anchors.fill: parent
            onClicked: {
                parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
                console.log("mouse coordinates:", mouse.x, mouse.y);
            }
            onWheel: {
                console.log("mouse wheel:", wheel.angleDelta.y);
                parent.rotation += wheel.angleDelta.y / 30;
            }
        }
    }
}

Skript pro spuštění příkladu:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from QmlViewer import *
 
QML_FILE = "16_mouse_wheel.qml"
 
if __name__ == '__main__':
    main(QML_FILE)

9. Událost, která vznikne po úplné inicializaci komponenty

Před popisem dalších operací, které souvisí s myší či podobným polohovacím zařízením, se musíme zmínit o události, která nastane (přesněji řečeno je vyvolána) ve chvíli, kdy je určitá komponenta plně inicializována a celé prostředí (QML) je kompletně připraveno pro provedení uživatelských skriptů. Tato událost se jmenuje completed a popsána je na stránce https://doc.qt.io/qt-5/qml-qtqml-component.html#completed-signal. Příslušný handler má jméno onCompleted a je možné ho použít u jakékoli komponenty:

Component.onCompleted: {
    ...
    ...
    ...
    uživatelský skript
    ...
    ...
    ...
}

Podívejme se nyní na jednoduchý demonstrační příklad, který ukazuje použití handleru této události. V příkladu je použit obdélník reprezentující celou plochu okna, na němž je další čtverec:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
    }
 
    Component.onCompleted: {
        console.log("ok, everything is prepared");
    }
}
Poznámka: existuje i událost destruction s handlerem onDestruction, tu však většinou příliš často v praxi nepoužijete.

10. Použití pojmenovaných (neanonymních) funkcí při programování reakcí na události

Další problematikou, o které se musíme alespoň ve stručnosti zmínit, je použití klasických pojmenovaných (tj. neanonymních) funkcí, které je možné zavolat v handlerech událostí. Prozatím jsme celý programový kód související se zpracováním určité události vkládali přímo do handleru příslušné události, například:

onClicked: {
    parent.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
    console.log("mouse coordinates:", mouse.x, mouse.y);
}
onWheel: {
    console.log("mouse wheel:", wheel.angleDelta.y);
    parent.rotation += wheel.angleDelta.y / 30;
}

Ovšem ve skutečnosti můžeme použít i běžné JavaScriptové funkce, tedy takto:

function onRect1Click(mouse) {
    r1.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
    console.log("mouse coordinates:", mouse.x, mouse.y);
}
 
function onRect1WheelRotate(wheel) {
    console.log("mouse wheel:", wheel.angleDelta.y);
    r1.rotation += wheel.angleDelta.y / 30;
}

Tyto funkce již můžeme zavolat z handleru popř. je můžeme přímo propojit s příslušným signálem, což je ještě lepší:

Component.onCompleted: {
    console.log("completed")
    mouseArea1.clicked.connect(onRect1Click)
    mouseArea1.wheel.connect(onRect1WheelRotate)
}

11. Zdrojový kód šestého demonstračního příkladu

V šestém demonstračním příkladu je deklarováno šest funkcí, které jsou (postupně) propojeny s následujícími událostmi:

  1. Kliknutí na první čtverec
  2. Otočení kolečkem myši nad prvním čtvercem
  3. Kliknutí na druhý čtverec
  4. Otočení kolečkem myši nad druhým čtvercem
  5. Kliknutí na třetí čtverec
  6. Otočení kolečkem myši nad třetím čtvercem

Zdrojový kód příkladu není nijak refaktorován a je psán podobným způsobem, jakoby se jednalo o kód, který je exportovaný z nástrojů určených pro návrh grafického uživatelského rozhraní v QML:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    function onRect1Click(mouse) {
        r1.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
        console.log("mouse coordinates:", mouse.x, mouse.y);
    }
 
    function onRect1WheelRotate(wheel) {
        console.log("mouse wheel:", wheel.angleDelta.y);
        r1.rotation += wheel.angleDelta.y / 30;
    }
 
    function onRect2Click(mouse) {
        r2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
        console.log("mouse coordinates:", mouse.x, mouse.y);
    }
 
    function onRect2WheelRotate(wheel) {
        console.log("mouse wheel:", wheel.angleDelta.y);
        r2.rotation += wheel.angleDelta.y / 30;
    }
 
    function onRect3Click(mouse) {
        r3.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
        console.log("mouse coordinates:", mouse.x, mouse.y);
    }
 
    function onRect3WheelRotate(wheel) {
        console.log("mouse wheel:", wheel.angleDelta.y);
        r3.rotation += wheel.angleDelta.y / 30;
    }
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
 
        MouseArea {
            id: mouseArea1
            anchors.fill: parent
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        z: 1
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
 
        MouseArea {
            id: mouseArea2
            anchors.fill: parent
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        rotation: 45
        anchors.right: parent.right
        anchors.bottom: parent.bottom
 
        MouseArea {
            id: mouseArea3
            anchors.fill: parent
        }
    }
 
    Component.onCompleted: {
        console.log("completed")
        mouseArea1.clicked.connect(onRect1Click)
        mouseArea1.wheel.connect(onRect1WheelRotate)
        mouseArea2.clicked.connect(onRect2Click)
        mouseArea2.wheel.connect(onRect2WheelRotate)
        mouseArea3.clicked.connect(onRect3Click)
        mouseArea3.wheel.connect(onRect3WheelRotate)
    }
}

12. Refaktoring předchozího kódu a použití „univerzálních“ callback funkcí

Předchozí demonstrační příklad nebyl naprogramován příliš dobře, protože se podobný programový kód (handler pro kliknutí tlačítkem myši a pro otočení kolečkem) musel psát třikrát. Je tomu tak z toho důvodu, že do handlerů událostí jsou implicitně předávány pouze objekty představující vlastní událost. Pokud budeme chtít do handlerů předávat i další data, musíme si nepatrně pomoci, a to takto:

Component.onCompleted: {
    console.log("completed")
    mouseArea1.clicked.connect(function(event) {onRectClick(r1, event)})
    mouseArea2.clicked.connect(function(event) {onRectClick(r2, event)})
    mouseArea3.clicked.connect(function(event) {onRectClick(r3, event)})
 
    mouseArea1.wheel.connect(function(event) {onRectWheelRotate(r1, event)})
    mouseArea2.wheel.connect(function(event) {onRectWheelRotate(r2, event)})
    mouseArea3.wheel.connect(function(event) {onRectWheelRotate(r3, event)})
}

V JavaScriptu se jedná o zápis anonymní funkce, která volá handler a předává mu kromě objektu představujícího událost i identifikátor objektu, nad nímž k události došlo. Samotné handlery jsou tedy již jen dva:

function onRectClick(rectangle, mouse) {
    rectangle.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
    console.log("mouse coordinates:", mouse.x, mouse.y);
}
 
function onRectWheelRotate(rectangle, wheel) {
    console.log("mouse wheel:", wheel.angleDelta.y);
    rectangle.rotation += wheel.angleDelta.y / 30;
}

13. Zdrojový kód sedmého demonstračního příkladu

Opět se pro úplnost podívejme na úplný zdrojový kód dnešního sedmého demonstračního příkladu:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    function onRectClick(rectangle, mouse) {
        rectangle.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1);
        console.log("mouse coordinates:", mouse.x, mouse.y);
    }
 
    function onRectWheelRotate(rectangle, wheel) {
        console.log("mouse wheel:", wheel.angleDelta.y);
        rectangle.rotation += wheel.angleDelta.y / 30;
    }
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        rotation: 45
        anchors.left: parent.left
        anchors.bottom: parent.bottom
 
        MouseArea {
            id: mouseArea1
            anchors.fill: parent
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        z: 1
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
 
        MouseArea {
            id: mouseArea2
            anchors.fill: parent
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        rotation: 45
        anchors.right: parent.right
        anchors.bottom: parent.bottom
 
        MouseArea {
            id: mouseArea3
            anchors.fill: parent
        }
    }
 
    Component.onCompleted: {
        console.log("completed")
        mouseArea1.clicked.connect(function(event) {onRectClick(r1, event)})
        mouseArea2.clicked.connect(function(event) {onRectClick(r2, event)})
        mouseArea3.clicked.connect(function(event) {onRectClick(r3, event)})
 
        mouseArea1.wheel.connect(function(event) {onRectWheelRotate(r1, event)})
        mouseArea2.wheel.connect(function(event) {onRectWheelRotate(r2, event)})
        mouseArea3.wheel.connect(function(event) {onRectWheelRotate(r3, event)})
    }
}

14. Tažení (drag) objektů s využitím myši

Další operací, kterou je možné provádět s využitím myši popř. podobného polohovacího zařízení (zde samozřejmě včetně touchscreenu), je tažení (drag) objektů, nebo ještě lépe úplná operace typu drag and drop. Pokud například budeme potřebovat, aby uživatel mohl manipulovat s objektem s identifikátorem object1 na nějaké ploše, postačuje použít nám použít již známý objekt MouseArea a v něm specifikovat, jak se má operace tažení chovat a jakého prvku grafického uživatelského rozhraní se týká (zde objektu object1):

MouseArea {
    id: mouseArea1
    anchors.fill: parent
    drag.target: object1
    drag.axis: Drag.XAndYAxis
}

Zajímavé je použití atributu drag.axis, kterým specifikujeme, jakým směrem lze tažení provést:

  • Drag.XAxis – pouze horizontálně
  • Drag.YAxis – pouze vertikálně
  • Drag.XAndYAxis – oběma směry

Také je možné specifikovat další atributy, jejichž význam si postupně popíšeme v dalších demonstračních příkladech.

15. Demonstrační příklad, v němž je možné objekty přemisťovat

V dalším demonstračním příkladu je ukázána deklarace operace tažení. Každý z barevných čtverců lze přemisťovat pomocí myši:

  1. Levý čtverec lze přemístit libovolným směrem
  2. Prostřední čtverec se dá přesunovat jen nahoru nebo dolů (tedy vertikálně)
  3. Pravý čtverec se naproti tomu přesunuje pouze vlevo či vpravo (tedy horizontálně)

Obrázek 4: Výchozí umístění čtverců v ploše okna.

Obrázek 5: Horizontální přesun pravého čtverce.

Obrázek 6: Vertikální přesun prostředního čtverce.

Obrázek 7: Levý čtverec můžeme přesunou kamkoli, i mimo plochu okna.

Opět následuje výpis zdrojového kódu tohoto demonstračního příkladu:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        x: 0
        y: 0
 
        MouseArea {
            id: mouseArea1
            anchors.fill: parent
            drag.target: r1
            drag.axis: Drag.XAndYAxis
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        x: 80
        y: 80
        z: 1
 
        MouseArea {
            id: mouseArea2
            anchors.fill: parent
            drag.target: r2
            drag.axis: Drag.YAxis
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        x: 160
        y: 0
 
        MouseArea {
            id: mouseArea3
            anchors.fill: parent
            drag.target: r3
            drag.axis: Drag.XAxis
        }
    }
 
}

16. Deklarativní omezení plochy, po níž se jednotlivé objekty mohou přemisťovat

V případě, že budeme chtít omezit přesuny čtverců pouze po ploše okna (tj. aby nám čtverce „neujely“ mimo viditelnou plochu), musíme specifikovat atributy minimumX, maximumX, minimumY a maximumY. Hodnoty těchto atributů je nutné dopočítat. U minimálních hodnot je to jednoduché – nastavíme je na nulu. U hodnot maximálních bude maximální hodnota vypočtena z výšky/šířky okna a výšky/šířky příslušného čtverce:

MouseArea {
    id: mouseArea1
    anchors.fill: parent
    drag.target: r1
    drag.axis: Drag.XAndYAxis
    drag.minimumX: 0
    drag.maximumX: main.width - r1.width
    drag.minimumY: 0
    drag.maximumY: main.height - r1.height
}

Opět následuje výpis úplného zdrojového kódu příkladu, který tentokrát neumožní, aby se čtverce přesunuly mimo hlavní okno:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        x: 0
        y: 0
 
        MouseArea {
            id: mouseArea1
            anchors.fill: parent
            drag.target: r1
            drag.axis: Drag.XAndYAxis
            drag.minimumX: 0
            drag.maximumX: main.width - r1.width
            drag.minimumY: 0
            drag.maximumY: main.height - r1.height
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        x: 80
        y: 80
        z: 1
 
        MouseArea {
            id: mouseArea2
            anchors.fill: parent
            drag.target: r2
            drag.axis: Drag.YAxis
            drag.minimumY: 0
            drag.maximumY: main.height - r2.height
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        x: 160
        y: 0
 
        MouseArea {
            id: mouseArea3
            anchors.fill: parent
            drag.target: r3
            drag.axis: Drag.XAxis
            drag.minimumX: 0
            drag.maximumX: main.width - r3.width
        }
    }
 
}

17. Alternativní způsob zápisu atributů, které se vztahují k operaci „drag“

Ještě se podívejme na alternativní způsob zápisu atributů pro jeden QML objekt. Prozatím jsme například u objektu popisujícího operaci tažení používali „tečkovou“ notaci:

drag.target: r1
drag.axis: Drag.XAndYAxis
drag.minimumX: 0
drag.maximumX: main.width - r1.width
drag.minimumY: 0
drag.maximumY: main.height - r1.height

Alternativně můžeme využít i jiný zápis:

drag {
    target: r1
    axis: Drag.XAndYAxis
    minimumX: 0
    maximumX: main.width - r1.width
    minimumY: 0
    maximumY: main.height - r1.height
}

Předchozí demonstrační příklad lze tedy přepsat následujícím způsobem:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        x: 0
        y: 0
 
        MouseArea {
            id: mouseArea1
            anchors.fill: parent
            drag {
                target: r1
                axis: Drag.XAndYAxis
                minimumX: 0
                maximumX: main.width - r1.width
                minimumY: 0
                maximumY: main.height - r1.height
            }
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        x: 80
        y: 80
        z: 1
 
        MouseArea {
            id: mouseArea2
            anchors.fill: parent
            drag {
                target: r2
                axis: Drag.YAxis
                minimumY: 0
                maximumY: main.height - r2.height
            }
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        x: 160
        y: 0
 
        MouseArea {
            id: mouseArea3
            anchors.fill: parent
            drag {
                target: r3
                axis: Drag.XAxis
                minimumX: 0
                maximumX: main.width - r3.width
            }
        }
    }
 
}

18. Nastavení prahové hodnoty pro operaci tažení

Posledním užitečným atributem při deklaraci operace tažení je tzv. prahová hodnota (threshold). Ta udává, o kolik pixelů se musí posunout kurzor myši se stlačeným tlačítkem, aby se vůbec tažení uskutečnilo. Vyšší hodnoty se hodí použít pro zamezení tažení ve chvíli, kdy uživatel pouze potřebuje na objekt kliknout a omylem myš o několik pixelů posune. Nastavení prahové hodnoty je snadné (v příkladu je ovšem nastavena na příliš vysokou hodnotu):

drag {
    target: r1
    axis: Drag.XAndYAxis
    minimumX: 0
    maximumX: main.width - r1.width
    minimumY: 0
    maximumY: main.height - r1.height
    threshold: 40
}

Upravený příklad bude vypadat takto:

import QtQuick 2.0
 
Rectangle {
    id: main
    width: 320
    height: 240
    color: "lightgray"
 
    Rectangle {
        id: r1
        width: 160
        height: 160
        color: "red"
        opacity: 0.5
        x: 0
        y: 0
 
        MouseArea {
            id: mouseArea1
            anchors.fill: parent
            drag {
                target: r1
                axis: Drag.XAndYAxis
                minimumX: 0
                maximumX: main.width - r1.width
                minimumY: 0
                maximumY: main.height - r1.height
                threshold: 40
            }
        }
    }
 
    Rectangle {
        id: r2
        width: 160
        height: 160
        color: "yellow"
        opacity: 0.5
        x: 80
        y: 80
        z: 1
 
        MouseArea {
            id: mouseArea2
            anchors.fill: parent
            drag {
                target: r2
                axis: Drag.YAxis
                minimumY: 0
                maximumY: main.height - r2.height
                threshold: 40
            }
        }
    }
 
    Rectangle {
        id: r3
        width: 160
        height: 160
        color: "blue"
        opacity: 0.5
        x: 160
        y: 0
 
        MouseArea {
            id: mouseArea3
            anchors.fill: parent
            drag {
                target: r3
                axis: Drag.XAxis
                minimumX: 0
                maximumX: main.width - r3.width
                threshold: 40
            }
        }
    }
 
}

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes popsaných demonstračních příkladů byly, podobně jako tomu bylo i v předchozích článcích, uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/pre­sentations. Pokud nechcete klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Příklad Adresa
1 13_mouse_click.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/13_mouse_clic­k.py
2 13_mouse_click_variant2.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/13_mouse_clic­k_variant2.py
3 14_mouse_click_coordinates.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/14_mouse_clic­k_coordinates.py
4 15_more_mouse_areas.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/15_more_mou­se_areas.py
5 16_mouse_wheel.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/16_mouse_whe­el.py
6 17_on_completed.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/17_on_com­pleted.py
7 18_connect_signals.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/18_connec­t_signals.py
8 19_better_connect_signals.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/19_better_con­nect_signals.py
9 20_mouse_drag.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/20_mouse_drag­.py
10 21_mouse_drag_limits.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/21_mouse_drag_li­mits.py
11 22_mouse_drag_limits_B.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/22_mouse_drag_li­mits_B.py
12 23_mouse_threshold.py https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/23_mouse_threshol­d.py

Následuje tabulka s odkazy na soubory QML s popisem grafického uživatelského rozhraní, které taktéž budete potřebovat:

# QML soubor Adresa
1 13_mouse_click.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/13_mouse_clic­k.qml
2 13_mouse_click_variant2.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/13_mouse_clic­k_variant2.qml
3 14_mouse_click_coordinates.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/14_mouse_clic­k_coordinates.qml
4 15_more_mouse_areas.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/15_more_mou­se_areas.qml
5 16_mouse_wheel.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/16_mouse_whe­el.qml
6 17_on_completed.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/17_on_com­pleted.qml
7 18_connect_signals.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/18_connec­t_signals.qml
8 19_better_connect_signals.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/19_better_con­nect_signals.qml
9 20_mouse_drag.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/20_mouse_drag­.qml
10 21_mouse_drag_limits.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/21_mouse_drag_li­mits.qml
11 22_mouse_drag_limits_B.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/22_mouse_drag_li­mits_B.qml
12 23_mouse_threshold.qml https://github.com/tisnik/pre­sentations/blob/master/Pyt­hon_GUI/PySide2/23_mouse_threshol­d.qml

20. Odkazy na Internetu

  1. QML Tutorial
    https://pyside.github.io/doc­s/pyside/tutorials/qmltuto­rial/index.html
  2. QML Advanced Tutorial
    https://pyside.github.io/doc­s/pyside/tutorials/qmladvan­cedtutorial/index.html
  3. User interface markup language
    https://en.wikipedia.org/wi­ki/User_interface_markup_lan­guage
  4. Signal and Handler Event System
    https://doc.qt.io/qt-5/qtqml-syntax-signals.html
  5. Qt Documentation: MouseEvent QML Type
    https://doc.qt.io/qt-5/qml-qtquick-mouseevent.html
  6. Qt Documentation: WheelEvent QML Type
    https://doc.qt.io/qt-5/qml-qtquick-wheelevent.html
  7. Qt Documentation: MouseArea QML Type
    https://doc.qt.io/qt-5/qml-qtquick-mousearea.html
  8. UsiXML
    https://en.wikipedia.org/wiki/UsiXML
  9. Anchor-based Layout in QML
    https://het.as.utexas.edu/HET/Sof­tware/html/qml-anchor-layout.html#anchor-layout
  10. PySide.QtDeclarative
    https://pyside.github.io/doc­s/pyside/PySide/QtDeclara­tive/index.html
  11. PySide and Qt Quick/QML Playground
    https://wiki.qt.io/PySide-and-QML-Playground
  12. Hand Coded GUI Versus Qt Designer GUI
    https://stackoverflow.com/qu­estions/387092/hand-coded-gui-versus-qt-designer-gui
  13. Qt Creator Manual
    http://doc.qt.io/qtcreator/
  14. Qt Designer Manual
    http://doc.qt.io/qt-5/qtdesigner-manual.html
  15. Qt Creator (Wikipedia)
    https://en.wikipedia.org/wi­ki/Qt_Creator
  16. PySide 1.2.1 documentation
    https://pyside.github.io/doc­s/pyside/index.html
  17. PySide na PyPi
    https://pypi.org/project/PySide/
  18. QML for JavaScript programmers
    https://wiki.qt.io/QML_for_Ja­vaScript_programmers
  19. JavaScript Expressions in QML Documents
    https://doc.qt.io/qt-5/qtqml-javascript-expressions.html
  20. JavaScript Host Environment
    https://doc.qt.io/qt-5/qtqml-javascript-hostenvironment.html

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.