与主线程通信
当Qt应用程序开始执行时,只有主线程是在运行的。主线程是唯一允许创建QAplication 或者QCoreApplication对象,并且可以对创建的对象调用exec()的线程。在调用exec()之后,这个线程 或者等待一个事件,或者处理一个事件。
通过创建一些QThread子类的对象,主线程可以开始一些新的线程,就像上一节中所做的那样。如果这些新的线程需要在它们之间进行通信,则可以使用含有互斥量、读-写锁、信号或者等待条件的共享变量。但在这些技术中,没有任何一个可以用来与主线程进行通信,因为它们会锁住事件循环并且会冻结用户界面。
在次线程和主线程之间进行通信的一个解决方案是在线程之间使用信号-槽的连接。通常情况下,信号和槽机制可以同步操作,这就意味着在发射信号的时候,使用直接函数即可立刻调用连接到一个信号上的多个槽。
然而,当连接位于不同线程中的对象时,这一机制就会变得不同步起来[这种状态可以通过修改QObject::connect()中的第5个可选参数而改变]。在底层,实际是通过置入一个事件来实现这些连接的。这个槽接着就会由线程的事件循环调用,而在该线程中存在着接收器对象。在默认情况下,QObject存在于创建它的线程中,通过调用QObject::moveToThread()可以在任意时刻修改它。
为了描绘线程之间的信号槽连接是如何工作的,我们将查看一下在ImagePro应用程序中的代码。该程序是一个基本的图像处理应用程序,允许用户旋转图片、重定义图片的大小以及改变图片的色彩深度。这个应用程序(如图14.3所示)使用一个次线程在不锁住事件循环的情况下执行对图片的操作。在处理非常大的图片时,这样做会在效果上产生很大的差别。次线程具有一系列的任务,或者是“事务”,可以用来完成事件并发送事件给主窗口以报告进度。
ImageWindow::ImageWindow()
{
imageLabel = new QLabel;
imageLabel->setBackgroundRole(QPalette::Dark);
imageLabel->setAutoFillBackground(true);
imageLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
setCentralWidget(imageLabel);
createActions();
createMenus();
statusBar()->showMessage(tr("Ready"), 2000);
connect(&thread, SIGNAL(transactionStarted(const QString &)),
statusBar(), SLOT(showMessage(const QString &)));
connect(&thread, SIGNAL(allTransactionsDone()),
this, SLOT(allTransactionsDone()));
setCurrentFile("");
}
在ImageWindow构造函数中值得注意的部分是这两个信号-槽连接。它们两个都与transactionThread对象发射的信号直接有关。稍后将介绍TransactionThread对象。
void ImageWindow::flipHorizontally()
{
addTransaction(new FlipTransaction(Qt::Horizontal));
}
flipHorizontally()槽用来创建一个“翻转”事务并且用私有函数addTransaction()注册它。而flipVertically()、resizeImage()、convertTo32Bit()、convertTo8Bit()和convertTo1Bit()这几个函数都很相似。
void TransactionThread::addTransaction(Transaction *transact)
{
QMutexLocker locker(&mutex);
transactions.enqueue(transact);
transactionAdded.wakeOne();
}
addTransaction()函数会向次线程的事务队列中添加一一个事务,并且当正在处理这些事务时禁用Open、Save和Save As等操作。
void TransactionThread::addTransaction(Transaction *transact)
{
QMutexLocker locker(&mutex);
transactions.enqueue(transact);
transactionAdded.wakeOne();
}
当transactionThread的事务队列变为空时,就会调用这个alTransactionsDone()槽。
现在,我们来看看TransactionThread 类。与大多数QThread子类相似,TransactionThread类的实现是需要慎重对待的,因为run()函数在它自已的线程内执行,其他函数(包含构造函数和析构函数)则都从主线程中调用。该类的定义如下:
class TransactionThread : public QThread
{
Q_OBJECT
public:
TransactionThread();
~TransactionThread();
void addTransaction(Transaction *transact);
void setImage(const QImage &image);
QImage image();
signals:
void transactionStarted(const QString &message);
void allTransactionsDone();
protected:
void run();
private:
QImage currentImage;
QQueue<Transaction *> transactions;
QWaitCondition transactionAdded;
QMutex mutex;
};
TransactionThread类维护着一个事务队列,并且在后台一个接一个地处理及执行它们。在上述代码的私有[private()]部分段,声明了4个成员变量:
● currentImage保存事务应用的图片。
● transactions是待处理事务的队列。
● transactionAdded 是一个等待条件,当有新的事务添加到队列中时,用于触发线程。
● mutex 用于防止currentImage和transactions成员变量并发访问。
这里给出类的构造函数如下:
TransactionThread::TransactionThread()
{
start();
}
在构造函数中,只调用QThread::start()以启动将执行事务的线程。
TransactionThread::~TransactionThread()
{
{
QMutexLocker locker(&mutex);
while (!transactions.isEmpty())
delete transactions.dequeue();
transactions.enqueue(EndTransaction);
transactionAdded.wakeOne();
}
wait();
}
在析构函数中,我们清空事务队列,并且给队列添加一个专门的EndTransaction标记。然后,在基类析构函数被隐式调用之前,触发线程并使用QThread::wait()等待到它结束为止。如果没有调用wait()函数,则当线程尝试读取类的成员变量时极有可能导致失败。
在调用wait()函数之前,QMutexIsocker的析构函数会在其内部程序模块的末尾解锁互斥量。调用wait() 函数之前解锁互斥量是非常重要的;否则,程序将以死锁的情况结束:次线程将永久地等待互斥量被解锁,而主线程则在进一步执行之前保持互斥量并等待次线程执行结束。
void TransactionThread::addTransaction(Transaction *transact)
{
QMutexLocker locker(&mutex);
transactions.enqueue(transact);
transactionAdded.wakeOne();
}
addTransaction()函数会把事务添加到事务队列中,并且如果事务线程还没有运行,就触发它。所有对transactions成员变量的访问都由一个互斥量保护起来,因为主线程或许会在次线程遍历所有事务的同时通过addTransaction()修改这些成员变量。
void TransactionThread::setImage(const QImage &image)
{
QMutexLocker locker(&mutex);
currentImage = image;
}
QImage TransactionThread::image()
{
QMutexLocker locker(&mutex);
return currentImage;
}
setImage()和image()函数可以让主线程设立一个图片,在该图片上可以执行事务处理,并且可以在所有事务处理完毕时找回最终的结果图片。
void TransactionThread::run()
{
Transaction *transact = 0;
QImage oldImage;
forever {
{
QMutexLocker locker(&mutex);
if (transactions.isEmpty())
transactionAdded.wait(&mutex);
transact = transactions.dequeue();
if (transact == EndTransaction)
break;
oldImage = currentImage;
}
emit transactionStarted(transact->message());
QImage newImage = transact->apply(oldImage);
delete transact;
{
QMutexLocker locker(&mutex);
currentImage = newImage;
if (transactions.isEmpty())
emit allTransactionsDone();
}
}
}
函数run()会遍历事务队列,并且可以通过对它们调用apply()函数依次执行每一个事务,直到读到EndTransaction标记为止。如果事务队列为空,则线程在"事务添加"条件下等待。
在开始执行事务之前,我们会发射transactionStarted() 信号并在这个应用程序的状态栏上显示相应的消息。当所有的事务都已经处理完毕,会发射allTransactionsDone()信号。
class Transaction
{
public:
virtual ~Transaction() { }
virtual QImage apply(const QImage &image) = 0;
virtual QString message() = 0;
};
Transaction类是一个用户可以对图片进行相关操作的抽象基类。需要通过一个Transaction指针来删除Transaction子类的实例,因此虛析构函数是必要的。Transaction类有三个具体子类:
FIipTransaction、ResizeTransaction、ConvertDepthTransaction。这里将只查看FlipTransaction 子类,其他两个子类基本与此相似。
class FlipTransaction : public Transaction
{
public:
FlipTransaction(Qt::Orientation orientation);
QImage apply(const QImage &image);
QString message();
private:
Qt::Orientation orientation;
};
FlipTransaction的构造函数带有一个参数,该参数用来指定翻转方向(水平翻转或垂直翻转)。
QImage FlipTransaction::apply(const QImage &image)
{
return image.mirrored(orientation == Qt::Horizontal,
orientation == Qt::Vertical);
}
apply()函数对QImage调用QImage::mirrored(),它既作为接收参数也是返回值。
QString FlipTransaction::message()
{
if (orientation == Qt::Horizontal) {
return QObject::tr("Flipping image horizontally...");
} else {
return QObject::tr("Flipping image vertically...");
}
}
message()函数返回正在处理的操作的消息,这些消息会显示在状态栏中。当发射transactionStarted()信号时,就会在transactionThread::run()中调用这个函数。
Image Pro应用程序展示了Qt 的信号-槽机制是如何使主线程与次线程之间的通信变得简单易行的。实现次线程时要谨慎对待,因为必须使用互斥量来保护成员变量,而且必须使用一个等待条件以在适当的时候停止或者触发线程。
imagewindow.h
#ifndef IMAGEWINDOW_H
#define IMAGEWINDOW_H
#include <QMainWindow>
#include "transactionthread.h"
class QAction;
class QLabel;
class QMenu;
class ImageWindow : public QMainWindow
{
Q_OBJECT
public:
ImageWindow();
protected:
void closeEvent(QCloseEvent *event);
private slots:
void open();
bool save();
bool saveAs();
void flipHorizontally();
void flipVertically();
void resizeImage();
void convertTo32Bit();
void convertTo8Bit();
void convertTo1Bit();
void about();
void allTransactionsDone();
private:
void createActions();
void createMenus();
bool okToContinue();
void loadFile(const QString &fileName);
void saveFile(const QString &fileName);
void setCurrentFile(const QString &fileName);
QString strippedName(const QString &fullFileName);
void addTransaction(Transaction *transact);
TransactionThread thread;
QLabel *imageLabel;
QByteArray imageFormat;
QString curFile;
QMenu *fileMenu;
QMenu *editMenu;
QMenu *convertToSubMenu;
QMenu *helpMenu;
QAction *openAction;
QAction *saveAction;
QAction *saveAsAction;
QAction *exitAction;
QAction *flipHorizontallyAction;
QAction *flipVerticallyAction;
QAction *rotateAction;
QAction *resizeAction;
QAction *convertTo32BitAction;
QAction *convertTo8BitAction;
QAction *convertTo1BitAction;
QAction *aboutAction;
QAction *aboutQtAction;
};
#endif
transactionthread.h
#ifndef TRANSACTIONTHREAD_H
#define TRANSACTIONTHREAD_H
#include <QImage>
#include <QMutex>
#include <QQueue>
#include <QThread>
#include <QWaitCondition>
class Transaction
{
public:
virtual ~Transaction() { }
virtual QImage apply(const QImage &image) = 0;
virtual QString message() = 0;
};
class FlipTransaction : public Transaction
{
public:
FlipTransaction(Qt::Orientation orientation);
QImage apply(const QImage &image);
QString message();
private:
Qt::Orientation orientation;
};
class ResizeTransaction : public Transaction
{
public:
ResizeTransaction(const QSize &size);
QImage apply(const QImage &image);
QString message();
private:
QSize size;
};
class ConvertDepthTransaction : public Transaction
{
public:
ConvertDepthTransaction(int depth);
QImage apply(const QImage &image);
QString message();
private:
int depth;
};
class TransactionThread : public QThread
{
Q_OBJECT
public:
TransactionThread();
~TransactionThread();
void addTransaction(Transaction *transact);
void setImage(const QImage &image);
QImage image();
signals:
void transactionStarted(const QString &message);
void allTransactionsDone();
protected:
void run();
private:
QImage currentImage;
QQueue<Transaction *> transactions;
QWaitCondition transactionAdded;
QMutex mutex;
};
#endif
imagewindow.cpp
#include <QtGui>
#include "imagewindow.h"
#include "ui_resizedialog.h"
ImageWindow::ImageWindow()
{
imageLabel = new QLabel;
imageLabel->setBackgroundRole(QPalette::Dark);
imageLabel->setAutoFillBackground(true);
imageLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
setCentralWidget(imageLabel);
createActions();
createMenus();
statusBar()->showMessage(tr("Ready"), 2000);
connect(&thread, SIGNAL(transactionStarted(const QString &)),
statusBar(), SLOT(showMessage(const QString &)));
connect(&thread, SIGNAL(allTransactionsDone()),
this, SLOT(allTransactionsDone()));
setCurrentFile("");
}
void ImageWindow::closeEvent(QCloseEvent *event)
{
if (okToContinue()) {
event->accept();
} else {
event->ignore();
}
}
void ImageWindow::open()
{
if (okToContinue()) {
QString fileName = QFileDialog::getOpenFileName(this);
if (!fileName.isEmpty())
loadFile(fileName);
}
}
bool ImageWindow::save()
{
if (curFile.isEmpty()) {
return saveAs();
} else {
saveFile(curFile);
return true;
}
}
bool ImageWindow::saveAs()
{
QString fileName = QFileDialog::getSaveFileName(this);
if (QFile::exists(fileName)) {
int r = QMessageBox::warning(this, tr("Image Pro"),
tr("File %1 already exists.\n"
"Do you want to overwrite it?")
.arg(QDir::toNativeSeparators(fileName)),
QMessageBox::Yes | QMessageBox::No);
if (r == QMessageBox::No)
return true;
}
if (!fileName.isEmpty())
saveFile(fileName);
return true;
}
void ImageWindow::flipHorizontally()
{
addTransaction(new FlipTransaction(Qt::Horizontal));
}
void ImageWindow::flipVertically()
{
addTransaction(new FlipTransaction(Qt::Vertical));
}
void ImageWindow::resizeImage()
{
QDialog dialog;
Ui::ResizeDialog ui;
ui.setupUi(&dialog);
ui.widthSpinBox->setValue(imageLabel->pixmap()->width());
ui.heightSpinBox->setValue(imageLabel->pixmap()->height());
if (dialog.exec()) {
QSize newSize(ui.widthSpinBox->value(),
ui.heightSpinBox->value());
addTransaction(new ResizeTransaction(newSize));
}
}
void ImageWindow::convertTo32Bit()
{
addTransaction(new ConvertDepthTransaction(32));
}
void ImageWindow::convertTo8Bit()
{
addTransaction(new ConvertDepthTransaction(8));
}
void ImageWindow::convertTo1Bit()
{
addTransaction(new ConvertDepthTransaction(1));
}
void ImageWindow::addTransaction(Transaction *transact)
{
thread.addTransaction(transact);
openAction->setEnabled(false);
saveAction->setEnabled(false);
saveAsAction->setEnabled(false);
}
void ImageWindow::about()
{
QMessageBox::about(this, tr("About Image Pro"),
tr("<h2>Image Pro 1.1</h2>"
"<p>Copyright © 2008 Software Inc."
"<p>Image Pro is a small application that demonstrates "
"signal-slot connections across threads."));
}
void ImageWindow::allTransactionsDone()
{
openAction->setEnabled(true);
saveAction->setEnabled(true);
saveAsAction->setEnabled(true);
imageLabel->setPixmap(QPixmap::fromImage(thread.image()));
setWindowModified(true);
statusBar()->showMessage(tr("Ready"), 2000);
}
void ImageWindow::createActions()
{
openAction = new QAction(tr("&Open..."), this);
openAction->setShortcut(QKeySequence::Open);
openAction->setStatusTip(tr("Open an existing image file"));
connect(openAction, SIGNAL(triggered()), this, SLOT(open()));
saveAction = new QAction(tr("&Save"), this);
saveAction->setShortcut(QKeySequence::Save);
saveAction->setStatusTip(tr("Save the image to disk"));
connect(saveAction, SIGNAL(triggered()), this, SLOT(save()));
saveAsAction = new QAction(tr("Save &As..."), this);
saveAsAction->setStatusTip(tr("Save the image under a new name"));
connect(saveAsAction, SIGNAL(triggered()), this, SLOT(saveAs()));
exitAction = new QAction(tr("E&xit"), this);
exitAction->setShortcut(tr("Ctrl+Q"));
exitAction->setStatusTip(tr("Exit the application"));
connect(exitAction, SIGNAL(triggered()), this, SLOT(close()));
flipHorizontallyAction = new QAction(tr("Flip &Horizontally"),
this);
flipHorizontallyAction->setShortcut(tr("Ctrl+H"));
flipHorizontallyAction->setStatusTip(tr("Flip the image "
"horizontally"));
connect(flipHorizontallyAction, SIGNAL(triggered()),
this, SLOT(flipHorizontally()));
flipVerticallyAction = new QAction(tr("Flip &Vertically"), this);
flipVerticallyAction->setShortcut(tr("Ctrl+V"));
flipVerticallyAction->setStatusTip(tr("Flip the image vertically"));
connect(flipVerticallyAction, SIGNAL(triggered()),
this, SLOT(flipVertically()));
resizeAction = new QAction(tr("&Resize..."), this);
resizeAction->setShortcut(tr("Ctrl+R"));
resizeAction->setStatusTip(tr("Resize the image"));
connect(resizeAction, SIGNAL(triggered()),
this, SLOT(resizeImage()));
convertTo32BitAction = new QAction(tr("32 Bit"), this);
convertTo32BitAction->setStatusTip(tr("Convert to 32-bit image"));
connect(convertTo32BitAction, SIGNAL(triggered()),
this, SLOT(convertTo32Bit()));
convertTo8BitAction = new QAction(tr("8 Bit"), this);
convertTo8BitAction->setStatusTip(tr("Convert to 8-bit image"));
connect(convertTo8BitAction, SIGNAL(triggered()),
this, SLOT(convertTo8Bit()));
convertTo1BitAction = new QAction(tr("1 Bit"), this);
convertTo1BitAction->setStatusTip(tr("Convert to 1-bit image"));
connect(convertTo1BitAction, SIGNAL(triggered()),
this, SLOT(convertTo1Bit()));
aboutAction = new QAction(tr("&About"), this);
aboutAction->setStatusTip(tr("Show the application's About box"));
connect(aboutAction, SIGNAL(triggered()), this, SLOT(about()));
aboutQtAction = new QAction(tr("About &Qt"), this);
aboutQtAction->setStatusTip(tr("Show the Qt library's About box"));
connect(aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
}
void ImageWindow::createMenus()
{
fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(openAction);
fileMenu->addAction(saveAction);
fileMenu->addAction(saveAsAction);
fileMenu->addSeparator();
fileMenu->addAction(exitAction);
editMenu = menuBar()->addMenu(tr("&Edit"));
editMenu->addAction(flipHorizontallyAction);
editMenu->addAction(flipVerticallyAction);
editMenu->addAction(resizeAction);
editMenu->addSeparator();
convertToSubMenu = editMenu->addMenu(tr("&Convert to"));
convertToSubMenu->addAction(convertTo32BitAction);
convertToSubMenu->addAction(convertTo8BitAction);
convertToSubMenu->addAction(convertTo1BitAction);
menuBar()->addSeparator();
helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(aboutAction);
helpMenu->addAction(aboutQtAction);
}
bool ImageWindow::okToContinue()
{
if (isWindowModified()) {
int r = QMessageBox::warning(this, tr("Image Pro"),
tr("The image has been modified.\n"
"Do you want to save your changes?"),
QMessageBox::Yes | QMessageBox::No
| QMessageBox::Cancel);
if (r == QMessageBox::Yes) {
return save();
} else if (r == QMessageBox::Cancel) {
return false;
}
}
return true;
}
void ImageWindow::loadFile(const QString &fileName)
{
QImage newImage;
QApplication::setOverrideCursor(Qt::WaitCursor);
bool loaded = newImage.load(fileName);
QApplication::restoreOverrideCursor();
if (loaded) {
thread.setImage(newImage);
imageFormat = QImageReader::imageFormat(fileName);
imageLabel->setPixmap(QPixmap::fromImage(newImage));
setCurrentFile(fileName);
statusBar()->showMessage(tr("File loaded"), 2000);
} else {
QMessageBox::warning(this, tr("Image Pro"),
tr("Error when loading image."));
statusBar()->showMessage(tr("Loading canceled"), 2000);
}
}
void ImageWindow::saveFile(const QString &fileName)
{
QImage image = thread.image();
QApplication::setOverrideCursor(Qt::WaitCursor);
bool saved = image.save(fileName, imageFormat);
QApplication::restoreOverrideCursor();
if (saved) {
setCurrentFile(fileName);
statusBar()->showMessage(tr("File saved"), 2000);
} else {
QMessageBox::warning(this, tr("Image Pro"),
tr("Error when saving image."));
statusBar()->showMessage(tr("Saving canceled"), 2000);
}
}
void ImageWindow::setCurrentFile(const QString &fileName)
{
curFile = fileName;
setWindowModified(false);
bool hasImage = !curFile.isEmpty();
if (hasImage) {
setWindowTitle(tr("%1[*] - %2").arg(strippedName(curFile))
.arg(tr("Image Pro")));
} else {
setWindowTitle(tr("Image Pro"));
}
saveAction->setEnabled(hasImage);
saveAsAction->setEnabled(hasImage);
flipHorizontallyAction->setEnabled(hasImage);
flipVerticallyAction->setEnabled(hasImage);
resizeAction->setEnabled(hasImage);
convertTo32BitAction->setEnabled(hasImage);
convertTo8BitAction->setEnabled(hasImage);
convertTo1BitAction->setEnabled(hasImage);
}
QString ImageWindow::strippedName(const QString &fullFileName)
{
return QFileInfo(fullFileName).fileName();
}
transactionthread.cpp
#include <QtGui>
#include "transactionthread.h"
Transaction * const EndTransaction = 0;
FlipTransaction::FlipTransaction(Qt::Orientation orientation)
{
this->orientation = orientation;
}
QImage FlipTransaction::apply(const QImage &image)
{
return image.mirrored(orientation == Qt::Horizontal,
orientation == Qt::Vertical);
}
QString FlipTransaction::message()
{
if (orientation == Qt::Horizontal) {
return QObject::tr("Flipping image horizontally...");
} else {
return QObject::tr("Flipping image vertically...");
}
}
ResizeTransaction::ResizeTransaction(const QSize &size)
{
this->size = size;
}
QString ResizeTransaction::message()
{
return QObject::tr("Resizing image...");
}
QImage ResizeTransaction::apply(const QImage &image)
{
return image.scaled(size, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
ConvertDepthTransaction::ConvertDepthTransaction(int depth)
{
this->depth = depth;
}
QImage ConvertDepthTransaction::apply(const QImage &image)
{
QImage::Format format;
switch (depth) {
case 1:
format = QImage::Format_Mono;
break;
case 8:
format = QImage::Format_Indexed8;
break;
case 24:
default:
format = QImage::Format_RGB32;
}
return image.convertToFormat(format);
}
QString ConvertDepthTransaction::message()
{
return QObject::tr("Converting image depth...");
}
TransactionThread::TransactionThread()
{
start();
}
TransactionThread::~TransactionThread()
{
{
QMutexLocker locker(&mutex);
while (!transactions.isEmpty())
delete transactions.dequeue();
transactions.enqueue(EndTransaction);
transactionAdded.wakeOne();
}
wait();
}
void TransactionThread::addTransaction(Transaction *transact)
{
QMutexLocker locker(&mutex);
transactions.enqueue(transact);
transactionAdded.wakeOne();
}
void TransactionThread::run()
{
Transaction *transact = 0;
QImage oldImage;
forever {
{
QMutexLocker locker(&mutex);
if (transactions.isEmpty())
transactionAdded.wait(&mutex);
transact = transactions.dequeue();
if (transact == EndTransaction)
break;
oldImage = currentImage;
}
emit transactionStarted(transact->message());
QImage newImage = transact->apply(oldImage);
delete transact;
{
QMutexLocker locker(&mutex);
currentImage = newImage;
if (transactions.isEmpty())
emit allTransactionsDone();
}
}
}
void TransactionThread::setImage(const QImage &image)
{
QMutexLocker locker(&mutex);
currentImage = image;
}
QImage TransactionThread::image()
{
QMutexLocker locker(&mutex);
return currentImage;
}
main.cpp
#include <QApplication>
#include "imagewindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
ImageWindow imageWin;
imageWin.resize(400, 300);
imageWin.show();
return app.exec();
}
resizedialog.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ResizeDialog</class>
<widget class="QDialog" name="ResizeDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>190</width>
<height>129</height>
</rect>
</property>
<property name="windowTitle">
<string>Image Pro</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="mainLabel">
<property name="text">
<string>Enter new size:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QSpinBox" name="widthSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="xLabel">
<property name="text">
<string>x</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="heightSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>21</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="okButton">
<property name="text">
<string>OK</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>okButton</sender>
<signal>clicked()</signal>
<receiver>ResizeDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>145</x>
<y>79</y>
</hint>
<hint type="destinationlabel">
<x>76</x>
<y>88</y>
</hint>
</hints>
</connection>
<connection>
<sender>cancelButton</sender>
<signal>clicked()</signal>
<receiver>ResizeDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>243</x>
<y>84</y>
</hint>
<hint type="destinationlabel">
<x>280</x>
<y>92</y>
</hint>
</hints>
</connection>
</connections>
</ui>