Windows API - グラフ描画クラス編 第4回 〜グラフ描画クラス実装〜

描画クラスの実装

SubPlot() 静的メソッド

スタティックコントロールを指定した行・列数のグラフに分割した指定インデックスの CVAxes& を取得し、 存在しない場合は生成する。

CVAxes& CSGraphWnd::sSubPlot(
 DWORD nRow, DWORD nCol, DWORD nIndex){
 CVFigure& cf=sGcf();
 CVstdFigure* axesItr=cf.pChildren;
 bool toNewAxes=!axesItr;
 for(DWORD i=0;!toNewAxes&&i<nRow*nCol-1;++i)
 toNewAxes=!(axesItr=axesItr->pNext);
 if(!toNewAxes) return *reinterpret_cast<CVAxes*>(axesItr);
 CVAxes& ca=cf.mAxes();
 const int wMr=130, hMr=110;
 const int wVr=(1000-wMr*(nCol+1))/nCol;
 const int hVr=(1000-hMr*(nRow+1))/nRow;
 SetRect(&ca.pPosition,
  wVr*(nIndex%nRow)+wMr*(nIndex%nRow+1),
  hVr*(nIndex/nRow)+hMr*(nIndex/nRow+1),
  wVr*(nIndex%nRow)+wMr*(nIndex%nRow+1)+wVr,
  hVr*(nIndex/nRow)+hMr*(nIndex/nRow+1)+hVr
 );
 ca.mResetLim();
 return ca;
}

サブクラス化

メモリ DC へのダブルバッファリングをするためのスタティックコントロールのサブクラス化について復習する。

まず、親ウィンドウハンドル hWndParent に対して、InitWnd() メソッドがウィンドウプロシージャを、 CSGraphWnd::mWndProc() メソッドを呼び出す StaticProc に切り替えると同時に、 Figure 構造体、及び メモリ DC と ビットマップを初期化する。

void CSGraphWnd::mInitWnd(HWND hParentWnd){
 RECT wndRect={0};
 GetClientRect(hParentWnd,&wndRect);
 hStatic=CreateWindow(
  TEXT("STATIC"), TEXT(""), 
  WS_CHILD | WS_VISIBLE | SS_BITMAP,
  0, 0, wndRect.right, wndRect.bottom, hParentWnd, NULL,
  (HINSTANCE)GetWindowLong(hParentWnd,GWL_HINSTANCE), NULL
 );
 pPrevWndProc=(WNDPROC)SetWindowLong(
  hStatic,GWL_WNDPROC,(LONG)StaticProc
 );
 pFigure=new CVFigure;
 GetClientRect(hStatic,&pFigure->pPosition);
 hClientDc=GetDC(hStatic);
 hMemDc=CreateCompatibleDC(hClientDc);
 hOldBitmap=SelectObject(
  hMemDc,CreateCompatibleBitmap(hClientDc,0,0)
 );
 ReleaseDC(hStatic,hClientDc);
}

肝心の StaticProc が呼び出そうとする mWndProc は、WM_PAINT と WM_DESTROY の2つを処理する。

LRESULT CSGraphWnd::mWndProc(
 HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
 switch(uMsg){
 case WM_PAINT:
  onWmPaint();
  break;
 case WM_DESTROY:
  onWmDestroy();
  break;
 default:
  return CallWindowProc(pPrevWndProc,hWnd,uMsg,wParam,lParam);
 }
 return 0;
}

最後に、onWmDestroy() イベントハンドラは、グラフの解放とビットマップ・メモリ DC の解放を行うことになる。

void CSGraphWnd::onWmDestroy(void){
 pFigure->mClf();
 delete pFigure;
 DeleteObject(SelectObject(hMemDc,hOldBitmap));
 DeleteDC(hMemDc);
}

ここまでがサブクラス化の復習となる。

mResize() メソッド

親ウィンドウが WM_SIZE メッセージを処理する場合、CSGraphWnd オブジェクトの mResize() メソッドを呼ぶ。

void CSGraphView::mResize(WORD width, WORD height){
 CVFigure& cf=sGcf();
 cf.pPosition.right=width;
 cf.pPosition.bottom=height;
 for(CVstdFigure* itr=cf.pChildren;itr;itr=itr->pNext)
 reinterpret_cast<CVAxes*>(itr)->mResetLim();
 MoveWindow(hStatic,0,0,width,height,FALSE);
 InvalidateRect(hStatic,NULL,pFlag|=0x00000001);
}

親ウィンドウのサイズ変更に伴ってスタティックコントロールの大きさも変える。 実際の CVAxes の幅や高さが変わるため、それにともなって mResetLim() メソッドを呼ぶことで、 目盛りの間隔などを変えられる。

