マンデルブロー
マンデルブロのサンプルは Qt を使ったマルチスレッドプログラミングのデモです。メインスレッドのイベントループをブロックすることなく、ワーカースレッドを使って重い計算を実行する方法を示しています。
ここでの重い計算とは、おそらく世界で最も有名なフラクタルであるマンデルブロ集合です。最近では、XaoSのような洗練されたプログラムがマンデルブロ集合のリアルタイム・ズームを提供しているが、標準的なマンデルブロ・アルゴリズムは我々の目的には十分遅い。
実生活では、ここで説明したアプローチは、同期ネットワークI/Oやデータベースアクセスなど、何らかの重い操作が行われている間もユーザーインターフェイスが応答し続けなければならないような、多くの問題に適用できる。ブロッキング・フォーチュン・クライアントの例は、TCPクライアントで同じ原理が働いていることを示しています。
Mandelbrotアプリケーションは、マウスやキーボードを使ったズームやスクロールをサポートしています。メイン・スレッドのイベント・ループ(そして結果としてアプリケーションのユーザー・インターフェース)のフリーズを避けるために、すべてのフラクタル計算を別のワーカー・スレッドに置いた。このスレッドは、フラクタルのレンダリングが終わるとシグナルを発する。
ワーカースレッドが新しいズーム倍率の位置を反映するためにフラクタルを再計算している間、メインスレッドは単に以前にレンダリングされたピクセルマップをスケーリングして即座にフィードバックを提供します。結果はワーカースレッドが最終的に提供するものほど良くは見えませんが、少なくともアプリケーションの応答性は良くなります。下の一連のスクリーンショットは、元の画像、スケーリングされた画像、そして再レンダリングされた画像を示しています。
同様に、ユーザーがスクロールすると、直前のpixmapが即座にスクロールされ、pixmapの端から先の描画されていない領域が表示され、その間にワーカースレッドによって画像がレンダリングされます。
アプリケーションは2つのクラスで構成されています:
RenderThread
はマンデルブロ集合をレンダリングする のサブクラスである。QThreadMandelbrotWidget
は のサブクラスで、マンデルブロ集合を画面に表示し、ユーザーにズームとスクロールをさせる。QWidget
Qtのスレッドサポートにまだ慣れていない場合は、Qtのスレッドサポートの概要を読むことから始めることをお勧めします。
RenderThreadクラスの定義
RenderThread
クラスの定義から始めます:
class RenderThread : public QThread { Q_OBJECT public: RenderThread(QObject *parent = nullptr); ~RenderThread(); void render(double centerX, double centerY, double scaleFactor, QSize resultSize, double devicePixelRatio); static void setNumPasses(int n) { numPasses = n; } static QString infoKey() { return QStringLiteral("info"); } signals: void renderedImage(const QImage &image, double scaleFactor); protected: void run() override; private: static uint rgbFromWaveLength(double wave); QMutex mutex; QWaitCondition condition; double centerX; double centerY; double scaleFactor; double devicePixelRatio; QSize resultSize; static int numPasses; bool restart = false; bool abort = false; static constexpr int ColormapSize = 512; uint colormap[ColormapSize]; };
このクラスは、QThread を継承し、別スレッドで実行できるようになっています。コンストラクタとデストラクタを除けば、render()
が唯一のパブリック関数です。スレッドが画像のレンダリングを終えるたびに、renderedImage()
シグナルを発する。
プロテクトされたrun()
関数はQThread から再実装されたもので、スレッドが開始されると自動的に呼び出されます。
private
セクションには、QMutex 、QWaitCondition 、その他いくつかのデータ・メンバがある。ミューテックスは、他のデータ・メンバーを保護します。
RenderThreadクラスの実装
RenderThread::RenderThread(QObject *parent) : QThread(parent) { for (int i = 0; i < ColormapSize; ++i) colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize)); }
コンストラクタでは、restart
とabort
変数をfalse
に初期化します。これらの変数はrun()
関数のフローを制御します。
また、colormap
配列も初期化します。この配列には一連の RGB カラーが格納されています。
RenderThread::~RenderThread() { mutex.lock(); abort = true; condition.wakeOne(); mutex.unlock(); wait(); }
デストラクタは、スレッドがアクティブであればいつでも呼び出すことができる。run()
、できるだけ早く実行を停止するよう指示するために、abort
をtrue
に設定する。また、スレッドがスリープしている場合は、QWaitCondition::wakeOne ()を呼び出してスリープを解除する。(run()
を見直すときにわかるように、スレッドは何もすることがないときにスリープ状態になる)。
ここで注目すべき重要な点は、run()
がそれ自身のスレッド(ワーカースレッド)で実行されるのに対し、RenderThread
のコンストラクタとデストラクタ(およびrender()
関数)は、ワーカースレッドを作成したスレッドから呼び出されるということです。したがって、abort
とcondition
変数へのアクセスを保護するためにミューテックスが必要です。run()
変数にはいつでもアクセスできる可能性があります。
デストラクタの最後に、QThread::wait ()を呼び出し、run()
が終了するまで待ってから、ベース・クラスのデストラクタを呼び出します。
void RenderThread::render(double centerX, double centerY, double scaleFactor, QSize resultSize, double devicePixelRatio) { QMutexLocker locker(&mutex); this->centerX = centerX; this->centerY = centerY; this->scaleFactor = scaleFactor; this->devicePixelRatio = devicePixelRatio; this->resultSize = resultSize; if (!isRunning()) { start(LowPriority); } else { restart = true; condition.wakeOne(); } }
render()
関数は、MandelbrotWidget
がマンデルブロ集合の新しい画像を生成する必要があるときに呼び出されます。centerX
,centerY
,scaleFactor
パラメータは、描画するフラクタル部分を指定します。resultSize
は、生成されるQImage のサイズを指定します。
この関数はパラメータをメンバ変数に格納します。そうでない場合は、restart
をtrue
に設定し(run()
に、未完了の計算を停止し、新しいパラメータで再スタートするように指示する)、スリープ状態のスレッドを起動する。
void RenderThread::run() { QElapsedTimer timer; forever { mutex.lock(); const double devicePixelRatio = this->devicePixelRatio; const QSize resultSize = this->resultSize * devicePixelRatio; const double requestedScaleFactor = this->scaleFactor; const double scaleFactor = requestedScaleFactor / devicePixelRatio; const double centerX = this->centerX; const double centerY = this->centerY; mutex.unlock();
run()
は非常に大きな関数である。
関数本体は無限ループで、レンダリングパラメータをローカル変数に格納することから始まります。いつものように、クラスのミューテックスを使ってメンバ変数へのアクセスを保護します。メンバ変数をローカル変数に格納することで、ミューテックスで保護する必要のあるコードを最小限に抑えることができます。これにより、メイン・スレッドがRenderThread
のメンバ変数にアクセスする必要があるときに、長時間ブロックする必要がなくなります(render()
など)。
forever
キーワードはQt擬似キーワードです。
const int halfWidth = resultSize.width() / 2; const int halfHeight = resultSize.height() / 2; QImage image(resultSize, QImage::Format_RGB32); image.setDevicePixelRatio(devicePixelRatio); int pass = 0; while (pass < numPasses) { const int MaxIterations = (1 << (2 * pass + 6)) + 32; constexpr int Limit = 4; bool allBlack = true; timer.restart(); for (int y = -halfHeight; y < halfHeight; ++y) { if (restart) break; if (abort) return; auto scanLine = reinterpret_cast<uint *>(image.scanLine(y + halfHeight)); const double ay = centerY + (y * scaleFactor); for (int x = -halfWidth; x < halfWidth; ++x) { const double ax = centerX + (x * scaleFactor); double a1 = ax; double b1 = ay; int numIterations = 0; do { ++numIterations; const double a2 = (a1 * a1) - (b1 * b1) + ax; const double b2 = (2 * a1 * b1) + ay; if ((a2 * a2) + (b2 * b2) > Limit) break; ++numIterations; a1 = (a2 * a2) - (b2 * b2) + ax; b1 = (2 * a2 * b2) + ay; if ((a1 * a1) + (b1 * b1) > Limit) break; } while (numIterations < MaxIterations); if (numIterations < MaxIterations) { *scanLine++ = colormap[numIterations % ColormapSize]; allBlack = false; } else { *scanLine++ = qRgb(0, 0, 0); } } } if (allBlack && pass == 0) { pass = 4; } else { if (!restart) { QString message; QTextStream str(&message); str << " Pass " << (pass + 1) << '/' << numPasses << ", max iterations: " << MaxIterations << ", time: "; const auto elapsed = timer.elapsed(); if (elapsed > 2000) str << (elapsed / 1000) << 's'; else str << elapsed << "ms"; image.setText(infoKey(), message); emit renderedImage(image, requestedScaleFactor); } ++pass; } }
次に、アルゴリズムの核となる部分である。完全なマンデルブロ集合の画像を作ろうとするのではなく、何度もパスを行い、より正確な(そして計算コストのかかる)フラクタルの近似を生成します。
デバイスのピクセル比をターゲットサイズに適用して、高解像度のピクセルマップを作成します(Drawing High Resolution Versions of Pixmaps and Images 参照)。
ループ内で、restart
が(render()
によって)true
に設定されていることを発見したら、すぐにループから抜け出し、制御を外側のループの一番上(forever
ループ)に素早く戻し、新しいレンダリングパラメータをフェッチします。同様に、abort
が(RenderThread
デストラクタによって)true
に設定されたことが判明した場合は、直ちに関数から戻り、スレッドを終了します。
コアとなるアルゴリズムは、このチュートリアルの範囲外です。
mutex.lock(); if (!restart) condition.wait(&mutex); restart = false; mutex.unlock(); } }
すべての繰り返しが終わったら、restart
がtrue
でない限り、QWaitCondition::wait() を呼び出してスレッドをスリープさせます。何もすることがない間は、ワーカースレッドを無限にループさせておいても仕方がない。
uint RenderThread::rgbFromWaveLength(double wave) { double r = 0; double g = 0; double b = 0; if (wave >= 380.0 && wave <= 440.0) { r = -1.0 * (wave - 440.0) / (440.0 - 380.0); b = 1.0; } else if (wave >= 440.0 && wave <= 490.0) { g = (wave - 440.0) / (490.0 - 440.0); b = 1.0; } else if (wave >= 490.0 && wave <= 510.0) { g = 1.0; b = -1.0 * (wave - 510.0) / (510.0 - 490.0); } else if (wave >= 510.0 && wave <= 580.0) { r = (wave - 510.0) / (580.0 - 510.0); g = 1.0; } else if (wave >= 580.0 && wave <= 645.0) { r = 1.0; g = -1.0 * (wave - 645.0) / (645.0 - 580.0); } else if (wave >= 645.0 && wave <= 780.0) { r = 1.0; } double s = 1.0; if (wave > 700.0) s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0); else if (wave < 420.0) s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0); r = std::pow(r * s, 0.8); g = std::pow(g * s, 0.8); b = std::pow(b * s, 0.8); return qRgb(int(r * 255), int(g * 255), int(b * 255)); }
rgbFromWaveLength()
関数は、波長を32ビットQImagesと互換性のあるRGB値に変換するヘルパー関数である。コンストラクタから呼び出され、colormap
配列を心地よい色で初期化する。
マンデルブロ・ウィジェット・クラスの定義
MandelbrotWidget
クラスはRenderThread
を使ってマンデルブロ集合を画面に描画する。以下がクラス定義です:
class MandelbrotWidget : public QWidget { Q_DECLARE_TR_FUNCTIONS(MandelbrotWidget) public: MandelbrotWidget(QWidget *parent = nullptr); protected: QSize sizeHint() const override { return {1024, 768}; }; void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; #if QT_CONFIG(wheelevent) void wheelEvent(QWheelEvent *event) override; #endif void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; #ifndef QT_NO_GESTURES bool event(QEvent *event) override; #endif private: void updatePixmap(const QImage &image, double scaleFactor); void zoom(double zoomFactor); void scroll(int deltaX, int deltaY); #ifndef QT_NO_GESTURES bool gestureEvent(QGestureEvent *event); #endif RenderThread thread; QPixmap pixmap; QPoint pixmapOffset; QPoint lastDragPos; QString help; QString info; double centerX; double centerY; double pixmapScale; double curScale; };
QWidgetさらに、updatePixmap()
スロットがあり、ワーカースレッドのrenderedImage()
シグナルに接続して、スレッドから新しいデータを受け取るたびに表示を更新します。
プライベート変数には、RenderThread
型のthread
と、最後にレンダリングされた画像を格納するpixmap
がある。
MandelbrotWidgetクラスの実装
constexpr double DefaultCenterX = -0.637011; constexpr double DefaultCenterY = -0.0395159; constexpr double DefaultScale = 0.00403897; constexpr double ZoomInFactor = 0.8; constexpr double ZoomOutFactor = 1 / ZoomInFactor; constexpr int ScrollStep = 20;
実装は、後で必要となるいくつかの定数から始まる。
MandelbrotWidget::MandelbrotWidget(QWidget *parent) : QWidget(parent), centerX(DefaultCenterX), centerY(DefaultCenterY), pixmapScale(DefaultScale), curScale(DefaultScale) { help = tr("Zoom with mouse wheel, +/- keys or pinch. Scroll with arrow keys or by dragging."); connect(&thread, &RenderThread::renderedImage, this, &MandelbrotWidget::updatePixmap); setWindowTitle(tr("Mandelbrot")); #if QT_CONFIG(cursor) setCursor(Qt::CrossCursor); #endif }
コンストラクタの興味深い部分は、QObject::connect()の呼び出しです。
これは2つのQObjectの間の標準的なシグナル・スロット接続のように見えますが、シグナルはレシーバーが住んでいるスレッドとは別のスレッドで発せられるので、この接続は実質的にqueued connection です。これらの接続は非同期(つまりノンブロッキング)なので、スロットはemit
ステートメントの後のある時点で呼び出されることになります。さらに、スロットはレシーバーが存在するスレッドで呼び出されます。ここでは、シグナルはワーカースレッドで発せられ、スロットはイベントループに制御が戻ったときにGUIスレッドで実行されます。
キューイングされた接続では、Qtはシグナルに渡された引数のコピーを保存して、後でスロットに渡せるようにしなければなりません。Qtは多くのC++やQtの型のコピーを取る方法を知っているので、QImage 。カスタム型を使用する場合、その型をキュー接続のパラメータとして使用する前に、テンプレート関数qRegisterMetaType() を呼び出す必要があります。
void MandelbrotWidget::paintEvent(QPaintEvent * /* event */) { QPainter painter(this); painter.fillRect(rect(), Qt::black); if (pixmap.isNull()) { painter.setPen(Qt::white); painter.drawText(rect(), Qt::AlignCenter|Qt::TextWordWrap, tr("Rendering initial image, please wait...")); return; }
paintEvent ()では、まず背景を黒で塗りつぶす。まだ何も描画するものがない場合(pixmap
が NULL の場合)、ウィジェットにメッセージを表示し、ユーザに我慢してすぐに関数から戻るように求めます。
if (qFuzzyCompare(curScale, pixmapScale)) { painter.drawPixmap(pixmapOffset, pixmap); } else { const auto previewPixmap = qFuzzyCompare(pixmap.devicePixelRatio(), qreal(1)) ? pixmap : pixmap.scaled(pixmap.deviceIndependentSize().toSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); const double scaleFactor = pixmapScale / curScale; const int newWidth = int(previewPixmap.width() * scaleFactor); const int newHeight = int(previewPixmap.height() * scaleFactor); const int newX = pixmapOffset.x() + (previewPixmap.width() - newWidth) / 2; const int newY = pixmapOffset.y() + (previewPixmap.height() - newHeight) / 2; painter.save(); painter.translate(newX, newY); painter.scale(scaleFactor, scaleFactor); const QRectF exposed = painter.transform().inverted().mapRect(rect()) .adjusted(-1, -1, 1, 1); painter.drawPixmap(exposed, previewPixmap, exposed); painter.restore(); }
pixmapのスケールファクターが正しければ、そのpixmapを直接ウィジェットに描画します。
そうでない場合は、計算が終わるまで表示されるプレビューpixmapを作成し、それに応じて座標系を変換します。
ペインタ上でトランスフォームを使用し、QPainter::drawPixmap() のオーバーロードを使用します。この場合、高解像度の pixmap はサポートされないので、デバイスピクセル比 1 の pixmap を作成します。
スケーリングされたペインタ行列を使用してウィジェットの矩形を逆マッピングすることで、pixmap の露出している領域だけが描画されるようにします。QPainter::save() とQPainter::restore() の呼び出しは、その後に実行されるペイントが標準座標系を使用することを確認します。
const QFontMetrics metrics = painter.fontMetrics(); if (!info.isEmpty()){ const int infoWidth = metrics.horizontalAdvance(info); const int infoHeight = (infoWidth/width() + 1) * (metrics.height() + 5); painter.setPen(Qt::NoPen); painter.setBrush(QColor(0, 0, 0, 127)); painter.drawRect((width() - infoWidth) / 2 - 5, 0, infoWidth + 10, infoHeight); painter.setPen(Qt::white); painter.drawText(rect(), Qt::AlignHCenter|Qt::AlignTop|Qt::TextWordWrap, info); } const int helpWidth = metrics.horizontalAdvance(help); const int helpHeight = (helpWidth/width() + 1) * (metrics.height() + 5); painter.setPen(Qt::NoPen); painter.setBrush(QColor(0, 0, 0, 127)); painter.drawRect((width() - helpWidth) / 2 - 5, height()-helpHeight, helpWidth + 10, helpHeight); painter.setPen(Qt::white); painter.drawText(rect(), Qt::AlignHCenter|Qt::AlignBottom|Qt::TextWordWrap, help); }
ペイントイベントハンドラの最後に、テキスト文字列と半透明の矩形をフラクタルの上に描画します。
void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */) { thread.render(centerX, centerY, curScale, size(), devicePixelRatio()); }
ユーザがウィジェットのサイズを変更するたびに、render()
を呼び出して新しい画像の生成を開始します。centerX
、centerY
、curScale
のパラメータは同じですが、ウィジェットのサイズは変更されます。
resizeEvent()
は、ウィジェットが最初に表示されたときに Qt によって自動的に呼び出され、初期画像が生成されます。
void MandelbrotWidget::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Plus: zoom(ZoomInFactor); break; case Qt::Key_Minus: zoom(ZoomOutFactor); break; case Qt::Key_Left: scroll(-ScrollStep, 0); break; case Qt::Key_Right: scroll(+ScrollStep, 0); break; case Qt::Key_Down: scroll(0, -ScrollStep); break; case Qt::Key_Up: scroll(0, +ScrollStep); break; case Qt::Key_Q: close(); break; default: QWidget::keyPressEvent(event); } }
キー押下イベント・ハンドラは、マウスを持っていないユーザーのために、いくつかのキーボード・バインディングを提供します。zoom()
とscroll()
関数については後で説明します。
void MandelbrotWidget::wheelEvent(QWheelEvent *event) { const int numDegrees = event->angleDelta().y() / 8; const double numSteps = numDegrees / double(15); zoom(pow(ZoomInFactor, numSteps)); }
ホイール・イベント・ハンドラは、マウス・ホイールがズーム・レベルをコントロールするように再実装されている。QWheelEvent::angleDelta() はホイール・マウスの動きの角度を8分の1度単位で返します。ほとんどのマウスでは、ホイール1ステップは15度に相当する。マウスのステップ数を求め、その結果のズーム率を決定する。例えば、正の方向(つまり+30度)に2回のホイールステップがある場合、ズーム倍率はZoomInFactor
の2乗、つまり0.8 * 0.8 = 0.64となります。
void MandelbrotWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) lastDragPos = event->position().toPoint(); }
ウィジェットとグラフィックス・ビューのジェスチャーで説明したように、QGesture でピンチ・トゥ・ズームが実装されています。
#ifndef QT_NO_GESTURES bool MandelbrotWidget::gestureEvent(QGestureEvent *event) { if (auto *pinch = static_cast<QPinchGesture *>(event->gesture(Qt::PinchGesture))) { if (pinch->changeFlags().testFlag(QPinchGesture::ScaleFactorChanged)) zoom(1.0 / pinch->scaleFactor()); return true; } return false; } bool MandelbrotWidget::event(QEvent *event) { if (event->type() == QEvent::Gesture) return gestureEvent(static_cast<QGestureEvent*>(event)); return QWidget::event(event); } #endif
ユーザーがマウスの左ボタンを押すと、lastDragPos
にマウスポインターの位置が保存されます。
void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { pixmapOffset += event->position().toPoint() - lastDragPos; lastDragPos = event->position().toPoint(); update(); } }
ユーザーがマウスの左ボタンを押したままマウスポインタを動かすと、pixmapOffset
を調整して、ずらした位置に pixmap を描画し、QWidget::update() を呼び出して強制的に再描画します。
void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { pixmapOffset += event->position().toPoint() - lastDragPos; lastDragPos = QPoint(); const auto pixmapSize = pixmap.deviceIndependentSize().toSize(); const int deltaX = (width() - pixmapSize.width()) / 2 - pixmapOffset.x(); const int deltaY = (height() - pixmapSize.height()) / 2 - pixmapOffset.y(); scroll(deltaX, deltaY); } }
マウスの左ボタンが離されると、マウスの移動のときと同じようにpixmapOffset
を更新し、lastDragPos
をデフォルト値にリセットします。そして、scroll()
を呼び出して、新しい位置に新しい画像をレンダリングします。(pixmapをドラッグしたときに見える領域は黒で描画されるため、pixmapOffset
を調整するだけでは十分ではありません)。
void MandelbrotWidget::updatePixmap(const QImage &image, double scaleFactor) { if (!lastDragPos.isNull()) return; info = image.text(RenderThread::infoKey()); pixmap = QPixmap::fromImage(image); pixmapOffset = QPoint(); lastDragPos = QPoint(); pixmapScale = scaleFactor; update(); }
updatePixmap()
スロットは、ワーカースレッドが画像のレンダリングを終了したときに呼び出されます。まず、ドラッグが有効かどうかをチェックし、有効な場合は何もしません。通常の場合は、pixmap
に画像を保存し、他のメンバのいくつかを再初期化します。最後に、QWidget::update ()を呼び出して表示を更新します。
この時点で、なぜパラメータにQImage 、データ・メンバにQPixmap を使うのか不思議に思うかもしれない。なぜ1つの型にこだわらないのか?その理由は、QImage が、ワーカースレッドで必要なピクセルの直接操作をサポートする唯一のクラスだからです。一方、画像を画面に描画する前に、画像をピクセルマップに変換する必要がある。この変換は、paintEvent()
で行うよりも、ここで一度に行う方がよいでしょう。
void MandelbrotWidget::zoom(double zoomFactor) { curScale *= zoomFactor; update(); thread.render(centerX, centerY, curScale, size(), devicePixelRatio()); }
zoom()
では、curScale
を再計算する。そして、QWidget::update() を呼び出して、スケーリングされたpixmapを描画し、ワーカースレッドに、新しいcurScale
の値に対応する新しい画像をレンダリングするように依頼します。
void MandelbrotWidget::scroll(int deltaX, int deltaY) { centerX += deltaX * curScale; centerY += deltaY * curScale; update(); thread.render(centerX, centerY, curScale, size(), devicePixelRatio()); }
scroll()
は と似ていますが、影響を受けるパラメータが と である点が異なります。zoom()
centerX
centerY
main()関数
アプリケーションのマルチスレッド化は、main()
関数には影響しません:
int main(int argc, char *argv[]) { QApplication app(argc, argv); QCommandLineParser parser; parser.setApplicationDescription(u"Qt Mandelbrot Example"_s); parser.addHelpOption(); parser.addVersionOption(); QCommandLineOption passesOption(u"passes"_s, u"Number of passes (1-8)"_s, u"passes"_s); parser.addOption(passesOption); parser.process(app); if (parser.isSet(passesOption)) { const auto passesStr = parser.value(passesOption); bool ok; const int passes = passesStr.toInt(&ok); if (!ok || passes < 1 || passes > 8) { qWarning() << "Invalid value:" << passesStr; return -1; } RenderThread::setNumPasses(passes); } MandelbrotWidget widget; widget.grabGesture(Qt::PinchGesture); widget.show(); return app.exec(); }
本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。