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」テンプレートクラスを参照して下さい。