Hlavní navigace

Programování pro X Window System (8)

20. 5. 2004
Doba čtení: 8 minut

Sdílet

V dalším článku věnovaném toolkitu Qt si ukážeme, jak jednoduše vytvořit nový typ widgetů. Podíváme se, jak navzájem spolupracují utility tvořící součást Qt. Dále probereme mechanismus přidělování pozice a velikosti widgetům a skončíme kreslením.

Definice nové třídy widgetů

Vytvořit nový typ widgetu je v toolkitu Qt velice jednoduché. Jako základ vybereme třídu, která má vlastnosti co nejbližší tomu, čeho chceme dosáhnout. Jako příklad si ukážeme widget LCDRange z tutoriálu Qt (v dokumentaci Qt je to Tutorial #1, kap. 7). Program se skládá ze tří souborů: lcdrange.h (definice třídy

LCDRange), lcdrange.cpp (implementace metod třídy LCDRange) a main.cpp (hlavní program používající třídu LCDRange). Widget LCDRange se skládá ze zobrazení čísla ve stylu LCD displeje (QLCDNumber) a z šoupátka

QSlider, kterým se nastavuje zobrazená hodnota, viz obr. 1.

LCDRange
Obr. 1: Widget LCDRange

Náš nový widget obsahuje své dvě komponenty pod sebou, proto bude odvozen z vertikálního boxu QVBox (funguje podobně jako GtkVBox). Na začátku souboru lcdrange.h musíme načíst příslušný hlavičkový soubor. Ve třídě LCDRange nebudeme potřebovat přímo objekt QSlider, ale pouze ukazatel na něj. Proto stačí pouze deklarovat jméno třídy. Tím, že se neincluduje hlavičkový soubor qslider.h, ušetříme čas při překladu.

#include <qvbox.h>
class QSlider;

Aby Qt správně rozpoznalo náš widget jako nový typ, musíme na začátku těla třídy použít makro Q_OBJECT. Tím se deklarují pomocné metody pro typový systém Qt a zároveň se řekne, že tento zdrojový soubor se bude zpracovávat pomocí utility moc.

class LCDRange : public QVBox {
  Q_OBJECT

Následně deklarujeme jednotlivé metody a datové položky třídy včetně signálů a slotů. Sloty můžeme deklarovat jako public, protected, nebo private, signály jsou vždy protected. Makro slots expanduje na prázdný řetězec, makro signals má hodnotuprotected. Tato makra informují program moc, že má určité metody ošetřit speciálním způsobem, a zároveň zajistí, že při překladu dostaneme po průchodu preprocesorem korektní zdrojový text v C++.

public:
  LCDRange(QWidget *parent = 0, const char *name = 0);
  int value() const;
public slots:
  void setValue(int);
signals:
  void valueChanged(int);
private:
  QSlider *slider;
};

V souboru lcdrange.cpp definujeme jednotlivé metody. Konstruktor vytvoří dva synovské widgety – LCD číslo a slider. Spojením signáluvalueChanged slideru se slotem display LCD čísla zajistíme změnu zobrazené hodnoty při posunu slideru. Druhé volání connect nastaví, že signál QSlider::valu­eChanged způsobí vygenerování signálu LCDRange::valu­eChanged. Uživatel našeho widgetu LCDRange má možnost dozvědět se prostřednictvím tohoto signálu o změně hodnoty. Nemusí přitom vůbec vědět, že widget má ve skutečnosti dva synovské widgety a signál pochází od jednoho z nich.

#include "lcdrange.h"
#include <qslider.h>
#include <qlcdnumber.h>

LCDRange::LCDRange(QWidget *parent, const char *name) : QVBox(parent, name)
{
  QLCDNumber *lcd = new QLCDNumber(2, this, "lcd");
  slider = new QSlider(Horizontal, this, "slider");
  slider->setRange(0, 99);
  slider->setValue(0);
  connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
  connect(slider, SIGNAL(valueChanged(int)), SIGNAL(valueChanged(int)));
} 

Dál už zbývá pouze implementovat metodu value a slot setValue. Tělo signálu valueChanged nedefinujeme. O správnou definici, která bude volat připojené handlery, se postará program moc.

int LCDRange::value() const
{
  return slider->value();
}

void LCDRange::setValue(int value)
{
  slider->setValue(value);
}

