GTK+でお絵かきしてみた(Ruby)
(後記:2018/9/22) Gem の使い方のリファレンスを作りました。
Gem 'oekaki' リファレンス
Ruby だけだと遊びで線を引く程度のことも簡単でないので、Ruby で GTK+ を使ってシンプルにお絵かきするだけのモジュールを書いてみました。できるのは、点を打つ、線を引く、長方形を描く、円(楕円、弧)を描く、多角形を描く、文字の表示、画像の取得と表示、画像ファイルの読み込みと書き出し、キー入力、マウスクリック、そんなことくらいですか。
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