VC++5.0入門
CObjectとシリアライズ 97/11/16

やったああああああああああああああああああああああああああああああああああ!
日本代表!よくやったあああああああああああああああああああああああああああ!
(年甲斐もなく興奮してすみませんでした。でも、スポーツを見て泣きそうになったなんてはじめてです。)
では、今回の入門をはじめましょう。ところで、最近気が付いたのですが、ときどき、「後のページ」へのリンクが間違っていますね。すみません。時間ができたら直すつもりですが、しばらくは、あまり気にしないでください。
さて、前回はプロジェクトNekoを、ウインドウの再描画に耐えられるものにしました。ウインドウの再描画はウインドウズアプリケーションの基本ですから、まあ、当然のことをしたわけです。ここで、Nekoの実行画面をもう一度見てみましょう。メニューありボタンありですが、良く見ると「保存」という項目もあります。これも、まあ、普通のアプリケーションでは基本ですね。、、、しかし、まだ、保存の機能はありません。あるのは、メニューの項目やボタンだけで、その中身はこれから書かなくてならないのです。
とは言え、そんなに難しいことではありません。今回は、ここを実装してみましょう。(つまり、保存できるように、具体的にコードを書き込もうという意味です。)
まず、Nekoのワークスペースを開いて、ファイルビューなどで、CNekoStrokeを開き、次のように書き直してください。
class CNekoStroke : public CObject
{
    DECLARE_SERIAL(CNekoStroke)  //;は要りません。
public:
    CString str;
    CNekoStroke(){str="猫";}
    CArray<CPoint, CPoint> m_points;
    void DrawNekoStroke(CDC* pDC);
    virtual void Serialize(CArchive& ar);
};
次に、NekoDoc.cppのCNekoStrokeのメンバ関数の定義の場所の先頭に、
IMPLEMENT_SERIAL(CNekoStroke, CObject, 1)  //;は要らない。
と書き、その次に、Serializeの定義を次のように書いてください。
void CNekoStroke::Serialize(CArchive& ar)
{
    m_points.Serialize(ar);
}
です。説明は後でしますので、まず、コードを完成させましょう。NekoDoc.cppの中を探して、CNekoDoc::Serializeという関数の定義がある場所を見つけてください。(上の関数はCNekoStroke::Serializeなので、違う関数です。注意してください。)そして、次のように、1行だけ付け加えてください。
void CNekoDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // TODO: この位置に保存用のコードを追加してください。
    }
    else
    {
        // TODO: この位置に読み込み用のコードを追加してください。
    }
    //下の1行だけ付け加える。
    data.Serialize(ar);
}
これで、保存ができるようになりました。プログラムを実行して、字(の絵)を書き、適当な名前を付けたファイルにデータを保存し、プログラムを終了し、もう一度プログラムを起動し、メニューの「ファイル−>開く」で前のファイルを開いてみてください。前に書いた字(の絵)が再現されるはずです。ちょっと、気持ちが良いと思いますが、どうでしょう。

