石原高のオレ流C++独学塾 - 第2回 C言語の知識で挑むC++Builder

By: Hitoshi Fujii

Abstract: C言語の知識でC++Builder 2007を触ってみたところ、文字列型に大きな違いがあることが分かった。

Hide image
ishihara

「おまえはもうコードを書くな」と上司にいわれてはや3年。管理職業務も板についてきた今日この頃だが、オレ的には、やはり手がうずく。軽快にコードを入力してEnterキーのストロークを決めたい。

ものの記事によると、今組み込み市場がホットで、CやC++の需要が高まっているらしい。N88 BASICでプログラミングを始めて、仕事じゃFORTRANやCを使ってきたオレとしては、最近の言語はどうもなじまない。どうせオブジェクト指向やるなら、オレ流としては風呂釜にJavaじゃなくてC++だろう。管理職のオレ様としては、背中は窓でうしろに立つものはいないし、こっそり残業してC++でも始めてみるか。


    C++Builderを触ってみたけれど

一ヶ月かけてC++Builder 2007を評価してみた。元々C言語の知識がちょっとあるぐらいのオレには、見よう見まねであるものの、コンポーネントをマウス操作でひょいひょいと置いて、ちょっとしたコードで動くプログラムを作れたのは正直魅力的だ。だが、何事も簡単というのには、落とし穴がある。C言語の知識だけで格闘した場合、ちょっとやっかいな問題に遭遇することにも気がついた。

    とにかく文字列

そうだ。前回は、調子に乗って、こんなコードを書いた。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    ShowMessage("Hello World, Hello " + Edit1->Text +"!");
}

ShowMessageというのは、引数にある文字列をメッセージダイアログに表示する関数だ。だが、ここでいう文字列とは何だ?C言語のオレにとっては、文字列とは、文字配列へのポインタのことだ。だが、それでは、+ なんて便利な連結はできないはずだ。オレの知識じゃせいぜいstrcatだ。

よく分からないので、C++Builderのデバッガを使ってみた。別にトレースするほどではないのだが、デバッガで中に入ってみれば、文字列の正体が分かると思ったからだ。

オレが書いたコードは一行しかないが、そこにブレークポイントを設定する。これは前回もやった。プログラムを実行してボタンをクリックすれば、ここで実行が止まるはずだ。

Hide image
cb02_1

図1 ブレークポイントの設定

プログラムを実行して操作してやると、ShowMessageのところでプログラムが止まった。

Hide image
cb02_2

図2 プログラムの停止

ここで[実行|トレース実行]を行う。ホットキーは、[F7]だ。すると、dstring.hというファイルが表示された。

Hide image
cb02_3

図3 dstring.h

コメントには、

// DSTRING.H - Support for delphi strings in C++
//            (AnsiString and template<sz> SmallString)

とある。どうやら、ここで使っている文字列は、Delphi互換の文字列AnsiStringらしい。C++BuilderのコンポーネントVCLは、Delphiと共通だ。だとすれば、そこで使う文字列が、Delphi互換の型だというのは合点がいく。だがこれはちょっと面倒だ。char * ではなく、AnsiStringを使うんだとすると、オレの知っているstrcmpやstrcpyは使えないじゃないか。ファイル入出力はどうするんだ。

オレ的にはこの不安を解消するには、とにかくAnsiStringを調べてみるしかないと判断した。

    C言語ユーザーのためのAnsiString講座

ヘルプで、dstring.h AnsiStringと引いても、「AnsiString は、Delphi の long 文字列型に相当する C++ の型です。」ぐらいのことしか書いていない。仕方がないので、Delphi関連の情報と、dstring.hの定義を見て、まとめてみることにした。

まず宣言だが、このように書けばいいようだ。

AnsiString str;

初期値を与える手段には、どうやら便利な方法がいっぱいあるらしい。dstring.h には、次のように記述してある。

    // Constructors
    __fastcall AnsiString(): Data(0) {}
    __fastcall AnsiString(const char* src);
    __fastcall AnsiString(const AnsiString& src);
    __fastcall AnsiString(const char* src, unsigned int len);
    __fastcall AnsiString(const wchar_t* src);
    __fastcall AnsiString(char src);
    __fastcall AnsiString(short);
    __fastcall AnsiString(unsigned short);
    __fastcall AnsiString(int src);
    __fastcall AnsiString(unsigned int);
    __fastcall AnsiString(long);
    __fastcall AnsiString(unsigned long);
    __fastcall AnsiString(__int64);
    __fastcall AnsiString(unsigned __int64);
    __fastcall AnsiString(double src);
    __fastcall AnsiString(const WideString &src);
