Warning
This page is located in archive. Go to the latest version of this course pages. Go the latest version of this page.

2. Histogram

V 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: 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 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<int> *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 QVariant, ale pro naše účely celá čísla postačí.

Histogram::Histogram (QWidget *parent) : QWidget (parent)
{
    // prazny vektor cisel typu int
    data = new QVector<int>();
}

Klíčovou metodou je reimplementace 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 QWidget::size(), z návratové hodnoty dtového typu QSize lze pak metodami |QSize::width() a QSize::height() extrahovat šířku a výšku1).

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 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 QToolBar. Položkami v menu jsou instance třídy QAction, které po kliknutí emitují signál 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 QLayout::setMenuBar().

dockLayout = new QVBoxLayout; 
dockLayout->setMenuBar(toolBar);

Další komponentou, která slouží pro průběžné informování uživatele o činnosti programu, je QStatusBar. Pomocí metody 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.

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());
}

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();
}

1)
Výška je snížena o 20px, což je výška popisky. Určitě lze stejného efektu dosáhnout i jinak.
courses/b2b99ppc/solutions/02.txt · Last modified: 2020/05/06 12:50 by viteks