Ruby/gtk3+glade3でDatePicker(ぽいモノ)を扱う

# 前説

gtkはDatePicker無いし使いづら...とぼやいていたら、@kojix2 さんがgtk3でもPopOverとの組み合わせで実現できるよ！とアドバイスしてくれた。

rubyでちょっとしたフロントエンドのGUIの皮をかぶせたく、GUI部分はビルダーツールでぼーっと書きたいけど、wxWidgetが使えなくなった今、QT creatorかJavaFX SceneEditorの二択だよなあ、とおもって愚痴っていたところ上記のような返信をもらった。
gtk3のコードを提示していただいたので、gladeでDatePickerというかDateChooserというか日付選択できるテキストボックス(Gtk::Entry)を作れるか試してみた記録。というか忘れる前に書いておくメモ。

※gtkの何たるかがわかっていないので、用語等間違いがあれば指摘ください。

# 準備

動作前提は ruby2.7, gtk3, glade 3.36.0 です。
rubyは簡単に導入できると思いますが、Windows上でのgladeに少々罠があったので下記を参考にしてください。

## macOSやUnixLikeOS

Debianならapt+gemで、macOSならhomebrewとかでなんとでもなります。

## Windows

Windowsでgladeを使おうと[公式サイト](https://glade.gnome.org/)を見ると、実行バイナリがgtk2用等古いものしか置いてありません。
そこで[scoop](https://scoop.sh/)でmsys2を使うことにした。
(scoopじたいの導入については、[別のQiita記事](https://qiita.com/Dooteeen/items/12dc8fb14042888113d0)を参考にするといいでしょう)

msys2をインストールしたら、msys2のbash上で、pacmanでgtkのライブラリとglade3をインストールします。
scoopで直接gladeをインストールするわけではないことに注意してください。

```bash
$ pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-glade

$ /mingw64/bin/glade
```

上記のようにgladeが起動できるようになれば準備完了(rubyもよしなに準備してください)

# gladeでガワを作る

gtkの流儀にそって(?)、ガワを作っていきます。
gladeの画面にあるメニューをウェブで例えれば、HTML/BODYがトップレベル(≒ウィンドウ)、ユーザ操作を伴うボタンやテキストフィールドがControl(Button,Entry等)、表示用ラベルやイメージがDisplay、divがコンテナ(GtkBox等)といったところでしょうか。

今回は、テキストフィールド(GtkEntry)にフォーカスがあたったら、カレンダー上のウィジェットが表示されるようにしたいので、メインとなるウィンドウのほかに、PopOverするコンテナを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23242/4286958c-b37d-f8d1-25ec-957ee7a90494.png)

せっかく自分でせこせこ作らないといけないということで、ただのカレンダーにプラスして昨日～明日を選択するボタンを追加しておきます。変わりゆく私。

あとでruby上から呼び出すときに用に各ウィジェットにIDで名前を振っておきます。
またボタンクリックなどのイベントを、シグナルタブのなかから選んで実行するメソッドをハンドラーとして登録しておきます。
ruby実行時にハンドラ登録したメソッドがないとエラーになるので注意してください。

保存される.gladeファイルはXMLです。


## キモな部分

DatePickerを呼び出すGtkEntryのシグナル focus-in-event イベントにハンドラを記述して、フォーカスが来た時にPopOverを表示させるようにする。
ただし逆にフォーカスが外れたときにPopOverを閉じるようにすると、当たり前だがCalendarが操作できなくなるので注意。

Calenderで日付をダブルクリックしたときのday-select-double-clickシグナルに、ハンドラを割り当てる

PopOver表示をオフにする条件として「閉じる」ボタンを作り、clickedシグナルで visible = false にする。

## 操作のトリビア

左側ペインでGtkWindowを選択した状態で、下部のギアボタンを押すと実行時のプレビューが行えます。

昔のVisualBASICやDelphiではウィンドウのしたに自由にボタン類が配置できるのが標準でしたが、gtkではコンテナ等にパッキングして追加し、自動レイアウトするのが基本みたいです。
自由配置する場合は、GtkFixedというコンテナを使います。Fixedの中に配置したウィジェットはShiftキーを押しながらドラッグすることで動かしたりできます。

GUIビルダではあるが、ウィジェットの移動がままならないgladeさんは、パッキングタブで位置の順序を動かすことで調整を行う。
別のBoxに動かしたいとき等はCutしてPaste...

# 実装用コードを作成する

一部mvcぽくならんかと試したところが入ってるので、シンプルなコードは[元ネタのツイート](https://twitter.com/2xijok/status/1296811545884626945)を参考に…

```ruby:app.rb
# frozen_string_literal: true

%w[pp gtk3 date observer].each { |lib| require lib }

class ObjectController
  include Observable
  attr_reader :content

  def content=(object)
    @content = object
    @content.add_observer(self)
    @content
  end

  def update
    changed
    notify_observers
    self
  end
end

class View
  attr_reader :widget

  def controller=(c)
    @controller = c
    @controller.add_observer(self)
    c
  end
end

class State
  include Observable
  def initialize
    @select_date = Date.today
  end

  attr_reader :selected_date

  def selected_date=(date)
    @selected_date = date
    changed
    notify_observers
    date
  end
end

class SelectedDateView < View
  def initialize(parent_builder)
    @widget = parent_builder.get_object('entrydate')
    @widget.signal_connect('changed') do
      @controller.content.selected_date = value
      true
    end
  end

  def value
    @widget.text
  end

  def value=(f)
    @widget.text = f.to_s
  end

  def update
    self.value = @controller.content.selected_date
    self
  end
end

class GyoumuApp
  def initialize
    @select_date = Date.today

    @builder = Gtk::Builder.new(file: 'glade1.glade')

    @seldate_controller = ObjectController.new
    @seldate_controller.content = State.new
    @seldate_view = SelectedDateView.new(@builder)
    @seldate_view.controller = @seldate_controller

    @win = @builder.get_object('main')
    @date_picker = @builder.get_object('date_picker') # PopOver
    @calendar = @builder.get_object('calendar') # PopOverのなかのCalendar

    @buf_code = @builder.get_object('entrybuffer1')
    @buf_code.text = 'K6205'

    @btn_yday = @builder.get_object('select_yesterday')
    @btn_tday = @builder.get_object('select_today')
    @btn_tmrw = @builder.get_object('select_tomorrow')

    # PopOver内のボタンはruby上からシグナルハンドラを設定する
    @btn_yday.signal_connect('clicked') do
      select_day(-1)
    end
    @btn_tday.signal_connect('clicked') do
      select_day(0)
    end
    @btn_tmrw.signal_connect('clicked') do
      select_day(1)
    end

    @builder.connect_signals { |handler| method(handler) } # handler は String
  end

  def select_day(day)
    @seldate_controller.content.selected_date = Date.today + day
    click_dateclose
  end

  # [✕] が押された時にアプリを終了する
  def on_main_destroy
    Gtk.main_quit
  end

  # entrydateにフォーカスが移ったとき
  def focus_in_entrydate
    @date_picker.visible = true
  end

  # calendarの日付をダブルクリックしたとき
  def dblclick_date
    @seldate_controller.content.selected_date = sprintf('%04d-%02d-%02d', @calendar.year, @calendar.month, @calendar.day)
    click_dateclose
  end

  # date_pickerでキャンセルボタンを押下
  def click_dateclose
    @date_picker.visible = false
  end
end

class App < GyoumuApp
  def initialize
    super
    @win.show_all
    Gtk.main
  end
end
App.new
```


# まとめ

雑なリポジトリは[こちら](https://github.com/paichi81/glade_tut1)

gtk3ライブラリは、Gtk::Builder.newでgladeファイルを読んで、ささっと使えるようになるので便利だね。

WxWidgetのDatePicker等、既製部品がささっと使えると便利ではあるが、今回のように独自のボタンを追加したいようなときに小回りが利かない。gtkはunixらしいアプローチで解決できることがわかった。
ただ面倒ではある…。

gladeの設定をみていると、とっつきにくいgtkでいじれる部分がかなり細やか。
逆にいうと生gtkはウィジェットをかなり意識しないといけないので、MVVM的なものが恋しいヨ...。