VC++5.0入門17
メニューとダイアログボックス(後編) 97/1/11

あけましておめでとうございます。
VC入門は中途半端で年を越してしまいましたが、みなさん、いかがお過ごしでしょうか。
なんにしましても、今年も良い年でありますように。
というわけで、とにかくメニューとダイアログボックスを終わらせましょう。^^)
前回のプロジェクトを開いて、思い出すために、もう一度実行してみてください。メニューに文字という項目があり、その下の「文字の変更」という項目を選ぶと、のっぺらなダイアログ(ダイアログボックスのボックスは省略します)が開き、OKかキャンセルと押すと、、、別に何も起こらないというものでした。
ここまで、してきたことは、メニューに項目(文字、文字の変更)を増やし、そこにダイアログをくっつけたというだけで、それ以上のことはまだしていないのです。
では、まず、ダイアログの形をちょっと変更しましょう。前にも書きましたが、この辺は図がなくてちょっと読みずらいとは思います。思いますが、ご容赦のほどを。実際、自分でごちゃごちゃしているうちに人の説明を読むよりわかってくると思います。^^;
まあ、それで、とにかく、いきましょう。えーと、リソースビューを見てください。(タブをクリックするのでしたね。)Nekoリソースのフォルダを展開して、DialogのIDD_LETTERをダブルクリックしてください。(このIDD_LETTERが、私たちのダイアログのIDでしたね。)すると、画面右の方に、のっぺらなダイアログが見えるでしょう。これはダイアログの編集画面なのです。画面を探すと、コントロール(とかコント...とか)書かれた小さいウインドウがあると思います。無ければ、VisualStudioのメニューの右の方の空白で右ボタンをクリックし、コントロールを選択してください。
このウインドウの中に「ab|」のように書かれている場所があると思います。ここにマウスを合わせると、エディットボックスという説明文が出てくるでしょう。エディットボックスとは、文字などの入力出力に使うコントロールのことです。(コントロールとは、ダイアログなどに貼り付ける小さなウインドウのことでした。覚えていますか?)ここをクリックして、マウスを編集中のダイアログ上に持ってきてください。すると、まか不思議、、、でもないですが、マウスが十字形になるでしょう。それでは、適当なところで、左ボタンを押し、そのまま、左ボタンを離さずに、マウスを右下に移動させ(こういうのをドラッグと言うのでしたね)、左ボタンを離してください。ダイアログ上に白い四角ができるでしょう。これがエディットボックス(編集中のエディットボックス)です。プログラム実行中には、ここに文字を入力するのですが、上下幅はあまり大きくてもカッコ悪いので、1文字分プラスアルファにしておきましょう。前に書きましたが、縁をドラッグすると、コントロールの大きさは変えられます。
このエディットボックスの上で右ボタンをクリックし、プロパティを選ぶと、このコントロールにはIDC_EDIT1というIDが付けられていることがわかります。このIDはプログラマが変えても良いのですが、今回はこのままにしておきましょう。で、プロパティウインドウを閉じてください。(確認で開いただけで何もしていないのですよ。)
さて、なんらかの方法でClassWizardを開いてください。できますか?メニューか右ボタンクリックから開けるはずですね。そこで、メンバ変数のタブをクリックし、クラス名でCLetterDlgを選んでください。これが私たちのダイアログを表すクラスでしたね。このクラスに変数を追加するのです。左下のコントロールの窓でIDC_EDIT1(つまり、さっき付けたエディットボックスです)を選択し、「変数の追加」ボタンを押してください。
すると「メンバ変数名」を聞いてきます。、、、ちょっと、混乱してきましたか? 今、何をやっているんだろう?と思いましたか。、、、途中ですが、ちょっと考えてみましょう。さっきダイアログにエディットボックスというコントロールを貼り付けました。さあ、それでこのコントロールを使おう、、、と気ははやりますが、そう簡単ではないのです。今やっていることは、ダイアログの編集ですが、ダイアログなどはリソースと呼ばれるもので、C++のコードと直接の関係は無いのでした。前にダイアログを作ったときには、その後でClassWizardでそのダイアログに対応する(もちろんC++のコードである)クラスCLetterDlgを作ったのでした。このクラスのオブジェクトを生成し、メンバ関数DoModalを使うとリソースのダイアログが使えるようになったのです。
さて、そう考えると、エディットボックスもリソース(の一部)ですから、これだけではC++のコードに対応がついていないとわかるでしょう。そこで、このエディットボックスに対応するメンバ変数(CLetterDlgのメンバ変数)を作りたいのです。これが今やっていることです。
さあ、そういうわけで、メンバ変数の名前を決めましょう。何でもいいのですが、m_letterとでもしましょう。カテゴリ(種類という意味ですね)は「値」としてください。この変数は「文字(列)」という「値」をいれる変数なので、そうします。変数のタイプはCStringとしてください。CStringはMFCで文字列を表す重要なクラスです。
これでOKとすると、またClassWizardのウインドウにもどります。ここで、CStringの最大文字数を聞いてきますので、適当に入力してください。まあ、20くらいにしておきましょうか。ここでまたOKすると先ほどのエディットボックスに対応するメンバ変数m_letterがCLetterDlgにできたことになるのです。ファイルビューなどでLetterDlg.hを見ると確かに、m_letterがCLetterDlgのメンバになっていると思います。ぜひ、確認しておいてください。前にも書きましたが、字の色が灰色になったりしているのは、ここはClassWizardなどが使う領域だ、とVC様がおっしゃっているだけで、深い意味はありません。
次に、CNekoDocに新しいメンバ変数を追加しましょう。私たちが作った文字変更ダイアログを通じてユーザが指定した文字(列)を記録しておく場所が必要だからです。どうしましょうか?やり方はいろいろありますが、簡単に、ファイルビューでNekoDoc.hを開き、CNekoDocの宣言中のCTypedPtrList m_strokes;の下にCString m_CurLett; といれてください。つまり、そのあたりは、
// アトリビュート
public:
    CTypedPtrList<CObList, CNekoStroke*> m_strokes;
    CString m_CurLett;  //付け加えた1行
