Yardを閲覧するだけのGUIアプリをRubyで作る

# はじめに

ここでは1-2年前にGUIの練習のために作ったyardを見るだけのソフトを振り返って、RubyでどうやってGUIソフトを作るんだったかというのを思い出して、自分用テンプレートにまとめたいと思います。この記事は `ruby/gtk3` について中級的な内容をまとめています。初心者の方はこちらを見てください。

[【Ruby】10分でGladeを使って作るRuby/GTK3 GUIアプリ](https://qiita.com/kojix2/items/d9da8e08757dcc6b1a7d)

このソフトは、ローカルにインストールされたgemを表示するだけのやつなので、かならずしも実用的ではないよ。

Github : https://github.com/kojix2/yardview

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/05bf3c1e-9ad8-6d10-5494-46c27315b81e.png)

ちょっと透けるようになっています。コードではなくGladeの設定でいじっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/28e87f70-1b67-ba45-2707-e2e697122536.png)

一応Gemで試せます。（環境によっては動かないかも）

```
gem install yardview_gtk3
```

起動

```
yardview
```


## 元ネタ

Lazarus Lazaridisさんの記事。Todoアプリをつくています。

* https://iridakos.com/programming/2018/01/25/creating-a-gtk-todo-application-with-ruby
* https://github.com/iridakos/gtk-todo-tutorial
 
この元ネタの英語の内容がすらすら読める人はそれで十分なのですが、この記事にもシグナルハンドリングで少し楽をする方法など、元ネタ以上の内容が少し含まれています。

## Gemにする

実行ファイルを追加するオプション `--exe` `-b` 

```
bundle gem yardview --exe
```

いつもどおり`gemspec`ファイルを編集

```ruby:gemspec
  spec.add_dependency 'yard'
  spec.add_dependency 'gtk3'
  spec.add_dependency 'webkit2-gtk'
  spec.add_development_dependency 'bundler'
  spec.add_development_dependency 'rake'
```

ユーザーはRubyに詳しい　→　Gemfile.lock は含めない
ユーザーはRuby関係ない　→　Gemfile.lock 残す

## ディレクトリ構成


```shell-session
.
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── exe
│   └── yardview
├── lib
│   ├── yardview
│   │   ├── application.rb
│   │   ├── application_window.rb
│   │   └── version.rb
│   └── yardview.rb
├── resources
│   ├── LICENSE
│   ├── gresources.xml
│   ├── ruby.png
│   ├── yardview.ui
│   └── yardview_screenshot.png
└── yardview.gemspec
```

面倒なので、`exe/yardview` に直接いろいろ書いています。

ポイントは、途中で `glib-compile-resources` を実行しているところです。GResourceというGnomeのリソースフレームワークを利用します。このコマンドにパスが通ってない場合は実行できないと思うので、何らかの対策が必要かも知れません。Windowsだとどうなんだろう…。あと、毎回リソースをコンパイルして、終了時に捨てる方式が本当にいいのかという問題はある気がします。

```ruby:yardview
#!/usr/bin/env ruby

require 'optparse'
require 'yardview'
require 'fileutils'

port_num ||= 8808

opt = OptionParser.new
opt.program_name = 'Yardview'
opt.version = Yardview::VERSION
opt.on('-p', '--port VAL', Integer) { |v| port_num = v }
opt.parse!(ARGV)

resource_xml = File.expand_path('../resources/gresources.xml', __dir__)
resource_bin = File.expand_path('../gresource.bin', __dir__)

system('glib-compile-resources',
       '--target', resource_bin,
       '--sourcedir', File.dirname(resource_xml),
       resource_xml)

at_exit do
  FileUtils.rm_f(resource_bin)
end

app = Yardview.application

resource = Gio::Resource.load(resource_bin)
Gio::Resources.register(resource)

app.signal_connect :activate do |application|
  window = Yardview::ApplicationWindow.new application, port: port_num
  window.present
end

app.run
```

