VC++5.0入門
CArray 97/11/9

こんにちは。今回も、ゆっくり、前回まで改造してきたプロジェクトNekoを改造し続けてみます。
前回は、マウスの左ボタンを押しながら書いた猫という文字の最後のものだけが、ウインドウが再描画されても残るようにしたのでした。(ウインドウの再描画とは、ウインドウがアイコン化されてもう一度ウインドウ化されたときなどに、このウインドウが再び描かれるという意味です。)なぜ最後の文字だけにしたかというと、説明の順番として、そうした方がプログラムの変更が少なく、わかりやすいと思ったからです。
しかし、もちろん、これでは面白くないので、画面に書いた文字全部が再現されるようにプログラムを書き直すことにしましょう。(前回、予告を書いたときには、左ボタンを押して字を書いてボタンを放し、また、左ボタンを押して字を書いてボタンを放し、、、としていったときの最後に書いた一連の文字の列だけ再現するようにしようと思ったのですが、実際にやってみると、全部の文字を再現できるように改造することの方が単純でした。単純である、ということは、先々の改良が面倒になるという欠点でもあるのですが、とりあえず、わかりながら進むことが大切だと考え、単純な方をとることにしました。)

まず、プロジェクトNekoを開き、その中のNekoDoc.hを見てください。クラスCNekoDocの宣言の中に
    CPoint point_data;
というのがありますが、これは点(の座標)を表すクラスCPointのインスタンスpoint_dataをドキュメント(つまりデータを扱うクラス)が持つという意味でした。NekoView.cppに定義のあるCNekoView::OnMouseMoveの中で、このpoint_dataにその時のマウスの座標を記録させ、ウインドウが再描画されるときには、そのとき呼び出される特別な関数CNekoView::OnDrawで、point_dataの位置(だけ)に「猫」という字を書かせたのでした。
問題は、マウスの位置というデータの記録場所が、CPointのインスタンス1個だけ(point_data)ということですね。これでは点をひとつしか覚えられないので、直前に記録されたデータはどんどん捨られていってしまうのです。
では、どうすれば良いのでしょう。まず、思いつくことは、CPointの集合ですから、CPointの配列でしょうか。しかし、
    CPoint data[100];
