Windows API - グラフ描画クラス編 第3回 〜グラフ描画クラス設計〜

グラフ 基本構造体

グラフを表現するために必要なデータは大きく分けると、プロットする点や線のデータと、 その位置をグラフ上に対応させる軸のデータの2つである。

これらを構造体としてまとめ、それを有機的に結合するために、これから先のデータを表現するための 構造体すべてを、ある基底クラスの連結による木構造として実装する。

CVstdFigure 構造体

struct CVstdFigure{
 CVstdFigure* pParent;
 CVstdFigure* pChildren;
 CVstdFigure* pNext;
 CVstdFigure* pPrev;
 static CVstdFigure* sCo;
 CVstdFigure(void);
 CVstdFigure* mInsert(CVstdFigure* newData);
 void mErase(void);
};

見ての通り、CVstdFigure の継承インスタンスは必ず、Parent, Children, Next, Prev という4つの方向へのポインタを メンバとして持ちます。

また初期化用のコンストラクタと、現在参照している CVstdFigure インスタンスへのポインタを保持する静的メンバ、 及び、Children に対するインスタンスの追加と削除を表す Insert メソッドと Erase メソッドを持ちます。

Insert メソッド

引数のインスタンスを Children から辿れるリストの末尾に追加する。Children の Prev を末尾に割り当てるとすれば、 パターンとして、Children がない場合、Children->Prev がない場合、Children->Prev がある場合の3パターンが考えられる。

CVstdFigure* CVstdFigure::mInsert(CVstdFigure* newData){
 newData->pParent=this;
 if(pChildren){
  if(pChildren-gt;pPrev){
   newData->pPrev=pChildren->pPrev;
   pChildren->pPrev=pChildren->pPrev->pNext=newData;
  }else{
   newData->pPrev=pChildren;
   pChildren->pPrev=pChildren->pNext=newData;
  }
 }else pChildren=newData;
 return newData;
}

Erase メソッド

Children から Next を通じてたどれる全てのリストを削除する。連鎖的に各インスタンスの Children に対して、 Erase メソッドを呼ぶため、ただ1度呼ぶだけで、Children から先に接続される全てのインスタンスが消去される。

なお、線形リストの削除は、常套手段として必ず次のように2つのイテレータを用いる。

void CVstdFigure::mErase(void){
 CVstdFigure *itr=pChildren, *next=0;
 if(!itr) return;
 while(itr){
  next=itr->pNext;
  itr->mErase();
  delete itr;
  itr=next;
 }
}

Line 構造体

プロットするためのデータとともに、線や点の情報を持つ。プロットするために必要な情報であるが、 これがどのようにプロットされるかは一切 Line 構造体は関知しない。つまり、コンストラクタとデストラクタ以外に、 メソッドは何もない。

CVLine 構造体

struct CVLine : public CVstdFigure{
 DWORD pLength;
 LOGPEN pColor;
 CVstdFigure pMarker;
 double** pData;
 CVLine(void);
 ~CVLine(void);
};

主となるメンバは、データ長を表す Length、線種を表す Color、実際のデータが記録される Data と、 プロットポイントに対する点種を格納する Marker の4つとなる。

Marker の実装は CVstdFigure を継承するインスタンスであればどんなものでも pMarker.pChildren に格納できるため、 様々なプロット形式に合わせてこれをカスタマイズすることが出来る。

コンストラクタとデストラクタ

pLength と pData はデータを追加する操作をした時に書き換えることになる。 pData は次元の数だけ double* 型を持つため、例えば 2 次元ならば、

pData=new double*[2];
pData[0]=pData[1]=0; 

ということになる。必然的にデストラクタは *pData が持つ全ての配列を解放しつつ、pData 自身を解放することとなる。

pMarker の構築と解体をコンストラクタとデストラクタに任せることとなる。

Axes 構造体

