====== 2. Histogram ====== V [[https://youtu.be/eanD2nPrwl4|online přednášce]] jsme se podívali na program, který zpracovává soubor přesměrovaný na standardní vstup programu. U desktopových aplikací to není úplně obvyklé, proto upravíme program tak, aby umožňoval výběr zdrojového souboru za běhu pomocí dialogového okna. Kompletní kód je ke stažení v git repozitáři nebo ve formě balíčku: {{ :courses:b2b99ppc:solutions:ppc-sol02.zip |}} ===== Vykreslovací komponenta - Histogram ===== Základem je vykreslovací komponenta, která obsahuje vektor dat, metodu pro naplnění tohoto vektoru a metodu pro ošetření události [[https://doc.qt.io/qt-5/qwidget.html#paintEvent|QWidget::paintEvent()]]. Protože bude komponenta rozčleněna do více metod, je vhodné zařadit mezi atributy také velikost vykreslovací plochy, která bude inicializována před samotným vykreslením, stejně jako šířka jednoho sloupce pro vykreslení. class Histogram : public QWidget { Q_OBJECT QVector *data; int width, height; double barWidth; void drawBar (Painter *, int, int); void drawLabel (Painter *, int, QString); public: Histogram (QWidget *parent=0); void paintEvent(QPaintEvent *event); void push_back (int x) {data->push_back(x);} }; Konstruktor komponenty nemá na starosti nic jiného, než inicializovat vektor dat. Bylo by asi výhodné pro uložení dat použít datový typ [[https://doc.qt.io/qt-5/qvariant.html|QVariant]], ale pro naše účely celá čísla postačí. Histogram::Histogram (QWidget *parent) : QWidget (parent) { // prazny vektor cisel typu int data = new QVector(); } Klíčovou metodou je reimplementace [[https://doc.qt.io/qt-5/qwidget.html#paintEvent|QWidget::paintEvent()]]. Metoda naší komponenty musí mít stejný prototyp ve smyslu počtu a datového typu argumentů. void Histogram::paintEvent(QPaintEvent *event) Pokud nebudeme nijak pracovat s parametrem metody, lze potlačit hlášení překladače makrem Q_UNUSED(event); Je třeba si uvědomit, že událost se vyvolá při každém překreslení, tj. změna velikosti, překrytí jiným oknem atd. Pokud nemáme k dispozici data, můžeme běh metody ukončit. if (data->size() < 1) return; Šířku a výšku samotné komponenty lze zjistit voláním [[https://doc.qt.io/qt-5/qwidget.html#size-prop|QWidget::size()]], z návratové hodnoty dtového typu [[https://doc.qt.io/qt-5/qsize.html|QSize]] lze pak metodami [[https://doc.qt.io/qt-5/qsize.html#width||QSize::width()]] a [[https://doc.qt.io/qt-5/qsize.html#height|QSize::height()]] extrahovat šířku a výšku((Výška je snížena o 20px, což je výška popisky. Určitě lze stejného efektu dosáhnout i jinak.)). Vykreslovač použijeme s podporou antialiasingu. Není to úplně nutné, s ohledem na charakter dat, ale je dobré vědět že to lze a jak to udělat. QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); Následuje samotné vykreslení grafických prvků, tentokrát v podobě metod komponenty. Za zmínku určitě stojí způsob předávání vykreslovače do metody - je totiž použit ukazatel. Proč? Představme si ukazatel jako list papíru, na který kreslíme. Pokud by metoda pracovala s hodnotou vykreslovače, vytvořila by se v těle metody kopie, tj. nový papír. A co se s tím papírem stane po ukončení metody? Vlastně je to docela názorný příklad použití ukazatelů. for (int i = 0; i < data->size(); i++) { int d = data->at(i); drawBar(&painter, i, d); drawLabel(&painter, i, QString::number(d)); } Kódy metod pro vykreslení jednotlivých částí histogramu jsou stejné, jako v online přednášce. Za zmínku stojí jiný způsob volby barev: zde použijeme barevný model ''HSV'', rozsah hodnot ''H'' je rozdělen lineárně tolik intervalů, kolik je ve vektoru dat, takže histogram vytvoři duhu. QColor color; p->fillPath(path, color.fromHsv(int(position*(350/data->size())), 255, 255)); ===== Kontejner - Dock ===== Vykreslovací komponentu vložíme do jiného kontejneru, který bude obsahovat menu pro výběr datového souboru a případně i další GUI prvky, které budou umístěny v [[https://doc.qt.io/qt-5/qvboxlayout.html|QVBoxLayout]]. class Dock : public QWidget { Q_OBJECT QVBoxLayout *dockLayout; Histogram *histogram; QToolBar *toolBar; QStatusBar *statusBar; public: Dock (QWidget *parent = 0); private slots: void loadFromFile (); }; Klíčovou komponentou je sice třída ''Histogram'', ale určitě je žádoucí rozšířit funkcionalitu aplikace tak, aby si uživatel mohl vybrat za běhu programu jiná data pro vizualizaci a měl přehled o tom, co program dělá. K prvnímu účelu vytvoříme menu pomocí třídy [[https://doc.qt.io/qt-5/qtoolbar.html|QToolBar]]. Položkami v menu jsou instance třídy [[https://doc.qt.io/qt-5/qaction.html|QAction]], které po kliknutí emitují signál [[https://doc.qt.io/qt-5/qaction.html#triggered|QAction::triggered()]]. Tento signál bude propojen se slotem - metodou, která vyvolá dialog pro otevření souboru a naplní datový vektor histogramu. toolBar = new QToolBar; QAction *openAction = new QAction("Open"); toolBar->addAction(openAction); connect(openAction, SIGNAL(triggered()), this, SLOT(loadFromFile())); Takto vytvořené menu (zatím s jednou položkou) lze vložit do layoutu metodou [[https://doc.qt.io/qt-5/qlayout.html#setMenuBar|QLayout::setMenuBar()]]. {{:courses:b2b99ppc:solutions:solutions_histogram_01.png?300 |}} dockLayout = new QVBoxLayout; dockLayout->setMenuBar(toolBar); Další komponentou, která slouží pro průběžné informování uživatele o činnosti programu, je [[https://doc.qt.io/qt-5/qstatusbar.html|QStatusBar]]. Pomocí metody [[https://doc.qt.io/qt-5/qstatusbar.html#showMessage|QStatusBar::showMessage()]] lze poslat do komponenty textovou informaci. Zároveň je součástí komponenty grafický prvek v pravém dolním rohu, který umožňuje snadnější manipulaci s velikostí okna. statusBar = new QStatusBar; statusBar->setMaximumHeight(20); statusBar->showMessage("No data loaded"); // vlozeni status baru do layoutu dockLayout->addWidget(statusBar); Pro načtení dat z textového souboru slouží metoda ''loadFromFile()''. Na jejím začátku je vyvolán dialog pro vyhledání souboru (dialog jen vrátí název souboru, který je potom třeba otevřít, načíst z něho data. Nejsou zde uvedeny kontroly existence souboru, najdete je v komplentním kódu. {{:courses:b2b99ppc:solutions:solutions_histogram_02.png?300 |}} QString fileName = QFileDialog::getOpenFileName(this, tr("Open Data File"), "", tr("Text File (*.txt);;All Files (*)")); QFile file(fileName); QString data = file.readAll(); QStringList vals = data.split(' '); foreach (QString str, vals) { histogram->push_back(str.toInt()); } Po načtení dat je třeba vyvolat v komponentě ''Histogram'' překreslení. A oznámit třeba uživateli, že se načetly data ze souboru. // zavola Histogra::paintEvent(QPaintEvent *) histogram->update(); statusBar->showMessage(QString("Loaded data from %1").arg(QFileInfo(fileName).fileName())); ===== Vylepšení aplikace ===== Program sice pěkně funguje, ale popisky jsou trochu nečitelné. Možná by nebylo od věci stanovit nějakou nejmenší šířku sloupce (např. 30px) a podle toho změnit šířku vykreslovací plochy. if (histogram->size().width() < 30*vals.size()) { histogram->resize(30*vals.size(), histogram->size().height()*(30*vals.size()/histogram->size().width())); // zmena sirky hlavniho okna aplikace, levy horni okraj a vyska zustavani stejne setGeometry(x(), y(), 30*vals.size(), height()); } {{ :courses:b2b99ppc:solutions:solutions_histogram_03.png?500 |}} Druhým nedostatkem aplikace je načtení dalších dat. Protože je datový vektor plněn metodou ''push_back'', je třeba ho před načtením nových dat vypráznit. K tomu učelu rozšíříme rozhraní třídy ''Histogram'', kterou budeme volat před plněním. void Histogram::clear() { data->clear(); }