Shoes -- Rubyとグラフィック

Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。

そのような状況の中で、現時点でグラフィックやグラフィック・ユーザ・インターフェース(GUI)のRubyをめぐる状況を調べていました。 今回はShoesというライブラリを取り上げます。

Shoesとは?

2022/11/15の時点で、Shoesには安定版のバージョン3.3と開発版のバージョン4がありますが、安定/開発だけではない違いがあります。 バージョン3はCを使っていましたが、バージョン4はJRubyのgemとなっています。つまり、Javaベースです。 また、その開発は現時点では活発とはいえません。 Shoes3の最後のコミットが2020年1月9日、Shoes4が2019年4月5日です。

今回はShoes4を試してみました。 PC環境はUBUNTU22.10です。 解説記事というよりは、使用記です。

インストール

Javaのインストール

Java(open-jdk)がインストールされているかを確認します。

$ java --version
openjdk 11.0.17 2022-10-18
OpenJDK Runtime Environment (build 11.0.17+8-post-Ubuntu-1ubuntu2)
OpenJDK 64-Bit Server VM (build 11.0.17+8-post-Ubuntu-1ubuntu2, mixed mode, sharing)

このようにコマンドラインからjava --versionでバージョン表示がされれば、すでにインストールはできています。 なお、これはUBUNTUでのデフォルトのバージョンです(OpenJDK-11JDK)。 もっと新しいバージョンもあります(最も高いバージョンは2022/11/15の時点でOpenJDK-20-JDK)ので、それを使いたい場合はaptでインストールします。

先程のコマンドでバージョンが表示されなければJavaをaptでインストールしてください。

JRubyのインストール

私の場合はrbenvを使ってRubyをインストールしているので、JRubyにもrbenvを使います。 インストール可能なバージョンを確認します。

$ rbenv install -l
2.6.10
2.7.6
3.0.4
3.1.2
jruby-9.3.4.0
mruby-3.0.0
rbx-5.0
truffleruby-22.1.0
truffleruby+graalvm-22.1.0

Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all / -L' to show all local versions.

JRuby-9.3.4.0があるので、それをインストールします。

$ rbenv install jruby-9.3.4.0

この段階で私のUBUNTUには、「Ruby 3.1.2」「JRuby 9.3.4.0」の2つのRubyがインストールされました。 この使い分けには「rbenv local」コマンドを使います。 あるディレクトリでJRubyを使いたい場合は、そのディレクトリに移動して

$ rbenv local jruby-9.3.4.0

とします。 このディレクトリでrubyを起動するとJRubyが呼ばれます。 なお、このときディレクトリ内にRubyバージョンを書いた隠しファイル「.ruby-version」が置かれ、rbenvはそれを手がかりにRubyを起動するのです。

Shoes4のインストール

JRubyを指定したディレクトリ(上記の rbenv local jruby-9.3.4.0 したディレクトリ)でshoesのgemをインストールします。

$ gem install shoes --pre
Fetching shoes-4.0.0.rc1.gem
Fetching shoes-core-4.0.0.rc1.gem
Fetching shoes-package-4.0.0.rc1.gem
Fetching furoshiki-0.6.1.gem
Fetching shoes-swt-4.0.0.rc1.gem
Successfully installed shoes-core-4.0.0.rc1
Successfully installed furoshiki-0.6.1
Successfully installed shoes-package-4.0.0.rc1
Successfully installed shoes-swt-4.0.0.rc1
Building native extensions. This could take a while...
Successfully installed shoes-4.0.0.rc1
Parsing documentation for shoes-core-4.0.0.rc1
Installing ri documentation for shoes-core-4.0.0.rc1
Parsing documentation for furoshiki-0.6.1
Installing ri documentation for furoshiki-0.6.1
Parsing documentation for shoes-package-4.0.0.rc1
Installing ri documentation for shoes-package-4.0.0.rc1
Parsing documentation for shoes-swt-4.0.0.rc1
Installing ri documentation for shoes-swt-4.0.0.rc1
Parsing documentation for shoes-4.0.0.rc1
Installing ri documentation for shoes-4.0.0.rc1
Done installing documentation for shoes-core, furoshiki, shoes-package, shoes-swt, shoes after 5 seconds
5 gems installed

