MIDIスイッチャを作る:ダイヤルウィジェットがほしい（つづき）

# はじめに

[先日作ったダイヤルウィジェット](https://qiita.com/fgtsystems/items/765ac2638642d1e79b3f)ですが、
あとから調べてていろいろわかったことがあったので、続きです。

# 前回イマイチだった点

[前回の実装](https://github.com/KiyohitoSUZUKI/gmidisw/blob/master/midisw/gtk3/dial-old.py)でイマイチだったのは、値が変化した時のイベントハンドラの所です。
ダイヤルまわした時の値は`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` のイメージです。

# 作り方

とりあえず：[詳細はソースを参照ください。](https://github.com/KiyohitoSUZUKI/gmidisw/blob/master/midisw/gtk3/dial.py)

まず`Dial`クラスを`Gtk.Frame`クラスからの継承をやめて`Gtk.Range`を継承して作ります。
`Gtk.Frame`はコンストラクタに`label`が渡せましたが、
`Gtk.Range`にはラベルを作る機能はないので削ります。
他のコンストラクタは同じです。

```dial.py
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`をくっつけてみましょう。

```dial.py

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`イベントのハンドラを登録できない！！

しかたないので

```dial.py
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ではこのへんの[イベントハンドリングの仕組みがガラっと変わるらしく、](https://blog.gtk.org/2020/04/29/custom-widgets-in-gtk-4-input/)
ジェスチャーとかが取り込めるようになるみたいです...
(GIMPも最近やっとGtk+3対応が終わったばかりなのになぁ）

まー、変わるもんはしかたねぇですだね。