おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(35)Glimmer -- RubyとGUI

今回もRubyGUIのトピックです。 Glimmerを取り上げます。

Glimmerとは

GlimmerはRubyにおけるDSLDomain specific language, ドメイン固有言語)です。 すなわち、Rubyを用いてその中にプログラミング言語を作ったものです。

RubyDSLを比較的簡単に作ることのできる言語です。 例えばRakeなどは一種のDSLで、taskやfileなどの独自のコマンドを持っています。 これらのコマンドはRubyのメソッドやブロックを用いていることが多いです。

GlimmerはGUIに関する言語をRubyにおいて作ったものです。 これは言語ですから、GUIライブラリとは異なります。 GUIライブラリを土台にして、その上にライブラリを動かす言語を作っています。 GUIライブラリはJava/SWTJRuby使用)、Opal(Rails使用、ウェブブラウザ上のGUI)、libui、Tk、Gtk3(開発初期段階)などが可能です。 今回はlibui上のGlimmerを紹介します。

libuiは開発段階がmid-alpha(アルファ版の中段階)となっています。 まだ安定版は出ていませんので、このライブラリを採用するかどうかは迷うかもしれません。 もしも安定版を使いたい場合は「Glimmer dsl for SWT」を考えても良いかもしれません。 ただ、こちらはJRubyなので起動が遅い、メモリ使用量が多いなどのデメリットもあります。 長所、短所を見比べて選ぶことになると思います。

「Glimmer-dsl-libui」のGitHubページのREADME.mdが詳しいドキュメント(英語)になっています。 読むのが大変なくらい長いですが、詳しく丁寧なのでとても助かります。

ここでは「Glimmer-dsl-libui」をインストールし、使ってみた経験を書きたいと思います。 この記事はドキュメントではないので、詳細については上記のGitHubを参照してください。

インストール

Gemをインストールするだけです。 今回はGemfileを作ってBundlerでインストールしました。 Glimmerのプログラムを作る作業ディレクトリを作成し、そこにGemfileを作ります。

source "https://rubygems.org"

gem 'glimmer-dsl-libui', '~> 0.5.24'

そのディレクトリにカレントディレクトリを移動して、Bundlerでインストールします。

$ bundle install
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Using bundler 2.3.23
Using matrix 0.4.2
Fetching os 1.1.4
Fetching libui 0.0.15
... ... ...
Installing libui 0.0.15
... ... ...
Fetching glimmer 2.7.3
Installing glimmer 2.7.3
Fetching glimmer-dsl-libui 0.5.24
Installing glimmer-dsl-libui 0.5.24
Bundle complete! 1 Gemfile dependency, 15 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
$

正しくインストールできていれば、次のコマンドでウィンドウが表示されます。

$ ruby -r glimmer-dsl-libui -e "require 'examples/meta_example'"

Glimmerのexample

Hello world

ものごとの始まりはいつも「Hello world」の表示です。 プログラムを見てみましょう。

require 'glimmer-dsl-libui'

include Glimmer

w = window('hello') {
  label('Hello world')
}

w.show

Glimmerを使うには、glimmer-dsl-libuiをrequireする必要があります。 また、Glimmerモジュールをインクルードしておきます。

  • winodwメソッドは引数にウィンドウのタイトル、幅、高さを指定する。 今回は幅と高さは省略している。 ブロックにウィンドウ内に配置するオブジェクト(Glimmerでは「コントロール」と呼ぶ)を配置する。
  • labelメソッドはウィンドウ上のラベル(文字列の表示をするオブジェクト)を定義する
  • windowメソッドの返すウィンドウ・オブジェクトをwに代入する。 そのオブジェクトに対してshowメソッドを呼び出すと、ウィンドウが表示される

表示された画面は次のようになりました。

Hello world

Glimmerの使い方

requireとincludeはhello.rbのサンプル同様に指定します。

ウィンドウとメニュー

メインウィンドウはwindowメソッドで定義します。

window(タイトル, 幅, 高さ, メニュー有無) {... ... ...}

メニューの有無はtrue/falseで指定します。 デフォルトはtrueです。 trueでもメニューを定義しなければメニューは現れません。

メニューを定義するにはmenuメソッド(メニューバーに現れる項目)、menu_itemメソッド(メニューバーをクリックしたときに現れるメニューの項目)を使います。

menu(項目名)

menu_item(項目名) {... ... ...}

menu_itemではメニューがクリックされたときの動作をブロックに書きます。 動作はリスナー(クリックを聞いているもの、クリックされると動作するもの)といいます。 リスナーはon_clickedメソッドで書きます。

window、menu、menu_itemなどのメソッドはコントロールを定義し、on_clickedはリスナーを定義しますが、両者でブロックの書き方を変えるのがGlimmerの流儀です。

  • コントロール ⇒ 波括弧で書く
  • リスナー ⇒ do〜endで書く