間違って、Ruby 3.1.2 が起動するディレクトリでgem install shoesとすると、Shoes3のgemがインストールされるので注意してください。 また、このgemだけではShoes3は動かないようです。

Hello world

手始めはいつも「Hello world」の表示です。 次のプログラムをShoes4で動かします。

Shoes.app title: "Hello" do
  stack do
    para "Hello world"
  end
end
  • クラスShoesの特異メソッドappを呼び出すとグラフィック画面に表示するウィンドウを作成する
  • 引数のtitleはアプリケーション名(上部の「アクティビティ」や日付のあるバーに表示される)とウィンドウのタイトルになる
  • ブロックの中にウィンドウのパーツを書く(パーツをShoesではエレメントという)
  • stackは上下にエレメントを並べるコンテナ(このプログラムでは無くても良い)
  • paraは段落(paragraph)のことで、文字列を表示する

このプログラムをhello.rbのファイル名で保存し(Jrubyが動くディレクトリに、以下Jrubyを前提とする)コマンドラインからshoesを起動すると次のような画面が現れる。

$ shoes hello.rb

Hello

Shoes.app内のself

Shoesの使い方を説明します。

ウィンドウを作成するにはShoes.appの特異メソッドのブロックにエレメントを作成するメソッドを書きます。 このとき、ブロックのselfはShoes::APPクラスのオブジェクトに変更されます。 なお、Rubyの原則ではメソッドのブロックのselfはメソッド外側のselfと同じです。 この特異メソッドでは原則と異なる扱いになるように設定されているということです。

ブロック内の関数形式のメソッドはselfをレシーバとするので、Shoes.appのブロック内の関数形式のメソッドはShoes::APPクラスのインスタンスメソッドになります。 hello.rbで使ったparaというメソッドもShoes::APPのインスタンスメソッドです。

hello.rbではstackメソッドも使いました。 stackメソッドのブロックではselfの変更はしません。 ほとんどのShoes4のメソッドはそのブロックでselfの変更をしませんが、マニュアルによるとwindowメソッドもselfの変更をするそうです。

エレメント

ウィンドウ内にエレメントを置くメソッドには次のようなものがあります。

  • para ⇒ 段落(テキストと考えて良い)
  • button ⇒ ボタン
  • edit_line ⇒ 一行入力の枠
  • oval ⇒ 円や楕円。その他にも図形を描画するメソッドlineやrectなどがある
  • list_box ⇒ リストボックス

他にも沢山エレメントがあるので、Shoesのマニュアルを参照してください。

これらのメソッドの返り値はそれぞれのオブジェクトを返します。 例えば、paraメソッドはShoes::Paraクラスのオブジェクトを返します。 このオブジェクトに対してメソッドを使うことができます。

@para = para "こんにちは" #=> 「こんにちは」をウィンドウ内に段落として表示
@para.text = "さようなら" #=> その段落の文字列を「さようなら」に変更する

どのようなメソッドがあるかを調べるにはAPIドキュメントを見れば良いのですが、なかなか探しにくいかもしれません。 Shoes::ParaクラスがAPIドキュメントでは書かれていませんが、これはShoes::TextBlockのサブクラスです。 このことは、ソースファイルを見るか、あるいはShoes::Para.ancestorsShoes::Para.superclassといったメソッドを実行して調べることで分かります。 Shoes::TextBlockのメソッドにはtexttext=があるので、これらはShoes::Paraでも使えることが分かります。

buttonメソッドには文字列の引数を与え、表示されたボタンのラベル(ボタンに書かれる文字列)を指定できます。 また、buttonメソッドが返すShoes::Buttonクラスのオブジェクトにはclickメソッドがあり、クリックされたときの動作を記述できます。

