GTK+でお絵かきしてみた(Ruby)

後記:2018/9/22) Gem の使い方のリファレンスを作りました。
Gem 'oekaki' リファレンス
 



 
Ruby だけだと遊びで線を引く程度のことも簡単でないので、RubyGTK+ を使ってシンプルにお絵かきするだけのモジュールを書いてみました。できるのは、点を打つ、線を引く、長方形を描く、円(楕円、弧)を描く、多角形を描く、文字の表示、画像の取得と表示、画像ファイルの読み込みと書き出し、キー入力、マウスクリック、そんなことくらいですか。

Gem 'gtk2' が必要です。
後注:このモジュールを RubyGem 'oekaki' として公開しました。参照MyGtk.app の代わりに Oekaki.app とするだけで、あとはすべて同じです。)


円を書いてみます。

require_relative 'mygtk'

MyGtk.app width: 300, height: 300 do            #ウィンドウの大きさは 300×300
  draw do
    white = color(65535, 65535, 65535)
    red   = color(65535, 0, 0)
    
    rectangle(true, 0, 0, 300, 300, white)        #画面全体を白く塗りつぶす
    arc(true, 0, 0, 300, 300, 0, 64 * 360, red)   #赤い円を描く
  end
end

モジュール 'mygtk' を require_relative しています。下のモジュール 'mygtk.rb' をカレントディレクトリに保存して下さい。以下、require_relative は同じなので書きません。
描画する部分を drawメソッドのブロックに入れます。ウィンドウの大きさの変更などで再描画される際にも呼び出されます。
rectangle は長方形、arc は円を描きます。第一引数の true は中を埋めて描くという意味です。(0, 0, 300, 300) は図形の左上隅の座標と横縦の大きさです。arc の (0, 64 * 360) は円弧の描画を開始する角度と終了する角度で、1°の 64分の1 が単位です。


線が移動していくアニメーションです。

MyGtk.app width:300, height: 300, title: :lines do
  r = 0
  id = Gtk.timeout_add(40) do
    color(0, 65535, 0)
    line(r, 0, 299 - r, 299)    #線を描く
    Gtk.timeout_remove(id) if r >= 300
    r += 1
  end

  draw do
    color(65535, 65535, 65535)  
    rectangle(true, 0, 0, 300, 300)
  end
end

Gtk.timeout_add(interval) を直接使ってアニメーションにしています(interval は 1/1000秒単位です)。


画像ファイルから画像を読み込んで表示し、その上に点の雪を降らせるアニメーションです。エンターキーの入力で終了します(参照)。

MyGtk.app width: 300, height: 300 do
  timer(5) do
    color(65535, 65535, 65535)
    point(rand(300), rand(300))  
  end
  
  key_in do |w, e|                           #キー入力
    Gtk.main_quit if e.keyval == Gdk::Keyval::GDK_Return
  end

  draw do
    color(65535, 65535, 65535)
    rectangle(true, 0, 0, 300, 300)
  
    img = load_pic("import_codes/cairo_sample2.png")   #画像ファイルの読み込み
    show_pic(img, 0, 0)                                #読み込んだ画像の表示
  end
end

timer(interval) メソッドは interval の間隔でブロック内を繰り返し呼び出します(内部で Gtk.timeout_add を使っています)。ここでは点の雪を降らせています。
 

多角形と文字列を表示します。png ファイルとして出力もしています。

MyGtk.app width:300, height: 300 do
  draw do
    white = color(65535, 65535, 65535)
    red   = color(65535, 0, 0)
    blue  = color(0, 0, 65535)
    
    rectangle(true, 0, 0, 300, 300, white)
    
    ar = []
    50.times {ar << [rand(300), rand(300)]}    #線の数は50本(配列に座標を入れる)
    polygon(true, ar, blue)                    #多角形を描く
    
    text("Polygon", 180, 260, 20 * 1000, red)  #文字列を描く
    
    img = get_pic(0, 0, 300, 300)              #画像の取り込み
    save_pic(img, "sample.png")                #画像ファイルに保存
  end