という感じになるでしょう。(Curはcurrentつまり「現在の」という意味です。)
このm_CurLettはどこかで初期化(初期の値を代入する)した方が良いようです。このようなメンバ変数の初期化は、MFCプログラミングでは普通OnNewDocumentという関数でやるようです。(これは、メニューのファイルで新規作成を選んだ時に呼び出される関数です。)ファイルビューなどを使ってCNekoDoc.cppのCNekoDoc::OnNewDocumentの定義の場所(CNekoDoc.cpp内にあります)を見つけ出し、
BOOL CNekoDoc::OnNewDocument()
{
    if (!CDocument::OnNewDocument())
        return FALSE;

    m_CurLett="猫";  //付け加えた1行
    
    return TRUE;
}
としてください。
さて、ここまでやってきたことは、まだ準備です。げげっ、、、。そろそろ疲れましたね。でも、あと少しです。
次に、NekoDoc.cppのvoid CNekoDoc::OnChangeLetter()のところを書き換えます。ClassWizardかファイルビューなどでこの場所を探してください。
この関数はメニューで「文字の変更」を選んだ時に、呼び出されるのでした。
void CNekoDoc::OnChangeLetter() 
{
    CLetterDlg dlg;
    dlg.DoModal();
}
となっているでしょう。つまり、CLetterDlgのオブジェクト(インスタンス)dlgを生成し、そのメンバ関数DoModalを呼び出しているのです。この部分を
void CNekoDoc::OnChangeLetter() 
{
    CLetterDlg dlg;
    if(dlg.DoModal()==IDOK){
        m_CurLett=dlg.m_letter;
    }        
}
としてください。ifの中身ですが、想像がつく人はもうだいぶ馴れた人でしょう。実はDoModalはダイアログを表示するだけでなく、どのボタンが押されて終了したかを知らせてくれるのです。つまり、もし、OKボタンが押されるとそのIDのIDOK(これは決まっているのです)が戻され、もし、キャンセルボタンが押されればIDCANCELが戻されるのです。
それで上のコードは、もしOKボタンが押されればm_CurLettにdlgのm_letterの値(文字列)が代入されるという意味になるのです。
あとは、マウスのドラッグの際にこの文字列を使うように変更すれば良いだけですね。CNekoView.cppのvoid CNekoView::OnLButtonDown(UINT nFlags, CPoint point)を
void CNekoView::OnLButtonDown(UINT nFlags, CPoint point) 
{
    SetCapture();  //マウスをキャプチャ(補足)する
    m_pCurStroke=new CNekoStroke;
    CNekoDoc *pDoc=GetDocument();
    m_pCurStroke->str=pDoc->m_CurLett;  //ここだけ付け加えた
    pDoc->m_strokes.AddTail(m_pCurStroke);
}
としてください。付け加えた行はドキュメントに保持されているユーザの選んだ文字列(m_CurLett)を現在の一筆(m_pCurStroke)の文字(str)にせよ、という意味ですね。

