VC++5.0入門11
ドキュメントとOnDraw 97/11/3

こんにちは。連休はどうでしたか? うちは風邪をひいたり、掃除をしたりでした。
前回は、左ボタンを押しながらマウスを動かすという操作で猫という字をたくさん書くプログラムを作りました。このプログラムではどこに猫という字を書いたか記録を残さないので、ウインドウをアイコン化したりするとそのデータは失われてしまい、結局、せっかく書いた猫という字が消えてしまうのでした。
このプログラムのプロジェクトはNekoでした。みなさん、また、このプロジェクトをメニューの「ファイル−>ワークスペースを開く」から選択し、開いておいてください。今回もこれを少し書き換えます。
初心者には、今回の内容はいきなり難しくなったように思えるかもしれません。その場合、まず、解説は適当に読んで、Nekoを書き換え、実行し、動作を見てからもう一度解説を読んでみることをお勧めします。
今回は、説明のため、ウインドウをアイコン化してまたウインドウに戻しても、ウインドウに猫という字がひとつだけ残る(実はあとで説明しますが、最後の猫)プログラムにしてみます。
さあ、ゆっくり考えましょう。まず、前回のプログラムのOnLButtonDown、OnMouseMove、OnLButtonUpを見てください。どこに猫という字を書いたかというと、OnMouseMove関数にpointとしてウインドウズから与えられた場所に書いたのでした。このpointという変数は関数内で使われるだけで、関数が終了すると消滅する(破棄される)ことになっています。(そういう規則になっているのです。)そのため、このデータを残したければ、どこか関数の外にCPointオブジェクトを作り、(関数の中で)pointをそのオブジェクトにコピーしておけば良いのです。ここで、CPointオブジェクトってなんだっけと思いましたか?CPointとはマイクロソフトが提供してくれている点(の場所)を表すクラスです。例えば、どこかに、
    CPoint point_data;
などとしてあり、OnMouseMoveの定義が、
void CNekoView::OnMouseMove(UINT nFlags, CPoint point) 
{
    .....
    //pointのデータをpoint_dataに代入
    point_data=point;
    .....
}
などとなっていれば、このpointで与えられたデータはpoint_dataに保存されます。つまり、pointという変数はOnMouseMoveの終了とともに消滅しますが、point_dataは(適当なところに定義されていれば)関数終了後にも存在していて、いつでも使えるのです。
では、point_dataはどこにおくべきでしょうか?、、、、、実は、これは決まっています。簡単に言うと、このようにデータを置く場所がドキュメントクラスなのです。もちろん、そうしなくてもプログラムは書けますが、マイクロソフトはそういう方針でMFCやAppWizardを作っているので、従うことにしましょう。これは、具体的にはNekoDoc.hの中のCNekoDocクラスの宣言(定義)を
class CNekoDoc : public CDocument
{
protected: // シリアライズ機能のみから作成します。
    CNekoDoc();
    DECLARE_DYNCREATE(CNekoDoc)

// アトリビュート
public:
    CPoint point_data;

// オペレーション
public:

//オーバーライド
    // ClassWizard は仮想関数のオーバーライドを生成します。
    //{{AFX_VIRTUAL(CNekoDoc)
    public:
    virtual BOOL OnNewDocument();
    virtual void Serialize(CArchive& ar);
    //}}AFX_VIRTUAL

// インプリメンテーション
public:
    virtual ‾CNekoDoc();
#ifdef _DEBUG
    virtual void AssertValid() const;
    virtual void Dump(CDumpContext& dc) const;
#endif

protected:

// 生成されたメッセージ マップ関数
protected:
    //{{AFX_MSG(CNekoDoc)
        // メモ - ClassWizard はこの位置にメンバ関数を追加または削除します。
        //        この位置に生成されるコードを編集しないでください。
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};
のようにするということです。ごちゃごちゃあって嫌ですが、書き足したのは
    CPoint point_data;
