はじめに
先日作ったダイヤルウィジェットですが、
あとから調べてていろいろわかったことがあったので、続きです。
前回イマイチだった点
前回の実装でイマイチだったのは、値が変化した時のイベントハンドラの所です。
ダイヤルまわした時の値はget_value()
でとれますが、
値が変化したタイミングで何かしたい.....たとえば、get_value()
した値を
MIDIメッセージに変換してリアルタイムにMID機器に出力したい場合とか、
ちょっとどうしようかなってカンジですよね。
Dial
を継承してハンドラくっつけるとか、
もう一回mouse_pressed
とかmotion_notify_event
のイベントハンドラを書くとか....。
うーん面倒くさいなーDial.value
を監視する奴があればいいんだけど、うーん、
新たにObserver作るのもタルいなーーと思ってGtkのマニュアルを眺めてたら、
なーるほど、一応ちゃんとありました。
どうやらGtk.Scale
とかGtk.Scrollbar
などに使われている仕組みが使えるようです。
Gtk.Range
とGtk.Adjustment
GtkにはGtk.Scale
とか今回作ったDial
みたいに
- マウスorキーボード操作で数値を操作する
- 最大&最小値をもつ
- 設定した値を監視することがある
といった特徴をもつウィジェットを作るためのクラスがあります。
それがGtk.Range
と、Gtk.Range
に紐づくGtk.Adjustment
というクラスで、
MVCでいうとView=Gtk.Range
でModel=Gtk.Adjustment
のイメージです。
作り方
とりあえず:詳細はソースを参照ください。
まずDial
クラスをGtk.Frame
クラスからの継承をやめてGtk.Range
を継承して作ります。
Gtk.Frame
はコンストラクタにlabel
が渡せましたが、
Gtk.Range
にはラベルを作る機能はないので削ります。
他のコンストラクタは同じです。
class Dial(Gtk.Range):
def __init__(self,min=0,max=255,initial_value=0, meter_scale=12):
super().__init__()
そして、前のバージョンではインスタンス変数としてmax,min,valueなどを持ってましたが、
これらをやめてGtk.Adjustment
に最大値、最小値、値、増加量を設定して
親クラスのメソッドGtk.Range.set_adjustment()
で登録します。
self.button_pressed
は、マウスのドラッグ処理を検出するためのフラグです(後述)。
self.set_adjustment( Gtk.Adjustment(value=initial_value,lower=min,upper=max,step_increment=1) )
self.button_pressed = False
次に、自身のイベントハンドラを登録します。
self.connect("draw", self.on_draw)
self.connect("button_press_event", self.on_pressed)
self.connect("button_release_event", self.on_released)
self.connect("motion_notify_event",self.on_motion_notified)
(中略)
def _set_and_queue_draw(self, widget, ev):
self.set_value( self._get_value_from_xy(ev.x, ev.y) )
self.queue_draw()
def on_pressed(self, widget, ev):
self.button_pressed = True
self._set_and_queue_draw(widget, ev)
def on_released(self, widget, ev):
self.button_pressed = False
self._set_and_queue_draw(widget, ev)
def on_motion_notified(self, widget, ev):
if self.button_pressed is True:
self._set_and_queue_draw(widget,ev)
マウスが動いた時はmotion_notify_event
が通知されます。
Gtk.Frame
を親クラスにしてたときは、ボタンが押されてからでないと
motion_notify_event
が通知されなかったのですが、
Gtk.Range
は違うみたいなので、フラグself.button_pressed
で制御します。
まずボタンがおされてない時にmotion_notify_event
が来たときはスルーします。
ボタンが初めておされたら、self.button_pressed
フラグをオンにして、
親クラスのGtk.Range.set_value()
を呼んで押された場所から値を計算
(前回作ったDial._get_value_from_xy()
にイベント情報を渡す)してセットします。
親クラスGtk.Range
は、コンストラクタで設定したGtk.Adjustment
を持っていて、
Gtk.Range.set_value()
が呼ばれると内部で
Gtk.Adjustment.set_value()
が呼ばれて内部で最大値・最小値のチェックとかをしてくれます。
ドラッグ中はself.button_pressed
がTrueなので、
ボタンが押された時と同様に押された場所から値を計算してセットします。
ボタンが話されたらself.button_pressed
をFalseにしてから値を計算してセットします。
あれ?コレ別にGtk.Adjustment
いらなくね?、と思われるかもしれませんが、
実はこのGtk.Adjustment
は他のGtk.Range
のような内部で値をもつウィジェットと
共有することができます。
スピンボタンをつける(Gtk.Adjustment
の役割)
それはこのダイヤルにスピンボタン:Gtk.SpinButton
をくっつけてみましょう。
class DialWithSpin(Gtk.Range):
def __init__(self,**kwargs):
super().__init__()
self.dial = Dial(**kwargs)>
self.spin = Gtk.SpinButton()
self.spin.set_adjustment(self.dial.get_adjustment())
Dial
に設定したGtk.Adjustment
をGtk.SpinButton
に設定することで、
Dial
を回しての値を変更すると、自動的にGtk.SpinButton
の値が変更されます。
逆にGtk.SpinButton
の+-ボタンを押たりエントリボックスの値を変更すると、
それらはDial
側にも反映されます。
まぁうまくいかないこともある。
ここでウィジェットを縦に並べようと思って
self.vbox = Gtk.VBox()
self.vbox.pack_start(self.dial,True,True,0)
self.vbox.pack_start(self.spin,False,False,0)
self.add(vbox) ### !!!!
としたんですが、なんとこれができない。
そう、Gtk.DialWithSpin
がGtk.Range
を継承しているせいで、
Gtk.Range
はGtk.Container
を継承してないので、子にウィジェットが作れないのです!!
やむなくGtk.VBox
をDial
とGtk.SpinButton
を配置するのですが、
class DialWithSpin(Gtk.VBox): # Gtk.Rangeの子クラスをやめる
(中略)
self.connect('value_changed',self.on_value_changed) # エラー
こんどはGtk.VBox
がvalue_changed
イベントのハンドラを登録できない!!
しかたないので
class DialWithSpin(Gtk.VBox):
(中略)
def connect(self,signame,func):
#self.connect(signame,func) #自身へのconnectは受けずに流す
self.dial.connect(signame,func) #子クラスにシグナルをそれぞれconnect
self.spin.connect(signame,func) #
というダサい実装をしてみました.....
実はGtk.Adjustment
自体ががvalue_changed
イベントを発行する機能があるので
ウィジェット側にハンドラを設定せずに
def on_model_changed(w):
print("value=%d"%w.get_value())
model = Gtk.Adjustment(value=50,lower=0,upper=100)
d = DialWithSpin(min=0,max=100,value=0)
d.set_adjustment(model)
model.connect('value_changed',on_model_changed)
といった実装もできなくはないです。
でも、モデルクラスにGtk.XXXXのインスタンス変数は絶対入れたくないですけどね!!!......
おわりに
Gtk.Range
とかGtk.Adjustment
に行き着く前に考えたのが、
Dial.set_value()
する時にvalue_changed
イベントを発生させる方法でしたけど、
なんか普通のウィジェットはvalue_changed
イベントは受け取れないみたいです。
javaみたいに標準で汎用的な(?)Observerがあれば解決するのになー、とも思いましたが
ちょっと見つからなかったです。みんなどうしてんのかな?
threadと組み合わせて自作とかしてんのかな???
まぁ、イベントハンドリングってどのプラットフォームでも複雑怪奇ですよねーー
ちなみにGTK4ではこのへんのイベントハンドリングの仕組みがガラっと変わるらしく、
ジェスチャーとかが取り込めるようになるみたいです...
(GIMPも最近やっとGtk+3対応が終わったばかりなのになぁ)
まー、変わるもんはしかたねぇですだね。