コード全体はgithubのogata-k/GUI_cmp/example_conrodを参考にしてください。
conrod
今回はこちらを参考にしてそこに挙げられている例の指定の通りのバージョン0.53.0
を使用しています。
どんなクレートか
conrodはpistonというゲームエンジンを開発しているところが作成しているRust純正のGUIライブラリです。widget
の構成や管理、widget
に対するイベントの伝播などの基本的な機能を担当できます。しかし、実際の描画やOSからのイベントの受取はconrodは、conrodに用意されている描画用のglium(OpenGL)、イベントの管理用のwinitのバックエンドが用意されているので、そちらを使うことになります。
conrodで作られたGUIはRustらしくマルチプラットフォームで動作するので簡単にほかのOSに対応できます。
conrodはwidget
毎にId
を用意して管理します。複雑になればなるほどId
の用意が大変になって管理しづらくなってくるのですが、Id
の一意性からwidget
の変数名みたいな扱いができる帯出すのが。これにより取得したId
によってwidget
を判断したり、widget
を指定して呼び出すのが簡単にできるようになります。
具体例がここにいくつか載っているので参考になると思います。
コード
カウントアップをするプログラムを書いていきます。
#[macro_use]
extern crate conrod;
extern crate find_folder;
use conrod::{widget, color, Colorable, Borderable, Sizeable, Positionable, Labelable, Widget};
use conrod::backend::glium::glium;
use conrod::backend::glium::glium::{DisplayBuild, Surface};
// 使用するid一覧
widget_ids!(
struct Ids {
canvas,
num_lbl,
button,
});
fn main() {
// 設定値
const TITLE: &'static str = "カウントアップ";
let width = 400;
let height = 300;
// windowの作成
let display = glium::glutin::WindowBuilder::new()
.with_dimensions(width, height)
.with_title(TITLE)
.build_glium() // windowの構築
.unwrap();
// Uiの作成
let mut ui = conrod::UiBuilder::new([width as f64, height as f64]).build();
// Uiで使うFontをassets以下のファイルからfont::Mapに追加
let assets = find_folder::Search::KidsThenParents(3, 5)
.for_folder("assets")
.unwrap();
let font_path = assets.join("fonts/NotoSans/NotoSans-Regular.ttf");
ui.fonts.insert_from_file(font_path).unwrap();
// idを管理するための管理者の作成
let ids = &mut Ids::new(ui.widget_id_generator());
// gliumで描画するためのrendererの準備
let mut renderer = conrod::backend::glium::Renderer::new(&display).unwrap();
// The widgetとimageを結びつけて管理するmapping
let image_map = conrod::image::Map::<glium::texture::Texture2d>::new();
let mut num = "0".to_string();
let mut event_loop = EventLoop::new();
// windowのイベントループ
'main: loop {
for event in event_loop.next(&display) {
// windowのイベントのハンドラーをuiにセット
if let Some(event) = conrod::backend::winit::convert(event.clone(), &display) {
ui.handle_event(event);
event_loop.needs_update();
}
match event {
// Escapeはwindowの削除用に
glium::glutin::Event::KeyboardInput(
_,
_,
Some(glium::glutin::VirtualKeyCode::Escape),
) |
glium::glutin::Event::Closed => break 'main,
_ => {}
}
}
set_widgets(ui.set_widgets(), ids, &mut num);
// Uiの描画とその表示
if let Some(primitives) = ui.draw_if_changed() {
renderer.fill(&display, primitives, &image_map);
let mut target = display.draw();
target.clear_color(0.0, 0.0, 0.0, 1.0);
renderer.draw(&display, &mut target, &image_map).unwrap();
target.finish().unwrap();
}
}
}
// 配置するwidgetの配置方法の指定関数
fn set_widgets(ref mut ui: conrod::UiCell, ids: &mut Ids, num: &mut String) {
// 背景(canvas)
widget::Canvas::new()
.pad(0.0)
.color(conrod::color::rgb(0.2, 0.35, 0.45))
.set(ids.canvas, ui);
// canvasのidを使い指定することでuiからcanvasの横と縦の配列を取得
let canvas_wh = ui.wh_of(ids.canvas).unwrap();
// 数値の表示
widget::Text::new(num)
.middle_of(ids.canvas)
.font_size(140) // フォントのサイズを指定!!
.color(color::WHITE) // 色の指定も簡単にできる
.set(ids.num_lbl, ui);
// カウントボタン
if widget::Button::new()
.w_h(canvas_wh[0] - 10.0, 40.0) // 幅
.mid_bottom_with_margin_on(ids.canvas, 5.0)// 位置
.rgb(0.4, 0.75, 0.6) // 色
.border(2.0) // 境界
.label("count +1")
.set(ids.button, ui)
.was_clicked()
{ // if式の実行部分
if let Ok(count) = num.parse::<u32>() {
*num = (count+1).to_string();
} else {
println!("invalid number");
}
}
}
// イベントの管理用構造体
struct EventLoop {
ui_needs_update: bool,
last_update: std::time::Instant,
}
impl EventLoop {
pub fn new() -> Self {
EventLoop {
last_update: std::time::Instant::now(),
ui_needs_update: true,
}
}
/// すべての更新対象となるイベントの為の順次取得用関数
pub fn next(&mut self, display: &glium::Display) -> Vec<glium::glutin::Event> {
// 60FPSより早くならないようにするために一つ前の更新対象から少なくても16ms待つことにしておく。
let last_update = self.last_update;
let sixteen_ms = std::time::Duration::from_millis(16);
let duration_since_last_update = std::time::Instant::now().duration_since(last_update); // 前回の更新時と今の時間差の取得
if duration_since_last_update < sixteen_ms {
std::thread::sleep(sixteen_ms - duration_since_last_update);
}
// イベント全体の取得
let mut events = Vec::new();
events.extend(display.poll_events()); // displayにおけるイベントの取得
// displayで更新があればUiでのイベント更新は次に持ち越し
if events.is_empty() && !self.ui_needs_update {
events.extend(display.wait_events().next());
}
// イベントの更新の後処理
self.ui_needs_update = false;
self.last_update = std::time::Instant::now();
events
}
// Uiで他のイベントの更新があるかないかを要求することをeventのループでは確認しておくこと
// これはいくつかのUiを描画する最初のタイミングや更新を要求するタイミングで使われる。
pub fn needs_update(&mut self) {
self.ui_needs_update = true;
}
}
コードの説明
まず最初に今回のプログラムで使用するId
を管理するための構造体をマクロで生成します。
widget_ids!(
struct Ids {
canvas,
num_lbl,
button,
});
そしてよくあるwindow
の作成します。
次にレイアウトを管理するui
を作成して、必要なassets
(今回はフォント)をui
に追加しています。そしてui
用のId
を生成しています。
そして次にgliumで描画するためのrendererの準備をして、その時に使用するwidget
とそのwidget
用の画像とを対応させる辞書を作成しています。
最後にevent
周り、データ変数とui
とId
を結び付ける、再描画関係を用意してmain
関数は終了です。
後はwidget
設置用の補助関数とよくあるevent
ループの処理関数だと思うので説明は省略しても分かると思います。
所感
簡単に使ってみたところconrodはラベルのサイズや色の変更が簡単で細かく装飾したい人には良さそうなクレートだと思います。まだまだチュートリアルなどの情報が少なく調べにくいですが、比較的アセット、レイアウト、イベント、変更するデータの分離ができているクレートだと思います。
まとめ
Id
での管理の面倒臭さを除いても(慣れれば)比較的使いやすいライブラリだと思う。