などとやると、点の場所を100まで記録できますが、それ以上は無理です。この数を非常に大きくしておけば、良いかもしれませんが、どうも無駄が多くて気持ちが悪いですね。
実はこの問題を解決するのに、MFCの中には、長さが変えられる配列のクラスCArrayが定義されているのです。このようにいきなりMFCの中には、、、なんて言われると、嫌なもんですね。じゃあ、どれだけ知っていれば良いのだろうか、と思ってしまいます。しかし、まあ、VC/MFCを使う以上、少しずつ覚えていくことになるのでしょう。(たぶん^^;)
さて、CArrayの詳しい情報を知りたい方はヘルプで読んでください。(私が駆け出しの頃、先輩に言われて一番むっとしたのが「ヘルプを読め」でした。「そんなのヘルプに書いてあるだろう。ちゃんと読めよ」と言われると「ヘルプを読んでわかんないから聞いてんだよぉぉぉ。おまえはあほかぁ」と怒鳴りたくなるのをぐっとこらえて、引きつった笑顔をみせながら、「えへへ、でも、ちょっと教えてくださいよぉ」などと言ったものでした。実際、初心者がヘルプを読むのは大変です。私の講義では、最低限のことはヘルプ無しでできるようにしますので、必要を感じなければ、しばらくは見なくてかまいません。ただ、少しずつ挑戦してみてください。いずれひとりでヘルプを読めるようになる必要はあるのです。
CArrayで例えば、整数の配列を作りたければ、
    CArray<int, int> h;
とすれば良いのです。<int, int>は整数の配列だよ、という意味です。(なんで<や>があるんだろう。intがふたつあるのはなぜかな、と思ったらヘルプを見てください。ここではこれでゆるしてくださいね。)配列なのに[100]みたいなのがないぞ、と思うかもしれません。これがCArrayのみそなんです。普通の
    int x[100];
では、要素の数が100と決められていて、後からその数を大きくすることはできません。しかし、CArrayのhははじめに大きさを決めないで、要素が増えると(必要に応じて)勝手に大きくなってくれるのです。その詳しい使い方はまた後でやりましょう。
さて、このCArrayを使うとなると、もう、後はどうすれば良いか、わかるかもしれません。普通ならCArrayをCNekoDocのメンバにするところでしょう。しかし、後の拡張などを考えて、ここではCArrayをメンバに持った新しいクラスを定義することにします。そのクラスの名はCNekoStrokeとし、NekoDoc.hのCNekoDocクラスの宣言の前に書くことにします。Strokeという単語の意味は、一筆書きの「一筆」などに相当するのですが、今回は一筆でもなんでもないので、気にしないでください。(後々の改造で、本当に「一筆」にしていく予定です。うまくいかなければごめんなさい。)
それでは、NekoDoc.hを開いて、
class CNekoDoc : public CDocument
の直前に、
class CNekoStroke
{
public:
    CArray<CPoint, CPoint> m_points;
    void DrawNekoStroke(CDC* pDC);
};
と入れてください。このクラスはCPointを要素に持つCArrayのインスタンスm_pointsをデータメンバに持ち、DrawNekoStrokeをパブリックなメンバ関数に持つクラスです。繰り返しますが、m_pointsがマウスの動いた点(CPoint)を格納する配列です。(その配列の大きさははじめに決めなくても、要素が増えると自然に大きくなっていくのでした。)DrawNekoStrokeは引数に受け取ったpDC(デバイスコンテキスト(へのポインタ))を使って「猫」と書いていく関数です。
その定義はNekoDoc.cppに書かなければなりませんね。NekoDoc.cppを開いて、
/////////////////////////////////////////////////////////////////////////////
// CNekoDoc
の直前に、
void CNekoStroke::DrawNekoStroke(CDC* pDC)
{
    for(int i=0; i<m_points.GetSize(); i++)
        pDC->TextOut(m_points[i].x, m_points[i].y, "猫");
}
としてください。ちょっと難しいですか?まず、CArrayであるm_pointsにはたくさんのCPointが格納されていると思ってください。(後でそのようにします。)
それぞれのCPointの場所に「猫」と書きたいのですが、まず、全部で何個のCPointが格納されているか知る必要があります。これは
    m_points.GetSize()
で知ることができます。このGetSizeはCArrayのメンバ関数で、実際に格納されている要素の数を戻す関数なのです。また、i番の要素は普通の配列のようにm_points[i]と表されます。以上で、上のコードの意味はわかると思います。(pDCやTextOutがわからない人はちょっと復習してください。)この関数の使い方は後で考えましょう。
さて、こうしてCNekoStrokeが定義できました。もう一度、NekoDoc.hのCNekoDocの定義に戻って、
     CPoints point_data;
    CNekoStroke data;
に直してください。CNekoStrokeのインスタンスdata(の中のm_points)が、マウスの位置CPointを格納する場所になるのです。
マウスの位置(正確には左ボタンを押しながらマウスを動かしたときのマウスの位置)をCNekoStrokeに格納するのは、もちろん、OnMouseMoveです。どうやって格納するかは、まず、答えを見てから考えましょう。NekoView.cppのOnMouseMoveを
void CNekoView::OnMouseMove(UINT nFlags, CPoint point) 
{
    //マウスをキャプチャしていなければ、何もしない
    if(GetCapture()!=this)
        return;
    CNekoDoc* pDoc=GetDocument();
    pDoc->data.m_points.Add(point);
    CClientDC dc(this);
    dc.TextOut(point.x, point.y, "猫");
}
と直してください。もちろん、ポイントは
    pDoc->data.m_points.Add(point);
です。pDocの中のdataの中のm_pointsを取ってきて、その関数Addを使っているのがわかるでしょうか。(pDocはドキュメント、つまりCNekoDoc(のインスタンス)へのポインタで、dataはCNekoDocのデータメンバのCNekoStroke、m_pointsはCNekoStrokeのデータメンバのCArrayなのでした。)AddはCArrayのメンバ関数で、上のようにすると、m_pointsに新たな要素としてpointが格納されるのです。(m_pointsの大きさは何もしなくてもAddとするだけで自動的に大きくなっていきます。)これで、マウスの位置が記録されてしまうのです。簡単ですね。(ただ、pDoc->data.m_pointsという、何々の何々の何々、、みたいなコードはわかりづらいかもしれません。これは後の方で改良するかもしれません。)
ところで、マウスの位置を記録するためのクラスCNekoStrokeには、DrawNekoStrokeという関数を付けました。これはどこで使うのでしょう。実はOnDrawです。この関数はウインドウを(再)描画するときに使われるのでした。(もうわかっていた人は優秀です。)ここで、NekoView.cppを開いて、OnDrawの所を
void CNekoView::OnDraw(CDC* pDC)
{
    CNekoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    pDoc->data.DrawNekoStroke(pDC);
}
と直してください。これで、ウインドウの大きさを変えたりしても、常に、OnDrawが働いて「猫」の軌跡が再描画されるようになりました。
ただし、重要なことをひとつCArrayを使うためには、#include <afxtempl.h>が必要です。(afxtempl.hをインクルードする、つまり、このファイルを読み込むという意味です。)これは、StdAfx.hの適当な場所におけば良いのですが、例えば、
#include <afxwin.h>         // MFC のコアおよび標準コンポーネント
#include <afxext.h>         // MFC の拡張部分
#include <afxtempl.h>
のように、#include <afxext.h>の後あたりが良さそうです。
これで、少し実験して見てください。

実は、ここまでで、ちょっとおまけができています。というのは、もう、印刷可能なんです。(実は前回からでしたが。)実はOnDrawという関数は印刷のときにも使われるのです。逆に言うとOnDrawをちゃんと書けば、印刷もできるのです。適当に、マウスで猫という字を書いたら、メニューかボタンで印刷してみてください。
ちょっと残念ですが、ウインドウの画面で見るのと、印刷された紙の上で見るのでは、縮尺が違っていて、思ったように印刷されていないと思います。この縮尺を調節する方法ももちろんありますが、この講座では省略します。(例えば、もう少し後にチュートリアルを見てください。)


これで、今回の分は終わりですが、あとひとつだけ、手直しをさせてください。プログラム中で「猫」という文字が2度出てきますが、このように同じものが別のところで顔を出すのは気に入りません。そこで、CNekoStrokeのデータメンバにCString(文字列を表すMFCのクラス)を入れ、これに「猫」という字を記憶させておくことにします。具体的には、CNekoStrokeを
class CNekoStroke
{
public:
    CString str;
    //コンストラクタで、strに「猫」を代入
    //CStringを使うと下のように書ける
    CNekoStroke(){str="猫";}
    CArray<CPoint, CPoint> m_points;
    void DrawNekoStroke(CDC* pDC);
};
とし、DrawNekoStrokeの定義を
void CNekoStroke::DrawNekoStroke(CDC* pDC)
{
    for(int i=0; i<m_points.GetSize(); i++)
        pDC->TextOut(m_points[i].x, m_points[i].y, str);

}
とし、CNekoViewのOnMouseMoveを
void CNekoView::OnMouseMove(UINT nFlags, CPoint point) 
{
    //マウスをキャプチャしていなければ、何もしない
    if(GetCapture()!=this)
        return;
    CNekoDoc* pDoc=GetDocument();
    pDoc->data.m_points.Add(point);
    CClientDC dc(this);
    dc.TextOut(point.x, point.y, pDoc->data.str);
}
と直してください。こうすれば、猫を犬に変更したい場合、CNekoStrokeのコンストラクタをstr="猫";からstr="犬";に書き換えれば良いことになります。元気な人はちょっとやってみてください。
今回はここまでとして、次回にまた別の改造を考えます。
なお、以下に一応まとめたコードを載せますが、インデントはファイルの転送中にめちゃくちゃになっているかもしれませんので注意してください。(バグは無いと思いますが、あったらお知らせください。このコードでも他のコードでも、問題が起きた場合に保証はできないということは、もう一度確認させてください。)StdAfx.hは自分で直してください。
NekoDoc.h
NekoDoc.cpp
NekoView.cpp

目次のページ
前のページ
後のページ