2016. 1. 29.
石立 喬

Visual Studio Community 2015の使い方(8)

―― マウスで座標位置を入力して、回帰直線を求める ――

 マウスで座標上の位置をデータとして入力したいことがある。画面上に方眼紙を用意し、マウスで点を入力するプログラムは、Windowsフォームを用いたものが、「Visual C++ 2010 Expressの易しい使い方(17)」、「同(18)」で紹介してある。
 ここでは、これと同様のプログラムを、Win32で作成した。ただし、マウス入力の方法の解説を主眼としているので、入力点を結ぶ方法は、一次式への当てはめ(回帰直線の作成)の簡単なものにとどめた。「当てはめ」の詳細については、「Visual C++ 2010 Expressの易しい使い方(18)」を参照されたい。また、C++の比較的新しい機能のvectorを使用しているが、この詳細については、「Visual Studio Express (Visual C++)の新しい使い方(1)」にある。

マウス関連のメッセージ
 画面上でマウスを移動させると、WM_MOUSEMOVEのメッセージがウィンドウに送られる。
 WM_MOUSEMOVEメッセージのwParamには、「SHIFT」キーや「CTRL」キーなどが同時に押されている場合には、MK_SHIFTやMK_CTRLなどが入っている。lParamには、マウスの座標位置が入っていて、
   nXPos = GET_X_LPARAM(lParam);
   nYPos = GET_Y_LPARAM(lParam);
で得られる。GET_X_LPARAMなどのマクロを使用するには、"Windowsx.h"のインクルードが必要である。
 マウスの左ボタンを押し下げる(クリックと同様に使用できる)と、WM_LBUTTONDOWNのメッセージが送られる。wParamとlParamは、WM_MOUSEMOVEの場合と同じである。

回帰直線(regression line)
 統計学で用いられるもので、二次元グラフ上の各データ点に対して、誤差が最も少ない直線を回帰直線と呼び、最小二乗法で求める。この直線を一次式で表したものが回帰式、または回帰直線の式で、これを y = ax + b とすると、
    
で求まる。

マウスを使ってグラフ用紙上に測定点を設定し、回帰直線を求めるプログラム
 画面上に、座標のピクセル値で480 x 320、グラフ上の値で60 x 40の方眼紙を作成し、目盛を付ける。マウスを方眼紙上で移動すると、その位置に対応するグラフ値が画面左下に表示される。マウスを方眼紙上でクリックすると、その場所に赤丸が描かれ、データが格納される。右上の「回帰直線」ボタンをクリックすると、回帰直線が表示され、直線を表す式が右下に表示される。「クリヤー」ボタンを押すと、赤丸と回帰直線が消える。
 マウスやボタンに対するイベント処理は、別途個別に紹介することにして、プログラムは下記から成る。
 1)二個のボタンの識別子を定義する(図1)。
 2)入力点座標と換算したグラフ値を総合したDATAPOINTクラスを定義し、グローバル変数を宣言する(図1)。
 3)メインウィンドウと二個のボタンを作成する(図2)。
 4)画面上に方眼紙を作成し、マウスで入力した点があれば、それを赤丸で表示する(図7)。
◎マウスを移動させたときの処理(図3)
 WndProcのcase WM_MOUSEMOVEに記述する。
 1)WM_MOUSEMOVEメッセージのlParamから、座標上の位置を求め、uXPos、uYPosに格納する。
 2)その位置が、方眼紙内であれば、グラフ値に換算してfXValue、fYValueに格納し、画面下方に表示する。
 3)位置が方眼紙外であれば、「out of range」と表示する。
◎マウスをクリックしたときの処理(図4)
 WndProcのcase WM_LBUTTONDOWNに記述する。
 1)すでに、WM_MOUSEMOVEで得られているuXPos、uYPosとfXValue、fYValueをvector型のDataに格納する。
 2)赤丸を表示するために、InvalidateRect関数でWM_PAINTを呼び出す。ただし、最後の引数をFALSEにして、グラフ値表示を残す。
◎「回帰直線」ボタンが押されたときの処理(図5)
 WndProcのccase WM_COMMAND / case IDC_BUTTON1に記述する。
 1)vector型のDataを順次読み取り、Data[i].xとData[i].yから、それぞれの平均を求め、fAveX、fAveYとする。
 2)同様に、Data[i].x、Data[i].y、fAveX、fAveYを用いて、fSumXXとfSumXYを求める。
 3)回帰直線の係数fAと切片fBを求め、画面右下に、回帰直線の式として表示する。
 4)回帰直線を方眼紙上に描画する。
   ・方眼紙の左端から描画するが、もしY座標が方眼紙の範囲外の場合は、何もしない。
   ・最初に範囲内に入った場合には、bGraphAreaフラグをTRUEにし、座標位置をuXOld、uYOldに入れるのみとする。
   ・bGraphAreaがTRUEであれば、uXOld、uYOldからuX、uYへ向けて線を引く。ただし、uX、uYが範囲外の場合は描画しない。
◎「クリヤー」ボタンが押されたときの処理(図6)
 WndProcのccase WM_COMMAND / case IDC_BUTTON1に記述する。
 1)Dataをクリヤーし、メモリ領域を解放する。
 2)InvalidateRect関数により、WM_PAINTを実行する。赤丸は表示されなくなり、回帰曲線は消去される。