これは分かりやすくするための習慣で、異なる書き方をしてもエラーにはなりません。 ウィンドウとメニューの例menu.rbのコードを以下に示します。

require 'glimmer-dsl-libui'

include Glimmer

menu('File') {
  menu_item('Open') {
    on_clicked do
      file =  open_file
      @label.text = File.read(file)
    end
  }
  quit_menu_item
}

window('Menu', 800, 600) {
  @label = label('')
}.show

open_fileメソッドはファイル・オープン・ダイアログを表示し、選択されたファイルのパス名を返します。

quit_menu_itemは終了メニューです。 これだけ書いておけばメニューからクリックにより終了できます。

これに似たメニューアイテムにabout_menu_itemがあります。 このメソッドは「About」という名前のメニューアイテムを追加します。 on_clickedリスナーはブロックの中に書きます。

画面のスクリーンショットは、Openメニューからファイル「menu.rb」を読み込んだ後に、終了メニューをクリックする直前の様子です。

メニュー

ボックス

コントロールを縦または横に並べるコンテナとしてhorizontal_box(横に並べる)とvertical_box(縦に並べる)があります。 それぞれのメソッドのブロックの中にコントロールを記述します。

ホリゾンタル・ボックス内のコントロールに与えられる幅は等しくなります。 バーティカル・ボックス内のコントロールに与えられる高さは等しくなります。

ボックス内の幅、高さが等分にならないようにするには、コントロールのブロック内に

stretchy false

を入れておきます。 これにより次のコントロールが詰めて配置されます。

コントロール

windowあるいはボックスに配置できるコントロールには、以下のようなものがあります(全てではない)。

  • label
  • button
  • color_button ⇒ カラー選択のダイアログが開く
  • font_button ⇒ フォント選択のダイアログが開く
  • checkbox
  • radio_buttons
  • combobox ⇒ 複数の選択肢(リスト)からひとつを選ぶことができる
  • entry ⇒ 文字列の入力ができる
  • search_entry
  • non_wrapping_multiline_entry ⇒ 複数行のテキストを編集できる
  • msg_box ⇒ message_boxも同じ。ダイアログが開く
  • progress_bar ⇒ プログレス・バー(作業の進捗を示す棒)を表示

この中のボタンとエントリーを使うと、前回のShoesで作った電卓プログラムを作れます。

ボタンは次のような構文で使います。

button(ボタンに表示する文字列) { ... ... ...}
  • ブロックのところにはリスナーのon_clickedを入れることができる。 on_clickedメソッドはボタンがクリックされたときの動作をそのブロックに記述する
  • またbuttonメソッドは、ボタンのオブジェクトを生成して返す
  • textプロパティがある。 プロパティを設定する(.text=)、プロパティの設定を読む(.text)ことができる。 Rubyのattr_accessorで定義したメソッドと、ここでいうプロパティは同じ

エントリーは次のような構文で使います。

entry {... ... ...}
  • ブロックのところにはリスナーのon_changedを入れることができる。 on_changedメソッドはentryの内容が変更されたときの動作をそのブロックに記述する
  • またentryメソッドは、エントリーのオブジェクトを生成して返す
  • readonlyとtextプロパティがある。 プロパティを設定する(.readonly=または.text=)、プロパティの設定を読む(.readonlyまたは.text)ことができる。

なお、ブロックにはパラメータをひとつ付けることができ、そのパラメータは各メソッドの返すオブジェクトと同じものです。 例えば、

entry {|e| ... ... ...}

eはentryが返すエントリーオブジェクトと同じものです。

ファイルはcalc.rblib_calc.rbの2つです。 lib_calc.rbは前回の記事(Shoesの記事)で出てきたものと同じですので省略します。 以下にcalc.rbソースコードを示します。

require 'glimmer-dsl-libui'
require_relative 'lib_calc.rb'

include Glimmer

def get_answer a
  if a.instance_of?(Float) &&  a.to_i == a
    a.to_i.to_s
  else
    a.to_s
  end
end

window('calc', 400, 80) { |w|
  calc = Calc.new
  vertical_box {
    horizontal_box {
      @e = entry
      horizontal_box {
        button("計算") {
          on_clicked do
            @answer.text = "  "+get_answer(calc.run(@e.text))
          end
        }
        button("クリア") {
          on_clicked do
            @e.text = ""
          end
        }
        button("終了") {
          on_clicked do
            w.destroy
            LibUI.quit
            0
          end
        }
      }
    }    
    @answer = label("")
  }
}.show