では説明しましょう。前々回、前回に見たように、ウインドウの(再)描画には特別な関数OnDrawという関数が用意されていました。この関数はMFCで定義されている特別な関数ですから、基本的には、名前を変えることはできません。プログラマはただコードを書き込むだけでした。ウインドウズアプリケーションならどのみちウインドウの描画は必要です。そのいつも同じコードをプログラマが何度も書くより、MFCの一部にしておいて、プログラマはその中心部分だけ自分で書くようにすれば良い、とマイクロソフト社が考えたから、このような特別な関数があるのです。(でしょう。)
さて、今回のテーマは「保存」ですが、これも大抵のアプリケーションには必要です。そのため、描画に対するOnDrawのように、保存(と「開く」)のためにMFCが用意してくれている関数があります。それがSerialize(シリアライズ)です。
CNekoDoc::Serializeを見てください。このコードは最後のdata.Serialize(ar);以外はAppWizardが書いてくれる部分です。少しわかりづらい所ですから、ゆっくりいきましょう。
まず、この関数がどこで使われるかですが、この関数は、ユーザがメニュー(やボタン)で「保存」や「開く」を選択したときに、フレームワークが(自動的に)呼び出すのです。(フレームワークとはMFCプログラムでプログラマを助けてくれる(通常は見えない)MFCライブラリの一部のことでした。)そして、データをファイルに保存したり、ファイルからデータを読み出したりするのです。ただ、実際には、MFCはその形を与えてくれているだけなので、内容は私たちプログラマが書かなければなりません。
Serializeの書き方は大変そうですが、慣れればどうということもないようです。今、必要なことだけ説明しましょう。、、、えーと、その前に、ひとつだけ。引数を見てわかるように、この関数には、CArchiveオブジェクトarというものが渡されます。このarは、簡単に言うと、データを保存する(あるいはそこからデータを読み出す)ファイルを表しているのです。そう覚えておいてください。
さて、CNekoDoc::Serializeの中はif文でふたつの部分に分けられていますが、はじめの「//TODO...」の部分はデータを保存するときに使われる部分、あとの「//TODO...」はデータを読み出すときに使われる部分です。ただ、今のプログラムでは、この部分は使わないで済みます。(今はあまりなやまないでください。)繰り返しますが、この関数は特別な関数で、データの保存と読み出しの両方で使われます。
ところで、このCNekoDocのSerialize関数を有効に利用するためには、自分で作ったクラスにも、Serializeという関数を定義すべきなのです。今、私たちのデータはCNekoDocというクラスで扱っていますが、より具体的には、CNekoDocのデータメンバCNekoStrokeのdataにデータを(プログラム実行時のみ)格納しているのでした。、、、、と、こうくれば、当然予想されるように、私たちの作ったクラスCNekoStrokeにもSerializeを定義するべきであり、実際、そうしているのです。
話がやや前後して複雑ですが、とにかく、CNekoStrokeにもSerializeという関数を定義するのだ、と思ってください。そうすれば、CNekoDocのSerializeでは、このCNekoStrokeのSerializeを呼び出すだけで良いのです。そして、CNekoStrokeのSerializeもデータの保存と読み出しの両方に使えるように、定義するつもりなので、if文の外に置いたのです。もちろん、
void CNekoDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        data.Serialize(ar);
    }
    else
    {
        data.Serialize(ar);
    }
}
としても良いのですが、まあ、おんなじですよね。それから、どうせif文が活きていないのだから、
void CNekoDoc::Serialize(CArchive& ar)
{
    data.Serialize(ar);
}
でいいじゃないか、と思う人は、良く分かっている人です。ただ、if文のところは将来の拡張のときに必要になるかもしれないので、消さなかっただけです。とりあえず、今回までなら、上のようにすっきりしたコードの方が良いかもしれません。
まあ、CNekoDocのSerializeはこの辺にしましょう。そういうわけでCNekoStrokeにSerializeを付け加えなければならないのです。じゃあ、、、と簡単にはできません。既存のコード、つまり、MFCと整合させるためには、いくつかのルールがあります。(とヘルプに書いてあります。)それは、
1.自分のクラスをCObject (または、CObject の派生クラス) から派生させる。
2.DECLARE_SERIAL(後述)を書く。
3.引数を持たないコンストラクタを定義する。
4.Serialize を書く。
5.cppファイルで、IMPLEMENT_SERIAL(後述)を宣言する。
です。 CObjectなんてはじめて聞いたという人がほとんどでしょう。(これは初心者講座ですから。)このクラスはMFCのほとんどのクラスの大元の基底クラスとして作られています。さらに、自分のクラスも、このクラスの派生クラスとしておくと、いろいろ便利なのです。
えっ、どう便利かですか?例えば、Serializeの機能が使える、ということなのです。そういうわけです。こうして、最初の書き換え、
class CNekoStroke : public CObject
があったのです。これだけで、CNekoStrokeはCObjectの派生クラスになるのでした。(C++は簡単で良いですよね。)ただ、このSerializeの機能を使うためには、DECLARE_SERIALという呪文(マクロと言います)をクラスの中に書いておく必要があるのです。それが、
    DECLARE_SERIAL(CNekoStroke)  //;は要りません。
