$szTitle = "Qt 4.8: Пример Scribble"; include "../_header.inc"; ?>
Файлы:
Пример "Scribble" показывает, как переопределить некоторые обработчики событий QWidget'а, чтобы получить события, сгенерированные для виджетов приложения.
Мы переопределили обработчики событий мыши чтобы реализовать отрисовку, обработчик событий рисования - чтобы обновить приложение и обработчик событий изменения размера - для оптимизации внешнего вида приложения. Кроме того переопределили обработчик событий закрытия программы чтобы перехватить события закрытия до завершения работы приложения.
Пример также демонстрирует, как использовать QPainter для рисования изображения в реальном времени, а также перерисовку виджетов.
С помощью приложения Scribble пользователи могут рисовать картинку. Меню File даёт возможность пользователям открыть и редактировать существующий файл изображения, сохранять изображение и завершать работу приложения. Во время рисования меню Options позволяет пользователям выбирать цвет и толщину пера, а также очищать экран. Дополнительно меню Help предоставляет пользователям общую информацию о Qt и о примере в частности.
Пример состоит из двух классов:
Начнём обзор с класса ScribbleArea. Затем рассмотрим класс MainWindow, использующий ScribbleArea.
class ScribbleArea : public QWidget { Q_OBJECT public: ScribbleArea(QWidget *parent = 0); bool openImage(const QString &fileName); bool saveImage(const QString &fileName, const char *fileFormat); void setPenColor(const QColor &newColor); void setPenWidth(int newWidth); bool isModified() const { return modified; } QColor penColor() const { return myPenColor; } int penWidth() const { return myPenWidth; } public slots: void clearImage(); void print(); protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event); private: void drawLineTo(const QPoint &endPoint); void resizeImage(QImage *image, const QSize &newSize); bool modified; bool scribbling; int myPenWidth; QColor myPenColor; QImage image; QPoint lastPoint; };
Класс ScribbleArea унаследован от QWidget. Мы переопределили функции mousePressEvent(), mouseMoveEvent() и mouseReleaseEvent(), чтобы реализовать рисование. Мы переопределили функцию paintEvent(), чтобы обновлять область рисования, а также функцию resizeEvent() , чтобы гарантировать, что QImage, на котором рисуем, в любой момент времени как минимум такой же величины, что и виджет.
Нам необходимы несколько открытых функций: openImage() загружает изображение из файла в область рисования, позволяя пользователю редактировать изображение; save() записывает текущее изображение на экране в файл; слот clearImage() очищает изображение, отображаемое в области рисования. Для фактического рисования необходима закрытая функция drawLineTo(), а также resizeImage() - для изменения размера QImage. Слот print() обрабатывает печать.
Также необходимы следующие закрытые переменные:
ScribbleArea::ScribbleArea(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_StaticContents); modified = false; scribbling = false; myPenWidth = 1; myPenColor = Qt::blue; }
В конструкторе устанавливаем атрибут виджета Qt::WA_StaticContents, указывающий, что содержимое виджета размещено в верхнем левом углу и не меняется при изменении размера виджета. Qt использует этот атрибут для оптимизации событий рисования при изменении размера. Это исключительно оптимизация и будет использовать только для виджетов, чьё содержимое является статичным и размещённым в верхнем левом углу.
bool ScribbleArea::openImage(const QString &fileName) { QImage loadedImage; if (!loadedImage.load(fileName)) return false; QSize newSize = loadedImage.size().expandedTo(size()); resizeImage(&loadedImage, newSize); image = loadedImage; modified = false; update(); return true; }
В функции openImage() загружаем заданное изображение. Затем изменяем размер загруженного QImage с тем, чтобы его размер как минимум был не меньше виджета по обеим осям, используя закрытую функцию resizeImage(), а также устанавливаем загруженное изображение в переменную-член image. В заключение, вызываем QWidget::update() чтобы запланировать перерисовку.
bool ScribbleArea::saveImage(const QString &fileName, const char *fileFormat) { QImage visibleImage = image; resizeImage(&visibleImage, size()); if (visibleImage.save(fileName, fileFormat)) { modified = false; return true; } else { return false; } }
Функция saveImage() создаёт объект QImage, который закрывает только видимый участок реального image и сохраняет его используя QImage::save(). Если изображение успешно сохранено, устанавливаем переменную области рисования modified в значение false, поскольку нет не сохранённых данных.
void ScribbleArea::setPenColor(const QColor &newColor) { myPenColor = newColor; } void ScribbleArea::setPenWidth(int newWidth) { myPenWidth = newWidth; }
Функции setPenColor() и setPenWidth() устанавливают цвет и толщину текущего пера. Эти переменные будут использоваться в будущих операциях рисования.
void ScribbleArea::clearImage() { image.fill(qRgb(255, 255, 255)); modified = true; update(); }
Открытый слот clearImage() очищает изображение, отображаемое в области рисования. Мы просто заполняет всё изображение белым цветом, который соответствую значению RGB (255, 255, 255). Как обычно, при изменении изображения устанавливаем modified в значение true и планируем перерисовку.
void ScribbleArea::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { lastPoint = event->pos(); scribbling = true; } } void ScribbleArea::mouseMoveEvent(QMouseEvent *event) { if ((event->buttons() & Qt::LeftButton) && scribbling) drawLineTo(event->pos()); } void ScribbleArea::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && scribbling) { drawLineTo(event->pos()); scribbling = false; } }
Для поиска какая кнопка послужила источником в событиях нажатия и отпускания кнопок мыши мы используем функцию QMouseEvent::button(). Для поиска нажатой кнопки (в виде комбинации ИЛИ) в событиях перемещения мыши используем QMouseEvent::buttons().
Если пользователь нажал левую кнопку мыши, сохраняем позицию курсора мыши в lastPoint. Также создаём запись о том, что пользователь сейчас рисует. (Переменная scribbling необходима, поскольку мы не можем предположить, что события перемещения и отпускания кнопки мыши всегда будут предшествовать событию нажатия кнопки мыши в том же самом виджете.)
Если пользователь передвинет мышь с нажатой левой кнопкой или отпустит кнопку, вызываем для рисования закрытую функцию drawLineTo().
void ScribbleArea::paintEvent(QPaintEvent *event) { QPainter painter(this); QRect dirtyRect = event->rect(); painter.drawImage(dirtyRect, image, dirtyRect); }
В переопределённой функции paintEvent() мы просто создаём QPainter для области рисования, и отрисовываем изображение.
Здесь у вас может возникнуть удивление, почему мы просто не нарисуем непосредственно на виджете вместо рисования в QImage и копирования QImage на экран в paintEvent(). Для этого имеются как минимум три хороших причины:
void ScribbleArea::resizeEvent(QResizeEvent *event) { if (width() > image.width() || height() > image.height()) { int newWidth = qMax(width() + 128, image.width()); int newHeight = qMax(height() + 128, image.height()); resizeImage(&image, QSize(newWidth, newHeight)); update(); } QWidget::resizeEvent(event); }
При запуске пользователем приложения Scribble, генерируется событие изменения размера и создаётся изображение и выводится на экран в области рисования. Мы сделали исходное изображение слегка больше, чем главное окно приложения и область рисования с тем, чтобы избежать постоянного изменения размера изображения при изменении пользователем размеров главного окна (которые могут быть очень неподходящими). Но при превышении размера главного окна сверх исходного необходимо изменить размер изображения.
void ScribbleArea::drawLineTo(const QPoint &endPoint) { QPainter painter(&image); painter.setPen(QPen(myPenColor, myPenWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); painter.drawLine(lastPoint, endPoint); modified = true; int rad = (myPenWidth / 2) + 2; update(QRect(lastPoint, endPoint).normalized() .adjusted(-rad, -rad, +rad, +rad)); lastPoint = endPoint; }
В функции drawLineTo() мы рисуем линию от точки, где была расположена мышь, когда произошло последнее событие нажатия или перемещения мыши, устанавливаем modified в значение true, генерируем событие перерисовки и обновляем lastPoint так, что вызывается drawLineTo(), продолжаем отрисовку с того места, где остановились.
Можем вызвать функцию update() без параметров, но в качестве лёгкой оптимизации передаём QRect, который задаёт прямоугольник для обновления внутри области рисования, чтобы избежать полной перерисовки виджета.
void ScribbleArea::resizeImage(QImage *image, const QSize &newSize) { if (image->size() == newSize) return; QImage newImage(newSize, QImage::Format_RGB32); newImage.fill(qRgb(255, 255, 255)); QPainter painter(&newImage); painter.drawImage(QPoint(0, 0), *image); *image = newImage; }
В QImage нет удачного API для изменения размера изображения. Есть функция QImage::copy(), которая делает трюк, но при использвании для растягивания изображения, она заливает новые области чёрным цветом, тогда как мы хотим белый.
Поэтому трюк заключается в создании нового объекта QImage с правильным размером, заливкой его белым цветом и отрисовкой старого изображения внутри него используя QPainter. Новое изображение получается в формате QImage::Format_RGB32, который означает, что каждый пиксель сохраняется в виде 0xffRRGGBB (где RR, GG и BB - красный, зеленый и синий цветовые каналы, ff - шестнадцатеричное значение 255).
Печать обрабатывается слотом print():
void ScribbleArea::print() { #ifndef QT_NO_PRINTER QPrinter printer(QPrinter::HighResolution); QPrintDialog *printDialog = new QPrintDialog(&printer, this);
Создаём объект QPrinter с высоким разрешением для требуемого формата вывода, используя QPrintDialog для запроса у пользователя размера страницы и для показа того, как вывод будет форматирован на странице.
Если в диалоге пользователь согласился, выполняем задачу печати на устройство рисования:
if (printDialog->exec() == QDialog::Accepted) { QPainter painter(&printer); QRect rect = painter.viewport(); QSize size = image.size(); size.scale(rect.size(), Qt::KeepAspectRatio); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(image.rect()); painter.drawImage(0, 0, image); } #endif // QT_NO_PRINTER }
Печать изображения в файл таким способом - это просто вопрос рисования на QPrinter. Перед отрисовкой на устройстве рисования масштабируем изображение так, чтобы оно заполнило доступное пространство на странице.
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(); protected: void closeEvent(QCloseEvent *event); private slots: void open(); void save(); void penColor(); void penWidth(); void about(); private: void createActions(); void createMenus(); bool maybeSave(); bool saveFile(const QByteArray &fileFormat); ScribbleArea *scribbleArea; QMenu *saveAsMenu; QMenu *fileMenu; QMenu *optionMenu; QMenu *helpMenu; QAction *openAct; QList<QAction *> saveAsActs; QAction *exitAct; QAction *penColorAct; QAction *penWidthAct; QAction *printAct; QAction *clearScreenAct; QAction *aboutAct; QAction *aboutQtAct; };
Класс MainWindow унаследован от QMainWindow. Переопределим обработчик closeEvent() из QWidget. Слоты open(), save(), penColor() и penWidth() соответствуют пунктам меню. Дополнительно создадим пять закрытых функций.
Мы используем булеву функцию maybeSave() для проверки наличия несохранённых изменений. Если несохранённые изменения имеются, то даём пользователю возможность сохранить эти изменения. Функция возвращает false если пользователь щёлкнет по кнопке Cancel. Мы используем функцию saveFile() для того, чтобы дать пользователю возможность сохранить текущее изображение, выведенное на экран в области рисования.
MainWindow::MainWindow() { scribbleArea = new ScribbleArea; setCentralWidget(scribbleArea); createActions(); createMenus(); setWindowTitle(tr("Scribble")); resize(500, 500); }
Создаём в конструкторе область рисования, которую сделаем центральным виджетом виджета MainWindow. Затем создаём связанные с ним действия и меню.
void MainWindow::closeEvent(QCloseEvent *event) { if (maybeSave()) { event->accept(); } else { event->ignore(); } }
События закрытия отправляются в виджеты, которые хочет закрыть пользователь, обычно после щелчка по File|Exit или кнопке X в строке заголовка. Переопределив обработчик событий, мы можем перехватывать попытки закрытия приложения.
В данном примере мы используем событие закрытия для задания вопроса пользователю о сохранении каких-либо несохранённых изменений. Вся логика для этого находится в функции maybeSave(). Если maybeSave() вернёт true, то нет изменений или пользователи успешно их сохранили, а мы принимаем событие. После этого приложение может нормально завершить работу. Если maybeSave() возвратит false, пользователь щёлкнул по кнопке Cancel, поэтому мы "игнорируем" событие, оставляя приложение без его воздействия.
void MainWindow::open() { if (maybeSave()) { QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::currentPath()); if (!fileName.isEmpty()) scribbleArea->openImage(fileName); } }
В слоте open() перед загрузкой в область рисования нового изображения сначала даём пользователю возможность сохранить любые изменения в изображении, выведенном на экран в настоящий момент. Затем запрашиваем у пользователя имя файла и загружаем файл в ScribbleArea.
void MainWindow::save() { QAction *action = qobject_cast<QAction *>(sender()); QByteArray fileFormat = action->data().toByteArray(); saveFile(fileFormat); }
Слот save() вызывается когда пользователь выбрал пункт меню Save As, а затем выбрал пункт меню из меню фомат. Первое, что нам нужно сделать, это найти какой действие отправит сигнал используя QObject::sender(). Эта функция возвращает отправителя в виде указателя на QObject. Поскольку нам известно, что отправителем является объект действия, то мы можем безопасно привести тип к QObject. Можно использовать приведение типа в стиле C или static_cast<>() C++, но в качестве защитного приема используем при программировании qobject_cast(). Преимуществом является то, что если объект имеет неверный тип, возвращается нулевой указатель. Крах из-за нулевых указателей значительно легче выявлять, чем падения от небезопасного приведения типов.
Поскольку у нас имеется действие, то извлечём выбранный формат используя QAction::data(). (При создании действий мы использовали QAction::setData() для установки наших собственных пользовательских данных прикрепленных к действию, в виде QVariant. Подробнее об этом будет рассказано при рассмотрении createActions().)
Теперь, когда нам известен формат, вызываем закрытую функцию saveFile() для сохранения изображения, выведенного на экран в настоящий момент.
void MainWindow::penColor() { QColor newColor = QColorDialog::getColor(scribbleArea->penColor()); if (newColor.isValid()) scribbleArea->setPenColor(newColor); }
Используем слот penColor() для получения с помощью QColorDialog нового цвета от пользователя. Если пользователь выбрал новый цвет, то делаем его цветом области рисования.
void MainWindow::penWidth() { bool ok; int newWidth = QInputDialog::getInteger(this, tr("Scribble"), tr("Select pen width:"), scribbleArea->penWidth(), 1, 50, 1, &ok); if (ok) scribbleArea->setPenWidth(newWidth); }
Чтобы получить в слоте penWidth() новую толщину пера, используем QInputDialog. Класс QInputDialog предоставляет простой удобный диалог для получения значения от пользователя. Мы используем статическую функцию QInputDialog::getInt(), которая сочетает QLabel и QSpinBox. QSpinBox инициализируется значением пера области рисования, допустимое значение из диапазона от 1 до 50 с шагом 1 (что означает увеличение или уменьшение значения на 1 кнопками вверх и вниз).
Булева переменная ok будет установлена в значение true если пользователь щёлкнет по кнопке OK и false - если пользователь нажмёт Cancel.
void MainWindow::about() { QMessageBox::about(this, tr("About Scribble"), tr("<p>The <b>Scribble</b> example shows how to use QMainWindow as the " "base widget for an application, and how to reimplement some of " "QWidget's event handlers to receive the events generated for " "the application's widgets:</p><p> We reimplement the mouse event " "handlers to facilitate drawing, the paint event handler to " "update the application and the resize event handler to optimize " "the application's appearance. In addition we reimplement the " "close event handler to intercept the close events before " "terminating the application.</p><p> The example also demonstrates " "how to use QPainter to draw an image in real time, as well as " "to repaint widgets.</p>")); }
Мы реализовали слот about() для создания окна сообщения, описывающего для чего спроектирован пример.
void MainWindow::createActions() { openAct = new QAction(tr("&Open..."), this); openAct->setShortcuts(QKeySequence::Open); connect(openAct, SIGNAL(triggered()), this, SLOT(open())); foreach (QByteArray format, QImageWriter::supportedImageFormats()) { QString text = tr("%1...").arg(QString(format).toUpper()); QAction *action = new QAction(text, this); action->setData(format); connect(action, SIGNAL(triggered()), this, SLOT(save())); saveAsActs.append(action); } printAct = new QAction(tr("&Print..."), this); connect(printAct, SIGNAL(triggered()), scribbleArea, SLOT(print())); exitAct = new QAction(tr("E&xit"), this); exitAct->setShortcuts(QKeySequence::Quit); connect(exitAct, SIGNAL(triggered()), this, SLOT(close())); penColorAct = new QAction(tr("&Pen Color..."), this); connect(penColorAct, SIGNAL(triggered()), this, SLOT(penColor())); penWidthAct = new QAction(tr("Pen &Width..."), this); connect(penWidthAct, SIGNAL(triggered()), this, SLOT(penWidth())); clearScreenAct = new QAction(tr("&Clear Screen"), this); clearScreenAct->setShortcut(tr("Ctrl+L")); connect(clearScreenAct, SIGNAL(triggered()), scribbleArea, SLOT(clearImage())); aboutAct = new QAction(tr("&About"), this); connect(aboutAct, SIGNAL(triggered()), this, SLOT(about())); aboutQtAct = new QAction(tr("About &Qt"), this); connect(aboutQtAct, SIGNAL(triggered()), qApp, SLOT(aboutQt())); }
В функции createAction() создаём действия, отображающие пункты меню и соединяющие их с соответствующими слотами. В частности, создаём действия из подменю Save As. Используем QImageWriter::supportedImageFormats() чтобы получить список поддерживаемых форматов (в виде QList<QByteArray>).
Затем перебираем список, создавая действие для каждого формата. Вызываем QAction::setData() с файлом формата, поэтому можем получить его позже в виде QAction::data(). Также мы можем определить формат файл из текст действия, отбросив "...", но это будет грубо.
void MainWindow::createMenus() { saveAsMenu = new QMenu(tr("&Save As"), this); foreach (QAction *action, saveAsActs) saveAsMenu->addAction(action); fileMenu = new QMenu(tr("&File"), this); fileMenu->addAction(openAct); fileMenu->addMenu(saveAsMenu); fileMenu->addAction(printAct); fileMenu->addSeparator(); fileMenu->addAction(exitAct); optionMenu = new QMenu(tr("&Options"), this); optionMenu->addAction(penColorAct); optionMenu->addAction(penWidthAct); optionMenu->addSeparator(); optionMenu->addAction(clearScreenAct); helpMenu = new QMenu(tr("&Help"), this); helpMenu->addAction(aboutAct); helpMenu->addAction(aboutQtAct); menuBar()->addMenu(fileMenu); menuBar()->addMenu(optionMenu); menuBar()->addMenu(helpMenu); }
В функции createMenu() добавляем предварительно созданные действия с форматами в saveAsMenu. Затем добавляем остальные действия, а ткже подменю saveAsMenu в меню File, Options и Help.
Класс QMenu предоставляет виджет меню для использования в панелях меню, контекстных меню и других всплывающих меню. Класс QMenuBar предоставляет панель горизонтального меню со списком выпадающих меню QMenu. В заключение поместим меню File и Options в панель меню MainWindow, которую получаем используя функцию QMainWindow::menuBar().
bool MainWindow::maybeSave() { if (scribbleArea->isModified()) { QMessageBox::StandardButton ret; ret = QMessageBox::warning(this, tr("Scribble"), tr("The image has been modified.\n" "Do you want to save your changes?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Save) { return saveFile("png"); } else if (ret == QMessageBox::Cancel) { return false; } } return true; }
В mayBeSave() проверяем имеются ли не сохранённые изменения. Если имеются, то используем QMessageBox для выдачи пользователю предупреждения о модификации изображения и возможности сохранить изменения.
Как и с QColorDialog и QFileDialog, самым лёгким способом создания QMessageBox является использование его статических функций. QMessageBox предоставляет ряд различных сообщений упорядоченных по двум осям: серьёзности (вопрос, информация, предупреждение и критическое сообщение) и сложности (количеству необходимых реагирующих кнопок). Здесь мы используем функцию warning() поскольку сообщение несколько важнее.
Если пользователь выберет сохранение, вызываем закрытую функцию saveFile(). Для простоты используем в качестве формата файла PNG; пользователь всегда может нажать кнопку Cancel и сохранить файл, используя другой формат.
Функция maybeSave() возвращает false если пользователь щёлкает по кнопке Cancel; в противном случае, она возвращает true.
bool MainWindow::saveFile(const QByteArray &fileFormat) { QString initialPath = QDir::currentPath() + "/untitled." + fileFormat; QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), initialPath, tr("%1 Files (*.%2);;All Files (*)") .arg(QString(fileFormat.toUpper())) .arg(QString(fileFormat))); if (fileName.isEmpty()) { return false; } else { return scribbleArea->saveImage(fileName, fileFormat); } }
В функции saveFile() высвечиваем на экране диалог выбора файла с запросом имени файла. Статическая функция QFileDialog::getSaveFileName() возвращает имя файла, выбранное пользователем. Файл не должен существовать.