end

print の 20 * 1000 はフォントの大きさです。指定の意味は GTK+ の仕様そのままです。


最後にモジュール本体のコードを置いておきます。
mygtk.rb

require 'gtk2'
require 'matrix'
include Math

module MyGtk
  W = Gtk::Window.new
  class Tool
    def initialize
      @window = W
      @drawable = W.window
      @gc = Gdk::GC.new(@drawable)
      @colormap = Gdk::Colormap.system
      @color = Gdk::Color.new(0, 0, 0)
      @fontdesc = Pango::FontDescription.new
      @width, @height = 0, 0
    end
    attr_reader :window
    attr_accessor :width, :height
    
    def color(r, g, b)
      @color = Gdk::Color.new(r, g, b)
      @colormap.alloc_color(@color, false, true)
      @color
    end
    
    def rectangle(fill, x, y, width, height, color = nil)
      set_color(color)
      @drawable.draw_rectangle(@gc, fill, x, y, width, height)
    end
    
    def arc(fill, x, y, width, height, d1, d2, color = nil)
      set_color(color)
      @drawable.draw_arc(@gc, fill, x, y, width, height, d1, d2)
    end
    
    def circle(fill, x, y, r, color = nil)
      arc(fill, x - r, y - r, 2 * r, 2 * r, 0, 64 * 360, color)
    end
    
    def point(x, y, color = nil)
      set_color(color)
      @drawable.draw_point(@gc, x, y)
    end
    
    def line(x1, y1, x2, y2, color = nil)
      set_color(color)
      @drawable.draw_lines(@gc, [[x1, y1], [x2, y2]])
    end
    
    def lines(array, color = nil)
      set_color(color)
      @drawable.draw_lines(@gc, array)
    end
    
    def polygon(fill, array, color = nil)
      set_color(color)
      @drawable.draw_polygon(@gc, fill, array)
    end
    
    def text(str, x, y, size, color = nil)
      set_color(color)
      @fontdesc.set_size(size)
      layout = Pango::Layout.new(W.pango_context)
      layout.font_description = @fontdesc
      layout.text = str
      @drawable.draw_layout(@gc, x, y, layout)
    end
    
    def set_color(color)
      @color = color if color
      @gc.set_foreground(@color)
    end
    private :set_color
    
    def load_pic(filename)
      GdkPixbuf::Pixbuf.new(file: filename)
    end
    
    def save_pic(img, filename, type = "png")
      img.save(filename, type)
    end
    
    def show_pic(img, x, y)
      @drawable.draw_pixbuf(@gc, img, 0, 0, x, y, img.width, img.height,
               Gdk::RGB::DITHER_NONE, 0, 0)
    end
    
    def get_pic(x, y, width, height)
      GdkPixbuf::Pixbuf.from_drawable(nil, @drawable, x, y, width, height)
    end
    
    def timer_stop(id)
      Gtk.timeout_remove(id)
    end
    
    def star(fill, x1, y1, x2, y2, color = nil)
      set_color(color)
      Star.new(fill, x1, y1, x2, y2, @color).draw
    end

    def clear(color = nil)
      color ||= Gdk::Color.new(0, 0, 0)
      set_color(color)
      rectangle(true, 0, 0, @width, @height)
    end

    def get_window_size
      W.size
    end
  end
  
  class Event < Tool
    def initialize
      super
    end
    
    def draw(&bk)
      W.signal_connect("expose_event", &bk)
    end
    
    def timer(interval, &bk)
      Gtk.timeout_add(interval, &bk)
    end
    
    def key_in(&bk)
      W.signal_connect("key_press_event", &bk)
    end
    
    def mouse_button(&bk)
      W.add_events(Gdk::Event::BUTTON_PRESS_MASK)
      W.signal_connect("button_press_event", &bk)
    end
    
    def make_window(&bk)
      w = Gtk::Window.new
      w.instance_eval(&bk)
      w.show_all
      w
    end

    def window_changed(&bk)
      W.signal_connect("configure_event") do
        @width, @height = get_window_size
        yield
      end
    end
  end
  
  class Star < Tool
    module Add
      refine Vector do
        def to_w(o)
          v = self
          [o[0] + v[0], o[1] - v[1]]
        end
      end
    end
    using Add
    
    def initialize(fill, x1, y1, x2, y2, color)
      @fill = fill
      @o = []; @a = []; @b = []
      @o[0], @o[1] = x1, y1
      @a[0] = Vector[x2 - x1, y1 - y2]
      θ = PI / 5
      rt1 = Matrix[[cos(θ), -sin(θ)], [sin(θ), cos(θ)]]
      rt2 = rt1 * rt1
      1.upto(4) {|i| @a[i] = rt2 * @a[i - 1]}
      t = cos(2 * θ) / cos(θ)
      @b[0] = rt1 * @a[0] * t
      1.upto(4) {|i| @b[i] = rt2 * @b[i - 1]}
      super()
      @color = color
    end
    
    def draw_triangle(n)
      ar = [@a[n].to_w(@o), @b[n].to_w(@o), @b[(n - 1) % 5].to_w(@o)]
      polygon(@fill, ar)
    end
    private :draw_triangle
    
    def draw
      if @fill
        5.times {|i| draw_triangle(i)}
        ar = []
        5.times {|i| ar << @b[i].to_w(@o)}
        polygon(@fill, ar)
      else
        ar = []
        5.times {|i| ar << @a[i].to_w(@o); ar << @b[i].to_w(@o)}
        polygon(@fill, ar)
      end
    end
  end
  
  class Turtle < Tool
    def initialize
      super
      @pen = Tool.new
      @pen_po = Vector[0, 0]
      @dir = Vector[1, 0]
      @color_t = [65535, 65535, 65535]
      @width, @height = @pen.get_window_size
    end
    attr_accessor :pen_po, :dir
    
    def left(deg)
      θ = PI * deg / 180
      @dir = Matrix[[cos(θ), -sin(θ)], [sin(θ), cos(θ)]] * @dir
    end
    
    def right(deg)
      left(-deg)
    end
    
    def forward(length, draw = true)
      next_po = @pen_po + @dir * length
      if draw
        @pen.color(*@color_t)
        @pen.line(@width / 2 + next_po[0], @height / 2 - next_po[1],
           @width / 2 + @pen_po[0], @height / 2 - @pen_po[1])
      end
      @pen_po = next_po
    end
    
    def back(length)
      forward(-length, false)
    end
    
    def color(r, g, b)
      @color_t = [r, g, b]
      @pen.color(*@color_t)
    end
    
    def circle(radius, fill = false)
      @pen.color(*@color_t)
      @pen.circle(fill, @width / 2 + @pen_po[0], @height / 2 - @pen_po[1], radius)
    end
    
    def move(x, y)
      @pen_po = Vector[x, y]
    end
  end
  
  def self.app(width: 300, height: 300, title: "gtk", resizable: false, &bk)
    W.title = title
    W.set_size_request(width, height)
    W.set_resizable(resizable)
    W.set_app_paintable(true)
    W.realize
    
    e = Event.new
    e.width, e.height = width, height
    e.clear
    
    e.instance_eval(&bk)

    W.signal_connect("destroy") {Gtk.main_quit}
    W.show_all
    Gtk.main
  end
end

class Gtk::Window
  def button(&bk)
    b = Gtk::Button.new
    b.instance_eval(&bk)
    b
  end
end

注意すべきは、draw や timer などの取るブロックは必ず true を返すようにして下さい。わかりにくいバグを引き起こすことがあります。
 

※参考
Ruby-GNOME2 Project Website - Ruby-GNOME2 Project Website