windowメソッドの部分を説明しましょう。

  • Calcクラスのインスタンスを生成し、ローカル変数calcに代入する。
  • バーティカルボックスに2つの要素を入れる。 (1)ホリゾンルボックス(2)ラベル(初期値は空文字列、@answer変数にそのオブジェクトを代入しておく)
  • ホリゾンルボックスには2つの要素を入れる。 (1)エントリー、そのオブジェクトは@eに代入(2)ホリゾンルボックス
  • 内側のホリゾンルボックスにはボタンを3つ入れる。
  • 「計算」ボタンはクリックされたときに@answer(これはラベルオブジェクト)のテキストにエントリの文字列@e.textから計算した結果を代入する
  • 「クリア」ボタンはクリックされたときにエントリの文字列@e.textを空文字列にする
  • 「終了」ボタンはクリックされたときにウィンドウ・オブジェクトを閉じ(w.destroy)、libuiのメインループを終了させ(LibUI.quit)0を返す

実行すると次のような画面になります。 「(2+3)*4」を実行したところです。

Calc

プログラムの大筋はShoesと似ている感じがします。

表をウィンドウ上に作るためのメソッドが用意されています。 二重配列で表のデータを作り、その配列を渡すことでウィンドウ上に表が現れます。

まず、text_columnメソッドでタイトル行を定義し、表本体は二重配列をcell_rowsメソッドに引数として渡します。

以下に例を示します。 この例は政府統計から取ってきたもので、通信機器利用を世帯単位で調査したものです。

政府統計へのリンク

require 'glimmer-dsl-libui'

include Glimmer

tbl = [
  ["20-29","79.0","99.0","41.8","30.9","51.5","4.6","0.8"],
  ["30-39","74.9","99.0","51.5","44.1","47.3","3.5","0.9"],
  ["40-49","78.1","97.8","53.0","37.6","43.1","3.6","1.7"],
  ["50-59","80.2","96.4","46.0","32.4","27.8","3.4","2.4"],
  ["60-69","75.3","94.3","36.3","26.4","14.3","1.4","2.6"],
  ["70-79","68.7","86.7","26.5","20.4","9.1","1.9","5.5"],
  ["80-","67.5","84.5","25.6","22.0","8.8","2.1","5.7"] 
  ]
  
window('インターネット利用機器調査', 800, 600) {
  margined true
  vertical_box{
    label("政府統計 令和3年通信利用動向調査より") {
      stretchy false
    }
    label("※ 複数回答あり") {
      stretchy false
    }
    horizontal_box {
      table {
        text_column('世帯主年齢')
        text_column('PC')
        text_column('携帯スマホ')
        text_column('タブレット')
        text_column('テレビ')
        text_column('ゲーム機')
        text_column('その他')
        text_column('無回答')
      
        cell_rows tbl
      }
    }
  }
}.show

実行すると次のような画面が現れます。

政府統計の表

ここで用いたのは基本的なメソッドのみです。 他のメソッドなどについてはGlimmer-dsl-libuiのドキュメントを参考にして下さい。

蛇足ですが、この統計を見ると携帯・スマホが普及していることがわかります。 これから、ウェブサイトのスマホ対応は必須だということが分かりますね。 政府統計は公開されているので、個人や会社がコストをかけずに統計を手に入れることができ、便利です。 マーケティングのコストダウンにも繋がると思います。

図形の描画

図形描画のためのキャンバスにあたるコントロールがareaコントロールです。 areaの内部にpathコントロール、さらにpathの内部に正方形(square)長方形(rectangle)円(circle)円弧(arc)直線(line)ベジェ曲線(bezier)などを描くことができます。

また、静的な図形(一度描き、そのまま居座り続ける図形)だけでなく、動的な図形(書き直しのきく図形)も可能です。 動的な図形の場合は、その図形のために用いたメモリを描画後に解放し、メモリ効率を良くします。 ここでは、動的な図形は扱いませんが、難しくはないのでドキュメントを参考にしていただけば、すぐに理解できると思います。

図形描画の例を以下に示します。

require 'glimmer-dsl-libui'

include Glimmer
include Math

window('図形描画', 800, 600) {
  margined true
  
  vertical_box {
    area {
      path {
        arc(200, 300, 150, 90, 180, true)
        fill r: 200, g: 200, b: 255, a: 1.0
      }
      path {
        circle(200, 300, 150)
        rectangle(200,100,500,400)
        stroke r: 0, g: 0, b: 0
      }
      path {
        polygon(500,300-150, 500-150*cos(PI/6),300+150*sin(PI/6), 500+150*cos(PI/6), 300+150*sin(PI/6))
        fill r: 255, g: 200, b: 200, a: 1.0
      }
      path {
        polygon(500,300-150, 500-150*cos(PI/6),300+150*sin(PI/6), 500+150*cos(PI/6), 300+150*sin(PI/6))
        stroke r: 0, g: 0, b: 0
      }
    }
  }
}.show

areaはボックスの中に入れて使います。 pathはareaの中に入れ、更にその中にrectangleなどの図形を入れ、描き方(fillまたはstroke)を指定します。