Každý hlavičkový soubor, jenž obsahuje definici třídy odvozené z QObject, je nutné zpracovat pomocí nástroje Meta Object Compiler (moc). Aby moc třídu zpracoval, musí v ní být použito makro Q_OBJECT. Dále může obsahovat deklarace signálů, slotů a properties (hodnot přístupných pomocí řetězcového klíče). Meta Object Compiler vygeneruje pro každou tříduXYZ soubor moc_XYZ.cpp. V něm se nacházejí definice metod implementujících signály a také definice meta-objektu. Každá třída má svůj meta-objekt typu QMetaObject přístupný prostřednictvím virtuální metody QObject::meta­Object. Meta-objekt popisuje vlastnosti třídy: jméno třídy, jméno a odkaz na meta-objekt bázové třídy, dále seznamy jmen a typů slotů, signálů a properties. Informace z meta-objektu používá např. Qt Designer nebo skriptovací engine QSA.

Postup při překladu programu

Obr. 2 ukazuje, jak probíhá překlad programu, od zdrojových textů až k výslednému spustitelnému souboru. Příklad vychází ze stejného ukázkového programu, na jakém jsme si ukázali definici nové třídy v první části článku.

překlad programu
Obr. 2: Postup překladu programu

Na začátku máme v adresáři pouze soubory se zdrojovými texty (main.cc, lcdrange.h, lcdrange.cc). Pří­kaz

qmake -project

pro ně vytvoří projektový soubor a další spuštění qmake vygeneruje Makefile obsahující pravidla pro volání kompilátoru, linkeru a Meta Object Compileru. Příkaz make nejprve z hlavičkového souboru lcdrange.h vytvoří moc_lcdrange.cpp pomocí utilitymoc. Pak už se spustí kompilátor a linker.

Alternativně můžeme použít nástroj Qt Designer pro grafický návrh aplikace. Designer vytváří pro každý formulář (tj. top-level okno) popis vzhledu okna obsahující rozložení a vlastnosti widgetů. Definici formuláře ukládá do souboru OKNO.ui ve formátu XML. Přímo v designeru lze definovat i chování widgetů tím, že Qt Designer definuje pro každé okno samostatnou třídu a programátor doplní těla jednotlivých slotů. Designer pak definice slotů exportuje do souboru OKNO.ui.h. Vygenerovat se dá i standardní soubor main.cc s funkcí main, která zobrazí hlavní okno a spustí zpracování událostí. Qt Designer vygeneruje také projektový soubor pro qmake, v němž je řečeno, že v Makefile má být volání utility uic (User Interface Compiler). Ta z XML souboru OKNO.ui vytvoří souboryOKNO.h a OKNO.cpp. Další postup je stejný jako bez použití designeru. Na obr. 3 je příklad založený na ukázkovém programu z tutoriálu pro Qt Designer.

Qt Designer
Obr. 3: Překlad aplikace vytvořené v Qt Designeru

Automatické umísťování widgetů

Podobně jako v GTK+, i v Qt se pozice a velikosti widgetů nastavují automaticky. Každý widget má doporučenou velikost, kterou vrací metoda QWidget::sizeHint. Navíc má ještě metodu QWidget::mini­mumSizeHint určující minimální velikost widgetu. Metoda QWidget::sizePolicy definuje, zda má widget vždy velikost sizeHint, nebo se může zvětšovat či zmenšovat. Přidělování velikosti probíhá podobně jako v GTK+. Nejprve se průchodem stromu widgetů zjistí požadavky jednotlivých widgetů na velikost. U widgetu, jenž má potomky, závisí jeho velikost na jeho vlastních potřebách a na požadavcích potomků. Následně dostane top-level okno ve spolupráci s window managerem přidělené místo, které se pak rozděluje mezi potomky ve stromě widgetů.

Nejjednodušší způsob, jak do rodičovského widgetu rozmístit synovské widgety, je použití některého layout widgetu. Vybírat lze ze tří typů: QHBox (synovské widgety seřazené v řádku zleva doprava), QVBox (synovské widgety seřazené ve sloupci shora dolů) a QGrid (synovské widgety ve dvojrozměrné mřížce). Layout widgety fungují podobně jako v GTK+ kontejnery GtkHBox, GtkVBox aGtkTable. V konstruktoru QGrid se zadává počet sloupců. Jednotlivé synovské widgety se vkládají vedle sebe, po zaplnění nastaveného počtu sloupců se pokračuje v dalším řádku. Všechny tři typy layout widgetů jsou odvozené z QFrame, takže umí kolem vložených widgetů kreslit rámeček. Když potřebujeme nadefinovat složitější rozložení widgetů, dají se layout widgety vnořovat. Na obr. 4 jsou všechny tři typy layout widgetů. Příslušný zdrojový kód je layout_widget­s.cpp.

layout widgety
Obr. 4: Layout widgety QHBox, QVBox a QGrid