@button = button "ボタン"
@button.click do
  (ボタンがクリックされたときの動作を記述)
end

クリック時の動作はbuttonメソッドにブロックを付けてそこに記述することもできます。

スロット

スロットはエレメントを並べるためのコンテナで、フローとスタックがあります。

  • フロー(flow): エレメントを横に並べる
  • スタック(stack):エレメントを縦に並べる

エレメントはflowまたはstackメソッドのブロックに記述します。

電卓プログラム

簡単な電卓プログラムを書きました。 2つのファイルcalc.rblib_calc.rbから成ります。 calc.rbがShoesを使ってウィンドウを表示し、lib_calc.rbが文字列を構文解析して計算をします。

このプログラムを実行すると次のような画面が現れます。

$ shoes calc.rb

calc

また、計算を実行すると次のようになります。

calc

  • 入力枠に数式を書く
  • 四則以外に累乗(**)、三角関数、指数関数、対数関数が可能
  • 前回計算した値を文字vで参照できる
  • 変数を使うことができる。a=10+2のようにイコールで代入する
  • 「計算」ボタンをクリックすると計算を実行する
  • 「クリア」ボタンをクリックすると入力枠内の文字列を消去する
  • 「終了」ボタンをクリックするとウィンドウを閉じる
  • タブを押すことによって入力枠やボタンをフォーカスが移動するので、ボタンにフォーカスをあててエンターキーを押すことでクリックの代わりにすることができる

以下にcalc.rbのプログラムを示します。

require_relative 'lib_calc.rb'

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

Shoes.app title: "calc", width: 400, height: 80 do
  @calc = Calc.new
  flow do
    @edit_line = edit_line "", margin_left: 10
    @do_calc = button "計算", margin_left: 10
    @clear = button "クリア", margin_left: 3
    @close = button "終了", margin_left: 10
  end
  stack do
    @answer = para "", margin_left: 10
  end

  @do_calc.click do
    @answer.text = get_answer(@calc.run(@edit_line.text))
  end
  @clear.click {@edit_line.text = ""}
  @close.click {close}
end
  • Calcクラスはlib_calc.rbで定義されている
  • Calcのインスタンスメソッドrunは引数に文字列を与えるとその計算をして答え(Floatオブジェクト)を返す。 エラーが発生したときは答えの代わりにエラーメッセージを返す
  • get_answerメソッドは答えが整数のとき、Integerクラスに変えてから文字列にしている。 このことにより、例えば「12.0」でなく「12」という文字列にする
  • Shoes.appメソッドの中は2つのスロット(フローとスタック)を設定している
  • フローには入力枠、「計算」ボタン、「クリア」ボタン、「終了」ボタンを入れている。 それぞれ左マージンを10ピクセルまたは3ピクセル与え、エレメント間のスペースを作っている
  • スタックには答えを表示するための段落を設けている
  • @do_calcは「計算」ボタンのオブジェクト。 clickメソッドで、入力枠の文字列から@calc.runで計算し、get_answerで文字列化して@answerの段落エレメントの文字列に代入している
  • @clearは「クリア」ボタンのオブジェクトで、クリックされたときに@edit_lineオブジェクト(入力枠)の文字列を空文字列にする
  • @closeは「終了」ボタンのオブジェクトで、クリックされたときにcloseメソッドを呼び出す。 closeメソッドはウィンドウを閉じる。

Shoesのプログラムはこのように簡単です。 clickメソッドは、ボタンクリックのイベントに対するハンドラを定義しています。 この段階ではイベント処理をしているのではなく、イベント処理のハンドラのセットをしているだけです。

開発が活発でないのは残念ですが、電卓のような簡単なプログラムであれば開発には十分です。 ちょっと気になるのはJRubyの起動に時間がかかることです。

最後にlib_calc.rbのソースを示しますが、長いので説明は省略します。 なお、プログラムのコードはGitHubのBlog-about-Rubyレポジトリにあります。 ディレクトリは_example/shoes/です。