今回はウェブページを表示するので `webkit2-gtk` を追加します。

```ruby:lib/yardview.rb
require 'yardview/version'
require 'yardview/application'
require 'yardview/application_window'
require 'webkit2-gtk'

module Yardview
end
```


Glade側でシグナルのハンドラーの名前を指定しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/e8f69211-4808-d2b8-6dbd-fc71ae0f53b9.png)


使用するファイルを書いておく。

```xml:gresource.xml
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/com/github/kojix2/yardview">
    <file preprocess="xml-stripblanks">yardview.ui</file>
    <file>ruby.png</file>
  </gresource>
</gresources>
```


ここがミソなのですが、クラス変数にApplicationのインスタンスを詰め込んで参照できるようにしておきます。

```ruby:lib/yardview/application.rb
require 'gtk3'

module Yardview
  class << self
    def application
      @@application ||= Gtk::Application.new('com.github.kojix2.yardview', :flags_none)
    end
  end
end
```

`type_register`, `set_template`, `bind_template_child` `set_connect_func` あたりが重要なおまじないです。
`type_register` はGlibやGObjectの世界と統合につかわれるそうです。（正直よくわかっていません）
`set_template` で`ui` ファイルを指定します。ファイルパスではなく、GResourceから指定していることに注意してください。
`set_template_child` は、widgetにアクセスできるメソッドを提供します。下の方で変数のように登場する `box` は実はwidgetにアクセスするためのメソッド呼び出しです。
`set_connect_func` は自動でシグナルをRubyのメソッドにつないでいます。（ここは元ネタではやっていない部分です）

```ruby:lib/yardview/application_window.rb
require 'gtk3'

module Yardview
  class ApplicationWindow < Gtk::ApplicationWindow
    type_register

    def self.init
      set_template resource: '/com/github/kojix2/yardview/yardview.ui'
      bind_template_child 'box'
      set_connect_func do |handler_name|
        lambda do
          Yardview.application.active_window.__send__(handler_name)
        end
      end
    end

    attr_accessor :port

    def initialize(application, port: port_num)
      super application: application
      set_title 'YardView'
      set_icon GdkPixbuf::Pixbuf.new resource: '/com/github/kojix2/yardview/ruby.png'

      @port = port
      start_yard_server
      create_gui
    end

    def create_gui
      signal_connect('destroy') do
        Process.kill(:INT, @yard)
        @yard = nil
      end
      at_exit { Process.kill(:INT, @yard) unless @yard.nil? }
      @view = WebKit2Gtk::WebView.new
      @view.load_uri("http://localhost:#{port}")
      box.add @view, expand: true, fill: true
      @view.show
    end

    def port_open?(port)
      !system("lsof -i:#{port}", out: '/dev/null')
    end

    def on_home_clicked
      @view.load_uri("http://localhost:#{port}")
    end

    def on_back_clicked
      @view.go_back
    end

    def on_top_clicked
      @view.run_javascript('window.scrollTo(0,0);')
    end

    def on_refresh_clicked
      @view.reload
    end

    def start_yard_server
      if port_open? port
        @yard = spawn("yard server -g -p #{port} --reload")
        sleep 1
      else
        raise "port #{port} is in use!"
      end
    end
  end
end
```

RubyでGUIアプリを作っていると問題になりやすいのは非同期っぽい処理ですが、コマンドの実行だけでなんとかなってしまうものは`spawn` がオススメです。比較的プラットフォームを問わずに動作してくれます。

以上、書いている本人もよくわかっていないところが多い記事ですが、だいたいこんな感じ書けば動きます。

これは積極的にGnomeの機能を使っていこうという感じのテンプレートです。いやいやGResourceとかいらないし、GObjectとRubyのオブジェクトを統合する必要もないし、シグナルハンドリングも毎回ベタ書きすればいいじゃん、ということならもっと簡単にできるとは思います。

この記事は以上です。