Help us understand the problem. What is going on with this article?

[VisualStudio]C++/CLIのUIデザイナーをC++で使う

More than 5 years have passed since last update.

はじめに

 最近はゲームエンジンを活用する事例が増えていますね。
ゲームエンジンを使うと3Dが楽とかプログラミングの量が減るとか色々利点もあるんですが、痒い所に手が届かないとか、勉強するために直接コード書きたいとか、ロゴ消すのに金がかかるとかあって使いたく無い人はたくさんいるわけですね。

 全部コード書いてなんとかする場合に面倒なのがGUIのデザインです。座標等を直接コードに書くCUIのスタイルでGUIのデザインをするのは苦行すぎます。

 そんなこんなでVisualStudioのC++/CLIのフォームデザイナをC++で使う方法を解説します。

 まずC++/CLIのWindowフォームの仕様をざくっと解説した後、生成されるコードを解析するコードを解説します。

※VisualStudioは2013Communityを使っています、他は知らない

フォームを追加する

cppのプロジェクトを作成しているとします。

『ソリューションエクスプローラー>追加>新しい項目>UI>Windowsフォーム』でフォームを追加します。
何か警告が出ますが無視して大丈夫です。
 ファイル名は好きに設定して下さい。(ここでは"MyForm.h"で作成したとします。)
"MyForm.cpp"は使わないので完全に削除して下さい。

フォームの設定

MyForm.hを開きデザイナのフォームを右クリックしてプロパティウィンドウを表示します。

必要なら設定を変更します。
Sizeでフォームのサイズを変更します、枠やタイトル部分を含めたくない場合+16,+38するか、FormBorderStyleをNoneにして値を設定します
編集するとき視認性が悪い場合とかはBackColorを適切な色に変更したり、BackGroundImageを変えます。

コントロールの配置

 『上部メニュー>表示>ツールボックス』を表示し、UIをデザインしていきます。
テキストはlabel、画像等はPictureBox、後はButton等を配置すると良いと思います。

コードの確認

 フォームを右クリックして、『コードの表示』を選択すると、生成されたC++/CLIのコードが見れます。

 C++/CLIのコードなので、あたり前ですが純粋なC++のコードとは互換性がなくコンパイルは出来ません、defineマクロでなんとかするのも多分無理です。

//defineマクロでエラーを回避出来ない所の例
public ref class MyForm : public System::Windows::Forms::Form//public ref が邪魔
private: System::Windows::Forms::PictureBox^  pictureBox1;//*が^になってる

 じゃあ、ソースコードを修正すればコンパイル出来るの?となりますが。
かなり面倒な上に修正するとデザイナで編集出来なくなるので、おすすめ出来ません。
 あと出力されるコードのエンコーディングは基本的には『Shift-JIS』ですが、コード中にUnicodeに存在するがShift-JISに存在しない文字がある場合、『UTF-8(BOM有り)』になるので注意。

ソースコードの解析

 コンパイル出来ないなら、構文解析してコンパイル可能なコードを生成すれば良いじゃない?という発想になります。

//一部抜粋
this->pictureBox1->Location = System::Drawing::Point(88, 108);
this->pictureBox1->Name = L"pictureBox1";
this->pictureBox1->Size = System::Drawing::Size(18, 18);

this->button1->Location = System::Drawing::Point(337, 95);
this->button1->Name = L"button1";
this->button1->Size = System::Drawing::Size(75, 23);

this->label1->Location = System::Drawing::Point(147, 209);
this->label1->Name = L"label1";
this->label1->Size = System::Drawing::Size(43, 12);

 細かい違いはありますが、どのコントロールもこの辺りが共通です。
一行ずつ読み込んで、Locationの文字を探してどうのこうのすれば簡単に値が取り出せそうです。
 コントロールの種類は『private: System::Windows::Forms::』の行で調べられると思われます。
良く分からない9割の行は無視すれば良いでしょう。
 独自の仕様を取り入れたい場合はTag属性が好きに使えると思います。

実際に解析してみる

 こんな感じで値を取り出せます。
※File読み込み関連のクラスは自作ライブラリの物を使っているので置き換えてください
※その他、自分が使ってるライブラリ等にとって便利なように改変が必要です
※エラーチェックやら、効率等は無視しているので指摘は不要です

//一応ライセンスはパブリックドメインでどうぞ
enum class ControlType
{
    Label,
    Picture,
    Button,
    Unknown
};

struct UIData
{
    Rect Size;
    ControlType Type;
    std::string Name;
    std::string Tag;
};