class Calc
  include Math

  def initialize
    @table = {}
    @value = 0.0
  end

  # calculate s
  # error => return error message
  # success => return the result as a string
  def run(s)
    a = parse(s)
    if a.instance_of? Float
      @value = a # keep the result of the calcukation.
      a = a.to_i if a.to_i == a
    end
    a.to_s
  end

  # error => return nil
  # success => return array like:
  # [[:id, "var"], [:=, nil], [:num, 12.34], [:+, nil], ... ... ...]
  def lex(s)
    result = []
    while true
      break if s == ""
      case s[0]
      when /[[:alpha:]]/
        m = /\A([[:alpha:]]+)(.*)\Z/m.match(s)
        name = m[1]; s = m[2]
        if name =~ /sin|cos|tan|asin|acos|atan|exp|log|sqrt|PI|E|v/
          result << [$&.to_sym, nil]
        else
          result << [:id, name]
        end
      when /[[:digit:]]/
        m = /\A([[:digit:]]+(\.[[:digit:]]*)?)(.*)\Z/m.match(s)
        result << [:num, m[1].to_f]
        s = m[3]
      when /[+\-*\/()=]/
        if s =~ /^\*\*/
          result << [s[0,2].to_sym,nil]
          s = s[2..-1]
        else
          result << [s[0].to_sym, nil]
          s = s[1..-1]
        end
      when /\s/
        s = s[1..-1] 
      else
        @error_message = "Unexpected character."
        result = nil
        s = "" # remove the rest of the string.
      end
    end
    result
  end

  # BNF
  # program: statement;
  # statement: ID '=' expression
  #   | expression
  #   ;
  # expression: expression '+' factor1
  #   | expression '-' factor1
  #   | factor0
  #   ;
  # factor0: factor1
  #   | '-' factor1
  #   ;
  # factor1: factor1 '*' power
  #   | factor1 '/' power
  #   | power
  #   ;
  # power: primary ** power
  #   | primary
  #   ;
  # primary: NUM | 'PI' | 'E' | '(' expression ')' | function '(' expression ')' | 'v';
  # function: 'sin' | 'cos' | 'tan' | 'asin' | 'acos' | 'atan' | 'exp' | 'log' ;

  # parser
  # error => return error message
  # success => return the result of the calculation (Float)
  def parse(s)
    tokens = lex(s)
    return @error_message unless tokens # lex error
    tokens.reverse!
    a = statement(tokens) # error
    return "syntax error." unless tokens == []
    a ? a : @error_message
  end

  private

  # error => return false and the error message is assigned to @error_message
  # success => return the result of the calculation (Float)
  def statement(tokens)
    token = tokens.pop.to_a
    case token[0]
    when :id
      a = token[1]
      b = tokens.pop.to_a
      if  b[0] == :'='
        return false unless c = expression(tokens)
        install(a, c)
        c
      else
        tokens.push(b) if b[0]
        tokens.push(token)
        expression(tokens)
      end
    when nil # token is now empty.
      syntax_error
      false
    else
      tokens.push(token)
      expression(tokens)
    end
  end

  def expression(tokens)
    return false unless (a = factor0(tokens))
    while true
      token = tokens.pop.to_a
      case token[0]
      when :'+'
        b = factor1(tokens)
        unless b
          break false
        end
        a = a+b
      when :'-'
        b = factor1(tokens)
        unless b
          break false
        end
        a = a-b
      when nil
        return a
      else
        tokens.push(token)
        break a
      end
    end
  end

  def factor0(tokens)
    token = tokens.pop.to_a
    case token[0]
    when :'-'
      b = factor1(tokens)
      b ? -b : false
    when nil
      syntax_error
      false
    else
      tokens.push(token)
      factor1(tokens)
    end
  end

  def factor1(tokens)
    return false unless (a = power(tokens))
    while true
      token = tokens.pop.to_a
      case token[0]
      when :'*'
        b = power(tokens)
        unless b
          break false
        end
        a = a*b
      when :'/'
        b = power(tokens)
        unless b
          break false
        end
        if b == 0
          @error_message = "Division by 0.\n"
          break false
        end
        a = a/b
      when nil
        break a
      else
        tokens.push(token)
        break a
      end
    end
  end

  def power(tokens)
    return false unless (a = primary(tokens))
    token = tokens.pop.to_a
    case token[0]
    when :'**'
      b = power(tokens)
      if b
        a**b
      else
        false
      end
    when nil
      a
    else
      tokens.push(token)
      a
    end
  end

  def primary(tokens)
    token = tokens.pop.to_a
    case token[0]
    when :id
      a = lookup(token[1])
      @error_message = "Variable #{token[1]} not defined.\n" unless a
      a ? a : false
    when :num
      token[1]
    when :PI
      PI
    when :E
      E
    when :'('
      b = expression(tokens)
      return false unless b
      unless tokens.pop.to_a[0] == :')'
        syntax_error
        return false
      end
      b
    when :sin, :cos, :tan, :asin, :acos, :atan, :exp, :log, :sqrt
      f = token[0]
      unless tokens.pop.to_a[0] == :'('
        syntax_error
        return false
      end
      b = expression(tokens)
      return false unless b
      unless tokens.pop.to_a[0] == :')'
        syntax_error
        return false
      end
      method(f).call(b)
    when :v
      @value
    when nil
      syntax_error
      false
    else
      syntax_error
      false
    end
  end

  def install(name, value)
    @table[name] = value
  end
  def lookup(name)
    @table[name]
  end

  def syntax_error
    @error_message = "syntax error."
  end
