Windows プログラムで矢印を表示する必要が出てきたので、 いろいろ調べたのですが、Win32API には矢印を描画するという API を見つけることが出来ませんでした。
そこで、DC(Device Context) を利用して自分で矢印を描画することにしました。 このページではそのときに調べた内容をまとめます。
このページで想定する矢印(Fig.1)は、
から構成され、羽根(Fletching)を持ちません。
また、4 つの点を線で結ぶことで矢印を描画することが出来ます。
// POINT arrowTail; // 矢の頭 // POINT arrowHead; // 矢の先端(矢尻) // POINT arrowHead_L; // 鏃の左端 // POINT arrowHead_R; // 鏃の右端 dc.MoveTo(arrowTail); dc.LineTo(arrowHead); dc.LineTo(arrowHead_L); dc.MoveTo(arrowHead); dc.LineTo(arrowHead_R);
任意の二点間に矢印を描画するとなると、 CDC::LineTo() で線を引こうにも矢印の向きが 360 度回転する可能性があるので、 矢印の各部の座標を計算で求めなくては成りません(Fig.2)。
そこで、まずは原点を中心として矢印を回転させることだけを考えます。 矢印の回転には座標の回転の公式(Fig.3)を利用します(Fig.4)。
回転角 r の単位はラジアン(Radian)なので、角度から算出する際には
という式を使います。
また、気を付けなくてはいけないこととして、Windows の座標系があります。 Windows の座標系は数学で用いる座標系(Fig.5)と異なり、 y の向きが異なります(Fig.6)。 Windows の描画を扱うプログラムを書く際(特に数学系の計算を取り入れる場合)は、 この点に注意してプログラムを書く必要があるでしょう。 (回転の計算も、数学では時計の逆に回転しますが、 Windows でプログラムを書くと時計回りに回転をします)
Fig.5 数学の座標系 |
Fig.6 Windows の座標系 |
原点を中心とした矢印の回転のサンプルプログラムは サンプルコードのダウンロード でダウンロードすることが出来ます。
次に、任意の二点間に矢印を描画することを考えます。 任意の二点間に矢印を描画する場合、原点を中心に矢印を回転させるのと比べると
という点が異なります。
回転角が不定である という問題は、アークタンジェント(arctan) を 利用することで解決できます。 具体的には、回転角 r は
#include <math.h> // atan2() を利用するために必要。 // POINT startPos; // 矢印の起点 // POINT endPos; // 矢印の終点(矢尻) double r = atan2(endPos.y - startPos.y, endPos.x - startPos.x);
という式で求めることが出来ます。
矢印の軸の長さ・矢尻の座標が不定である という問題は
ということで解決できます。
軸は任意の二点を直線で結ぶことで描画することができます。
// POINT startPos; // 矢印の起点 // POINT endPos; // 矢印の終点(矢尻) dc.MoveTo(startPos); dc.LineTo(endPos);
鏃の部分は頂点の部分を原点(0,0)に置いて右向きに用意し(Fig.7)、 それを回転角 r で回転させます *1 (Fig.8)。
// POINT arrowHead_L, arrowHead_R; // 鏃の左端(右端) POINT newArrowHead_L, newArrowHead_R; // 鏃の左端部 の回転 newArrowHead_L.x = arrowHead_L.x * cos(r) - arrowHead_L.y * sin(r); newArrowHead_L.y = arrowHead_L.x * sin(r) + arrowHead_L.y * cos(r); // 鏃の右端部 の回転 newArrowHead_R.x = arrowHead_R.x * cos(r) - arrowHead_R.y * sin(r); newArrowHead_R.y = arrowHead_R.x * sin(r) + arrowHead_R.y * cos(r);
Fig.7 鏃部の回転(1) |
Fig.8 鏃部の回転(2) |
その後、矢尻の座標の分だけスライドして描画します(Fig.9)。
// POINT arrowHead; // 矢の先頭(矢尻) dc.MoveTo(arrowHead); dc.LineTo(arrowHead + arrowHead_L); dc.MoveTo(arrowHead); dc.LineTo(arrowHead + arrowHead_R);
以上で描画のルーチンは完成ですが、描画に当たってもう一つ注意点があります。 サンプルコードのようにマウスの移動とともに矢印の描画を行う場合、 マウスの移動を行うたびに再描画処理が必要になります。
通常、再描画処理には API の CWnd::Invalidate() を用いますが、 矢印の再描画の再描画範囲は計算で求めることが出来るため、 再描画範囲を指定することのできる CWnd::InvalidateRect() を用いることで 再描画コストを下げることが出来ます。
CWnd::InvalidateRect() には再描画範囲を示した四角形 RECT を渡す必要があります。 この RECT は
// POINT startPos; // 矢印の起点 // POINT curPos; // 現在の矢印の終点(矢尻) // POINT newPos; // マウスが移動した座標(新しい矢印の終点) RECT rc; rc.left = min(startPos.x, min(curPos.x, newPos.x)); rc.top = min(startPos.y, min(curPos.y, newPos.y)); rc.right = max(startPos.x, max(curPos.x, newPos.x)); rc.bottom = max(startPos.y, max(curPos.y, newPos.y)); // 適当な大きさだけ RECT を拡張する(回転時の誤差) InflateRect(&rc, 30, 30);
という計算で求めることが出来ます。
Visual C++ 6.0 で作成したサンプルコードです。MFC を利用しています。
ここで公開するコードは BSD ライセンスで提供します。
*1 頂点を原点にする理由は、計算回数を減らすためです。
頂点が原点にある場合、頂点の座標を回転させなくて済むので計算回数が減ります:)