Obsah
1. Tvorba GUI v Pythonu s využitím frameworku PySide: použití cest při kreslení 2D scén
2. Koncepty, na nichž je založen objekt typu QPainterPath
3. Základní příkazy určené pro vytvoření cesty
4. První demonstrační příklad: vytvoření a vykreslení jednoduché cesty složené z úsečkových segmentů
5. Kvadratické Bézierovy křivky
8. Tvorba vyplněných cest s využitím konstantní barvy, gradientu nebo textury
9. Třetí demonstrační příklad: vykreslení vyplněných cest
10. Množinové operace aplikované na cesty
15. Demonstrační příklad: množinové operace aplikované na cesty
16. Repositář s demonstračními příklady
17. Obsah následující části seriálu
1. Tvorba GUI v Pythonu s využitím frameworku PySide: použití cest při kreslení 2D scén
V dnešním článku o frameworku PySide se budeme zabývat popisem konceptu takzvaných cest (paths), protože z cest jsou složeny prakticky všechny složitěji tvarované dvourozměrné objekty, ať již se jedná o objekty otevřené (úsečky, polyčáry/lomené čáry, oblouky či křivky) či naopak objekty uzavřené (kruh, elipsa, obdélník, vyplněné mnohoúhelníky apod.). Každá cesta se skládá z prakticky libovolného množství takzvaných segmentů (segments), přičemž je zajímavé, že jednotlivé segmenty na sebe mohou, ale také nemusí navazovat (cesta tedy může obsahovat „skoky“). Použití cest v 2D grafice samozřejmě není nic nového; spíš by se dalo říci, že se jedná o dlouhým časem prověřenou technologii použitou například v PostScriptu či v SVG (PostScript lze přitom chápat jako souborový formát, programovací jazyk a současně i vykreslovací knihovnu).
Obrázek 1: Příklad cesty vytvořené z několika úsečkových (lineárních) segmentů a s nastaveným režimem vyplňování.
2. Koncepty, na nichž je založen objekt typu QPainterPath
Cesty jsou ve frameworku PySide představovány instancemi třídy pojmenované QPainterPath. Jak jsme si již řekli v úvodní kapitole, může se každá cesta skládat z libovolného množství segmentů, přičemž segmentem může být úsečka, kvadratická Bézierova křivka, kubická Bézierova křivka, kruhový oblouk, eliptický oblouk, mnohoúhelník, text, elipsa či obdélník se zaoblenými hranami. Uzavřené segmenty cest tvoří takzvané podcesty (subpath); v případě potřeby je možné podcesty vytvořit programově pomocí metod moveTo a closeSubpath.
Pokud navíc máme vytvořeno několik cest, lze je pomocí množinových operací skládat a vytvořit tak novou cestu s využitím operace sjednocení, průniku, rozdílu a nepřímo také symetrické diference. Na vytvořenou cestu je také možné aplikovat vybranou lineární transformaci, což znamená, že celý 2D objekt lze snadno přesunout, otočit, zkosit nebo změnit jeho měřítko, a to bez nutnosti ručního přepočtu jednotlivých vrcholů. V dalších kapitolách se setkáme se všemi těmito koncepty.
Samotné vytvoření (prázdné) cesty a její následné vykreslení je snadné. Celý postup vypadá zhruba následovně:
# vytvoření objektu typu QPainter s předáním # reference na "pokreslovaný" objekt qp = QtGui.QPainter(self.image) # vytvoření cesty path = QtGui.QPainterPath() ... ... ... # vytvoření pera a nastavení barvy kreslení pen = QtGui.QPen(QtGui.QColor(255, 0, 0)) # kreslit se bude právě vytvořeným perem qPainter.setPen(pen) # vykreslení cesty qPainter.drawPath(path)
Obrázek 2: Cesta složená ze dvou kvadratických Bézierových křivek.
3. Základní příkazy určené pro vytvoření cesty
Třída QPainterPath programátorům nabízí poměrně velké množství příkazů určených pro definici segmentů cest. Jednotlivé typy segmentů budou podrobněji popsány v navazujících kapitolách, proto si zde pouze uveďme jednotlivé metody a jejich význam:
# | Metoda | Stručný popis |
---|---|---|
1 | closeSubpath | uzavření podcesty nakreslením úsečky do jejího počátečního vrcholu |
2 | moveTo | přesun aktivního bodu bez kreslení |
3 | lineTo | lineární segment (úsečka) |
4 | quadTo | kvadratická Bézierova křivka |
5 | cubicTo | kubická Bézierova křivka |
6 | arcTo | kruhový či eliptický oblouk |
7 | arcMoveTo | přesun po kruhovém či eliptickém oblouku |
8 | addEllipse | přidání podcesty ve tvaru elipsy |
9 | addRect | přidání podcesty ve tvaru obdélníku |
10 | addRoundRect | přidání podcesty ve tvaru obdélníku se zaoblenými hranami |
11 | addPolygon | přidání podcesty ve tvaru polygonu |
12 | addText | přidání podcesty složené z textu (křivek znaků) |
13 | addPath | přidání další cesty ke stávající cestě (propojení pomocí úsečky) |
14 | addRegion | přidání regionu (oblasti) do cesty. Viz druhá část článku |
Doplňme si nyní úryvek kódu ze druhé kapitoly o příkazy, které vykreslí domek jedním tahem. Pro tento účel nám poslouží metody moveTo (přesun do počátečního bodu kreslení) a lineTo (kreslení úsečkového segmentu):
# vytvoření objektu typu QPainter s předáním # reference na "pokreslovaný" objekt qp = QtGui.QPainter(self.image) # vytvoření cesty path = QtGui.QPainterPath() # nakreslení domku jedním tahem path.moveTo(100, 200) path.lineTo(200, 200) path.lineTo(100, 100) path.lineTo(100, 200) path.lineTo(200, 100) path.lineTo(100, 100) path.lineTo(150, 50) path.lineTo(200, 100) path.lineTo(200, 200) # vytvoření pera a nastavení barvy kreslení pen = QtGui.QPen(QtGui.QColor(255, 0, 0)) # kreslit se bude právě vytvořeným perem qPainter.setPen(pen) # vykreslení cesty qPainter.drawPath(path)
Obrázek 3: Domek vykreslený předchozím úryvkem kódu.
4. První demonstrační příklad: vytvoření a vykreslení jednoduché cesty složené z úsečkových segmentů
V dnešním prvním demonstračním příkladu je ukázán způsob vykreslení jednoduché cesty složené pouze z úsečkových segmentů. Z úseček je vykreslen domek jedním tahem (viz předchozí kapitolu), takže výsledek je podobný příkladu z předchozí části tohoto seriálu, v níž jsme si ukázali podobný příklad, v němž však byla použita „pouze“ polyčára (lomená čára), jejíž možnosti jsou menší než možnosti objektů typu cesta:
#!/usr/bin/env python # vim: set fileencoding=utf-8 import sys import math # import "jádra" frameworku Qt i modulu pro GUI from PySide import QtCore from PySide import QtGui # nastavení barvy kreslení (pera) na zadanou barvu def setColor(qPainter, color): # vytvoření pera a nastavení barvy kreslení pen = QtGui.QPen(QtGui.QColor(*color)) # kreslit se bude právě vytvořeným perem qPainter.setPen(pen) # vytvoření cesty def createPath(): path = QtGui.QPainterPath() path.moveTo(100, 200) path.lineTo(200, 200) path.lineTo(100, 100) path.lineTo(100, 200) path.lineTo(200, 100) path.lineTo(100, 100) path.lineTo(150, 50) path.lineTo(200, 100) path.lineTo(200, 200) return path # funkce pro vykreslení cesty zadanou barvou def drawPath(qPainter, color, path): setColor(qPainter, color) # vykreslení cesty qPainter.drawPath(path) # nový widget bude odvozen od obecného hlavního okna class MainWindow(QtGui.QMainWindow): # rozměry rastrového obrázku IMAGE_WIDTH = 300 IMAGE_HEIGHT = 250 def __init__(self): # zavoláme konstruktor předka super(MainWindow, self).__init__() self.prepareImage() # konfigurace GUI + přidání widgetu do okna self.prepareGUI() def prepareImage(self): # vytvoření instance třídy QImage self.image = QtGui.QImage(MainWindow.IMAGE_WIDTH, MainWindow.IMAGE_HEIGHT, QtGui.QImage.Format_RGB32) # vymazání obrázku self.image.fill(0) # vytvoření objektu typu QPainter s předáním # reference na "pokreslovaný" objekt qp = QtGui.QPainter(self.image) # konstanty s n-ticemi představujícími základní barvy BLACK = (0, 0, 0) BLUE = (0, 0, 255) CYAN = (0, 255, 255) GREEN = (0, 255, 0) YELLOW = (255, 255, 0) RED = (255, 0, 0) MAGENTA = (255, 0, 255) WHITE = (255, 255, 255) # vytvoření cesty path = createPath() # vykreslení cesty drawPath(qp, YELLOW, path) # vytvoření instance třídy QPixmap z objektu QImage self.pixmap = QtGui.QPixmap.fromImage(self.image) def prepareGUI(self): # velikost okna nezadávejte ručně - špatně se počítá kvůli toolbaru # self.resize(256, 300) self.setWindowTitle('QPainter') # tlačítko Quit quitAction = QtGui.QAction(QtGui.QIcon('icons/application-exit.png'), '&Quit', self) quitAction.triggered.connect(self.close) quitAction.setStatusTip('Quit the application') quitAction.setShortcut('Ctrl+Q') # nástrojový pruh self.toolbar = self.addToolBar('title') self.toolbar.setMovable(False) # přidání tlačítka na nástrojový pruh self.toolbar.addAction(quitAction) # doprostřed okna přidáme návěští s rastrovým obrázkem self.addLabelWithPixmap() # zobrazení hlavního okna self.show() def addLabelWithPixmap(self): # vytvoření návěští label = QtGui.QLabel("test") # přiřazení rastrového obrázku k návěští label.setPixmap(self.pixmap) # vložení návěští do hlavního okna self.setCentralWidget(label) def run(self, app): # zobrazení okna na obrazovce self.show() # vstup do smyčky událostí (event loop) app.exec_() def main(): app = QtGui.QApplication(sys.argv) MainWindow().run(app) if __name__ == '__main__': main()
5. Kvadratické Bézierovy křivky
Při tvorbě cest jsou podporovány jak kvadratické, tak i kubické Bézierovy křivky, což je jen dobře, protože kvadratické křivky jsou často používány například při definici fontů zatímco křivky kubické najdeme například v mnoha vektorových grafických editorech a tím pádem i v souborech exportovaných z těchto nástrojů. Bézierovy kvadratické křivky jsou určeny pouze jedním řídicím bodem a dvojicí bodů kotvicích (koncových). Křivka prochází prvním a třetím bodem (kotvicí body), druhý bod (řídicí) určuje současně oba tečné vektory. Ukázka Bézierovy kvadratické křivky spolu s jejími určujícími body je zobrazena na čtvrtém obrázku:
Obrázek 4: Bézierova kvadratická křivka zadaná dvojicí kotvicích bodů a jedním bodem řídicím.
Následuje příklad cesty s jedinou Bézierovou kvadratickou křivkou. Povšimněte si nutnosti použít metody moveTo pro specifikaci počátečního bodu křivky:
# vytvoření cesty složené z jediné kvadratické Bézierovy křivky def createPath1(): path = QtGui.QPainterPath() path.moveTo(10, 100) path.quadTo(50, 10, 90, 100) return path
Hladké napojení Bézierových kvadratických křivek lze zajistit zadáním identického koncového bodu první křivky a počátečního bodu křivky druhé. Současně musí být shodné tečné vektory v těchto bodech, tj. prostřední (řídicí) body musí být středově symetrické okolo společného bodu obou křivek.
6. Kubické Bézierovy křivky
Kubické Bézierovy křivky většina uživatelů používajících vektorové grafické editory již pravděpodobně velmi dobře zná (ale nalezneme je i v rastrových grafických editorech, například v GIMPu). Připomeňme si tedy, že tyto křivky jsou definovány počátečním bodem, koncovým bodem a dvojicí řídicích bodů (těmi křivka obecně neprochází, tyto body však ovlivňují její tvar). Větší množství řídicích bodů dává uživatelům i větší možnosti tvarování křivky, protože je možné vytvořit i esíčko, smyčku atd. Ve frameworku PySide se tyto křivky (resp. segmenty složené z kubických Bézierových křivek) přidávají do cesty pomocí metody cubicTo, přičemž tato metoda očekává tři body popř.ךestici souřadnic, protože počáteční bod již známe – je jím dočasný poslední bod aktuálně vytvářené cesty.
Obrázek 5: Bézierova kubická křivka zadaná dvojicí kotvicích (koncových) bodů a dvojicí bodů řídicích.
Následuje příklad cesty s jedinou Bézierovou kubickou křivkou. Opět si povšimněte nutnosti použít metody moveTo pro specifikaci počátečního bodu křivky:
# vytvoření cesty složené z jediné kubické Bézierovy křivky def createPath(): path = QtGui.QPainterPath() path.moveTo(10, 100) path.quadTo(50, 10, 90, 100) return path
Obrázek 6: Rozdíl mezi kvadratickými a kubickými Bézierovými křivkami.
Poznámka: Bézierovy kubické křivky jsou v počítačové grafice velmi rozšířeny. Mezi jejich hlavní východy patří intuitivní zadávání a snadné hladké navazování křivek na sebe. Také výpočet bodů, které leží na křivce, je velmi jednoduchý a rychlý. Pomocí Bézierových kubických křivek však nelze přesně modelovat kuželosečky, zejména kruh a elipsu, což omezuje použití těchto křivek v CAD systémech. Také nelze k obecné Bézierově kubice vytvořit offsetovou křivku, tj. křivku, která se od zadané křivky nachází v určité vzdálenosti. Toto omezení se teoreticky může projevit i v komerční grafice (doména programů typu „Illustrator“), ale je nutno říci, že chyba vzniklá použitím Bézierových křivek bývá velmi malá, mnohdy pod rozlišovací schopností lidského oka (opět však platí, že pro přesné CAD a CAM je nutné přijít s přesnějším řešením). Většinou postačí (automatické) rozdělení Bézierovy křivky, která má tvořit offsetovou cestu, na více částí, čímž se získá i větší množství řídicích bodů, se kterými je možné manipulovat. Tuto funkci však přímo v PySide nenajdeme.
7. Druhý demonstrační příklad: vytvoření cest složených z kvadratických a kubických Bézierových křivek
V dnešním druhém demonstračním příkladu je ukázán způsob použití výše zmíněných metod cubicTo a quadTo pro vytvoření několika cest složených z Bézierových kvadratických a kubických křivek. Po spuštění tohoto příkladu by se mělo zobrazit toto okno:
Obrázek 7: Okno s vykreslenými křivkami získanými druhým demonstračním příkladem.
Následuje výpis zdrojového kódu příkladu:
#!/usr/bin/env python # vim: set fileencoding=utf-8 import sys import math # import "jádra" frameworku Qt i modulu pro GUI from PySide import QtCore from PySide import QtGui # nastavení barvy kreslení (pera) na zadanou barvu def setColor(qPainter, color): # vytvoření pera a nastavení barvy kreslení pen = QtGui.QPen(QtGui.QColor(*color)) # kreslit se bude právě vytvořeným perem qPainter.setPen(pen) # vytvoření cesty složené z kvadratických křivek def createPath1(): path = QtGui.QPainterPath() path.moveTo(10, 100) path.quadTo(50, 10, 90, 100) path.quadTo(130, 190, 170, 100) path.quadTo(210, 10, 250, 100) path.quadTo(290, 190, 330, 100) return path # vytvoření cesty z kubických křivek def createPath2(): path = QtGui.QPainterPath() path.moveTo(10, 250) path.cubicTo(120, 100, -20, 100, 90, 250) return path # vytvoření cesty z kubických křivek def createPath3(): path = QtGui.QPainterPath() path.moveTo(150, 200) path.cubicTo(225, 50, 225, 350, 300, 200) return path # funkce pro vykreslení cesty zadanou barvou def drawPath(qPainter, color, path): # nastavení barvy setColor(qPainter, color) # vykreslení cesty qPainter.drawPath(path) # nový widget bude odvozen od obecného hlavního okna class MainWindow(QtGui.QMainWindow): # rozměry rastrového obrázku IMAGE_WIDTH = 340 IMAGE_HEIGHT = 300 def __init__(self): # zavoláme konstruktor předka super(MainWindow, self).__init__() self.prepareImage() # konfigurace GUI + přidání widgetu do okna self.prepareGUI() def prepareImage(self): # vytvoření instance třídy QImage self.image = QtGui.QImage(MainWindow.IMAGE_WIDTH, MainWindow.IMAGE_HEIGHT, QtGui.QImage.Format_RGB32) # vymazání obrázku self.image.fill(0) # vytvoření objektu typu QPainter s předáním # reference na "pokreslovaný" objekt qp = QtGui.QPainter(self.image) # konstanty s n-ticemi představujícími základní barvy BLACK = (0, 0, 0) BLUE = (0, 0, 255) CYAN = (0, 255, 255) GREEN = (0, 255, 0) YELLOW = (255, 255, 0) RED = (255, 0, 0) MAGENTA = (255, 0, 255) WHITE = (255, 255, 255) # vytvoření cesty path1 = createPath1() # vykreslení cesty drawPath(qp, GREEN, path1) # vytvoření cesty path2 = createPath2() # vykreslení cesty drawPath(qp, YELLOW, path2) # vytvoření cesty path3 = createPath3() # vykreslení cesty drawPath(qp, MAGENTA, path3) # vytvoření instance třídy QPixmap z objektu QImage self.pixmap = QtGui.QPixmap.fromImage(self.image) def prepareGUI(self): # velikost okna nezadávejte ručně - špatně se počítá kvůli toolbaru # self.resize(256, 300) self.setWindowTitle('QPainter') # tlačítko Quit quitAction = QtGui.QAction(QtGui.QIcon('icons/application-exit.png'), '&Quit', self) quitAction.triggered.connect(self.close) quitAction.setStatusTip('Quit the application') quitAction.setShortcut('Ctrl+Q') # nástrojový pruh self.toolbar = self.addToolBar('title') self.toolbar.setMovable(False) # přidání tlačítka na nástrojový pruh self.toolbar.addAction(quitAction) # doprostřed okna přidáme návěští s rastrovým obrázkem self.addLabelWithPixmap() # zobrazení hlavního okna self.show() def addLabelWithPixmap(self): # vytvoření návěští label = QtGui.QLabel("test") # přiřazení rastrového obrázku k návěští label.setPixmap(self.pixmap) # vložení návěští do hlavního okna self.setCentralWidget(label) def run(self, app): # zobrazení okna na obrazovce self.show() # vstup do smyčky událostí (event loop) app.exec_() def main(): app = QtGui.QApplication(sys.argv) MainWindow().run(app) if __name__ == '__main__': main()
8. Tvorba vyplněných cest s využitím konstantní barvy, gradientu nebo textury
Již z předchozích kapitol víme, že cesty mohou tvořit buď uzavřené tvary, nebo se jedná o cesty složené z liniových segmentů. Pokud při kresbě cesty vybereme neprůhledný (viditelný) štětec, budou uzavřené tvary tímto štětcem jednoduše vyplněny. U cesty tvořených liniovými segmenty také dojde k vyplnění, a to takovým způsobem, že se cesta (či jednotlivé izolované segmenty) automaticky uzavřou úsečkou spojující konec cesty s jejím začátkem. V takové situaci může dojít k tomu, že se některé hrany tvořící hranici cesty mohou protínat. Podívejme se na tři příklady ukazující, jakým způsobem k vyplnění cest dojde.
V prvním příkladu je cesta tvořena jen dvěma úsečkovými segmenty zobrazenými širší zelenou stopou:
path = QtGui.QPainterPath() path.moveTo(50, 50) path.lineTo(50, 150) path.lineTo(150, 150)
Výsledek vykreslení cesty vyplněné lineárním gradientem:
Obrázek 8: Cesta tvořená jen dvěma úsečkovými segmenty, která byla vyplněna lineárním gradientem.
Ve druhém příkladu je cesta opět otevřená a tvoří ji protínající se úsečky tvořící hvězdu (ovšem povšimněte si chybějící hrany):
# výpočet souřadnic n-tého vrcholu hvězdy def starVertex(cx, cy, radius, n): angle = math.radians(n*144) return cx + radius * math.sin(angle), cy - radius * math.cos(angle) path = QtGui.QPainterPath() path.moveTo(*starVertex(140, 140, 120, 0)) path.lineTo(*starVertex(140, 140, 120, 1)) path.lineTo(*starVertex(140, 140, 120, 2)) path.lineTo(*starVertex(140, 140, 120, 3)) path.lineTo(*starVertex(140, 140, 120, 4))
Výsledek vykreslení cesty vyplněné radiálním gradientem:
Obrázek 9: Cesta tvořená hranami hvězdy.
V posledním příkladu můžeme vidět použití segmentů tvořených Bézierovými křivkami. Jedna cesta je vyplněna texturou, další lineárním barevným přechodem a třetí přechodem radiálním:
# vytvoření cesty složené z kvadratických křivek def createPath1(): path = QtGui.QPainterPath() path.moveTo(10, 100) path.quadTo(50, 10, 90, 100) path.quadTo(130, 190, 170, 100) path.quadTo(210, 10, 250, 100) path.quadTo(290, 190, 330, 100) return path # vytvoření cesty z kubických křivek def createPath2(): path = QtGui.QPainterPath() path.moveTo(10, 250) path.cubicTo(120, 100, -20, 100, 90, 250) return path # vytvoření cesty z kubických křivek def createPath3(): path = QtGui.QPainterPath() path.moveTo(150, 200) path.cubicTo(225, 50, 225, 350, 300, 200) return path
Obrázek 10: Cesty tvořené Bézierovými křivkami.
9. Třetí demonstrační příklad: vykreslení vyplněných cest
Metody pro tvorbu vyplněných cest, které byly popsané v předchozí kapitole, použijeme ve třetím demonstračním příkladu, jehož zdrojový kód je vypsán níže:
#!/usr/bin/env python # vim: set fileencoding=utf-8 import sys import math # import "jádra" frameworku Qt i modulu pro GUI from PySide import QtCore from PySide import QtGui # vytvoření štětce z pixmapy def createBrushFromPixmap(filename): pixmap = QtGui.QPixmap(filename) return QtGui.QBrush(pixmap) # vytvoření štětce z gradientního přechodu def createBrushFromLinearGradient(color1, color2): gradient = QtGui.QLinearGradient(100, 100, 100, 140) gradient.setColorAt(0.2, QtGui.QColor(*color1)) gradient.setColorAt(1.0, QtGui.QColor(*color2)) gradient.setSpread(QtGui.QGradient.Spread.ReflectSpread) return QtGui.QBrush(gradient) # vytvoření štětce z gradientního přechodu def createBrushFromRadialGradient(color1, color2): gradient = QtGui.QRadialGradient(225, 200, 10) gradient.setColorAt(0.2, QtGui.QColor(*color1)) gradient.setColorAt(1.0, QtGui.QColor(*color2)) gradient.setSpread(QtGui.QGradient.Spread.ReflectSpread) return QtGui.QBrush(gradient) # nastavení barvy kreslení (pera) na zadanou barvu def setColor(qPainter, color): # vytvoření pera a nastavení barvy kreslení pen = QtGui.QPen(QtGui.QColor(*color)) # kreslit se bude právě vytvořeným perem qPainter.setPen(pen) # vytvoření cesty složené z kvadratických křivek def createPath1(): path = QtGui.QPainterPath() path.moveTo(10, 100) path.quadTo(50, 10, 90, 100) path.quadTo(130, 190, 170, 100) path.quadTo(210, 10, 250, 100) path.quadTo(290, 190, 330, 100) return path # vytvoření cesty z kubických křivek def createPath2(): path = QtGui.QPainterPath() path.moveTo(10, 250) path.cubicTo(120, 100, -20, 100, 90, 250) return path # vytvoření cesty z kubických křivek def createPath3(): path = QtGui.QPainterPath() path.moveTo(150, 200) path.cubicTo(225, 50, 225, 350, 300, 200) return path # funkce pro vykreslení cesty zadanou barvou a stylem štětce def drawPath(qPainter, color, brush, path): # nastavení barvy setColor(qPainter, color) # nastavení stylu štětce qPainter.setBrush(brush) # vykreslení cesty qPainter.drawPath(path) # nový widget bude odvozen od obecného hlavního okna class MainWindow(QtGui.QMainWindow): # rozměry rastrového obrázku IMAGE_WIDTH = 340 IMAGE_HEIGHT = 300 def __init__(self): # zavoláme konstruktor předka super(MainWindow, self).__init__() self.prepareImage() # konfigurace GUI + přidání widgetu do okna self.prepareGUI() def prepareImage(self): # vytvoření instance třídy QImage self.image = QtGui.QImage(MainWindow.IMAGE_WIDTH, MainWindow.IMAGE_HEIGHT, QtGui.QImage.Format_RGB32) # vymazání obrázku self.image.fill(0) # vytvoření objektu typu QPainter s předáním # reference na "pokreslovaný" objekt qp = QtGui.QPainter(self.image) # konstanty s n-ticemi představujícími základní barvy BLACK = (0, 0, 0) BLUE = (0, 0, 255) CYAN = (0, 255, 255) GREEN = (0, 255, 0) YELLOW = (255, 255, 0) RED = (255, 0, 0) MAGENTA = (255, 0, 255) WHITE = (255, 255, 255) # vytvoření štětců brush1 = createBrushFromLinearGradient(WHITE, BLUE) brush2 = createBrushFromPixmap("pixmaps/voronoi.png") brush3 = createBrushFromRadialGradient(YELLOW, BLACK) # vytvoření cesty path1 = createPath1() # vykreslení cesty drawPath(qp, GREEN, brush1, path1) # vytvoření cesty path2 = createPath2() # vykreslení cesty drawPath(qp, YELLOW, brush2, path2) # vytvoření cesty path3 = createPath3() # vykreslení cesty drawPath(qp, MAGENTA, brush3, path3) # vytvoření instance třídy QPixmap z objektu QImage self.pixmap = QtGui.QPixmap.fromImage(self.image) def prepareGUI(self): # velikost okna nezadávejte ručně - špatně se počítá kvůli toolbaru # self.resize(256, 300) self.setWindowTitle('QPainter') # tlačítko Quit quitAction = QtGui.QAction(QtGui.QIcon('icons/application-exit.png'), '&Quit', self) quitAction.triggered.connect(self.close) quitAction.setStatusTip('Quit the application') quitAction.setShortcut('Ctrl+Q') # nástrojový pruh self.toolbar = self.addToolBar('title') self.toolbar.setMovable(False) # přidání tlačítka na nástrojový pruh self.toolbar.addAction(quitAction) # doprostřed okna přidáme návěští s rastrovým obrázkem self.addLabelWithPixmap() # zobrazení hlavního okna self.show() def addLabelWithPixmap(self): # vytvoření návěští label = QtGui.QLabel("test") # přiřazení rastrového obrázku k návěští label.setPixmap(self.pixmap) # vložení návěští do hlavního okna self.setCentralWidget(label) def run(self, app): # zobrazení okna na obrazovce self.show() # vstup do smyčky událostí (event loop) app.exec_() def main(): app = QtGui.QApplication(sys.argv) MainWindow().run(app) if __name__ == '__main__': main()
10. Množinové operace aplikované na cesty
V případě, že máme vytvořeny dvě cesty, lze nad nimi aplikovat základní množinové operace sjednocení, průniku a rozdílu. Výsledkem těchto operací je nová cesta. Pro množinové operace se používají buď metody united, intersected a subtracted, nebo lze alternativně použít i přetížené operátory aplikovatelné na dvojici libovolných cest:
Operace | Metoda | Přetížený operátor | Alternativní operátor |
---|---|---|---|
sjednocení | united | |, |= | +, += |
průnik | intersected | &, &= | |
rozdíl | subtracted | -, -= |
Vzhledem k tomu, že výsledkem těchto operací je nová cesta, je možné operace snadno „zřetězit“, například takto:
path = path1 + path2 - path3 & path4
V těchto případech se aplikují běžná pravidla priorit operátorů!
Poznámka: pokud jsou použity operátory ve tvaru +=, -=, &= a |=, je nově vytvořená cesta uložena do prvního (tj. levého) operandu, což je u těchto operátorů očekávané chování.
Obrázek 11: Dvě překrývající se uzavřené cesty, na které budeme aplikovat množinové operace.
11. Sjednocení
Operaci sjednocení dvou cest je možné provést buď metodou united, nebo alternativně s využitím přetížených operátorů + a | (ty samozřejmě existují i ve variantách += a |=). Ukažme si použití operátoru | pro sjednocení dvou cest do cesty jediné:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # SJEDNOCENÍ # # vykreslení kombinace obou cest drawPath(qp, RED, 4, brush, path1 | path2)
Zcela stejný výsledek bude mít použití přetíženého operátoru +:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # SJEDNOCENÍ # # vykreslení kombinace obou cest drawPath(qp, RED, 4, brush, path1 + path2)
Použití metody united:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # SJEDNOCENÍ # # vykreslení kombinace obou cest drawPath(qp, RED, 4, brush, path1.united(path2))
Obrázek 12: Výsledek operace sjednocení dvou cest.
Poznámka: operace sjednocení realizovaná pomocí + či | je komutativní, takže výsledek nezáleží na pořadí operandů.
12. Rozdíl
Operace rozdílu, tj. „odečtení“ tvaru jedné cesty od cesty druhé, je realizována buď metodou subtracted, nebo přetíženým operátorem -. Tato operace pochopitelně není komutativní, neboť výsledkem path1 – path2 bude cesta odlišná od výsledku operace path2 – path1:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # ROZDÍL # vykreslení kombinace obou cest drawPath(qp, YELLOW, 4, brush, path1 - path2)
Použití metody subtracted:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # ROZDÍL # vykreslení kombinace obou cest drawPath(qp, YELLOW, 4, brush, path1.subtracted(path2))
Obrázek 13: Výsledek operace rozdílu dvou cest.
Poznámka: výsledkem této operace může být prázdná cesta, což si ostatně můžete sami otestovat: postačí, aby první cesta byla celá překrytá cestou druhou.
13. Průnik
Třetí přímo podporovanou množinovou operací je průnik, který lze realizovat metodou intersected nebo alternativně pomocí přetíženého operátoru & (ale už ne operátoru *, ten má odlišný význam!). Použití této komutativní operace je ukázáno na dalším úryvku kódu:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # PRŮNIK # vykreslení kombinace obou cest drawPath(qp, CYAN, 4, brush, path1 & path2)
Použití metody intersected:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # PRŮNIK # vykreslení kombinace obou cest drawPath(qp, CYAN, 4, brush, path1.intersected(path2))
Obrázek 14: Výsledek operace průniku dvou cest.
Poznámka: i výsledkem této operace může být prázdná cesta, podobně jako tomu bylo i u operace předchozí.
14. Symetrická diference
Zajímavé je, že poslední často používaná množinová operace – symetrická diference – není přímo podporována. Ve skutečnosti ji ale můžeme snadno nahradit již výše popsanými operacemi, a to například takto:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # SYMETRICKÁ DIFERENCE # vykreslení kombinace obou cest drawPath(qp, MAGENTA, 4, brush, (path1 | path2) - (path1 & path2))
Použití metod namísto přetížených operátorů vede k relativně špatně čitelnému kódu:
# vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # SYMETRICKÁ DIFERENCE # vykreslení kombinace obou cest drawPath(qp, MAGENTA, 4, brush, path1.united(path2).subtracted(path1.intersected(path2)))
Obrázek 15: Výsledek operace symetrické diference dvou cest.
15. Demonstrační příklad: množinové operace aplikované na cesty
V dalším, dnes již posledním demonstračním příkladu, jsou ukázány všechny výše popsané množinové operace aplikované na cesty:
Obrázek 16: Screenshot demonstračního příkladu s výsledky všech výše popsaných množinových operací nad cestami.
Zdrojový kód celého příkladu vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 import sys import math # import "jádra" frameworku Qt i modulu pro GUI from PySide import QtCore from PySide import QtGui # vytvoření štětce z pixmapy def createBrushFromPixmap(filename): pixmap = QtGui.QPixmap(filename) return QtGui.QBrush(pixmap) # nastavení barvy kreslení (pera) na zadanou barvu a šířku stopy def setColorAndWidth(qPainter, color, width): # vytvoření pera a nastavení barvy kreslení pen = QtGui.QPen(QtGui.QColor(*color)) pen.setWidth(width) # kreslit se bude právě vytvořeným perem qPainter.setPen(pen) # vytvoření cesty složené z obdélníku def createPath1(): path = QtGui.QPainterPath() path.addRect(10, 10, 200, 200) return path # vytvoření cesty složené z obdélníku def createPath2(): path = QtGui.QPainterPath() path.addRect(100, 100, 200, 200) return path # funkce pro vykreslení cesty zadanou barvou a stylem štětce def drawPath(qPainter, color, width, brush, path): # nastavení barvy setColorAndWidth(qPainter, color, width) # nastavení stylu štětce qPainter.setBrush(brush) # vykreslení cesty qPainter.drawPath(path) # nový widget bude odvozen od obecného hlavního okna class MainWindow(QtGui.QMainWindow): # rozměry rastrového obrázku IMAGE_WIDTH = 640 IMAGE_HEIGHT = 640 def __init__(self): # zavoláme konstruktor předka super(MainWindow, self).__init__() self.prepareImage() # konfigurace GUI + přidání widgetu do okna self.prepareGUI() def prepareImage(self): # vytvoření instance třídy QImage self.image = QtGui.QImage(MainWindow.IMAGE_WIDTH, MainWindow.IMAGE_HEIGHT, QtGui.QImage.Format_RGB32) # vymazání obrázku self.image.fill(0) # vytvoření objektu typu QPainter s předáním # reference na "pokreslovaný" objekt qp = QtGui.QPainter(self.image) # konstanty s n-ticemi představujícími základní barvy BLACK = (0, 0, 0) BLUE = (0, 0, 255) CYAN = (0, 255, 255) GREEN = (0, 255, 0) YELLOW = (255, 255, 0) RED = (255, 0, 0) MAGENTA = (255, 0, 255) WHITE = (255, 255, 255) # vytvoření štětce brush = createBrushFromPixmap("pixmaps/voronoi.png") # vytvoření první cesty path1 = createPath1() # vytvoření druhé cesty path2 = createPath2() # SJEDNOCENÍ # # vykreslení kombinace obou cest drawPath(qp, RED, 4, brush, path1 | path2) # posun path1.translate(320, 0) path2.translate(320, 0) # ROZDÍL # vykreslení kombinace obou cest drawPath(qp, YELLOW, 4, brush, path1 - path2) # posun path1.translate(-320, 320) path2.translate(-320, 320) # PRŮNIK # vykreslení kombinace obou cest drawPath(qp, CYAN, 4, brush, path1 & path2) # posun path1.translate(320, 0) path2.translate(320, 0) # SYMETRICKÁ DIFERENCE # vykreslení kombinace obou cest drawPath(qp, MAGENTA, 4, brush, (path1 | path2) - (path1 & path2)) # vytvoření instance třídy QPixmap z objektu QImage self.pixmap = QtGui.QPixmap.fromImage(self.image) def prepareGUI(self): # velikost okna nezadávejte ručně - špatně se počítá kvůli toolbaru # self.resize(256, 300) self.setWindowTitle('QPainter') # tlačítko Quit quitAction = QtGui.QAction(QtGui.QIcon('icons/application-exit.png'), '&Quit', self) quitAction.triggered.connect(self.close) quitAction.setStatusTip('Quit the application') quitAction.setShortcut('Ctrl+Q') # nástrojový pruh self.toolbar = self.addToolBar('title') self.toolbar.setMovable(False) # přidání tlačítka na nástrojový pruh self.toolbar.addAction(quitAction) # doprostřed okna přidáme návěští s rastrovým obrázkem self.addLabelWithPixmap() # zobrazení hlavního okna self.show() def addLabelWithPixmap(self): # vytvoření návěští label = QtGui.QLabel("test") # přiřazení rastrového obrázku k návěští label.setPixmap(self.pixmap) # vložení návěští do hlavního okna self.setCentralWidget(label) def run(self, app): # zobrazení okna na obrazovce self.show() # vstup do smyčky událostí (event loop) app.exec_() def main(): app = QtGui.QApplication(sys.argv) MainWindow().run(app) if __name__ == '__main__': main()
16. Repositář s demonstračními příklady
Zdrojové kódy všech pěti dnes popsaných demonstračních příkladů byly opět, podobně jako tomu bylo i v předchozích článcích, uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/presentations. Pokud nechcete klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
Příklad | Adresa |
---|---|
78_path_lineto.py | https://github.com/tisnik/presentations/blob/master/Python_GUI/PySide/78_path_lineto.py |
79_path_curves.py | https://github.com/tisnik/presentations/blob/master/Python_GUI/PySide/79_path_curves.py |
80_path_and_brush.py | https://github.com/tisnik/presentations/blob/master/Python_GUI/PySide/80_path_and_brush.py |
81_path_and_brush_star.py | https://github.com/tisnik/presentations/blob/master/Python_GUI/PySide/81_path_and_brush_star.py |
82_set_operations.py | https://github.com/tisnik/presentations/blob/master/Python_GUI/PySide/82_set_operations.py |
17. Obsah následující části seriálu
V následující části seriálu o tvorbě grafického uživatelského rozhraní s využitím frameworku PySide dokončíme téma tvorby 2D grafiky. Budeme se zabývat dvěma užitečnými technikami – použitím takzvaných oblastí (region) sloužících pro určení té části plochy, do níž má být provedeno vykreslování. Ve druhé části si pak ukážeme způsoby vzájemné kompozice dvourozměrných entit v z-ové rovině.
18. Odkazy na Internetu
- PySide 1.2.1 documentation
https://pyside.github.io/docs/pyside/index.html - QImage
https://pyside.github.io/docs/pyside/PySide/QtGui/QImage.html - QPixmap
https://pyside.github.io/docs/pyside/PySide/QtGui/QPixmap.html - QBitmap
https://pyside.github.io/docs/pyside/PySide/QtGui/QBitmap.html - QPaintDevice
https://pyside.github.io/docs/pyside/PySide/QtGui/QPaintDevice.html - QPicture
https://pyside.github.io/docs/pyside/PySide/QtGui/QPicture.html - QPainter
https://pyside.github.io/docs/pyside/PySide/QtGui/QPainter.html - QPainterPath
https://pyside.github.io/docs/pyside/PySide/QtGui/QPainterPath.html - QGradient
https://pyside.github.io/docs/pyside/PySide/QtGui/QGradient.html - QLinearGradient
https://pyside.github.io/docs/pyside/PySide/QtGui/QLinearGradient.html - QRadialGradient
https://pyside.github.io/docs/pyside/PySide/QtGui/QRadialGradient.html - Afinní zobrazení
https://cs.wikipedia.org/wiki/Afinn%C3%AD_zobrazen%C3%AD - Differences Between PySide and PyQt
https://wiki.qt.io/Differences_Between_PySide_and_PyQt - PySide 1.2.1 tutorials
https://pyside.github.io/docs/pyside/tutorials/index.html - PySide tutorial
http://zetcode.com/gui/pysidetutorial/ - Drawing in PySide
http://zetcode.com/gui/pysidetutorial/drawing/ - Qt Core
https://pyside.github.io/docs/pyside/PySide/QtCore/Qt.html - QLayout
https://pyside.github.io/docs/pyside/PySide/QtGui/QLayout.html - QStackedLayout
https://pyside.github.io/docs/pyside/PySide/QtGui/QStackedLayout.html - QFormLayout
https://pyside.github.io/docs/pyside/PySide/QtGui/QFormLayout.html - QBoxLayout
https://pyside.github.io/docs/pyside/PySide/QtGui/QBoxLayout.html - QHBoxLayout
https://pyside.github.io/docs/pyside/PySide/QtGui/QHBoxLayout.html - QVBoxLayout
https://pyside.github.io/docs/pyside/PySide/QtGui/QVBoxLayout.html - QGridLayout
https://pyside.github.io/docs/pyside/PySide/QtGui/QGridLayout.html - QAction
https://pyside.github.io/docs/pyside/PySide/QtGui/QAction.html - QMessageBox
https://pyside.github.io/docs/pyside/PySide/QtGui/QMessageBox.html - QListWidget
https://pyside.github.io/docs/pyside/PySide/QtGui/QListWidget.html - Signals & Slots
http://doc.qt.io/qt-4.8/signalsandslots.html - Signals and Slots in PySide
http://wiki.qt.io/Signals_and_Slots_in_PySide - Intro to PySide/PyQt: Basic Widgets and Hello, World!
http://www.pythoncentral.io/intro-to-pysidepyqt-basic-widgets-and-hello-world/ - QWidget
https://pyside.github.io/docs/pyside/PySide/QtGui/QWidget.html - QMainWindow
https://pyside.github.io/docs/pyside/PySide/QtGui/QMainWindow.html - QLabel
https://pyside.github.io/docs/pyside/PySide/QtGui/QLabel.html - QAbstractButton
https://pyside.github.io/docs/pyside/PySide/QtGui/QAbstractButton.html - QCheckBox
https://pyside.github.io/docs/pyside/PySide/QtGui/QCheckBox.html - QRadioButton
https://pyside.github.io/docs/pyside/PySide/QtGui/QRadioButton.html - QButtonGroup
https://pyside.github.io/docs/pyside/PySide/QtGui/QButtonGroup.html - QFrame
https://pyside.github.io/docs/pyside/PySide/QtGui/QFrame.html#PySide.QtGui.PySide.QtGui.QFrame - QFrame.frameStyle
https://pyside.github.io/docs/pyside/PySide/QtGui/QFrame.html#PySide.QtGui.PySide.QtGui.QFrame.frameStyle - Leo editor
http://leoeditor.com/ - IPython Qt Console aneb vylepšený pseudoterminál
https://mojefedora.cz/integrovana-vyvojova-prostredi-ve-fedore-ipython-a-ipython-notebook/#k06 - Vývojová prostředí ve Fedoře (4. díl)
https://mojefedora.cz/vyvojova-prostredi-ve-fedore-4-dil/ - Seriál Letní škola programovacího jazyka Logo
http://www.root.cz/serialy/letni-skola-programovaciho-jazyka-logo/ - Educational programming language
http://en.wikipedia.org/wiki/Educational_programming_language - Logo Tree Project:
http://www.elica.net/download/papers/LogoTreeProject.pdf - Hra Breakout napísaná v Tkinteri
https://www.root.cz/clanky/hra-breakout-napisana-v-tkinteri/ - Hra Snake naprogramovaná v Pythone s pomocou Tkinter
https://www.root.cz/clanky/hra-snake-naprogramovana-v-pythone-s-pomocou-tkinter/ - 24.1. turtle — Turtle graphics
https://docs.python.org/3.5/library/turtle.html#module-turtle - TkDND
http://freecode.com/projects/tkdnd - Python Tkinter Fonts
https://www.tutorialspoint.com/python/tk_fonts.htm - The Tkinter Canvas Widget
http://effbot.org/tkinterbook/canvas.htm - Ovládací prvek (Wikipedia)
https://cs.wikipedia.org/wiki/Ovl%C3%A1dac%C3%AD_prvek_%28po%C4%8D%C3%ADta%C4%8D%29 - Rezervovaná klíčová slova v Pythonu
https://docs.python.org/3/reference/lexical_analysis.html#keywords - TkDocs: Styles and Themes
http://www.tkdocs.com/tutorial/styles.html - Drawing in Tkinter
http://zetcode.com/gui/tkinter/drawing/ - Changing ttk widget text color (StackOverflow)
https://stackoverflow.com/questions/16240477/changing-ttk-widget-text-color - The Hitchhiker's Guide to Pyhton: GUI Applications
http://docs.python-guide.org/en/latest/scenarios/gui/ - 7 Top Python GUI Frameworks for 2017
http://insights.dice.com/2014/11/26/5-top-python-guis-for-2015/ - GUI Programming in Python
https://wiki.python.org/moin/GuiProgramming - Cameron Laird's personal notes on Python GUIs
http://phaseit.net/claird/comp.lang.python/python_GUI.html - Python GUI development
http://pythoncentral.io/introduction-python-gui-development/ - Graphic User Interface FAQ
https://docs.python.org/2/faq/gui.html#graphic-user-interface-faq - TkInter
https://wiki.python.org/moin/TkInter - Tkinter 8.5 reference: a GUI for Python
http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/index.html - TkInter (Wikipedia)
https://en.wikipedia.org/wiki/Tkinter - appJar
http://appjar.info/ - appJar (Wikipedia)
https://en.wikipedia.org/wiki/AppJar - appJar na Pythonhosted
http://pythonhosted.org/appJar/ - appJar widgets
http://appjar.info/pythonWidgets/ - Stránky projektu PyGTK
http://www.pygtk.org/ - PyGTK (Wikipedia)
https://cs.wikipedia.org/wiki/PyGTK - Stránky projektu PyGObject
https://wiki.gnome.org/Projects/PyGObject - Stránky projektu Kivy
https://kivy.org/#home - Stránky projektu PyQt
https://riverbankcomputing.com/software/pyqt/intro - PyQt (Wikipedia)
https://cs.wikipedia.org/wiki/PyGTK - Stránky projektu PySide
https://wiki.qt.io/PySide - PySide (Wikipedia)
https://en.wikipedia.org/wiki/PySide - Stránky projektu Kivy
https://kivy.org/#home - Kivy (framework, Wikipedia)
https://en.wikipedia.org/wiki/Kivy_(framework) - QML Applications
http://doc.qt.io/qt-5/qmlapplications.html - KDE
https://www.kde.org/ - Qt
https://www.qt.io/ - GNOME
https://en.wikipedia.org/wiki/GNOME - Category:Software that uses PyGTK
https://en.wikipedia.org/wiki/Category:Software_that_uses_PyGTK - Category:Software that uses PyGObject
https://en.wikipedia.org/wiki/Category:Software_that_uses_PyGObject - Category:Software that uses wxWidgets
https://en.wikipedia.org/wiki/Category:Software_that_uses_wxWidgets - GIO
https://developer.gnome.org/gio/stable/ - GStreamer
https://gstreamer.freedesktop.org/ - GStreamer (Wikipedia)
https://en.wikipedia.org/wiki/GStreamer - Wax Gui Toolkit
https://wiki.python.org/moin/Wax - Python Imaging Library (PIL)
http://infohost.nmt.edu/tcc/help/pubs/pil/ - Why Pyjamas Isn’t a Good Framework for Web Apps (blogpost z roku 2012)
http://blog.pyjeon.com/2012/07/29/why-pyjamas-isnt-a-good-framework-for-web-apps/