end

2023

単項マイナスと構文解析

1 minute read

単項マイナスとは 単項マイナスと括弧 括弧なし単項マイナスを許容する場合のBNF calcの場合

Raccライブラリと構文解析

3 minute read

パーサ・ジェネレータとは 少し複雑な文法 四則(加減乗除)計算のBNF Racc で実装 クラス定義、BNFの記述部分 ヘッダー、インナー、フッター コンパイルと実行 演算子の優先順位と結合における左右の優先順位 まとめ

StrScanライブラリと字句解析

less than 1 minute read

StrScanライブラリのドキュメント 字句解析とは StrScanライブラリ StrScanライブラリを使った字句解析 実例

Gem

1 minute read

lbtというgemを作って公開してみた lbtはどんなgemか ファイルの配置 lbt.gemspec Rakefile gemのビルド RubyGems.orgへのアップロード 補足・・rake/gempackagetaskサブライブラリについて

Encoding

1 minute read

文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。

Thread

less than 1 minute read

Fiberを書いたときから、次はスレッドを書こうと思っていましたが、時間がかかってしまいました。 その理由は、期待したとおりのスレッドの効果がなかったためです。 今回はそのことを書きますが、これはRubyのスレッドの抱えている問題なのか、自分のやり方が悪いのかははっきりしていません。

Fiber

1 minute read

Fiberは「ノンプリエンプティブな軽量スレッド」とRubyのマニュアルに記載されています。

RDoc

less than 1 minute read

今回はRubyプログラムから自動的にドキュメントを作成するRDocについて書きたいと思います。 私はこのことについて、エキスパートではありません。 この記事も、初心者の体験談だと考えてください。

Back to Top ↑

2022

Ruby/GTK4

5 minute read

Ruby/Gtkの記事を先日書いたときに、「これはかなり使える」という手応えを感じたので、WordBook(Railsで作った単語帳プログラム)のGTK 4版を作りました。 プログラムは「徒然なるままにRuby」のGitHubレポジトリに置いてあります。 レポジトリをダウンロードし、ディレクトリ_example/...

Shoes – Rubyとグラフィック

5 minute read

Rubyはグラフィックについて弱い印象があります。 しかし、グラフィックはデバイスに関することなので、言語そのものには直接の関係はないはずで、あるとすればライブラリです。 今後グラフィック関係のgemが開発されることに期待しましょう。

Rails7 テスト

5 minute read