の1行だけです。VC入門8で書きましたが、このクラスはデータを扱うためのクラスなので、我々のデータは、このクラスのメンバとして保持するわけです。
このように変数を付け加えるのにもClassWizardが使えますが、手で書いても同じです。ClassWizardを使いたい人はチュートリアル等で後で研究してください。私は、ここでは、手で書きました。
手で書き込む場合、元のクラスの宣言がごちゃごちゃしていて面食らうかもしれません。どこに書き込めば良いのかと。実際どこでも良いのです。どこでも良いのですが、まあ、ちょっと考えてみましょう。まず、アトリビュートとかオペレーションとか、、、何でしょう。これは、詳しくはヘルプを見てください。
でも難しいですね。^^;簡単に言うと、//アトリビュート部にはパブリックなデータメンバとSet/Get関数、//オペレーション部にはオブジェクトのする操作の関数、//オーバーライド部には仮想関数、//インプリメンテーション部には人に見せないメンバ、ということでしょうか。結局、これはマイクロソフトの考えであり、どうしてもこのような分類をしなければいけないということではない(つまりどう書いてもコンパイルできる)はずですが、とりあえず、それっぽい所の//アトリビュート部に、パブリックなデータとしていれました。、、、、なぜ、パブリックなのか?、、、深い考えはありません。ただ、CPointはすでに、インターフェースのはっきりしたクラスで、他のデータとも独立しています。(というか今は他にはデータが無いのでした。^^;)これをさらにプライベートなデータとして隠蔽する必要はないだろうと言う意味で、簡単にパブリックにしました。より、深いソフトウエアエンジニアリングを目指すのなら、これはプライベートにし、パブリックなSet/Get関数を定義すべきかもしれません。しかし、今回は簡単のためパブリックなデータメンバでいきましょう。(パブリックなデータメンバがあっても悪くはないのです。念のため。)
そして、OnMouseMoveを下のように変えます。
void CNekoView::OnMouseMove(UINT nFlags, CPoint point) 
{
    //マウスをキャプチャしていなければ、何もしない
    if(GetCapture()!=this)
        return;

    //CNekoDocへのポインタを得る
    CNekoDoc* pDoc=GetDocument();
    //CNekoDocのpoint_dataにpointを代入し、保持する
    pDoc->point_data=point;

    CClientDC dc(this);
    dc.TextOut(point.x, point.y, "猫");
}
ちょっと説明しましょう。まず、
    //CNekoDocへのポインタを得る
    CNekoDoc* pDoc=GetDocument();
ですが、GetDocument関数は特別な関数です。まず、私たちは、ビューというウインドウのクラス(の派生クラス、CNekoView)の関数を書き直していることを思い出してください。入門8に少し書きましたが、ビューとは見せることが専門のクラスで、普通のMFCアプリケーションでは、ビュー(クラス)はデータを扱うドキュメント(クラス)とペアを組むようになっています。私たちのドキュメントクラスはCNekoDocで、実際、このクラスにはpoint_dataという「猫という字を書く場所」を記憶する変数を定義しておきました。
ところで、ビューとドキュメントはペアなんですが、同じクラスではありません。それで、お互いのメンバを自由に使うことはできないのです。さて、ビューがドキュメントの何かを使いたい場合にどうすれば良いのでしょう。、、、実は、上のようにGetDocument関数を使えば対応するドキュメントへのポインタが得られるようになっているのです。
ドキュメントへのポインタが得られれば、そのクラス(のオブジェクト)のデータメンバpoint_dataにpointを代入するのは簡単です。pDocがポインタであることに注意して、
    //CNekoDocのpoint_dataにpointを代入し、保持する
    pDoc->point_data=point;
