Způsobů, jak dělat v Pythonu aplikace s GUI, je mnoho. Dá se použít zabudovaný, ale ošklivý Tkinter, nebo nějaký externí framework.
V tomto cvičení budeme používat framework Qt, protože je multiplatformní, používá se i v jiných oblastech, než je Python, je dostatečně robustní a dá se na většinu systémů nainstalovat bez větších problémů.
Pomocí aplikace Qt Designer se dá navíc základní kostra GUI poměrně jednoduše naklikat, takže není nutné psát layout aplikace v kódu.
Na tomto cvičení budete potřebovat balíček PyQt5 a aplikaci Qt5 Designer. Pokud budete používat svůj počítač, prosíme vás o instalaci již předem, na cvičení toho bude opravdu hodně a nemůžeme si dovolit plýtvat časem.
Pokud máte Python alespoň 3.5 a jednu z platforem, pro které je připraven wheel na PyPI, stačí udělat:
(__venv__) $ python -m pip install --upgrade pip
(__venv__) $ python -m pip install PyQt5
Pro starší verzi Pythonu nebo 32bitový Linux to ale nebude fungovat.
V takovém případě můžete PyQt5 zkusit najít v balíčkovacím systému vaší distribuce (např. balíček python3-qt5
ve Fedoře nebo python3-pyqt5
v Debianu).
Virtualenv pak může vytvořit s přepínačem --system-site-packages
, který zajistí, že i z virtualenvu uvidíte PyQt5 nainstalované z distribučního balíčku.
$ python3 -m venv --system-site-packages __venv__
Pokud nic z toho nepomůže, můžete zkusit přeložit PyQt5 ze zdrojových souborů (návod).
První aplikace níže by vám měla fungovat.
Pokud narazíte na chybu Could not find or load the Qt platform plugin "xcb"
, podívejte se do naší issue.
Na Linuxu najdete Qt5 Designer v balíčkách, třeba qt5-designer
na Fedoře nebo qttools5-dev-tools
na Debianu.
Na Windows (i na Macu) si můžete stáhnout instalátor Qt 5, který (doufáme) nainstaluje i Designer.
Pokud používáte na Macu homebrew
, můžete to udělat i takto:
$ brew install qt5
$ brew linkapps qt5
Existují i Python wheely pyqt5-tools pro Windows obsahující Qt5 Designer. Ten je pak potřeba pro spuštění dohledat v nainstalované lokaci.
Do virtuálního prostředí s PyQt5 si nainstalujte i NumPy:
$ python -m pip install numpy
Napište si první aplikaci, ať vidíte, jak kód v PyQt vypadá. Detaily toho, jak to funguje, si ukážeme později.
from PyQt5 import QtWidgets
app = QtWidgets.QApplication([])
button = QtWidgets.QPushButton("Click to Exit")
button.setWindowTitle("Goodbye World")
button.clicked.connect(app.quit)
button.show()
app.exec()
Qt je aplikační framework napsaný v C++, který zjednodušuje psaní multiplatformních aplikací (od počítačů s Linuxem, Mac OS či Windows po různá vestavěná zařízení).
PyQt je knihovna, která umožňuje použít Qt z Pythonu. Na rozdíl od samotného Qt je licencovaná pod GNU GPL v3, která (stručně řečeno) vyžaduje, aby programy napsané s použitím PyQt byly šířeny pod stejnou licencí a se zdrojovým kódem. Tedy: kdokoliv, kdo dostane kopii programu, musí mít možnost dostat odpovídající zdrojový kód a má možnost tento kód dál šířit pod stejnou licencí.
Pokud by se vám tato licence nelíbila, je možnost použít PySide, které má permisivnější licenci a téměř stejné API jako PyQt, ale není tak stabilní.
Qt je rozděleno na několik tzv. modulů. Pro grafická uživatelská rozhraní (GUI), kterými se budeme zabývat, použijeme hlavně QtGui a QtWidgets.
Dále je tu modul QtCore, který obsahuje mj. základní datové typy jako QString a QList (které PyQt automaticky převádí na pythonní ekvivalenty a zpět) nebo třeba QRect – abstraktní obdélník.
Další moduly jsou nadstavby od vykreslování SVG nebo práci s multimédii (které se můžou hodit) po třeba práci s SQL a XML nebo síťovou komunikaci, kde je pro Python pohodlnější použít jiné knihovny.
Ačkoli se Qt dá použít z Pythonu, bohužel zjistíte, že ne všechno funguje a vypadá tak, jako kdyby to byla knihovna od základů napsaná pro Python. Tady jsou některé zvláštnosti, na které se můžete připravit.
Qt pojmenovává funkce, metody a atributy konvencí camelCase
, místo pythonistického snake_case
.
PyQt tuto konvenci nemění: je užitečnější používat identická jména, a kromě toho knihovna PyQt vznikla ještě před PEP 8.
Hledáte-li dokumentaci, doporučuji zadat do vyhledávače qt5 <hledaný objekt>
.
Dostanete se tak na dokumentaci pro C++ (např. QObject).
Hledáte-li pyqt5 <hledaný objekt>
, dostanete se k dokumentaci pro Python, která ale většinou jen odkazuje
na verzi pro C++ (např. pro QObject).
Rozdíly mezi C a pythonní verzí jsou většinou intuitivní (např. None místo NULL), ale jsou popsány v dokumentaci PyQt.
Qt zásadně používá pro přístup k atributům objektů funkce.
Funkce pro čtení se typicky jmenuje podle atributu, funkce pro nastavení má předponu set
.
Namísto pythonního c = obj.color
a obj.color = ...
tedy použijeme c = obj.color()
a obj.setColor(...)
.
Python a C++/Qt mají, bohužel, rozdílný přístup ke správě paměti. Python používá reference counting a garbage collection. C++ má objekty s destruktory, což Qt zjednodušuje (alespoň pro C++) stromem vlastnictví.
Základní třída v Qt, ze které dědí téměř všechny ostatní, je QObject.
Ten má seznam potomků (children), o které se „stará“, a když uvolníme rodiče, uvolní se rekurzivně i všichni potomci.
Z Pythonu pak můžeme dostat chybu wrapped C/C++ object has been deleted
.
Jinak ale kombinace QObject a pythonních objektů funguje dobře.
Větší problémy můžou nastat s pomocnými objekty, které nedědí z QObject a nemají potřebné „dynamické“ vlastnosti. Takový objekt doporučujeme používat jen v rámci jedné funkce (t.j. neukládat si ho jinde), pokud si nejste jistí že ho „nevlastníte“ i ve smyslu C++/Qt.
Občas se stane, že program spadne pro chybu jako nepovolený přístup do paměti. Bez hlubší znalosti Qt a PyQt se taková chyba odstraňuje poměrně těžko, ale vaše znalosti C++ (z jiných kurzů) a CPython C API (z minula) vám v tom pomůžou. Doporučujeme dělat malé commity a psát jednoduchý kód.
Qt funguje na principu smyčky událostí (event loop).
Metoda QApplication.exec
obsahuje v podstatě nekonečnou smyčku, která čeká na externí události (klik myši,
žádost OS o vykreslení okna atd.) a na jejich základě volá příslušné funkce – ať už interní
nebo námi definované.
Pro komunikaci mezi objekty v rámci aplikace pak Qt používá mechanismus signálů a slotů (variantu observer pattern). Signál je vyslán (emitted) při události jako kliknutí na tlačítko, výběr položky z menu, zavření okna atp. K signálu může být připojeno několik slotů, což jsou funkce, které se po vyslání signálu zavolají. Kód, který vysílá signál, obecně neví o tom, kolik slotů je připojeno (a jsou-li nějaké).
V C++ jsou signály a sloty vždy staticky nadefinované na nějaké třídě, která dědí z QObject
.
V PyQt takto musí být nadefinovány jen signály; za slot poslouží jakákoli pythonní funkce.
V příkladu výše jsme připojili signál clicked
tlačítka na slot quit
aplikace.
Stejně bychom mohli připojit jakoukoli funkci/metodu, která bere správný počet argumentů – v následujícím případě nula:
button.clicked.connect(lambda: print('Exiting program'))
V C++ je časté přetěžování funkcí (včetně signálů), což Pythonistům občas ztěžuje život. PyQt většinou automaticky vybere variantu signálu podle připojené funkce, ale ne vždy je to možné.
Ukažme si to na následujícím kódu, který napojuje funkci print
na dvě varianty signálu QComboBox.activated.
Ten se vyšle při výběru položky ze seznamu buď jako QComboBox.activated[int]
, kdy předává index vybrané položky,
nebo jako QComboBox.activated[str]
, kdy předává text položky:
from PyQt5 import QtWidgets
app = QtWidgets.QApplication([])
# QComboBox - políčko pro výběr z několika možností
box = QtWidgets.QComboBox()
box.addItem('First Option')
box.addItem('Second Option')
# Základní varianta napojí na activated[int]
box.activated.connect(print)
# Výběr varianty signálu pomocí hranatých závorek
box.activated[str].connect(print)
box.activated[int].connect(print)
box.show()
app.exec()
Základní způsob, jak v Qt vytvářet grafické rozhraní, je skládání funkčních prvků (widgets) do hierarchie oken, skupin a panelů.
Základní třída pro funkční prvky, QWidget, dědí z už zmíněného QObject
.
Každý QObject může obsahovat potomky (children), a v případě QWidget se potomci vykreslují
jako součást svého rodiče.
Navíc může mít každý widget tzv. layout, který určuje pozici a velikost widgetů, které jsou
do něj přidané.
Ukažme to v kódu:
from PyQt5 import QtWidgets
app = QtWidgets.QApplication([])
# Hlavní okno
main = QtWidgets.QWidget()
main.setWindowTitle('Hello Qt')
# Layout pro hlavní okno
layout = QtWidgets.QHBoxLayout()
main.setLayout(layout)
# Nápis
label = QtWidgets.QLabel('Click the button to change me')
# Přidáním do layoutu se nápis automaticky stane potomkem hlavního okna
layout.addWidget(label)
# Tlačítko
button = QtWidgets.QPushButton('Click me')
layout.addWidget(button)
# Funkcionalita
def change_label():
label.setText('Good job. +100 points.')
button.clicked.connect(change_label)
# Spuštění
main.show()
app.exec()
Zabudovaných layoutů i widgetů existuje spousta, jednodušší programy stačí „poskládat“ z nich a napojit je na logiku. Pro složitější programy jsou pak možnosti, jak si widgety přizpůsobit.
Na tomto cvičení si připravíme aplikaci pro editaci dlaždicových map (tile maps) – obrázků složených z omezené nabídky čtverečků („dlaždic“), známých mj. ze starých her.
Aplikace bude mít část s mapou, paletu pro výběr dlaždice ke kreslení a navíc menu a panel nástrojů pro akce jako ukládání a otevírání souborů:
Vytvářet GUI v kódu je poměrně neefektivní, a tak existuje nástroj, kde si okna můžeme „naklikat“.
Jmenuje se Qt Designer a měli byste ho mít nainstalovaný.
Na školních počítačích se spouští příkazem designer -qt=5
.
Spustíme Designer a vytvoříme v něm nové Main Window. Do něj si z palety přidáme Scroll Area a doprava vedle něj List Widget. Poté aplikujeme layout: na volnou plochu okna klikneme pravým tlačítkem a vybereme Lay Out ‣ Horizontally. (Dá se to udělat i tlačítkem v liště.)
Pomocí Ctrl+R lze zkontrolovat, jak okno vypadá a jak reaguje na změny velikosti.
Potom přidáme položku do menu: místo Type Here napíšeme Map a pod něj podobně přidáme položky New a Quit.
V panelu Property Editor jde měnit vlastnosti jednotlivých prvků. U skrolovacího okna nastavíme objectName na scrollArea. U ListWidget nastavíme objectName na palette a sizePolicy ‣ Horizontal na Preferred. V panelu ActionEditor najdeme položky pro New a Quit a nastavíme jim objectName na actionNew, resp. actionQuit.
Potom přes pravé tlačítko na nevyužité ploše okna přidáme lištu nástrojů (Add Toolbar) a z panelu Action Editor do něj akci actionQuit přetáhneme.
Pomocí Ctrl+R opět zkontrolujeme, jak okno vypadá a jak po nastavení sizePolicy reaguje na změny velikosti.
V Designeru jde i napojovat signály. V panelu Signal/Slot Editor přidáme tento řádek:
actionQuit
triggered()
MainWindow
close()
Pomocí Ctrl+R jde ověřit, že zavírání okna funguje.
Návrh okna uložíme do souboru mainwindow.ui
.
Soubor s návrhem jde převést na pythonní zdrojový soubor pomocí programu pyuic5
nebo
ho vždy načíst přímo z programu.
My použijeme druhou variantu, je však dobré o pyuic5
vědět, kdybyste někdy potřebovali
základ pro vytváření UI v kódu (např. na vytvoření sady několika podobných tlačítek v cyklu).
Načíst .ui
soubor z programu do předpřipraveného okna QMainWindow
lze pomocí funkce uic.loadUi:
from PyQt5 import QtWidgets, uic
def main():
app = QtWidgets.QApplication([])
window = QtWidgets.QMainWindow()
with open('mainwindow.ui') as f:
uic.loadUi(f, window)
window.show()
return app.exec()
main()
Qt neobsahuje předpřipravený widget na dlaždicové mapy. Musíme si tedy vyrobit vlastní.
Mapu budeme reprezentovat jako NumPy matici (viz lekce o NumPy). Zatím budeme používat dva druhy dlaždic: trávu (v matici reprezentovanou jako 0) a zeď (-1).
Velikost widgetu se zadává v pixelech. Musíme ho udělat dostatečně velký, aby se do něj vešla všechna políčka mapy. Velikost jednoho políčka v pixelech zvolíme pro jednoduchost konstantou.
Souřadnice v Qt jsou v pixelech ve formě (x, y)
– klasicky jak jsme zvyklí, x
je horizontální souřadnice –
kdežto matice je uložená po políčkách (řádek, sloupec)
.
Abychom se v tom neztratili, je dobré hned ze začátku udělat funkce pro převod mezi souřadnými systémy
a důsledně rozlišovat (x, y)
vs. (row, column)
.
CELL_SIZE = 32
def pixels_to_logical(x, y):
return y // CELL_SIZE, x // CELL_SIZE
def logical_to_pixels(row, column):
return column * CELL_SIZE, row * CELL_SIZE
class GridWidget(QtWidgets.QWidget):
def __init__(self, array):
super().__init__() # musíme zavolat konstruktor předka
self.array = array
# nastavíme velikost podle velikosti matice, jinak je náš widget příliš malý
size = logical_to_pixels(*array.shape)
self.setMinimumSize(*size)
self.setMaximumSize(*size)
self.resize(*size)
GridWidget
vložíme do QScrollArea
, kterou jsme si vytvořili v Qt Designeru:
import numpy
...
# mapa zatím nadefinovaná rovnou v kódu
array = numpy.zeros((15, 20), dtype=numpy.int8)
array[:, 5] = -1 # nějaká zeď
# získáme oblast s posuvníky z Qt Designeru
scroll_area = window.findChild(QtWidgets.QScrollArea, 'scrollArea')
# dáme do ní náš grid
grid = GridWidget(array)
scroll_area.setWidget(grid)
...
Po spuštění aplikace zatím nic nového neuvidíte, maximálně se trochu změní posuvníky. Potřebujeme ještě zařídit, aby se data z matice vykreslovala do gridu. Nejlepší je vykreslovat, kdykoliv nás OS (nebo Qt) vyzve, že potřebuje kus okna překreslit: při prvním zobrazení, odminimalizování okna, ukázání nové části mapy přes scrollování. Také je zbytečné vykreslovat obrázky mimo oblast, kterou je vidět na obrazovce.
K tomuto účelu nám poslouží událost (event).
Jak bylo řečeno v úvodu, na rozdíl od signálů a slotů, které zajišťují komunikaci v rámci aplikace,
události vznikají mimo aplikaci.
Jde například o kliknutí myší (mouse*Event), vstup z klávesnice (key*Event)
nebo žádost OS o překreslení okna (paintEvent).
Na poslední jmenovanou událost, paintEvent
, teď budeme reagovat.
Události se obsluhují předefinováním příslušné metody, která jako argument bere objekt popisující danou událost.
V rámci reakce na událost paintEvent
můžeme používat QPainter, objekt, který generalizuje kreslení
na různé „povrchy“ jako widgety, obrázky, nebo i instrukce pro tiskárnu.
from PyQt5 import QtWidgets, QtGui, QtCore, uic
class GridWidget(QtWidgets.QWidget):
...
def paintEvent(self, event):
rect = event.rect() # získáme informace o překreslované oblasti
# zjistíme, jakou oblast naší matice to představuje
# nesmíme se přitom dostat z matice ven
row_min, col_min = pixels_to_logical(rect.left(), rect.top())
row_min = max(row_min, 0)
col_min = max(col_min, 0)
row_max, col_max = pixels_to_logical(rect.right(), rect.bottom())
row_max = min(row_max + 1, self.array.shape[0])
col_max = min(col_max + 1, self.array.shape[1])
painter = QtGui.QPainter(self) # budeme kreslit
for row in range(row_min, row_max):
for column in range(col_min, col_max):
# získáme čtvereček, který budeme vybarvovat
x, y = logical_to_pixels(row, column)
rect = QtCore.QRectF(x, y, CELL_SIZE, CELL_SIZE)
# šedá pro zdi, zelená pro trávu
if self.array[row, column] < 0:
color = QtGui.QColor(115, 115, 115)
else:
color = QtGui.QColor(0, 255, 0)
# vyplníme čtvereček barvou
painter.fillRect(rect, QtGui.QBrush(color))
Nyní by již mapa měla být v okně vidět barevně.
Protože barvičky jsou příliš nudné, přidáme do mapového widgetu obrázky.
Veškerou, ke cvičení i k úkolu potřebnou, grafiku najdete na GitHubu. Je k dispozici pod public domain (tj. „dělej si s tím, co chceš“), pochází ze studia Kenney a je (společně se další volně licencovanou grafikou) ke stažení z OpenGameArt.org.
Zatím budeme potřebovat jen dva obrázky:
Nejprve si načteme SVG soubory jako objekty QSvgRenderer
:
from PyQt5 import QtWidgets, QtCore, QtGui, QtSvg, uic
SVG_GRASS = QtSvg.QSvgRenderer('grass.svg')
SVG_WALL = QtSvg.QSvgRenderer('wall.svg')
A poté je na správných místech vyrendrujeme:
...
rect = QtCore.QRectF(x, y, CELL_SIZE, CELL_SIZE)
# podkladová barva pod poloprůhledné obrázky
white = QtGui.QColor(255, 255, 255)
painter.fillRect(rect, QtGui.QBrush(white))
# trávu dáme všude, protože i zdi stojí na trávě
SVG_GRASS.render(painter, rect)
# zdi dáme jen tam, kam patří
if self.array[row, column] < 0:
SVG_WALL.render(painter, rect)
Nyní trochu odbočíme a povíme si krátce o dalším podsystému Qt: o modelech.
Qt obsahuje framework, který mapuje informace do podoby tabulek, seznamů nebo obecných stromů. Vzniklé modely se potom dají zobrazit ve specializovaných widgetech. Samotná data můžou být uložena kdekoli – v paměti, SQL databázi, souborech a podobně. Dokonce nemusí být všechna dostupná: existuje vestavěný model pro souborový systém, který se dá zobrazit aniž by se procházely všechny soubory. Když je informace potřeba, model se postará o její načtení. Pomocí modelů a modelových widgetů lze informace i měnit, a pokud je model zobrazen ve více widgetech zároveň, změny se projeví ve všech.
Obecné modely je bohužel relativně obtížné implementovat v Pythonu, protože používají třídy, které nedědí z QObject, takže je potřeba sledovat, jestli je „vlastní“ Python nebo C++. Naštěstí ale existují widgety se zabudovanými modely, které obsahují i samotná data. Tyto modely je složitější napojit na existující aplikační logiku, ale pro většinu účelů postačí.
O obecných modelech si můžete přečíst v dokumentaci.
Jeden z widgetů se zabudovaným modelem je QListWidget
, který umí spravovat a zobrazovat
nějaký seznam.
My jsme si v Qt Designeru připravili QListWidget
s názvem palette
, který použijeme
jako paletu jednotlivých dílků, které budeme moci vkládat do mapy.
Položky se do tohoto modelu přidávají následovně:
def main():
...
# získáme paletu vytvořenou v Qt Designeru
palette = window.findChild(QtWidgets.QListWidget, 'palette')
item = QtWidgets.QListWidgetItem('Grass') # vytvoříme položku
icon = QtGui.QIcon('grass.svg') # ikonu
item.setIcon(icon) # přiřadíme ikonu položce
palette.addItem(item) # přidáme položku do palety
Stejným způsobem lze do palety přidat další položky: kromě trávy budeme na toto cvičení potřebovat i stěnu. Protože v úkolu bude položek více, je lepší si na to vytvořit funkci či metodu. To necháme na vás.
Zatím jsme vytvořili paletu, ve které uživatel může položky vybírat.
Výběr položky aktivuje signál itemSelectionChanged
, na který můžeme
navázat volání funkce.
(Pokud bychom měli pod kontrolou třídu widgetu, jako tomu je u třídy Grid
,
mohli bychom místo toho i předefinovat metodu itemSelectionChanged()
.)
def main():
...
def item_activated():
"""Tato funkce se zavolá, když uživatel zvolí položku"""
# Položek může obecně být vybráno víc, ale v našem seznamu je to
# zakázáno (v Designeru selectionMode=SingleSelection).
# Projdeme "všechny vybrané položky", i když víme že bude max. jedna.
for item in palette.selectedItems():
row_num = palette.indexFromItem(item).row()
print(row_num)
palette.itemSelectionChanged.connect(item_activated)
Nyní, když uživatel zvolí položku, vypíše se do konzole její pořadí.
Nás by ale spíš zajímalo, jak bude tato položka reprezentována v matici s mapou.
K položce v paletě můžeme uložit informace pomocí item.setData(<role>, <data>)
.
Rolí pro informace je spousta a několik z nich Qt používá pro vykreslování.
Pro vlastní data můžeme použít QtCore.Qt.UserRole
.
V případě potřeby ukládat více dat můžeme dále zvolit QtCore.Qt.UserRole + 1
atd.
Pro případ, že budeme potřebovat rolí víc, je dobré si je vhodně pojmenovat.
VALUE_ROLE = QtCore.Qt.UserRole
def main():
...
self.palette.addItem(item) # přidáme položku do palety
item.setData(VALUE_ROLE, -1) # přiřadíme jí data
...
def item_activated():
for item in palette.selectedItems():
print(item.data(VALUE_ROLE)) # čteme data stejné role z položky
Nyní byste měli mít v paletě trávu a stěnu s patřičnými čísly (0
a -1
), které se vypisují do konzole při zvolení položky.
Nakonec si číslo místo vypisování uložíme do gridu, abychom ho mohli později použít.
def item_activated():
for item in palette.selectedItems():
grid.selected = item.data(VALUE_ROLE)
Nyní nezbývá nic jiného, než pomocí klikání nanášet zvolené dílky do mapy.
K tomu opět použijeme událost, tentokrát událost kliknutí, tedy mousePressEvent
.
class GridWidget(QtWidgets.QWidget):
...
def mousePressEvent(self, event):
# převedeme klik na souřadnice matice
row, column = pixels_to_logical(event.x(), event.y())
# Pokud jsme v matici, aktualizujeme data
if 0 <= row < self.array.shape[0] and 0 <= column < self.array.shape[1]:
self.array[row, column] = self.selected
# tímto zajistíme překreslení widgetu v místě změny:
# (pro Python 3.4 a nižší volejte jen self.update() bez argumentů)
self.update(*logical_to_pixels(row, column), CELL_SIZE, CELL_SIZE)
Zde víme, že kliknutí může změnit vykreslenou mapu pouze v místě kliknutí.
Pokud by ale kliknutí na určité políčko mohlo změnit obsah mapy někde jinde,
je lepší zavolat self.update()
bez argumentů a říct tak systému, že se má překreslit celý widget.
Protože po spuštění aplikace není zvolena žádná položka a self.selected
není definován, je rozumné prostě nějakou položku zvolit:
# Za přidáním položek do palety a napojení signálu
palette.setCurrentRow(1)
Můžete si vyzkoušet, že se mapa mění při použití jakéhokoliv tlačítka myši.
Je to proto, že mousePressEvent
se stane, kdykoli na widgetu stiskneme libovolné tlačítko.
Pokud bychom chtěli řešit pouze levé (primární) tlačítko, můžeme zjistit, které tlačítko událost vyvolalo:
if event.button() == QtCore.Qt.LeftButton:
self.array[row, column] = self.selected
else:
return
self.update(*logical_to_pixels(row, column), CELL_SIZE, CELL_SIZE)
Na pravé tlačítko myši můžeme namapovat funkci mazání:
elif event.button() == QtCore.Qt.RightButton:
self.array[row, column] = 0
Pro splnění úkolu bude stačit objekty mazat a klást pomocí klikání na jednotlivá políčka. Pokud však chcete poskytnout uživateli větší komfort, prozkoumejte další události a můžete políčka nanášet/mazat i při kliknutí a táhnutí. (Možná tu narazíte na problém, kdy se při příliš rychlém pohybu myši generují události pro body příliš daleko od sebe. Když program nestíhá, OS nebo Qt spojuje víc událostí pohybu myši dohromady a posílá jen jednu poslední. Jednotlivé body můžete spojit čárou pomocí knihovny bresenham.)
Naše aplikace bude umět vytvořit novou, prázdnou mapu. Ukážeme si, jak vytvořit modální dialog pro volby (šířka a výška nové mapy). „Modální dialog“ znamená okno, které musí uživatel zavřít, než může pracovat se zbytkem aplikace.
Layout okna nejprve naklikáme v Qt Designeru:
widthBox
, heightBox
).Tab
, je rozumné.newmaze.ui
.Poté připravíme funkci pro zobrazení dialogu a pro jeho vyhodnocení:
def new_dialog(window, grid):
# Vytvoříme nový dialog.
# V dokumentaci mají dialogy jako argument `this`;
# jde o "nadřazené" okno.
dialog = QtWidgets.QDialog(window)
# Načteme layout z Qt Designeru.
with open('newmaze.ui') as f:
uic.loadUi(f, dialog)
# Zobrazíme dialog.
# Funkce exec zajistí modalitu (tzn. nejde ovládat zbytek aplikace,
# dokud je dialog zobrazen) a vrátí se až potom, co uživatel dialog zavře.
result = dialog.exec()
# Výsledná hodnota odpovídá tlačítku/způsobu, kterým uživatel dialog zavřel.
if result == QtWidgets.QDialog.Rejected:
# Dialog uživatel zavřel nebo klikl na Cancel.
return
# Načtení hodnot ze SpinBoxů
cols = dialog.findChild(QtWidgets.QSpinBox, 'widthBox').value()
rows = dialog.findChild(QtWidgets.QSpinBox, 'heightBox').value()
# Vytvoření nové mapy
grid.array = numpy.zeros((rows, cols), dtype=numpy.int8)
# Mapa může být jinak velká, tak musíme změnit velikost Gridu;
# (tento kód používáme i jinde, měli bychom si na to udělat funkci!)
size = logical_to_pixels(rows, cols)
grid.setMinimumSize(*size)
grid.setMaximumSize(*size)
grid.resize(*size)
# Překreslení celého Gridu
grid.update()
def main():
...
# Napojení signálu actionNew.triggered
action = window.findChild(QtWidgets.QAction, 'actionNew')
action.triggered.connect(lambda: new_dialog(window, grid))
Další dialogy, které budeme potřebovat, jsou tak rozšířené (a mezi jednotlivými platformami tak různé), že je Qt má předpřipravené. Jsou to dialogy pro ukázání hlášky, výběr souboru, barvy nebo fontu nebo pro nastavení tisku.
Tyto předpřipravené dialogy mají typicky statické metody, které dialog vytvoří a přímo zavolají
exec()
a vrátí výsledek.
Pro splnění úkolu (dalších položek v menu) se vám můžou hodit tyto dialogy:
setWindowIcon(icon)
), v About dialogu bude automaticky vidětFunkce main
se nám pomalu rozrůstá a další funkce, které volá, musí být buď definované v ní (jako item_activated
)
nebo musí brát relativně hodně argumentů (jako new_dialog
).
Abychom si zjednodušili práci, můžeme logiku místo do funkce dát do třídy, ve které si důležité prvky
uložíme do atributů (self.grid
, self.window
, self.app
atd.).
Doporučujeme udělat přípravu v __init__
a volání window.show()
a return app.exec()
dát do metody run
.
A to je zatím vše! Další vylepšení budete mít za úkol – nebo si aplikaci přetvořte podle svého uvážení.
{ "data": { "sessionMaterial": { "id": "session-material:2019/plzen-jaro-2019:proj2:0", "title": "GUI v Pythonu", "html": "\n \n \n\n <h1>GUI v Pythonu: PyQt5</h1>\n<p>Způsobů, jak dělat v Pythonu aplikace s GUI, je mnoho. Dá se použít zabudovaný, ale ošklivý Tkinter, nebo nějaký externí framework.</p>\n<p>V tomto cvičení budeme používat framework Qt, protože je multiplatformní, používá se i v jiných oblastech, než je Python,\nje dostatečně robustní a dá se na většinu systémů nainstalovat bez větších problémů.</p>\n<p>Pomocí aplikace Qt Designer se dá navíc základní kostra GUI poměrně jednoduše <em>naklikat</em>, takže není nutné psát layout aplikace v kódu.</p>\n<h2>Instalace</h2>\n<p>Na tomto cvičení budete potřebovat balíček PyQt5 a aplikaci Qt5 Designer.\nPokud budete používat svůj počítač, prosíme vás o instalaci již předem, na cvičení toho bude opravdu hodně a nemůžeme si dovolit plýtvat časem.</p>\n<h3>PyQt5</h3>\n<p>Pokud máte Python alespoň 3.5 a jednu z platforem, pro které je připraven <a href=\"https://pypi.org/project/PyQt5/\">wheel na PyPI</a>, stačí udělat:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"gp\">(__venv__) $ </span>python -m pip install --upgrade pip\n<span class=\"gp\">(__venv__) $ </span>python -m pip install PyQt5\n</pre></div><p>Pro starší verzi Pythonu nebo 32bitový Linux to ale nebude fungovat.\nV takovém případě můžete PyQt5 zkusit najít v balíčkovacím systému vaší distribuce (např. balíček <code>python3-qt5</code> ve Fedoře nebo <code>python3-pyqt5</code> v Debianu).\nVirtualenv pak může vytvořit s přepínačem <code>--system-site-packages</code>, který zajistí, že i z virtualenvu uvidíte PyQt5 nainstalované z distribučního balíčku.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"gp\">$ </span>python3 -m venv --system-site-packages __venv__\n</pre></div><p>Pokud nic z toho nepomůže, můžete zkusit přeložit PyQt5 ze <a href=\"https://www.riverbankcomputing.com/software/pyqt/download5\">zdrojových souborů</a>\n(<a href=\"http://pyqt.sourceforge.net/Docs/PyQt5/installation.html#building-and-installing-from-source\">návod</a>).</p>\n<p><em>První aplikace</em> níže by vám měla fungovat.</p>\n<div class=\"admonition note\"><p>Pokud narazíte na chybu <code>Could not find or load the Qt platform plugin "xcb"</code>, podívejte se do <a href=\"https://github.com/cvut/MI-PYT/issues/57\">naší issue</a>.</p>\n</div><h3>Qt5 Designer</h3>\n<p>Na Linuxu najdete Qt5 Designer v balíčkách, třeba <code>qt5-designer</code> na Fedoře nebo <code>qttools5-dev-tools</code> na Debianu.</p>\n<p>Na Windows (i na Macu) si můžete <a href=\"https://www.qt.io/download-open-source/#section-2\">stáhnout</a> instalátor Qt 5, který (doufáme) nainstaluje i Designer.</p>\n<p>Pokud používáte na Macu <code>homebrew</code>, můžete to udělat i takto:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"gp\">$ </span>brew install qt5\n<span class=\"gp\">$ </span>brew linkapps qt5\n</pre></div><p>Existují i Python wheely <a href=\"https://pypi.python.org/pypi/pyqt5-tools\">pyqt5-tools</a> pro Windows obsahující Qt5 Designer.\nTen je pak potřeba pro spuštění dohledat v nainstalované lokaci.</p>\n<h3>NumPy</h3>\n<p>Do virtuálního prostředí s PyQt5 si nainstalujte i NumPy:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"gp\">$ </span>python -m pip install numpy\n</pre></div><h2>První aplikace</h2>\n<p>Napište si první aplikaci, ať vidíte, jak kód v PyQt vypadá.\nDetaily toho, jak to funguje, si ukážeme později.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">PyQt5</span> <span class=\"kn\">import</span> <span class=\"n\">QtWidgets</span>\n\n<span class=\"n\">app</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QApplication</span><span class=\"p\">([])</span>\n\n<span class=\"n\">button</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QPushButton</span><span class=\"p\">(</span><span class=\"s2\">"Click to Exit"</span><span class=\"p\">)</span>\n<span class=\"n\">button</span><span class=\"o\">.</span><span class=\"n\">setWindowTitle</span><span class=\"p\">(</span><span class=\"s2\">"Goodbye World"</span><span class=\"p\">)</span>\n<span class=\"n\">button</span><span class=\"o\">.</span><span class=\"n\">clicked</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"n\">app</span><span class=\"o\">.</span><span class=\"n\">quit</span><span class=\"p\">)</span>\n\n<span class=\"n\">button</span><span class=\"o\">.</span><span class=\"n\">show</span><span class=\"p\">()</span>\n\n<span class=\"n\">app</span><span class=\"o\">.</span><span class=\"k\">exec</span><span class=\"p\">()</span>\n</pre></div><h2>O Qt, PyQt a PySide</h2>\n<p><a href=\"https://www.qt.io/\">Qt</a> je aplikační framework napsaný v C++, který zjednodušuje psaní multiplatformních aplikací (od počítačů s Linuxem, Mac OS či Windows po různá vestavěná zařízení).</p>\n<p><a href=\"https://riverbankcomputing.com/software/pyqt\">PyQt</a> je knihovna, která umožňuje použít Qt z Pythonu.\nNa rozdíl od samotného Qt je licencovaná pod <a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\">GNU GPL v3</a>, která (stručně řečeno) vyžaduje, aby programy napsané s použitím PyQt byly šířeny pod stejnou licencí a se zdrojovým kódem.\nTedy: kdokoliv, kdo dostane kopii programu, musí mít možnost dostat odpovídající zdrojový kód a má možnost tento kód dál šířit pod stejnou licencí.</p>\n<p>Pokud by se vám tato licence nelíbila, je možnost použít <a href=\"https://wiki.qt.io/PySide\">PySide</a>, které má permisivnější licenci a téměř stejné API jako PyQt, ale není tak stabilní.</p>\n<h3>Moduly Qt</h3>\n<p>Qt je rozděleno na několik tzv. <a href=\"http://doc.qt.io/qt-5/qtmodules.html\">modulů</a>.\nPro grafická uživatelská rozhraní (GUI), kterými se budeme zabývat, použijeme hlavně <a href=\"http://doc.qt.io/qt-5/qtgui-index.html\">QtGui</a> a <a href=\"http://doc.qt.io/qt-5/qtwidgets-index.html\">QtWidgets</a>.</p>\n<p>Dále je tu modul <a href=\"http://doc.qt.io/qt-5/qtcore-index.html\">QtCore</a>, který obsahuje mj. základní datové typy jako QString a QList (které PyQt\nautomaticky převádí na pythonní ekvivalenty a zpět) nebo třeba <a href=\"http://doc.qt.io/qt-5/qrect.html\">QRect</a> – abstraktní obdélník.</p>\n<p>Další moduly jsou nadstavby od <a href=\"http://doc.qt.io/qt-5/qtsvg-index.html\">vykreslování SVG</a> nebo <a href=\"http://doc.qt.io/qt-5/qtmultimedia-index.html\">práci s multimédii</a> (které se můžou hodit) po\ntřeba práci s <a href=\"http://doc.qt.io/qt-5/qtsql-index.html\">SQL</a> a <a href=\"http://doc.qt.io/qt-5/qtxml-index.html\">XML</a> nebo <a href=\"http://doc.qt.io/qt-5/qtnetwork-index.html\">síťovou komunikaci</a>, kde je pro Python pohodlnější použít jiné knihovny.</p>\n<h2>Specifika PyQt</h2>\n<p>Ačkoli se Qt dá použít z Pythonu, bohužel zjistíte, že ne všechno funguje a vypadá tak, jako kdyby to byla knihovna od základů napsaná pro Python.\nTady jsou některé zvláštnosti, na které se můžete připravit.</p>\n<h3>Jména a dokumentace</h3>\n<p>Qt pojmenovává funkce, metody a atributy konvencí <code>camelCase</code>, místo pythonistického <code>snake_case</code>.\nPyQt tuto konvenci nemění: je užitečnější používat identická jména, a kromě toho knihovna PyQt vznikla ještě před PEP 8.</p>\n<p>Hledáte-li dokumentaci, doporučuji zadat do vyhledávače <code>qt5 <hledaný objekt></code>.\nDostanete se tak na dokumentaci pro C++ (např. <a href=\"http://doc.qt.io/qt-5/qobject.html\">QObject</a>).\nHledáte-li <code>pyqt5 <hledaný objekt></code>, dostanete se k dokumentaci pro Python, která ale většinou jen odkazuje\nna verzi pro C++ (např. <a href=\"http://pyqt.sourceforge.net/Docs/PyQt5/api/qobject.html\">pro QObject</a>).</p>\n<p>Rozdíly mezi C a pythonní verzí jsou většinou intuitivní (např. None místo NULL), ale jsou popsány\nv <a href=\"http://pyqt.sourceforge.net/Docs/PyQt5/index.html\">dokumentaci PyQt</a>.</p>\n<h3>Atributy</h3>\n<p>Qt zásadně používá pro přístup k atributům objektů funkce.\nFunkce pro čtení se typicky jmenuje podle atributu, funkce pro nastavení má předponu <code>set</code>.\nNamísto pythonního <code>c = obj.color</code> a <code>obj.color = ...</code> tedy použijeme <code>c = obj.color()</code> a <code>obj.setColor(...)</code>.</p>\n<h3>Správa paměti</h3>\n<p>Python a C++/Qt mají, bohužel, rozdílný přístup ke správě paměti.\nPython používá <em>reference counting</em> a <em>garbage collection</em>.\nC++ má objekty s destruktory, což Qt zjednodušuje (alespoň pro C++) <em>stromem vlastnictví</em>.</p>\n<p>Základní třída v Qt, ze které dědí téměř všechny ostatní, je QObject.\nTen má seznam potomků (children), o které se „stará“, a když uvolníme rodiče, uvolní se rekurzivně i všichni potomci.\nZ Pythonu pak můžeme dostat chybu <code>wrapped C/C++ object has been deleted</code>.\nJinak ale kombinace QObject a pythonních objektů funguje dobře.</p>\n<p>Větší problémy můžou nastat s pomocnými objekty, které nedědí z QObject a nemají potřebné „dynamické“ vlastnosti.\nTakový objekt doporučujeme používat jen v rámci jedné funkce (t.j. neukládat si ho jinde), pokud si nejste jistí že\nho „nevlastníte“ i ve smyslu C++/Qt.</p>\n<p>Občas se stane, že program spadne pro chybu jako nepovolený přístup do paměti.\nBez hlubší znalosti Qt a PyQt se taková chyba odstraňuje poměrně těžko, ale vaše znalosti C++ (z jiných kurzů)\na CPython C API (z <a href=\"/2019/plzen-jaro-2019/intro/cython/\">minula</a>) vám v tom pomůžou.\nDoporučujeme dělat malé commity a psát jednoduchý kód.</p>\n<h3>Smyčka událostí, signály a sloty</h3>\n<p>Qt funguje na principu smyčky událostí (event loop).\nMetoda <code>QApplication.exec</code> obsahuje v podstatě nekonečnou smyčku, která čeká na externí <em>události</em> (klik myši,\nžádost OS o vykreslení okna atd.) a na jejich základě volá příslušné funkce – ať už interní\nnebo námi definované.</p>\n<p>Pro komunikaci mezi objekty v rámci aplikace pak Qt používá mechanismus <em>signálů a slotů</em> (variantu <em>observer pattern</em>).\nSignál je vyslán (<em>emitted</em>) při události jako kliknutí na tlačítko, výběr položky z menu, zavření okna atp.\nK signálu může být připojeno několik slotů, což jsou funkce, které se po vyslání signálu zavolají.\nKód, který vysílá signál, obecně neví o tom, kolik slotů je připojeno (a jsou-li nějaké).</p>\n<p>V C++ jsou signály a sloty vždy staticky nadefinované na nějaké třídě, která dědí z <code>QObject</code>.\nV PyQt takto musí být nadefinovány jen signály; za slot poslouží jakákoli pythonní funkce.</p>\n<p>V příkladu výše jsme připojili signál <code>clicked</code> tlačítka na slot <code>quit</code> aplikace.\nStejně bychom mohli připojit jakoukoli funkci/metodu, která bere správný počet argumentů – v následujícím případě nula:</p>\n<div class=\"highlight\"><pre><span></span> <span class=\"n\">button</span><span class=\"o\">.</span><span class=\"n\">clicked</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"k\">lambda</span><span class=\"p\">:</span> <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s1\">'Exiting program'</span><span class=\"p\">))</span>\n</pre></div><p>V C++ je časté přetěžování funkcí (včetně signálů), což Pythonistům občas ztěžuje život.\nPyQt většinou automaticky vybere variantu signálu podle připojené funkce, ale ne vždy je to možné.</p>\n<p>Ukažme si to na následujícím kódu, který napojuje funkci <code>print</code> na dvě varianty signálu <a href=\"http://doc.qt.io/qt-5/qcombobox.html#activated\">QComboBox.activated</a>.\nTen se vyšle při výběru položky ze seznamu buď jako <code>QComboBox.activated[int]</code>, kdy předává index vybrané položky,\nnebo jako <code>QComboBox.activated[str]</code>, kdy předává text položky:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">PyQt5</span> <span class=\"kn\">import</span> <span class=\"n\">QtWidgets</span>\n\n<span class=\"n\">app</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QApplication</span><span class=\"p\">([])</span>\n\n<span class=\"c1\"># QComboBox - políčko pro výběr z několika možností</span>\n<span class=\"n\">box</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QComboBox</span><span class=\"p\">()</span>\n<span class=\"n\">box</span><span class=\"o\">.</span><span class=\"n\">addItem</span><span class=\"p\">(</span><span class=\"s1\">'First Option'</span><span class=\"p\">)</span>\n<span class=\"n\">box</span><span class=\"o\">.</span><span class=\"n\">addItem</span><span class=\"p\">(</span><span class=\"s1\">'Second Option'</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Základní varianta napojí na activated[int]</span>\n<span class=\"n\">box</span><span class=\"o\">.</span><span class=\"n\">activated</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"k\">print</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Výběr varianty signálu pomocí hranatých závorek</span>\n<span class=\"n\">box</span><span class=\"o\">.</span><span class=\"n\">activated</span><span class=\"p\">[</span><span class=\"nb\">str</span><span class=\"p\">]</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"k\">print</span><span class=\"p\">)</span>\n<span class=\"n\">box</span><span class=\"o\">.</span><span class=\"n\">activated</span><span class=\"p\">[</span><span class=\"nb\">int</span><span class=\"p\">]</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"k\">print</span><span class=\"p\">)</span>\n\n<span class=\"n\">box</span><span class=\"o\">.</span><span class=\"n\">show</span><span class=\"p\">()</span>\n\n<span class=\"n\">app</span><span class=\"o\">.</span><span class=\"k\">exec</span><span class=\"p\">()</span>\n</pre></div><h2>Skládání GUI</h2>\n<p>Základní způsob, jak v Qt vytvářet grafické rozhraní, je skládání funkčních prvků (<em>widgets</em>) do\nhierarchie oken, skupin a panelů.</p>\n<p>Základní třída pro funkční prvky, <a href=\"http://doc.qt.io/qt-5/qwidget.html\">QWidget</a>, dědí z už zmíněného <code>QObject</code>.\nKaždý QObject může obsahovat potomky (children), a v případě QWidget se potomci vykreslují\njako součást svého rodiče.\nNavíc může mít každý widget tzv. <em>layout</em>, který určuje pozici a velikost widgetů, které jsou\ndo něj přidané.</p>\n<p>Ukažme to v kódu:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">PyQt5</span> <span class=\"kn\">import</span> <span class=\"n\">QtWidgets</span>\n\n<span class=\"n\">app</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QApplication</span><span class=\"p\">([])</span>\n\n<span class=\"c1\"># Hlavní okno</span>\n<span class=\"n\">main</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QWidget</span><span class=\"p\">()</span>\n<span class=\"n\">main</span><span class=\"o\">.</span><span class=\"n\">setWindowTitle</span><span class=\"p\">(</span><span class=\"s1\">'Hello Qt'</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Layout pro hlavní okno</span>\n<span class=\"n\">layout</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QHBoxLayout</span><span class=\"p\">()</span>\n<span class=\"n\">main</span><span class=\"o\">.</span><span class=\"n\">setLayout</span><span class=\"p\">(</span><span class=\"n\">layout</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Nápis</span>\n<span class=\"n\">label</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QLabel</span><span class=\"p\">(</span><span class=\"s1\">'Click the button to change me'</span><span class=\"p\">)</span>\n<span class=\"c1\"># Přidáním do layoutu se nápis automaticky stane potomkem hlavního okna</span>\n<span class=\"n\">layout</span><span class=\"o\">.</span><span class=\"n\">addWidget</span><span class=\"p\">(</span><span class=\"n\">label</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Tlačítko</span>\n<span class=\"n\">button</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QPushButton</span><span class=\"p\">(</span><span class=\"s1\">'Click me'</span><span class=\"p\">)</span>\n<span class=\"n\">layout</span><span class=\"o\">.</span><span class=\"n\">addWidget</span><span class=\"p\">(</span><span class=\"n\">button</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Funkcionalita</span>\n<span class=\"k\">def</span> <span class=\"nf\">change_label</span><span class=\"p\">():</span>\n <span class=\"n\">label</span><span class=\"o\">.</span><span class=\"n\">setText</span><span class=\"p\">(</span><span class=\"s1\">'Good job. +100 points.'</span><span class=\"p\">)</span>\n\n<span class=\"n\">button</span><span class=\"o\">.</span><span class=\"n\">clicked</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"n\">change_label</span><span class=\"p\">)</span>\n\n<span class=\"c1\"># Spuštění</span>\n<span class=\"n\">main</span><span class=\"o\">.</span><span class=\"n\">show</span><span class=\"p\">()</span>\n<span class=\"n\">app</span><span class=\"o\">.</span><span class=\"k\">exec</span><span class=\"p\">()</span>\n</pre></div><p>Zabudovaných <a href=\"http://doc.qt.io/qt-5/layout.html#horizontal-vertical-grid-and-form-layouts\">layoutů</a> i <a href=\"https://doc.qt.io/qt-5/gallery.html\">widgetů</a> existuje spousta, jednodušší\nprogramy stačí „poskládat“ z nich a napojit je na logiku.\nPro složitější programy jsou pak možnosti, jak si widgety přizpůsobit.</p>\n<h2>Qt Designer</h2>\n<p>Na tomto cvičení si připravíme aplikaci pro editaci dlaždicových map (<em>tile maps</em>)\n– obrázků složených z omezené nabídky čtverečků („dlaždic“),\nznámých mj. ze <a href=\"https://www.google.cz/search?q=game+boy+overworld+map&tbm=isch\">starých her</a>.</p>\n<p>Aplikace bude mít část s mapou, paletu pro výběr dlaždice ke kreslení a navíc\nmenu a panel nástrojů pro akce jako ukládání a otevírání souborů:</p>\n<p><img src=\"/2019/plzen-jaro-2019/intro/pyqt/static/basic-screenshot.png\" alt=\"Obrázek aplikace\"></p>\n<p>Vytvářet GUI v kódu je poměrně neefektivní, a tak existuje nástroj, kde si okna můžeme „naklikat“.\nJmenuje se Qt Designer a měli byste ho mít nainstalovaný.\nNa školních počítačích se spouští příkazem <code>designer -qt=5</code>.</p>\n<p>Spustíme Designer a vytvoříme v něm nové <em>Main Window</em>.\nDo něj si z palety přidáme <em>Scroll Area</em> a doprava vedle něj <em>List Widget</em>.\nPoté aplikujeme layout: na volnou plochu okna klikneme pravým tlačítkem a vybereme <em>Lay Out ‣ Horizontally</em>.\n(Dá se to udělat i tlačítkem v liště.)</p>\n<p>Pomocí <kbd>Ctrl</kbd>+<kbd>R</kbd> lze zkontrolovat, jak okno vypadá a jak reaguje na změny velikosti.</p>\n<p>Potom přidáme položku do menu: místo <em>Type Here</em> napíšeme <em>Map</em> a pod něj podobně přidáme položky <em>New</em> a <em>Quit</em>.</p>\n<p>V panelu <em>Property Editor</em> jde měnit vlastnosti jednotlivých prvků.\nU skrolovacího okna nastavíme <em>objectName</em> na <em>scrollArea</em>.\nU <em>ListWidget</em> nastavíme <em>objectName</em> na <em>palette</em> a <em>sizePolicy ‣ Horizontal</em> na <em>Preferred</em>.\nV panelu <em>ActionEditor</em> najdeme položky pro <em>New</em> a <em>Quit</em> a nastavíme jim <em>objectName</em> na <em>actionNew</em>, resp. <em>actionQuit</em>.</p>\n<p>Potom přes pravé tlačítko na nevyužité ploše okna přidáme lištu nástrojů (<em>Add Toolbar</em>) a z panelu\n<em>Action Editor</em> do něj akci <em>actionQuit</em> přetáhneme.</p>\n<p>Pomocí <kbd>Ctrl</kbd>+<kbd>R</kbd> opět zkontrolujeme, jak okno vypadá a jak po nastavení <em>sizePolicy</em> reaguje na změny velikosti.</p>\n<p>V Designeru jde i napojovat signály. V panelu <em>Signal/Slot Editor</em> přidáme tento řádek:</p>\n<ul>\n<li>Sender: <code>actionQuit</code></li>\n<li>Signal: <code>triggered()</code></li>\n<li>Receiver: <code>MainWindow</code></li>\n<li>Slot: <code>close()</code></li>\n</ul>\n<p>Pomocí <kbd>Ctrl</kbd>+<kbd>R</kbd> jde ověřit, že zavírání okna funguje.</p>\n<p>Návrh okna uložíme do souboru <code>mainwindow.ui</code>.</p>\n<p>Soubor s návrhem jde převést na pythonní zdrojový soubor pomocí programu <code>pyuic5</code> nebo\nho vždy načíst přímo z programu.\nMy použijeme druhou variantu, je však dobré o <code>pyuic5</code> vědět, kdybyste někdy potřebovali\nzáklad pro vytváření UI v kódu (např. na vytvoření sady několika podobných tlačítek v cyklu).</p>\n<p>Načíst <code>.ui</code> soubor z programu do předpřipraveného okna <code>QMainWindow</code> lze pomocí funkce <a href=\"http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#the-uic-module\">uic.loadUi</a>:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">PyQt5</span> <span class=\"kn\">import</span> <span class=\"n\">QtWidgets</span><span class=\"p\">,</span> <span class=\"n\">uic</span>\n\n<span class=\"k\">def</span> <span class=\"nf\">main</span><span class=\"p\">():</span>\n <span class=\"n\">app</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QApplication</span><span class=\"p\">([])</span>\n\n <span class=\"n\">window</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QMainWindow</span><span class=\"p\">()</span>\n\n <span class=\"k\">with</span> <span class=\"nb\">open</span><span class=\"p\">(</span><span class=\"s1\">'mainwindow.ui'</span><span class=\"p\">)</span> <span class=\"k\">as</span> <span class=\"n\">f</span><span class=\"p\">:</span>\n <span class=\"n\">uic</span><span class=\"o\">.</span><span class=\"n\">loadUi</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"p\">,</span> <span class=\"n\">window</span><span class=\"p\">)</span>\n\n <span class=\"n\">window</span><span class=\"o\">.</span><span class=\"n\">show</span><span class=\"p\">()</span>\n\n <span class=\"k\">return</span> <span class=\"n\">app</span><span class=\"o\">.</span><span class=\"k\">exec</span><span class=\"p\">()</span>\n\n<span class=\"n\">main</span><span class=\"p\">()</span>\n</pre></div><h2>Vlastní widget - Grid</h2>\n<p>Qt neobsahuje předpřipravený widget na dlaždicové mapy. Musíme si tedy vyrobit vlastní.</p>\n<p>Mapu budeme reprezentovat jako NumPy matici (viz <a href=\"/2019/plzen-jaro-2019/intro/numpy/\">lekce o NumPy</a>).\nZatím budeme používat dva druhy dlaždic: trávu (v matici reprezentovanou jako 0) a zeď (-1).</p>\n<p>Velikost widgetu se zadává v pixelech. Musíme ho udělat dostatečně velký, aby se do něj vešla všechna políčka mapy.\nVelikost jednoho políčka v pixelech zvolíme pro jednoduchost konstantou.</p>\n<p>Souřadnice v Qt jsou v pixelech ve formě <code>(x, y)</code> – klasicky jak jsme zvyklí, <code>x</code> je horizontální souřadnice –\nkdežto matice je uložená po políčkách <code>(řádek, sloupec)</code>.\nAbychom se v tom neztratili, je dobré hned ze začátku udělat funkce pro převod mezi souřadnými systémy\na důsledně rozlišovat <code>(x, y)</code> vs. <code>(row, column)</code>.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"n\">CELL_SIZE</span> <span class=\"o\">=</span> <span class=\"mi\">32</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">pixels_to_logical</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">,</span> <span class=\"n\">y</span><span class=\"p\">):</span>\n <span class=\"k\">return</span> <span class=\"n\">y</span> <span class=\"o\">//</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">,</span> <span class=\"n\">x</span> <span class=\"o\">//</span> <span class=\"n\">CELL_SIZE</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">logical_to_pixels</span><span class=\"p\">(</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">):</span>\n <span class=\"k\">return</span> <span class=\"n\">column</span> <span class=\"o\">*</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">,</span> <span class=\"n\">row</span> <span class=\"o\">*</span> <span class=\"n\">CELL_SIZE</span>\n\n\n<span class=\"k\">class</span> <span class=\"nc\">GridWidget</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QWidget</span><span class=\"p\">):</span>\n <span class=\"k\">def</span> <span class=\"fm\">__init__</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">,</span> <span class=\"n\">array</span><span class=\"p\">):</span>\n <span class=\"nb\">super</span><span class=\"p\">()</span><span class=\"o\">.</span><span class=\"fm\">__init__</span><span class=\"p\">()</span> <span class=\"c1\"># musíme zavolat konstruktor předka</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span> <span class=\"o\">=</span> <span class=\"n\">array</span>\n <span class=\"c1\"># nastavíme velikost podle velikosti matice, jinak je náš widget příliš malý</span>\n <span class=\"n\">size</span> <span class=\"o\">=</span> <span class=\"n\">logical_to_pixels</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">array</span><span class=\"o\">.</span><span class=\"n\">shape</span><span class=\"p\">)</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">setMinimumSize</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">size</span><span class=\"p\">)</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">setMaximumSize</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">size</span><span class=\"p\">)</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">resize</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">size</span><span class=\"p\">)</span>\n</pre></div><p><code>GridWidget</code> vložíme do <code>QScrollArea</code>, kterou jsme si vytvořili v Qt Designeru:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">import</span> <span class=\"nn\">numpy</span>\n\n <span class=\"o\">...</span>\n\n <span class=\"c1\"># mapa zatím nadefinovaná rovnou v kódu</span>\n <span class=\"n\">array</span> <span class=\"o\">=</span> <span class=\"n\">numpy</span><span class=\"o\">.</span><span class=\"n\">zeros</span><span class=\"p\">((</span><span class=\"mi\">15</span><span class=\"p\">,</span> <span class=\"mi\">20</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">numpy</span><span class=\"o\">.</span><span class=\"n\">int8</span><span class=\"p\">)</span>\n <span class=\"n\">array</span><span class=\"p\">[:,</span> <span class=\"mi\">5</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"o\">-</span><span class=\"mi\">1</span> <span class=\"c1\"># nějaká zeď</span>\n\n <span class=\"c1\"># získáme oblast s posuvníky z Qt Designeru</span>\n <span class=\"n\">scroll_area</span> <span class=\"o\">=</span> <span class=\"n\">window</span><span class=\"o\">.</span><span class=\"n\">findChild</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QScrollArea</span><span class=\"p\">,</span> <span class=\"s1\">'scrollArea'</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># dáme do ní náš grid</span>\n <span class=\"n\">grid</span> <span class=\"o\">=</span> <span class=\"n\">GridWidget</span><span class=\"p\">(</span><span class=\"n\">array</span><span class=\"p\">)</span>\n <span class=\"n\">scroll_area</span><span class=\"o\">.</span><span class=\"n\">setWidget</span><span class=\"p\">(</span><span class=\"n\">grid</span><span class=\"p\">)</span>\n\n <span class=\"o\">...</span>\n</pre></div><p>Po spuštění aplikace zatím nic nového neuvidíte, maximálně se trochu změní posuvníky.\nPotřebujeme ještě zařídit, aby se data z matice vykreslovala do gridu.\nNejlepší je vykreslovat, kdykoliv nás OS (nebo Qt) vyzve, že potřebuje kus okna překreslit:\npři prvním zobrazení, odminimalizování okna, ukázání nové části mapy přes scrollování.\nTaké je zbytečné vykreslovat obrázky mimo oblast, kterou je vidět na obrazovce.</p>\n<p>K tomuto účelu nám poslouží událost (<em>event</em>).\nJak bylo řečeno v úvodu, na rozdíl od signálů a slotů, které zajišťují komunikaci v rámci aplikace,\nudálosti vznikají mimo aplikaci.\nJde například o kliknutí myší (<a href=\"http://doc.qt.io/qt-5/qwidget.html#mousePressEvent\">mouse*Event</a>), vstup z klávesnice (<a href=\"http://doc.qt.io/qt-5/qwidget.html#keyPressEvent\">key*Event</a>)\nnebo žádost OS o překreslení okna (<a href=\"http://doc.qt.io/qt-5/qwidget.html#paintEvent\">paintEvent</a>).\nNa poslední jmenovanou událost, <code>paintEvent</code>, teď budeme reagovat.</p>\n<p>Události se obsluhují předefinováním příslušné metody, která jako argument bere objekt popisující\ndanou událost.</p>\n<p>V rámci reakce na událost <code>paintEvent</code> můžeme používat <a href=\"http://doc.qt.io/qt-5/qpainter.html\">QPainter</a>, objekt, který generalizuje kreslení\nna různé „povrchy“ jako widgety, obrázky, nebo i instrukce pro tiskárnu.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">PyQt5</span> <span class=\"kn\">import</span> <span class=\"n\">QtWidgets</span><span class=\"p\">,</span> <span class=\"n\">QtGui</span><span class=\"p\">,</span> <span class=\"n\">QtCore</span><span class=\"p\">,</span> <span class=\"n\">uic</span>\n\n\n<span class=\"k\">class</span> <span class=\"nc\">GridWidget</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QWidget</span><span class=\"p\">):</span>\n\n <span class=\"o\">...</span>\n\n <span class=\"k\">def</span> <span class=\"nf\">paintEvent</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">,</span> <span class=\"n\">event</span><span class=\"p\">):</span>\n <span class=\"n\">rect</span> <span class=\"o\">=</span> <span class=\"n\">event</span><span class=\"o\">.</span><span class=\"n\">rect</span><span class=\"p\">()</span> <span class=\"c1\"># získáme informace o překreslované oblasti</span>\n\n <span class=\"c1\"># zjistíme, jakou oblast naší matice to představuje</span>\n <span class=\"c1\"># nesmíme se přitom dostat z matice ven</span>\n <span class=\"n\">row_min</span><span class=\"p\">,</span> <span class=\"n\">col_min</span> <span class=\"o\">=</span> <span class=\"n\">pixels_to_logical</span><span class=\"p\">(</span><span class=\"n\">rect</span><span class=\"o\">.</span><span class=\"n\">left</span><span class=\"p\">(),</span> <span class=\"n\">rect</span><span class=\"o\">.</span><span class=\"n\">top</span><span class=\"p\">())</span>\n <span class=\"n\">row_min</span> <span class=\"o\">=</span> <span class=\"nb\">max</span><span class=\"p\">(</span><span class=\"n\">row_min</span><span class=\"p\">,</span> <span class=\"mi\">0</span><span class=\"p\">)</span>\n <span class=\"n\">col_min</span> <span class=\"o\">=</span> <span class=\"nb\">max</span><span class=\"p\">(</span><span class=\"n\">col_min</span><span class=\"p\">,</span> <span class=\"mi\">0</span><span class=\"p\">)</span>\n <span class=\"n\">row_max</span><span class=\"p\">,</span> <span class=\"n\">col_max</span> <span class=\"o\">=</span> <span class=\"n\">pixels_to_logical</span><span class=\"p\">(</span><span class=\"n\">rect</span><span class=\"o\">.</span><span class=\"n\">right</span><span class=\"p\">(),</span> <span class=\"n\">rect</span><span class=\"o\">.</span><span class=\"n\">bottom</span><span class=\"p\">())</span>\n <span class=\"n\">row_max</span> <span class=\"o\">=</span> <span class=\"nb\">min</span><span class=\"p\">(</span><span class=\"n\">row_max</span> <span class=\"o\">+</span> <span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"o\">.</span><span class=\"n\">shape</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">])</span>\n <span class=\"n\">col_max</span> <span class=\"o\">=</span> <span class=\"nb\">min</span><span class=\"p\">(</span><span class=\"n\">col_max</span> <span class=\"o\">+</span> <span class=\"mi\">1</span><span class=\"p\">,</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"o\">.</span><span class=\"n\">shape</span><span class=\"p\">[</span><span class=\"mi\">1</span><span class=\"p\">])</span>\n\n <span class=\"n\">painter</span> <span class=\"o\">=</span> <span class=\"n\">QtGui</span><span class=\"o\">.</span><span class=\"n\">QPainter</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">)</span> <span class=\"c1\"># budeme kreslit</span>\n\n <span class=\"k\">for</span> <span class=\"n\">row</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">row_min</span><span class=\"p\">,</span> <span class=\"n\">row_max</span><span class=\"p\">):</span>\n <span class=\"k\">for</span> <span class=\"n\">column</span> <span class=\"ow\">in</span> <span class=\"nb\">range</span><span class=\"p\">(</span><span class=\"n\">col_min</span><span class=\"p\">,</span> <span class=\"n\">col_max</span><span class=\"p\">):</span>\n <span class=\"c1\"># získáme čtvereček, který budeme vybarvovat</span>\n <span class=\"n\">x</span><span class=\"p\">,</span> <span class=\"n\">y</span> <span class=\"o\">=</span> <span class=\"n\">logical_to_pixels</span><span class=\"p\">(</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">)</span>\n <span class=\"n\">rect</span> <span class=\"o\">=</span> <span class=\"n\">QtCore</span><span class=\"o\">.</span><span class=\"n\">QRectF</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">,</span> <span class=\"n\">y</span><span class=\"p\">,</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">,</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># šedá pro zdi, zelená pro trávu</span>\n <span class=\"k\">if</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">[</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">]</span> <span class=\"o\"><</span> <span class=\"mi\">0</span><span class=\"p\">:</span>\n <span class=\"n\">color</span> <span class=\"o\">=</span> <span class=\"n\">QtGui</span><span class=\"o\">.</span><span class=\"n\">QColor</span><span class=\"p\">(</span><span class=\"mi\">115</span><span class=\"p\">,</span> <span class=\"mi\">115</span><span class=\"p\">,</span> <span class=\"mi\">115</span><span class=\"p\">)</span>\n <span class=\"k\">else</span><span class=\"p\">:</span>\n <span class=\"n\">color</span> <span class=\"o\">=</span> <span class=\"n\">QtGui</span><span class=\"o\">.</span><span class=\"n\">QColor</span><span class=\"p\">(</span><span class=\"mi\">0</span><span class=\"p\">,</span> <span class=\"mi\">255</span><span class=\"p\">,</span> <span class=\"mi\">0</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># vyplníme čtvereček barvou</span>\n <span class=\"n\">painter</span><span class=\"o\">.</span><span class=\"n\">fillRect</span><span class=\"p\">(</span><span class=\"n\">rect</span><span class=\"p\">,</span> <span class=\"n\">QtGui</span><span class=\"o\">.</span><span class=\"n\">QBrush</span><span class=\"p\">(</span><span class=\"n\">color</span><span class=\"p\">))</span>\n</pre></div><p>Nyní by již mapa měla být v okně vidět barevně.</p>\n<h3>Obrázky</h3>\n<p>Protože barvičky jsou příliš nudné, přidáme do mapového widgetu obrázky.</p>\n<p>Veškerou, ke cvičení i k úkolu potřebnou, grafiku najdete na <a href=\"https://github.com/pyvec/naucse.python.cz/tree/master/lessons/intro/pyqt/static/pics\">GitHubu</a>.\nJe k dispozici pod public domain (tj. „dělej si s tím, co chceš“), pochází ze studia <a href=\"http://kenney.nl/\">Kenney</a>\na je (společně se další volně licencovanou grafikou) ke stažení z <a href=\"http://opengameart.org/users/kenney\">OpenGameArt.org</a>.</p>\n<p>Zatím budeme potřebovat jen dva obrázky:</p>\n<ul>\n<li><a href=\"/2019/plzen-jaro-2019/intro/pyqt/static/pics/grass.svg\">grass.svg</a></li>\n<li><a href=\"/2019/plzen-jaro-2019/intro/pyqt/static/pics/wall.svg\">wall.svg</a></li>\n</ul>\n<p>Nejprve si načteme SVG soubory jako objekty <code>QSvgRenderer</code>:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">PyQt5</span> <span class=\"kn\">import</span> <span class=\"n\">QtWidgets</span><span class=\"p\">,</span> <span class=\"n\">QtCore</span><span class=\"p\">,</span> <span class=\"n\">QtGui</span><span class=\"p\">,</span> <span class=\"n\">QtSvg</span><span class=\"p\">,</span> <span class=\"n\">uic</span>\n\n<span class=\"n\">SVG_GRASS</span> <span class=\"o\">=</span> <span class=\"n\">QtSvg</span><span class=\"o\">.</span><span class=\"n\">QSvgRenderer</span><span class=\"p\">(</span><span class=\"s1\">'grass.svg'</span><span class=\"p\">)</span>\n<span class=\"n\">SVG_WALL</span> <span class=\"o\">=</span> <span class=\"n\">QtSvg</span><span class=\"o\">.</span><span class=\"n\">QSvgRenderer</span><span class=\"p\">(</span><span class=\"s1\">'wall.svg'</span><span class=\"p\">)</span>\n</pre></div><p>A poté je na správných místech vyrendrujeme:</p>\n<div class=\"highlight\"><pre><span></span> <span class=\"o\">...</span>\n <span class=\"n\">rect</span> <span class=\"o\">=</span> <span class=\"n\">QtCore</span><span class=\"o\">.</span><span class=\"n\">QRectF</span><span class=\"p\">(</span><span class=\"n\">x</span><span class=\"p\">,</span> <span class=\"n\">y</span><span class=\"p\">,</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">,</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># podkladová barva pod poloprůhledné obrázky</span>\n <span class=\"n\">white</span> <span class=\"o\">=</span> <span class=\"n\">QtGui</span><span class=\"o\">.</span><span class=\"n\">QColor</span><span class=\"p\">(</span><span class=\"mi\">255</span><span class=\"p\">,</span> <span class=\"mi\">255</span><span class=\"p\">,</span> <span class=\"mi\">255</span><span class=\"p\">)</span>\n <span class=\"n\">painter</span><span class=\"o\">.</span><span class=\"n\">fillRect</span><span class=\"p\">(</span><span class=\"n\">rect</span><span class=\"p\">,</span> <span class=\"n\">QtGui</span><span class=\"o\">.</span><span class=\"n\">QBrush</span><span class=\"p\">(</span><span class=\"n\">white</span><span class=\"p\">))</span>\n\n <span class=\"c1\"># trávu dáme všude, protože i zdi stojí na trávě</span>\n <span class=\"n\">SVG_GRASS</span><span class=\"o\">.</span><span class=\"n\">render</span><span class=\"p\">(</span><span class=\"n\">painter</span><span class=\"p\">,</span> <span class=\"n\">rect</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># zdi dáme jen tam, kam patří</span>\n <span class=\"k\">if</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">[</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">]</span> <span class=\"o\"><</span> <span class=\"mi\">0</span><span class=\"p\">:</span>\n <span class=\"n\">SVG_WALL</span><span class=\"o\">.</span><span class=\"n\">render</span><span class=\"p\">(</span><span class=\"n\">painter</span><span class=\"p\">,</span> <span class=\"n\">rect</span><span class=\"p\">)</span>\n</pre></div><h2>Model/View</h2>\n<p>Nyní trochu odbočíme a povíme si krátce o dalším podsystému Qt: o modelech.</p>\n<p>Qt obsahuje framework, který mapuje informace do podoby tabulek, seznamů nebo obecných stromů.\nVzniklé modely se potom dají zobrazit ve specializovaných widgetech.\nSamotná data můžou být uložena kdekoli – v paměti, SQL databázi, souborech a podobně.\nDokonce nemusí být všechna dostupná: existuje vestavěný model pro souborový systém,\nkterý se dá zobrazit aniž by se procházely všechny soubory.\nKdyž je informace potřeba, model se postará o její načtení.\nPomocí modelů a modelových widgetů lze informace i měnit, a pokud je model\nzobrazen ve více widgetech zároveň, změny se projeví ve všech.</p>\n<p>Obecné modely je bohužel relativně obtížné implementovat v Pythonu, protože používají třídy,\nkteré nedědí z QObject, takže je potřeba sledovat, jestli je „vlastní“ Python nebo C++.\nNaštěstí ale existují widgety se zabudovanými modely, které obsahují i samotná data.\nTyto modely je složitější napojit na existující aplikační logiku, ale pro většinu účelů postačí.</p>\n<p>O obecných modelech si můžete přečíst v <a href=\"http://doc.qt.io/qt-5/model-view-programming.html\">dokumentaci</a>.</p>\n<h2>QListWidget - Paleta</h2>\n<p>Jeden z widgetů se zabudovaným modelem je <code>QListWidget</code>, který umí spravovat a zobrazovat\nnějaký seznam.\nMy jsme si v Qt Designeru připravili <code>QListWidget</code> s názvem <code>palette</code>, který použijeme\njako paletu jednotlivých dílků, které budeme moci vkládat do mapy.\nPoložky se do tohoto modelu přidávají následovně:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">main</span><span class=\"p\">():</span>\n <span class=\"o\">...</span>\n\n <span class=\"c1\"># získáme paletu vytvořenou v Qt Designeru</span>\n <span class=\"n\">palette</span> <span class=\"o\">=</span> <span class=\"n\">window</span><span class=\"o\">.</span><span class=\"n\">findChild</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QListWidget</span><span class=\"p\">,</span> <span class=\"s1\">'palette'</span><span class=\"p\">)</span>\n\n <span class=\"n\">item</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QListWidgetItem</span><span class=\"p\">(</span><span class=\"s1\">'Grass'</span><span class=\"p\">)</span> <span class=\"c1\"># vytvoříme položku</span>\n <span class=\"n\">icon</span> <span class=\"o\">=</span> <span class=\"n\">QtGui</span><span class=\"o\">.</span><span class=\"n\">QIcon</span><span class=\"p\">(</span><span class=\"s1\">'grass.svg'</span><span class=\"p\">)</span> <span class=\"c1\"># ikonu</span>\n <span class=\"n\">item</span><span class=\"o\">.</span><span class=\"n\">setIcon</span><span class=\"p\">(</span><span class=\"n\">icon</span><span class=\"p\">)</span> <span class=\"c1\"># přiřadíme ikonu položce</span>\n <span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">addItem</span><span class=\"p\">(</span><span class=\"n\">item</span><span class=\"p\">)</span> <span class=\"c1\"># přidáme položku do palety</span>\n</pre></div><p>Stejným způsobem lze do palety přidat další položky: kromě trávy budeme na toto\ncvičení potřebovat i stěnu.\nProtože v úkolu bude položek více, je lepší si na to vytvořit funkci či metodu.\nTo necháme na vás.</p>\n<p>Zatím jsme vytvořili paletu, ve které uživatel může položky vybírat.\nVýběr položky aktivuje signál <code>itemSelectionChanged</code>, na který můžeme\nnavázat volání funkce.\n(Pokud bychom měli pod kontrolou třídu widgetu, jako tomu je u třídy <code>Grid</code>,\nmohli bychom místo toho i předefinovat metodu <code>itemSelectionChanged()</code>.)</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">main</span><span class=\"p\">():</span>\n <span class=\"o\">...</span>\n\n <span class=\"k\">def</span> <span class=\"nf\">item_activated</span><span class=\"p\">():</span>\n <span class=\"sd\">"""Tato funkce se zavolá, když uživatel zvolí položku"""</span>\n\n <span class=\"c1\"># Položek může obecně být vybráno víc, ale v našem seznamu je to</span>\n <span class=\"c1\"># zakázáno (v Designeru selectionMode=SingleSelection).</span>\n <span class=\"c1\"># Projdeme "všechny vybrané položky", i když víme že bude max. jedna.</span>\n <span class=\"k\">for</span> <span class=\"n\">item</span> <span class=\"ow\">in</span> <span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">selectedItems</span><span class=\"p\">():</span>\n <span class=\"n\">row_num</span> <span class=\"o\">=</span> <span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">indexFromItem</span><span class=\"p\">(</span><span class=\"n\">item</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">row</span><span class=\"p\">()</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">row_num</span><span class=\"p\">)</span>\n\n <span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">itemSelectionChanged</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"n\">item_activated</span><span class=\"p\">)</span>\n</pre></div><p>Nyní, když uživatel zvolí položku, vypíše se do konzole její pořadí.\nNás by ale spíš zajímalo, jak bude tato položka reprezentována v matici s mapou.\nK položce v paletě můžeme uložit informace pomocí <code>item.setData(<role>, <data>)</code>.\nRolí pro informace je <a href=\"http://doc.qt.io/qt-5/qt.html#ItemDataRole-enum\">spousta</a> a několik z nich Qt používá pro vykreslování.\nPro vlastní data můžeme použít <code>QtCore.Qt.UserRole</code>.\nV případě potřeby ukládat více dat můžeme dále zvolit <code>QtCore.Qt.UserRole + 1</code> atd.\nPro případ, že budeme potřebovat rolí víc, je dobré si je vhodně pojmenovat.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"n\">VALUE_ROLE</span> <span class=\"o\">=</span> <span class=\"n\">QtCore</span><span class=\"o\">.</span><span class=\"n\">Qt</span><span class=\"o\">.</span><span class=\"n\">UserRole</span>\n\n<span class=\"k\">def</span> <span class=\"nf\">main</span><span class=\"p\">():</span>\n <span class=\"o\">...</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">addItem</span><span class=\"p\">(</span><span class=\"n\">item</span><span class=\"p\">)</span> <span class=\"c1\"># přidáme položku do palety</span>\n <span class=\"n\">item</span><span class=\"o\">.</span><span class=\"n\">setData</span><span class=\"p\">(</span><span class=\"n\">VALUE_ROLE</span><span class=\"p\">,</span> <span class=\"o\">-</span><span class=\"mi\">1</span><span class=\"p\">)</span> <span class=\"c1\"># přiřadíme jí data</span>\n <span class=\"o\">...</span>\n\n <span class=\"k\">def</span> <span class=\"nf\">item_activated</span><span class=\"p\">():</span>\n <span class=\"k\">for</span> <span class=\"n\">item</span> <span class=\"ow\">in</span> <span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">selectedItems</span><span class=\"p\">():</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">item</span><span class=\"o\">.</span><span class=\"n\">data</span><span class=\"p\">(</span><span class=\"n\">VALUE_ROLE</span><span class=\"p\">))</span> <span class=\"c1\"># čteme data stejné role z položky</span>\n</pre></div><p>Nyní byste měli mít v paletě trávu a stěnu s patřičnými čísly (<code>0</code> a <code>-1</code>), které se vypisují do konzole při zvolení položky.</p>\n<p>Nakonec si číslo místo vypisování uložíme do gridu, abychom ho mohli později použít.</p>\n<div class=\"highlight\"><pre><span></span> <span class=\"k\">def</span> <span class=\"nf\">item_activated</span><span class=\"p\">():</span>\n <span class=\"k\">for</span> <span class=\"n\">item</span> <span class=\"ow\">in</span> <span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">selectedItems</span><span class=\"p\">():</span>\n <span class=\"n\">grid</span><span class=\"o\">.</span><span class=\"n\">selected</span> <span class=\"o\">=</span> <span class=\"n\">item</span><span class=\"o\">.</span><span class=\"n\">data</span><span class=\"p\">(</span><span class=\"n\">VALUE_ROLE</span><span class=\"p\">)</span>\n</pre></div><h2>Klikání do gridu</h2>\n<p>Nyní nezbývá nic jiného, než pomocí klikání nanášet zvolené dílky do mapy.\nK tomu opět použijeme událost, tentokrát událost kliknutí, tedy <code>mousePressEvent</code>.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">class</span> <span class=\"nc\">GridWidget</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QWidget</span><span class=\"p\">):</span>\n <span class=\"o\">...</span>\n\n <span class=\"k\">def</span> <span class=\"nf\">mousePressEvent</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">,</span> <span class=\"n\">event</span><span class=\"p\">):</span>\n <span class=\"c1\"># převedeme klik na souřadnice matice</span>\n <span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span> <span class=\"o\">=</span> <span class=\"n\">pixels_to_logical</span><span class=\"p\">(</span><span class=\"n\">event</span><span class=\"o\">.</span><span class=\"n\">x</span><span class=\"p\">(),</span> <span class=\"n\">event</span><span class=\"o\">.</span><span class=\"n\">y</span><span class=\"p\">())</span>\n\n <span class=\"c1\"># Pokud jsme v matici, aktualizujeme data</span>\n <span class=\"k\">if</span> <span class=\"mi\">0</span> <span class=\"o\"><=</span> <span class=\"n\">row</span> <span class=\"o\"><</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"o\">.</span><span class=\"n\">shape</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">]</span> <span class=\"ow\">and</span> <span class=\"mi\">0</span> <span class=\"o\"><=</span> <span class=\"n\">column</span> <span class=\"o\"><</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"o\">.</span><span class=\"n\">shape</span><span class=\"p\">[</span><span class=\"mi\">1</span><span class=\"p\">]:</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">[</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">selected</span>\n\n <span class=\"c1\"># tímto zajistíme překreslení widgetu v místě změny:</span>\n <span class=\"c1\"># (pro Python 3.4 a nižší volejte jen self.update() bez argumentů)</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">update</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">logical_to_pixels</span><span class=\"p\">(</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">),</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">,</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">)</span>\n</pre></div><div class=\"admonition note\"><p>Zde víme, že kliknutí může změnit vykreslenou mapu pouze v místě kliknutí.\nPokud by ale kliknutí na určité políčko mohlo změnit obsah mapy někde jinde,\nje lepší zavolat <code>self.update()</code> bez argumentů a říct tak systému, že se má překreslit celý widget.</p>\n</div><p>Protože po spuštění aplikace není zvolena žádná položka a <code>self.selected</code> není definován, je rozumné prostě nějakou položku zvolit:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"c1\"># Za přidáním položek do palety a napojení signálu</span>\n<span class=\"n\">palette</span><span class=\"o\">.</span><span class=\"n\">setCurrentRow</span><span class=\"p\">(</span><span class=\"mi\">1</span><span class=\"p\">)</span>\n</pre></div><h3>Více tlačítek myši</h3>\n<p>Můžete si vyzkoušet, že se mapa mění při použití jakéhokoliv tlačítka myši.\nJe to proto, že <code>mousePressEvent</code> se stane, kdykoli na widgetu stiskneme libovolné tlačítko.\nPokud bychom chtěli řešit pouze levé (primární) tlačítko, můžeme zjistit, které tlačítko událost vyvolalo:</p>\n<div class=\"highlight\"><pre><span></span> <span class=\"k\">if</span> <span class=\"n\">event</span><span class=\"o\">.</span><span class=\"n\">button</span><span class=\"p\">()</span> <span class=\"o\">==</span> <span class=\"n\">QtCore</span><span class=\"o\">.</span><span class=\"n\">Qt</span><span class=\"o\">.</span><span class=\"n\">LeftButton</span><span class=\"p\">:</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">[</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">selected</span>\n <span class=\"k\">else</span><span class=\"p\">:</span>\n <span class=\"k\">return</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">update</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">logical_to_pixels</span><span class=\"p\">(</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">),</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">,</span> <span class=\"n\">CELL_SIZE</span><span class=\"p\">)</span>\n</pre></div><p>Na pravé tlačítko myši můžeme namapovat funkci mazání:</p>\n<div class=\"highlight\"><pre><span></span> <span class=\"k\">elif</span> <span class=\"n\">event</span><span class=\"o\">.</span><span class=\"n\">button</span><span class=\"p\">()</span> <span class=\"o\">==</span> <span class=\"n\">QtCore</span><span class=\"o\">.</span><span class=\"n\">Qt</span><span class=\"o\">.</span><span class=\"n\">RightButton</span><span class=\"p\">:</span>\n <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">array</span><span class=\"p\">[</span><span class=\"n\">row</span><span class=\"p\">,</span> <span class=\"n\">column</span><span class=\"p\">]</span> <span class=\"o\">=</span> <span class=\"mi\">0</span>\n</pre></div><h3>Tažení myši</h3>\n<p>Pro splnění úkolu bude stačit objekty mazat a klást pomocí klikání na jednotlivá políčka.\nPokud však chcete poskytnout uživateli větší komfort, prozkoumejte další <a href=\"http://doc.qt.io/qt-5/qwidget.html\">události</a>\na můžete políčka nanášet/mazat i při kliknutí a táhnutí.\n(Možná tu narazíte na problém, kdy se při příliš rychlém pohybu myši generují události\npro body příliš daleko od sebe.\nKdyž program nestíhá, OS nebo Qt spojuje víc událostí pohybu myši dohromady a posílá\njen jednu poslední.\nJednotlivé body můžete spojit čárou pomocí knihovny <a href=\"https://pypi.org/project/bresenham/\">bresenham</a>.)</p>\n<h2>Menu a modální dialog</h2>\n<p>Naše aplikace bude umět vytvořit novou, prázdnou mapu.\nUkážeme si, jak vytvořit modální dialog pro volby (šířka a výška nové mapy).\n„Modální dialog“ znamená okno, které musí uživatel zavřít, než může pracovat se zbytkem aplikace.</p>\n<p>Layout okna nejprve naklikáme v Qt Designeru:</p>\n<ol>\n<li>Po spuštění zvolíme <em>Dialog with Buttons Bottom</em> a <em>Create</em>.</li>\n<li>Přes pravé tlačítko pro dialog zvolíme <em>Lay Out ‣ Vertically</em>.</li>\n<li>Nad tlačítka <em>Cancel</em> a <em>OK</em> přetáhneme z <em>Widet Box</em> <em>Form Layout</em> (layouty lze takto přímo vnořovat).</li>\n<li>Do něj přetáhneme postupně dvakrát <em>Label</em> a <em>Spin Box</em>, abychom vytvořili formulář.</li>\n<li>Přejmenujeme v panelu <em>Property Editor</em> jednotlivé přidané položky tak, aby dávaly v kódu smysl (<code>widthBox</code>, <code>heightBox</code>).</li>\n<li>Poklikáním na <em>Labely</em> změníme jejich text.</li>\n<li>Nastavíme v panelu <em>Property Editor</em> rozumné limity a výchozí hodnoty pro <em>Spin Boxy</em>.</li>\n<li>Okno případně zmenšíme, aby nebylo zbytečně velké.</li>\n<li>V menu zvolíme <em>Edit ‣ Edit Buddies</em> a táhnutím z <em>Labelu</em> na <em>Spin Box</em> nastavíme, ke kterému prvku se <em>Label</em> vztahuje.</li>\n<li>V menu zvolíme <em>Edit ‣ Edit Tab Order</em> a zkontrolujeme, že pořadí, ve kterém bude prvky vybírat klávesa <code>Tab</code>, je rozumné.</li>\n<li>Můžeme se vrátit zpět na <em>Edit ‣ Edit Widgets</em>.</li>\n<li>Dialog uložíme jako <code>newmaze.ui</code>.</li>\n</ol>\n<p>Poté připravíme funkci pro zobrazení dialogu a pro jeho vyhodnocení:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">new_dialog</span><span class=\"p\">(</span><span class=\"n\">window</span><span class=\"p\">,</span> <span class=\"n\">grid</span><span class=\"p\">):</span>\n <span class=\"c1\"># Vytvoříme nový dialog.</span>\n <span class=\"c1\"># V dokumentaci mají dialogy jako argument `this`;</span>\n <span class=\"c1\"># jde o "nadřazené" okno.</span>\n <span class=\"n\">dialog</span> <span class=\"o\">=</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QDialog</span><span class=\"p\">(</span><span class=\"n\">window</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># Načteme layout z Qt Designeru.</span>\n <span class=\"k\">with</span> <span class=\"nb\">open</span><span class=\"p\">(</span><span class=\"s1\">'newmaze.ui'</span><span class=\"p\">)</span> <span class=\"k\">as</span> <span class=\"n\">f</span><span class=\"p\">:</span>\n <span class=\"n\">uic</span><span class=\"o\">.</span><span class=\"n\">loadUi</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"p\">,</span> <span class=\"n\">dialog</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># Zobrazíme dialog.</span>\n <span class=\"c1\"># Funkce exec zajistí modalitu (tzn. nejde ovládat zbytek aplikace,</span>\n <span class=\"c1\"># dokud je dialog zobrazen) a vrátí se až potom, co uživatel dialog zavře.</span>\n <span class=\"n\">result</span> <span class=\"o\">=</span> <span class=\"n\">dialog</span><span class=\"o\">.</span><span class=\"k\">exec</span><span class=\"p\">()</span>\n\n <span class=\"c1\"># Výsledná hodnota odpovídá tlačítku/způsobu, kterým uživatel dialog zavřel.</span>\n <span class=\"k\">if</span> <span class=\"n\">result</span> <span class=\"o\">==</span> <span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QDialog</span><span class=\"o\">.</span><span class=\"n\">Rejected</span><span class=\"p\">:</span>\n <span class=\"c1\"># Dialog uživatel zavřel nebo klikl na Cancel.</span>\n <span class=\"k\">return</span>\n\n <span class=\"c1\"># Načtení hodnot ze SpinBoxů</span>\n <span class=\"n\">cols</span> <span class=\"o\">=</span> <span class=\"n\">dialog</span><span class=\"o\">.</span><span class=\"n\">findChild</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QSpinBox</span><span class=\"p\">,</span> <span class=\"s1\">'widthBox'</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">value</span><span class=\"p\">()</span>\n <span class=\"n\">rows</span> <span class=\"o\">=</span> <span class=\"n\">dialog</span><span class=\"o\">.</span><span class=\"n\">findChild</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QSpinBox</span><span class=\"p\">,</span> <span class=\"s1\">'heightBox'</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">value</span><span class=\"p\">()</span>\n\n <span class=\"c1\"># Vytvoření nové mapy</span>\n <span class=\"n\">grid</span><span class=\"o\">.</span><span class=\"n\">array</span> <span class=\"o\">=</span> <span class=\"n\">numpy</span><span class=\"o\">.</span><span class=\"n\">zeros</span><span class=\"p\">((</span><span class=\"n\">rows</span><span class=\"p\">,</span> <span class=\"n\">cols</span><span class=\"p\">),</span> <span class=\"n\">dtype</span><span class=\"o\">=</span><span class=\"n\">numpy</span><span class=\"o\">.</span><span class=\"n\">int8</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># Mapa může být jinak velká, tak musíme změnit velikost Gridu;</span>\n <span class=\"c1\"># (tento kód používáme i jinde, měli bychom si na to udělat funkci!)</span>\n <span class=\"n\">size</span> <span class=\"o\">=</span> <span class=\"n\">logical_to_pixels</span><span class=\"p\">(</span><span class=\"n\">rows</span><span class=\"p\">,</span> <span class=\"n\">cols</span><span class=\"p\">)</span>\n <span class=\"n\">grid</span><span class=\"o\">.</span><span class=\"n\">setMinimumSize</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">size</span><span class=\"p\">)</span>\n <span class=\"n\">grid</span><span class=\"o\">.</span><span class=\"n\">setMaximumSize</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">size</span><span class=\"p\">)</span>\n <span class=\"n\">grid</span><span class=\"o\">.</span><span class=\"n\">resize</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">size</span><span class=\"p\">)</span>\n\n <span class=\"c1\"># Překreslení celého Gridu</span>\n <span class=\"n\">grid</span><span class=\"o\">.</span><span class=\"n\">update</span><span class=\"p\">()</span>\n\n\n<span class=\"k\">def</span> <span class=\"nf\">main</span><span class=\"p\">():</span>\n <span class=\"o\">...</span>\n\n\n <span class=\"c1\"># Napojení signálu actionNew.triggered</span>\n\n <span class=\"n\">action</span> <span class=\"o\">=</span> <span class=\"n\">window</span><span class=\"o\">.</span><span class=\"n\">findChild</span><span class=\"p\">(</span><span class=\"n\">QtWidgets</span><span class=\"o\">.</span><span class=\"n\">QAction</span><span class=\"p\">,</span> <span class=\"s1\">'actionNew'</span><span class=\"p\">)</span>\n <span class=\"n\">action</span><span class=\"o\">.</span><span class=\"n\">triggered</span><span class=\"o\">.</span><span class=\"n\">connect</span><span class=\"p\">(</span><span class=\"k\">lambda</span><span class=\"p\">:</span> <span class=\"n\">new_dialog</span><span class=\"p\">(</span><span class=\"n\">window</span><span class=\"p\">,</span> <span class=\"n\">grid</span><span class=\"p\">))</span>\n</pre></div><p>Další dialogy, které budeme potřebovat, jsou tak rozšířené (a mezi jednotlivými platformami tak různé),\nže je Qt má předpřipravené.\nJsou to dialogy pro ukázání hlášky, výběr souboru, barvy nebo fontu nebo pro nastavení tisku.</p>\n<p>Tyto předpřipravené dialogy mají typicky statické metody, které dialog vytvoří a přímo zavolají\n<code>exec()</code> a vrátí výsledek.</p>\n<p>Pro splnění úkolu (dalších položek v menu) se vám můžou hodit tyto dialogy:</p>\n<ul>\n<li><a href=\"http://doc.qt.io/qt-5/qfiledialog.html#getOpenFileName\">QtWidgets.QFileDialog.getOpenFileName</a></li>\n<li><a href=\"http://doc.qt.io/qt-5/qfiledialog.html#getSaveFileName\">QtWidgets.QFileDialog.getSaveFileName</a></li>\n<li><a href=\"http://doc.qt.io/qt-5/qmessagebox.html#critical\">QtWidgets.QMessageBox.critical</a></li>\n<li><a href=\"http://doc.qt.io/qt-5/qmessagebox.html#about\">QtWidgets.QMessageBox.about</a><ul>\n<li>Tip: Do <em>QMessageBoxu</em> jde dávat i HTML (ale v řetězci s ním nesmí být zalomeny řádky, jinak to nefunguje)</li>\n<li>Tip: Když hlavnímu oknu aplikace nastavíte ikonu (<code>setWindowIcon(icon)</code>), v <em>About</em> dialogu bude automaticky vidět</li>\n</ul>\n</li>\n</ul>\n<h2>Třída pro GUI aplikace</h2>\n<p>Funkce <code>main</code> se nám pomalu rozrůstá a další funkce, které volá, musí být buď definované v ní (jako <code>item_activated</code>)\nnebo musí brát relativně hodně argumentů (jako <code>new_dialog</code>).\nAbychom si zjednodušili práci, můžeme logiku místo do funkce dát do třídy, ve které si důležité prvky\nuložíme do atributů (<code>self.grid</code>, <code>self.window</code>, <code>self.app</code> atd.).\nDoporučujeme udělat přípravu v <code>__init__</code> a volání <code>window.show()</code> a <code>return app.exec()</code> dát do metody <code>run</code>.</p>\n<p>A to je zatím vše!\nDalší vylepšení budete mít za úkol – nebo si aplikaci přetvořte podle svého uvážení.</p>\n\n\n " } } }