上記のプログラムを実行すると次の画面が現れます。

図形描画

  • arc(中心, 半径, 描き始めの角度で右向きが0度で反時計回りに測る, 中心角, 円弧の角の進む方向で反時計回りがtrueで時計回りがfalse)
  • circle(中心, 半径)
  • rectangle(左上のx座標, 左上のy座標, 幅, 高さ) 座標はareaの左上隅が(0,0)で、上から下に向かう方向が正、左から右に向かう方向が正
  • polygon(各点のx座標, y座標, ... ...)
  • fill r:赤, g:緑, b:青, a:透明度 各色は0から255までの数(つまり8ビット)a(アルファ・チャンネル、不透明度)は0から1 (注:ドキュメントには明確には書いてない。が、おそらく合っていると思う)。図形の境界および内部を塗りつぶす
  • stroke r:赤, g:緑, b:青 各色は0から255まで。図形の境界線を描画

ここに書いていない図形のメソッドはドキュメントを参考にしてください。 また、アニメーションもできます。 例えばサンプルプログラムの「テトリス」を試してみてください。

データ結合

Glimmerは画面(View)とモデル(Model: データを保持しているもの)をプレゼンター(Presennter)が管理するMVPの方法をとることができます。 これはRailsなどのMVCと同じ考え方です。 具体的にはビューとモデルのデータに双方向の結合(バインディング)または片方向の結合を設定できます。 片方向の場合はモデルからビューへの方向のみ可能です。 それぞれ<=>または<=という演算子を用います。

まず、モデルを用意します。 モデルはRubyのオブジェクトで、ビューに対応させたいデータはattr_accessorで定義をしておきます。 例えば、インスタンス変数@dataをビューに対応させたいときは

class A
  attr_accessor :data
end

このようにします。 それにより、クラスAのインスタンスaとすると、a.data@dataを参照でき、a.data=@dataに代入することができます。

双方向のデータ結合をentryのテキストと行いたいときは

entry {
  text <=> [a, :data]
}

のように、右辺は配列で`[オブジェクト, インスタンス変数名のシンボル]とします。 左辺のtextはentryのプロパティ、すなわち入力された文字列です。

片方向のデータ結合は、例えば

label {
  text <= [a.data]
}

のようにします。

例としてentryとlabelがオブジェクトaのインスタンス変数@dataバインディングされたプログラムを紹介します。

require 'glimmer-dsl-libui'

include Glimmer

class A
  attr_accessor :data
  def initialize
    @data = ""
  end
end

window('バインディング', 800, 100) {
  margined true
  
  a = A.new
  vertical_box {
    entry {
      text <=> [a, :data]
    }
    label {
      text <= [a, :data]
    }
  }
}.show

同じデータがentryとlabelのテキストと結合されているので、エントリに文字列を打ち込むとラベルにもそれが反映されます。

データ結合

glimmer-dsl-libuiに付属の例

インストールしたglimmer-dsl-libuiのgemにはexampleフォルダに沢山の例が入っています。 インストール先は次のようにして調べられます。

$ gem Environment
RubyGems Environment:
  - RUBYGEMS VERSION: 3.3.7
  - RUBY VERSION: 3.1.2 (2022-04-12 patchlevel 20) [x86_64-linux]
  - INSTALLATION DIRECTORY: /(ユーザディレクトリ)/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0
... ... ...
... ... ...

INSTALLATION DIRECTORYと書いてあるのがgemのインストール先です。

この例を調べることにより、Glimmerの書き方が理解できます。 実行してみて面白いと思った例を以下にあげてみます。

  • area_based_custom_controls.rb
  • class_based_custom_controls.rb
  • control_gallery.rb
  • custom_draw_text.rb
  • dynamic_area.rb
  • editable_table.rb ⇒ ダブルクリックで編集できる
  • form_table.rb
  • histogram.rb
  • meta_example.rb
  • shape_coloring.rb
  • tetris.rb ⇒ 以前大流行したテトリス
  • timer.rb

実行してみて面白いだけでなく、プログラムを見ることによってGlimmerの書き方を習得することができます。

まとめ

Glimmer-dsl-libuiはShoes4と比べると起動が速く、精神的なストレスがありません。 まだ安定版がないので、使うかどうかを迷うかもしれませんが、開発版の段階でもある程度は実用になると思います。

Glimmerのベースになっているlibuiというライブラリ(こちらも開発段階)はGtk3ベースと書いてありました。 Gtk4になって2年くらいになるので、ぜひともGtk4に対応してもらいたいです。 Gtk3とGtk4ではいろいろな違いがあり、Gtk4はかなり進歩していますから。

なお、Glimmer-dsl-libuiは2022年10月15日の「福岡 mruby Kaigi」で特別賞を受賞しています。 今後の開発に期待したいと思います。