#if defined(_STRING_DT_CURR_CTR)
    __fastcall AnsiString(const Currency& curr);
    __fastcall AnsiString(const TDateTime& dt);
#endif

つまり、型変換をひととおりやってくれるらしい。こんな書き方もOKだった。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    AnsiString str = M_PI;  // M_PI を使うために math.h をインクルードする
    ShowMessage(str);
}

Hide image
cb02_4

図4 文字列の表示

だが、オレ的には、桁数ぐらい自分の責任で制御したい。printfみたいに”%4.2f” とか指定できないもんかと調べたらあるじゃないか。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    AnsiString str;
    double pi = M_PI;  // M_PI を使うために math.h をインクルードする
    str.printf("円周率は %4.2lf です。", pi);

    ShowMessage(str);
}

よし、これで、C言語の知識で文字列のフォーマットができるようになった。あとは入力だが、これはGUIに頼ればよかろう。入力した文字列を数値に変換する方法だけチェックしておくとしよう。

    int          __fastcall ToInt() const;
    int          __fastcall ToIntDef(int defaultValue) const;
    double       __fastcall ToDouble() const;

ToIntもToDoubleも、変換できないとEConvertError例外が発生する。いわば、この例外を処理すれば、入力エラーにも対応できるわけだ。

こんな感じだろうか。

    double x;

    try {
        x = Edit1->Text.ToDouble();
    } catch (EConvertError *e) {
        ShowMessage ("入力データが不正です\n\n" + e->Message);
    }

ただ、エラーを表示するなら、エラーアイコン付きのダイアログを表示したいものだ。ちょっと調べたら、MessageDlgという関数があって、これでアイコンの種類を指定できるようだ。

int MessageDlg(string Msgconst, TMsgDlgType DlgType, TMsgDlgButtons Buttons, Longint HelpCtx);
int MessageDlg(string Msgconst, TMsgDlgType DlgType, TMsgDlgButtons Buttons, Longint HelpCtx,
TMsgDlgBtn DefaultButton);

TMsgDlgTypeというのが、アイコンの種類のようで、次のような列挙型だ。

enum TMsgDlgType {
  mtWarning,
  mtError,
  mtInformation,
  mtConfirmation,
  mtCustom
};

アイコン

説明

mtWarning

Hide image
mtWarning

「警告」ダイアログ

mtError

Hide image
mtError

「エラー」ダイアログ

mtInformation

Hide image
mtInformation

「情報」ダイアログ

mtConfirmation

Hide image
mtConfirmation

「確認」ダイアログ

mtCustom

「カスタム」ダイアログ


アイコンについては分かったが、TMsgDlgButtonsというのがよく分からない。TMsgDlgButtonというのは、以下の列挙型だ。これは、言うまでもなく、それぞれのボタンを表していると思われる。

enum TMsgDlgBtn {
  mbYes,
  mbNo,
  mbOK,
  mbCancel,
  mbAbort,
  mbRetry,
  mbIgnore,
  mbAll,
  mbNoToAll,
  mbYesToAll,
  mbHelp
};

これを複数指定できるのがTMsgDlgButtonsらしいのだが、はて、どうやって記述したものか。ヘルプを括ってもよく分からなかったので、書籍をいくつか覗いてみた。それによると、どうやらTMsgDlgButtonsというのは、集合型というもので、TMsgDlgButtonを複数個保持できる型らしい。オレ的には、 mbYes | mbNo のように指定できるようにしてもらいたかったが、仕方がない。とにかく書式はこうだ。

TMsgDlgButtons() << mbYes << mbNo

これで、mbYesとmbNoを保持することになるらしいのだ。理屈は分からん。まあ、とりあえず、先のエラー処理をMessageDlgを使って書き直すとこうなる。

    double x;

    try {
        x = Edit1->Text.ToDouble();
    } catch (EConvertError *e) {
        MessageDlg("入力データが不正です\n\n" + e->Message, mtError, TMsgDlgButtons() << mbOK, 0);
    }

Hide image
cb02_5

図5 MessageDlg

【補足情報】

TMsgDlgButtonsは、Delphiの集合型をC++で実装したSetクラスです。集合型は、しばしばVCLコンポーネントで複数のbool値を持つことのできるプロパティとして利用されています。

例えば、多くのコンポーネントにあるFontのStyleプロパティも集合型です。このプロパティは、列挙型TFontStyleのfsBold、fsItalic、fsUnderline、fsStrikeOutの4つの値を複数設定できます。値の設定には、<< 演算子を使います。以下は、Label1のフォントをボールドに変更するコードです。

Label1->Font->Style = Label1->Font->Style << fsBold;

>> 演算子を用いると、逆に特定の値を取り除くこともできます。値が含まれているかどうかを調べるContainsメソッドを利用して、ラベルをクリックするとフォントのボールドスタイルがオン/オフされるコードを作成してみました。