int main(int argc, char* argv[])
{
    //Fileは自作ライブラリのクラス、一行ずつ文字列を読みこんで格納する
    File file("MyForm.h", FileMode::Read);
    std::vector<std::string> strS = file.GetLineS();

    std::vector<UIData> uiS;
    UIData client;

    int index = -1;
    int a, b,c;

    for (auto &it : strS)
    {

        if (it.find("private: System::Windows::Forms::") != std::string::npos)
        {
            uiS.push_back(UIData());
            if (it.find("Button") != std::string::npos) { uiS[uiS.size()-1].Type = ControlType::Button; }
            else if (it.find("PictureBox") != std::string::npos) { uiS[uiS.size() - 1].Type = ControlType::Picture; }
            else if (it.find("Label") != std::string::npos) { uiS[uiS.size() - 1].Type = ControlType::Label; }
            else { uiS[uiS.size() - 1].Type = ControlType::Unknown; }
        }
        //->を検索文字に入れると誤爆しない
        else if (it.find("->Location") != std::string::npos)
        {
            ++index;
            a = it.find_first_of("(");
            b = it.find_first_of(",");
            c = it.find_first_of(")");
            uiS[index].Size.x = std::atoi(it.substr(a+1, b - a).c_str());
            uiS[index].Size.y = std::atoi(it.substr(b+1, c - b).c_str());
        }
        else if (it.find("->Name") != std::string::npos)
        {
            a = it.find_first_of("\"");
            b = it.find_last_of("\"");
            uiS[index].Name = it.substr(a+1, b - a - 1).c_str();
        }
        else if (it.find("->Size") != std::string::npos)
        {
            a = it.find_first_of("(");
            b = it.find_first_of(",");
            c = it.find_first_of(")");
            uiS[index].Size.widthRight = std::atoi(it.substr(a + 1, b - a).c_str());
            uiS[index].Size.heightDown = std::atoi(it.substr(b + 1, c - b).c_str());
        }
        else if (it.find("->ClientSize") != std::string::npos)
        {
            //フォーム全体の大きさ
            a = it.find_first_of("(");
            b = it.find_first_of(",");
            c = it.find_first_of(")");
            client.Size = { 0, 0, 0, 0 };
            client.Size.widthRight = std::atoi(it.substr(a + 1, b - a).c_str());
            client.Size.heightDown = std::atoi(it.substr(b + 1, c - b).c_str());            
            break;//ここで終了
        }
        else if (it.find("->Tag") != std::string::npos)
        {
            a = it.find_first_of("\"");
            b = it.find_last_of("\"");
            uiS[index].Tag = it.substr(a + 1, b - a - 1).c_str();
        }
    }

    file.Close();
    //必要ならcsvにして出力とかする

    file.Open("UIData.csv",FileMode::Write);
    std::string data;

    for (auto && it : uiS)
    {
        data = it.Name + "," + it.Tag + "," + std::to_string((int)it.Type) + "," + std::to_string(it.Size.x) + "," + std::to_string(it.Size.y) + "," + std::to_string(it.Size.widthRight) + "," + std::to_string(it.Size.heightDown) + "\r\n";
        file.Write(data,false);
    }

    file.Close();
    //こんな感じでコードの雛形、あるいはコンパイル可能なコードを作成する
    file.Open("Sample.h", FileMode::Write);
    file.Write(data = "void Draw() override\r\n{\r\n", false);

    for (auto & it : uiS)
    {
        switch (it.Type)
        {
            case ControlType::Button:
                data = "DrawButton(" + it.Tag + "," + std::to_string(it.Size.x) + ");\r\n" /*以下パラメータ*/;
                break;
            case ControlType::Label:
                data = "DrawString(" + it.Tag + "," + std::to_string(it.Size.x) + ");\r\n" /*以下パラメータ*/;
                break;
            case ControlType::Picture:
                data = "DrawImage(" + it.Tag + "," + std::to_string(it.Size.x) + ");\r\n" /*以下パラメータ*/;
                break;
            default:
                break;
        }
        file.Write(data, false);
    }

    file.Write(data = "}" , false);
    file.Close();

    return 0;
}

 まず1回起動してコードを出力、生成したヘッダーをプロジェクトに取り込む。それ以降はデバッグビルド時はCSV作成とCSV読込をして位置等の調整を行い、リリースビルド時はCSVの読み込みだけを行なうみたいな運用で良さそうです。
 下のようなコードを適切な所に置くと、起動時に1回読み込み、デザイナーで修正後キーを押すとUIの位置を更新みたいなシステムも簡単に実現出来ますね。

static bool load = true;
if (Enter.on){ load = true; }
if( load )
{
    LoadUIData();
    load = false;
}

 コードの生成部分をいじるのも簡単ですね、使い勝手を良くしたりライブラリ化するにしてもそんなに手間ではなさそう。例えばDXライブラリ用に改変するとかなら半日もあれば出来るんじゃないでしょうか?

まとめ

 MSVC単体で完結するのが他に無い利点ですね。
 デザイナーや他OS使いの開発者と分業するのは無理そうだったりしますし、RPGのマップエディタみたいな用途で使うのも無理なので、Tiledとかのエディタ使った方が良い事の方が多いとは思われます。

 なんやかんやで本来の用途で使ってないわりには案外使えました。そういえば、C++/CLIの用途ってなんだっけ?

参考/2Dのマップエディターの定番って何だろ

 多分、Tiledを利用するパターンに続く・・・
追記:2015/3/26
続き書いた

mrdagon
趣味でゲーム作って無料公開したりしてます http://tacoika.blog87.fc2.com/
https://www.vector.co.jp/vpack/browse/person/an047099.html
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away