RX72N Envision Kit での開発(その5)GUI編

GUI_sample (RX65N/RX72N Envision Kit)

C++ GUI フレームワーク

RX65N Envision Kit から採用された GUI ライブラリとして、emWin が既にあります。

ですが、これは C で実装されており、アプリケーションを作るには、ハードルが高いと思えます。

以前に PC 向けに、OpenGL を描画エンジンとして使った、GUI フレームワーク glfw3_app を実装した経緯があり、GUI 操作に必要な構成は研究して判っているつもりなので、それらの知見を使い、組み込みマイコンでも扱いやすいようにダイエットした GUI Widget のフレームワークを実装しました。

「漢字」が標準で使えるのも特徴です。
※現在は16x16ピクセルのフォント「東雲16ドット漢字フォント」を利用させてもらっています。
※メモリに余裕があるので、ROM 領域にビットマップとして持っています。(260キロバイト程度消費する)
※漢字が必要無い場合は、含めない事も出来ますし、又はSDカードから読み込んでキャッシュする事も出来ます。

描画に関しては、DRW2D エンジンが無くても利用可能なように、現在はソフトウェアーで処理しています。
※RX64M/RX71M などに LCD をバス接続した場合や、フレームバッファ内蔵の LCD を接続する場合を考慮しています。
※DRW2D でアクセレートする事も可能な構造にしてあります。(DRW2D 版は開発中)

このフレームワークでは、記憶割り当てを使わない事を念頭に設計してあり、比較的小規模なアプリ向けとして機能を絞ってあります。
もっと「リッチ」な物が必要なら、新たに実装して追加出来る余地も残してあります。

現状で、用意してあるのは以下の Widget です。
※ソースコードは、「RX/graphics」以下にあります。

Widget 機能 ソース 完成度
frame フレーム frame.hpp
button ボタン button.hpp
check チェックボックス check.hpp
radio ラジオボタン radio.hpp
slider スライダー slider.hpp
menu メニュー menu.hpp
spinbox スピンボックス spinbox.hpp ×
group グループ管理 group.hpp

「見た目」は、シンプルなものにしてあり、ピクセルデータを用意する事無く、プリミティブの組み合わせで描画しています。

今後、必要な widget を拡充して行く予定です。

全体の構成

GUI は一般的には、メッセージ通信により、機能を提供する事が一般的だと思います。
※代表的なのは Windows でしょうか・・

ただ、この方式は、冗長なコードになりやすいし、機能が複雑になるとメッセージの順番や、メッセージのマスクなど、トリッキーなコードになりやすいと思います。

このフレームワークでは、「同期式」と呼ばれる、リアルタイムなゲームで使われるようなシステムを使っています。

画面の更新は 60Hz 程度なので、それに合わせて、タッチパネルの情報を取得して順次処理を行っています。

又、C++ の機能を積極的に利用する事で、シンプルな構成に出来、アプリケーションを実装しやすくします。

GUI 部品管理

この GUI フレームワークでは、管理する Widget の数をテンプレートで定義しています(有限個)。

    // 最大32個の Widget 管理
    typedef gui::widget_director<RENDER, TOUCH, 32> WIDD;
    WIDD        widd_(render_, touch_);

new などを使い、動的に増やす事も出来ますが、メモリが足りなくなった場合の対応を考えると、「有限数」の方が管理し易いように思います。

LCD は 4.3 インチで、解像度も 480 x 272 程度と小さいので、PC のディスクトップのように、複雑でリッチな GUI は、当面必要無いと思える為です。


C++ での実装で、少し問題な点があります、現状の実装では、widget の追加と削除は、グローバル関数としています。

extern bool insert_widget(gui::widget* w);
extern void remove_widget(gui::widget* w);

この関数は、「widget_director」テンプレートクラス内の API「insert、remove」を呼ぶようにしなければなりません。
そこで、サンプルでは、以下のように、widget_director のインスタンスを置いてあるソースで定義してあります。

/// widget の登録・グローバル関数
bool insert_widget(gui::widget* w)
{
    return widd_.insert(w);
}

