ベジェ曲線とQPainterを使用してC++で丸みを帯びた角の形状を取得する方法:ステップバイステップガイド
公開: 2022-03-11序章
グラフィックデザインの現在の傾向は、あらゆる種類の形で多くの丸い角を使用することです。 この事実は、多くのWebページ、モバイルデバイス、およびデスクトップアプリケーションで確認できます。 最も注目すべき例は、アプリケーションのプッシュボタンです。これは、クリックされたときに何らかのアクションをトリガーするために使用されます。 角が90度の厳密な長方形ではなく、角が丸い形で描かれることがよくあります。 角が丸いので、ユーザーインターフェイスがよりスムーズで快適になります。 私はこれについて完全に確信しているわけではありませんが、私のデザイナーの友人は私にそう言っています。
ユーザーインターフェイスの視覚的要素はデザイナーによって作成され、プログラマーはそれらを適切な場所に配置するだけで済みます。 しかし、その場で角が丸い形状を生成する必要があり、それをプリロードできない場合はどうなりますか? 一部のプログラミングライブラリは、角が丸い事前定義された形状を作成するための機能が制限されていますが、通常、より複雑なケースでは使用できません。 たとえば、QtフレームワークにはクラスQPainter
があり、ウィジェット、ピックスマップ、画像など、 QPaintDevice
から派生したすべてのクラスを描画するために使用されます。 これにはdrawRoundedRect
というメソッドがあり、その名前が示すように、角が丸い長方形を描画します。 しかし、もう少し複雑な形状が必要な場合は、自分で実装する必要があります。 直線セグメントのグループで囲まれた平面形状であるポリゴンを使用して、これをどのように行うことができますか? 一枚の紙に鉛筆で多角形を描いた場合、最初のアイデアは、消しゴムを使用して各コーナーの線の小さな部分を削除し、残りのセグメントの端を円弧で接続することです。 全体のプロセスは、次の図に示されています。
クラスQPainter
には、円弧を描画できるdrawArc
という名前のオーバーロードされたメソッドがいくつかあります。 それらはすべて、円弧の中心とサイズ、開始角度、および円弧の長さを定義するパラメータを必要とします。 回転していない長方形に必要なこれらのパラメータの値を決定するのは簡単ですが、より複雑なポリゴンを扱う場合はまったく別の問題です。 さらに、すべてのポリゴン頂点に対してこの計算を繰り返す必要があります。 この計算は時間と手間がかかる作業であり、人間はその過程であらゆる種類の計算エラーを起こしがちです。 ただし、コンピュータを人間のために機能させるのはソフトウェア開発者の仕事であり、その逆はありません。 そこで、ここでは、複雑な多角形を角の丸い形に変えることができる単純なクラスを開発する方法を示します。 このクラスのユーザーは、ポリゴンの頂点を追加するだけでよく、残りはクラスが行います。 このタスクに使用する基本的な数学ツールは、ベジェ曲線です。
ベジェ曲線
ベジェ曲線の理論を説明する数学の本やインターネットリソースがたくさんあるので、関連するプロパティの概要を簡単に説明します。
定義上、ベジェ曲線は2次元表面上の2点間の曲線であり、その軌道は1つ以上の制御点によって制御されます。 厳密に言えば、追加の制御点がない2点間の曲線もベジェ曲線です。 ただし、これにより2つのポイントが直線になるため、特に興味深いものでも、有用なものでもありません。
二次ベジェ曲線
二次ベジェ曲線には1つの制御点があります。 理論によれば、制御点P1を持つ点P0とP2の間の2次ベジェ曲線は次のように定義されます。
B(t)=(1-t) 2 P 0 + 2t(1-t)P 1 + t 2 P 2 、ここで0≤t≤1 (1)
したがって、 tが0に等しい場合、 B(t)はP 0を生成し、 tが1に等しい場合、 B(t)はP 2を生成しますが、それ以外の場合、 B(t)の値もP1 。 式2t(1-t)はt = 0.5で最大値を持つため、 B(t)に対するP1の影響が最大になります。 P 1は、関数の軌道をそれ自体に引き寄せる架空の重力源と考えることができます。 次の図は、開始点、終了点、および制御点を持つ2次ベジェ曲線のいくつかの例を示しています。
では、ベジェ曲線を使用して問題をどのように解決するのでしょうか。 下の図は説明を提供します。
ポリゴンの頂点とその周囲の接続された線分の短い部分を削除することを想像すると、一方の線分はP 0で終わり、もう一方の線分はP 2で終わり、削除された頂点はP1であると考えることができます。 この点と出来上がりのセットに2次ベジェ曲線を適用します。目的の丸みを帯びた角があります。
QPainterを使用したC++/Qtの実装
クラスQPainter
には、2次ベジェ曲線を描画する方法がありません。 式(1)に従って最初から実装するのは非常に簡単ですが、Qtライブラリはより優れたソリューションを提供します。 2D描画用のもう1つの強力なクラスがあります: QPainterPath
。 クラスQPainterPath
は、後でQPainter
オブジェクトで追加および使用できる線と曲線のコレクションです。 現在のコレクションにベジェ曲線を追加するオーバーロードされたメソッドがいくつかあります。 特に、メソッドquadTo
は、2次ベジェ曲線を追加します。 曲線は現在のQPainterPath
ポイント( P 0 )から始まりますが、 P1とP2はパラメーターとしてquadTo
に渡す必要があります。
QPainter
のメソッドdrawPath
は、アクティブペンとブラシを使用して、パラメーターとして指定する必要があるQPainterPath
オブジェクトから線と曲線のコレクションを描画するために使用されます。
それでは、クラス宣言を見てみましょう。
class RoundedPolygon : public QPolygon { public: RoundedPolygon() { SetRadius(10); } void SetRadius(unsigned int iRadius) { m_iRadius = iRadius; } const QPainterPath& GetPath(); private: QPointF GetLineStart(int i) const; QPointF GetLineEnd(int i) const; float GetDistance(QPoint pt1, QPoint pt2) const; private: QPainterPath m_path; unsigned int m_iRadius; };
QPolygon
をサブクラス化して、頂点やその他のものを自分で追加する必要がないようにすることにしました。 半径を適切な初期値に設定するコンストラクターの他に、このクラスには他に2つのパブリックメソッドがあります。
-
SetRadius
メソッドは、半径を指定された値に設定します。 半径は、各頂点の近くの直線の長さ(ピクセル単位)であり、丸みを帯びた角の場合は削除されます(より正確には描画されません)。 -
GetPath
は、すべての計算が行われる場所です。RoundedPolygon
QPainterPath
を返します。
プライベート部分のメソッドは、 GetPath
で使用される単なる補助メソッドです。
実装を見てみましょう。プライベートメソッドから始めます。
float RoundedPolygon::GetDistance(QPoint pt1, QPoint pt2) const { float fD = (pt1.x() - pt2.x())*(pt1.x() - pt2.x()) + (pt1.y() - pt2.y()) * (pt1.y() - pt2.y()); return sqrtf(fD); }
ここで説明することはあまりありませんが、このメソッドは、指定された2点間のユークリッド距離を返します。
QPointF RoundedPolygon::GetLineStart(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX((1.0f-fRat)*pt1.x() + fRat*pt2.x()); pt.setY((1.0f-fRat)*pt1.y() + fRat*pt2.y()); return pt; }
GetLineStart
メソッドは、ポイントが時計回りにポリゴンに追加されている場合、最後の図からポイントP2の位置を計算します。 より正確には、 (i+1)
番目の頂点に向かう方向にi
番目の頂点からm_uiRadius
ピクセル離れた点を返します。 (i+1)
番目の頂点にアクセスするとき、ポリゴンには最後の頂点と最初の頂点の間に線分もあることを覚えておく必要があります。これにより、閉じた形状になり、式(i+1)%count()
。 これにより、メソッドが範囲外になるのを防ぎ、代わりに最初のポイントにアクセスします。 変数fRat
は、半径とi
番目の線分の長さの比率を保持します。 fRat
の値が0.5
を超えないようにするチェックもあります。 fRat
の値が0.5
を超えると、2つの連続する丸みを帯びた角が重なり、視覚的な結果が悪くなります。