ちょっと、本論と関係無いのですが、変数の命名方についてコメントです。私は文字列を表す変数名に、普通str(stringの頭3文字)などを使うのですが、今回のプログラムでは文字を表すletterを途中から使い、なんかちぐはぐになってしまいました。すみませんでした。

さて、とりあえず、できたみたいなので、ちょっと実行してみてください。実行画面で、まず、普通にマウスをドラッグすると、猫という字が書けるでしょう。これは以前のままです。次に、メニューで文字の変更を選んでください。すると、文字変更のダイアログが開くので、適当な文字(列)を入力し、OKボタンを押してください。私は「犬」(私は犬も好きです。)と入力してみました。それからまたマウスのドラッグをやると、ちゃんと犬という文字が書けます!
しかし、実は、まだ未完成なのです。ここで、今、書いた画面を保存してください。つまり、メニューで保存を選び適当な名前をつけて保存するのです。そして、一度Nekoを終了し、もう一度実行してから、ファイルの「開く」で今保存したファイルを開いてください。すると、、、。全部「猫」になっているはずです。
これはどうしたことか! 落ち着いて考えてみましょう。(もうやんなりましたか?)今、Serializeかなと思った人はとても優秀です。CNekoStroke::Serializeを見てください。ここで、文字を書く場所を表すm_pointsはシリアライズ(つまりファイルへの書き込みとファイルからの書き出し)していますが、書き込む文字はシリアライズしていないのです。このため、前の例なら「犬」という文字が保存されていないのです。では、なぜ、同様に保存していない「猫」という字が書けたのでしょうか。それはCNekoStrokeのデフォルトのコンストラクタ(引数をとらないコンストラクタ)で「猫」という字を指定しているからです。
つまり、ファイルからの読み出しで、文字が読み出されず(はじめから保存していないのですから)、文字が書かれる場所だけが読み出されると、引数のないコンストラクタによって、自動的にstrが猫になってしまうのです。試しに、このコンストラクタを何もしないものに書き換えると、ファイルを読み込んでも、画面には何も表示されなくなります。それは、書くべき(人間に見える)文字が無いからです。
この問題をクリアするには、要するに、ちゃんと文字(str)そのものも保存するようにすれば良いのです。それには、CNekoStroke::Serializeを
void CNekoStroke::Serialize(CArchive& ar)
{
    if(ar.IsStoring()){
        ar<<str;
        m_points.Serialize(ar);
    }
    else{
        ar>>str;
        m_points.Serialize(ar);
    }

}
とすれば良いのです。Serializeは書き込みと読み出しの両方で使われる関数です。今までは、CNekoStrokeのSerializeは単にCArrayのSerializeを呼ぶだけだったので、気にしませんでしたが、今度は、新しいメンバstrのことを考えなければならないので、ちゃんと書き込みと読み出しを区別しなければならないのです。そのため、ifとelseがあるのです。
ここまで書けばわかるかもしれませんが、実は、ar.IsStoring()が書き込みと読み出しを区別してくれる関数なのです。この関数は、現在書き込み中なら「真」を戻すのです。簡単に言うと、ファイルへの書き込み中なら、はじめの中カッコ内が、そうでなければ(つまり読み込み中なら)あとの中カッコ内が実行されるのです。そして、ar<<str;はar(ファイルを表すオブジェクト)への書き込み、ar>>str;はarからの読み出しを意味します。
以上で、一応ちゃんと動くプログラムになりました。まだ、言い残しが少しあります。その言い残しは次回にまわしますが、ひとつだけ注意です。今のやり方で、ファイルへの書き込みと読み出しの仕方が変わったので、改造したプログラムでは以前に保存した猫文字ファイルは読めないようになってしまいました。残念ですがしかたありませんね。
次回は、メニューとダイアログボックスの補足です。(いつまでやってんだぁ。はい、次で終わりです。たぶん。)
目次のページ
前のページ
後のページ