でした。上に書いたように、この呪文(マクロ)には引数がひとつあって、そこにはクラス名をいれる決まりです。そういう決まりなのですから、そうしました。
次に、引数を持たないコンストラクタですが、偶然、すでに定義してあるので、何もする必要はありませんでした。(無い場合には、ただ書き加えれば良いだけです。)
そして、Serializeを宣言し、NekoDoc.cppで定義しているのです。Serializeの型は、上に書いたように決まっているので、忠実に上に書いたように書く必要があります。(安心してください。つまり、いつも同じように書けば良いだけです。)
定義は簡単で、
void CNekoStroke::Serialize(CArchive& ar)
{
    m_points.Serialize(ar);
}
だけです。データの構造が階層になっていてめんどうですが、ちょっとがまんして思い出してください。(この辺がよく思い出せない人は、前回のhファイルなどをプリントアウトして眺めてみることをお勧めします。)さて、(猫という字を書く場所の)データはCNekoDocで扱われるべきであり、実際そうなっているのですが、具体的には、データはCNekoDocのメンバであるCNekoStrokeのdataに格納されるのでした。ところがさらに、CNekoStroke内にはCArrayというクラスのm_pointsがあり、実際のデータはこの中に収められているのでした。
というわけで、CNekoStrokeのSerialize(つまり、保存と読み出しの関数)は、CArrayであるm_pointsのデータを保存したりすれば良いのです。そのためには、CArrayにさらにSerializeを定義して、、、となって行きそうですが、実は、CArrayには最初からSerializeが定義されているのです。すばらしいですね。
それで、結局、上のようにCNekoStrokeのSerializeはm_pointsのSerializeを呼び出すだけで良いのです。めでたし、めでたし、、、。
でも、こんなに複雑(面倒)になるなら、CNekoStrokeなんて使わずに、直接CArrayをCNekoDocのメンバにすれば良かったじゃんと思った人、するどいですね〜。でも、CNekoStrokeは後の拡張のために作ったのです。もし、元気があれば、CNekoStrokeなしで、ここまでのNekoと同じ動作をするプログラムを作って見てください。ちょっと、良い勉強になるかもしれません。
それは、さておき、最後になりましたが、Serializeを機能させるためには、もうひとつの呪文IMPLEMENT_SERIALが必要なので、
IMPLEMENT_SERIAL(CNekoStroke, CObject, 1)  //;は要らない。
と(cppのはじめのところに)書いたわけです。はじめと二番目の引数にはこのクラスの名前とその基底クラスの名前を書くのだとヘルプにあります。だから、そうしました。最後の数字はいわゆるバージョンナンバですが、ここではとりあえずバージョン1という意味で1と書きました。
知らないことがいつまで出てくんだよぉ〜、それにいつもヘルプ読んで使い方を調べなきゃなんないのぉ〜と暗くなっていませんか。でも、使い方は(初心の内は)いつも同じです。ですから、上の形で使い方を覚えておけば、大丈夫のはずです。

説明が意外に長くなってしまいました。大部分の人は、今ごろ、疲れきっているかもしれませんね。でも、元気が戻ったら、もう一度、前回から書き換えた場所を見てください。ほんのわずかです。自分でプログラムを書くときも、いつも上のように書くだけで良いのです。つまり、簡単なんです。(そう信じたいですね。)

最近、初心者にはちょっと難しいかもしれません。ただ、実は、難しいというより、なかなかしっくり来ないという感じではないでしょうか。いろいろ疑問に思うことはあると思いますが、考え込むより、実行して慣れることをお勧めします。まあ、いつも言っていることですね。
今回はこれくらいにしましょうね。それにしても、サッカー日本代表はすばらしかった、、、。
目次のページ
前のページ
後のページ