スタティックコントロールのサイズは、MoveWindow() API 関数で変更でき、この後は、 いつも通り、InvalidateRect() API 関数を使うことで全面を再描画するが、もちろん、軸が変わる可能性があるため、 結局、全体を再描画させるために、再描画フラグを立てる。

再描画の実装

基本的な流れはメモリ DC をスタティックコントロールのクライアント DC にコピーすることである。

onWmPaint() イベントハンドラ

void CSGraphWnd::onWmPaint(void){
 CVFigure& cf=sGcf();
 if(pFlag&0x00000001) mDraw(cf);
 hClientDc=BeginPaint(hStatic,&pPs);
 BitBlt(hClientDc,
  pPs.rcPaint.left,pPs.rcPaint.top,
  pPs.rcPaint.right-pPs.rcPaint.left,
  pPs.rcPaint.bottom-pPs.rcPaint.top,
  hMemDc,pPs.rcPaint.left,pPs.rcPaint.top,SRCCOPY
 );
 EndPaint(hStatic,&pPs);
}

今回は、PAINTSTRUCT 構造体の rcPaint という情報を使う。これは、InvalidateRect() の第2引数に 対応するものと考えればよい。つまり、この情報を使うことで、全体ではなく無効領域のみをコピーすることが出来る。

Figure 構造体の再描画

sGcf() は名前の通り、*CVFigure::sCf を参照する静的メソッドであるが、 Flag の第1ビットが立っているとき、第2行にあるようにここで取得される CVFigure& を引数として、 mDraw() メソッドというものが呼び出される。

void CSGraphWnd::mDraw(CVFigure& rFigure){
 HBRUSH hNewBrush;
 DeleteObject(SelectObject(hMemDc,hOldBitmap));
 hClientDc=GetDC(hStatic);
 hOldBitmap=SelectObject(
  hMemDc,CreateCompatibleBitmap(
   hClientDc=GetDC(hStatic),
   rFigure.pPosition.right,rFigure.pPosition.bottom
  )
 );
 ReleaseDC(hStatic,hClientDc);
 FillRect(
  hMemDc,&rFigure.pPosition,
  hNewBrush=CreateBrushIndirect(&rFigure.pColor)
  );
 SetBkColor(hMemDc,rFigure.pColor.lbColor);
 DeleteObject(hNewBrush);
 for(
  CVstdFigure* axesItr=rFigure.pChildren;
  axesItr; axesItr=axesItr->pNext
 )mDraw(*reinterpret_cast<CVAxes*>(axesItr));
 InvalidateRect(hStatic,NULL,TRUE);
 pFlag&=~0x00000001;
}

大きく分けると、まず、ビットマップの初期化を行い、続いて、全ての軸を引数として mDraw() メソッドを呼ぶ。 そして、最後にウィンドウ全体を再描画し、再描画フラグを下ろしている。

つまり、再描画フラグが立っているときにだけ再描画をスタティックコントロール全面に対して行い、 これ以降は、メモリ DC に発生した無効領域に限定して再描画を行うことが出来る。

Line 構造体の再描画

mDraw(CVAxes&) は当然、mDraw(CVLine&) を呼び出すことになるので、まずこちらを先に考えてみる。 グラフの描画を突き詰めていけば、データをスタティックコントロール上の点に変換し、前に配置した点と線でつなげば いいだけの話である。

void CSGraphWnd::mDraw(CVLine& rLine){
 POINT tDrawPoint={0}, isReadyChk={0};
 CVAxes& axesItr=*reinterpret_cast<CVAxes*>(rLine.pParent);
 hOldPen=SelectObject(hMemDc,CreatePenIndirect(&rLine.pColor));
 for(DWORD i=0;i<rLine.pLength;++i){
  axesItr.mGetPoint(
   tDrawPoint,rLine.pData[0][i],rLine.pData[1][i]
  );
  if(axesItr.mIsValidPoint(tDrawPoint)){
   if(isReadyChk.x) LineTo(hMemDc,tDrawPoint.x,tDrawPoint.y);
   else MoveToEx(hMemDc,tDrawPoint.x,tDrawPoint.y,&isReadyChk);
  }
  ...
 }
 DeleteObject(SelectObject(hMemDc,hOldPen));
} 

前回書いたように、GetPoint() メソッドで変換し、これが IsValidPoint() メソッドで正しい点であることを確認する。 MoveToEx() API 関数の第5引数が描画先を変更する前の位置を返すため、ここで有効な値が返ってくるかどうかで、 LineTo() API 関数を呼ぶかどうかを決めている。

今回は省略するが、CVLine::pMarker を参照して、マーカーを描画する記述を実際には追加することになる。

さて、現状では、ただスタティックコントロールにでたらめに直線がたくさん引かれるだけである。  どれだけグラフとして有効な情報を持つかは、Axes 構造体をどう描画するかにかかっていると言える。