グローバル変数の使用について
 このプログラムはマウス移動に対して反応するので、WM_MOUSEMOVEのメッセージによって、WndProcコールバック関数が頻繁に呼び出される。このプロジェクトの変数は、、ほとんどWndProc関数内部のみで使用されるので、本来ならば、WndProc関数内部で、これらを宣言すべきである。この場合、WndProcが呼び出されるたびにリセットされるのでは困るので、static化しておく必要がある。
 結局、これらの変数は外部のグローバル変数と同等なので、グローバル変数として宣言した。
 ただし、Visual C++のテンプレートでは、hWndはInitInstance関数内部の変数に設定されていて、WndProc関数には引数として渡されるので、hdc = GetDC(hWnd);はWndProc内に残した。

vectorテンプレートクラスの使い方
 vectorは、動的に長さを変えることのできる配列である。ここで使用する場合のように、あらかじめ個数が決まっていないデータの集まりを操作するのに便利である。vectorを使用するには、<vector>ヘッダファイルをインクルードし、std名前空間を指定しておく。
 vectorオブジェクトに値を設定・追加するには、オブジェクト名をabcdとして、
    abcd.push_back(値);
を用いる。vectorの全てをスキャンするには、反復子(iterator、イテレータ)itによる、
    for(auto it = abcd.begin(); it != abcd.end(); ++it)
を用いる。
 クラス型のvectorで、メンバー変数memberを取得したい場合には、
    it->member または (*it).member
を用いる。
 vector全体を消去したい場合には、
    abcd.clear();
ですべての配列内の値を空にする(sizeを0にする)。しかし、確保されたメモリ領域は残っているので、これを解放するには、
    abcd.shurink_to_fit();
を使用する。

使用した関数の説明
◎InvalidateRect関数
 WM_PAINTを呼び出して、画面を描画するのに用いる。引数は、
  hWnd --------- 更新したい領域を含むウィンドウへのハンドル
  *lpRect ------- RECT構造体へのポインタ、NULLだと全領域を更新する。 NULLが一般的。
  bErase ------- 更新する領域の背景を、一旦消去するときはTRUE、しないときはFALSE。
である。.
◎MoveToEx関数
 線を書き始める始点の位置を設定するのに使用する。引数は、
  hdc ----------- デバイスコンテキストへのハンドル
  x ------------- X座標
  y ------------- Y座標
  lpPoint --------- POINT構造体へのポインタ。ここへ前の座標位置が入る。NULLだと入らない。一般的には、NULLで良い。
である。
◎LineTo関数
 上述のMoveToEx関数と対で用いられ、線の終点を与えて、線を引くのに使用する。引数は、
  hdc ----------- デバイスコンテキストへのハンドル
  nXEnd --------- 線の終点のX座標
  nYEnd --------- 線の終点のY座標
である。
◎CreatePen関数
 ペンを生成する。引数は、
  fnPenStyle ------ ペンのスタイルで、PS_SOLID、PS_DASH、PS_DOTなどから選んで指定する。
  nWidth --------- ペンの太さ、0にすると1ピクセル幅になる。
  crColor -------- COLORREF型の色で、一般にRGBマクロで指定する。
で、戻り値は、ペンを識別するハンドルである。使用するには、後述のSelectObject関数で設定する。
◎SelectObject関数
 ペンなどのオブジェクトを選択して、デバイスコンテキストへ組み込む関数で、引数は、
   hdc -------- デバイスコンテキストへのハンドル
   hgdiobj ----- 選択するオブジェクトへのハンドル、ペンの場合は、hPenなどが用いられる。
である。戻り値は、それまでのオブジェクトへのハンドルである。これは、元のオブジェクトに戻す必要がある場合に使用する。
◎DeleteObject関数
 使い終わったオブジェクトは、破棄しておく必要がある。引数はオブジェクトへのハンドルのみである。
◎Ellipse関数
 楕円(または円)を描く。その時点で設定されているペンで描かれ、引数は、
   hdc ----------- デバイスコンテキストへのハンドル
   nLeftRect ------ 楕円を囲む矩形の左端のX座標
   nTopRect ------ 楕円を囲む矩形の上端のY座標
   nRightRect ----- 楕円を囲む矩形の右端のX座標
   nBottomRect --- 楕円を囲む矩形の下端のY座標
である。



図1 最初の部分でボタンの識別子を設定し、自作のPOINTF構造体を定義する



図2 InitInstanceでメインウィンドウの他、ボタンを作成して識別子と関連づける



図3 WinProcの最初の部分に、マウス移動のイベントに対する処理を記述する



図4 マウスクリックに対しては、その座標位置、換算したデータ値をvector型のDataに格納し、赤丸で描画する




図5 「回帰直線」ボタンに対しては、回帰直線の式の係数を求め、式と直線を表示する



図6 「クリヤー」ボタンに対しては、vector型Dataをクリヤーして再描画する




図7 画面上に方眼紙を作成し、入力点があれば赤丸で表示する


得られた結果
 図8は、このプログラムを使用した一例である。マウスは、参考のために、グラフ用紙右上の入力点上に置いてあるので、そのデータが、画面左下に表示されている。


図8 マウス入力を終えて、「回帰直線」ボタンを押した結果






「Visual C++の勉強部屋」(目次)