前説
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を使おうと公式サイトを見ると、実行バイナリがgtk2用等古いものしか置いてありません。
そこでscoopでmsys2を使うことにした。
(scoopじたいの導入については、別のQiita記事を参考にするといいでしょう)
msys2をインストールしたら、msys2のbash上で、pacmanでgtkのライブラリとglade3をインストールします。
scoopで直接gladeをインストールするわけではないことに注意してください。
$ 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するコンテナを作ります。
せっかく自分でせこせこ作らないといけないということで、ただのカレンダーにプラスして昨日~明日を選択するボタンを追加しておきます。変わりゆく私。
あとで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ぽくならんかと試したところが入ってるので、シンプルなコードは元ネタのツイートを参考に…
# 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
まとめ
雑なリポジトリはこちら
gtk3ライブラリは、Gtk::Builder.newでgladeファイルを読んで、ささっと使えるようになるので便利だね。
WxWidgetのDatePicker等、既製部品がささっと使えると便利ではあるが、今回のように独自のボタンを追加したいようなときに小回りが利かない。gtkはunixらしいアプローチで解決できることがわかった。
ただ面倒ではある…。
gladeの設定をみていると、とっつきにくいgtkでいじれる部分がかなり細やか。
逆にいうと生gtkはウィジェットをかなり意識しないといけないので、MVVM的なものが恋しいヨ...。