ポイントP1からP2まで直線で移動し、距離の30%を完了すると、式0.7•P 1 + 0.3•P2を使用して位置を特定できます。 一般に、全距離の一部を達成し、 α= 1が全距離を表す場合、現在の位置は(1-α)•P1+α•P2になります。
これは、 GetLineStart
メソッドが、 i
番目の頂点から(i+1)
番目の方向にm_uiRadius
ピクセル離れたポイントの位置を決定する方法です。
QPointF RoundedPolygon::GetLineEnd(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX(fRat*pt1.x() + (1.0f - fRat)*pt2.x()); pt.setY(fRat*pt1.y() + (1.0f - fRat)*pt2.y()); return pt; }
このメソッドは、 GetLineStart
と非常によく似ています。 これは、 i
番目ではなく(i+1)
番目の頂点の点P0の位置を計算します。 つまり、 0
からn-1
までのすべてのi
についてGetLineEnd(i)
GetLineStart(i)
に線を引くと、 n
はポリゴン内の頂点の数であり、頂点が消去されたポリゴンとその周囲の近く。
そして今、メインクラスのメソッド:
const QPainterPath& RoundedPolygon::GetPath() { m_path = QPainterPath(); if (count() < 3) { qWarning() << "Polygon should have at least 3 points!"; return m_path; } QPointF pt1; QPointF pt2; for (int i = 0; i < count(); i++) { pt1 = GetLineStart(i); if (i == 0) m_path.moveTo(pt1); else m_path.quadTo(at(i), pt1); pt2 = GetLineEnd(i); m_path.lineTo(pt2); } // close the last corner pt1 = GetLineStart(0); m_path.quadTo(at(0), pt1); return m_path; }
このメソッドでは、 QPainterPath
オブジェクトを作成します。 ポリゴンに少なくとも3つの頂点がない場合、2D形状を処理しなくなります。この場合、メソッドは警告を発行し、空のパスを返します。 十分なポイントが利用可能になると、ポリゴンのすべての直線セグメントをループし(もちろん、線分の数は頂点の数に等しくなります)、丸められた間の各直線セグメントの開始と終了を計算します。コーナー。 現在の頂点の位置を制御点として使用して、これら2つの点の間に直線を置き、前の線分の終わりと電流の始まりの間に2次ベジェ曲線を置きます。 ループの後、最後の線分と最初の線分の間のベジェ曲線でパスを閉じる必要があります。これは、ループではベジェ曲線よりも1本多く直線を描いたためです。
クラスRoundedPolygon
使用法と結果
次に、このクラスを実際に使用する方法を確認します。
QPixmap pix1(300, 200); QPixmap pix2(300, 200); pix1.fill(Qt::white); pix2.fill(Qt::white); QPainter P1(&pix1); QPainter P2(&pix2); P1.setRenderHints(QPainter::Antialiasing); P2.setRenderHints(QPainter::Antialiasing); P1.setPen(QPen(Qt::blue, 2)); P1.setBrush(Qt::red); P2.setPen(QPen(Qt::blue, 2)); P2.setBrush(Qt::red); RoundedPolygon poly; poly << QPoint(147, 187) << QPoint(95, 187) << QPoint(100, 175) << QPoint(145, 165) << QPoint(140, 95) << QPoint(5, 85) << QPoint(5, 70) << QPoint(140, 70) << QPoint(135, 45) << QPoint(138, 25) << QPoint(145, 5) << QPoint(155, 5) << QPoint(162, 25) << QPoint(165, 45) << QPoint(160, 70) << QPoint(295, 70) << QPoint(295, 85) << QPoint(160, 95) << QPoint(155, 165) << QPoint(200, 175) << QPoint(205, 187) << QPoint(153, 187) << QPoint(150, 199); P1.drawPolygon(poly); P2.drawPath(poly.GetPath()); pix1.save("1.png"); pix2.save("2.png");
このソースコードは非常に単純です。 2つのQPixmaps
とそれらのQPainters
を初期化した後、 RoundedPolygon
オブジェクトを作成し、ポイントで塗りつぶします。 Painter P1
は正多角形を描画し、 P2
は多角形から生成された角の丸いQPainterPath
を描画します。 結果の両方のピックスマップがファイルに保存され、結果は次のようになります。
結論
特にQtなどの優れたプログラミングフレームワークを使用する場合、ポリゴンから角が丸い形状を生成することは、結局のところそれほど難しくないことがわかりました。 このプロセスは、概念実証としてこのブログで説明したクラスによって自動化できます。 ただし、次のような改善の余地はまだたくさんあります。
- 選択した頂点でのみ丸みを帯びた角を作成し、すべての頂点では作成しません。
- さまざまな頂点でさまざまな半径の丸みを帯びたコーナーを作成します。
- 角が丸いポリラインを生成するメソッドを実装します(Qt用語のポリラインは、最後の頂点と最初の頂点の間の線分が欠落しているため、閉じた形状ではないことを除いて、ポリゴンと同じです)。
-
RoundedPolygon
を使用してビットマップを生成します。これを背景ウィジェットマスクとして使用して、クレイジーな形のウィジェットを作成できます。 -
RoundedPolygon
クラスは、実行速度が最適化されていません。 コンセプトをわかりやすくするためにそのままにしておきました。 最適化には、ポリゴンに新しい頂点を追加するときに多くの中間値を計算することが含まれる場合があります。 また、GetPath
が生成されたQPainterPath
への参照を返そうとしているときに、オブジェクトが最新であることを示すフラグを設定する可能性があります。GetPath
を次に呼び出すと、何も再計算せずに、同じQPainterPath
オブジェクトのみが返されます。 ただし、開発者は、ポリゴンの頂点が変更されるたびに、また新しい頂点ごとにこのフラグがクリアされるようにする必要があります。これにより、最適化されたクラスは、派生ではなくゼロから開発する方がよいと思います。QPolygon
から。 幸いなことに、これは思ったほど難しくはありません。
全体として、 RoundedPolygon
クラスは、ピックスマップやシェイプを事前に準備しなくても、GUIにデザイナータッチをその場で追加したいときにいつでもツールとして使用できます。