Pokud potřebujeme větší kontrolu nad rozmístěním widgetů, můžeme použít některý z potomků z třídy QLayout. K dispozici jsou QBoxLayout, QHBoxLayout, QVBoxLayout a QGridLayout. Tyto layouty však nejsou widgety. Jsou to objekty, které se vloží do widgetu a řídí rozmístění jeho synovských widgetů. V rámci jednoho widgetu je možné layouty vnořovat. Pokud chceme do layoutu vložit widget, je třeba ho vytvořit tak, že za rodiče bude mít widget, který je vlastníkem layoutu. Následně widget vložíme do layoutu metodou add neboaddWidget.

QHBoxLayout *box = new QHBoxLayout(parent);
box->addWidget(new QLabel("1", parent));
box->addWidget(new QLabel("2", parent));

Alternativně můžeme nastavit voláním QLayout::setAu­toAdd, že všichni noví synové widgetu budou automaticky vloženi do layoutu. Tato metoda však funguje pouze pro top-level layout, tj. layout, jehož rodičem je widget a nikoliv jiný layout.

QHBoxLayout *box = new QHBoxLayout(widget);
box->setAutoAdd(TRUE);
new QLabel("1", widget);
new QLabel("2", widget);

Stejného vzhledu jako na obr. 4 lze s použitím vnořených layoutů místo layout widgetů dosáhnout programem layouts.cpp.

Kreslení

Widget dostane událost QPaintEvent, jestliže je potřeba ho celý nebo jeho část překreslit. Vlastní kreslení provádí handler QWidget::pain­tEvent. Metoda QPaintEvent::region slouží ke zjištění oblasti, která se má kreslit. Zevnitř programu lze kreslení iniciovat voláním QWidget::repaint nebo QWidget::update. Rozdíl mezi nimi je v tom, že repaint provede překreslení okamžitě přímým zavoláním paintEvent, kdežto update pouze vygeneruje událostQPaintEvent a vloží ji do fronty. Kreslení 2D grafiky se provádí pomocí třídy QPainter. Pro 3D grafiku je možné použít knihovnu OpenGL, přístupnou prostřednictvím widgetu QGLWidget.

Třída QPainter kreslí do QPaintDevice, resp. do některého potomka této třídy. Nejčastěji používaný je widget (QWidget). Další možnosti jsou: pixmapa (QPixmap, rastrový obrázek), tiskárna (QPrinter) a záznamník kreslících příkazů (QPicture). Objekt třídy QPicture si dokáže zapamatovat posloupnost kreslicích operací a následně tyto operace zopakovat na nějakém objektu QPainter. Případně lze celou posloupnost uložit do souboru v proprietárním formátu nebo ve formátu SVG.

Ve třídě QPainter jsou definovány funkce pro kreslení geometrických tvarů (čar, polygonů, kruhů, oblouků a Beziérových křivek), psaní textu a kreslení obrázků (kopírování dat z QPixmap, QImage nebo QPicture). Souřadnice se implicitně měří z levého horního rohu v pixelech doprava a dolů, ale soustavu souřadnic lze transformovat pomocí operací posunu, rotace, změny měřítka a zkosení. Je také možné přímo nastavit transformační matici ve tvaru

[m11 m12 0; m21 m22 0; dx dy 1]

Matice transformuje každý zadaný bod (x, y) na (m11x+m21y+dx, m12x+m22y+dy). Každý objekt, který se má nakreslit, je nejdřív transformován do souřadnic modelu pomocí transformační matice, následně se provede oříznutí do obdélníku QPainter::window a výsledek se umístí na QPaintDevicetak, že window (v souřadnicích modelu) se ztotožní s obdélníkem QPainter::viewport (v souřadnicích zařízení). QPainter obsahuje také parametry kreslení, jako je barva a tloušťka čar, barva a vzorek pro vyplňování nebo font.

root_podpora

Typická metoda paintEvent vypadá tak, že na začátku se vytvoří painter, nastaví se parametry, provedou se kreslicí operace a na konci se painter automaticky zruší. Následující příklad, opět převzatý z tutoriálu Qt, nakreslí to, co je ve žlutém obdélníku na obr. 5.

void CannonField::paintEvent(QPaintEvent *)
{
  QPainter p(this);

  p.setBrush(blue);
  p.setPen(NoPen);

  p.translate(0, rect().bottom());
  p.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16);
  p.rotate(-ang);
  p.drawRect(QRect(33, -4, 15, 8));
}

příklad kreslení
Obr. 5: Příklad kreslení

Byl pro vás článek přínosný?