とします。これで、pointが消滅した後も、そのデータはCNekoDoc(のオブジェクト)のpoint_dataに記録され残っていることになるのです。ここまでできたら、念のため、実行してみてください。、、、、どうなりましたか?
前と変わらないでしょうね。^^)ここまででやったことは、データの記録ですが、ウインドウが一度(アイコン化なので)失われた後に、再描画する方法は決めていなかったので、このデータは無駄に記録されているだけなのです。
さて、いよいよ今回の大詰めです。記録されたデータを使って再描画されるように、プログラムを書き換えましょう。
そのために、まず、ファイルビューなどを使って、CNekoView.hを開き、眺め回してください。OnDrawという関数があったでしょうか?この関数がドキュメントクラスと並んで本日の主役です。この関数はウインドウが(再)描画されるとき(つまり、アイコンからウインドウに戻されたり、他のウインドウが上からどいたときなど)に、どうするか決める特別な関数です。
この関数は、はじめは
void CNekoView::OnDraw(CDC* pDC)
{
    CNekoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    // TODO: この場所にネイティブ データ用の描画コードを追加します。
}
と書いてあるはずです。(この関数の見つけ方はいろいろありますが、どれでも結果は同じです。)
この関数のはじめの1行はドキュメントクラスへのポインタの取得ですね。2行目は、私たちにはさしあたって関係ありません。ほっておきましょう。
そして、// TODO: と書いてある部分を書き直すのです。では、以下のようにしてください。
void CNekoView::OnDraw(CDC* pDC)
{
    CNekoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    //関数に与えれられるデバイスコンテキスト(へのポインタ)pDCを使って描画
    pDC->TextOut(pDoc->point_data.x, pDoc->point_data.y, "猫");
}
ここまでフォローできた人は、何をしているかは、もうわかるかもしれません。単に、ドキュメントのpoint_dataの場所に猫という字を書いているだけです。
ちょっとゆっくり説明してみるとこうです。まず、関数OnDrawは、この関数を使う(呼び出すというのでした)側から、CDCつまりデバイスコンテキストを渡されています。(正確に言うとCDC*というデバイスコンテキストへのポインタを渡されている、ですね。CDCはいろいろなデバイスコンテキストを表すいろいろなクラスの基底クラスです。)この関数を呼び出しているのは誰なんでしょうか。これはこのプログラムの実行を影から支え(支配し?)ている、マイクロソフトのプラグラマさんたちが書いたコードです。これをフレームワークというようです。フレームワークという言葉は、プログラムの枠組みという意味で使われたりもしますが、ここではプログラムの大部分の実行を制御しているMFCライブラリの(あまり見えない)部分という意味です。まあ、こういうものがあるから「簡単に」ウインドウズプログラミングができるのです。(でもあんまり簡単じゃないと言いたいのはわかります。)
まとめましょう。つまり、ウインドウがアイコンなどから復帰した場合、ウインドウズはこのプログラムに「再描画せよ」というメッセージを送るわけですが、プログラムに付随している(しかし私たちが書いたのではない)フレームワークという部分がこのメッセージに反応し、OnDrawを呼び出し、ウインドウを再描画するのです。
フレームワークはいろいろ面倒を見てくれるようですが、実際、どう(再)描画するかは、私たちが決めるのです。その時に、フレームワークから渡されるデバイスコンテキスト(へのポインタ)がpDCなのです。上の例では、pDCを使ってpDocのpoint_dataの位置に猫という字を書いているのです。
ちょっと紛らわしいですが、pDCはデバイスコンテキストへのポインタ、pDocはドキュメントへのポインタです。間違えないでください。

では、実行してみてください。まず、白いビューウインドウの上でマウスの左ボタンを押しながら、マウスを動かします。すると猫という字で軌跡ができるでしょう。(ああ、すばらしぃ。^^;)次に、このウインドウをアイコン化して、もう一度、ウインドウに戻してください。猫という字がひとつだけウインドウに残っていると思います。
まあ、今日のはそれだけです。でも、なぜ、最後の1字だけなんでしょう。、、、という程のことはないですよね。
一応答えを言います。残るのが1字だけなのは、CPointオブジェクトがpoint_dataひとつだけだから当然です。また、マウスを動かすとOnMouseMoveが呼び出され、このとき左ボタンが押されていれば、ドキュメントのpoint_dataにマウスの場所が記録され、猫という字が書かれるわけですが、この部分は、(左ボタンが押されていて)マウスが動くたびに実行されるので、その都度point_dataが新しく書き換えられてしまうのです。そのため、point_dataにはいつも最後のデータ(だけ)が残されるのです。

今日は、ビューとドキュメントの関係、GetDocumentにOnDrawと、いろいろ出てきました。その割にあまり進んだ気がしない所でした。でも、もう少しの辛抱です。次回は、最後のひとつだけでなく、(一筆の)すべての猫という字が再現されるプログラムに改造しましょう。
目次のページ
前のページ
後のページ