/// widget の解除・グローバル関数
void remove_widget(gui::widget* w)
{
    widd_.remove(w);
}

※他に良い方法を思い付かなかったので、このように、あまりスマートとは言えない方法になっています。
※こうしておけば、widget_director を複数持って、場合により、切り替える事も出来そうです。

widget_director は、描画ループの中から「update」を呼び出せば、全ての管理が行われます。

    while(1) {
        render_.sync_frame();
        touch_.update();

        widd_.update();

...

    }

※「touch_.update();」は、タッチパネルインターフェース(FT5206)のサービスです。
※widget_director テンプレートでは、レンダリングクラスと、タッチパネルクラスの型を必要とし、コンストラクター時、参照で与えます。

    // GLCDC 関係リソース
    typedef device::glcdc_mgr<device::GLCDC, LCD_X, LCD_Y, PIX> GLCDC;

    // フォントの定義
    typedef graphics::font8x16 AFONT;
//  for cash into SD card /kfont16.bin
//  typedef graphics::kfont<16, 16, 64> KFONT;
    typedef graphics::kfont<16, 16> KFON
    typedef graphics::font<AFONT, KFONT> FONT;

    // DRW2D レンダラー
//  typedef device::drw2d_mgr<GLCDC, FONT> RENDER;
    // ソフトウェアーレンダラー
    typedef graphics::render<GLCDC, FONT> RENDER;

    GLCDC       glcdc_(nullptr, reinterpret_cast<void*>(LCD_ORG));
    AFONT       afont_;
    KFONT       kfont_;
    FONT        font_(afont_, kfont_);
    RENDER      render_(glcdc_, font_);

    FT5206_I2C  ft5206_i2c_;
    typedef chip::FT5206<FT5206_I2C> TOUCH;
    TOUCH       touch_(ft5206_i2c_);

    // 最大32個の Widget 管理
    typedef gui::widget_director<RENDER, TOUCH, 32> WIDD;
    WIDD        widd_(render_, touch_);

widget 親子関係とグループ化

widget は、階層構造が可能なようにしてあり、座標管理も差分で行えるようにしてあります。
※その為、親、子、関係があります。

また、ラジオボタンのように、自分の変化を受けて、他も変化が必要な場合があり、この場合、グループ化が役立ちます。

以下のように、3つのラジオボタンをグループ化しておけば、チェック、アンチェックの管理は自動で行えます。
※ラジオボタンの実装では、自分の状態が変化した時、「自分の親」に登録されている、「子」で、ラジオボタンを調べて、その状態を変更しています。

    typedef gui::group<3> GROUP3;
    GROUP3      group_(vtx::srect(   10, 10+50*2, 0, 0));
    typedef gui::radio RADIO;
    RADIO       radioR_(vtx::srect(   0, 50*0, 0, 0), "Red");
    RADIO       radioG_(vtx::srect(   0, 50*1, 0, 0), "Green");
    RADIO       radioB_(vtx::srect(   0, 50*2, 0, 0), "Blue");

※ラジオボタンは、グループ化する為、グループ座標の差分となっている。
※各 widget では、サイズ指定で「0」を指定すると、標準的なサイズがロードされます、この定義は、各 widget 内で定義されています。

C++ では、オペレータを使って、特別な機能を割り当て出来ます。
この場合、「+」は、group への、radio ボタン登録として機能します。

    // グループにラジオボタンを登録
    group_ + radioR_ + radioG_ + radioB_;

コールバックとラムダ式

C++ では、C++11 からラムダ式が使えるようになりました。

GUI の操作では、何か「変化」が発生した場合に、コールバック関数が呼ばれます。

C++ では、std::function テンプレートを使っています。

たとえば、button widget では、以下のように定義してあります。

    typedef std::function<void(uint32_t)> SELECT_FUNC_TYPE;

ボタンが押された(ボタンをタッチして、離れた瞬間)時、押された回数をパラメータに、コールバック関数が呼ばれます。

C++ では、ラムダ式が使えるので、コールバック関数を登録しないで、ラムダ式により、直接動作を実装出来ます。

    button_.at_select_func() = [=](uint32_t id) {
        utils::format("Select Button: %d\n") % id;
    };