Line 構造体の情報を実際にグラフにプロットするための変換を司る、グラフ描画アルゴリズムの心臓とも言える構造体である。

CVAxes 構造体

struct CVAxes : public CVstdFigure{
 DWORD pDim;
 RECT pPosition;
 LOGBRUSH pColor;
 LOGFONT pFont;
 CVstdFigure pAxes;	
 static CVAxes* sCa;	
 CVAxes(void);
 ~CVAxes(void);	
 CVLine& mLine(DWORD nLength, double rXData[], double rYData[]);
 void mGetPos(RECT& tDrawPos);
 void mGetPoint(POINT& tDrawPoint, double nXPos, double nYPos);
 void mResetLim(void);
 BOOL mIsValidPoint(POINT rDrawPoint);
};

次元を表す Dim, 軸の相対位置を表す Position, グラフ背景色である Color, ラベルのフォントである Font, そして、実際の軸を表現する Axes という5つのメンバを持つ。

Axes の実装もまた、CVstdFigure を継承するインスタンスであれば格納できる。

静的メンバ Ca は現在参照される軸であり、コンストラクタ・デストラクタはこれまで通り初期化と後処理に使われる。 さらに、要所要所で使われるメソッドとして 5つの操作をここでは提供する。

GetPos() は相対位置を絶対位置に変換する。相対位置で記録していれば、描画ウィンドウが如何なるサイズであろうと、 常に定数として Position を記録できるが、当然描画する場合はこれを絶対位置に変換しなくてはならない。

そして、Line 構造体に記録されるデータを絶対位置に変換するのが、GetPoint() メソッドであるが、 この結果、変換されたプロット点がプロットウィンドウの範囲内かどうかをチェックするのが IsValidPoint() メソッドとなる。

CVAxisProp 構造体

例として、最大・最小の境界値とその間を線形に等間隔で分割するような軸にする。 当然、CVAxes::pAxes.pChildren メンバからたどる線形リストにこのインスタンスを連結することでこれを定義する。

struct CVAxisProp : public CVstdFigure{
 DWORD pMode;
 LOGPEN pColor;
 double pLim[3];	
 CVAxisProp(void);
 void mResetLim(DWORD rLineIndex, int minSect=5, int maxSect=11);
 static double sTick(
  double minVal, double maxVal, double minSect, double maxSect
 );
};

コロン演算子的に書けば、軸は、pLim[0] : pLim[1] : pLim[2] となるように配置されることになる。 Mode はこれを自動的に算出するか固定とするかをフラグとして持つものであり、Color が軸の色となる。

sTick 静的メソッドは、引数を元に初期値、あるいは、逐次適切な pLim[1] を計算するものである。

ResetLim() メソッド

同名のメソッドが CVAxes 構造体と CVAxisProp 構造体双方にあり、当然ながら、CVAxisProp::mResetLim() は、 CVAxes::mResetLim() から呼び出される。

void CVAxes::mResetLim(void){
 RECT pos;
 mGetPos(pos);
 LONG width=pos.right-pos.left;
 CVAxisProp& X=*reinterpret_cast<CVAxisProp*>(pAxes.pChildren);
 if(width>350) X.mResetLim(0);
 else if(width>200) X.mResetLim(0,3,6);
 else X.mResetLim(0,2,3);
 LONG height=pos.bottom-pos.top;	
 CVAxisProp& Y=*reinterpret_cast<CVAxisProp*>(X.pNext);
 if(height>250) Y.mResetLim(1);
 else if(height>100) Y.mResetLim(1,3,6);
 else Y.mResetLim(1,2,3);
}

いろいろとごちゃごちゃ書いてるが、本質は pAxes.pChildren 経由で CVAxisProp::mResetLim() を呼ぶこと、 ただそれだけである。GetPos() を使って絶対位置を取得することで、軸の幅と高さを取得し、 それによって目盛りの数を調整しています。

Line() メソッド

CVLine では定義しなかった初期化のためのメソッドである。

