Portál AbcLinuxu, 25. dubna 2024 17:56
V tomto díle se dozvíte, jak kontrolovat vstupní textová pole pomocí regulárních výrazů, jak a kdy používat vlákna a jak zobrazovat průběh nějaké déle trvající operace.
Pro práci s regulárními výrazy Qt poskytuje třídu QRegExp. Ukázkový program, který jsem pro vás připravil, umožňuje zvolit si typ výrazu (rozšířený regulární výraz, rozšířený regulární výraz s hladovými (greedy) kvantifikátory, výraz s žolíkovými znaky (wildcards) nebo pevný řetězec, což je totéž jako regulární výraz použitý na řetězec s escapovanými metaznaky).
regex.h
: API.
#ifndef REGEX_H #define REGEX_H #include <QWidget> class QCheckBox; class QComboBox; class QLabel; class QLineEdit; class QRegExp; class Regex : public QWidget { Q_OBJECT public: Regex(QWidget *parent = 0); private: QCheckBox* caseCheckbox; QComboBox* syntaxCombo; QRegExp* regex; QLabel* regexIcon; QLabel* stringIcon; QLineEdit* regexLine; QLineEdit* stringLine; private slots: void changeSyntax(int); void changeCaseSensitivity(int); void checkString(QString); void checkRegex(QString); }; #endif // REGEX_H
regex.cpp
: Pro vyhledání daného výrazu v řetězci vytvoříme instanci třídy QRegExp
, které nastavíme požadovaný výraz buď při vytváření přes konstruktor, nebo pomocí metody setPattern()
a následně zavoláme metodu indexIn()
, které předáme řetězec. V případě žolíkových znaků musíme místo indexIn()
vždy volat exactMatch()
, což je metoda jinak sloužící ke zjištění přesné shody, tzn. volání exactMatch("string")
při použití regulárních výrazů odpovídá indexIn("^string$")
. Metoda indexIn()
vrací index (int
), na kterém byl začátek výrazu nalezen (nebo -1 v případě nenalezení), zatímco exactMatch()
vrací bool
odrážející úspěch hledání.
#include "regex.h" #include <QCheckBox> #include <QComboBox> #include <QGridLayout> #include <QLabel> #include <QLineEdit> #include <QRegExp> #include <QStyle> Regex::Regex(QWidget *parent) : QWidget(parent), regexIcon(new QLabel), stringIcon(new QLabel) { // QRegExp objekt pro práci s regulárními výrazy regex = new QRegExp(QString(), Qt::CaseInsensitive); // nabídka syntaxí syntaxCombo = new QComboBox; syntaxCombo->addItem(tr("RegExp"), QRegExp::RegExp); syntaxCombo->addItem(tr("RegExp2"), QRegExp::RegExp2); syntaxCombo->addItem(tr("Wildcard"), QRegExp::Wildcard); syntaxCombo->addItem(tr("FixedString"), QRegExp::FixedString); // zaškrtávací pole určující rozlišování malých/velkých písmen caseCheckbox = new QCheckBox(tr("Case sensitive")); // vstupní pole pro regulární výraz a řetězec regexLine = new QLineEdit; stringLine = new QLineEdit; // propojíme ovládací prvky tak, aby změny na nich zpracovaly naše sloty connect(syntaxCombo, SIGNAL(activated(int)), this, SLOT(changeSyntax(int))); connect(caseCheckbox, SIGNAL(stateChanged(int)), this, SLOT(changeCaseSensitivity(int))); connect(regexLine, SIGNAL(textChanged(QString)), this, SLOT(checkRegex(QString))); connect(stringLine, SIGNAL(textChanged(QString)), this, SLOT(checkString(QString))); // teprve teď (po propojení signálů se sloty) nastavíme výchozí text, // aby se rovnou projevily změny // výchozí reg. výraz pro označení emailové adresy regexLine->setText("[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}"); // výchozí řetězec stringLine->setText("qt4@watzke.cz"); // vytvoříme mřížkové rozložení QGridLayout* layout = new QGridLayout(this); // přidáme popisek na 0. řádek, 0. sloupec layout->addWidget(new QLabel(tr("Reg. expression syntax:")), 0, 0); layout->addWidget(syntaxCombo, 0, 1); layout->addWidget(caseCheckbox, 1, 1); layout->addWidget(new QLabel(tr("Regular expression:")), 2, 0); // přidáme vstupní pole pro reg. výraz na 2. řádek, 1. sloupec layout->addWidget(regexLine, 2, 1); layout->addWidget(regexIcon, 2, 2); layout->addWidget(new QLabel(tr("String to match:")), 3, 0); layout->addWidget(stringLine, 3, 1); layout->addWidget(stringIcon, 3, 2); // toto nastavení layoutu není nutné, když vytvoříme pouze jeden // s rodičem "this", nicméně ani neuškodí setLayout(layout); // změníme výchozí šířku okna, ale ne napevno resize(500, sizeHint().height()); } // volá se pro kontrolu aktuálního řetězce (zda odpovídá reg. výrazu) void Regex::checkString(QString newString) { // odpovídá? bool match; // wildcard vyžaduje použití exactMatch(), jinak nefunguje if(regex->patternSyntax() == QRegExp::Wildcard) match = regex->exactMatch(newString); else match = (regex->indexIn(newString) >= 0); // nastavíme ikonu znázorňující zda řetězec odpovídá či neodpovídá if(match) stringIcon->setPixmap(style()->standardIcon(QStyle::SP_DialogOkButton).pixmap(16, 16)); else stringIcon->setPixmap(style()->standardIcon(QStyle::SP_DialogNoButton).pixmap(16, 16)); } // volá se pro kontrolu správnosti aktuálního reg. výrazu void Regex::checkRegex(QString newRegex) { // nastavíme nový regulární výraz regex->setPattern(newRegex); // ověříme správnost reg. výrazu, nastavíme patřičnou ikonku a // v případě, že správný je, zkontrolujeme, zda mu řetězec odpovídá if(regex->isValid()) { regexIcon->setPixmap(style()->standardIcon(QStyle::SP_DialogOkButton).pixmap(16, 16)); checkString(stringLine->text()); } else { regexIcon->setPixmap(style()->standardIcon(QStyle::SP_DialogNoButton).pixmap(16, 16)); // pokud je reg. výraz chybný, nastavíme u řetězce varovnou ikonku // signalizující neznámý stav stringIcon->setPixmap(style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(16, 16)); } } // volá se při změně syntaxe reg. výrazu z nabídky void Regex::changeSyntax(int index) { // získáme data o vybrané položce nabídky int newSyntax = syntaxCombo->itemData(index).toInt(); // a nastavíme vybranou syntaxi regex->setPatternSyntax((QRegExp::PatternSyntax)newSyntax); // nyní je třeba překontrolovat zda aktuální reg. výraz // je validní při nově zvolené syntaxi checkRegex(regexLine->text()); } // volá se při změně stavu zaškrtávacího pole void Regex::changeCaseSensitivity(int state) { if(state == Qt::Checked) regex->setCaseSensitivity(Qt::CaseSensitive); else regex->setCaseSensitivity(Qt::CaseInsensitive); // po změně nastavení rozlišování malých/velkých písmen zkontrolujeme // zda aktuální řetězec stále odpovídá checkString(stringLine->text()); }
Qt podporuje vlákna. Používá vždy nativní implementaci vláken podle platformy (např. POSIX threads, Win32). Dnes si ukážeme pouze naprosto základní a jednoduché použití. Když v programu nepoužíváte vlákna, tak se váš kód obvykle provádí ve vlákně obstarávajícím GUI, které má mj. vlastní smyčku událostí (event loop). To je v pořádku jen do té doby, než se objeví potřeba spustit něco, co bude trvat déle, jako třeba nějaký výpočet. Kdybychom tento výpočet prováděli ve vlákně s GUI, tak by se program po dobu výpočtu z uživatelského hlediska zasekl – přestalo by reagovat uživatelské rozhraní. Jedním z řešení je provést výpočet v odděleném vlákně, což je přesně to, co si teď ukážeme.
Přestože se může zdát jednodušší přistupovat z vlákna ke grafickému rozhraní přímo (například přes předaný ukazatel), vhodnějším řešením je vysílat z vlákna signály, na které bude GUI reagovat. Kód je pak univerzálnější a navíc si tak jasně oddělíme GUI od zbytku programu.
To je pro začátek dost teorie. Jak se tedy vytváří vlákno? Je třeba vytvořit třídu, která dědí QThread
a reimplementovat její chráněnou metodu run()
, která se provede po spuštění vlákna metodou start()
.
Následuje zdrojový kód programu, ve kterém zvolíte soubor, čímž se spustí výpočet jeho kontrolního součtu (MD5, MD4 nebo SHA1), přičemž průběh výpočtu bude znázorněn na grafickém ukazateli (QProgressBar
).
progress.h
: API.
#ifndef PROGRESS_H #define PROGRESS_H #include <QWidget> class QComboBox; class QLabel; class QProgressBar; class ProgressThread; class Progress : public QWidget { Q_OBJECT public: Progress(QWidget *parent = 0); ~Progress(); private: QComboBox* hashCombo; QLabel* hashLabel; QProgressBar* progress; ProgressThread* thread; private slots: void progressTest(); }; #endif // PROGRESS_H
progress.cpp
: Okno programu.
#include "progress.h" #include "progressthread.h" #include <QHBoxLayout> #include <QVBoxLayout> #include <QComboBox> #include <QLabel> #include <QPushButton> #include <QProgressBar> #include <QFileDialog> Progress::Progress(QWidget *parent) : QWidget(parent), thread(0) { // vytvoříme label (popisek) hashLabel = new QLabel(tr("Select a file to hash")); // text v labelu lze označit (a zkopírovat do schránky) hashLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); // vytvoříme ukazatel průběhu progress = new QProgressBar; // vytvoříme tlačítko zahajující výběr souboru QPushButton* btn = new QPushButton("..."); // vytvoříme seznam pro výběr hashovacího algoritmu hashCombo = new QComboBox; hashCombo->addItem("MD5", QCryptographicHash::Md5); hashCombo->addItem("MD4", QCryptographicHash::Md4); hashCombo->addItem("SHA1", QCryptographicHash::Sha1); // propojíme tlačítko se slotem zahajujícím hashování connect(btn, SIGNAL(clicked()), this, SLOT(progressTest())); // do horizontálního rozložení přidáme ukazatel průběhu, seznam a tlačítko QHBoxLayout* hLayout = new QHBoxLayout; hLayout->addWidget(progress); hLayout->addWidget(hashCombo); hLayout->addWidget(btn); // do (hlavního) vertikálního rozložení přidáme připravené horiz. rozložení // a pod něj ještě label se stavovou zprávou QVBoxLayout* layout = new QVBoxLayout(this); layout->addLayout(hLayout); layout->addWidget(hashLabel); // nastavíme výchozí šířku okna na 700px; výška je automaticky zvolená resize(700, baseSize().height()); } Progress::~Progress() { // uklidíme po sobě (pokud je co) if(thread && !thread->isRunning()) delete thread; } void Progress::progressTest() { if(thread) { // pokud vlákno běží if(thread->isRunning()) { // informujeme uživatele, že zrovna hashuje a skončíme hashLabel->setText(tr("Hashing is in progress!")); return; } else { // jinak smažeme staré vlákno delete thread; } } // aktualizujeme zprávu ("Čekám na vybrání souboru") hashLabel->setText(tr("Waiting for user to select a file")); // zobrazíme dialog pro výběr souboru QString fileName = QFileDialog::getOpenFileName(this); // pokud uživatel nic nevybere (zavře dialog), skončíme if(fileName.isEmpty()) return; // nastavíme zprávu na "Zpracovávám [soubor]" hashLabel->setText(tr("Processing ") + fileName); // vytvoříme vlákno, ve kterém se bude hashovat thread = new ProgressThread(fileName, (QCryptographicHash::Algorithm) hashCombo->itemData(hashCombo->currentIndex()).toInt()); // propojíme ukazatel průběhu se signálem hashovacího vlákna connect(thread, SIGNAL(updateProgress(int)), progress, SLOT(setValue(int))); // propojíme label se signálem vlákna tak, aby zobrazoval příchozí zprávy connect(thread, SIGNAL(message(QString)), hashLabel, SLOT(setText(QString))); // spustíme vlákno thread->start(); }
progressthread.h
: API vlákna.
#ifndef PROGRESSTHREAD_H #define PROGRESSTHREAD_H #include <QCryptographicHash> //#include <QDebug> #include <QThread> class QFile; class ProgressThread : public QThread { Q_OBJECT public: ProgressThread(QString, QCryptographicHash::Algorithm); // ~ProgressThread() { qDebug() << "terminating the thread"; } private: QFile* file; QCryptographicHash::Algorithm m_hashtype; protected: void run(); signals: void message(QString); void updateProgress(int); }; #endif // PROGRESSTHREAD_H
progressthread.cpp
: vlákno, ve kterém probíhá hashování.
#include "progressthread.h" #include <QFile> ProgressThread::ProgressThread(QString fileName, QCryptographicHash::Algorithm hashtype) { // vytvoříme objekt pro manipulaci se souborem file = new QFile(fileName, this); // uložíme si zvolený hashovací algoritmus m_hashtype = hashtype; } void ProgressThread::run() { // otevřeme soubor pro čtení if(!file->open(QIODevice::ReadOnly | QIODevice::Unbuffered)) { // pokud nelze otevřít, informujeme uživatele emit message(tr("Couldn't open ") + file->fileName()); return; } // objekt obstarávající hashování QCryptographicHash* hash = new QCryptographicHash(m_hashtype); QByteArray buffer; // velikost bufferu (512 KiB) // qint64 je na všech platformách, kde běží Qt, zaručeně 64bitový integer, // jehož literál lze vytvořit pomocí makra Q_INT64_C qint64 bufSize = Q_INT64_C(512*1024); // 1 % je step bajtů qint64 step = file->size() / 100; // přečteno bajtů qint64 read = 0; // procenta průběhu int percent = -1; // dokud je co číst while(!(buffer = file->read(bufSize)).isEmpty()) { // hashujeme data po jednotlivých bufferech hash->addData(buffer); // ukládáme si kolik bajtů už je zpracováno read += buffer.size(); // pokud se změnilo procentu průběhu, vyšleme signál if((read / step) != percent) { percent = read / step; emit updateProgress(percent); } } // ujistíme se, že ukazatel průběhu ukáže 100% po dokončení // (i když je soubor menší než buffer) if(percent != 100) emit updateProgress(100); // vyšleme signál s hashem emit message(file->fileName() + ": " + hash->result().toHex()); // uklidíme po sobě delete hash; }
K vláknům se ještě určitě vrátíme, protože jde o dost obsáhlé téma. S tímto si vystačíte jen pro různé jednoduché úkony, které je třeba provádět na pozadí. Na závěr mám ještě jeden tip. Pokud vaše vlákno nemá smyčku událostí (tzn. nespustili jste exec()
) a chcete zničit instanci vlákna poté, co se vykoná run()
, tak přidejte do konstruktoru vlákna toto propojení:
connect(this, SIGNAL(finished()), this, SLOT(deleteLater()));
V příštím díle si kromě jiného ukážeme, jak do GUI přidat karty (taby), jak v programu otevřít nové okno (například s nastavením) a řekneme si něco o modálnosti oken. Těšit se můžete také na ukázku použití WebKitu pro prohlížení webu a Phononu pro přehrávání zvuku.
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.