Scribble の例
Scribbleの例では、アプリケーションのウィジェット用に生成されたイベントを受け取るために、QWidget'のイベントハンドラの一部を再実装する方法を示しています。
描画を実装するマウス・イベント・ハンドラ、アプリケーションを更新するペイント・イベント・ハンドラ、アプリケーションの外観を最適化するリサイズ・イベント・ハンドラを再実装します。さらに、アプリケーションを終了する前にcloseイベントをインターセプトするためにcloseイベント・ハンドラを再実装します。
この例では、QPainter を使用してリアルタイムで画像を描画する方法と、ウィジェットを再描画する方法も示します。
Scribbleアプリケーションで、ユーザーは画像を描くことができます。File メニューでは、既存の画像ファイルを開いたり編集したり、画像を保存したり、アプリケーションを終了したりすることができます。描画中、Options メニューでは、ペンの色と幅を選択したり、画面をクリアすることができます。さらに、Help メニューでは、特に Scribble のサンプルと Qt 全般に関する情報をユーザーに提供します。
このサンプルは2つのクラスで構成されています:
ScribbleArea
はカスタムウィジェットで、 を表示し、ユーザーが描画できるようにします。QImageMainWindow
は の上にメニューを表示します。ScribbleArea
まず、ScribbleArea
クラスを復習します。次に、ScribbleArea
を使用するMainWindow
クラスを確認します。
ScribbleAreaクラスの定義
class ScribbleArea : public QWidget { Q_OBJECT public: ScribbleArea(QWidget *parent = nullptr); 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) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; private: void drawLineTo(const QPoint &endPoint); void resizeImage(QImage *image, const QSize &newSize); bool modified = false; bool scribbling = false; int myPenWidth = 1; QColor myPenColor = Qt::blue; QImage image; QPoint lastPoint; };
ScribbleArea
クラスはQWidget を継承しています。mousePressEvent()
、mouseMoveEvent()
、mouseReleaseEvent()
関数を再実装して描画を実装しています。paintEvent()
関数を再実装してらくがき領域を更新し、resizeEvent()
関数を再実装して、描画するQImage がいつでもウィジェットと同じ大きさであるようにします。
いくつかのパブリック関数が必要です。openImage()
はファイルから画像を落書き領域にロードし、ユーザが画像を編集できるようにします。save()
は現在表示されている画像をファイルに書き込みます。clearImage()
スロットは落書き領域に表示されている画像を消去します。実際に描画を行うにはプライベート関数drawLineTo()
が必要で、QImage のサイズを変更するにはresizeImage()
が必要です。print()
スロットは印刷を処理します。
また、以下のプライベート変数も必要です:
modified
落書きエリアに表示されている画像に未保存の変更がある場合、 。true
scribbling
is ユーザーが落書き領域内でマウスの左ボタンを押している間。true
penWidth
と は、アプリケーションで使用されているペンの現在設定されている幅と色を保持します。penColor
image
は、ユーザーが描いた画像を保存します。lastPoint
最後のマウス押下またはマウス移動イベントにおけるカーソルの位置を保持します。
ScribbleAreaクラスの実装
ScribbleArea::ScribbleArea(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_StaticContents); }
コンストラクタでは、ウィジェットに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 のサイズを、privateresizeImage()
関数を使用して、両方向で少なくともウィジェットと同じ大きさに変更し、メンバ変数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; } return false; }
saveImage()
関数は、実際のimage
の可視部分のみをカバーするQImage オブジェクトを作成し、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(); }
publicclearImage()
スロットは、落書き領域に表示されている画像を消去します。RGB値(255, 255, 255)に対応する白で画像全体を塗りつぶすだけです。画像を変更するときは、通常通り、modified
をtrue
に設定し、再描画をスケジュールします。
void ScribbleArea::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { lastPoint = event->position().toPoint(); scribbling = true; } } void ScribbleArea::mouseMoveEvent(QMouseEvent *event) { if ((event->buttons() & Qt::LeftButton) && scribbling) drawLineTo(event->position().toPoint()); } void ScribbleArea::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && scribbling) { drawLineTo(event->position().toPoint()); scribbling = false; } }
マウスのプレス・イベントとリリース・イベントについては、QMouseEvent::button ()関数を使用して、どのボタンがイベントを発生させたかを調べます。マウスの移動イベントについては、QMouseEvent::buttons ()を使用して、現在どのボタンが押されているかを(OR-組み合わせとして)調べます。
ユーザーがマウスの左ボタンを押した場合、マウスカーソルの位置をlastPoint
に格納する。また、ユーザーが現在落書きをしていることも記録する。(scribbling
変数が必要なのは、マウスの移動とマウスを離すイベントが、常に同じウィジェットのマウスを押すイベントの前にあると仮定できないからである)。
ユーザーが左ボタンを押したままマウスを動かすか、ボタンを離すと、drawLineTo()
関数を呼び出して描画します。
void ScribbleArea::paintEvent(QPaintEvent *event) { QPainter painter(this); QRect dirtyRect = event->rect(); painter.drawImage(dirtyRect, image, dirtyRect); }
paintEvent()関数の再実装では、らくがき領域用にQPainter を作成し、画像を描画するだけです。
この時点で、QImage で描画し、paintEvent()
でQImage を画面にコピーするのではなく、なぜウィジェットに直接描画しないのかと思うかもしれません。これには少なくとも3つの理由があります:
- ウィンドウ・システムでは、いつでもウィジェットを再描画できる必要があります。たとえば、ウィンドウが最小化され、復元された場合、ウィンドウ・システムはウィジェットの内容を忘れてしまい、ペイント・イベントを送ってくるかもしれません。つまり、ウィンドウ・システムが画像を記憶しているかどうかは当てにならないのです。
- Qtは通常、
paintEvent()
の外部でペイントすることを許可しません。特に、マウス・イベント・ハンドラからペイントすることはできません。(この動作はQt::WA_PaintOnScreen widget属性で変更できます)。 - 正しく初期化された場合、QImage は各色チャンネル(赤、緑、青、アルファ)に8ビットを使用することが保証されます。一方、QWidget は、モニタの構成によっては色深度が低くなる可能性があります。つまり、24ビットまたは32ビットの画像を読み込んでQWidget にペイントし、QWidget を再度QImage にコピーすると、情報が失われる可能性があります。
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アプリケーションを開始すると、リサイズイベントが生成され、画像が作成されて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 に設定し、repaint イベントを発生させ、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は16進数で255)として保存されます。
印刷はprint()
スロットで処理されます:
void ScribbleArea::print() { #if defined(QT_PRINTSUPPORT_LIB) && QT_CONFIG(printdialog) QPrinter printer(QPrinter::HighResolution); QPrintDialog printDialog(&printer, this);
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_CONFIG(printdialog) }
この方法で画像をファイルに印刷するのは、単にQPrinterにペイントするだけのことです。ペイント・デバイスに画像をペイントする前に、ページ上の利用可能なスペースに収まるように画像を拡大縮小します。
MainWindow クラスの定義
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); protected: void closeEvent(QCloseEvent *event) override; 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()
スロットはメニュー項目に対応しています。さらに、4つのプライベート関数を作成します。
未保存の変更があるかどうかをチェックするために、maybeSave()
関数を使用します。未保存の変更がある場合、ユーザーにその変更を保存する機会を与えます。ユーザがCancel をクリックすると、この関数はfalse
を返します。saveFile()
関数を使用して、現在落書き領域に表示されている画像を保存できるようにしています。
MainWindow クラスの実装
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), scribbleArea(new ScribbleArea(this)) { 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 タイトルバーボタンをクリックします。イベントハンドラを再実装することで、アプリケーションを閉じようとする試みをインターセプトすることができます。
この例では、closeイベントを使って、ユーザーに未保存の変更を保存するように要求しています。そのためのロジックは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スタイルのキャストやC++のstatic_cast<>()
を使用することもできましたが、防御的なプログラミング・テクニックとして、qobject_cast ()を使用します。この利点は、オブジェクトの型が間違っている場合、NULLポインタが返されることです。ヌル・ポインタによるクラッシュは、安全でないキャストによるクラッシュよりも診断がはるかに簡単です。
アクションを取得したら、QAction::data ()を使用して選択したフォーマットを抽出します。QVariant createActions()
(アクションが作成されたら、QAction::setData()を使用して、アクションに独自のカスタムデータを設定します。)
フォーマットがわかったので、現在表示されている画像を保存するために、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::getInt(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
は、ユーザーがOK をクリックした場合はtrue
に設定され、ユーザーがCancel を押した場合はfalse
に設定されます。
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, &QAction::triggered, this, &MainWindow::open); const QList<QByteArray> imageFormats = QImageWriter::supportedImageFormats(); for (const QByteArray &format : imageFormats) { QString text = tr("%1...").arg(QString::fromLatin1(format).toUpper()); QAction *action = new QAction(text, this); action->setData(format); connect(action, &QAction::triggered, this, &MainWindow::save); saveAsActs.append(action); } printAct = new QAction(tr("&Print..."), this); connect(printAct, &QAction::triggered, scribbleArea, &ScribbleArea::print); exitAct = new QAction(tr("E&xit"), this); exitAct->setShortcuts(QKeySequence::Quit); connect(exitAct, &QAction::triggered, this, &MainWindow::close); penColorAct = new QAction(tr("&Pen Color..."), this); connect(penColorAct, &QAction::triggered, this, &MainWindow::penColor); penWidthAct = new QAction(tr("Pen &Width..."), this); connect(penWidthAct, &QAction::triggered, this, &MainWindow::penWidth); clearScreenAct = new QAction(tr("&Clear Screen"), this); clearScreenAct->setShortcut(tr("Ctrl+L")); connect(clearScreenAct, &QAction::triggered, scribbleArea, &ScribbleArea::clearImage); aboutAct = new QAction(tr("&About"), this); connect(aboutAct, &QAction::triggered, this, &MainWindow::about); aboutQtAct = new QAction(tr("About &Qt"), this); connect(aboutQtAct, &QAction::triggered, qApp, &QApplication::aboutQt); }
createAction()
関数では、メニューエントリを表すアクションを作成し、適切なスロットに接続します。特に、Save As サブメニューにあるアクションを作成します。QImageWriter::supportedImageFormats ()を使って、サポートされているフォーマットのリストを取得します(QList<QByteArray>のように)。
そして、リストを繰り返し、各フォーマットのアクションを作成します。ファイル・フォーマットを指定してQAction::setData() を呼び出し、後でQAction::data() として取得できるようにしています。アクションのテキストからファイル形式を推測することもできます。
void MainWindow::createMenus() { saveAsMenu = new QMenu(tr("&Save As"), this); for (QAction *action : std::as_const(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 QMainWindow::menuBar クラスは、プルダウンQMenuのリストを持つ水平メニュー・バーを提供します。最後に、File とOptions メニューを、MainWindow
'のメニュー・バーに配置します。
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 は、重大度(質問、情報、警告、重大)と複雑さ(必要な応答ボタンの数)の2つの軸に沿って配置されたさまざまなメッセージを提供します。ここでは、メッセージがかなり重要なので、warning()
関数を使用します。
ユーザーが保存を選択した場合、saveFile()
関数を呼び出します。簡単にするために、ファイル形式としてPNGを使用します。ユーザーはいつでもCancel を押して、別の形式でファイルを保存することができます。
maybeSave()
関数は、ユーザーがCancel をクリックした場合はfalse
を返し、そうでない場合は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::fromLatin1(fileFormat.toUpper())) .arg(QString::fromLatin1(fileFormat))); if (fileName.isEmpty()) return false; return scribbleArea->saveImage(fileName, fileFormat.constData()); }
saveFile()
では、ファイル名を提案するファイルダイアログをポップアップ表示します。静的関数QFileDialog::getSaveFileName() は、ユーザーが選択したファイル名を返します。ファイルは存在する必要はありません。
©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。