VC++5.0入門22
チュートリアル読書会4 98/4/19

こんにちは。ながいことお休みしましたが、みなさん、いかがお過ごしでしょう。リアルタイムで読んでくれている(た)みなさん、本当にご迷惑をおかけしました。2月19日に生まれた娘も元気で、生活も軌道に乗ってきました。(夜2時まで泣かれるというパターンが確立し、すばらしい寝不足の日々が、、、。)
さて、今週は「第5章ビュー」の話でしたね。ちょっと復習すると、まず、普通のMFCアプリケーションにはドキュメントとビューというものがある、ドキュメントはデータを管理するもので、ビューはそれをみせるもの、、、というところはよいですね。そして、前回までにドキュメントの大部分を書いたのでした。
さて、今回は、ビューですが、相変わらず初心者には読みづらいかと思います。私のコメントも相変わらずで、とりあえず手を動かしながら、わかるところから、わかっていけば良い、、、です。それで、実際に手を動かすのは、
    CStroke*   m_pStrokeCur;    // The stroke in progress
    CPoint     m_ptPrev;    // The last mouse pt in the stroke in progress
を追加するあたりからですね。基本的にデータはドキュメントにあるべきなのに、何でCStrokeなんて「データのクラス」があるんだと思った人は、実は、あと少しで(この辺のことは)全部わかるところまで来ています。もう少しがんばってみてください。(CStrokeは一筆書きの一筆に相当するのでしたね。このCStrokeオブジェクトの集まりがデータです。)
まあ、それはともかく、ビューであっても、作業用にデータは必要です。つまり、ビューがこのプログラムScribbleで、CStrokeを描画していく際に、現在描いているCStrokeを表す変数は(たぶん)必要なのです。それが、これです。(実際にはそのポインタです。)
もうひとつのCPointはなぞかもしれませんが、要するに画面上の点(の座標)を表す変数です。これがどう使われるかは、後で見てみましょう。

さて、このScribbleというプログラムは、線を描いていくプログラムですが、どのように線を描くのかについては、まだ、説明が書いてありません。それで、たくさんの人が戸惑ったかもしれませんね。
線を描くにはMoveToという関数とLineToという関数を使います。例えば、ふたつの点を結ぶ直線はどう描くのでしょう。、、、えーと、その前に「点」はどうやって表しますか?えっ、はい、そうですね。点はCPointというクラスで表現できるのでした。例えば、
    CPoint p1(0,50),p2(100,200);
とあれば、p1は座標(0$50)の点、p2は座標(100$200)の点を表しています。 では、このふたつの点を結ぶ直線を引くにはどうしたら良いのでしょう。そこで、MoveToとLineToの登場です。それは、簡単に書くと、
    MoveTo(p1);
    LineTo(p2);
こんな感じです。(正確な書き方はすぐ後のDrawStrokeのところで見ます。)実は、ウインドウズで線を描く基本は簡単なのです。筆かなにかを使っているつもりになってください。(実際そんな感じです。)はじめにすることは「書き出す点の位置に筆をおく」ということでしょう。それが、MoveToです。さて、筆を置いたら、次にこれを目的地まで、線をさっと引くだけですが、これがLineToなのです。、、、たぶん、なんとなくはわかったと思います。今はそれで十分です。詳しくはもう少し後で見ましょう!
さて、基本的な線の描き方がわかったとしても、まだまだわからないことだらけですね。もう少しがんばってみましょう。
何度も書きましたが、Scribbleがどういう構造を持っているのか、だんだんわかるように書いてあるので、最初は何をやっているのかわからないのですね。まあ、ちょっと、がまんしましょうね。
Scribbleは私のNekoプログラムに似ています(真似たんですから当然です)が、マウスの左ボタンを押しながら線を引くプログラムです。
どんな仕組みか簡単に書きましょう。「左ボタンが押されて、マウスが動かされ、左ボタンが離されるまで」が「一筆」に相当し、その間にCStrokeオブジェクトがひとつ生成され、マウスが通った道でのマウスの位置(一筆上のたくさんの点を表すCPoint)が、CStrokeに記録されるのです。何筆も描けばそれだけCStrokeオブジェクトが生成され、それはstrokeListという場所に保持されます。そして、これらのCStrokeを画面に描けば、(基本は)おしまいなのです。まあ、この辺は来週詳しく見ることにして、今週は「描画」を見てみましょう。
さて、描画は基本的にビューが行います。しかし、より細かく見ると、いろいろな可能性が出てくると思います。このチュートリアルの作者は、CStrokeに自分を描画する関数を付け加えるという方針をとりました。このように、データの一部に描画機能を付ける方針が良いのか悪いのか、、、考えてしまいますが、とりあえずこの方針に従いましょう。

(初心者は読み飛ばしてもよい注:
変なことを書いてすみません。絶対ウインドウズでしか仕事をしないのならこういう方針でも良いのかな、と思います。ただ、MFCプログラムについては、具体例付きの良い「方針の書」があまり無いようなので、ときどき悩むことがあるのです。ヘルプ版のチュートリアルにも、わざわざ「ただし、このようにデータ自身に描画機能を持たせるやり方は、描画方法の 1 つにすぎません。」なんてありますよね。)

