C++/CLI の薄い本(第3巻)

本ページは2013年に「Visual Studio 2008 express」を試用した際に書いたものを
「Visual Studio 2015 express」用に書き直したものです。
一部、VS2008とVS2015の差異による動作の違いがある可能性が御座います。

Copyright © 2013-2016 GATARO All Rights Reserved. 筆者のメインサイト

TextからPanelへ

SAMPLE07をPanel化

以下のソースはSAMPLE07で用いたRichiTextBoxPanelに置き換えたものです(赤字が変更部分)。
PanelRichiTextBoxにおけるテキスト編集の様に何か特別な事ができる訳ではありません。しかし基本的なウィンドウであるが故、様々な機能を独自に追加するのに最も適しているウィンドウであるとも言えます。

実際には「様々な機能を独自に追加する」と言っても、多くの機能(コントロール)はVC++に標準装備されています。新たな機能をもつウィンドウ(コントロール)を作成する前に標準コントロールで使えるものが無いかを調べる事が大事です。本稿でPanelを使うのは説明の為の便宜にすぎません。

//SAMPLE08
#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 MyPanel : public System::Windows::Forms::Panel{
    protected: virtual void OnMouseDown(MouseEventArgs^ e) override {
        System::Drawing::Graphics^ g = CreateGraphics();
        g->FillRectangle(System::Drawing::Brushes::Red,e->X,e->Y,10,10);
    }
};
ref class MyForm : public System::Windows::Forms::Form{
    public : MyForm(){InitializeComponent();}
    private: MyPanel^ mypanel;
    private: void InitializeComponent();
};
void MyForm::InitializeComponent(void){
    Size = System::Drawing::Size(140,180);
    mypanel = (gcnew MyPanel());
    mypanel->Dock = DockStyle::Fill;
    mypanel->BackColor = System::Drawing::Color::FromArgb(255,255,180); 
    Controls->Add(mypanel); 
}
void main(){
    Application::Run(gcnew MyForm());
}

実行結果は次に紹介するSAMPLE09と全く同じに付き割愛

定義とロジックの分離

前述のソースはクラス定義の中にロジックを書くなど、見辛い所が多々ありますので少し整形したのが以下のSAMPLE09です。
具体的にはOnMouseDown()を外出しにしています。
またOnMouseDown()内で確保したMyGraphicsを明示的に解放するためにdelete MyGraphicsを追加しています。

C++/CLIはガべッジコレクションがありますが、それは万能ではありません。CreateGraphics()の様な関数を明示的に用いた場合は、やはり明示的ににメモリを確保する必要があります。どの関数が意識的な解放が必要かは一概には言えませんが「領域を確保する系」の関数を使用する際はMSの公式サイトにてdeleteが必要かを確認してから使用するのが無難です。

//SAMPLE09
#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 MyPanel : public System::Windows::Forms::Panel{
    protected: virtual void OnMouseDown(MouseEventArgs^ e) override ;
};
ref class MyForm : public System::Windows::Forms::Form{
    public : MyForm(){InitializeComponent();}
    private: MyPanel^ mypanel;
    private: void InitializeComponent();
};
void MyPanel::OnMouseDown(MouseEventArgs^ e){
    System::Drawing::Graphics^ MyGraphics ;
    MyGraphics = CreateGraphics();
    MyGraphics->FillRectangle(System::Drawing::Brushes::Red,e->X,e->Y,10,10);
    delete MyGraphics ;
}
void MyForm::InitializeComponent(void){
    Size = System::Drawing::Size(140,180);
    mypanel = (gcnew MyPanel());
    mypanel->Dock = DockStyle::Fill;
    mypanel->BackColor = System::Drawing::Color::FromArgb(255,255,180); 
    Controls->Add(mypanel); 
}
void main(){
    Application::Run(gcnew MyForm());
}

再表示問題の解決

実行結果

下記がSAMPLE09(SAMPLE08も同じ)の実行結果です。一見何の問題もなく表示されている様に見えますが、ウィンドウのサイズを縮めて再度広げると一部の表示が失われてしまいます。これはアプリケーションが行なった表示結果を維持するのはOS(Windows)の仕事ではなく、アプリケーション自身の仕事だからです。SAMPLE09ではこれに相当するコードが一切書かれていない為におきる現象です。

SANPLE07ではこの問題は起きません。RichTextBoxは表示結果を維持するためのコードがRichTextBoxクラスの内部に書かれているので、プログラマはこれを意識する必要がないのです。これに対しPanelはプログラマが自分で表示結果を維持しなければならないのです。



【左】起動後、Panel領域画面を何度かクリックし、赤い四角を幾つか描いた所。
【中】マウス操作でウィンドウサイズを縦方向に縮めた所。
【右】ウィンドウサイズを元の大きさに戻した所。一部の表示が失われている。

表示結果の維持と再表示ロジックの追加

上記の問題を解決するには、プログラマは自分で「何を描いたか」を記憶し、ウィンドウサイズ変更等のイベントが生じた際に、損傷を受けた部分(もしくは表示領域全体)を再表示する必要があります。SAMPLE10SAMPLE9に対して再表示の為のコードを追記したものです。具体的な変更点は以下の通りです。

表示四角形の座標を記憶保持する

