はじめに
ついに始まりましたQt Advent Calendar 2023です。新型コロナの影響で勉強会は停止、サーバーの移管トラブルでユーザー会のサーバーが停止となっていた影響もあってか昨年はさっぱり振るわずでしたね。かく言う私自身も昨年は体調をだましだましで仕事に追われてさっぱり記事を書けませんでしたが。
今年はなんとか勉強会のWebページも復旧しましたし、鈴木会長主催で東京の勉強会も再開しています。私は重症化リスクを複数抱えていたることもあり体調管理と仕事で精一杯だったり、オフラインのイベント開催やら主催が難しいため日本Qtユーザー会関連は会長をはじめとした方々におまかせして引退状態ですが、今年は記事の作成くらいは頑張ってみようかなと思います。
ということで例年カレンダーの予定表には「何か書きます」と書いて当日まで悩むわけですが、今回は「何かかく」にかこつけて何かを描くために必要なQPainterについて何回かに記事を分けてかいておこうかなと思います。
QPainterについて
QPainterの役割
QPainterは一般的な2D GUIアプリケーションが表示上必要となる描画に関する機能を取りまとめたクラスです。
QtではGUIアプリケーションを構築するための一般的なウィジェットが提供されていますし、簡単な見栄えのカスタマイズ程度であればスタイルシートを使って見栄えに手を入れることも可能になっています。
しかし、お絵かきソフトを作りたいとか、Add-Ondだとライセンス的に問題があるとか、提供されているものだと過不足を感じるだとか言った場合はやはり独自に用意する必要がでてきます。私の場合は組込み用でそれなりの速度でグラフ描画する必要があったりでQPainterでガリガリ描いたりすることがありますね。
描画機能としては、線・四角形・円(楕円)・円弧・弦・文字・グリフ・多角形・ペイントパスのほか、別に用意されている絵を描き込むなど様々な機能があります。四角形やパスを使ったクリッピング、ウィンドウ・ビューの組み合わせを使った描画範囲・サイズの調整のほか、サイズを指定しての移動・拡大・縮小・回転・剪断などの機能も持ち合わせています。
描画対象
QPainterは、QPaintDeviceを継承するクラスに対して描画命令を行うクラスになります。Qt6では以下のようなクラスが対象となります。
- QWidget
- QPaintDeviceWindow
- QOpenGLWindow
- QRasterWindow
- QOpenGLPaintDevice
- QPagedPaintDevice
- QImage
- QPixmap
- QPicture
- QSvgGenerator
一般的なQWidget系の他、QImageやQPixmapなどの画像クラスなどが描画対象となります。
とりあえず描いてみよう
とまぁ、説明文ばかりでは肩も凝るのでとりあえず、実際どうんあ風に描画するのか見てみましょう。今回は横着をするためにQRasterWindowを使います。
#include <QtGui>
class Window : public QRasterWindow
{
Q_OBJECT
public:
Window() : QRasterWindow()
{
}
void paintEvent(QPaintEvent* event)
{
Q_UNUSED(event);
QPainter painter(this); // 1
auto font = qApp->font();
font.setPixelSize(14);
painter.setFont(font); // 2
painter.setBackground(Qt::white); // 3
painter.eraseRect(rect()); // 4
auto r = rect() - QMargins(5,5,6,6);
painter.drawRect(r); // 5
painter.drawEllipse(r); // 6
painter.drawText(r, Qt::AlignCenter, "2023"); // 7
}
QRect rect() const { return QRect(0,0,width(),height()); }
};
int main(int argc, char* argv[])
{
QGuiApplication app(argc, argv);
Window window;
window.create();
window.show();
return app.exec();
}
#include "main.moc"
$ qmake -project
$ qmake
$ make
今どきはQt Creator + CMakeのほうが一般的なのですが少量サンプルのときはこのほうが便利なので、今回はテキストエディタでコードを書き、qmakeとmakeでのビルド手順で記載しました。横着してすいません。
QRasterWindow は QWindow に2D描画のため 描画エンジンとして Raster(CPU描画)を利用する QPaintDevice を継承させたQWindowです。モジュールとしてcoreとguiだけで使えるのでかなり軽量ですがそれだけにあまり使っているのを見たことがありません。GPUを搭載していないようなウィンドウシステムも使わない非常にプアな組込み環境で、OS起動中のスプラッシュを自力で用意したいといった時には良いかもしれませんが、イマイチ用途が思い浮かばない残念な子です。
実際の描画は paintEvent で実装していますが、
- QPainterにPaintDevice(QRasterWindow)を指定してアクティブ化
- 利用するフォントをシステムフォントで設定(デフォルトと同じになるはずですが)
- 背景色を白に設定
- 背景色で塗りつぶし
- マージンをとった四角形を描画
- その四角形内部に円を描画
- その四角形中心に2023を描画
しています。結果は以下のようになります。
QRasterWindowの幅や高さは設定していないのでQtが勝手に設定したデフォルト値です。
QPainterのAPIは、大きく分けると
- QPainterのactive化や終了、設定の保存や終了などを行う管理API
- 設定API
- 描画API
- その他ユーティリティAPI
と言ったものがあります。上記の例でもコンストラクタでのアクティブ化の後、各種設定、描画を実施しています。APIについては後日詳説することとしてまずは座標系に扱いについて触れておきます。
座標系
2Dグラフィックの基本は、横軸(X軸)と縦軸(Y軸)とからなる二次元の座標系となります。GUIプログラミングだけを見ても実はいろいろな座標系が存在します。
・物理モニタ上の座標
・複数モニタを仮想的に1枚とする座標
・1枚のウィンドウの座標
・ウィンドウ内部のウィジェットの座標
さらに3Dグラフィックも入れると、奥行きも含めたXYZの3軸の座標が出てきます。これらの座標もプラットフォームや利用するライブラリによっては表現方法が異なります。有名所ですと同じ3軸の座標でもOpenGLとDirectXとでZ軸を奥に行くほどマイナス方向と表現するのか、プラス方向として表現するのかが異なります。
ちなみにOpenGL/DirectXとも基本的にY軸は上にいくとプラス下に行くとマイナスとなりますが、QPainterでの2D描画の座標系ではY軸は下に行くほどプラスと逆になっています。
このように画面上に何かを表すための座標系だけでも色々な座標系が存在します。
QPainterを使うときの座標系
最初の例示では特に意識しませんでしたが、QPainterを使うときの座標は2つの座標系が存在します。
・QPainterの論理座標系
・QPaintDeviceのデバイス座標系
QPainterで利用する論理系座標は、デフォルトでは対象の左上を原点(0,0)とし、右方向X軸プラス、下方向Y軸プラスの座標系となります。
他方、デバイス座標系となるQRasterWindow(QWindow)は、geometry()として返すQRect(四角形を表すクラス)は、ウィンドウシステム座標系であるためウィンドウ左上(x,y) 座標は(0,0)ではありません。
QRectは、複数の表現方法があり、左上座標(1,2), 右下座標(7,6)の四角形(width:6, height:4)の場合、
- QRect(QPoint(1,2), QPoint(7,6))
- QRect(QPoint(1,2), QSize(6,4))
- QRect(1,2,6,4)
のようなコンストラクタがあるのですが、今回はqDebug()などで表現されるQRect(1,2 6x4)のような表記で統一します。
たとえば複数枚のモニタを並べている私の環境では、先ほどの例題のgeometry()はQRect(5360,534 160x160)となり、drawRectに渡している四角はrは、QRect(5,5 149x149)となります。なお単位は原則としてピクセルです。1920x1200解像度をもつディスプレイは左上を0,0とするとQRect(0,0 1900x1200)となります。
例題では論理系とデバイス系が異なるので、width/heightが同じ論理座標になるようrect()関数を用意していました。
今回は QPaintDevice と QPainter が異なるため例示にもちょうどよかったのでQRasterWindow を利用していますが、QWidget の場合は最初からrect()が用意されており左上を(0,0)とする論理座標系と同じ四角形情報を返してきます。
QWidgetは自分の子としてさらに中に子ウィジェットを入れられます。このような場合は、QPainterの座標系に、2つの異なるデバイス座標系が存在することになります。
QWidgetの場合、親子関係を含め複雑になるため、グローバル座標系(ウィンドウシステムが提供する座標系)とウィジェット座標系を変換するためのQWidget::mapToGlobal()QWidget::mapFromGloba()や、親ウィジェット上の座標系に変換すQWidget::mapTo()やQWidget::mapToParent()なども用意されています。
座標系とピクセルとペン幅と・・・
自分がどんな座標にどのように描画命令を出すとどう描画されるのかを意識することは重要です。例えば、先のサンプルでは rect() からマージンを引いた四角形を用意していましたが、マージンを使わずに描画してみましょう。
auto r = rect(); // - QMargins(5,5,6,6);
ちょっとわかりにくいですが上と左にはうっすら黒い線が見えますが、右と下は見切れています。bottomとrightに1のマージンを入れてみましょう。
auto r = rect() - QMargins(0,0,1,1);
今度は四角が表示されているかと思います。QPainterは特に指定しない場合、ペン幅1の黒の線で描画します。QRect(0,0 160x160)の場合、右下座標は(160,160)になるのですが、このデバイスいっぱいに幅1ピクセルの黒線を引くなら(0,0 159x159)の四角形の描画命令を出す必要があるのです。
では、マージンなしでアンチエイリアシングをかけるとどうなるのでしょう。
painter.setRenderHints(QPainter::Antialiasing);
auto r = rect(); // - QMargins(5,5,6,6);
あ、はい。枠があるような無いような・・・微妙。いや、一応それっぽくあるのか、という結果ですね。わざわざ拡大画像までは入れないので気になるかたは、ブラウザ上で画像だけ表示して拡大してみて下さい。
ちなみに、この画像はKUbuntu 22.04上で表示させた上でスクリーンショットで生成しています。利用しているプラットフォームやプラグイン次第では別の結果になる可能性もあります。
このように、座標系を意識していないとちょっと恥ずかしい結果を表示してしまう場合があるわけです。
いきなりなんで座標系?と思われた人もいるかもしれませんが、このように意識していないと見切れて思った表現にならない場合があるわけです。座標系についてはQtのドキュメントでも単独でページが作られるほど重要な項目となります。
論理座標系とデバイス座標系と実際の描画
Qtの座標系のページでは非常に細かく解説してくれています。
四角形の理想と現実
ペン幅を操作していない場合、つまり1ピクセルの場合の説明が非常にわかりやすいので抜粋します。
理想的な論理座標での描画イメージ
QRect(QPoint(1,2), QPoint(7,6))つまりQRect(1,2 6x4)の論理系座標でペン幅を無視した理想的な図は上記のようになります。
実際にデバイスに描画されるイメージ
QPainterのデフォルト設定はエイリアス化して描画します。結果は、以下の通りです。
最近はRetinaディスプレイのように1ピクセルを複数ドットで表現するディスプレイも出てきていますが原則1ピクセル1ドットになるのでこうなります。
#### アンチエイリアスをかけた場合
アンチエイリアスは、輪郭と背景を融合させ境界を曖昧にしてぼやかせることで、あたかも滑らかな絵になるように描画する技法です。
まぁ、四角形は恩恵は少ないですが、例題の円などはアンチエイリアスにするとジャギーが目立たない滑らかな円に見えるかと思います。その代わり、指定の色や幅以外も使われる描画となります。
ペン幅と四角形
ペン幅についてもどのように描画されるのか理解しておく必要があります。アンチエイリアスでは、もともと指定サイズよりも広い幅で表現されるのでここではエイリアス化状態での描画表現を見ていきます。
論理表現
左上とサイズで表した四角形(理想的な四角形)を下図の通りとすると
ペン幅1ピクセルでの表現
1ピクセル幅は下図のようになります。
こちらは、先ほどの図を座標と軸を省いたものですね。
右と下のラインはペン幅分外にはみ出します。
ペン幅2ピクセルでの表現
2ピクセルにするとどうなるかというと下図のようになります。
偶数ピクセルをもつペンでのレンダリングの場合は、論理的に定義された図形にたいし対象的にレンダリングされます。つまり上と左も外側にはみ出す形で表現されます。
ペン幅3ピクセルでの表現
3ピクセルを指定すると奇数で対照的に増やせないので、また右側と下側方面に大きくなります。
まとめ
カレンダー初日は、QPainterについて簡単な利用サンプルと、利用するときにまず把握しておくべき座標と実際に描画されるもののずれ、アンチエイリアスでどのようなことが起きるのかを簡単に説明しました。
明日はあまり時間が取れる気はしないのですが、ウィンドウ、ビューによる描画の拡大縮小とTransfomationについて見ていこうと思います。