C++/CLI の薄い本(第1巻)
本ページは2013年に「Visual Studio 2008 express」を試用した際に書いたものを
「Visual Studio 2015 express」用に書き直したものです。
一部、VS2008とVS2015の差異による動作の違いがある可能性が御座います。
Copyright © 2013-2015 GATARO All Rights Reserved. 筆者のメインサイト
はじめに
ソース自動生成の罪
マイクロソフト社のVisual Sutudio(以下VSと略記)はデザインを指定するだけでウィンドウズアプリケーションの殆どのソースを自動生成してくれる機能を持ちます。
それはそれで便利なのですが、ソースの自動生成は諸刃の剣でもあります。
便利な反面、言語の本質を何も理解していないのにプログラムを作れる様になったという錯覚を初心者に与えてしまうのです。
これは非常に不幸な錯覚であり、この錯覚の罠に陥ると遅かれ早かれソフトウエア技術者としての壁にぶち当たります。
ここではそのような不幸を防ぐ目的も兼ね、VSの自動生成機能を使わずにすべて手打ちでウィンドウアプリケーションを作る方法を、C++/CLI言語の解説を交えながら説明していきたいと思います。
紹介するいくつかのサンプルプログラムを実際に動かす事により、VSが自動生成するプログラムの詳細が自然に理解できる事と思います。
C++/CLI での最小ウィンドウズアプリケーション
たった5行でウィンドウを表示する
VS 2015にて言語をC++/CLIとした場合のウィンドウアプリケーションの最小ソースがSAMPLE01です。
必要なファイルは以下の実質5行を記述したmain.cppのみです。ヘッダーファイルもリソースファイルもありません。
このmaim.cppだけで、ウィンドウが現れるアプリケーションが動きます。(現れるウィンドウは当然のことですが左上に終了ボタン等があるだけの「のっぺらぼう」です)
// SAMPLE01
#using <System.dll>
#using <System.Windows.Forms.dll>
void main(){
System::Windows::Forms::Application::Run(gcnew System::Windows::Forms::Form());
}
とりあえず先に実行結果を紹介
この画面を出すための具体的手順
1.VS 2015 express for Windows Disktop(無料)をインストールして実行。
2.[ファイル][新しいプロジェクト]→[空のCLRプロジェクト]を選択し適当な名前でプロジェクトを作る
3.[プロジェクト][新しい項目の追加]、[C++ファイル]を選択し、ファイル名をmain.cppとして[追加]
4.本ページのサンプルをコピペして実行
#include <XXX.h>ではなく#using <XXX.dll>を使う
先頭の2行はアプリケーションで使用するDLL(ダイナミックリンクライブラリ)の宣言です。インクルード文に似ていますが、インクルードファイルの中の記述はソースファイルの一部ですのでコンパイルの対象となります。
それに対し、この書き方はコンパイル済みのライブラリを直接指定します。意味的には「インクルード文と同じように『使うもの』を宣言する文」ですが、無駄なコンパイルを防ぎビルドを高速にします。
main()の正しい書き方
3行目はおなじみのmain()ですが、このmain()は戻り値を持つものや引数を持つものもあります。歴史的な理由から上記の書き方でもコンパイルは通りますが、C++/CLIで記述するならば下記の書き方がより良いと思います。但し本稿ではmain()の書き方の説明が目的ではありませんので、以後も上記の書き方で統一します。
// SAMOLE01のmain()の書き方を変更したソース
int main(array<System::String ^> ^args){
System::Windows::Forms::Application::Run(gcnew System::Windows::Forms::Form());
return 0 ;
}
スコープ演算子の善と悪
4行目がこの最小アプリケーションの本体となりますが、まず目に付くのは::(スコープ演算子)の多用です。スコープ演算子は限定演算子とも言い、同じ名前の識別子を区別するために用いるものです。これは「佐藤さん」は日本に大勢いるので、それだけでは区別ができませんがこれを「昭和の時代に::総理大臣を務めた::佐藤さん」とい書けば特定の1人に限定されるのと同じ理屈です。
しかし、スコープ演算子を使用するとソースの記述がどうしても長くなります。この問題はusing namespace文で「名前空間」を指定すると解決します。このプログラムのApplicationとFormは共に System::Windows::Formsという名前空間に属しますので、これを宣言するすることにより下記のように簡素な記述が可能になります。
// SAMOLE01に対しnamespaceを使用したソース
#using <System.dll>
#using <System.Windows.Forms.dll>
using namespace System::Windows::Forms ;
void main(){
Application::Run(gcnew Form());
}
名前空間はソースの記述を簡単にする反面、何がどの名前空間に属しているのかを知らないとソース中に現れた識別子が何なのかの判断に迷う場合があります。慣れない内は勉強だと思って名前空間を用いずにスコープ演算子の多段重ねで記述して方が良いかもしれません。
簡潔な記述をあえて冗長に書く
Application::Run(gcnew Form());
5行目は「Form()クラスのインスタンスを生成し、その結果をApplicationクラスのRunに引数として渡せ」という意味です。これを2行に分けて記述するとこうなります。
Form^ MyWindow = gcnew Form();
Application::Run(MyWindow);
但し2行に分けると、どこにも使われない変数(ここでは MyWindow という識別子)が登場します。これはC言語に例えると
wait(1000) ;
//1秒待つ
という関数をわざわざ
int wait_time = 1000; //1秒
wait(wait_time) ;
//待つ
と書くのと同様に、あきらかに無駄ですので普通は1行で書きます。しかし、簡潔さを最優先するソースの書き方とプログラムの意味を理解するためのソースの書き方は時として相反するものです。以降はあえて2行に分けたソースプログラムで詳細を解説します。
gcnewとRun
以下がmain()の中をあえて2行に分けて書いたソースプログラムです。
// SAMOLE01に対しnamespaceを使用し、さらにmainの中を2行に分けて記述したたソース
#using <System.dll>
#using <System.Windows.Forms.dll>
using namespace System::Windows::Forms ;
void main(){
Form^ MyWindow = gcnew Form(); // ※1
Application::Run(MyWindow); // ※2
}
※1で一般的なフォーム(アプリケーションが表示するウィンドウ)を生成し、その結果をMyWindowに格納します。 C++/CLIではCやC++の様にnewは使用せずにgcnewを使います。gcnewとはガベッジコレクション(gc)を行うnewの意味で、これを使うと確保した領域を意識してdeleteする必要がなく、自動的に開放されます。またgcnewは確保した領域の先頭アドレスを示すポインタではなく、デリゲートと呼ばれる値が戻ります。デリゲートはポインタでもアドレスでもなく、「確保した領域を管理するための識別番号」の様な物です。 デリゲート型の変数はポインタ型と区別するために*の代りに^を使用します。
※2でApplicationクラスに属するRunという名前の関数を読んでいます。引数は※1で作成したフォームを示すデリゲート型の変数です。
この2行で、OS(Windows)はフォームを表示します。標準のフォームをそのまま使っているのでOSはフォームの右上に終了を示す[×]があることも認識します。従ってそこをマウスでクリックすると、終了要求が行われた事を認識し、自動的にフォームを消し、アプリケーションを終了します。
クラスの継承をしたC++/CLI での最小ウィンドウアプリ
その前に、ドス窓の隠蔽
ここまでに紹介したサンプルプログラムを実際に動かされた方はお気づきかと思いますが、起動するとDOSプロンプト画面(通称:ドス窓)が現れた後、アプリケーションのウィンドウが表示されます。これを回避するにはpragma文を用いて、起動時にドス窓経由ではなく直接ウィンドウアプリを起動する様に指示する必要があります。
//SAMOLE01に対しnamespaceを使用し、DOSプロンプト画面の表示を抑制したソース
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#using <System.dll>
#using <System.Windows.Forms.dll>
using namespace System::Windows::Forms ;
void main(){
Application::Run(gcnew Form());
}
Formクラスの継承
ここまではWindows標準のフォーム(Formクラス)をそのまま使いましたが、これを一度継承して独自のフォーム(MyFormクラス)を使う形に書き換えます。これにより標準のフォームの機能をオーバーライドしたり、新たな機能を追加できます。もちろん標準のフォームの機能で十分だと思う方はわざわざ継承する必要はないのですが、今は十分でも将来機能が拡張される可能性は常にあるので、継承して使う方が賢明です。単に継承するだけなら下記のように1行(※印の行)で済みます。
//SAMPLE02
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#using <System.dll>
#using <System.Windows.Forms.dll>
using namespace System::Windows::Forms ;
ref class MyForm : public System::Windows::Forms::Form{}; //※
void main(){
Application::Run(gcnew MyForm());
}
継承したクラスの宣言は標準のC++と同じですが、C++/CLIではそれがマネージ型クラスである場合は文頭にrefを付ける必要があります。
Formの属性変更
Windowには様々な「属性」があります。わかりやすい属性としては「大きさ」とか「背景色」があります。
これらの属性はFormクラスの個々の属性に対応するプロパティ(メンバ変数)に値を設定することで実現できます。問題はどのタイミングで値を設定するかです。上のSAMPLE002の場合、Application::Run(gcnew MyForm());の行の前ではまだフォームのインスタンスが生成されていないので属性の変更はできません。かといってApplication::Run(gcnew MyForm());の後は、アプリケーションの[×]ボタンが押されたときにしか到達しないので意味がありません。
この問題を解決するのがコンストラクタの利用です。
// SAMPLE02に対しフォーム属性の設定を追加
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#using <System.dll>
#using <System.Windows.Forms.dll>
#using <System.Drawing.dll>
using namespace System::Windows::Forms ;
ref class MyForm : public System::Windows::Forms::Form{
public:
MyForm(){
Text = "縦長で紫";
Width = 200;
Height= 300;
BackColor = System::Drawing::Color::FromArgb(180,180,255) ; //紫色
}
};
void main(){
Application::Run(gcnew MyForm());
}
関数MyFormはクラスMyFormのコンストラクタです。ここで様々な属性設定をする事ができます。
SAMPLE02では、ウィンドウのタイトルと大きさ、それと背景色のみを設定した例です。これを実行すると左の様なウィンドウが現われます。
表示用のウィンドウの追加
フォームに直接描画はしない
フォーム上に何か(例えば文章)を書く場合、直接フォーム上に描くことはお奨めできません。なぜならフォームはウィンドウアプリケーション全体に関わる処理(例えば終了とかウィンドウの移動)に特化したウィンドウであり、描画の対象となるのは「得意」ではないのです。描画が得意ではないウィンドウに無理に描画を行うとプログラムが恐ろしく煩雑になります。これを回避するためには、例えばテキストを表示したいのであれば「テキストを処理が得意なウィンドウ」をフォームの上に重ねて配置し、そこにテキストを表示するのが得策なのです。
// SAMPLE03
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#using <System.dll>
#using <System.Windows.Forms.dll>
#using <System.Drawing.dll>
using namespace System::Windows::Forms ;
ref class MyText : public System::Windows::Forms::RichTextBox{};
ref class MyForm : public System::Windows::Forms::Form{
public:
MyText^ mytext ;
MyForm(){
Text = "テキスト";
Width = 200;
Height = 300;
BackColor = System::Drawing::Color::FromArgb(180,180,255); //紫色
mytext = (gcnew MyText());
mytext->Text = "こんにちは";
mytext->Width = 100;
mytext->Height = 150;
mytext->Location = System::Drawing::Point(40,60);
mytext->BackColor = System::Drawing::Color::FromArgb(255,255,180); //黄色
Controls->Add(mytext); //リッチテキストボックスをフォームの支配下に置く
}
};
void main(){
Application::Run(gcnew MyForm());
}
上記がリッチテキストボックスと呼ばれるテキストの表示・編集に特化したウィンドウを作成し、フォームの上に配置するプログラムです。実行結果は左の画像の様にフォーム(背景紫)の中央付近にリッチテキストボックス(背景白)が現われます。フォームとの位置関係は、フォームの左上を基準座標とした相対座標を示すmytext->Locationへ代入した値で決まります。同様にリッチテキストボックスのサイズはmytext->Widthとmytext->Heighで決まります。
境界の自動調整
フォームを用いると、マウスを用いてのウィンドウサイズ変更ができます。
しかし、SAMPLE03のリッチテキストボックスはフォームのサイズを変更してもフォームとの相対位置が維持されるだけで、大きさは変化しません。多くのウィンドウアプリケーションはフォームの大きさを変えると、内部のウィンドウも自動的にサイズが変わります。これを実現するには、サイズや位置を指定せずに、フォームに対する占有方法を指定します。(下記ソースの※印の行)
// SAMPLE04
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#using <System.dll>
#using <System.Windows.Forms.dll>
#using <System.Drawing.dll>
using namespace System::Windows::Forms ;
ref class MyText : public System::Windows::Forms::RichTextBox{};
ref class MyForm : public System::Windows::Forms::Form{
public:
MyText^ mytext ;
MyForm(){
Text = "テキスト";
Width = 200;
Height = 300;
mytext = (gcnew MyText());
mytext->Text = "こんにちは";
mytext->Dock = System::Windows::Forms::DockStyle::Fill; // ※
mytext->BackColor = System::Drawing::Color::FromArgb(255,255,180); //黄色
Controls->Add(mytext); //リッチテキストボックスをフォームの支配下に置く
}
};
void main(){
Application::Run(gcnew MyForm());
}
左が実行結果です。フォームの大きさを変えると、フォームの上にあるテキストボックスも自動的に大きさが変わrり、常にフォームがテキストボックスで満たされます。
この場合、フォームの背景色は意味を持ちませんのでSAMPLE03のフォームの背景色を紫色にする処理は不要となります。
とりあえず初回はここまで
ここまでの知識は、趣味で簡単なアプリケーション(例えばオセロゲーム)の画面を作るために必要な知識の1/10位だと思います。
更に趣味ではなく実務ならば、多くのソフトウエア開発企業が開発者に要求する基礎知識の1/100位だと思います。
言い換えればここまでを1時間で理解したならばあと99時間、1日かけて理解したならばあと99日間学習を続けることにより、C++/CLI言語を操ってウィンドウアプリケーションを作るプロとしての知識を得る事ができると思います。