[VC++] VisualC++ における矢印の描画

Windows プログラムで矢印を表示する必要が出てきたので、 いろいろ調べたのですが、Win32API には矢印を描画するという API を見つけることが出来ませんでした。

そこで、DC(Device Context) を利用して自分で矢印を描画することにしました。 このページではそのときに調べた内容をまとめます。

矢印の定義

このページで想定する矢印(Fig.1)は、

から構成され、羽根(Fletching)を持ちません。

矢印
Fig.1 矢印

また、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.2 矢印の回転

そこで、まずは原点を中心として矢印を回転させることだけを考えます。 矢印の回転には座標の回転の公式(Fig.3)を利用します(Fig.4)。

x の回転
y の回転
Fig.3 回転の公式

点の回転
Fig.4 点の回転

回転角 r の単位はラジアン(Radian)なので、角度から算出する際には

ラジアンの算出

という式を使います。

Windows の座標系と数学の座標系

また、気を付けなくてはいけないこととして、Windows の座標系があります。 Windows の座標系は数学で用いる座標系(Fig.5)と異なり、 y の向きが異なります(Fig.6)。 Windows の描画を扱うプログラムを書く際(特に数学系の計算を取り入れる場合)は、 この点に注意してプログラムを書く必要があるでしょう。 (回転の計算も、数学では時計の逆に回転しますが、 Windows でプログラムを書くと時計回りに回転をします)

数学の座標系
Fig.5 数学の座標系
Windows の座標系
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);

鏃部の回転(1)
Fig.7 鏃部の回転(1)
鏃部の回転(2)
Fig.8 鏃部の回転(2)

その後、矢尻の座標の分だけスライドして描画します(Fig.9)。

// POINT arrowHead;  // 矢の先頭(矢尻)

dc.MoveTo(arrowHead);
dc.LineTo(arrowHead + arrowHead_L);
dc.MoveTo(arrowHead);
dc.LineTo(arrowHead + arrowHead_R);

鏃部の移動
Fig.9 鏃部の移動

再描画の範囲

以上で描画のルーチンは完成ですが、描画に当たってもう一つ注意点があります。 サンプルコードのようにマウスの移動とともに矢印の描画を行う場合、 マウスの移動を行うたびに再描画処理が必要になります。

通常、再描画処理には 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 頂点を原点にする理由は、計算回数を減らすためです。 頂点が原点にある場合、頂点の座標を回転させなくて済むので計算回数が減ります:)