はじめに
先日作ったダイヤルウィジェットですが、
あとから調べてていろいろわかったことがあったので、続きです。
前回イマイチだった点
前回の実装でイマイチだったのは、値が変化した時のイベントハンドラの所です。
ダイヤルまわした時の値は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対応が終わったばかりなのになぁ)
まー、変わるもんはしかたねぇですだね。