17.2. Взаимодействие с главным потоком приложения.

На запуске программы, написанной с использованием библиотеки Qt, стартует главный поток приложения. Это единственный поток, в котором допускается создание экземпляра класса QApplication и вызов его метода exec(). Всвязи с этим, главный поток приложения часто называют GUI-потоком. После вызова функции exec() этот поток либо ждет поступления события, либо обрабатывает какое нибудь событие.

Главный поток может запускать другие потоки, с помощью экземпляров классов, производных от QThread. Новые потоки могут обмениваться информацией между собой через глобальные переменные, с использованием мьютексов, семафоров или ожидая наступления определенного состояния. Но это совершенно не подходит для организации взаимодействий между главным и второстепенными потоками в приложении, поскольку вышеперечисленные методики могут блокировать главный поток, "замораживая" тем самым интерфейс с пользователем.

Для этих целей обычно используется механизм событий Qt, который допускает создание нестандартных типов событий и их передачу через вызов метода QApplication::postEvent(). Кроме того, функция postEvent() является потоко-безопасной, поэтому она может использоваться для передачи событий в главный поток из любого другого потока.

Рисунок 17.3. Внешний вид приложения Image Pro.


Принципы организации обмена событиями между потоками, мы будем рассматривать на примере приложения Image Pro. Оно предназначено для работы с изображениями и позволяет вращать их, изменять размеры и глубину цветопередачи. Длительные операции будут выполняться в дополнительном потоке, чтобы избежать блокировки главного цикла обработки событий приложения. Это особенно актуально при работе с большими изображениями. Второстепенный поток имеет список заданий, или "транзакций", которые необходимо выполнить и посылает события главному потоку, чтобы проинформировать его о ходе выполнения операции. ImageWindow::ImageWindow(QWidget *parent, const char *name) : QMainWindow(parent, name) { thread.setTargetWidget(this); ... } В конструкторе назначается виджет-получатель событий. События второстепенного потока будут отправляться этому виджету. Переменная thread относится к классу TransactionThread, который мы опишем чуть ниже. void ImageWindow::flipHorizontally() { addTransaction(new FlipTransaction(Horizontal)); } Слот flipHorizontally() создает транзакцию "flip" ("отобразить") и регистрирует ее вызовом функции addTransaction(). Аналогичным образом реализованы функции flipVertical(), resizeImage(), convertTo32Bit(), convertTo8Bit() и convertTo1Bit(). void ImageWindow::addTransaction(Transaction *transact) { thread.addTransaction(transact); openAct->setEnabled(false); saveAct->setEnabled(false); saveAsAct->setEnabled(false); } Функция addTransaction() добавляет транзакцию в очередь заданий второстепенного потока и запрещает операции Open, Save и Save As на время ее выполнения. void ImageWindow::customEvent(QCustomEvent *event) { if ((int)event->type() == TransactionStart) { TransactionStartEvent *startEvent = (TransactionStartEvent *)event; infoLabel->setText(startEvent->message); } else if ((int)event->type() == AllTransactionsDone) { openAct->setEnabled(true); saveAct->setEnabled(true); saveAsAct->setEnabled(true); imageLabel->setPixmap(QPixmap(thread.image())); infoLabel->setText(tr("Ready")); modLabel->setText(tr("MOD")); modified = true; statusBar()->message(tr("Done"), 2000); } else { QMainWindow::customEvent(event); } } Функция customEvent() объявлена в классе QObject и предназначена для обработки нестандартных событий. Константы TransactionStart и AllTransactionsDone определены в transactionthread.h, как: enum { TransactionStart = 1001, AllTransactionsDone = 1002 }; Стандартные события Qt имеют значения ниже 1000, поэтому более высокие значения могут свободно использоваться для создания своих, нестандартных событий.

Нестандартные события создаются как экземпляры класса QCustomEvent, производного от QEvent, которые, кроме типа события, могут хранить дополнительный указатель типа void. Класс события TransactionStart порожден от QCustomEvent и имеет одну дополнительную переменную-член:

class TransactionStartEvent : public QCustomEvent { public: TransactionStartEvent(); QString message; }; TransactionStartEvent::TransactionStartEvent() : QCustomEvent(TransactionStart) { } В конструкторе класса мы передаем константу TransactionStart унаследованному конструктору, инициализируя таким образом тип события.

Теперь перейдем к классу TransactionThread:

class TransactionThread : public QThread { public: void run(); void setTargetWidget(QWidget *widget); void addTransaction(Transaction *transact); void setImage(const QImage &image); QImage image(); private: QWidget *targetWidget; QMutex mutex; QImage currentImage; std::list<Transaction *> transactions; }; Класс TransactionThread имеет список заданий (транзакций), которые исполняются в порядке очередности поступления. void TransactionThread::addTransaction(Transaction *transact) { QMutexLocker locker(&mutex); transactions.push_back(transact); if (!running()) start(); } Функция addTransaction() добавляет новое задание в очередь транзакций и запускает поток на исполнение, если он еще не запущен. void TransactionThread::run() { Transaction *transact; for (;;) { mutex.lock(); if (transactions.empty()) { mutex.unlock(); break; } QImage oldImage = currentImage; transact = *transactions.begin(); transactions.pop_front(); mutex.unlock(); TransactionStartEvent *event = new TransactionStartEvent; event->message = transact->messageStr(); QApplication::postEvent(targetWidget, event); QImage newImage = transact->apply(oldImage); delete transact; mutex.lock(); currentImage = newImage; mutex.unlock(); } QApplication::postEvent(targetWidget, new QCustomEvent(AllTransactionsDone)); } Функция run() обходит список заданий и выполняет их (вызовом apply()). Доступ к объектам transactions и currentImage осуществляется под защитой мьютекса.

Когда транзакция запускается, в приложение, выбранному виджету (ImageWindow), посылается событие TransactionStart. После выполнения всех транзакций -- событие AllTransactionsDone.

class Transaction { public: virtual QImage apply(const QImage &image) = 0; virtual QString messageStr() = 0; }; Класс Transaction -- это абстрактный класс, который служит основой для создания классов, выполняющих определенные действия над изображением. В нашем примере, это классы-потомки: FlipTransaction, ResizeTransaction и ConvertDepthTransaction. Мы рассмотрим только FlipTransaction, остальные два класса реализованы аналогичным образом. class FlipTransaction : public Transaction { public: FlipTransaction(Qt::Orientation orient); QImage apply(const QImage &image); QString messageStr(); private: Qt::Orientation orientation; }; Конструктору класса передается один аргумент, который определяет направление (ориентацию) отражения (Horizontal или Vertical). QImage FlipTransaction::apply(const QImage &image) { return image.mirror(orientation == Qt::Horizontal, orientation == Qt::Vertical); } Для того, чтобы отразить изображение, функция apply() обращается к методу QImage::mirror() и возвращает полученный результат. QString FlipTransaction::messageStr() { if (orientation == Qt::Horizontal) return QObject::tr("Flipping image horizontally..."); else return QObject::tr("Flipping image vertically..."); } Функция messageStr() возвращает текст сообщения, которое будет отображаться в строке состояния приложения во время выполнения транзакции. Эта функция вызывается из ImageWindow::customEvent(), в контексте главного потока приложения.

Для длительных операций можно предусмотреть передачу сведений о ходе выполнения. Для этого нужно создать дополнительное событие и с его помощью посылать приложению процент выполнения задания.