Složitější ovládací prvky ve frameworku PySide: tabulky a stromy

13. 2. 2018
Doba čtení: 26 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Dnes si popíšeme složitější ovládací prvky, které jsou však v aplikacích poměrně často používány. Jedná se o tabulky představované třídou QTableWidget a o stromy, které naopak vytvoříme z třídy QTreeWidget.

Obsah

1. Složitější ovládací prvky ve frameworku PySide: tabulky a stromy

2. Ovládací prvek typu QTableWidget aneb konfigurovatelná tabulka

3. První demonstrační příklad: jednoduchá tabulka s rozměry 5×5 buněk

4. Druhý demonstrační příklad: alternativní nastavení rozměrů tabulky

5. Programová a manuální modifikace buněk tabulky

6. Bublinová nápověda přiřazená k jednotlivým buňkám tabulky

7. Třetí demonstrační příklad: nastavení obsahu jednotlivých buněk tabulky

8. Reakce na události vznikající při práci s tabulkou

9. Čtvrtý demonstrační příklad: reakce na základní typy událostí

10. Zpracování událostí při kliknutí na titulky sloupců nebo řádků

11. Pátý demonstrační příklad: reakce na kliknutí na titulky sloupců nebo řádků

12. Ovládací prvek typu QTreeWidget aneb strom

13. Vytvoření prázdného stromu a specifikace popisu sloupců

14. Vložení prvků typu QTreeWidgetItem do stromu

15. Šestý demonstrační příklad: strom, jehož prvky mají pouze jednu hodnotu

16. Obsah následující části seriálu

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

18. Odkazy na Internetu

1. Složitější ovládací prvky ve frameworku PySide: tabulky a stromy

Kromě již popsaných relativně jednoduchých ovládacích prvků nabízí framework PySide programátorům (a nepřímo i uživatelům) několik složitějších widgetů. V první řadě se jedná o widget QTextEdit, s nímž jsme se seznámili minule a k jehož podrobnějšímu popisu se v tomto seriálu ještě vrátíme. Ovšem existují i další složitější a přitom velmi užitečné a často využívané ovládací prvky.

Obrázek 1: S widgetem zobrazujícím tabulku jsme se již setkali při popisu knihovny appJar.

Mezi ně patří především tabulka a strom. Tabulku je možné vytvořit s využitím ovládacího prvku nazvaného QTableWidget (popř. odvozením nové třídy od QTableWidget), strom je představován prvkem pojmenovaným QTreeWidget. Dnes si ukážeme základy použití těchto dvou typů widgetů, ovšem pro jejich plné využití je nutné použít model MVC (model-view-controller), v němž tabulka či strom slouží jako pohled na data.

Obrázek 2: Knihovna appJar obsahuje i podporu pro widget zobrazující stromovou datovou strukturu, i když se nejedná o tak dobře konfigurovatelný ovládací prvek, jako je tomu v případě frameworku PySide a widgetu QTreeWidget.

2. Ovládací prvek typu QTableWidget aneb konfigurovatelná tabulka

Prvním widgetem, s nímž se v dnešním článku setkáme, je ovládací prvek určený pro zobrazení tabulky s měnitelným obsahem. Tento ovládací prvek se jmenuje příhodně QTableWidget. Ve výchozím nastavení je zobrazena tabulka obsahující titulky sloupců a řádků, což vlastně není nic nového, protože stejnou funkcionalitu nabízí všechny tabulkové procesory. A podobně jako u tabulkových procesorů je možné myší (nebo i programově) vybírat jednotlivé buňky, vybírat celé sloupce nebo řádky, editovat obsah buněk, měnit šířku řádků a výšku sloupců apod.

Obrázek 3: Změna šířky sloupce a výšky řádku.

Tabulka navíc ve chvíli, kdy se její obsah nevejde do vybrané plochy okna (či jiného kontejneru), automaticky zobrazí scrollbary, což je užitečné, protože se o tuto funkcionalitu nemusí starat programátor. Kromě toho se při práci s tabulkou může generovat množství různých signálů, na které lze programově reagovat a například setřídit tabulku podle toho sloupce, na který uživatel klikl.

Obrázek 4: Tabulka se zobrazenými scrollbary.

3. První demonstrační příklad: jednoduchá tabulka s rozměry 5×5 buněk

V dnešním prvním demonstračním příkladu je ukázáno, jakým způsobem je možné vytvořit jednoduchou tabulku a následně ji vložit do okna aplikace. Samotné vytvoření tabulky je triviální, protože se o vše podstatné postará konstruktor QTableWidget volaný z metody prepareTable. Tomuto konstruktoru předáme rozměry tabulky a taktéž referenci na kontejner, do kterého má být tabulka vložena:

def prepareTable(self):
    # vytvoření tabulky
    return QtGui.QTableWidget(5, 5, self)

Umístění tabulky do okna zajistí nám již známý správce geometrie:

