Python+GTK+3(pygobject)での、テキスト変更シグナルについて
この記事の目的
GTK+3でのテキスト入力用ウィジェットの1つにGtk.Entryというクラスがあります。
このEntryウィジェットへの入力に応じて何か動作(整形や入力のバリデーションなど)を行いたいことがあります。
そのためには、Entryへはどのような経路でテキストが入力されるかを知っておく必要があります。
どういう経路があるか
大きく分けて4つです。
- キーボードからの直接入力
- IMEからの入力
- コンテキストメニューからの「貼り付け」による入力
- Gtk.Entry.set_text()関数による変更
キーボードからの直接入力
この入力は「key-press-event」でキャッチできます(Gtk.Widgetに属するシグナルです)。
起こる状況は
- IMEがオフの状態で
- Entryにフォーカスがあり
- キーボードが押された時(key-press-event)
または、
です。
この方法でテキストが変更された場合、Gtk.Editable (Gtk.Entryの親クラス)のchangedシグナルも発生します。
ただし、改行の入力のように、テキストが変更されない場合はchangedシグナルは発生しません。
「CTRL-C」や「CTRL-Z」のような、特にUnix系OSではOSのシグナルとして扱われるような入力も、特に設定することなくkey-press-eventとして届きます。
ただし、シグナルの引数は、通常のCTRLモディファイヤ付きのアスキー文字列ではなく(例えば、特にデフォルトのキーバインドがない、「CTRL-A」のようなキー入力は、CTRLモディファイヤ付きの文字「A」の入力となります)、特殊な文字が渡されます。
シグナルの引数の詳細については、Gtk.Widget - Classes - Gtk 3.0を参照してください。
通常のキーに関してはGdk.EventKeyのstringメンバ、Enterキーなどの特殊キーについてはkeyvalメンバを判定するのが有効です(SHIFTやCTRLのように、キーボードに複数あるキーは、それぞれ異なるkeyval値を持つことに注意してください)。
※厳密には、「key-release-event」でも同等のことはできますが、GTK+では、Entryへ入力が反映されるのは「キーが押された時」なので、key-press-eventの方を取り上げました。
※ちなみに、マウス入力に関しては、「button-release-event」が起こった時に反映されます。
IMEからの入力
この入力は、変更を直接キャッチするのは面倒です。
シグナルは「preedit-changed」です。
起こる状況は、
- IMEがオンの状態で、
- 文字が入力されたとき、または
- 文字が変換されたとき、または
- 文字が確定されたとき
です。
このシグナルから得られる文字列は、直接Entryに影響を及ぼすものではなく、また、最終的にどの文字列に確定されたかを知るのは少々面倒です。
最も確実な状況は、preedit-changedシグナルから得られる文字列が空のときで、これはIMEで編集中の文字列が確定されたことを示します。
そして、そのときにはすでにEntryのテキストは変更されてしまっているので、IMEから入力される前のテキストとの差分を得るには、IMEへの入力が行われる「前」にテキストをどこかへ覚えておかねばならず、面倒です。
できるならば、IMEの確定のタイミングで、Entryのテキスト全文に対して処理を行うように設計・実装するべきです。
この方法でテキストが変更された場合、Gtk.Editable (Gtk.Entryの親クラス)のchangedシグナルも発生します。
コンテキストメニューからの「貼り付け」による入力
シグナルは「paste-clipboard」です。
このシグナルに他の引数はなく、届いた時点でEntryのテキストは変更されてしまっています。
そのため、入力のバリデーションなどを行う場合は、シグナルが届いた時点で全文をチェックすることになります。
Gtk.Entryはデフォルトでコンテキストメニューが提供されるので、常にこの経路での適すと変更は起こると考えるべきです。
この方法でテキストが変更された場合、Gtk.Editable (Gtk.Entryの親クラス)のchangedシグナルも発生します。
確認用コード
以下に、上記シグナルの確認用コードを記しておきます。
3つのファイルを同じディレクトリに置き、ターミナルから実行してみてください。
print()で状態を出力しているので、IDEのコンソール出力か、ターミナルを見てください。
- gui.glade
<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.20.2 --> <interface> <requires lib="gtk+" version="3.20"/> <object class="GtkWindow" id="window"> <property name="can_focus">False</property> <child> <object class="GtkBox"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child> <object class="GtkEntry" id="entry"> <property name="visible">True</property> <property name="can_focus">True</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkButton" id="button"> <property name="label" translatable="yes">set "ABC123" to Entry </property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> </object> </child> <child> <placeholder/> </child> <style> <class name="window"/> </style> </object> </interface>
- style.css
.window { font-family: "Meiryo", "Hiragino Kaku Gothic ProN", "MS PGothic", Osaka, sans-serif; }
# -*- coding: utf-8 -*- ''' Created on 2018/02/21 @author: Lil68060 ''' import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from gi.repository import Gdk def entry_key_press(entry, event): print("key-press: is_modifier:{0}, keyval={1}, length={2}, state={3}, string=\"{4}\"".format( event.is_modifier, #0 event.keyval, #1 event.length, #2 decode_state(event.state), #3 event.string, #4 )) return False def decode_state(state): flags = [] if state & Gdk.ModifierType.SHIFT_MASK: flags.append("SHIFT") if state & Gdk.ModifierType.META_MASK: flags.append("META") if state & Gdk.ModifierType.CONTROL_MASK: flags.append("CTRL") if state & Gdk.ModifierType.SUPER_MASK: flags.append("SUPER") return flags def entry_preedit_changed(entry, text): print("preedit-change(IME): [{0}](len={1})".format(text, len(text))) return False def entry_paste_clipboard(entry): print("paste-clipboard: \"{0}\"".format(entry.get_text())) return False def entry_changed(entry): print("changed: \"{0}\"".format(entry.get_text())) return False def button_clicked(button, entry): txt = "ABC123" entry.set_text(txt) return False if __name__ == '__main__': # gladeファイルを読み込む builder = Gtk.Builder() builder.add_from_file("gui.glade") window = builder.get_object("window") window.connect("delete-event", Gtk.main_quit) # CSSを設定(デフォルトのフォントはあまり良くないので) css_provider = Gtk.CssProvider() css_provider.load_from_path('style.css') Gtk.StyleContext.add_provider_for_screen( window.get_screen(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) entry = builder.get_object("entry") entry.connect("key-press-event", entry_key_press) entry.connect("preedit-changed", entry_preedit_changed) entry.connect("paste-clipboard", entry_paste_clipboard) entry.connect("changed", entry_changed) button = builder.get_object("button") # connectメソッドへ追加の引数を渡すことで、シグナルハンドラへその引数を渡すことができる button.connect("clicked", button_clicked, entry) window.show_all() Gtk.main()
最後に
GTK+、特にPython(pygobject)から使用する方法については、なかなか資料がありません。
The Python GTK+ 3 Tutorial — Python GTK+ 3 Tutorial 3.4 documentationや、Classes - Gtk 3.0は貴重な資料となります。
みなさんも、使ってみて何か気付いたことがあれば、ぜひどこかで公開してください。