├── .gitignore ├── 00_preparations └── README.md ├── 01_first_program ├── README.md └── first │ ├── first.py │ └── view.qml ├── 02_clicker ├── README.md └── clicker │ ├── clicker.py │ └── view.qml ├── 03_dms_converter ├── README.md └── dms_converter │ ├── dms_converter.py │ └── view.qml ├── 04_city_list ├── README.md └── city_list │ ├── city_list.py │ ├── simple_view.qml │ ├── souradnice.json │ ├── view.qml │ └── wikidata_cities.sparql ├── 05_city_map ├── README.md └── city_map │ ├── city_map.py │ ├── souradnice.json │ └── view.qml ├── 06_todo_list ├── README.md └── todo_list │ ├── simple_view.qml │ ├── todo_list.py │ ├── ukoly.txt │ └── view.qml ├── 07_countdown ├── README.md └── countdown │ ├── countdown.py │ └── view.qml ├── 08_vehicle_positions └── vehicle_positions │ ├── .idea │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ └── modules.xml │ ├── positions.py │ └── view.qml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /00_preparations/README.md: -------------------------------------------------------------------------------- 1 | # Příprava prostředí pro vývoj 2 | 3 | Aby se vám pohodlně vyvíjelo, doporučuji instalaci následujících nástrojů: 4 | - nainstalujte si [Visual Studio Code](https://code.visualstudio.com/) 5 | - nainstalujte si [Qt Creator](https://www.qt.io/download) (Qt Creator stačí, stažení vyžaduje registraci) 6 | - registrace -> Go Open Source -> Download the Qt Online Installer -> Download 7 | - v rámci instalace není potřeba volit konkrétní verzi Qt, tu budeme instalovat z pipu 8 | - do VSCode si nainstalujte [Qt for Python](https://github.com/seanwu1105/vscode-qt-for-python) 9 | - zvolte / vytvořte si složku, ve které budete vytvářet všechny programy využívající Qt (může to být klidně složka cvičení, ve které už něco máte) 10 | - dle [návodu](https://naucse.python.cz/course/pyladies/beginners/venv-setup/) si v této slože vytvořte virtuální prostředí a toto prostředí si aktivujte 11 | - do terminálu (dole ve VSCode) napište `pip install PySide6` 12 | - stáhněte si oba soubory z [adresáře s prvním programem](../01_first_program/) a uložte je do dříve zvolené složky 13 | - zkuste si oba soubory otevřít, jestli se vám korektně zvýrazňuje syntaxe 14 | - zkuste si spustit `first.py` (pravé tlačítko myši -> run selected file in terminal) 15 | - uvidíte-li okno s nápisem *Hello World*, pravděpodobně máte vše správně nainstalované a nastavené 16 | - uvidíte-li v terminálu chybovou hlášku, zkuste zjistit její původ a chybu odstranit, nepovede-li se to, kontaktujte mne 17 | -------------------------------------------------------------------------------- /01_first_program/README.md: -------------------------------------------------------------------------------- 1 | # První program v Qt Quick 2 | 3 | ## Základní principy Qt Quick 4 | Grafické aplikace v Qt Quick sestávají ze dvou částí - grafického rozhraní v QML 5 | a výkonného (od slova vykonávat, nikoli výkon) kódu, který dodává grafickému 6 | rozhraní data k zobrazení. Tento kód je v původním Qt v jazyce C++, ale my 7 | budeme používat novější vazby do Pythonu pomocí PySide6, tudíž budeme psát v 8 | Pythonu. Pro Qt verze 5 se knihovna jmenovala PySide2, pro aktuální Qt 6 se 9 | přejmenovala na PySide6. Rozdily mezi PySide2 a PySide6 nejsou příliš významné, 10 | pokud narazíte na návod pro PySide2, pravděpodobně bude fungovat i pro PySide6. 11 | Poslední dobou se možnosti grafického rozhraní rozšiřují a je možné psát i 12 | aplikace pouze v QML (uvnitř QML lze používat JavaScript). Data mezi Pythonem a 13 | QML se předávají pomocí *context property*, o nich bude více v dalších dílech. 14 | 15 | ## Smyčka událostí (event loop) 16 | Narozdíl od běžných programů v Pythonu, které pracují "shora dolů" jsou aplikace 17 | s grafickým rozhraním (a často i serverové aplikace) založeny na principu smyčky 18 | událostí. Program po spuštění provede inicializační část, kde vytvoří grafické 19 | rozhraní a připraví objekty. Následně přejde do smyčky událostí, ve které čeká 20 | na události od grafického rozhraní (kliknutí, psaní textu, změna velikosti okna) 21 | a na tyto události reaguje. Některé události jsou obslouženy rovnou v grafickém 22 | rozhraní, jiné propadnou do Pythonu a jsou odbaveny tam. Po odbavení události 23 | program čeká ve smyčce na další událost. 24 | 25 | ## Popis programu 26 | Na začátku vytvoříme [`QGuiApplication`](https://doc.qt.io/qtforpython/PySide6/QtGui/QGuiApplication.html), která zajišťuje, že se naše aplikace 27 | bude chovat jako aplikace s grafickým rozhraním (bude mít okno apod.). 28 | 29 | Následně připravujeme grafické rozhraní - vytvoříme [`QQuickView`](https://doc.qt.io/qtforpython/PySide6/QtQuick/QQuickView.html) a řekneme mu, 30 | ze kterého souboru má vzít popis grafického rozhraní. [`QUrl`](https://doc.qt.io/qtforpython/PySide6/QtCore/QUrl.html) slouží pro převod 31 | cesty na `QUrl` objekt, definiční soubor rozhraní může být umístěn i někde 32 | vzdáleně a aplikace si ho může odtud stáhnout, ale nebývá to příliš časté. 33 | Nakonec nesmíme zapomenout view zobrazit, jinak se nám otevře prázdné okno. 34 | 35 | Když máme vše připraveno, spustíme pomocí `app.exec_()` smyčku událostí. Tato 36 | metoda nevrátí, dokud aplikaci neukončíme nebo nezavřeme okno. Za volání této 37 | funkce tedy nemá smysl psát další kód, protože by se vykonal až těsně před 38 | ukončením aplikace. 39 | 40 | ## Popis grafického rozhraní 41 | Jazyk [QML](https://doc.qt.io/qt-5/qmlfirststeps.html) je trochu podobný 42 | JavaScriptu a CSS, ale má zjednodušenou syntaxi a jeho základem je hierarchická 43 | struktura (nejen) grafických prvků. Každý prvek má jméno, začínající velkým 44 | písmenem, a blok ohraničený složenými závorkami, ve kterém jsou specifikovány 45 | vlastnosti prvku a deklarovány podřízené prvky. 46 | 47 | Prvky jsou definovány v knihovnách, ty je potřeba na začátku importovat pomocí 48 | `import `, kde `` je název knihovny, lze zjistit 49 | rozkliknutím prvku ze [seznamu QML typů](https://doc.qt.io/qt-5/qmltypes.html), 50 | `` je specifikována tamtéž. Verze obvykle odpovídá verzi Qt, pokud 51 | použijete, například zkopírováním ze starších ukázek, nižší verzi, nemělo by to 52 | vadit, naopak novější verze vám se starším Qt fungovat nebude, protože prvku 53 | mohly přibýt nové vlastnosti a metody. 54 | 55 | V našem příkladu vyrobíme 56 | [obdélník](https://doc.qt.io/qt-5/qml-qtquick-rectangle.html) s rozměry 200x200 57 | px a v něm umístíme prvek [`Text`](https://doc.qt.io/qt-5/qml-qtquick-text.html) s textem *Hello world!*. 58 | 59 | ## Zdroje 60 | - [Your First QtQuick/QML Application](https://doc.qt.io/qtforpython/tutorials/basictutorial/qml.html) 61 | - [Jazyk QML a PySide2](https://www.root.cz/clanky/jazyk-qml-qt-modeling-language-a-pyside-2/) 62 | - [Jazyk QML a PySide](https://www.root.cz/clanky/jazyk-qml-qt-modeling-language-a-pyside/) - pozor, vztahuje se ke starší verzi PySide i QtQuick. Na QtQuick se skoro nic nezměnilo, PySide a PySide2 se ale liší výrazně 63 | - [Making a QML Application in Python (video)](https://www.youtube.com/watch?v=JxfiUx60Mbg) 64 | -------------------------------------------------------------------------------- /01_first_program/first/first.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QUrl 2 | from PySide6.QtGui import QGuiApplication 3 | from PySide6.QtQuick import QQuickView 4 | import sys 5 | 6 | VIEW_PATH = "view.qml" 7 | 8 | # Create the application object and pass command line arguments to it 9 | app = QGuiApplication(sys.argv) 10 | 11 | # Create the view object 12 | view = QQuickView() 13 | # Set the QML file to view 14 | view.setSource(QUrl(VIEW_PATH)) 15 | # Resize the view with the window 16 | view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView) 17 | # Show the view (open the window) 18 | view.show() 19 | 20 | # Run the event loop 21 | app.exec() 22 | 23 | -------------------------------------------------------------------------------- /01_first_program/first/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | 3 | Rectangle { 4 | width: 200 5 | height: 200 6 | Text { 7 | text: "Hello, world!" 8 | } 9 | } -------------------------------------------------------------------------------- /02_clicker/README.md: -------------------------------------------------------------------------------- 1 | # Klikni na tlačítko 2 | 3 | ## Context property 4 | Abychom mohli propojit grafické rozhraní a výkonný kód v Pythonu, musíme si 5 | pořídit objekt, ke kterému bude rozumět jak Python, tak QML. Pokud takovýto 6 | objekt už máme, můžeme ho do QML zpřístupnit pomocí *context property*. 7 | `QQuickView` si drží kontext - stav - ke kterému lze přistupovat jak z Pythonu, 8 | tak z QML. My můžeme pomocí 9 | [`setContextProperty`](https://doc.qt.io/qtforpython/PySide6/QtQml/QQmlContext.html#PySide6.QtQml.PySide6.QtQml.QQmlContext.setContextProperty) 10 | do tohoto kontextu přidat náš objekt a určit mu, pod jakým jménem se bude 11 | objevovat v QML. 12 | 13 | Kontext získáme z `QQuickView` pomocí metody `view.rootContext()`. Pokud pak 14 | zavoláme `ctxt.setContextProperty('myObj', my_object)`, pak objekt `my_object` 15 | z Pythonu bude dostupný v QML jako `myObj`. Kde takový objekt vzít? 16 | Nejjednodušší je vytvořit si vlastní třídu, která bude dědit od [`QObject`](https://doc.qt.io/qtforpython/PySide6/QtCore/QObject.html) 17 | a v ní deklarovat potřebné signály, sloty a property (viz níže). Díky tomu, že 18 | naše třída dědí od QObject, je možné ji použít jako context property a Qt zařídí 19 | potřebné vazby. 20 | 21 | 22 | ## Signály a sloty 23 | Uživatel může vyvolat mnoho různých akcí a na některé akce potřebuje reagovat více 24 | komponent. Přímé volání funkcí by v tomto případě bylo otížně realizovatelné, 25 | proto je ve Qt zaveden mechanismus signálů a slotů. 26 | 27 | Pokud dojde k nějaké akci, například je stisknuto tlačítko, je při tom emitován 28 | signál. Pokud chceme na nějaký signál reagovat, musíme si vytvořit slot a 29 | následně připojit signál stisku tlačítka do našeho slotu. Jeden signál může být 30 | připojen do libovolného množství slotů (i do žádného), jeden slot může být 31 | připojen k libovolnému množství signálů. 32 | 33 | Každý signál a slot má definovanou signaturu (seznam argumentů a jejich typů), 34 | kterou je při propojování signálů a slotů potřeba dodržovat. Respektive slot 35 | může brát méně argumentů, než má signál, zbylé argumenty signálu se pak zahodí. 36 | Společné argumenty ale musí mít stejné typy. Toto je důsledek použití C++ jako 37 | výchozího jazyka pro Qt, proto to může v Pythonu působit zvláštně. 38 | 39 | Jak vytvořit slot nebo signál? Obojí je potřeba vytvořit v Pythonu jako metodu 40 | objektu, který, alespoň nepřímo, dědí z `QObject`. Signál vytvoříme pomocí 41 | funkce `Signal(,,...)` a takto vytvořený signál pak můžeme emitovat 42 | pomocí `signal.emit(,,...)`, kde `` a další jsou skutečné 43 | hodnoty dříve deklarovaných typů, které chceme v signálu poslat. 44 | 45 | Slot vytvoříme pomocí dekorátoru `@Slot(,,...)` k metodě objektu. 46 | Tato metoda pak kromě `self` bere daný počet argumentů typů deklarovaných v 47 | dekorátoru. 48 | 49 | Pokud potřebujeme propojit signál a slot přímo v Pythonu, můžeme použít metodu 50 | `connect` v podobě `signal.connect(slot)`. 51 | 52 | ## Property 53 | Často potřebujeme, aby nám GUI reflektovalo hodnotu nějaké proměnné v Pythonu, 54 | například aby byla zobrazena hodnota počítadla, nebo aby byl nápis *Vyhráli 55 | jste* zobrazen pouze, když je sudoku správně vyplněné. Toto by samozřejmě bylo 56 | realizovatelné i s použitím pouze signálů a slotů, ale bylo by to zbytečně 57 | pracné a náročné na údržbu. Proto byl zaveden koncept *property*, které 58 | takovouto činnost zjendodušují. 59 | 60 | Property je ve zjednodušeném pohledu lepší atribut objektu, který je kromě 61 | Pythonu viditelný i v QML. Opět se zde ukazuje původ v C++, abychom mohli 62 | vytvořit property, musíme deklarovat *getter*, *setter* a notifikační signál. 63 | Budeme tedy postupovat zcela v duchu OOP. Pro další text předpokládejme, že 64 | chceme vytvořit property `counter`. Vytvoříme si privátní atribut 65 | `self._counter`, ve kterém budeme uchovávat skutečnou hodnotu počitadla, ale 66 | nebudeme k ní přistupovat odjinud než z inicializátoru, getteru a setteru. 67 | 68 | *Getter* je metoda, která nám vrátí hodnotu interního atributu. 69 | Budeme dodržovat konvenci a gettery pojmenovávat `get_`, tedy v našem 70 | případě `get_counter`. Tato funkce pouze vrátí hodnotu `self._counter`. 71 | 72 | *Notifikační signál* může, ale nemusí, emitovat změněnou hodnotu. 73 | Jméno je `_changed`, tedy dle předchozí kapitoly `counter_changed = 74 | Signal()`. Jedná se také o metodu, i když je deklarovaná jinak, než jsme zvyklí. 75 | 76 | *Setter* je o něco složitější, protože v případě, že se hodnota změnila, musíme 77 | emitovat notifikační signál, aby se případně překreslilo GUI. Metoda bude 78 | vypadat například takto: 79 | 80 | ``` 81 | def set_counter(self,val): 82 | if val != self._counter: 83 | self._counter = val 84 | self.counter_changed.emit() 85 | 86 | ``` 87 | 88 | Když máme všechny tři metody připravené, můžeme vytvořit 89 | [`Property`](https://doc.qt.io/qtforpython/PySide6/QtCore/Property.html). 90 | `Property` bere 4 argumenty - typ, getter, setter a pojmenovaný argument 91 | `notify` s notifikační metodou. Naší property bychom vytvořili takto: 92 | `counter = Property(int,get_counter,set_counter,notify=counter_changed)`. Nyní 93 | se můžeme k propetry v Pythonu chovat jako k normálnímu atributu, tedy můžeme ho 94 | číst i nastavovat pomocí `self.counter = 42` a gettery a settery se zavolají 95 | automaticky. Také tuto property můžeme svázat (bind) v QML s libovolným počtem 96 | vlastností libovolného počtu komponent. Pokud se hodnota této property změní, 97 | automaticky se změní i na všech místech v GUI, kde je použita. Rovněž pokud ji 98 | změníme z GUI (viz další díl), pak se změní i v Pythonu. 99 | 100 | 101 | ## Popis programu 102 | Program reprezentuje jeden z nejjednoduššich interaktivních programů - obsahuje 103 | textové pole a tlačítko, při každém stisku tlačítka se číslo v textovém poli 104 | zvýší o jedna. Abychom toho dosáhli, potřebujeme mít v programu třídu, která 105 | bude jako context property přidaná do QML a která bude umět GUI předat hodnotu 106 | počitadla a bude schopna zpracovat událost stisku tlačítka. Tuto třídu jsme 107 | nazvali `ClickModel` a obsahuje jeden slot `increase` a jednu property `count`. 108 | 109 | V rámci inicializace ClickModelu je potřeba nezapomenout zavolat inicialízátor 110 | `QObject`, protože jinak nebudou vazby s GUI fungovat. Dále je v programu 111 | vytvořena instance ClickModelu, získán z view kontext a do něj přidána tato 112 | instance pod jménem `clickModel`. 113 | 114 | ## Popis grafického rozhraní 115 | V grafickém rozhraní je potřeba oproti minulému příkladu importovat 116 | `QtQuick.Controls`, abychom mohli použít `Button`. Prvky jsou uspořádány do 117 | sloupce pod sebou, k tomu slouží komponenta `Column`, pokud bychom chtěli mít 118 | komponenty v řádku, existuje ekvivalentní komponenta `Row`. V komponentě `Text` 119 | je provedena vazba property `count` našeho z našeho modelu v Pythonu k 120 | vlastnosti `text`, neboli tomu, co komponenta vypíše. V komponentě Button je pak 121 | následně provedeno připojení signálu `onClicked` do slotu `increase` našeho 122 | modulu. Po spuštění pak můžeme ověřit funkčnost klikáním na tlačítko, počitadlo 123 | by se mělo automaticky aktualizovat. Do konzole je vypisována původní a nová 124 | hodnota při zavolání setteru. 125 | 126 | ## Zdroje 127 | - [Signals and Slots](https://doc.qt.io/qt-5/signalsandslots.html) 128 | - [Embedding C++ Objects into QML with Context Properties](https://doc.qt.io/qt-5/qtqml-cppintegration-contextproperties.html) - sice pojednává o C++, ale principy jsou platné stejně i pro Python 129 | - [The Property System](https://doc.qt.io/qt-5/properties.html) - opět více zaměřené na C++, ale ukazuje to možnosti property 130 | -------------------------------------------------------------------------------- /02_clicker/clicker/clicker.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QObject, Slot, Property, QUrl, Signal 2 | from PySide6.QtGui import QGuiApplication 3 | from PySide6.QtQuick import QQuickView 4 | import sys 5 | 6 | VIEW_URL = "view.qml" 7 | 8 | 9 | class ClickModel(QObject): 10 | """ClickModel is the model class for the GUI. It holds the counter property 11 | and handles event generated by the click on the button.""" 12 | def __init__(self): 13 | # Initialize the parent object. If omitted, GUI will not start 14 | QObject.__init__(self) 15 | # Initialize the counter internal value. Because we propagate count as 16 | # a property to QML, getter, setter and notifier must be made 17 | self._count = 0 18 | 19 | def get_count(self): 20 | """Getter for the count property""" 21 | return self._count 22 | 23 | def set_count(self,val): 24 | """Setter for the count property""" 25 | print("Current: {}, new: {}".format(self._count,val)) 26 | # We set new value and notify of change only if the value 27 | # is really changed. 28 | if val != self._count: 29 | # Change internal value 30 | self._count = val 31 | # Notify the GUI that the value had changed 32 | self.counter_changed.emit() 33 | 34 | # Declare a notification method 35 | counter_changed = Signal() 36 | 37 | # Add a new property to ClickModel object. It can be used as an attribute 38 | # from Python. 39 | count = Property(int,get_count, set_count, notify=counter_changed) 40 | 41 | @Slot() 42 | def increase(self): 43 | """Handler for the button click. Increases counter by one.""" 44 | print("Increasing") 45 | # Use property as an attribute. Setter is called automatically and 46 | # notifies the GUI about the changed value. 47 | self.count = self.count+1 48 | 49 | 50 | app = QGuiApplication(sys.argv) 51 | view = QQuickView() 52 | url = QUrl(VIEW_URL) 53 | # Create the instance of a ClickModel 54 | click_model = ClickModel() 55 | 56 | # Get the context of the view 57 | ctxt = view.rootContext() 58 | # Set that 'click_model' will be available as 'clickModel' property in QML 59 | # This must be done before view.setSource is called 60 | ctxt.setContextProperty("clickModel",click_model) 61 | 62 | view.setSource(url) 63 | view.show() 64 | app.exec_() 65 | -------------------------------------------------------------------------------- /02_clicker/clicker/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | 4 | 5 | Column { 6 | Text { 7 | // Bind the clickModel.count property to the text property of the QML element 8 | text: clickModel.count 9 | } 10 | Button { 11 | text: 'Click me!' 12 | // Connect the clickModel.increase slot to the onClicked signal 13 | onClicked: clickModel.increase() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /03_dms_converter/README.md: -------------------------------------------------------------------------------- 1 | # Převod DMS na stupně a zpět 2 | 3 | # Modely a View v Qt 4 | V [minulém příkladu](../02_clicker/README.md) jsme se naučili používat 5 | kontextové proměnné jakožto způsob, jak dopravovat data z Pythonu do QML. Ve 6 | skutečnosti jsme použili jeden ze základních konceptů nejen Qt, sloužících k 7 | oddělení aplikační logiky od grafického rozhraní, a to Model-View-Delegate. O 8 | delegátech bude řeč příště, nyní si vysvětlíme Model a View. 9 | 10 | Model je objekt, který drží nějaká data a ke kterému přistupuje aplikační 11 | logika. Můžeme také říci, že model drží stav. Data, která drží, obvykle drží v 12 | nějaké formě, se kterou se dobře pracuje aplikační logice. V minulém případě to 13 | byla property 'counter', může to být seznam, slovník nebo i nějaká složitější 14 | objektová struktura. Model se nestará o to, jak budou data zobrazena, i když 15 | forma uložení může zvolena tak, aby následné zobrazení bylo jednoduché. 16 | 17 | View je pak nějaká komponenta, která data z modelu zobrazuje. Pojem view je zde 18 | nejednoznačný, protože jako view označujeme i celý soubor s grafickým rozhraním, 19 | v této kapitole budeme brát za view komponentu, která slouží ke zobrazení dat z 20 | modelu. Komponenty pro složitější modely se obvykle přímo jmenují `View`, 21 | ale jako view může sloužit v podstatě libovolná komponenta, která je schopna 22 | danou vlastnost modelu zobrazit. V minulém případě bylo view komponenta `Text`, 23 | modelem byl objekt `click_model`. 24 | 25 | Modelů může být v aplikaci více, stejně tak každý model může mít více view. 26 | Například model reprezentující nějaký bod na mapě může mít view vrstvu na mapě 27 | zobrazující bod a vedle mapy dále textové pole se souřadnicemi tohoto bodu. V 28 | mapě můžeme chtít zobrazovat i polygony, tyto polygony budou mít svůj vlastní 29 | model a jako view jim bude sloužit jiná vrstva v mapě. 30 | 31 | # Obousměrná vazba 32 | View nemusí sloužit jen k zobrazení nějakého modelu, ale může umožňovat i změny 33 | tohoto modelu. Vazby jsou vždy jednosměrné, pokud potřebujeme, aby view mohl 34 | změnit data v modelu, musíme druhý směr vazby přidat explicitně. Pokud to 35 | neuděláme, pak změna ve view zůstane ve view a když se změní model, view 36 | provedenou změnu zahodí a zobrazí hodnotu z modelu. Pokud přidáme vazbu opačným 37 | směrem explicitně, musíme být opratrní při implementaci modelu, hrozí totiž 38 | zacyklení. 39 | 40 | Vazba view a modelu není totiž dána přímo property, která je svázána, 41 | ale notifikacemi, že se property změnila. Pokud dojde v modelu ke změně hodnoty 42 | nějaké property, je vyslán notifikační signál (viz setter v minulém příkladu). 43 | View je na tento signál připojeno a když dostane notifikační signál, zeptá se na 44 | aktuální hodnotu dané property v modelu a tu zobrazí. Naopak to funguje stejně, 45 | když máme svázán view a model směrem z view do modelu a dojde ke změně view 46 | (například uživatel začne psát do 47 | [`TextInput`u](https://doc.qt.io/qt-5/qml-qtquick-textinput.html), po každém 48 | napsaném / smazaném znaku vyšle `TextInput` signál, který zachytí property a 49 | zavolá *setter* s aktuální hodnotou z view. Tím ale dojde ke změně modelu, tedy 50 | model vyšle notifikační signál, ten zachytí view a načte si z modelu aktuální 51 | hodnotu. Tím ale došlo ke změně ve view a tak je vyslán notifikační signál, 52 | který zachytí model ... a byli bychom v kruhu, kdybychom si na to nedali pozor. 53 | 54 | Z tohoto důvodu je potřeba být v setterech opatrný a vysílat notifikační signál 55 | jen tehdy, když skutečně dojde ke změně hodnoty v modelu. Jednak se tím ušetří 56 | trochu výkonu, kdy view nebude muset překreslovat jednu hodnotu za tu samou, ale 57 | hlavně tím rozbijeme zacyklení, protože když model dostane zprávu, že se změnila 58 | hodnota property a zavolá setter, tak setter již žádný signál nevyšle a cyklus 59 | je přerušen. 60 | 61 | Pokud vytvoříme obousměrnou vazbu, může si někdy QML stěžovat na `QML QQuickTextInput: Binding loop detected`. 62 | Je vhodné ověřit, že nedochází k zacyklení a následně je možné toto upozornění 63 | ignorovat. 64 | 65 | ## Popis programu 66 | Program slouží k převodu úhlových jednotek mezi desetinnými stupni a stupni, 67 | minutami a vteřinami. Program obsahuje 4 textová pole, kde každé je obousměrně 68 | svázáno s jednou property a 2 tlačítka, která jsou připojena k metodám objektu 69 | třídy `DMSModel`. 70 | 71 | Třída `DMSModel` slouží jako model pro celou aplikaci, v jednotlivých property 72 | je ukázka, jak lze zápis property postupně zkrátit při zachování stejné 73 | funkčnosti. Celý getter lze jako lambda funkci integrovat přímo do vytváření 74 | property. Setter je složitější a pokud bychom se pokusili o totéž, nebyl by 75 | výsledek o mnoho kratší a byl by výrazně méně čitelný. Notifikační signál nelze 76 | integrovat stejně jako getter, protože z důvodů vazeb musí být pojmenovanou 77 | metodou dané třídy. Zbytek kódu je obdobný jako u minulého příkladu. 78 | 79 | ## Popis grafického rozhraní 80 | Grafické rozhraní je tvořeno dvěma řádky (komponenta 81 | [`Row`](https://doc.qt.io/qt-5/qml-qtquick-row.html)), které jsou uspořádny do 82 | sloupce (komponenta [`Column`](https://doc.qt.io/qt-5/qml-qtquick-column.html)). 83 | Aby jednotlivé nápisy v řádku nesplývaly, má řádek nastavenu vlastnost `spacing` 84 | na 2 px. Jednotlivá pole, do kterých uživatel zadává stupně, minuty a vteřiny 85 | jsou tvořeny komponentami [`TextInput`](https://doc.qt.io/qt-5/qml-qtquick-textinput.html). 86 | 87 | Komponenta [`TextInput`](https://doc.qt.io/qt-5/qml-qtquick-textinput.html) 88 | se chová obdobně jako komponenta `Label`, ale navíc umožňuje editaci textu. 89 | Vazba směrem z Pythonu je vytvořena ve vlastnosti `text`. Abychom mohli vytvořit 90 | opačnou vazbu, musíme se umět odkázat na konkrétní komponentu `TextInput`. 91 | 92 | Každé komponentě QML můžeme nastavit vlastnost `id` - identifkátor, který by měl 93 | být v rámci celého QML unikátní. Pak na takovou komponentu můžeme odkudkoli z 94 | QML odkazovat právě pomocí jejího identifikátoru. Pojmenování identifikátoru je 95 | vhodné volit tak, aby z něj bylo jasné, co je daná komponenta zač a co v sobě 96 | uchovává. Pojmenovává se obvykle camelCase ve formátu ``, tedy 97 | například `secInput`. 98 | 99 | Vazbu z QML do modelu (a tedy i do Pythonu) vyrobíme pomocí komponenty 100 | [`Binding`](https://doc.qt.io/qt-5/qml-qtqml-binding.html). Tato komponenta má 101 | široké možnosti použití, my ji zatím použijeme jen velmi jednoduše. Abychom 102 | mohli vytvořit vazbu, musíme vědět, property kterého objektu svazujeme s jakou 103 | hodnotou / property. Objekt určíme pomocí vlastnosti `target`, jeho property 104 | (cíl vazby) pomocí vlastnosti `property` a zdroj vazby pomocí vlastnosti 105 | `value`(zde využijeme identifikátor `TextInput`u). Od této chvíle kdykoli se 106 | změní zdroj, změní se i cíl a díky tomu, že opačnou vazbu máme definovanou v 107 | TextInputu samotném, máme vytvořenou obousměrnou vazbu a kdykoli se změní model, 108 | změní se i text v `TextInput`u a naopak. 109 | 110 | 111 | Když program spustíme a zkusíme převést jednu vteřinu na desetiny stupně, 112 | program nám vypíše `QML QQuickTextInput: Binding loop detected for property 113 | "text"`. Je tomu tak proto, že `TextInput` má pomocí vlastnosti `maximumLength` 114 | omezenou maximální délku vloženého textu. Pokud převedeme jednu vteřinu na 115 | desetiny stupně, získáme periodické číslo. Po klikutí na *To float!* tedy 116 | vznikne dlouhé číslo které se přes *setter* zapíše do property `deg_float`. Přes 117 | notifikační signál se propíše do `TextInput`u, kam se ale celé nevejde, tedy 118 | zbylé číslice jsou zahozeny. Protože se `TextInput` změnil, je znovu vyvolán 119 | *setter* property `deg_float`, ale tentokrát se zkráceným číslem (protože se 120 | propisuje skutečný obsah `TextInput`u). Zkrácené číslo neodpovídá původnímu, 121 | tedy je znovu přiřazeno a znovu je vyslán notifikační signál. Proběhne nové 122 | kolečko až do *setter*u, kde už ale nedochází ke změně a tedy se celý cyklus 123 | zastaví. Qt ale toto chování vyhodnotí jako smyčku ve vazbách a proto vyvolá 124 | chybovou hlášku. 125 | 126 | ## Zdroje 127 | - [How to do Bi-directional Data Binding in Qt](http://imaginativethinking.ca/bi-directional-data-binding-qt-quick/) 128 | -------------------------------------------------------------------------------- /03_dms_converter/dms_converter/dms_converter.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QObject, Signal, Slot, Property, QUrl 2 | from PySide6.QtGui import QGuiApplication 3 | from PySide6.QtQuick import QQuickView 4 | import sys 5 | 6 | VIEW_URL = "view.qml" 7 | 8 | class DMSModel(QObject): 9 | def __init__(self): 10 | QObject.__init__(self) 11 | self._deg = 0 12 | self._min = 0 13 | self._sec = 0 14 | self._deg_float = 0.0 15 | 16 | # Property 'deg' 17 | def set_deg(self, val): 18 | print(f"Current: {self._deg}, new: {val}") 19 | if val != self._deg: 20 | self._deg = val 21 | self.deg_changed.emit(self.deg) 22 | 23 | def get_deg(self): 24 | return self._deg 25 | 26 | deg_changed = Signal(int) 27 | deg = Property(int, get_deg, set_deg, notify=deg_changed) 28 | 29 | # Property 'min' 30 | def set_min(self, val): 31 | if val != self._min: 32 | self._min = val 33 | self.min_changed.emit(self._min) 34 | 35 | # Normal definition of function was shortened using lambda function 36 | get_min = lambda self: self._min 37 | min_changed = Signal(int) 38 | min = Property(int, get_min, set_min, notify=min_changed) 39 | 40 | def set_sec(self, val): 41 | if val != self._sec: 42 | self._sec = val 43 | self.sec_changed.emit(self._sec) 44 | 45 | sec_changed = Signal(int) 46 | # Getter lambda can be moved into the Property creation 47 | sec = Property(int, lambda self: self._sec, set_sec, notify=sec_changed) 48 | 49 | def set_deg_float(self, val): 50 | print(f"Current: {self._deg_float}, new: {val}") 51 | if val != self._deg_float: 52 | self._deg_float = val 53 | self.deg_float_changed.emit(self._deg_float) 54 | 55 | deg_float_changed = Signal(float) 56 | deg_float = Property(float, lambda self: self._deg_float, set_deg_float, notify=deg_float_changed) 57 | 58 | @Slot() 59 | def to_float(self): 60 | print("To float!") 61 | self.deg_float = self.deg + self.min/60 + self.sec/3600 62 | 63 | @Slot() 64 | def to_dms(self): 65 | print("To DMS!") 66 | val = float(self.deg_float) 67 | self.deg = int(val) 68 | val = (val-self.deg)*60 69 | self.min = int(val) 70 | val = (val-self.min)*60 71 | self.sec = int(val) 72 | 73 | 74 | app = QGuiApplication(sys.argv) 75 | view = QQuickView() 76 | url = QUrl(VIEW_URL) 77 | dmsmodel = DMSModel() 78 | ctxt = view.rootContext() 79 | ctxt.setContextProperty('dmsmodel',dmsmodel) 80 | view.setSource(url) 81 | view.show() 82 | app.exec() 83 | -------------------------------------------------------------------------------- /03_dms_converter/dms_converter/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | 4 | Column { 5 | id: column 6 | Row{ 7 | spacing: 2 8 | Label { 9 | text: 'DMS:' 10 | } 11 | TextInput{ 12 | id: degInput 13 | maximumLength: 3 14 | text: dmsmodel.deg 15 | 16 | Binding { 17 | target: dmsmodel 18 | property: "deg" 19 | value: degInput.text 20 | } 21 | } 22 | Label{ 23 | text:'° ' 24 | } 25 | TextInput { 26 | id: minInput 27 | text: dmsmodel.min 28 | 29 | Binding { 30 | target: dmsmodel 31 | property: "min" 32 | value: minInput.text 33 | } 34 | } 35 | Label { 36 | text:'\'' 37 | } 38 | TextInput { 39 | id: secInput 40 | text: dmsmodel.sec 41 | 42 | Binding { 43 | target: dmsmodel 44 | property: "sec" 45 | value: secInput.text 46 | } 47 | } 48 | Label { 49 | text: '\'\'' 50 | } 51 | Button { 52 | onClicked: dmsmodel.to_float() 53 | text: 'To float' 54 | } 55 | } 56 | Row { 57 | spacing: 2 58 | Label { 59 | text: 'Degrees:' 60 | } 61 | TextInput { 62 | id: degFloatInput 63 | maximumLength: 8 64 | text: dmsmodel.deg_float 65 | 66 | Binding { 67 | target: dmsmodel 68 | property: "deg_float" 69 | value: degFloatInput.text 70 | } 71 | 72 | } 73 | Button { 74 | onClicked: dmsmodel.to_dms() 75 | text: 'To DMS' 76 | } 77 | } 78 | } 79 | 80 | 81 | -------------------------------------------------------------------------------- /04_city_list/README.md: -------------------------------------------------------------------------------- 1 | # Seznam měst 2 | 3 | ## Model-View-Delegate 4 | V tomto díle si na jednoduchém seznamu měst v ČR ukážeme použití celého konceptu Model-View-Delegate. Nejprve si tento princip obecně popíšeme, pak se pustíme do detailů jednotlivých částí a předávání dat mezi nimi. Našim cílem je zobrazit seznam všech měst ve sloupci pod sebou. 5 | 6 | *Model* v Pythonu bude poskytovat seznam měst. V QML pak bude [`ListView`](https://doc.qt.io/qt-5/qml-qtquick-listview.html), který tento seznam zobrazí. ListView umí prvky zobrazit pod sebou (nebo vedle sebe, nastavíme-li to), ale to, jakou grafickou reprezentaci mají mít jednotlivé prvky, to musí určit *delegát*. Delegát je zavolán pro každý prvek zvlášť a pro každý prvek vytvoří komponentu, která se pak zobrazí v seznamu. Toto rozdělení nám umožní využívat `ListView` různými kreativními způsoby. Jednotlivé prvky mohou být například obrázky, tlačítka nebo třeba jen barevné obdélníky s parametry závislými na modelu. 7 | 8 | ## Rozhraní a abstraktní třídy 9 | Aby bylo zobrazování seznamů dostatečně univerzální, musí podporovat mnoho různých operací - přidat prvky do seznamu, změnit hodnotu prvku, odebrat prvky, a mnoho dalších. Všechny tyto operace musí být synchronizované mezi grafickým rozhraním a modelem. V obecnějším případě často máme nějakou sadu operací, kterou musí náš kód splňovat, aby se dal použít k nějaké činnosti. Této sadě říkáme *[rozhraní](https://cs.wikipedia.org/wiki/Interface_(programov%C3%A1_konstrukce)) (interface)*. V našem případě nám `ListView` dá seznam funkcí (slotů), které musíme v našem modelu implementovat, abychom mohli náš model použít jako model pro `ListView`. 10 | 11 | Poznámka autora: Všimněme si, že tentokrát bude QML s modelem interagovat výhradně pomocí *slotů* a *signálů* a nikoli pomocí *property*. V našem modelu totiž nemusíme mít data, která chceme zobrazovat, ve formě seznamu, dokonce je nemusíme mít vůbec (například když je požadován nějaký prvek seznamu, tak ho jen vytáhneme z databáze a předáme do rozhraní a v Pythonu ho vůbec nedržíme). 12 | 13 | Abychom tyto operace nemuseli implementovat od nuly, máme na to připravené *abstraktní třídy*. Abstraktní třídu si můžeme představit jako nedokončenou třídu určenou k tomu, abychom si ji dokončili podle naší potřeby a ona nám pomohla implementovat nějaké rozhraní. Takováto třída má obvykle implmentovány takové metody, k jejichž implementaci jí stačí výsledky jiných jejích metod (včetně těch, co musíme implementovat my). Nemusíme se proto při implementaci těmito metodami zabývat, ale pokud bychom chtěli, můžeme je samozřejmě přetížit. 14 | 15 | Ukažme si příklad jednoduchého rozhraní a abstraktní třídy. Mějme rozhraní sloužící k přístupu k seznamu. Toto rozhraní vyžaduje následující metody: 16 | - `count(self)` - vrátí počet prvků v seznamu 17 | - `get(self,i)` - vrátí `i`-tý prvek 18 | - `first(self)` - vrátí první prvek 19 | - `last(self)` - vrátí poslední prvek 20 | 21 | Když se nad těmito metodami zamyslíme, tak pokud máme implementované metody `count` a `get`, tak umíme přímočaře implementovat metody `first` a `last`. Aby se nemusel každý, kdo chce implemetovat toto rozhraní, obtěžovat se psaním triviálních metod `first` a `last`, připravíme uživatelům abstraktní třídu `AbstractList`: 22 | 23 | from abc import ABC, abstractmethod 24 | import typing 25 | 26 | class AbstractList(ABC): 27 | 28 | @abstractmethod 29 | def count(self) -> int: 30 | """returns nomber of items in the list""" 31 | pass 32 | 33 | @abstractmethod 34 | def get(self,i:int) -> typing.Any: 35 | """returns i-th element of the list""" 36 | pass 37 | 38 | def first(self) -> typing.Any: 39 | """returns first element of the list""" 40 | return self.get(0) 41 | 42 | def last(self) -> typing.Any: 43 | """returns last element of the list""" 44 | return self.get(self.count()-1) 45 | 46 | Pokud nyní někdo chce implementovat výše uvedené rozhraní, stačí mu podědit naši třídu `AbstractList` a implementovat její abstraktní metody, tedy `count` a `get`, tedy například takto: 47 | 48 | class ListClass(AbstractList): 49 | 50 | def __init__(self,list): 51 | self.list = list 52 | 53 | def count(self): 54 | return len(self.list) 55 | 56 | def get(self,i): 57 | return self.list[i] 58 | 59 | Nyní se vrátíme zpět k našemu původnímu příkladu. Z [dokumentace](https://doc.qt.io/qt-5/qml-qtquick-listview.html#model-prop) zjistíme, že náš model musí dědit (být podtřídou) třídy [`QAbstractItemModel`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html). Nebudeme se ale hned hnát do implementací abstraktních metod této třídy, ale přečteme si [podrobný popis](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#detailed-description), kde se dočteme, že pro `ListView` máme zvážit použití [`QAbstractListModel`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractListModel.html), který pro našim potřebám vyhovuje více, proto jej použijeme. Zde nyní náš model opustíme a vrátíme se k němu v kapitole Model, až si vysvětlíme další věci potřebné k jeho implementaci. 60 | 61 | ## Delegát 62 | Delegát je další důležitou součástí skládačky. Delegát "dostane" položku a "vrátí" komponentu, která se následně vloží do `ListView`. Pojem "dostane" a "vrátí" je v uvozovkách, protože nejde o funkci, ale o nějakou komponentu v QML. Tato komponenta může uvnitř využívat property `model`, která drží aktuální prvek a tak, jak bude komponenta vypadat po "dosazení" aktuální položky za všechna použití property `model`, bude vložena do `ListView`. Nejlepší způsob, jak si popsat chování delegáta se všemi souvislostmi je představit si, že v místě, kde je uvedeno `delegate:` vložíme komponentu, která je delegátem, tolikrát, kolikrát je v modelu a za property `model` v každé vložené komponentě dosadíme odpovídající prvek modelu. 63 | 64 | ## Role 65 | Od property `model` v delegátovi bychom mohli očekávat, že se bude chovat jako objekt ze seznamu a budeme tedy moci odkazovat na jeho property. Bohužel tomu tak není, protože by to vyžadovalo, aby jednotlivé prvky seznamu dědily od `QObject`, aby mohly být vůbec z QML dostupné. Tento požadavek by ale byl příliš silný a řadu věcí by znesnadňoval, proto se k vlastnostem prvků seznamu přistupuje pomocí rolí. Pomocí [rolí](https://doc.qt.io/qtforpython/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.ItemDataRole) říkáme, o jakou vlastnost objektu máme zájem. Model se následně dozví, o jakou roli kterého prvku máme zájem a podle toho nám vrátí patřičná data. 66 | 67 | Máme k dispozici několik [předdefinovaných rolí](https://doc.qt.io/qtforpython/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.ItemDataRole), můžeme si ale snadno vytvořit role vlastní. Vlastními rolemi se budeme zabývat v příštím díle, v tomto si vystačíme s předdefinovanými a to konkrétně s tou nejčastěji používanou, `Qt.DisplayRole`. Tuto roli můžeme v QML použít pomocí `model.display`. Pokud nám v našem případě model vrací jako `Qt.DisplayRole` jméno města, delegát zobrazující jednoduchý text může vypadat například takto: 68 | 69 | delegate: Text { 70 | text: model.display 71 | } 72 | 73 | ## Model 74 | Nyní již víme, co jsou to role a můžeme implementovat naši třídu `CityListModel` dědící od `QAbstractListModel`. Z [dokumentace k dědění](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractListModel.html#subclassing) z `QAbstractListModel` zjistíme, že potřebujeme implementovat alespoň metody [`rowCount`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.rowCount) a [`data`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.data). 75 | 76 | Metoda [`rowCount(self,parent=QModelIndex()) -> int`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.rowCount) vrací počet řádek našeho modelu, její implementace je tedy přímočará. 77 | 78 | Metoda [`data(self, index: QModelIndex, role=Qt.DisplayRole) -> typing.Any`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.data) je složitější. Bere dva argumenty - `index`, ze kterého zjistíme, který prvek je požadován a `role`, která nám určuje, jakou roli daného prvku máme vrátit. `index` je typu [`QModelIndex`](https://doc.qt.io/qtforpython/PySide6/QtCore/QModelIndex.html), který umožňuje mít složitější strukturu dat než obyčejný seznam, nám ale z něj budou stačit jen dvě metody - [`isValid()`](https://doc.qt.io/qtforpython/PySide6/QtCore/QModelIndex.html#PySide6.QtCore.PySide6.QtCore.QModelIndex.isValid), která nám vrátí `True`, pokud jde o platný index a [`row()`](https://doc.qt.io/qtforpython/PySide6/QtCore/QModelIndex.html#PySide6.QtCore.PySide6.QtCore.QModelIndex.row), která nám řekne, kolikátý prvek máme vrátit. `role` je pak jednou z [rolí](https://doc.qt.io/qtforpython/PySide6/QtCore/Qt.html#PySide6.QtCore.PySide6.QtCore.Qt.ItemDataRole), v našem případě si vystačíme s `Qt.DisplayRole` a ostatní budeme zatím ignorovat. 79 | 80 | Implementujeme-li tyto dvě metody, můžeme naši třídu `CityListModel` použít jako model pro libovolné `ListView` v QML. 81 | 82 | ## Popis programu 83 | Program zobrazuje seznam všech měst v ČR. Program obsahuje jedno `ListView`, ve kterém jsou města zobrazena a pokud zvolíme pomocí proměnné `VIEW_URL` bohatši rozhraní `view.qml`, můžeme jednotlivá města označovat pomocí klávesnice nebo myši program do konzole vypíše, kolikáté město jsme zvolili. Třída `CityListModel` dědící od `QAbstractListModel` zajišťuje model pro `ListView`. 84 | 85 | Samotný seznam měst pochází z projektu [Wikidata](https://www.wikidata.org/wiki/Wikidata:Main_Page), dotaz ve [SPARQL](https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/Wikidata_Query_Help) si můžete [prohlédnout](city_list/wikidata_cities.sparql). Dotazovací jazyk není příliš přívětivý, pokud byste chtěli něco získat z Wikidat, doporučuji si v příkladech najít podobný dotaz a upravit ho, než se snažit psát dotaz od začátku. 86 | 87 | 88 | ## Popis grafického rozhraní 89 | Program má dvé grafické rozhraní - jednoduché v souboru [`simple_view.qml`](city_list/simple_view.qml) a pokročilé ve [`view.qml`](city_list/view.qml), které ukazuje typickou implementaci takového seznamu. 90 | 91 | ### Jednodušší rozhraní 92 | Okno jednoduššího rozhraní je tvořeno komponentou [`Rectangle`](https://doc.qt.io/qt-5/qml-qtquick-rectangle.html) o velikosti 200 x 500 pixelů. Uvnitř tohoto obdélníka je umístěn [`ListView`](https://doc.qt.io/qt-5/qml-qtquick-listview.html), který vyplňuje celý tento obdélník. Tento `ListView` má jako model `CityListModel`, který jsme vytvořili v Pythonu a delegátem je mu komponenta `Text`, která má nastavený text podle `DisplayRole` (v QML `display`) jednotlivých prvků modelu. 93 | 94 | Výsledek je okno seznamem měst, kde tento seznam vypadá z pohledu komponent jako mnoho komponent typu [`Text`](https://doc.qt.io/qt-5/qml-qtquick-text.html) uspořádaných pod sebou. `ListView` je [`Flickable`](https://doc.qt.io/qt-5/qml-qtquick-flickable.html), což znamená, že se předpokládá, že obsah komponenty bude větší než její velikost a je umožněno obsahem scrollovat. U `ListView` lze ve výchozím stavu scrollovat jen ve vertikálním směru. Uvědomme si, že tato vlastnost není výchozí pro komponenty, které jsme dosud používali - pokud uděláme v okně komponentu větší, než je okno, tak část z ní zůstane skrytá a nemůžeme scrollovat tak, abychom viděli její skryté části. 95 | 96 | ### Pokročilé rozhraní 97 | Toto rozhraní rozšiřuje jednodušší rozhraní a dodává možnost zvolit si město, a to pomocí myši nebo pomocí klávesnice. Nejprve se podívejme na delegáta. Delegát byl oddělen do samostatné komponenty typu [`Component`](https://doc.qt.io/qt-5/qml-qtqml-component.html). Narozdíl od ostatních komponent, které v rozhraní objeví tam, kde byly deklarovány, [`Component`](https://doc.qt.io/qt-5/qml-qtqml-component.html) se jen vytvoří komponentu s nastaveným `id` a můžeme ji vložit do rozhraní pomocí jejího `id` všude tam, kde je očekávána nějaká komponenta, například jako argument [`delegate`](https://doc.qt.io/qt-5/qml-qtquick-listview.html#delegate-prop), nebo pomocí komponenty [`Loader`](https://doc.qt.io/qt-5/qml-qtquick-loader.html), které se ale nyní věnovat nebudeme. Proč se nám to může hodit? Jednak si komponentu můžeme vytvořit v místě, kde to nezhorší čitelnost kódu, jednak ji pak můžeme použít vícekrát, například jako delegáta více různých seznamů, pokud mají tyto seznamy mít stejný vzhled a chování. 98 | 99 | Abychom mohli měnit zvolené město klávesnicí, je potřeba u [`ListView`](https://doc.qt.io/qt-5/qml-qtquick-listview.html) nastavit vlastnost [`focus`](https://doc.qt.io/qt-5/qml-qtquick-item.html#focus-prop) na `true`. Fokus je vlastnost, která říká, která komponenta dostane události klávesnice. Pokud někam klikneme myší je jasné, na kterou komponentu jsme kliknuli, v případě, že je komponent schopných zpracovat kliknutí či jinou událost od myši více, předá se událost té nejvíce nahoře. Pokud ale začneme psát na klávesnici a aplikace má více textových políček, není jasné, do kterého právě psaný text patří. K určení, která komponenta má dostat událost od klávesnice, slouží fokus. Fokus může mít pouze jedna komponenta a u textových políček je obvykle indikován blikajícím kurzorem, u jiných nemusí být fokus poznat. Ve výchozím stavu nemá fokus žádná komponenta a tudíž program na stisky kláves nijak nereaguje. Komponenta může mít povolení získat fokus ve výchozím stavu například různá textová pole, nebo tuto možnost můžeme povolit pomocí `focus: true`. K tomu, jak fokus funguje detailněji a jak ho lze předávat mezi komponentami se možná dostaneme později, pro náš současný případ stačí, že nastavením `focus: true` bude mít `ListView` možnost získat fokus a protože to bude jediná taková komponenta, tak ho i získá a můžeme přepínat vybraná města pomocí klávesnice. 100 | 101 | Vraťme se nyní do delegáta. Delegát obsahuje komponentu [`Item`](https://doc.qt.io/qt-5/qml-qtquick-item.html), což je obecná komponenta ze které dědí všechny ostatní grafické komponenty a samotná [`Item`](https://doc.qt.io/qt-5/qml-qtquick-item.html) se používá, když potřebujeme seskupit více prvků dohromady, což je i náš případ. Tato komponenta má nastavenou šířku podle šířky svého předka, což je v tomto případě [`ListView`](https://doc.qt.io/qt-5/qml-qtquick-listview.html), tedy bude zabírat celou šířku řádku. [`ListView`](https://doc.qt.io/qt-5/qml-qtquick-listview.html) nemá stanovenou výšku řádku, tu určuje podle velikosti komponent pro jednotlivé prvky. Chceme-li tedy určit výšku, musíme zjistit, jak bude vysoká komponenta [`Text`](https://doc.qt.io/qt-5/qml-qtquick-text.html), která obsahuje název města. To můžeme udělat buď explicitním dotazem přes její `id` na její vlastnost [`height`](https://doc.qt.io/qt-5/qml-qtquick-item.html#height-prop), nebo můžeme využít vlastnost [`childrenRect`](https://doc.qt.io/qt-5/qml-qtquick-item.html#childrenRect.height-prop), který pro každou komponentu popisuje, jak budou dohromady velké všechny komponenty v ní vložené. 102 | 103 | Kromě komponenty [`Text`](https://doc.qt.io/qt-5/qml-qtquick-text.html) obsahuje [`Item`](https://doc.qt.io/qt-5/qml-qtquick-item.html) ještě [`MouseArea`](https://doc.qt.io/qt-5/qml-qtquick-mousearea.html), tedy komponentu, která zpracovává události myši. Samotný `ListView` nijak neřeší výběr pomocí myši a nechává to na delegátech, je-li to potřeba. Přidáme tedy `MouseArea` přes celou plochu položky (Všimněte si, že `anchors.fill: parent` nekoliduje s `height: childrenRect.height`, protože je vyhodnocován, až je výška známa.) a jako akci při kliknutí nastavíme změnu zvoleného prvku. 104 | 105 | Index aktuálně zvoleného prvku je v `ListView` uložen ve vlastnosti [`currentIndex`](https://doc.qt.io/qt-5/qml-qtquick-listview.html#currentIndex-prop). Index prvku, který je reprezentován, zjistíme v delegátovi pomocí sepciální role [`index`](https://doc.qt.io/qt-5/qtquick-modelviewsdata-modelview.html#qml-data-models). Pokud tedy chceme změnit zvolený prvek `ListView` na aktuální, stačí nám do `currentIndex` daného `ListView` zapsat `index` delegáta, ve kterém bylo kliknuto. 106 | 107 | `ListView` v případě, že se změnil zvolený prvek, vyvolá signál `onCurrentItemChanged`, který využijeme a do konzole (v Visual Studio Code pod zdrojovým kódem, stejné místo, kde se vypisují chyby Pythonu) vypíšeme, jaký index je aktuální. 108 | 109 | Abychom měli vizuální zpětnou vazbu, který prvek je aktuálně zvolen, nastavíme vlastnost [`highlight`](https://doc.qt.io/qt-5/qml-qtquick-listview.html#highlight-prop) u `ListView` komponentu, která se zobrazí pod komponentou vybraného prvku. Geometrii (tedy pozici a velikost) nemusíme určovat, doplní se automaticky, aby zvýrazňující komponenta měla stejnou geometrii jako komponenta reprezentující vybraný prvek. V našem případě tedy jen vyrobíme obdélník, který bude mít světle modré pozadí. 110 | 111 | 112 | ## Zdroje 113 | - [Model/View Programming](https://doc.qt.io/qtforpython/overviews/model-view-programming.html) - popis principu, přibližně odpovídá tomuto a příštímu dílu 114 | - doporučuji od začátku po sekci [Models](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#models) včetně 115 | - od sekce [Model Classes](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#model-classes), po [Using model indexes](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#using-model-indexes) včetně 116 | - od sekce [Creating new models](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#creating-new-models) po [Inserting and removing rows](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#inserting-and-removing-rows) včetně 117 | - od sekce [Item data handling](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#item-data-handling) po [Parents and children](https://doc.qt.io/qtforpython/overviews/model-view-programming.html#parents-and-children) včetně 118 | - mezi těmito částmi jsou sekce věnované jinému druhu grafickému rozhraní, než je QML, tudíž pro nás nejsou důležité 119 | - [ListView QML Type](https://doc.qt.io/qt-5/qml-qtquick-listview.html) 120 | - [QML Listview selected item highlight on click](https://stackoverflow.com/questions/9400002/qml-listview-selected-item-highlight-on-click) 121 | - [QAbstractListModel Class](https://doc.qt.io/qt-5/qabstractlistmodel.html) - dokumentace k C++ variantě 122 | - [Using C++ Models with Qt Quick Views](https://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html) - sice pojednává o C++, ale principy jsou platné stejně i pro Python 123 | - [Keyboard focus in Qt Quick](https://doc.qt.io/qt-5/qtquick-input-focus.html) 124 | -------------------------------------------------------------------------------- /04_city_list/city_list/city_list.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QObject, Signal, Slot, Property, QUrl, QAbstractListModel 2 | from PySide6.QtGui import QGuiApplication 3 | from PySide6.QtQuick import QQuickView 4 | from PySide6 import QtCore 5 | import typing 6 | import sys 7 | import json 8 | 9 | #VIEW_URL = "simple_view.qml" # Simple user interface 10 | VIEW_URL = "view.qml" # Advanced user interface 11 | CITY_LIST_FILE = "souradnice.json" 12 | 13 | 14 | class CityListModel(QAbstractListModel): 15 | """ Class for maintaining list of cities""" 16 | 17 | def __init__(self,filename=None): 18 | """Initialize and load list from given file""" 19 | QAbstractListModel.__init__(self) 20 | self.city_list = [] 21 | if filename: 22 | self.load_from_json(filename) 23 | 24 | def load_from_json(self,filename): 25 | """Load list of cities from given file""" 26 | with open(filename,encoding="utf-8") as f: 27 | self.city_list = json.load(f) 28 | 29 | def rowCount(self, parent:QtCore.QModelIndex=...) -> int: 30 | """ Return number of cities in the list""" 31 | return len(self.city_list) 32 | 33 | def data(self, index:QtCore.QModelIndex, role:int=...) -> typing.Any: 34 | """ For given index and DisplayRole return name of the selected city""" 35 | # Return None if the index is not valid 36 | if not index.isValid(): 37 | return None 38 | # If the role is the DisplayRole, return name of the city 39 | if role == QtCore.Qt.DisplayRole: 40 | return self.city_list[index.row()]["muniLabel"] 41 | 42 | 43 | app = QGuiApplication(sys.argv) 44 | view = QQuickView() 45 | url = QUrl(VIEW_URL) 46 | citylist_model = CityListModel(CITY_LIST_FILE) 47 | ctxt = view.rootContext() 48 | ctxt.setContextProperty('cityListModel',citylist_model) 49 | view.setSource(url) 50 | view.show() 51 | app.exec() 52 | -------------------------------------------------------------------------------- /04_city_list/city_list/simple_view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | 4 | Rectangle { 5 | width: 200 6 | height: 500 7 | 8 | ListView { 9 | anchors.fill: parent 10 | model: cityListModel 11 | 12 | delegate: Text { 13 | text: model.display 14 | } 15 | 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /04_city_list/city_list/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | 4 | Rectangle { 5 | width: 200 6 | height: 500 7 | 8 | ListView { 9 | id: cityList 10 | anchors.fill: parent 11 | model: cityListModel 12 | focus: true 13 | 14 | Component { 15 | id: cityListDelegate 16 | Item { 17 | width: parent.width 18 | height: childrenRect.height 19 | Text { 20 | text: model.display 21 | } 22 | MouseArea { 23 | anchors.fill: parent 24 | onClicked: cityList.currentIndex = index 25 | } 26 | } 27 | } 28 | delegate: cityListDelegate 29 | 30 | onCurrentItemChanged: console.log(cityList.currentIndex + ' selected') 31 | 32 | highlight: Rectangle { 33 | color: "lightsteelblue" 34 | } 35 | 36 | } 37 | 38 | 39 | 40 | } -------------------------------------------------------------------------------- /04_city_list/city_list/wikidata_cities.sparql: -------------------------------------------------------------------------------- 1 | #Map and list of cities in Czech Republic 2 | #defaultView:Map 3 | select ?muni ?muniLabel ?location ?population ?area where { 4 | ?muni p:P31 ?instanceOf; # Get statement because we need this later 5 | wdt:P625 ?location. # Get location 6 | OPTIONAL { ?muni wdt:P1082 ?population.} # Get population 7 | OPTIONAL { ?muni wdt:P2046 ?area.} # Get area 8 | ?instanceOf ps:P31 wd:Q15978299. # P31 should be 'municipality of the Czech Republic' 9 | service wikibase:label { bd:serviceParam wikibase:language "cs". } # Show names in Czech 10 | } 11 | -------------------------------------------------------------------------------- /05_city_map/README.md: -------------------------------------------------------------------------------- 1 | # Mapa měst 2 | 3 | V tomto díle přidáme k seznamu měst z minulého dílu jejich mapu a zobrazení dalších informací o aktuálně zvoleném městu. Navíc při zvolení města v seznamu se mapa automaticky posune tak, aby zvolené město bylo uprostřed. 4 | 5 | ## Enum 6 | Pokud potřebujeme vyjádřit nějaký výčet, obvykle číselných, hodnot, kde každá hodnota má nějaký význam, je vhodné si hodnoty pojmenovat. Pro pojmenovávání výčtů je v Pythonu k dispozici třída [`Enum`](https://docs.python.org/3/library/enum.html), která umožňuje snadno jednotlivé hodnoty výčtu pojmenovat a použít. 7 | 8 | Enum vytvoříme snadno tak, že vytvoříme třídu, která dědí od [`Enum`](https://docs.python.org/3/library/enum.html). Jako atributy třídy (tedy přímo ve třídě, nikoli v inicializátoru) pak popíšeme jednotlivé prvky výčtu a přiřadíme k nim číselné hodnoty. Jednotlivé prvky výčtu je vhodné pojmenovávat velkými písmeny. 9 | 10 | Příklad: 11 | 12 | from enum import Enum 13 | 14 | class Roles(Enum): 15 | LOCATION = QtCore.Qt.UserRole+0 16 | AREA = QtCore.Qt.UserRole+1 17 | POPULATION = QtCore.Qt.UserRole+2 18 | 19 | K prvkům takto vytvořeného enumu můžeme přistupovat pomocí jména, například `Roles.LOCATION`, pokud chceme získat číselnou hodnotu, použijeme atribut `.value`, například `Roles.AREA.value`. 20 | 21 | Pokud nám na přesných číselných hodnotách nezáleží, můžeme použít místo číselné hodnoty funkci [`auto()`](https://docs.python.org/3/library/enum.html#enum.auto) z modulu `Enum` a ta čísla přiřadí automaticky. 22 | 23 | Příklad: 24 | 25 | from enum import Enum, auto 26 | 27 | class Fruit(Enum): 28 | APPLE = auto() 29 | PEAR = auto() 30 | GRAPEFRUIT = auto() 31 | 32 | 33 | ## Role 34 | Jak jsme si již [v minulém díle](../04_city_list/README.md) ukázali, k jednotlivým vlastnostem prvků v seznamu přistupujeme pomocí rolí. Zatím jsme si ukazovali pouze výchozí [`Qt.DisplayRole`](https://doc.qt.io/qtforpython/PySide2/QtCore/Qt.html#PySide2.QtCore.PySide2.QtCore.Qt.ItemDataRole), ale nyní budeme chtít kromě jména města umět získat i počet obyvatel, rozlohu a pro umístění v mapě i souřadnice. Pro každou tuto informaci si tedy vytvoříme vlastní roli. 35 | 36 | Qt podporuje uživatelsky vytvářené role. Aby nedocházelo ke kolizi rolí definovaných v Qt a uživatelských rolí, je existuje konstanta `Qt.UserRole`, která nám říká, od kterého čísla máme začít vytvářené role číslovat. Protože budeme vytvářet rolí více, vytvoříme si pro naše role enum. Tento enum můžeme vytvořit přímo uvnitř třídy `CityListModel`, protože jen uvnitř této třídy pro nás mají hodnoty význam. 37 | 38 | Abychom mohli přistupovat k nově vytvořeným rolím, musíme dát všem, kteří naši třídu používají, najevo, že naše třída tyto role podporuje. Ke zjištění, jaké role jsou pro danou třídu dostupné slouží metoda [`roleNames`](https://doc.qt.io/qtforpython/PySide2/QtCore/QAbstractItemModel.html#PySide2.QtCore.PySide2.QtCore.QAbstractItemModel.roleNames). Tato metoda vrací slovník, jehož klíči jsou čísla rolí a hodnotami pojmenování rolí, tedy to, jak budeme k rolím přistupovat z QML. Pokud metodu nepředefinujeme, pak se předávají [výchozí role](https://doc.qt.io/qtforpython/PySide2/QtCore/QAbstractItemModel.html#PySide2.QtCore.PySide2.QtCore.QAbstractItemModel.roleNames). My k nim chceme přidat i role vlastní, proto nejprve získáme slovík rolí od předka a následně k němu přidáme nové klíče pro námi vytvořené role. 39 | 40 | Hodnotami nemohou být obyčejné řetězce, ale kvůli vazbám do Qt musí jít o [`QByteArray`](https://doc.qt.io/qtforpython/PySide2/QtCore/QByteArray.html?highlight=qbytearray#PySide2.QtCore.QByteArray). Ten můžeme snadno vytvořit předáním [`bytes`](https://docs.python.org/3/library/stdtypes.html#bytes) objektu pri vytváření [`QByteArray`](https://doc.qt.io/qtforpython/PySide2/QtCore/QByteArray.html?highlight=qbytearray#PySide2.QtCore.QByteArray). [`bytes`](https://docs.python.org/3/library/stdtypes.html#bytes) objekt vytvoříme jako normální řetězec, akorát před první uvozovky předřadíme `b`, tedy například `b'location'`. Takovýto objekt pak není řetězcem, ale dokud v něm používáme jen základní znaky (pro nás převážně písmena anglické abecedy a čísla), chovají se tyto objekty obdobně. V našem případě s ním nepotřebujeme nijak pracovat, stačí nám ho jen vytvořit a předat ho do `QByteArray`, tedy například `QByteArray(b'location')`. 41 | 42 | Pokud máme metodu `roleNames()` takto předefinovanou, můžeme kdekoli, kde pracujeme s rolemi našeho modelu, používat i nově přidané role. 43 | 44 | ## Souřadnice 45 | Pro práci se zeměpisnými souřadnicemi se v Qt používá třída [`QGeoCoordinate`](https://doc.qt.io/qtforpython/PySide2/QtPositioning/QGeoCoordinate.html). Tato třída umožňuje uchovávat jak 2D, tak 3D souřadnice, souřadnice musí být v systému WGS84. Třída má i metody na výpočet vzdálenosti nebo azimutu mezi dvěma body. Při vytváření [`QGeoCoordinate`](https://doc.qt.io/qtforpython/PySide2/QtPositioning/QGeoCoordinate.html) se jako první parametr zadává zeměpisná šířka, jako druhý zeměpisná délka a jako třetí volitelný nadmořská výška. 46 | 47 | ## Popis programu 48 | Program zobrazuje seznam všech měst v ČR a při zvolení nějakého města ze seznamu program ukáže v prostředním sloupci jeho index (pořadí v seznamu), rozlohu a počet obyvatel a zároveň vystředí mapu v pravé části tak, aby bylo zvolené město uprostřed. 49 | 50 | Třída `CityListModel` slouží jako model pro seznam i mapu a jsou z ní získávány i rozšiřující informace o městech. 51 | 52 | ## Popis grafického rozhraní 53 | Rozhraní je rozděleno do tří sloupců. V prvním je seznam měst, ve druhém rozšiřující informace o zvoleném městě a ve třetím sloupci je zobrazená mapa s městy (reprezentovány svými popisky). 54 | 55 | Protože na mnoha místech potřebujeme pracovat s aktuálně zvolenou položkou ze seznamu, bylo by nepraktické ve všech místech, kde s ní potřebujeme pracovat, ji získávat ze seznamu. Také by to nebylo vhodné z pohledu rozšiřitelnosti, například pokud bychom umožnili výběr města i kliknutím do mapy, tak bychom museli celý systém navázání složitě upravovat. Proto si [vytvoříme novou property](https://doc.qt.io/qt-5/qtqml-syntax-objectattributes.html#property-attributes) `currentModelItem`, ve které budeme uchovávat aktuálně zvolené město ze seznamu. Přesněji model aktuálně zvoleného města, kterého se můžeme ptát na všechny role, které jsme si předtím v Pythonu deklarovali. Tato property je viditelná z celého QML a použijeme ji všude tam, kde chceme zobrazovat informace o aktuálně vybraném prvku. Pokud bychom v budoucnu umožnili vybrat město jiným způsobem, bude stačit jen nastavit tuto proměnnou a vše ostatní bude fungovat stejně bez potřeby zásahu. 56 | 57 | Pro zobrazení seznamu používáme [`ListView`](https://doc.qt.io/qtforpython/PySide2/QtPositioning/QGeoCoordinate.html) jako minule, [*delegát*](https://doc.qt.io/qtforpython/PySide2/QtPositioning/QGeoCoordinate.html) a [`highlight`](https://doc.qt.io/qtforpython/PySide2/QtPositioning/QGeoCoordinate.html) zůstaly nezměněny. Místo přímého použití property `cityListModel` jako modelu je nyní použit [`DelegateModel`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html), který nám umožní relativně snadno získat zvolenou položku. [`DelegateModel`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html) má vlastnost [`model`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html), která udává, z jakého modelu bude brát data, v našem případě to bude `cityListModel` a vlastnost [`delegate`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html), kam přiřadíme (nezměněného) delegáta z minule. Když je delegát nastavený u `DelegateModel`, již se u `ListView` nenastavuje. Dále je potřeba při změně vybraného města v seznamu nastavit property `currentModelItem`. 58 | 59 | Získání modelu aktuálně zvoleného prvku není úplně přímočaré, protože narážíme na univerzálnost jednotlivých komponent. Nejprve musíme získat [`DelegateModelGroup`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodelgroup.html) se všemi prvky z [`DelegateModel`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html)u, což uděláme pomocí vlastnosti [`items`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html). Následně můžeme získat pomocí [`.get()`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodelgroup.html#get-method) objekt reprezentující prvek modelu na daném indexu. Index zjistíme stejně jako v minulém případě pomocí vlastnosti [`currentIndex`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodelgroup.html#get-method). Ze získaného objektu ale ještě potřebujeme vlastnost [`model`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodelgroup.html#get-method), abychom získali model zvoleného prvku a mohli se ptát na jeho jednotlivé role. Výše zmíněné kroky můžeme zapsat za sebe do řádku a získáme výsledný tvar `cityListDelegateModel.items.get(cityList.currentIndex).model`. 60 | 61 | Sloupec s rozšířenými informacemi o vybraném městě tvoří sloupec s několika komponentami [`Text`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodelgroup.html#get-method), ve kterých se z `currentModelItem` pomocí rolí získává rozloha a počet obyvatel. Abychom mohli uvádět km2 s horním indexem, musíme u komponenty, která je zobrazuje, nastavit vlastnost [`textFormat`](https://doc.qt.io/qt-5/qml-qtqml-models-delegatemodelgroup.html#get-method) na `Text.RichText` a následně můžeme použít HTML značku [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup) a [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup) k ohraničení horního indexu. 62 | 63 | Poslední sloupec obsahuje mapu. Pro práci s mapou a se souřadnicemi, musíme nejprve importovat [`QtLocation`](https://doc.qt.io/qt-5/qtlocation-index.html) a [`QtPositioning`](https://doc.qt.io/qt-5/qtpositioning-index.html). Mapu v QML reprezentujeme pomocí komponenty [`Map`](https://doc.qt.io/qt-5/qtpositioning-index.html), která ale tvoří jen skořápku zajišťující interakci mezi zobrazovanou mapou, dalšími mapovými prvky a uživatelem. 64 | 65 | K zobrazení podkladní mapy v komponentě [`Map`](https://doc.qt.io/qt-5/qtpositioning-index.html) je třeba vytvořit komponentu [`Plugin`](https://doc.qt.io/qt-5/qtpositioning-index.html), ve které nastavíme, jakého typu mapa je a případné další parametry. V našem případě chceme zobrazit mapové dlaždice z projektu [OpenStreetMap](https://osm.org) a to ve variantě bez popisků, aby se naše popisky měst nepletly s popisky na mapě. Takové dlaždice poskytuje například [Wikimedia cloud services](wmflab.org). Plugin nám pomocí vlastnosti [`name`](https://doc.qt.io/qt-5/qml-qtlocation-plugin.html#name-prop) umožňuje vybrat z několika poskytovatelů dlaždic, my zvolíme `osm`, protože chceme zobrazovat data projektu OpenStreetMap. Tím vybereme [Qt Location Open Street Map Plugin](https://doc.qt.io/qt-5/location-plugin-osm.html), ve kterém můžeme pomocí komponent [`PluginParameter`](https://doc.qt.io/qt-5/qml-qtlocation-pluginparameter.html) upravovat jednotlivá nastavení. My si pomocí parametru [`osm.mapping.custom.host`](https://doc.qt.io/qt-5/qml-qtlocation-plugin.html#name-prop) zvolíme vlastního poskytovatele dlaždic. 66 | 67 | V komponentě [`Map`](https://doc.qt.io/qt-5/qml-qtlocation-map.html) nastavíme plugin pomocí vlastnosti [`plugin`](https://doc.qt.io/qt-5/qml-qtlocation-map.html) a aby byla zobrazena mapa od poskytovatele, kterého jsme nastavili v pluginu, musíme nastavit vlastnost [`activeMapType`](https://doc.qt.io/qt-5/qml-qtlocation-map.html#activeMapType-prop) [dle dokumentace](https://doc.qt.io/qt-5/qml-qtlocation-map.html#activeMapType-prop) na `supportedMapTypes[supportedMapTypes.length - 1]`. Tímto máme nastavenou podkladní mapu. 68 | 69 | Abychom na mapě mohli zobrazovat názvy měst z našeho modelu, musíme do mapy přidat komponentu [`MapItemView`](https://doc.qt.io/qt-5/qml-qtlocation-map.html#activeMapType-prop). Obdobně jako [`ListView`](https://doc.qt.io/qt-5/qml-qtquick-listview.html) nastavíme [`MapItemView`](https://doc.qt.io/qt-5/qml-qtlocation-mapitemview.html) dvě vlastnosti - [`model`](https://doc.qt.io/qt-5/qml-qtlocation-mapitemview.html#model-prop), ze kterého máme brát data a [`delegate`](https://doc.qt.io/qt-5/qml-qtlocation-mapitemview.html#delegate-prop), ve kterém určíme, jak mají data vypadat. Komponenta delegáta musí obsahovat právě jednu komponentu dědící od `MapItem`, což v našem případě je [`MapQuickItem`](https://doc.qt.io/qt-5/qml-qtlocation-mapquickitem.html). 70 | 71 | [`MapQuickItem`](https://doc.qt.io/qt-5/qml-qtlocation-mapquickitem.html) má několik vlastností, které je vhodné nastavit: 72 | - [`coordinate`](https://doc.qt.io/qt-5/qml-qtlocation-mapquickitem.html#coordinate-prop) - souřadnice, na kterých se má zobrazovaná komponenta zobrazit 73 | - [`sourceItem`](https://doc.qt.io/qt-5/qml-qtlocation-mapquickitem.html#sourceItem-prop) - zobrazovaná komponenta - QML komponenta, která se má zobrazit 74 | - [`anchorPoint`](https://doc.qt.io/qt-5/qml-qtlocation-mapquickitem.html#anchorPoint-prop) - který bod ze zobrazované komponenty má být ten, který se zobrazí na zadaných souřadnicích. Udává se v pixelech, ve výchozím stavu se na zadaných souřadnicích zobrazí levý horní roh zobrazované komponenty 75 | 76 | V našem případě je zobrazovaná komponenta jednoduchá komponenta [`Text`](https://doc.qt.io/qt-5/qml-qtquick-text.html), která obsahuje jméno daného města a je umístěna na souřadnice daného města. 77 | 78 | Nyní máme podkladní mapu, popisky s názvy měst a zbývá jen nastavit, jak má být mapa přiblížená a jak má být vystředěná. Přiblížení zvolíme pomocí atributu [`zoomLevel`](https://doc.qt.io/qt-5/qml-qtlocation-map.html#zoomLevel-prop) a střed mapy určený vlastnotí [`center`](https://doc.qt.io/qt-5/qml-qtlocation-map.html#center-prop) svážeme se souřadnicemi vybraného města ze seznamu, čímž zajistíme, aby se mapa při změně vybraného města automaticky vystředila na toto město. Samozřejmě dále můžeme mapou přibližovat, oddalovat a posouvat, ale když dojde ke změně vybraného města, mapa se automaticky vystředí na toto město. 79 | 80 | 81 | ## Zdroje 82 | - [Item roles](https://doc.qt.io/qt-5/model-view-programming.html#item-roles) - vybraná kapitola z Model/View programming, více viz minulý díl 83 | - [QML Object Attributes - Property Attributes](https://doc.qt.io/qt-5/qtqml-syntax-objectattributes.html#property-attributes) - popisuje možnosti vytváření a používání property v QML 84 | - [How to access ListView's current item from qml](https://stackoverflow.com/questions/16389831/how-to-access-listviews-current-item-from-qml) - trik s využitím `DelegateModel` 85 | - [Qt Location](https://doc.qt.io/qt-5/qtlocation-index.html) - souhrn možností knihovny Qt Location 86 | - [Map QML Type](https://doc.qt.io/qt-5/qml-qtlocation-map.html) 87 | -------------------------------------------------------------------------------- /05_city_map/city_map/city_map.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtCore import QObject, Signal, Slot, Property, QUrl, QAbstractListModel, QByteArray 2 | from PySide2.QtGui import QGuiApplication 3 | from PySide2.QtQuick import QQuickView 4 | from PySide2.QtPositioning import QGeoCoordinate 5 | from PySide2 import QtCore 6 | from enum import Enum 7 | import typing 8 | import sys 9 | import json 10 | 11 | VIEW_URL = "view.qml" 12 | CITY_LIST_FILE = "souradnice.json" 13 | 14 | 15 | class CityListModel(QAbstractListModel): 16 | """Class for maintaining list of cities""" 17 | 18 | class Roles(Enum): 19 | """Enum with added custom roles""" 20 | LOCATION = QtCore.Qt.UserRole+0 21 | AREA = QtCore.Qt.UserRole+1 22 | POPULATION = QtCore.Qt.UserRole+2 23 | 24 | def __init__(self,filename=None): 25 | """Initialize and load list from given file""" 26 | QAbstractListModel.__init__(self) 27 | self.city_list = [] 28 | if filename: 29 | self.load_from_json(filename) 30 | 31 | def load_from_json(self,filename): 32 | """Load list of cities from given file""" 33 | with open(filename,encoding="utf-8") as f: 34 | self.city_list = json.load(f) 35 | 36 | # Create QGeoCoordinate from the original JSON location 37 | for c in self.city_list: 38 | pos = c['location'] 39 | lon,lat = pos.split("(")[1].split(")")[0].split(" ") # Get the part between brackets and split it on space 40 | c['location'] = QGeoCoordinate(float(lat),float(lon)) # Create QGeoCoordinate and overwrite original `location` entry 41 | 42 | def rowCount(self, parent:QtCore.QModelIndex=...) -> int: 43 | """ Return number of cities in the list""" 44 | return len(self.city_list) 45 | 46 | def data(self, index:QtCore.QModelIndex, role:int=...) -> typing.Any: 47 | """ For given index and role return information of the city""" 48 | # print(index.row(),role) # Print requested row and role for debugging 49 | if role == QtCore.Qt.DisplayRole: # On DisplayRole return name 50 | return self.city_list[index.row()]["muniLabel"] 51 | elif role == self.Roles.LOCATION.value: # On location role return coordinates 52 | return self.city_list[index.row()]["location"] 53 | elif role == self.Roles.AREA.value: # On area role return area 54 | return self.city_list[index.row()]["area"] 55 | elif role == self.Roles.POPULATION.value: # On population role return population 56 | return self.city_list[index.row()]["population"] 57 | 58 | def roleNames(self) -> typing.Dict[int, QByteArray]: 59 | """Returns dict with role numbers and role names for default and custom roles together""" 60 | # Append custom roles to the default roles and give them names for a usage in the QML 61 | roles = super().roleNames() 62 | roles[self.Roles.LOCATION.value] = QByteArray(b'location') 63 | roles[self.Roles.AREA.value] = QByteArray(b'area') 64 | roles[self.Roles.POPULATION.value] = QByteArray(b'population') 65 | print(roles) 66 | return roles 67 | 68 | 69 | app = QGuiApplication(sys.argv) 70 | view = QQuickView() 71 | url = QUrl(VIEW_URL) 72 | citylist_model = CityListModel(CITY_LIST_FILE) 73 | ctxt = view.rootContext() 74 | ctxt.setContextProperty('cityListModel',citylist_model) 75 | view.setSource(url) 76 | view.show() 77 | app.exec_() 78 | -------------------------------------------------------------------------------- /05_city_map/city_map/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | import QtQml.Models 2.1 4 | import QtLocation 5.14 5 | import QtPositioning 5.14 6 | 7 | Row { 8 | width: 800 9 | height: 500 10 | 11 | // Create property holding model of currently selected city 12 | property var currentModelItem; 13 | 14 | ListView { 15 | id: cityList 16 | width: 250 17 | height: parent.height 18 | focus: true 19 | 20 | Component { 21 | id: cityListDelegate 22 | Item { 23 | width: parent.width 24 | height: childrenRect.height 25 | Text { 26 | text: model.display 27 | } 28 | MouseArea { 29 | anchors.fill: parent 30 | onClicked: cityList.currentIndex = index 31 | } 32 | } 33 | } 34 | 35 | model: DelegateModel { 36 | id: cityListDelegateModel 37 | model: cityListModel 38 | delegate: cityListDelegate 39 | } 40 | 41 | // When current item of the list is changed, update the currentModelItem property 42 | onCurrentItemChanged: currentModelItem = cityListDelegateModel.items.get(cityList.currentIndex).model 43 | 44 | highlight: Rectangle { 45 | color: "lightsteelblue" 46 | } 47 | } 48 | 49 | Column { 50 | Text { 51 | text: cityList.currentIndex 52 | } 53 | Text { 54 | text: "Rozloha:" 55 | } 56 | Text { 57 | textFormat: Text.RichText // We need RichText to render upper index correctly 58 | text: currentModelItem.area+" km2" 59 | } 60 | Text { 61 | text: "Počet obyvatel" 62 | } 63 | Text { 64 | text: currentModelItem.population 65 | } 66 | } 67 | 68 | Plugin { 69 | id: mapPlugin 70 | name: "osm" // We want OpenStreetMap map provider 71 | PluginParameter { 72 | name:"osm.mapping.custom.host" 73 | value:"https://maps.wikimedia.org/osm/" // We want custom tile server for tiles without labels 74 | } 75 | } 76 | 77 | Map { 78 | width: 500 79 | height: parent.height 80 | 81 | plugin: mapPlugin 82 | activeMapType: supportedMapTypes[supportedMapTypes.length - 1] // Use our custom tile server 83 | 84 | center: currentModelItem.location // Center to the selected city 85 | zoomLevel: 10 86 | 87 | MapItemView { 88 | model: cityListModel 89 | delegate: MapQuickItem { 90 | coordinate: model.location 91 | sourceItem: Text{ 92 | text: model.display 93 | } 94 | } 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /06_todo_list/README.md: -------------------------------------------------------------------------------- 1 | # TODO list 2 | 3 | V tomto díle vytvoříme jednoduchý TODO list 4 | 5 | ## Změna počtu prvků modelu 6 | Dosud jsme pracovali pouze s modely, které měly po celou dobu běhu programu 7 | stejný počet prvků. QML část programu se tedy na začátku dotázala, kolik má 8 | model prvků a pak o ně dle potřeby zobrazení žádala. Nyní budeme chtít mít 9 | možnost, jak počet prvků za běhu programu měnit a to tak, aby se tyto změny 10 | ihned projevovaly i v grafickém rozhraní. 11 | 12 | Z pohledu samotných dat v modelu je to jednoduché. Data držíme v seznamu, je 13 | tedy snadné tento seznam metodou [.append(elem)](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types) rozšířit, nebo pomocí 14 | metody [.pop(idx)](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types) některý prvek ze seznamu vyhodit. Pokud ale zkusíme udělat 15 | jen tuto jednoduchou změnu, zjistíme, že sice se model změnil, ale uživatelské 16 | rozhraní stále zobrazuje původní data z modelu. Je tomu tak proto, neboť 17 | [`ListView`](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types) (nebo jiné view) nedostane žádnou informaci o tom, že se data 18 | modelu změnila a tudíž používá ta, která má již načtená. Pokud tedy chceme měnit 19 | počet prvků modelu, musíme o tom informovat i QML a to si pak načte a správně 20 | zobrazí změněné položky. 21 | 22 | ### Přidávání prvků 23 | Pokud chceme prvky přidávat, musíme před samotnou změnou modelu zavolat metodu 24 | [`beginInsertRows(index,first,last`)](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.beginInsertRows), která vyšle signál o tom, že se budou 25 | přidávat do modelu řádky a po jejich přidání budou mít první přidaný řádek index 26 | `first` a poslední přidaný řádek index `last`. Tento signál zachytí QML a od 27 | této chvíle ví, že se data v modelu budou měnit. Poté je možné řádky do modelu 28 | přidat a ihned poté je třeba zavolat metodu [`endInsertRows()`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.endInsertRows), která vyšle 29 | signál o tom, že řádky byly vloženy a modifikace modelu ukončena. Tento signál 30 | také zachytí QML, dotáže se modelu na aktuální počet prvků a načte a zobrazí 31 | přidané prvky. 32 | 33 | Pokud přidáváme jeden prvek, bude `first` a `last` nastavené na stejnou hodnotu, 34 | protože první přidaný prvek je zároveň posledním přidaným prvkem. Pokud chceme 35 | vložit prvky na konec, tak bude mít `first` hodnotu rovnou aktuálnímu počtu 36 | řádků, protože první vložený prvek bude právě na indexu odpovídajícímu 37 | aktuálnímu počtu řádků. 38 | 39 | Metody `beginInsertRows` a `endInsertRows` je vhodné volat vždy co nejtěsněji 40 | kolem úprav samotného modelu. Mezi začátkem a koncem vkládání je model 41 | považovaný za měněný a to může mít důsledky na výkon aplikace. 42 | 43 | ### Odebírání prvků 44 | Odebírání prvků funguje obdobně jako přidávání. Než začneme měnit model, musíme 45 | zavolat metodu [`beginRemoveRows(index, first,last)`](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.beginRemoveRows), která vyšle signál o 46 | tom, že budou z modelu odebrány řádky a to tak, že první odebraný řádek bude 47 | ten, který má aktuálně index `first` a poslední odebraný řádek bude ten, který 48 | má aktuálně index `last`. Následně je možné řádky odebrat v modelu a 49 | bezprostředně po jejich odebrání je potřeba zavolat metodu `endRemoveRows(https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html#PySide6.QtCore.PySide6.QtCore.QAbstractItemModel.endRemoveRows)`, 50 | která vyšle signál, že modifikace modelu byla ukončena. Oba tyto signály zachytí 51 | QML a zajistí, aby zobrazené prvky po ukončení odebírání odpovídaly modelu. 52 | 53 | Obdobně jako `beginInsertRows` a `endInsertRows` by metody `beginRemoveRows` a 54 | `endRemoveRows` měly být volány co nejtěsněji kolem samotné modifikace modelu. 55 | 56 | ### Složitější změny modelu 57 | Pokud potřebujeme významnějším způsobem změnit model, lze při snaze o zachování 58 | jednoduchosti použít postup, kdy nejprve všechny prvky z modelu odebereme, 59 | provedeme modifikace a následně všechny prvky po modifikacích přidáme. Tento 60 | postup není vhodný pro větší datové sady a časově náročnější modifikace, protože 61 | prvky nakrátko zmizí z rozhraní a je výpočetně náročné je všechny z rozhraní 62 | odstranit a následně je tam opět přidat. Pro složitější třídění a filtrování je 63 | vhodné použít modely k tomu určené (např. 64 | [`QSortFilterProxyModel`](https://doc.qt.io/qt-6/qsortfilterproxymodel.html), 65 | ale to je již nad rámec tohoto tutoriálu. 66 | 67 | ## Popis programu 68 | Program zobrazuje seznam úkolů. Tyto úkoly při startu načte ze souboru a 69 | umožňuje pomocí textového pole nové úkoly přidávat a hotové úkoly odstraňovat. 70 | 71 | Modelem aplikace je třída `TaskListModel`, která je velmi podobná třídě 72 | `CityListModel` z [předminulého dílu](04_city_list). Kromě základních metod potřebných pro 73 | správné fungování spolu s QML dále obsahuje tři sloty, které slouží pro 74 | modifikaci seznamu. Všiměte si, že tyto sloty berou argumenty, které pak při 75 | modifikaci využívají. Slot `addTask` dostane jako argument řetězec, který přidá 76 | do seznamu úkolů jako další úkol, slot `deleteTask` dostane jako argument index 77 | úkolu, který má smazat. Tím pádem nepotřebují tyto sloty žádné další vazby na 78 | uživatelské rozhraní, protože rovnou dostanou všechny informace, které ke své 79 | funkci potřebují. 80 | 81 | ## Popis grafického rozhraní 82 | Program má dvě grafická rozhraní - jednoduché v souboru 83 | [`simple_view.qml`](todo_list/simple_view.qml) a pokročilé ve 84 | [`view.qml`](todo_list/view.qml), které ukazuje uživatelsky přívětivější 85 | variantu práce se seznamem úkolů. 86 | 87 | ### Jednodušší rozhraní 88 | Toto rozhraní umožňuje přidávat úkoly zapsáním do textového pole a kliknutím na 89 | "Přidej úkol" a odebrat úkol jeho zvolením v seznamu úkolů a následným kliknutím 90 | na "Odeber zvolený úkol". 91 | 92 | Implementace rozhraní je velmi podobná [seznamu měst](/04_city_list). Za 93 | povšimnutí stojí řešení přidávání úkolů, kdy text nově přidávaného úkolu 94 | nezískává Python z [`TextInputu`]() sám, ale řetězec je předán přímo v signálu. 95 | Umožňuje to tak větší flexibilitu v grafickém rozhraní, kdy je možné zadávat 96 | text různými způsoby a stačí pak jen vyslat správný signál k přidání úkolu. 97 | Obdobně rozhraní pro mazání úkolu je pomocí signálu, který obsahuje index úkolu, 98 | který má smazat. 99 | 100 | ### Pokročilé rozhraní 101 | Toto rozhraní umožňuje přidávat úkoly zapsáním do textového pole a následným 102 | stisknutím Enteru nebo kliknutím na "Přidej úkol". Po přidání úkolu se textové 103 | pole smaže. Odebrání úkolů je možné po jednom kliknutím na úkol nebo na zaškrtávací 104 | políčko vedle něj, dále lze odebrat všechny úkoly naráz pomocí kliknutí na 105 | tlačítko "Odeber všechny úkoly". Seznam úkolů se automaticky zvětšuje tak, aby 106 | vyplnil veškeré dostupné volné místo v okně, textové pole na zadávání nových 107 | úkolů spolu s tlačítky je zarovnáno na dolní stranu okna. 108 | 109 | Aby bylo možné snadno adaptovat uživatelské rozhraní při změně velikosti okna, 110 | budeme používat [`ColumnLayout`](https://doc.qt.io/qt-5/qml-qtquick-layouts-columnlayout.html) a [`RowLayout`](https://doc.qt.io/qt-5/qml-qtquick-layouts-rowlayout.html) z modulu 111 | [`QtQuick.Layouts`](https://doc.qt.io/qt-5/qtquicklayouts-index.html). Oproti 112 | jednoduchým komponentám [`Row`](https://doc.qt.io/qt-5/qml-qtquick-row.html) a [`Column`](https://doc.qt.io/qt-5/qml-qtquick-column.html) umí tyto komponenty roztahovat 113 | vložené prvky tak, aby vyplnily okno, zarovnávat je a dělat s nimi i další 114 | pokročilé pozicování. Tyto možnosti se nastavují ve vnořených komponentách 115 | pomocí atributu [`Layout.`](https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html) a vždy se vztahují k nejbližší nadřazené 116 | komponentě typu `Layout`. Speciálně pozor u `RowLayout`u, kde atributy 117 | [`Layout.fillWidth`](https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html#fillWidth-attached-prop) a [`Layout.alignment`](https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html#alignment-attached-prop) rovnou v `RowLayout`u se vztahují k 118 | umístění komponenty `RowLayout` v komponentě `ColumnLayout`, nikoli k prvkům 119 | komponenty `RowLayout`. Lidsky řečeno říkají, že se řádková komponenta má 120 | zarovnat dolů a roztáhnout přes celou šířku komponenty `ColumnLayout`. Atribut 121 | [`fillWidth`](https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html#fillWidth-attached-prop) / [`fillHeight`](https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html#fillHeight-attached-prop) říká, že má daná komponenta vyplnit celý 122 | zbývající prostor, pomocí atributu `alignment` lze určit, ke které straně / 123 | stranám se má zarovnávat. 124 | 125 | K zobrazení jednotlivých úkolů slouží komponenta [`CheckBox`](https://doc.qt.io/qt-5/qml-qtquick-controls2-checkbox.html), která dělá 126 | zatrhávací políčko s popiskem. Aby bylo možné políčko zaškrtnout, je potřeba 127 | povolit atribut [`checkable`](https://doc.qt.io/qt-5/qml-qtquick-controls2-abstractbutton.html#checkable-prop), ke stavu zaškrtnutí je pak možné přistupovat 128 | pomocí atributu [`checkState`](https://doc.qt.io/qt-5/qml-qtquick-controls2-checkbox.html#checkState-prop), v tomto programu toho ale nevyužíváme. Místo toho 129 | hlídáme signál [`clicked`](https://doc.qt.io/qt-5/qml-qtquick-controls2-abstractbutton.html#clicked-signal) a při jeho vyslání necháme daný úkol smazat. 130 | 131 | Aby byl [`TextInput`](https://doc.qt.io/qt-5/qml-qtquick-textinput.html) sloužící k zadávání textu nového úkolu výraznější, je 132 | obalen komponentou [`Rectangle`](https://doc.qt.io/qt-5/qml-qtquick-rectangle.html), která mu zařizuje orámování. Tato komponenta 133 | získá svou šířku z `Layout`u, ale výšku nemá danou, takže je potřeba nějakou 134 | zvolit. Protože hned vedle jsou umístěna tlačítka, byla zvolena výška 135 | orámovávacího obdélníka jako výška jednoho z tlačítek, aby to vizuálně 136 | navazovalo. U samotné vložené komponenty TextInput je potřeba nastavit výšku 137 | podle obklopujícího obdélníka a zmenšit ji o rozměry rámečku. Také se hodí 138 | samotný text vertikálně vystředit, aby navazoval na text na tlačítkách vedle. 139 | 140 | [`TextInput`](https://doc.qt.io/qt-5/qml-qtquick-textinput.html) má nastaven atribut [`focus`](https://doc.qt.io/qt-5/qml-qtquick-item.html#focus-prop), aby bylo možné rovnou začít 141 | psát nové úkoly. Dále je využit signál [`accepted`](https://doc.qt.io/qt-5/qml-qtquick-textinput.html#accepted-signal), který je vyvolán při 142 | potvrzení enterem. Při jeho zpracování je využit blok, který umožňuje zadat více 143 | příkazů jako reakci na signál. V tomto případě je přidán nový úkol se zadaným 144 | textem a následně je textové pole smazáno, aby bylo možné psát další úkol. 145 | 146 | ## Zdroje 147 | - [QAbstractItemModel](https://doc.qt.io/qtforpython/PySide6/QtCore/QAbstractItemModel.html) 148 | - [QSortFilterProxyModel](https://doc.qt.io/qt-6/qsortfilterproxymodel.html) 149 | - [Qt Quick Layouts Overview](https://doc.qt.io/qt-5/qtquicklayouts-overview.html) 150 | -------------------------------------------------------------------------------- /06_todo_list/todo_list/simple_view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | 4 | Column { 5 | width: 500 6 | height: 500 7 | 8 | Component { 9 | id: taskDelegate 10 | Item { 11 | width: parent.width 12 | height: childrenRect.height 13 | 14 | Text {text: display} 15 | MouseArea { 16 | anchors.fill: parent 17 | onClicked: taskListView.currentIndex = index 18 | } 19 | } 20 | } 21 | 22 | ListView { 23 | id: taskListView 24 | width: parent.width 25 | height: 300 26 | model: taskListModel 27 | delegate: taskDelegate 28 | highlight: Rectangle { 29 | color: "red" 30 | } 31 | } 32 | 33 | TextInput { 34 | id: newTaskInput 35 | text: "Zadej další úkol" 36 | } 37 | Button { 38 | text: "Přidej úkol" 39 | // Send signal with one argument - text written in newTaskInput 40 | onClicked: taskListModel.addTask(newTaskInput.text) 41 | } 42 | Button { 43 | text: "Odeber zvolený úkol" 44 | // Send signal with one argument - index of the currently selected item 45 | onClicked: taskListModel.deleteTask(taskListView.currentIndex) 46 | } 47 | } -------------------------------------------------------------------------------- /06_todo_list/todo_list/todo_list.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QObject, Signal, Slot, Property, QUrl, QAbstractListModel 2 | from PySide6.QtGui import QGuiApplication 3 | from PySide6.QtQuick import QQuickView 4 | from PySide6 import QtCore 5 | import typing 6 | import sys 7 | 8 | #VIEW_URL = "simple_view.qml" # Simple user interface 9 | VIEW_URL = "view.qml" # Advanced user interface 10 | TASK_LIST_FILE = "ukoly.txt" 11 | 12 | 13 | class TaskListModel(QAbstractListModel): 14 | """ Class for maintaining list of tasks""" 15 | 16 | def __init__(self,filename=None): 17 | """Initialize and load list from given file""" 18 | QAbstractListModel.__init__(self) 19 | self.task_list = [] 20 | if filename: 21 | self.load_from_file(filename) 22 | 23 | def load_from_file(self,filename): 24 | """Load list of tasks from given file""" 25 | with open(filename,encoding="utf-8") as f: 26 | self.task_list =[l.strip() for l in f.readlines()] 27 | 28 | def rowCount(self, parent:QtCore.QModelIndex=...) -> int: 29 | """ Return number of cities in the list""" 30 | return len(self.task_list) 31 | 32 | def data(self, index:QtCore.QModelIndex, role:int=...) -> typing.Any: 33 | """ For given index and DisplayRole return the corresponding task""" 34 | if not index.isValid(): 35 | return None 36 | if role == QtCore.Qt.DisplayRole: 37 | return self.task_list[index.row()] 38 | 39 | # Slot will take one string argument 40 | @Slot(str) 41 | # Number (and types) of the arguments in the @Slot decorator and in the function header must match (excluding `self`) 42 | def addTask(self,task: str) -> None: 43 | """ Add task to the end of the tasks list 44 | 45 | Arguments: 46 | - task - task to be added 47 | """ 48 | # Notify before change 49 | self.beginInsertRows(self.index(0).parent(), self.rowCount(), self.rowCount()) 50 | # Change the model 51 | self.task_list.append(task) 52 | # Notify that the change is complete 53 | self.endInsertRows() 54 | 55 | @Slot(int) 56 | def deleteTask(self,idx: int) -> None: 57 | """ Delete task with given index from the list""" 58 | # Notify before change 59 | self.beginRemoveRows(self.index(0).parent(), idx, idx) 60 | # Change the model 61 | self.task_list.pop(idx) 62 | # Notify that the change is complete 63 | self.endRemoveRows() 64 | 65 | @Slot() 66 | def clearTasks(self) -> None: 67 | """ Clear all tasks from the list""" 68 | self.beginRemoveRows(self.index(0).parent(), 0, self.rowCount()-1) 69 | self.task_list = [] 70 | self.endRemoveRows() 71 | 72 | app = QGuiApplication(sys.argv) 73 | view = QQuickView() 74 | url = QUrl(VIEW_URL) 75 | tasklist_model = TaskListModel(TASK_LIST_FILE) 76 | ctxt = view.rootContext() 77 | ctxt.setContextProperty('taskListModel',tasklist_model) 78 | view.setSource(url) 79 | view.show() 80 | app.exec() 81 | -------------------------------------------------------------------------------- /06_todo_list/todo_list/ukoly.txt: -------------------------------------------------------------------------------- 1 | sepsat šestou lekci tutoriálu 2 | dopsat text k osmé lekci tutoriálu -------------------------------------------------------------------------------- /06_todo_list/todo_list/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | import QtQuick.Layouts 1.15 4 | 5 | // Use ColumnLayout for resizable components 6 | ColumnLayout { 7 | implicitWidth: 500 // width on the start of the program 8 | implicitHeight: 500 // height on the start of the program 9 | anchors.fill: parent 10 | 11 | Component { 12 | id: taskDelegate 13 | Item { 14 | width: parent.width // width as parent 15 | height: childrenRect.height // height as CheckBox 16 | CheckBox { 17 | text: model.display // only `display` clashes with CheckBox property 18 | checkable: true // users can check 19 | onClicked: taskListModel.deleteTask(model.index) 20 | } 21 | } 22 | } 23 | 24 | 25 | ListView { 26 | Layout.fillWidth: true // fill all available horizontal space in the window 27 | Layout.fillHeight: true // fill all available vertical space in the window 28 | id: taskListView 29 | model: taskListModel 30 | delegate: taskDelegate 31 | } 32 | 33 | // Use RowLayout for resizable components 34 | RowLayout { 35 | Layout.fillWidth: true // this is property of the parent ColumnLayout 36 | Layout.alignment: Qt.AlignBottom // this too, align the RowLayout to the bottom of the window 37 | 38 | // Border around TextInput for better visibility 39 | Rectangle { 40 | id: inputRect 41 | Layout.fillWidth: true 42 | Layout.alignment: Qt.AlignVCenter 43 | height: addTaskButton.height // height is not implicitly inherited (width is), so use height from button 44 | border.width: 1 45 | border.color: "black" 46 | radius: 5 47 | 48 | TextInput { 49 | id: newTaskInput 50 | focus: true // Take focus on start 51 | width: parent.width - 2 // Decrease the size because of the border 52 | height: parent.height - 2 53 | verticalAlignment: TextInput.AlignVCenter // Align text vertically to the center of the component 54 | text: "Zadej další úkol" 55 | onAccepted: { // When Enter is pressed 56 | taskListModel.addTask(newTaskInput.text); // Add task to the list 57 | newTaskInput.text = "" // Clear the input text 58 | } 59 | } 60 | } 61 | Button { 62 | id: addTaskButton 63 | text: "Přidej úkol" 64 | onClicked: taskListModel.addTask(newTaskInput.text) 65 | } 66 | Button { 67 | text: "Odeber všechny úkoly" 68 | onClicked: taskListModel.clearTasks() 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /07_countdown/README.md: -------------------------------------------------------------------------------- 1 | # Odpočet 2 | 3 | V tomto díle vytvoříme jednoduchou aplikaci na odpočet. 4 | 5 | ## Smyčka událostí a čekání 6 | V aplikaci někdy potřebujeme nějaký čas počkat, nebo nějakou činnost provádět jednou za nějaký čas. Při skriptování v Pythonu se pro čekání obvykle využívá funkce [`time.sleep()`](https://docs.python.org/3/library/time.html#time.sleep), která počká `` sekund a potom skončí. V principu bychom mohli použít tuto funkci i v grafickém rozhraní, ale mělo by to několik nevýhod. Pokud si vzpomeneme na [první díl](../01_first_program/README.md), je zde popsáno, jak funguje smyčka událostí - když přijde nějaká událost, je odbavena (ať už Pythonem nebo Qt) a *poté* se čeká na další událost. Co přesně znamená odbavit událost v Pythonu? Znamená to, že funkce, která byla zavolána, aby událost odbavila, musí skončit. Teprve poté může být odbavena další událost. Pokud v rámci odbavování události potřebujeme počkat a použili bychom [`time.sleep()`](https://docs.python.org/3/library/time.html#time.sleep), pak by odbavování události skončilo až po tomto čekání a dokončení následných činností. Protože ale reakce na tlačítka nebo zvětšení či zmenšení okna jsou také události, muselo by jejich odbavení počkat, až skončí událost s čekáním a tudíž by aplikace "zamrzla" a nereagovala na žádné podněty od uživatele. 7 | 8 | ## Paralelismus 9 | Možná vás napadlo, proč nejsou jednotlivé události obsluhovány paralelně, čímž by se předešlo podobným problémům. Byla by to samozřejmě možnost, ale paralelní zpracování přináší kromě zřejmých výhod i mnoho ne až tak zřejmých problémů. Nejčastějším problémem je [*souběh*](https://cs.wikipedia.org/wiki/Soub%C4%9Bh) (anglicky *race condition*) - pokud více obslužných funkcí manipuluje s těmiže daty, při současném běhu těchto obslužných funkcí by si funkce tato společná data přepisovaly "pod rukama", což by mohlo vést k nekonzistentnímu stavu. Psát takový kód, aby k problematickým situacím při současném běhu nedocházelo, není jednoduché a vede často k špatně odhalitelným chybám, proto je vhodné se takovéto "neřízené" paralelizaci vyhnout. 10 | 11 | Pokud chceme v našem programu provádět časově náročné operace, je možné si vytvořit [*vlákno*](https://doc.qt.io/qt-5/thread-basics.html), které obvykle sdílí jen minimum informací se zbytkem programu a komunikuje se zbytkem programu pomocí postupů, které zajišťují správnou synchronizaci. K vláknům se možná vrátíme v jednom z budoucích dílů, zatím se bez nich obejdeme. 12 | 13 | ## Časovač 14 | Abychom zachovali naši aplikaci responzivní (odpovídající na činnosti uživatele) i když chceme čekat, budeme potřebovat prostředek, který nám vyvolá událost za stanovený čas nebo ve stanoveném intervalu. Takovýto prostředek v Qt je [`QTimer`](https://doc.qt.io/qtforpython/PySide2/QtCore/QTimer.html). `QTimer` je běžný objekt, který můžeme vytvořit (i vícekrát) a nastavit, aby jednorázově za nějaký čas, nebo opakovaně v určitém intervalu vyslal signál [`timeout`](https://doc.qt.io/qt-5/qtimer.html#timeout). Pokud připojíme tento signál do nějakého vytvořeného slotu a časovač spustíme, je jednorázově nebo opakovaně vyvolávána naše funkce. Aplikace přitom zůstává responzivní, protože funkce, která spustí časovač okamžitě vrátí a časovač pak běží v pozadí, zatímco ve smyčce událostí jsou odbavovány další nastalé události. Nepotřebujeme tedy řešit paralelismus (i když může být někde v pozadí použit) a pomocí jednoduchého rozhraní získáváme možnost spouštět činnosti opožděně nebo opakovaně ve stanoveném intervalu. 15 | 16 | Při použití `QTimer`u prvně vytvoříme časovač, připojíme signál [`timeout`](https://doc.qt.io/qt-5/qtimer.html#timeout) a pak buď nastavíme interval v milisekundách pomocí [`setInterval()`](https://doc.qt.io/qtforpython/PySide2/QtCore/QTimer.html#PySide2.QtCore.PySide2.QtCore.QTimer.setInterval) a spustíme pomocí [`start()`](https://doc.qt.io/qtforpython/PySide2/QtCore/QTimer.html#PySide2.QtCore.PySide2.QtCore.QTimer.start) nebo uvedeme interval přímo jako parametr funkce [`start()`](https://doc.qt.io/qtforpython/PySide2/QtCore/QTimer.html#id3). V obou případech se časovač spustí a po každém intervalu vyšle signál, pracuje tedy opakovaně. Pokud chceme časovač pouze jednorázový, musíme nastavit [`setSingleShot(False)`](https://doc.qt.io/qtforpython/PySide2/QtCore/QTimer.html#PySide2.QtCore.PySide2.QtCore.QTimer.setSingleShot) nebo časovač zaráz vytvořit a spustit pomocí statické metody (metody třídy) [`singleShot`](https://doc.qt.io/qtforpython/PySide2/QtCore/QTimer.html#id2). 17 | 18 | ## Popis programu 19 | Program má jednoduché rozhraní s textovým polem a třemi tlačítky - *Start*, *Pause* a *Stop*. V textovém poli je zobrazen zbývající čas. Po stisknutí tlačítka *Start* začne běžět odpočet. Odpočet lze pozastavit stiskem tlačítka *Pause*, po stisknutí tlačítka *Start* program pokračuje v odpočtu. Stiskem tlačítka *Stop* se odpočet zastaví a vrátí se na poslední hodnotu zadanou když časovač neběžel. Po doběhnutí času se zobrazí přes okno dialog s textem Time out! a po jeho odkliknutí se časovač vrátí na poslední hodnotu zadanou když časovač neběžel. 20 | 21 | Modelem aplikace je třída `CountdownModel`, která drží hodnotu časovače v [property](https://doc.qt.io/qtforpython/PySide2/QtCore/Property.html) `remaining`, poslední nastavenou hodnotu v atributu `total` a samotný časovač v atributu `timer`. Dále pak obsahuje sloty pro obsluhu tlačítek a časovače. 22 | 23 | Protože pro nastavování výchozí hodnoty odpočtu i zobrazování zbývajícího času slouží jedno textové pole a chceme mít možnost po zastavení odpočtu znovu nastavit výchozí hodnotu, je potřeba v rámci setteru mezi těmito situacemi rozlišit. Připomeňme, že abychom nemuseli mít tlačítko "Nastavit", musíme mít textové pole propojené obousměrnou vazbou s odpovídající property, aby to, co uživatel napíše se ihned přeneslo do modelu. Bohužel se ale stejným způsobem přenese i změna při odpočtu, musíme tedy mezi těmito situacemi rozlišit, což se zde děje pomocí detekce, jestli časovač běží. Pokud ano, výchozí hodnota v `total` se neaktualizuje, pokud neběží, je výchozí hodnota v `total` aktualizována. Odpovídá to chování, co byste od odpočtu očekávali, obdobně se chová například odpočet na mobilu. 24 | 25 | Zkuste si rozmyslet, zda vám tento způsob ovládání časovače přijde intuitivní nebo jestli byste očekávali jiné chování. Zkuste si své případné změny implementovat a ověřte, jestli se vámi upravený časovač ovládá pohodlněji. 26 | 27 | ## Popis grafického rozhraní 28 | Rozhraní je tvořeno textovým polem a třemi tlačítky v jednom sloupci. [`TextInput`](https://doc.qt.io/qt-5/qml-qtquick-textinput.html) obdobně jako v [převodu souřadnic](../03_dms_converter/) má pomocí [`Binding`](https://doc.qt.io/qt-5/qml-qtqml-binding.html) nastavenou oboustrannou vazbu s property `remaining` modelu. Tlačítka *Start*, *Pause* a *Stop* jsou připojena na sloty modelu. 29 | 30 | Pro zobrazení informace, že časový limit vypršel, používáme komponentu [`Popup`](https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html). Tato komponenta obvykle slouží ke zobrazení nějakého dialogového okna (např. *Soubor tohoto jména již existuje. Přejete si jej přepsat?*), ale dá se také využít pro snadné překrytí původního obsahu okna. Popup má nastavenou velikost podle sloupce, ve kterém jsou tlačítka a textové pole a je na tento sloupec také vystředěn, takže po zobrazení ho akorát překryje. Vlastností [`visible`](https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#visible-prop) nastavujeme, že nemá být po spuštění viditelný, do vlastnosti [`contentItem`](https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#contentItem-prop) pak vložíme samotný obsah popupu. Tím je červený obdélník pro přilákání pozornosti a text *Time out!*. Také je zde nastavena reakce na kliknutí myši a tou je zavření popupu, abychom mohli znovu spustit časovač. Vlastnost [`focus`](https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#focus-prop) nám říká, že popup má dostávat události od klávesnice a vlastnot [`modal`](https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#modal-prop) říká, zda se jedná o tzv. [*modální dialog*](https://cs.wikipedia.org/wiki/Mod%C3%A1ln%C3%AD_okno). 31 | 32 | Pokud aplikace otevře další okno / dialog, mohou nastat dvě situace. Buď vyžadujeme, aby uživatel musel vyřešit situaci, o které se dozvěděl v nově otevřeném okně (např. zvolit soubor k otevření, potvrdit přepsání souboru, ...) a do jejího vyřešení nemohl interagovat se zbytkem aplikace, pak takovéto okno nazýváme *modální*. Nebo jde o obyčejné okno nebo například okno s nastavováním vlastností aktuálně zvoleného objektu, pak obvykle chceme, aby uživatel mohl dále interagovat s dalšími okny aplikace a prvky v nich, a takovéto okno nazýváme *nemodální*. 33 | 34 | Poslední komponentou je komponenta [`Connections`](https://doc.qt.io/qt-5/qml-qtqml-connections.html), pomocí které můžeme definovat akci, která se vykoná při určitém signálu. V našem případě chceme při timeoutu časovače nastavit dialog *Time out!* jako viditelný, aby si uživatel všiml, že časovač doběhl. Všimněme si, že zde nesvazujeme žádné property, ale pouze říkáme "Když doběhne časovač, nastav popup jako viditelný". Když uživatel popup zavře, žádná další akce se nevykoná, ani to z modelu nijak nepoznáme. 35 | 36 | ## Zdroje 37 | - [Dialogové okno](https://cs.wikipedia.org/wiki/Dialogov%C3%A9_okno) 38 | - [Popup](https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html) 39 | - [QTimer](https://doc.qt.io/qt-5/qtimer.html) 40 | - [Qt event loop, networking and I/O API](https://www.qtdeveloperdays.com/2013/sites/default/files/presentation_pdf/Qt_Event_Loop.pdf) - pokrývá širší rozsah, více orientované na C++ 41 | -------------------------------------------------------------------------------- /07_countdown/countdown/countdown.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtCore import QObject, Slot, Property, QUrl, Signal, QTimer 2 | from PySide2.QtGui import QGuiApplication 3 | from PySide2.QtQuick import QQuickView 4 | import sys 5 | 6 | VIEW_URL = "view.qml" 7 | 8 | 9 | class CountdownModel(QObject): 10 | """CountdownModel is the model class for the GUI. It holds the counter property 11 | and handles event generated by the click on the button.""" 12 | 13 | def __init__(self): 14 | QObject.__init__(self) 15 | # Value to count from 16 | self.total = 30 17 | self._remaining = 30 18 | # Timer 19 | self.timer = QTimer() 20 | self.timer.setInterval(1000) 21 | self.timer.timeout.connect(self.process_timer) 22 | 23 | def set_remaining(self, val): 24 | if val != self._remaining: 25 | self._remaining = val 26 | self.remaining_changed.emit() 27 | # If the timer is inactive, update also a value to count from 28 | if not self.timer.isActive(): 29 | self.total = self.remaining 30 | 31 | remaining_changed = Signal() 32 | # Property holding actual remaining number of seconds 33 | remaining = Property(int, lambda self: self._remaining, set_remaining, notify=remaining_changed) 34 | 35 | timeout = Signal() 36 | 37 | @Slot() 38 | def process_timer(self): 39 | """Handler for the timer event. 40 | Decrease the remaining value or stop the timer and emit timeout signal if the time is over""" 41 | if self.remaining == 1: 42 | self.timer.stop() 43 | self.remaining = self.total # Reset the timer value 44 | self.timeout.emit() 45 | return 46 | self.remaining -= 1 47 | 48 | @Slot() 49 | def start(self): 50 | """Start the countdown""" 51 | print("Starting") 52 | print(self.total,self.remaining) 53 | self.timer.start() 54 | 55 | @Slot() 56 | def pause(self): 57 | """Pause the countdown""" 58 | print("Pausing") 59 | print(self.total,self.remaining) 60 | self.timer.stop() 61 | 62 | @Slot() 63 | def stop(self): 64 | """Stop (and reset) the countdown""" 65 | print("Stopping") 66 | print(self.total,self.remaining) 67 | self.timer.stop() 68 | self.remaining = self.total 69 | 70 | 71 | app = QGuiApplication(sys.argv) 72 | view = QQuickView() 73 | url = QUrl(VIEW_URL) 74 | countdown_model = CountdownModel() 75 | 76 | ctxt = view.rootContext() 77 | ctxt.setContextProperty("countdownModel", countdown_model) 78 | 79 | view.setSource(url) 80 | view.show() 81 | app.exec_() 82 | -------------------------------------------------------------------------------- /07_countdown/countdown/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtQuick.Controls 2.14 3 | 4 | 5 | Column { 6 | id: column 7 | TextInput { 8 | id: remainingInput 9 | text: countdownModel.remaining 10 | width: parent.width 11 | // Align text to the horizontal center of the column 12 | horizontalAlignment: TextInput.AlignHCenter 13 | 14 | // Update a property if the number is changed in the TextInput 15 | Binding { 16 | target: countdownModel 17 | property: "remaining" 18 | value: remainingInput.text 19 | } 20 | } 21 | Button { 22 | text: 'Start' 23 | onClicked: countdownModel.start() 24 | } 25 | Button { 26 | text: 'Pause' 27 | onClicked: countdownModel.pause() 28 | } 29 | Button { 30 | text: 'Stop' 31 | onClicked: countdownModel.stop() 32 | } 33 | 34 | Popup{ 35 | id: timeoutPopup 36 | // Exactly overlay the parent window 37 | anchors.centerIn: column.Center 38 | width: column.width 39 | height: column.height 40 | 41 | visible: false 42 | contentItem: Rectangle { 43 | 44 | color: "red" 45 | Text{ 46 | anchors.centerIn: parent 47 | text:"Time out!" 48 | } 49 | MouseArea{ 50 | anchors.fill: parent 51 | onClicked: { 52 | // Close the popup if there was clicked anywhere on it 53 | timeoutPopup.close() 54 | } 55 | } 56 | } 57 | 58 | // Popup is modal and wants focus 59 | modal: true 60 | focus: true 61 | } 62 | 63 | // Show the popup on timeout signal emitted 64 | Connections { 65 | target: countdownModel 66 | onTimeout: {timeoutPopup.visible = true } 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /08_vehicle_positions/vehicle_positions/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /08_vehicle_positions/vehicle_positions/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /08_vehicle_positions/vehicle_positions/positions.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtCore import QObject, Slot, Property, QUrl, Signal, QTimer, QAbstractListModel, QByteArray 2 | from PySide2.QtGui import QGuiApplication 3 | from PySide2.QtQuick import QQuickView 4 | from PySide2.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest 5 | from PySide2.QtPositioning import QGeoCoordinate 6 | from PySide2 import QtCore 7 | from dataclasses import dataclass 8 | import sys 9 | from enum import Enum 10 | import json 11 | import typing 12 | 13 | VIEW_URL = "view.qml" 14 | POSITIONS_URL = "https://mapa.idsjmk.cz/api/vehicles.json" 15 | UPDATE_INTERVAL = 5000 # ms 16 | 17 | @dataclass 18 | class Vehicle(object): 19 | pos : QGeoCoordinate 20 | id : int 21 | 22 | class VehiclesModel(QAbstractListModel): 23 | 24 | class Roles(Enum): 25 | LOCATION = QtCore.Qt.UserRole+0 26 | ID = QtCore.Qt.UserRole+1 27 | 28 | def __repr__(self): 29 | return "V({})".format(self.id) 30 | 31 | def __init__(self,na_manager: QNetworkAccessManager): 32 | QAbstractListModel.__init__(self) 33 | self.vehicle_list = [] 34 | self.na_manager = na_manager 35 | self.na_manager.finished[QNetworkReply].connect(self.download_finished) 36 | self.timer = QTimer() 37 | self.timer.setInterval(UPDATE_INTERVAL) 38 | self.timer.timeout.connect(self.start_download) 39 | self.timer.start() 40 | 41 | def rowCount(self, parent:QtCore.QModelIndex=...) -> int: 42 | return len(self.vehicle_list) 43 | 44 | def data(self, index:QtCore.QModelIndex, role:int=...) -> typing.Any: 45 | print(index.row(),role) 46 | if role == QtCore.Qt.DisplayRole: 47 | return self.vehicle_list[index.row()].id 48 | 49 | if role == self.Roles.ID.value: 50 | return self.vehicle_list[index.row()].id 51 | 52 | if role == self.Roles.LOCATION.value: 53 | print("Position:",self.vehicle_list[index.row()].pos) 54 | return self.vehicle_list[index.row()].pos 55 | 56 | def roleNames(self) -> typing.Dict: 57 | """Returns dict with role numbers and role names for default and custom roles together""" 58 | # Append custom roles to the default roles and give them names for a usage in the QML 59 | roles = super().roleNames() 60 | roles[self.Roles.LOCATION.value] = QByteArray(b'location') 61 | roles[self.Roles.ID.value] = QByteArray(b'id') 62 | print(roles) 63 | return roles 64 | 65 | @Slot() 66 | def start_download(self): 67 | print("Downlad started") 68 | self.na_manager.get(QNetworkRequest(QUrl(POSITIONS_URL))) 69 | 70 | @Slot() 71 | def download_finished(self,reply:QNetworkReply): 72 | print("Download finished") 73 | reply_str = reply.readAll().data() 74 | print(type(reply_str)) 75 | reply_str = reply_str.decode('utf-8-sig') 76 | data = json.loads(reply_str) 77 | print(data) 78 | self.update_data(data) 79 | 80 | def update_data(self,data): 81 | vehicles = data['Vehicles'] 82 | self.beginRemoveRows(self.index(0).parent(),0,len(self.vehicle_list)-1) 83 | self.vehicle_list = [] 84 | self.endRemoveRows() 85 | self.beginInsertRows(self.index(0).parent(),0,len(vehicles)-1) 86 | for v in vehicles: 87 | pos = QGeoCoordinate(float(v['Lat']),float(v['Lng'])) 88 | aid = v['ID'] 89 | self.vehicle_list.append(Vehicle(pos,aid)) 90 | self.endInsertRows() 91 | print(self.vehicle_list) 92 | 93 | 94 | 95 | 96 | 97 | app = QGuiApplication(sys.argv) 98 | view = QQuickView() 99 | url = QUrl(VIEW_URL) 100 | na_manager = QNetworkAccessManager() 101 | vehicles_model = VehiclesModel(na_manager) 102 | 103 | ctxt = view.rootContext() 104 | ctxt.setContextProperty("vehiclesModel", vehicles_model) 105 | 106 | view.setSource(url) 107 | view.show() 108 | app.exec_() 109 | -------------------------------------------------------------------------------- /08_vehicle_positions/vehicle_positions/view.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.14 2 | import QtLocation 5.14 3 | import QtPositioning 5.14 4 | 5 | Rectangle{ 6 | height: 500 7 | Plugin { 8 | id: mapPlugin 9 | name: "osm" // We want OpenStreetMap map provider 10 | PluginParameter { 11 | name:"osm.mapping.custom.host" 12 | value:"https://tiles.wmflabs.org/osm-no-labels/" // We want custom tile server for tiles without labels 13 | } 14 | } 15 | 16 | Map { 17 | width: 500 18 | height: parent.height 19 | 20 | plugin: mapPlugin 21 | activeMapType: supportedMapTypes[supportedMapTypes.length - 1] // Use our custom tile server 22 | 23 | center: QtPositioning.coordinate(49.19471,16.60911) // Center to the selected city 24 | zoomLevel: 14 25 | 26 | MapItemView { 27 | model: vehiclesModel 28 | delegate: MapQuickItem { 29 | coordinate: model.location 30 | sourceItem: Text{ 31 | text: model.display 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 xtompok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PySide2 QML tutorial 2 | Tutorial for using Python with Qt, specially with QtQuick 2 and PySide2/PySide6 libraries. 3 | 4 | ## About (EN) 5 | The text is in czech due to primarily targeting Czech students. 6 | Source code and comments is in english. Feel free to ask me if you can't 7 | understand any part. Translations, comments and contributions are welcome. 8 | 9 | ## O tomto návodu 10 | Tutoriál pro používání Qt Quick v Pythonu pomocí PySide2/PySide6, primárně zaměřen na 11 | studenty a zájemce o geografii. Pomocné texty jsou v češtině, zdrojové kódy a 12 | komentáře v nich jsou anglicky. Pokud by něco nebylo jasné, dejte mi vědět. 13 | Pokud byste chtěli pomocné texty přeložit do angličtiny nebo měli jiné náměty či 14 | připomínky, jsou vítány. Tento tutoriál vzniká převážně proto, že jsem nenašel 15 | jiný (anglický / český), který pokrývá používání Qt Quick a Pythonu více než 16 | povrchně. Pokud o nějakém takovém víte, dejte mi prosím vědět. 17 | 18 | Aby kód zbytečně nebobtnal, jsou nové věci komentovány vždy v tom díle, kde jsou 19 | představeny. V dalších dílech jsou již komentovány jen stručně, pokud vám není 20 | něco jasné, zkuste se podívat do předchozích dílů. 21 | 22 | Uživatelské rozhraní (QML soubor) se obvykle vyskytuje ve dvou variantách - 23 | `view.qml` obsahující jen nezbytně nutné prvky a `view_rich.qml` graficky 24 | propracovanější, ukazující možnosti přizpůsobení grafického rozhraní. Mezi 25 | rozhraními se přepíná ve zdrojovém kódu přepsáním `VIEW_PATH`. 26 | 27 | Pokud nějaký pojem nemá český ustálený ekvivalent, budu používat původní 28 | anglické názvy, text tedy může někdy vypadat poněkud krkolomně. 29 | 30 | ## Doporučený SW 31 | Pro Python doporučuji Visual Studio Code nebo jiný editor s doplňováním syntaxe, pro 32 | editaci QML pak Qt Creator (součástí balíku Qt). QML lze psát i ve Visual Studio 33 | Code, ale narozdíl od QtCreatoru nenabízí vizuální editor, kde se změny ihned 34 | projeví. Pro základní rozhraní není Qt Creator nutný. 35 | 36 | Příklady jsou testovány na Python 3.9. Qt je používáno ve verzi 6.2.3, tam kde 37 | není daná třída dostupná, používá se 5.15, ale pravděpodobně budou, alespoň 38 | úvodní příklady fungovat i se staršími verzemi. 39 | 40 | ## Spouštění příkladů 41 | Nejlépe ve složce příkladu vytvořit virtualenv s PySide2. Příklady budou mít v 42 | budoucnu `requirements.txt`. 43 | 44 | ## Díly (budou postupně přibývat) 45 | 0. [Instalace a nastavení](00_preparations) 46 | 1. [První program](01_first_program) 47 | 2. [Klikni na tlačítko](02_clicker) 48 | - binding proměnných 49 | - reakce na stisk tlačítka 50 | 3. [Převod DMS na stupně a zpět](03_dms_converter) 51 | - koncept model a view 52 | - obousměrná synchronizace mezi modelem a GUI 53 | 4. [Seznam měst](04_city_list) 54 | - model, view, delegate 55 | - abstraktní třídy 56 | - fokus - úvod 57 | 5. [Mapa měst](05_city_map) 58 | - Map View 59 | - property v QML 60 | 6. [TODO list](06_todo_list) 61 | - přidávání / ubírání prvků modelu za běhu 62 | 7. [Odpočet](07_countdown) 63 | - paralelismus 64 | - časovač 65 | - popup 66 | 8. [Mapa vozidel MHD](08_vehicle_positions) 67 | - stahování dat z internetu 68 | - *doprovodný text zatím chybí* 69 | 70 | 71 | ## Zdroje 72 | - [Dokumentace ke Qt](https://doc.qt.io/) 73 | - [Qt for Python](https://doc.qt.io/qtforpython/index.html#) 74 | - Seriál [Grafické uživatelské rozhraní v Pythonu](https://www.root.cz/serialy/graficke-uzivatelske-rozhrani-v-pythonu/), poslední 3 díly 75 | - [Seznam QML typů](https://doc.qt.io/qt-5/qmltypes.html) 76 | - [Seznam modulů v PySide2](https://doc.qt.io/qtforpython/modules.html) 77 | 78 | --------------------------------------------------------------------------------