これは、非常に便利で、アプリケーションを作成する時は大いに役立ち、シンプルに実装出来ます。
※クラス内の場合は、キャプチャーを「[this]」とする事で、クラス内のメソッドを呼べるようになります。

GUI サンプルのメイン

サンプルでは、一通りの GUI を定義、登録して、各 widget にラムダ式を使って、挙動を表示(シリアル出力)するようにしています。

widget の定義と登録:
※各 widget のコンストラクターで、widget_director へ登録される。

    typedef gui::button BUTTON;
    BUTTON      button_  (vtx::srect(   10, 10+50*0, 80, 32), "Button");
    typedef gui::check CHECK;
    CHECK       check_(vtx::srect(   10, 10+50*1, 0, 0), "Check");  // サイズ0指定で標準サイズ
    typedef gui::group<3> GROUP3;
    GROUP3      group_(vtx::srect(   10, 10+50*2, 0, 0));
    typedef gui::radio RADIO;
    RADIO       radioR_(vtx::srect(   0, 50*0, 0, 0), "Red");
    RADIO       radioG_(vtx::srect(   0, 50*1, 0, 0), "Green");
    RADIO       radioB_(vtx::srect(   0, 50*2, 0, 0), "Blue");
    typedef gui::slider SLIDER;
    SLIDER      sliderh_(vtx::srect(200, 20, 200, 0), 0.5f);
    SLIDER      sliderv_(vtx::srect(440, 20, 0, 200), 0.0f);
    typedef gui::menu MENU;
    MENU        menu_(vtx::srect(120, 70, 100, 0), "ItemA,ItemB,ItemC,ItemD");

登録された GUI を有効にして、コールバック関数に、ラムダ式で挙動を実装する。

    void setup_gui_()
    {
        button_.enable();
        button_.at_select_func() = [=](uint32_t id) {
            utils::format("Select Button: %d\n") % id;
        };

        check_.enable();
        check_.at_select_func() = [=](bool ena) {
            utils::format("Select Check: %s\n") % (ena ? "On" : "Off");
        };

        // グループにラジオボタンを登録
        group_ + radioR_ + radioG_ + radioB_;
        group_.enable();  // グループ登録された物が全て有効になる。
        radioR_.at_select_func() = [=](bool ena) {
            utils::format("Select Red: %s\n") % (ena ? "On" : "Off");
        };
        radioG_.at_select_func() = [=](bool ena) {
            utils::format("Select Green: %s\n") % (ena ? "On" : "Off");
        };
        radioB_.at_select_func() = [=](bool ena) {
            utils::format("Select Blue: %s\n") % (ena ? "On" : "Off");
        };
        radioG_.exec_select();  // 最初に選択されるラジオボタン

        sliderh_.enable();
        sliderh_.at_select_func() = [=](float val) {
            utils::format("Slider H: %3.2f\n") % val;
        };
        sliderv_.enable();
        sliderv_.at_select_func() = [=](float val) {
            utils::format("Slider V: %3.2f\n") % val;
        };

        menu_.enable();
        menu_.at_select_func() = [=](uint32_t pos, uint32_t num) {
            char tmp[32];
            menu_.get_select_text(tmp, sizeof(tmp));
            utils::format("Menu: '%s', %u/%u\n") % tmp % pos % num;
        };

まとめ

やはり、GUI のような構造的な体系には、C++ が必要だと痛感します。

今回の GUI フレームワークで、widget は「継承」を使っていますが、GUI の部品はスタティックに定義してあり、「new」や「delete」もしません。
※実際は、偶然そうなっているのでは無く、「しなくて済むよう」に工夫しています。

複雑な構成のアプリケーションを実装したい場合、シーン管理を使って、各シーンで登場する GUI を定義、実装すれば、シーン毎に GUI の定義を別ける事が出来ます。
※「LOGGER_sample」を参照。(未完成で実装中です)

シーンの定義については、「common/scene.hpp」テンプレートクラスを参照して下さい。