void __fastcall TForm1::Label1Click(TObject *Sender)
{
    if (Label1->Font->Style.Contains(fsBold))
        Label1->Font->Style = Label1->Font->Style >> fsBold;
    else
        Label1->Font->Style = Label1->Font->Style << fsBold;
}

新規に新しい集合型を作成して、値を設定する場合には、以下のようなコードを記述します。

Label1->Font->Style = TFontStyle() << fsBold;

石原氏が、ダイアログボックスの表示に使っている TMsgDlgButtons() << mbOK もこの書式と同じで、新しい集合型TMsgDlgButtonsにmbOKのみを設定しています。


AnsiStringには、まだいくつもの便利な関数が用意されているようだが、これらは必要に迫られたときにチェックすればよいだろう。ヘッダファイルを眺めていて、c_strという便利な関数を見つけた。C言語の文字列、つまりヌルで終わる文字列へのポインタを返す関数だ。これは参照のみなので使い方には注意が必要だが、C言語の文字列処理関数を理解していれば、何とかなる道筋を与えてくれる重宝しそうな関数である。

    char* __fastcall c_str() const        { return (Data)? Data: const_cast<char*>("");}

AnsiStringは、C言語なオレにとっては、ちょっとクセのある文字列型だが、文字列を格納する領域を考えなくてよいこと、文字列の連結が容易であることなどが明らかなメリットだと思う。有限の固定バッファを宣言して文字列処理をするのは、もはやレトロプログラミングなのかもしれないと思い知った。

    RGB表記文字列変換プログラムを作る

C++Builderの文字列処理をマスターしたところで、数値を入力して文字列に変換する簡単なプログラムを作ってみることにしよう。といっても実用性も大事だから、RGBの10進表記を16進に変換するプログラムを作ってみることにした。ありきたりだが、HTMLを記述するときに役に立つはずだ。

最初に、[ファイル|新規作成|VCLフォームアプリケーション]を選択する。図のような空のフォームが表示される。

Hide image
cb02_6

図6 VCLフォームアプリケーションの新規作成

このフォームのプロパティを変更する。Nameを「RGBForm」に、Caption(つまりウィンドウのタイトル)を「RGB変換」に設定しよう。

プロパティ

Name

RGBForm

Caption

RGB変換


これで、作成しているフォームクラスは、TRGBFormになった。ファイル名がデフォルトのままで気持ち悪いので、[ファイル|すべて保存]を実行し、Unitをrgbunit、Projectをrgbcnvという名前でそれぞれ保存する。

フォームには、コンポーネントを次のように配置する。

Hide image
cb02_7

図7 コンポーネントの配置

それぞれのプロパティを次のように設定する。

Label1

プロパティ

Caption

Red


Edit1

プロパティ

Name

edtRed

Text

0


Label2

プロパティ

Caption

Green


Edit2

プロパティ

Name

edtGreen

Text

0


Label3

プロパティ

Caption

Red


Edit3

プロパティ

Name

edtBlue

Text

0


Label4

プロパティ

Caption

Hex


Edit4

プロパティ

Name

edtHex

Text


Button1

プロパティ

Name

btnConvert

Caption

Convert


いくつか試作プログラムを作って気がついたが、配置したコンポーネントの名称は、コーディングで使う場合には、重要だ。C++Builderが勝手につけるEdit1とか2だと、何が1だったか分からなくなる。まあ、それなりに分かりやすい名前をつけておくことが無難だ。だが、Label1のようにまったくコーディングで登場しないコンポーネントなら、特に名前をつけておく必要もないと思う。Label1で十分だと思うがどうだ。

ところで、こんなテクニックも習得した。コンポーネントをマウス操作で移動させようとすると、ベースラインに合わせるガイドが出る。これは便利だ。

Hide image
cb02_8

図8 ベースラインに合わせる

それから、RGBの入力ボックスなどは、先に大きさを決めておいて、コピー&ペーストしたほうが早いようだ。

Hide image
cb02_9

図9 配置したコンポーネントのコピー

プログラムは何の変哲もないものだ。[Convert]ボタンをクリックしたときの処理を、btnConvertのonClickイベントに記述する。

Hide image
cb02_10

図10 onClickイベントの記述

void __fastcall TRGBForm::btnConvertClick(TObject *Sender)
{
    try {
        unsigned int r, g, b;

        r = edtRed->Text.ToInt();
        g = edtGreen->Text.ToInt();
        b = edtBlue->Text.ToInt();

        if (r > 255 || g > 255 || b > 255) {
            MessageDlg("入力可能な数値は、0から255の間です",
                mtError, TMsgDlgButtons() << mbOK, 0);
            return;
        }
        AnsiString str;
        str.sprintf("#%02x%02x%02x", r, g, b);
        edtHex->Text = str;
    } catch (EConvertError *e) {
        MessageDlg("入力データが不正です\n\n" + e->Message,
            mtError, TMsgDlgButtons() << mbOK, 0);
    }
}

