4クロックで支度しな

脊髄反射で思ったことを書き垂れます。68060とか名乗っときながら4クロックはヌルいとか言わないで。

Python+GTK+3(pygobject)での、テキスト変更シグナルについて

この記事の目的

GTK+3でのテキスト入力用ウィジェットの1つにGtk.Entryというクラスがあります。
このEntryウィジェットへの入力に応じて何か動作(整形や入力のバリデーションなど)を行いたいことがあります。
そのためには、Entryへはどのような経路でテキストが入力されるかを知っておく必要があります。

どういう経路があるか

大きく分けて4つです。

  1. キーボードからの直接入力
  2. IMEからの入力
  3. コンテキストメニューからの「貼り付け」による入力
  4. Gtk.Entry.set_text()関数による変更

キーボードからの直接入力

この入力は「key-press-event」でキャッチできます(Gtk.Widgetに属するシグナルです)。
起こる状況は

  • IMEがオフの状態で
  • Entryにフォーカスがあり
  • キーボードが押された時(key-press-event)

または、

  • IMEがオンの状態で
  • Entryにフォーカスがあり
  • Shift-Enterなどの、IMEをバイパスするキー入力があったとき

です。

この方法でテキストが変更された場合、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シグナルも発生します。

Gtk.Entry.set_text()関数による変更

シグナルは「changed」です。
シグナルに他の引数はなく、届いた時点でEntryのテキストは変更されており、差分を取得する直接的なサービスはありません。

これまで説明した他の変更イベントでもこのシグナルは発生するため、このルート経由での変更を考慮するにはコードに工夫が必要です。
ただ、通常のGUI操作によって、他のシグナルを経由せず、このシグナルによってのみキャッチ可能な変更操作は起こりません。

よって、プログラム内部でEntryのテキストを変更する際には、バリデーションなどの付随する処理も明示的に呼び出してやるのがベストです。

確認用コード

以下に、上記シグナルの確認用コードを記しておきます。
3つのファイルを同じディレクトリに置き、ターミナルから実行してみてください。
print()で状態を出力しているので、IDEのコンソール出力か、ターミナルを見てください。

<?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>
.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()

f:id:Lil68060:20180222212045j:plain
f:id:Lil68060:20180222212102j:plain

最後に

GTK+、特にPython(pygobject)から使用する方法については、なかなか資料がありません。
The Python GTK+ 3 Tutorial — Python GTK+ 3 Tutorial 3.4 documentationや、Classes - Gtk 3.0は貴重な資料となります。
みなさんも、使ってみて何か気付いたことがあれば、ぜひどこかで公開してください。