Portál AbcLinuxu, 25. dubna 2024 17:56

Grafické programy v Qt 4 - 5 (regexpy, vlákna a ukazatel průběhu)

20. 4. 2009 | David Watzke
Články - Grafické programy v Qt 4 - 5 (regexpy, vlákna a ukazatel průběhu)  

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.

Regulární výrazy

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 4 Regulární výrazy

Vlákna a ukazatel průběhu

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

Qt 4 progressbar thread, Ukazatel průběhu hashování

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

Závěr

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.

Seriál Qt 4 - psaní grafických programů (dílů: 12)

První díl: Grafické programy v Qt 4 - 1 (úvod, hello world), poslední díl: Grafické programy v Qt 4 – 12 (stylování GUI pomocí CSS).
Předchozí díl: Grafické programy v Qt 4 - 4 (Qt Creator podruhé)
Následující díl: Grafické programy v Qt 4 - 6 (WebKit, Phonon, taby, modálnost oken)

Související články

Cmake: zjednoduš si život
Seriál: Kommander
Seriál: KDE: tipy a triky
Seriál: Začíname KProgramovať
Co přináší KDE 4 - (alfaverze, porty a D-BUS)
Co přináší KDE 4 - (technologie)
Novinky v KDE 4
Jaké je KDE 4.0.0
Seriál: KDE 4.1 - megarecenze

Odkazy a zdroje

doc.trolltech.com

Další články z této rubriky

LLVM a Clang – více než dobrá náhrada za GCC
Ze 4 s na 0,9 s – programovací jazyk Vala v praxi
Reverzujeme ovladače pro USB HID zařízení
Linux: systémové volání splice()
Programování v jazyce Vala - základní prvky jazyka

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.