table = self.prepareTable()
quitButton = self.prepareQuitButton()
 
# vytvoření správce geometrie
layout = QtGui.QVBoxLayout()
 
# umístění widgetů do okna
layout.addWidget(table)
layout.addWidget(quitButton)
 
# nastavení správce geometrie a vložení všech komponent do okna
self.setLayout(layout)

Výsledkem by mělo být toto okno:

Obrázek 5: Screenshot prvního demonstračního příkladu.

Následuje výpis zdrojového kódu prvního demonstračního příkladu:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindowContent(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindowContent, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # vytvoření widgetů vkládaných do okna
        table = self.prepareTable()
        quitButton = self.prepareQuitButton()
 
        # vytvoření správce geometrie
        layout = QtGui.QVBoxLayout()
 
        # umístění widgetů do okna
        layout.addWidget(table)
        layout.addWidget(quitButton)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
    def prepareQuitButton(self):
        # tlačítko
        quitButton = QtGui.QPushButton('Quit', self)
        quitButton.resize(quitButton.sizeHint())
 
        # navázání akce na signál
        quitButton.clicked.connect(QtCore.QCoreApplication.instance().quit)
        return quitButton
 
    def prepareTable(self):
        # vytvoření tabulky
        return QtGui.QTableWidget(5, 5, self)
 
 
# nový widget bude odvozen od obecného hlavního okna
class MainWindow(QtGui.QMainWindow):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(450, 300)
        self.setWindowTitle("QTableWidget")
 
        # vložení komponenty do okna
        self.setCentralWidget(MainWindowContent())
 
    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()

4. Druhý demonstrační příklad: alternativní nastavení rozměrů tabulky

Druhý demonstrační příklad si popíšeme pouze velmi stručně, protože se od příkladu prvního odlišuje jen v tom, jakým způsobem se specifikují rozměry tabulky. Namísto volání konstruktoru s předáním rozměrů tabulky:

table = QtGui.QTableWidget(5, 5, self)

se nejprve tabulka vytvoří a teprve poté se explicitně nastaví počet řádků a sloupců:

table = QtGui.QTableWidget(self)
table.setColumnCount(5)
table.setRowCount(5)

Výsledek se nijak neliší od příkladu prvního:

Obrázek 6: Screenshot druhého demonstračního příkladu.

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

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindowContent(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindowContent, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # vytvoření widgetů vkládaných do okna
        table = self.prepareTable()
        quitButton = self.prepareQuitButton()
 
        # vytvoření správce geometrie
        layout = QtGui.QVBoxLayout()
 
        # umístění widgetů do okna
        layout.addWidget(table)
        layout.addWidget(quitButton)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
    def prepareQuitButton(self):
        # tlačítko
        quitButton = QtGui.QPushButton('Quit', self)
        quitButton.resize(quitButton.sizeHint())
 
        # navázání akce na signál
        quitButton.clicked.connect(QtCore.QCoreApplication.instance().quit)
        return quitButton
 
    def prepareTable(self):
        # vytvoření tabulky
        table = QtGui.QTableWidget(self)
        table.setColumnCount(5)
        table.setRowCount(5)
        return table
 
 
# nový widget bude odvozen od obecného hlavního okna
class MainWindow(QtGui.QMainWindow):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(450, 300)
        self.setWindowTitle("QTableWidget")
 
        # vložení komponenty do okna
        self.setCentralWidget(MainWindowContent())
 
    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. Programová a manuální modifikace buněk tabulky

Tabulka vytvořená s využitím frameworku PySide není v žádném případě pouze statickým prvkem grafického uživatelského rozhraní. Je tomu právě naopak, protože jednotlivé buňky tabulky je možné modifikovat, a to jak uživatelem, tak i programově. Modifikace prováděná uživatelem se používají snadno: dvojklikem na buňku, výběrem buňky a stiskem klávesy F2, popř. (což je nejjednodušší řešení) výběrem buňky následovaným libovolnou sekvencí alfanumerických znaků, které se ihned začnou do vybrané části tabulky vkládat.

Programová změna tabulky je založena na použití metody QTableWidget.setItem(). Této metodě je nutné předat index řádku a index sloupce (indexy se začínají počítat nuly) a taktéž referenci na objekt typu QTableWidgetItem. V nejjednodušším případě se tento objekt vytvoří konstruktorem, kterému se předá řetězec, jenž se má v tabulce zobrazit:

item = QtGui.QTableWidgetItem("pokus")
table.setItem(j, i, item)

6. Bublinová nápověda přiřazená k jednotlivým buňkám tabulky

Při přesunu kurzoru myši nad tabulku mohou jednotlivé buňky tabulky zobrazit bublinovou nápovědu. Ta se ve skutečnosti nastavuje velmi jednoduše metodou QTreeWidgetItem.setToolTip(). Podívejme se na příklad, který vše osvětlí:

item = QtGui.QTableWidgetItem("pokus")
item.setToolTip("toto je nápověda k buňce 'pokus'")
table.setItem(j, i, item)

Bublinovou nápovědu použijeme i ve třetím demonstračním příkladu, který je popsán v navazující kapitole.

Obrázek 7: Způsob zobrazení bublinové nápovědy.

7. Třetí demonstrační příklad: nastavení obsahu jednotlivých buněk tabulky

Výše popsaná metoda QTableWidget.setItem() je použita ve třetím příkladu pro vytvoření tabulky o rozměrech 10×10 buněk. Do této tabulky jsou programově vypsány hodnoty malé násobilky. Navíc se u každé hodnoty nastaví bublinová nápověda zmíněná v předchozí kapitole:

def prepareTable(self):
    # vytvoření tabulky
    table = QtGui.QTableWidget(self)
    table.setColumnCount(10)
    table.setRowCount(10)
 
    # naplnění tabulky
    for j in range(1, 11):
        for i in range(1, 11):
            item = QtGui.QTableWidgetItem(str(i*j))
            tooltip = u"výsledek součinu {x}×{y}".format(x=i, y=j)
            item.setToolTip(tooltip)
            table.setItem(j-1, i-1, item)
    return table

Obrázek 8: Screenshot třetího demonstračního příkladu.

Opět se podívejme na výpis zdrojového kódu dnešního třetího demonstračního příkladu:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindowContent(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindowContent, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # vytvoření widgetů vkládaných do okna
        table = self.prepareTable()
        quitButton = self.prepareQuitButton()
 
        # vytvoření správce geometrie
        layout = QtGui.QVBoxLayout()
 
        # umístění widgetů do okna
        layout.addWidget(table)
        layout.addWidget(quitButton)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
    def prepareQuitButton(self):
        # tlačítko
        quitButton = QtGui.QPushButton('Quit', self)
        quitButton.resize(quitButton.sizeHint())
 
        # navázání akce na signál
        quitButton.clicked.connect(QtCore.QCoreApplication.instance().quit)
        return quitButton
 
    def prepareTable(self):
        # vytvoření tabulky
        table = QtGui.QTableWidget(self)
        table.setColumnCount(10)
        table.setRowCount(10)
 
        # naplnění tabulky
        for j in range(1, 11):
            for i in range(1, 11):
                item = QtGui.QTableWidgetItem(str(i*j))
                tooltip = u"výsledek součinu {x}×{y}".format(x=i, y=j)
                item.setToolTip(tooltip)
                table.setItem(j-1, i-1, item)
        return table
 
 
# nový widget bude odvozen od obecného hlavního okna
class MainWindow(QtGui.QMainWindow):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(450, 300)
        self.setWindowTitle("QTableWidget")
 
        # vložení komponenty do okna
        self.setCentralWidget(MainWindowContent())
 
    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. Reakce na události vznikající při práci s tabulkou

Již ve druhé kapitole jsme si řekli, že se při práci s tabulkou generuje poměrně velké množství signálů, které je možné navázat na callback funkce či metody. Ve chvíli, kdy dojde k události reprezentované daným signálem, bude příslušná callback funkce/metoda zavolána a programátor tak může adekvátně zareagovat na akce prováděné uživatelem. Podívejme se na nejjednodušší a možná i nejčastěji používanou událost – výběr buňky v tabulce. Příslušný signál spojíme s callback metodou takto:

table.cellClicked.connect(self.onCellClicked)

Callback metoda musí v tomto případě akceptovat tři argumenty:

  1. Referenci na objekt (self, this). Kdyby se nejednalo o metodu, tento objekt by se nepoužil.
  2. Index řádku, v němž se nachází vybraná buňka.
  3. Index sloupce, v němž se nachází vybraná buňka.

Callback metoda tedy může vypadat takto:

def onCellClicked(self, row, column):
    message = u"kliknuto na buňku [{x}, {y}]".format(x=column, y=row)
    self.textEdit.appendPlainText(message)

Další typ události je nepatrně složitější. Tato událost vznikne ve chvíli, když se změní fokus vybrané buňky (ne její obsah, ale výběr):

table.currentCellChanged.connect(self.onCurrentCellChanged)

V tomto případě musí callback metoda akceptovat jak souřadnice původní buňky, tak i souřadnice nově vybrané (fokusované) buňky:

def onCurrentCellChanged(self, row1, column1, row2, column2):
    message = u"změna fokusu z buňky [{x2}, {y2}] na buňku [{x1}, {y1}]".format(
        x1=column1, y1=row1, x2=column2, y2=row2)
    self.textEdit.appendPlainText(message)

Mohlo by se zdát, že není nutné mít zaregistrované oba typy událostí, ovšem ve skutečnosti nemusí nastat situace, kdy při akcích prováděných uživatelem dojde k oběma typům událostí. Ostatně se podívejte na následující dvojici screenshotů:

Obrázek 9: Při výběru jednotlivých buněk se generují oba typy událostí.

Obrázek 10: Při výběru celých sloupců se generuje jen jeden typ události.

Další často používanou událostí je událost, která nastane ve chvíli, kdy uživatel změní obsah nějaké buňky tabulky:

table.cellChanged.connect(self.onCellChanged)

Handler této události musí (nezávisle na tom, zda se jedná o metodu či o obyčejnou funkci) akceptovat mj. i souřadnice modifikované buňky. Její nový obsah se získá snadno metodou QTableWidget.item(řádek, sloupec):

def onCellChanged(self, row, column):
    value = self.table.item(row, column).text()
    message = u"změna obsahu buňky [{x}, {y}] na hodnotu {v}".format(
        x=column, y=row, v=value)
    self.textEdit.appendPlainText(message)

9. Čtvrtý demonstrační příklad: reakce na základní typy událostí

V dnešním čtvrtém demonstračním příkladu je realizována programová reakce na základní typy událostí, které mohou při práci s tabulkou nastat. Pro všechny tři základní události, tj. pro signály cellClicked, currentCellChanged a cellChanged jsou zaregistrovány příslušné handlery, které po svém zavolání vypíšou informace do několikařádkového textového widgetu. Ten je vytvořen jednoduše (viz též předchozí část tohoto seriálu):

def prepareTextEdit(self):
    # víceřádkové vstupní textové pole
    textEdit = QtGui.QPlainTextEdit(self)
    textEdit.setReadOnly(True)
    return textEdit

Poznámka: vzhledem k tomu, že potřebujeme čistý textový výstup (bez dalšího formátování), je použit widget QPlainTextEdit a nikoli QTextEdit.

Obrázek 11: Screenshot čtvrtého demonstračního příkladu.

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

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindowContent(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindowContent, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # vytvoření widgetů vkládaných do okna
        self.table = self.prepareTable()
        self.textEdit = self.prepareTextEdit()
        quitButton = self.prepareQuitButton()
 
        # vytvoření správce geometrie
        layout = QtGui.QVBoxLayout()
 
        # umístění widgetů do okna
        layout.addWidget(self.table)
        layout.addWidget(self.textEdit)
        layout.addWidget(quitButton)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
    def prepareQuitButton(self):
        # tlačítko
        quitButton = QtGui.QPushButton('Quit', self)
        quitButton.resize(quitButton.sizeHint())
 
        # navázání akce na signál
        quitButton.clicked.connect(QtCore.QCoreApplication.instance().quit)
        return quitButton
 
    def prepareTextEdit(self):
        # víceřádkové vstupní textové pole
        textEdit = QtGui.QPlainTextEdit(self)
        textEdit.setReadOnly(True)
        return textEdit
 
    def prepareTable(self):
        # vytvoření tabulky
        table = QtGui.QTableWidget(self)
        table.setColumnCount(10)
        table.setRowCount(10)
 
        # naplnění tabulky
        for j in range(1, 11):
            for i in range(1, 11):
                item = QtGui.QTableWidgetItem(str(i*j))
                tooltip = u"výsledek součinu {x}×{y}".format(x=i, y=j)
                item.setToolTip(tooltip)
                table.setItem(j-1, i-1, item)
 
        # registrace callback funkcí
        table.cellClicked.connect(self.onCellClicked)
        table.currentCellChanged.connect(self.onCurrentCellChanged)
        table.cellChanged.connect(self.onCellChanged)
 
        return table
 
    def onCellClicked(self, row, column):
        message = u"kliknuto na buňku [{x}, {y}]".format(x=column, y=row)
        self.textEdit.appendPlainText(message)
 
    def onCurrentCellChanged(self, row1, column1, row2, column2):
        message = u"změna fokusu z buňky [{x2}, {y2}] na buňku [{x1}, {y1}]".format(
            x1=column1, y1=row1, x2=column2, y2=row2)
        self.textEdit.appendPlainText(message)
 
    def onCellChanged(self, row, column):
        value = self.table.item(row, column).text()
        message = u"změna obsahu buňky [{x}, {y}] na hodnotu {v}".format(
            x=column, y=row, v=value)
        self.textEdit.appendPlainText(message)
 
 
# nový widget bude odvozen od obecného hlavního okna
class MainWindow(QtGui.QMainWindow):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(450, 450)
        self.setWindowTitle("QTableWidget")
 
        # vložení komponenty do okna
        self.setCentralWidget(MainWindowContent())
 
    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. Zpracování událostí při kliknutí na titulky sloupců nebo řádků

Poměrně často se setkáme s nutností seřazení tabulky podle určitého sloupce. Tuto operaci je možné implementovat relativně jednoduše – postačí jen zareagovat na kliknutí na titulek daného sloupce. Nejprve tedy propojíme příslušný signál s handlerem:

table.horizontalHeader().sectionClicked.connect(self.onHorizontalHeaderClicked)

A následně můžeme napsat tělo handleru:

def onHorizontalHeaderClicked(self, c):
    message = u"vybrán sloupec {c}".format(c=c)
    self.textEdit.appendPlainText(message)

Podobným způsobem je možné reagovat na kliknutí na titulek jednotlivých řádků:

table.verticalHeader().sectionClicked.connect(self.onVerticalHeaderClicked)

Opět se podívejme, jak může vypadat tělo handleru této události:

def onVerticalHeaderClicked(self, r):
    message = u"vybrán řádek {r}".format(r=r)
    self.textEdit.appendPlainText(message)

11. Pátý demonstrační příklad: reakce na kliknutí na titulky sloupců nebo řádků

Pátý příklad vznikl rozšířením příkladu čtvrtého o dvě další callback funkce, které se zavolají ve chvíli, kdy uživatel klikne na titulek sloupce, popř. na titulek řádku. Callback funkce pouze vypíšou zprávu o výběru, ovšem v praxi by například mohlo dojít k seřazení dat podle vybraného sloupce atd.:

Obrázek 12: Výpis informace ve chvíli, kdy je vybrán sloupec, popř. řádek kliknutím na titulek.

Podívejme se na výpis zdrojového kódu dnešního pátého demonstračního příkladu:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindowContent(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindowContent, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # vytvoření widgetů vkládaných do okna
        self.table = self.prepareTable()
        self.textEdit = self.prepareTextEdit()
        quitButton = self.prepareQuitButton()
 
        # vytvoření správce geometrie
        layout = QtGui.QVBoxLayout()
 
        # umístění widgetů do okna
        layout.addWidget(self.table)
        layout.addWidget(self.textEdit)
        layout.addWidget(quitButton)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
    def prepareQuitButton(self):
        # tlačítko
        quitButton = QtGui.QPushButton('Quit', self)
        quitButton.resize(quitButton.sizeHint())
 
        # navázání akce na signál
        quitButton.clicked.connect(QtCore.QCoreApplication.instance().quit)
        return quitButton
 
    def prepareTextEdit(self):
        # víceřádkové vstupní textové pole
        textEdit = QtGui.QPlainTextEdit(self)
        textEdit.setReadOnly(True)
        return textEdit
 
    def prepareTable(self):
        # vytvoření tabulky
        table = QtGui.QTableWidget(self)
        table.setColumnCount(10)
        table.setRowCount(10)
 
        # naplnění tabulky
        for j in range(1, 11):
            for i in range(1, 11):
                item = QtGui.QTableWidgetItem(str(i*j))
                tooltip = u"výsledek součinu {x}×{y}".format(x=i, y=j)
                item.setToolTip(tooltip)
                table.setItem(j-1, i-1, item)
 
        # registrace callback funkcí
        table.cellClicked.connect(self.onCellClicked)
        table.currentCellChanged.connect(self.onCurrentCellChanged)
        table.cellChanged.connect(self.onCellChanged)
 
        # callback funkce při výběru sloupců nebo celých řádků
        table.horizontalHeader().sectionClicked.connect(self.onHorizontalHeaderClicked)
        table.verticalHeader().sectionClicked.connect(self.onVerticalHeaderClicked)
 
        return table
 
    def onCellClicked(self, row, column):
        message = u"kliknuto na buňku [{x}, {y}]".format(x=column, y=row)
        self.textEdit.appendPlainText(message)
 
    def onCurrentCellChanged(self, row1, column1, row2, column2):
        message = u"změna fokusu z buňky [{x2}, {y2}] na buňku [{x1}, {y1}]".format(
            x1=column1, y1=row1, x2=column2, y2=row2)
        self.textEdit.appendPlainText(message)
 
    def onCellChanged(self, row, column):
        value = self.table.item(row, column).text()
        message = u"změna obsahu buňky [{x}, {y}] na hodnotu {v}".format(
            x=column, y=row, v=value)
        self.textEdit.appendPlainText(message)
 
    def onHorizontalHeaderClicked(self, c):
        message = u"vybrán sloupec {c}".format(c=c)
        self.textEdit.appendPlainText(message)
 
    def onVerticalHeaderClicked(self, r):
        message = u"vybrán řádek {r}".format(r=r)
        self.textEdit.appendPlainText(message)
 
 
# nový widget bude odvozen od obecného hlavního okna
class MainWindow(QtGui.QMainWindow):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(450, 450)
        self.setWindowTitle("QTableWidget")
 
        # vložení komponenty do okna
        self.setCentralWidget(MainWindowContent())
 
    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()

12. Ovládací prvek typu QTreeWidget aneb strom

Druhým ovládacím prvkem, s nímž se v dnešním článku alespoň ve stručnosti seznámíme, je strom. Ten je představovaný třídou QTreeWidget nebo jejími potomky. S ovládacím prvkem, který dokáže zobrazit stromovou strukturu, jsme se již setkali při popisu knihovny appJar, v níž ovšem bylo možné použít jen jediný typ zdrojových dat – soubor ve formátu XML. Naproti tomu je widget typu QTreeWidget obecnější, neboť se v něm stromová struktura skládá z prvků typu QTreeWidgetItem, které opět mohou obsahovat další prvky téhož typu. V nejjednodušším případě jsou prvky představovány jednořádkovým textem, ovšem je možné použít i ikony popř. složitější struktury (například lze zobrazit několik sloupců s hodnotami).

Obrázek 13: Strom vytvořený z XML v knihovně appJar.

13. Vytvoření prázdného stromu a specifikace popisu sloupců

Vytvoření prázdného stromu je ještě jednodušší, než vytvoření prázdné tabulky, protože není nutné specifikovat žádné rozměry:

tree = QtGui.QTreeWidget(self)

Obrázek 14: Výchozí podoba stromu není příliš pěkná.

Implicitně se i v případě stromu zobrazuje titulkový pruh, který můžeme snadno upravit. Navíc můžeme (resp. většinou musíme) specifikovat počet sloupců. Následující dva příklady nastaví titulek zobrazený na pruhu a současně se specifikuje, že se má použít jen jediný sloupec:

tree.setHeaderLabel("strom")
tree.setColumnCount(1)

Obrázek 15: Nastavení nadpisu jediného sloupce.

14. Vložení prvků typu QTreeWidgetItem do stromu

Prvky vkládané do stromu jsou typu QTreeWidgetItem. V nejjednodušším případě mohou jednotlivé prvky reprezentovat pouhý text, i tak ale tento text musíme předávat jako jednoprvkový seznam, protože jsme si již v předchozí kapitole řekli, že se prvky stromu mohou zobrazit v několika sloupcích. Prvním parametrem konstruktoru je předek daného prvku:

items = []
for i in range(1, 11):
    item = QtGui.QTreeWidgetItem(None, ["prvek #{i}".format(i=i)])
    items.append(item)
tree.insertTopLevelItems(0, items)

Obrázek 16: Strom s několika prvky (uzly) na nejvyšší úrovni.

Relativně snadno můžeme vytvořit skutečnou stromovou strukturu, v níž je každý prvek na nejvyšší úrovni uzlem obsahujícím (například) tři další poduzly:

# vytvoření stromu
tree = QtGui.QTreeWidget(self)
tree.setHeaderLabel("strom")
tree.setColumnCount(1)
items = []
for i in range(1, 11):
    item = QtGui.QTreeWidgetItem(None, ["prvek #{i}".format(i=i)])
    items.append(item)
    QtGui.QTreeWidgetItem(item, ["podprvek A"])
    QtGui.QTreeWidgetItem(item, ["podprvek B"])
    QtGui.QTreeWidgetItem(item, ["podprvek C"])
tree.insertTopLevelItems(0, items)

Obrázek 17: Strom s uzly, z nichž každý obsahuje tři poduzly.

15. Šestý demonstrační příklad: strom, jehož prvky mají pouze jednu hodnotu

Předchozí úryvek kódu je použit v dnešním šestém a sedmém příkladu. Tyto příklady se od sebe liší jen jednou maličkostí – zda se pro každý uzel na nejvyšší úrovni vytváří i poduzly, či nikoli:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
import sys
 
# import "jádra" frameworku Qt i modulu pro GUI
from PySide import QtCore
from PySide import QtGui
 
 
# nový widget bude odvozen od obecného widgetu
class MainWindowContent(QtGui.QWidget):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindowContent, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        # vytvoření widgetů vkládaných do okna
        self.tree = self.prepareTree()
        quitButton = self.prepareQuitButton()
 
        # vytvoření správce geometrie
        layout = QtGui.QVBoxLayout()
 
        # umístění widgetů do okna
        layout.addWidget(self.tree)
        layout.addWidget(quitButton)
 
        # nastavení správce geometrie a vložení všech komponent do okna
        self.setLayout(layout)
 
    def prepareQuitButton(self):
        # tlačítko
        quitButton = QtGui.QPushButton('Quit', self)
        quitButton.resize(quitButton.sizeHint())
 
        # navázání akce na signál
        quitButton.clicked.connect(QtCore.QCoreApplication.instance().quit)
        return quitButton
 
    def prepareTree(self):
        # vytvoření stromu
        tree = QtGui.QTreeWidget(self)
        tree.setHeaderLabel("strom")
        tree.setColumnCount(1)
        items = []
        for i in range(1, 11):
            item = QtGui.QTreeWidgetItem(None, ["prvek #{i}".format(i=i)])
            items.append(item)
        tree.insertTopLevelItems(0, items)
 
        return tree
 
 
# nový widget bude odvozen od obecného hlavního okna
class MainWindow(QtGui.QMainWindow):
 
    def __init__(self):
        # zavoláme konstruktor předka
        super(MainWindow, self).__init__()
 
        # konfigurace GUI + přidání widgetu do okna
        self.prepareGUI()
 
    def prepareGUI(self):
        self.resize(450, 450)
        self.setWindowTitle("QTreeWidget")
 
        # vložení komponenty do okna
        self.setCentralWidget(MainWindowContent())
 
    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. Obsah následující části seriálu

V navazující části seriálu o tvorbě aplikací s grafickým uživatelským rozhraním v Pythonu se zaměříme na podrobnější popis tří složitějších widgetů, s nimiž jsme se setkali minule a dnes:

hacking_tip

  1. QTextEdit
  2. QTableWidget
  3. QTreeWidget

Nejvíce času budeme věnovat použití ovládacího prvku zobrazujícího stromové struktury, protože se tento prvek používá poměrně často a současně je příprava dat pro něj poněkud složitější.

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

Zdrojové kódy všech sedmi 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/pre­sentations. Pokud nechcete klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

18. Odkazy na Internetu

  1. PySide 1.2.1 documentation
    https://pyside.github.io/doc­s/pyside/index.html
  2. QImage
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QIma­ge.html
  3. QPixmap
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QPix­map.html
  4. QBitmap
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QBit­map.html
  5. QPaintDevice
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QPa­intDevice.html
  6. QPicture
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QPic­ture.html
  7. QPainter
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QPa­inter.html
  8. QPainterPath
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QPa­interPath.html
  9. QGradient
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QGra­dient.html
  10. QLinearGradient
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QLi­nearGradient.html
  11. QRadialGradient
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QRa­dialGradient.html
  12. QTableWidget
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QTa­bleWidget.html
  13. QTableWidgetItem
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QTa­bleWidgetItem.html
  14. QTreeWidget
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QTre­eWidget.html
  15. QTreeWidgetItem
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QTre­eWidgetItem.html
  16. Afinní zobrazení
    https://cs.wikipedia.org/wi­ki/Afinn%C3%AD_zobrazen%C3%AD
  17. Differences Between PySide and PyQt
    https://wiki.qt.io/Differen­ces_Between_PySide_and_PyQt
  18. PySide 1.2.1 tutorials
    https://pyside.github.io/doc­s/pyside/tutorials/index.html
  19. PySide tutorial
    http://zetcode.com/gui/py­sidetutorial/
  20. Drawing in PySide
    http://zetcode.com/gui/py­sidetutorial/drawing/
  21. Qt Core
    https://pyside.github.io/doc­s/pyside/PySide/QtCore/Qt­.html
  22. QLayout
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QLa­yout.html
  23. QValidator
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QVa­lidator.html
  24. QStackedLayout
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QStac­kedLayout.html
  25. QFormLayout
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QFor­mLayout.html
  26. QBoxLayout
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QBox­Layout.html
  27. QHBoxLayout
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QHBox­Layout.html
  28. QVBoxLayout
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QVBox­Layout.html
  29. QGridLayout
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QGrid­Layout.html
  30. QAction
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QAc­tion.html
  31. QMessageBox
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QMes­sageBox.html
  32. QListWidget
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QLis­tWidget.html
  33. Signals & Slots
    http://doc.qt.io/qt-4.8/signalsandslots.html
  34. Signals and Slots in PySide
    http://wiki.qt.io/Signals_an­d_Slots_in_PySide
  35. Intro to PySide/PyQt: Basic Widgets and Hello, World!
    http://www.pythoncentral.io/intro-to-pysidepyqt-basic-widgets-and-hello-world/
  36. QLineEdit
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QLi­neEdit.html
  37. QTextEdit
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QTex­tEdit.html
  38. QValidator
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QVa­lidator.html
  39. QIntValidator
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QIn­tValidator.html
  40. QRegExpValidator
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QRe­gExpValidator.html
  41. QWidget
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QWid­get.html
  42. QMainWindow
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QMa­inWindow.html
  43. QLabel
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QLa­bel.html
  44. QAbstractButton
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QAb­stractButton.html
  45. QCheckBox
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QChec­kBox.html
  46. QRadioButton
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QRa­dioButton.html
  47. QButtonGroup
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QBut­tonGroup.html
  48. QFrame
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QFra­me.html#PySide.QtGui.PySi­de.QtGui.QFrame
  49. QFrame.frameStyle
    https://pyside.github.io/doc­s/pyside/PySide/QtGui/QFra­me.html#PySide.QtGui.PySi­de.QtGui.QFrame.frameStyle
  50. Leo editor
    http://leoeditor.com/
  51. IPython Qt Console aneb vylepšený pseudoterminál
    https://mojefedora.cz/integrovana-vyvojova-prostredi-ve-fedore-ipython-a-ipython-notebook/#k06
  52. Vývojová prostředí ve Fedoře (4. díl)
    https://mojefedora.cz/vyvojova-prostredi-ve-fedore-4-dil/
  53. Seriál Letní škola programovacího jazyka Logo
    http://www.root.cz/serialy/letni-skola-programovaciho-jazyka-logo/
  54. Educational programming language
    http://en.wikipedia.org/wi­ki/Educational_programmin­g_language
  55. Logo Tree Project:
    http://www.elica.net/downlo­ad/papers/LogoTreeProject­.pdf
  56. Hra Breakout napísaná v Tkinteri
    https://www.root.cz/clanky/hra-breakout-napisana-v-tkinteri/
  57. Hra Snake naprogramovaná v Pythone s pomocou Tkinter
    https://www.root.cz/clanky/hra-snake-naprogramovana-v-pythone-s-pomocou-tkinter/
  58. 24.1. turtle — Turtle graphics
    https://docs.python.org/3­.5/library/turtle.html#mo­dule-turtle
  59. TkDND
    http://freecode.com/projects/tkdnd
  60. Python Tkinter Fonts
    https://www.tutorialspoin­t.com/python/tk_fonts.htm
  61. The Tkinter Canvas Widget
    http://effbot.org/tkinter­book/canvas.htm
  62. Ovládací prvek (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Ovl%C3%A1dac%C3%AD_prvek_­%28po%C4%8D%C3%ADta%C4%8D%29
  63. Rezervovaná klíčová slova v Pythonu
    https://docs.python.org/3/re­ference/lexical_analysis.html#ke­ywords
  64. TkDocs: Styles and Themes
    http://www.tkdocs.com/tuto­rial/styles.html
  65. Drawing in Tkinter
    http://zetcode.com/gui/tkin­ter/drawing/
  66. Changing ttk widget text color (StackOverflow)
    https://stackoverflow.com/qu­estions/16240477/changing-ttk-widget-text-color
  67. The Hitchhiker's Guide to Pyhton: GUI Applications
    http://docs.python-guide.org/en/latest/scenarios/gui/
  68. 7 Top Python GUI Frameworks for 2017
    http://insights.dice.com/2014/11/26/5-top-python-guis-for-2015/
  69. GUI Programming in Python
    https://wiki.python.org/mo­in/GuiProgramming
  70. Cameron Laird's personal notes on Python GUIs
    http://phaseit.net/claird/com­p.lang.python/python_GUI.html
  71. Python GUI development
    http://pythoncentral.io/introduction-python-gui-development/
  72. Graphic User Interface FAQ
    https://docs.python.org/2/faq/gu­i.html#graphic-user-interface-faq
  73. TkInter
    https://wiki.python.org/moin/TkInter
  74. Tkinter 8.5 reference: a GUI for Python
    http://infohost.nmt.edu/tcc/hel­p/pubs/tkinter/web/index.html
  75. TkInter (Wikipedia)
    https://en.wikipedia.org/wiki/Tkinter
  76. appJar
    http://appjar.info/
  77. appJar (Wikipedia)
    https://en.wikipedia.org/wiki/AppJar
  78. appJar na Pythonhosted
    http://pythonhosted.org/appJar/
  79. appJar widgets
    http://appjar.info/pythonWidgets/
  80. Stránky projektu PyGTK
    http://www.pygtk.org/
  81. PyGTK (Wikipedia)
    https://cs.wikipedia.org/wiki/PyGTK
  82. Stránky projektu PyGObject
    https://wiki.gnome.org/Pro­jects/PyGObject
  83. Stránky projektu Kivy
    https://kivy.org/#home
  84. Stránky projektu PyQt
    https://riverbankcomputin­g.com/software/pyqt/intro
  85. PyQt (Wikipedia)
    https://cs.wikipedia.org/wiki/PyGTK
  86. Stránky projektu PySide
    https://wiki.qt.io/PySide
  87. PySide (Wikipedia)
    https://en.wikipedia.org/wiki/PySide
  88. Stránky projektu Kivy
    https://kivy.org/#home
  89. Kivy (framework, Wikipedia)
    https://en.wikipedia.org/wi­ki/Kivy_(framework)
  90. QML Applications
    http://doc.qt.io/qt-5/qmlapplications.html
  91. KDE
    https://www.kde.org/
  92. Qt
    https://www.qt.io/
  93. GNOME
    https://en.wikipedia.org/wiki/GNOME
  94. Category:Software that uses PyGTK
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_PyGTK
  95. Category:Software that uses PyGObject
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_PyGObject
  96. Category:Software that uses wxWidgets
    https://en.wikipedia.org/wi­ki/Category:Software_that_u­ses_wxWidgets
  97. GIO
    https://developer.gnome.or­g/gio/stable/
  98. GStreamer
    https://gstreamer.freedesktop.org/
  99. GStreamer (Wikipedia)
    https://en.wikipedia.org/wi­ki/GStreamer
  100. Wax Gui Toolkit
    https://wiki.python.org/moin/Wax
  101. Python Imaging Library (PIL)
    http://infohost.nmt.edu/tcc/hel­p/pubs/pil/
  102. 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/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.