前回作ったWordbook(リソースフル)のテストを書いてみます。 RailsのテストはminitestをRails用に拡張したものです。

Rails7 モデルとデータベース

5 minute read

今回はRailsにおけるデータの作成と保存、そして変更について説明します。 そのベースになるモデルとデータベースの話から始め、appendとchangeの動作について詳しく説明します。

Rails7とBootstrap

3 minute read

一般に、HTMLは文書の構造を表し、CSSはその体裁(見栄え)を表します。 Railsは最終的にCSSを含むHTML文書を出力するので、この2つについての理解が必須です。 この記事ではとくにCSSの人気ライブラリであるBootstrapを紹介します。 BootstrapはJavascriptも含んでいます。

Rails7のインストール

2 minute read

Rubyの最も人気のあるアプリケーションであるRuby on Railsを取り上げようと思い、書き始めました。 予想してはいましたが、相当な分量になってしまいました。 そのため、何回かに分けて記事にすることにします。 また、対象となる読者のレベルをどうしようかと考えましたが、「徒然Ruby」が基礎的な内容から始ま...

GemとBundler

1 minute read

Rubyのライブラリ管理システムのRubygemsとコマンドgemおよびbundlerについて説明します。

minitest(3)モックの詳細

1 minute read

minitestについて連続して2回書いてきました。 「minitestはドキュメントが少ない」という人がいますが、私も同感です。 例えば、モックとスタブの説明も少ないです。 そこで、今回はmock.rbのソースコードを参考に、モックの私的ドキュメントを書いてみました。 あくまで私個人の考えであり、minites...

minitest(1)テストとは

2 minute read

アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。

public、private、protected

2 minute read

今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。

アプリ制作、インストール、テスト

1 minute read

2023/10/29 追記:この記事は新しく書き直しました。 古い記事で使っていたGitHubのCalcが大幅にアップデートされたためです。 そこで、この記事に合うようなプログラムsimple_calcを新たに作りました。 このプログラムは本レポジトリの_example/simple_calcにあります。

case文

2 minute read

if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。

Lambda

2 minute read

Procオブジェクトを生成するメソッドlambdaについて説明します。

Proc オブジェクト

2 minute read

今回はブロックを一般化したオブジェクトProcを説明します。

モジュール

1 minute read

モジュールには名前空間とミックスイン(Mix-in)の2つの機能があります。 ここではミックスインについて説明します。

Kernelモジュール

less than 1 minute read

Kernelモジュールのメソッドはどこでも使うことができます。 そのメソッドの中には便利で有用なものが多いです。

便利なメソッド

1 minute read

ここでは私が便利だと思ったメソッドを紹介します。

文字列と正規表現

3 minute read

文字列は最も使うオブジェクトのひとつです。 特にウェブ・アプリケーションでは、コンテンツだけでなくHTMLのタグやCSSを含めすべてが文字列です。 Rubyは文字列オブジェクトのメソッドが充実しており、またパターンマッチのための正規表現も充実しています。

配列

2 minute read

配列は、どのプログラミング言語にもあると思います。 複数の要素を一括して扱うことができるのが配列です。 Rubyの配列はメソッドが充実しているので、プログラムを効率的、機能的に書くのに役立ちます。

トップレベルのメソッド

1 minute read

今回はメソッド定義です。 メソッド定義はRubyの核心ですが、今回はトップレベルに限って説明します。 この限定によって、内容はかなり易しくなっています。

ブロックとイテレータ

less than 1 minute read

ブロックはRubyの特長です。 ブロックのおかげで記述が非常にすっきりと分かりやすくなります。 今回はブロックをイテレータの本体として使う方法を説明します。

整数

less than 1 minute read

ここではRubyの最も基本的なオブジェクトである整数について説明します。

Hello world

less than 1 minute read

「徒然なるままに」をネットで調べてみると、「することもなく、手持無沙汰なのにまかせてという意味」とありました。 まさに、自分の現状を言い当てた言葉。 しかも、ブログに書くネタもなかなか思いつかない日々。

Back to Top ↑