チュートリアルには、いきなり、OnDrawに次のようなコードを付け足せ、と書いてあります。
CTypedPtrList& strokeList = pDoc->m_strokeList;
POSITION pos = strokeList.GetHeadPosition( );
while (pos != NULL)
{
    CStroke* pStroke = strokeList.GetNext(pos);
    pStroke->DrawStroke( pDC );
}
はじめて読む人は戸惑うでしょう。つまり、このチュートリアルの作者はCStrokeにDrawStrokeという自分を描画する関数を後で書く予定で、上のようなコードを書いたのです。CTypedPtrListの使い方はVC入門14でも見てください。POSITIONなどもそこで説明したので、ここではわかっていることにします。strokeListには、(これも後で)たくさんのCStroke(一筆)が格納されるので、これをひとつずつ取り出して、DrawStrokeで描画しているのです。そう思って読めば、簡単ですよね。(そうですよね。ね。ね。、、、)
さて、そういうわけで、CStroke::DrawStrokeの定義が、次にあります。コードは複雑そうで、事実、初心者には難解かもしれません。しかし、MFCでのグラフィクスの基礎なので絶対に理解すべきだと思います。また、一度わかると、なんかマイクロソフトのやり方の一部(とても小さな一部ですが)がわかったような気がして、ちょっとうれしいかも知れません。
一応、コードを写すと
BOOL CStroke::DrawStroke( CDC* pDC )
{
    CPen penStroke;
    if( !penStroke.CreatePen(PS_SOLID, m_nPenWidth, RGB(0,0,0))) 
        return FALSE;
    CPen* pOldPen = pDC->SelectObject( &penStroke );
    pDC->MoveTo( m_pointArray[0] );
    for( int i=1; i < m_pointArray.GetSize(); i++ )
    {
        pDC->LineTo( m_pointArray[i] );
    }
    pDC->SelectObject( pOldPen );
    return TRUE;
}
この関数は、CStrokeを描く時に使われる関数なのですが、前に、何かを描画するときには、デバイスコンテキストというものが必要だという話をしました。(VC入門6)DrawStrokeの引数は、この関数を呼び出す関数(使うもの)が与えてくれるデバイスコンテキストへのポインタです。(慣れていない人にはわかり難いところですが、ここは避けて通れません。しっくりこなくてもがまんして読み進んで、考えてみてください。今全部わからなくても、だんだんわかってくると思います。)このデバイスコンテキスト(へのポインタ)に、描画を命令すれば、画面等に絵が描かれるのです。(ウインドウズでは字も絵の一種です。)
さて、最初にCPenオブジェクトのpenStrokeを生成しています。これは描画のためのペンです。このペンはすぐには使えません。使えるペンにするには、CreatePenという関数を呼び出す必要があるのです。また、このCreatePenの引数をいろいろ指定することで、実際に描かれる線の形態や色が決められます。上の例は、点線とかでない普通のペン(PS_SOLIDの意味です)で、m_nPenWidthという太さの、黒いペン(RGB(0,0,0))を作るという意味です。最初はこのコードでやってみて、うまくいったら、RGB(0,0,0)を例えばRGB(255,0,0)などと書き換えて遊んでみると納得できると思います。なお、m_PenWidthは整数で、別のところで与えられる量なので、今はあまり気にしないでください。
CreatePenは呼び出されて、問題なく終了するときには、BOOL値のTRUEを戻し、そうでないときには、FALSEを戻します。これから、DrawStrokeも、成功した時はTRUEを、失敗したときにはFALSEを戻すように、設計されています。慣れていない人のためにコメントしましょう。if(!penStroke.,,,)とありますが、もし、penStrokeのCreatePenが使えるペンの作成に失敗した時は、その戻り値がFALSEになり、したがって「!」のおかげでこれが逆転し、TRUEになります。つまり、使えるペンの作成に失敗した時は、return FALSE;が実行され、この関数DrawStrokeはFALSEを戻すのです。
さて、私は、最初、次の
    CPen* pOldPen = pDC->SelectObject( &penStroke );
に一番悩みました。みなさんはわかりましたか?描画するには普通デバイスコンテキストを使うと書きました。ここではそのポインタpDCが与えれています。このデバイスコンテキストは、描画のための道具をたくさん持っています。道具って、、、なんでしょう。それはペンやブラシなどです。(ブラシの話は今はしません。)そのため、まず、使いたいペンを作成し、これをデバイスコンテキストに渡す必要があるのです。それが、pDC->SelectObject( &penStroke )です。これは、前に作ったpenStrokeをデバイスコンテキストに渡しているのです。(正しくは、「選択させている」です。)
それでは、pOldPenって何でしょう?実は、このデバイスコンテキストは、DrawStrokeを呼び出す(使う)関数から渡されるのですが、このデバイスコンテキストの中に既にペンがはいっている(選択されている)わけです。SelectObjectは、新しいペンを登録すると同時に、古いペンへのポインタを戻すのです。そして、それが、pOldPenに記録されるのです。
その後はm_PointArrayに記録されている点の中で最初の点に「筆」を置き(MoveTo)、次々にLineToで線を描いていっているのです。最初に書いたときは「感じ」で書きましたが、実は、MoveTo、LineToはデバイスコンテキストのクラスのメンバ関数なので、このコードにあるように、「pDC->」(など)が頭に付くのです。
最後の「pDC->SelectObject( pOldPen );」は、古いペンを復帰させるコードです。この部分は無くても良いと思うかもしれませんが、ペン(やブラシなど)を戻しておくことにはとても大切です。(古いペンを選択することで、新しいペンを解放できるという意味もあります。そういうふうにできているのです。)そう言えば、子どもの頃、何でも遊んだら元の場所に戻しておけ、、、とよく言われたものですね。
そしてすべてうまくいったら、TRUEを戻して終わりです。
今日はこの辺にしましょう。来週でビューが終わり、Scribbleのファーストバージョンが動かせると思います。
目次のページ
前のページ
後のページ