CVLine& CVAxes::mLine(
 DWORD nLength, double rXData[], double rYData[]){
 CVLine& tLine=*reinterpret_cast<CVLine*>(mInsert(new CVLine));
 if(nLength>0){
  tLine.pLength=nLength;
  if(rXData){
   tLine.pData[0]=new double[nLength];
   CopyMemory(tLine.pData[0],rXData,nLength*sizeof(double));
  }
  if(rYData){
   tLine.pData[1]=new double[nLength];
   CopyMemory(tLine.pData[1],rYData,nLength*sizeof(double));
  }
 }
 return tLine;
}

描画クラスと Figure 構造体

CSGraphWnd クラス

スタティックコントロールのサブクラス化によって定義を行う。

class CSGraphWnd{
 HWND hStatic;
 WNDPROC pPrevWndProc;
 PAINTSTRUCT pPs;
 HDC hClientDc, hMemDc;
 HGDIOBJ hOldBitmap, hOldPen, hOldFont;
 CVFigure* pFigure;
 DWORD pFlag;
public:
 void mInitWnd(HWND hWndParent);
 LRESULT mWndProc(
   HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam
  );
 void onWmPaint(void);
 void onWmDestroy(void);
 void mDraw(CVFigure& rFigure);
 void mDraw(CVAxes& rAxes);
 void mDraw(CVLine& rLine);
 void mDrawValue(double rValue,
  LONG left, LONG top, LONG right, LONG bottom,
  UINT nFlag, LPCWSTR rFormat=TEXT("%6.2f")
 );
 void mResize(WORD width, WORD height);
 static CVAxes& sGca(void);
 static CVFigure& sGcf(void);
 static CVstdFigure& sGco(void);	
 static CVLine& sPlot(
   DWORD nLength, double rXData[], double rYData[]
  );
 static CVAxes& sSubPlot(DWORD nRow, DWORD nCol, DWORD nIndex);
};

hStatic から hOldBitmap までがこれまで説明した、メモリ DC へのダブルバッファリングに伴う スタティックコントロールのサブクラス化に必要なプロパティである。

Flag はその名の通り、フラグを管理するもので、ここでは再描画に対するフラグとして利用する。

CVFigure 構造体

CSGraphWnd クラスのプロパティであり、軸や線・点の情報であるグラフ木構造の根である。

struct CVFigure : public CVstdFigure{
 RECT pPosition;
 LOGBRUSH pColor;	
 static CVFigure* sCf;
 CVFigure(void);	
 CVAxes& mAxes(void);
 void mClf(void);
};

Position は、子である CVAxes の相対座標から絶対座標へ変換するために必要となるもので、 静的メンバ Gcf は、現在参照している Figure インスタンスを格納する。

Clf() メソッドは、Erase() メソッドを呼ぶだけの簡単な実装ではあるが、Figure 構造体からアクセスできる ありとあらゆる子インスタンスを全て削除することが出来るという意味で、特別に定義しておくものである。

sPlot() 静的メソッド

現在選択されている CVAxes& に対して、データを追加プロットする。

sGca() は、現在選択されている CVAxes& を取得する静的メソッドで、想像通り *CVAxes::sCa を返す。 sCa が 0 の場合は、スタティックコントロール全面をプロット面とする。

CVAxes& CSGraphWnd::sGca(void){
 if(!CVAxes::sCa) sSubPlot(1,1,0);
 return *CVAxes::sCa;
}
CVLine& CSGraphWnd::sPlot(
 DWORD nLength, double rXData[], double rYData[]){
 CVAxes& ca=sGca();
 CVLine& co=ca.mLine(nLength,rXData,rYData);	
 return co;
}

次回より、この sPlot() によって作られたグラフ構造体を描画する機構を CSGraphWnd クラスに実装していく。


文章作成 : yukki-ts (-+-twilight serenade-+- [stage])