今回は、2次元線形等間隔軸を例にどんな実装になりそうなのかを説明する。 大まかに分ければ、グラフ領域の初期描画・軸の描画・Line 構造体の描画 となる。

グラフ領域の初期描画

void CSGraphWnd::mDraw(CVAxes& rAxes){
 HBRUSH hNewBrush;
 POINT point, fontBox;
 double diff=0., current=0.;
 UINT yFlag=DT_SINGLELINE|DT_RIGHT|DT_VCENTER;
 UINT xFlag=DT_SINGLELINE|DT_CENTER|DT_TOP;
 RECT pos;
 TEXTMETRIC tm;	
 CVAxisProp& X=*reinterpret_cast<CVAxisProp*>(
  rAxes.pAxes.pChildren
 );
 CVAxisProp& Y=*reinterpret_cast<CVAxisProp*>(X.pNext);
 if(X.pLim[0]>=X.pLim[2]||Y.pLim[0]>Y.pLim[2]) return;
 rAxes.mGetPos(pos);
 FillRect(hMemDc,&pos,hNewBrush=CreateBrushIndirect(
  &rAxes.pColor)
 );
 DeleteObject(hNewBrush);
 hOldFont=SelectObject(hMemDc,CreateFontIndirect(&rAxes.pFont));
 GetTextMetrics(hMemDc,&tm);
 fontBox.x=tm.tmAveCharWidth*8+2;
 fontBox.y=tm.tmHeight+2;
 ...

描画そのものは、FillRect() というAPI 関数がしているわけで、それ以外は、CVAxisProp 構造体を解釈して、 X, Y という描画情報を取得している。

DrawValue() メソッド

それ以外にもごちゃごちゃとしているが、実は、目盛りを描画するためにフォントやメトリクスをいろいろといじっている。 目盛りを描画するためにはどうすればいいかといえば、結局、目盛りに相当する double 型の値を、 目盛りの位置を表す RECT 構造体内に書いてやればいいだけである。

void CSGraphWnd::mDrawValue(double rValue,
 LONG left, LONG top, LONG right, LONG bottom,
 UINT nFlag, LPCWSTR rFormat){
 WCHAR buf[8]={L'\0'};
 RECT fontPos={left,top,right,bottom};
 swprintf(buf,8,rFormat,rValue);
 DrawText(hMemDc,buf,-1,&fontPos,nFlag);
}

その目的は、DrawText() API 関数によって達成されるので、その前に swprintf() CRT 関数で double 型の値を、 Unicode 文字列に変換しておく。

軸の描画

DrawValue() メソッドを説明したところで、mDraw(CVAxes&) メソッドの実装の続きである。

 ...
 hOldPen=SelectObject(hMemDc,CreatePenIndirect(&X.pColor));
 MoveToEx(hMemDc,pos.left,pos.bottom,NULL);
 LineTo(hMemDc,pos.right,pos.bottom);
 current=X.pLim[0];
 if(fabs(diff=fmod(X.pLim[0],X.pLim[1]))>DBL_EPSILON)
 current-=fabs(diff);
 else mDrawValue(
  current,pos.left-fontBox.x/2,pos.bottom+fontBox.y/2-2,
  pos.left+fontBox.x/2,pos.bottom+fontBox.y*3/2,xFlag
 );
 while((current+=X.pLim[1])<=X.pLim[2]){
  rAxes.mGetPoint(point,current,Y.pLim[0]);
  mDrawValue(
   current,point.x-fontBox.x/2,point.y+fontBox.y/2-2,
   point.x+fontBox.x/2,point.y+fontBox.y*3/2,xFlag
  );
  MoveToEx(hMemDc,point.x,point.y,NULL);
  LineTo(hMemDc,point.x,point.y-5);
 }
 DeleteObject(SelectObject(hMemDc,CreatePenIndirect(&Y.pColor)));	
 ...

細かいことを抜きにすれば、要するに、まず x 軸そのものを描画し、X.pLim[1] の情報に従って、 軸の上を等間隔に刻んでいき、その近くに DrawValue() で目盛りを記録していくという作業をしている。

最後に、Y.pColor からペンを生成していることから分かるように、後は、Y 軸を同じように描画するだけである。

Line 構造体の描画

というわけで、Y 軸が描画し終われば、いよいよ、Line 構造体を描画するだけである。

 ...
 DeleteObject(SelectObject(hMemDc,hOldPen));
 DeleteObject(SelectObject(hMemDc,hOldFont));
 for(CVstdFigure* lineItr=rAxes.pChildren;
 lineItr; lineItr=lineItr->pNext)
 mDraw(*reinterpret_cast<CVLine*>(lineItr));
}

こんな流れで再描画を実装していきます。 


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