RubyでWindowsのGUIアプリを作る

この記事は、[Wano Group Advent Calendar 2021](https://qiita.com/advent-calendar/2021/wano-group) の記事です。

## 背景

何年かに一回WindowsのGUIアプリを作ることがありまして、その最初のバージョンを[exerb](http://exerb.osdn.jp/)を使ってRubyで作っていました。
長年それを使いまわしていたのですが、いい加減古すぎるので、新しいやり方で作ろうと探したら、[libui](https://qiita.com/kojix2/items/1bf6b999973ea3bc17e6)が見つかりましたので、それを使ってみた記録の記事です。

Rubyのコードをexeにするのには、[ocra](https://github.com/larsch/ocra)を使いました。

ちなみに筆者はRubyはほとんど書いたことありませんので、コードに変なところがあるかもしれませんが、ご容赦を。

## libui

libui はポータブルなUIライブラリということで、Goにもバインディングがあります。

じゃぁ、Goで作ればいいやんという話もありますが、以前のコードを使いまわしたいところもあったので、Rubyのバインディングを使いました。ただ、ドキュメントは整備されていないので、結局[Goのドキュメント](https://pkg.go.dev/github.com/andlabs/ui)を見るということにはなるのですが。

これの良いところは、普段どおり、Linuxで開発して、exeを作るときだけ、Windowsでやれば良いというところですね(ちょっとだけはまりどころがありましたが)。

## 作り方

Rubyのlibuiのドキュメントにもありますが、オブジェクト指向ではないので、基本的には、LibUIから生えているメソッドで各コンポーネントを作っていく感じで、それに対する操作も、LibUIのメソッドで行うと言った感じです。Goのドキュメントでは、型からメソッドが生えているので、若干読み替えが必要になります。

* Goでは、`type Entry` に `SetText`がある
    * `e := ui.NewEntry()`
    * `e.SetText("text")`
* Rubyでは、`UI.entry_set_text(entry, 'text')` となる
    * `e = UI.new_entry`
    * `UI.entry_set_text(e, "text")`

といった感じです。

GUIアプリ作るの久しぶりすぎて、何も覚えてなくいんですが、コンポーネントの大きさは勝手に調整される感じのようです。widthやheightと言った指定がないです(Gridを使えば自由に配置できるのかもしれません)。
Boxを入れ子にしていって、なんとなく配置する感じなのかなと思いました。

### コード

試しに、テキストを正規表現で置換するものを作ってみます。

```ruby
# coding: utf-8

require 'libui'

UI = LibUI

def main()
  
  UI.init

  # ウィンドウを作成
  main_window = UI.new_window('Sipmle Text Replace', 1000, 500, 0)
  # ×ボタンでウィンドウを消せるようにする
  UI.window_on_closing(main_window) do
    puts 'Close window'
    UI.control_destroy(main_window)
    UI.quit
    0
  end

  # 全体のbox
  box  = UI.new_vertical_box
  # 入力/結果用のbox
  box_inout = UI.new_vertical_box
  # 正規表現とボタン入れる用のbox
  box_regexp_button = UI.new_horizontal_box

  # 全体のboxに追加
  UI.box_append(box, box_regexp_button, 0)
  UI.box_append(box, box_inout, 1)

  # 全体のboxをウィンドウに追加
  UI.window_set_child(main_window, box)

  # 入力欄の作成
  entry_label = UI.new_label("テキスト")
  input_entry = UI.new_multiline_entry

  # 結果出力の領域
  result_label = UI.new_label("結果")
  result_entry  = UI.new_multiline_entry
  ## 結果なので入力できないようにする
  UI.multiline_entry_set_read_only(result_entry, 1)

  # 正規表現と置換文字列
  regexp_label = UI.new_label("正規表現")
  regexp_entry = UI.new_entry 
  replace_label = UI.new_label("置換文字列")
  replace_entry = UI.new_entry

  # 実行ボタン
  button = UI.new_button('実行')

  # クリアボタン
  clear_button = UI.new_button('クリア')

  # ボックスに追加
  UI.box_append(box_inout, entry_label, 0)
  UI.box_append(box_inout, input_entry, 1)
  UI.box_append(box_inout, result_label, 0)
  UI.box_append(box_inout, result_entry, 1)
  UI.box_append(box_regexp_button, regexp_label, 0)
  UI.box_append(box_regexp_button, regexp_entry, 1)
  UI.box_append(box_regexp_button, replace_label, 0)
  UI.box_append(box_regexp_button, replace_entry, 1)
  UI.box_append(box_regexp_button, button, 0)
  UI.box_append(box_regexp_button, clear_button, 0)

  # 実行ボタン押された
  UI.button_on_clicked(button) do
    text    = UI.multiline_entry_text(input_entry).to_s
    regexp  = UI.entry_text(regexp_entry).to_s
    replace = UI.entry_text(replace_entry).to_s
    if (text.empty? || regexp.empty? || replace.empty?)
      UI.msg_box(main_window, 'Error', "テキスト/正規表現/置換文字列のいずれかが入力されていません")
    else
      result  = replace(main_window, regexp, replace, text)
      UI.multiline_entry_set_text(result_entry, result)
    end
    0
  end

  # クリアボタン押された
  UI.button_on_clicked(clear_button) do
    UI.multiline_entry_set_text(input_entry, "")
    UI.entry_set_text(regexp_entry, "")
    UI.entry_set_text(replace_entry, "")
  end

  # ウインドウを表示
  UI.control_show(main_window)
  UI.main
  UI.quit
end

def replace(main_window, regexp, replace, text)
  result = ''
  begin
   result = text.gsub(/#{regexp}/, replace)
  rescue => e
   UI.msg_box(main_window, 'Error', e)
  end
  return result
end

main()
```

### 見た目

このコードを、`ruby sample.rb` のように実行すると、下記のように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11040/6867e78c-658a-0567-f871-c130762e56d1.png)


## exeファイル化

ocra を使って、exe ファイルにします。WindowsにRubyを入れるのは、[RubyInstaller](https://rubyinstaller.org/downloads/)を使います。

1. Ruby 3.0.3-1(x64) を入れる
   - コマンドプロンプトで聞かれる選択肢には、`1,2,3`を入力
1. インストール終わったら、コマンドプロンプトを立ち上げる
1. `gem install libui`
1. `gem install ocra`
1. 下記を実行するとGUIが立ち上がるので、x ボタンで終了させる。その後、exeができあがります。

```
   ocra  sample.rb   ^
      --dll ruby_builtin_dlls/libssp-0.dll  ^
      --dll ruby_builtin_dlls/libgmp-10.dll  ^
      --dll ruby_builtin_dlls/libffi-7.dll ^
      --gem-all=fiddle ^
      --windows ^
      --icon  sample_icon.ico
```
※ `^` はLinuxの `\` と同じです。

sample_icon.ico は、Windowsのiconファイルです。
[こちらの記事](https://arigayas.exblog.jp/15118069/)を参考にGimpで作成しました。

## はまったところ

### labelの使い方

Ubuntuで動かしていたときには、動いたのに、Windowsで実行すると動かない、というのがありました。
Labelなんですが、下記のような使い方なのかなと思っていたら、

```ruby
label = UI.new_label
UI.label_set_text(label, 'text')
```

単純に、下記で良かったです。上で書くとWindowsではエラーになりました。

```ruby
label = UI.new_label('text')
```

というか、Go の`NewLabel`も`string`引数に取るので、むしろ、Ubuntuで動いていたのがおかしいのかな...?

### ocra で "openssl.so"が読み込めないというエラーが出る

今回あげたコードには含まれていませんが、実際作っていたものでは、opensslをrequireしていました。
その場合に、ocraでexeを作ることはできるのですが、実行時にエラーが発生します。

ちょっとぐぐってみると、twitterやブログにそういった記事が見つかったりました。

* https://twitter.com/yoneapp/status/1360514334380576768
    * [同じ方のQiitaの記事](https://qiita.com/yoneapp/items/d0e5a341e134ac569a4c)
* https://aligach.net/diary/2019/1003/

Goで書き直すかなーと思っていたのですが、下記のオプションを追加することでちゃんと動きました。

```
      --dll ruby_builtin_dlls/libcrypto-1_1-x64.dll
      --dll ruby_builtin_dlls/libssl-1_1-x64.dll
```

後、証明書関係でもエラーが出ることがあるらしいのですが、下記でいけるそうです(試してません)

```
  --gem-file C:\Ruby30-x64\ssl
```

## 終わり

若干はまりましたが、お手軽に作れてよかったです。