以上で完成だ。実行してボタンをクリックすれば、変換が実行される。

Hide image
cb02_11

図11 プログラムの実行

不正なデータを入れれば、エラーメッセージも出る。

Hide image
cb02_12

図12 エラーメッセージの表示

    小気味よい反応に変える

変換プログラムを作って気がついたが、これだけの変換のためにいちいちボタンをクリックするのは、なんとも億劫だ。それに入力ミスは見れば分かる。いちいち大げさなダイアログが表示されたのでは、ますます面倒というものだ。

そこで、メッセージもさりげなく、文字を入力するそばから変換をかけてみるように変えてみた。

まず、フォームからボタンを取り去り、代わりにStatusBarを配置する。

Hide image
cb02_13

図13 StarusBarの配置

プロパティは以下のひとつだけ設定する。

StatusBar1

プロパティ

SimplePanel

true


次に、エディットボックスedtRedを選択し、オブジェクトインスペクタの[イベント]ページを表示する。ここで、onChangeイベントを設定するのだが、edtGreen、edtBlueにも同じメソッドを指定するというのがミソだ。そのため、C++Builderが勝手に命名するedtRedChangeではなく、edtRGBChangeという名前をつけよう。

Hide image
cb02_14

図14 edtRGBChangeを設定する

onChangeの値列のところに表示されるedtRedChangeをあわててダブルクリックしてしまわないで、これをedtRGBChangeに変更する。それからダブルクリックすれば、edtRGBChangeという名前のイベントハンドラが表示される。

ここに先ほどボタンのOnClickイベントに設定したのと同じようなコードを記述する。コピー&ペーストして利用するといいだろう。

void __fastcall TRGBForm::edtRGBChange(TObject *Sender)
{
    try {
        unsigned int r, g, b;

        r = edtRed->Text.ToInt();
        g = edtGreen->Text.ToInt();
        b = edtBlue->Text.ToInt();

        if (r > 255 || g > 255 || b > 255) {
            StatusBar1->SimpleText = "入力可能な数値は、0から255の間です";
            return;
        }
        AnsiString str;
        str.sprintf("#%02x%02x%02x", r, g, b);
        edtHex->Text = str;
    } catch (EConvertError *e) {
        StatusBar1->SimpleText = "入力データが不正です: " + e->Message;
        return;
    }
    StatusBar1->SimpleText = "";
}

唯一の違いは、エラーメッセージの出力先だ。このイベントハンドラを、edtGreen、edtBlueにも設定する。フォームデザイナでそれぞれのコンポーネントを選択して、onChangeイベントの値列のドロップダウンリストからedtRGBChangeを選択するだけだ。

Hide image
cb02_15

図15 修正したプログラム

ところで、btnConvertClickが残ってしまっている。削除するには、ヘッダファイルも修正しなければならないかと面倒に思ったが、メソッドの中身を消すだけでよいようだ。

void __fastcall TRGBForm::btnConvertClick(TObject *Sender)
{

}

このようにしてコンパイルすると、勝手にソースコードから削除された。中身があるうちは消さないとは賢い。用心深いオレは、コメントにした場合はどうなるかも試してみた。

void __fastcall TRGBForm::btnConvertClick(TObject *Sender)
{
    // comment
}

少なくとも、このコメントは消されずに残った。これもまた若干クセのある特徴だが、うまく付き合えば便利そうだ。

    コマンドライン版RGB変換プログラム

まあ、どうでもよいことだが、コマンドライン版はこうだ。

int main(int argc, char* argv[])
{
    if (argc < 4) {
        cout << "[Usage]" << endl
             << "rgb2hex red green blue" << endl;
        return -1;
    }

    unsigned int r, g, b;

    r = atoi(argv[1]);
    g = atoi(argv[2]);
    b = atoi(argv[3]);


    if (r > 255 || g > 255 || b > 255) {
        cerr << "入力可能な数値は、0から255の間です" << endl;
        return -1;
    }


    // printf("#%02x%02x%02x", r, g, b);
    // の代わり

    cout << "#";

    cout << hex;
    cout.width(2);
    cout.fill('0');
    cout << r;

    cout << hex;
    cout.width(2);
    cout.fill('0');
    cout << g;

    cout << hex;
    cout.width(2);
    cout.fill('0');
    cout << b << endl;

    return 0;
}

Server Response from: ETNASC04