まず、Point型の値をで要素数可変の配列に記憶するために、PointクラスvectorMyPanel内に用意します。(※1)
C++/CLIでvectorを使う場合は<vector>ではなく<cliext/vector>をインクルードすることに注意してください。(※2)
あとは、OnMouseDown()のイベントが生じる都度、その座標をvectorに格納するだけです。
OnMouseDown()の末尾にあるRefresh()は強制的に再描画を要求する関数です。(OnPaintイベントを発生します)

OnPaintのオーバーライド

Onpaint()は再描画が必要になった際にOS(=Windows)から呼ばれます。これをオーバライドします。(※3)
ここに再表示のコードを書くことにより、ウィンドウのサイズ変更等で描画が失われた際に再描画が行なわれます。具体的には前述のvectorに格納されている全ての座標位置を読み出し、そこに四角形を書いています。

CreateGraphics()とそのdeleteの廃止

SAMPLE09にあったガベッジコレクション型の言語としては不自然なCreateGraphics()とそのdelete処理が消えています。
これは表示を担当する関数が、OnMouseDown()からOnPaint()に変わったため、グラフィック処理の為に必要な変数をOnPaint()の引数であるPaintEventArgs^から取得できるようになった為です。

他にも、表示される四角形の色を青に変更していますが、この色の変更に特に深い意味はありません・

//SAMPLE10
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"") 
#using <System.dll>
#using <System.Windows.Forms.dll>
#using <System.Drawing.dll>
#include <cliext/vector>  //(※2)
using namespace System::Windows::Forms ;
ref class MyPanel : public System::Windows::Forms::Panel{
    protected: virtual void OnMouseDown(MouseEventArgs^ e) override ;
    protected: virtual void OnPaint(PaintEventArgs^ e) override ;  //(※3)
    private:   cliext::vector<System::Drawing::Point> MyPoints ;   //(※1)

};
ref class MyForm : public System::Windows::Forms::Form{
    public : MyForm(){InitializeComponent();}
    private: MyPanel^ mypanel;
    private: void InitializeComponent();
};
void MyPanel::OnPaint(PaintEventArgs^ e){
    System::Drawing::Graphics^ MyGraphics = e->Graphics;
    for(int i = 0; i <  MyPoints.size(); ++i) {
        MyGraphics->FillRectangle(System::Drawing::Brushes::Blue,MyPoints[i].X,MyPoints[i].Y,10,10);
    }
}
void MyPanel::OnMouseDown(MouseEventArgs^ e){
    MyPoints.insert(MyPoints.end(),System::Drawing::Point(e->X,e->Y));
    Refresh();
}
void MyForm::InitializeComponent(void){
    Size = System::Drawing::Size(140,180);
    mypanel = (gcnew MyPanel());
    mypanel->Dock = DockStyle::Fill;
    mypanel->BackColor = System::Drawing::Color::FromArgb(255,255,180); 
    Controls->Add(mypanel); 
}
void main(){
    Application::Run(gcnew MyForm());
}

以上の変更によりSAMPLE09で生じていた再表示の問題は解決します。

最終整形

以下のSAMPLE11はSAMPLE10と処理的には同じ物ですが、若干ソースを見やすくしています。具体的には以下の2点です。
(1)using namespace System::Drawing を宣言した。
(2)OnPaint()の中のfor文を、このソースの場合においてはより効率的なforeach文に変えた。
(3)MyGraphicsという名の変数のローカル変数宣言(OnPaint()内で宣言されていた)を宣言せずに済む形に書き替えた

//SAMPLE11
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"") 
#using <System.dll>
#using <System.Windows.Forms.dll>
#using <System.Drawing.dll>
#include <cliext/vector>
using namespace System::Windows::Forms ;
using namespace System::Drawing ;
ref class MyPanel : public System::Windows::Forms::Panel{
    protected: virtual void OnMouseDown(MouseEventArgs^ e) override ;
    protected: virtual void OnPaint(PaintEventArgs^ e) override ;
    private:   cliext::vector<Point> MyPoints ;
};
ref class MyForm : public Form{
    public : MyForm(){InitializeComponent();}
    private: MyPanel^ mypanel;
    private: void InitializeComponent();
};
void MyPanel::OnPaint(PaintEventArgs^ e){
    for each(Point p in MyPoints){
        e->Graphics->FillRectangle(Brushes::Blue,p.X,p.Y,10,10);
    }
}
void MyPanel::OnMouseDown(MouseEventArgs^ e){
    MyPoints.insert(MyPoints.end(),Point(e->X,e->Y));
    Refresh();
}
void MyForm::InitializeComponent(void){
    Size = System::Drawing::Size(140,180);
    mypanel = (gcnew MyPanel());
    mypanel->Dock = DockStyle::Fill;
    mypanel->BackColor = Color::FromArgb(255,255,180); 
    Controls->Add(mypanel); 
}
void main(){
    Application::Run(gcnew MyForm());
}

C++/CLIの基本は以上

以上でC++/CLIによるWindowsアプリ作成の基本は終わりです。
ここまでを理解できたなら、あとは「メニューはどうやればでるんだっけ?」とか「スクロールはどうするんだっけ?」という様な具体的な質問ができると思います。これらの質問はインターネットで検索すれば容易に情報を入手できると思います。
機会があれば、このような質問に極力短いソースの提示でお答えするサイトも作りたいのですが、とりあえずRETROF-16の作成に戻りたいので、続きが書かれるとしても数年後だと思います。(2013年8月4日)