Python + Kivy で作ってみた【Ⅵ】
ゲージ線
照準器のように水平、垂直方向の直線を表示するために、
最初はkivyのLineで実現しようと考えた
ただ、マウスでつかんで動かすことに不安がある
そこで、直線状のwidgetを作り、ViewAreaWidgetの上に置くことを考えた
widgetであれば、マウスでつかんでいることを検出しやすい
GaugeWidget作製
単純に、LeftGaugeWidget、RightGaugeWidget、TopGaugeWidget、BottomGaugeWidget
そして、ボックス部分のScopeGaugeWidgetの計五つのwidgetを作る予定だったが
どのGaugeWidgetをマウスでつかんでいるかを判別する時に、ループ処理が使えないため、
名前をGaugeWidgetに統一し、idプロパティの値で各Gaugewidgetを区別することにした
<GaugeWidget>:
size_hint: None, None
background_color: [ 1, 0, 0, 1 ]
canvas.before:
Color:
rgba: self.background_color
Rectangle:
pos: self.to_parent( *self.parent.pos, relative = True )
size: self.size
GaugeWidgetの位置と大きさの範囲をbackground_colorで塗りつぶす
位置と大きさ、そしてidプロパティの値はpythonコード部で指定する
各GaugeWidgetをViewAreaWidget内に配置する
ViewAreaWidget:
id: viewer
size_hint: None, None
size: self.parent.width, self.parent.height * 8 / 10
x: 0
y: self.parent.height / 10
canvas:
Rectangle:
texture: self.image_texture
size: self.image_width, self.image_height
pos: self.to_parent( self.image_x, self.image_y, relative = True )
GaugeWidget:
GaugeWidget:
GaugeWidget:
GaugeWidget:
GaugeWidget:
初期化
五つのGaugeWidgetの初期化を行う
設定する初期値は7個
- 位置(x、y)
- 大きさ(width、hight)
- 色(rgba)
- id値
- マウスポインター名
マウスポインター名は、widgetにプロパティとして持たせ、
ゲージ線にマウスオーバー、ドラッグのマウスポインター変更時に使う
先ず、これらを列挙型クラス(?)に定義する
class Gauges( Enum ) :
Left = ( auto(), 'LEFT_GAUGE', 6, -1, 350, 0, [ 1, 0, 0, 1 ], 'size_we' )
Right = ( auto(), 'RIGHT_GAUGE', 6, -1, 450, 0, [ 1, 0, 0, 1 ], 'size_we' )
Top = ( auto(), 'TOP_GAUGE', -1, 6, 0, 290, [ 1, 0, 0, 1 ], 'size_ns' )
Bottom = ( auto(), 'BOTTOM_GAUGE', -1, 6, 0, 190, [ 1, 0, 0, 1 ], 'size_ns' )
Scope = ( auto(), 'SCOPE_GAUGE', 100 - 6, 100 -6, 350 + 6, 190 + 6, [ 0, 0, 0, 0 ], 'size_all' )
def __init__( self, id, name, w, h, x, y, c, p ) -> None:
self.id = id
self.f_name = str( name )
self.width = w
self.height = h
self.left = x
self.bottom = y
self.color = c
self.icon = p
@classmethod
def getInitData( cls, name: str ) -> dict :
"""初期値取得
Returns:
dictionary: w:width, h:height, x:left, y:bottom, c:color, p:icon
"""
for v in [ *cls.__members__.values() ] :
if( v.f_name == name ) :
return { 'w': v.width, 'h': v.height, 'x': v.left, 'y': v.bottom, 'c': v.color, 'p': v.icon }
return {}
@classmethod
def getNames( cls ) :
names = []
for v in [ *cls.__members__.values() ] :
names.append( v.f_name )
return names
クラス内に二つのクラスメソッドを定義した
- idに設定する値のリストを返すgetNames
- id値に対応する辞書型の初期値データを返すgetInitData
次に、GaugeWidgetクラスのon_kv_postメソッド内で
idプロパティの値を設定し、初期値データを反映させる
class GaugeBase( Widget ) :
pointer = StringProperty( '' )
def on_kv_post( self, base_widget ) :
name_list = Gauges.getNames()
id_name = None
for n in name_list :
if not ( n in base_widget.ids.keys() ) :
base_widget.ids[ n ] = self
id_name = n
break
init_data = Gauges.getInitData( str( id_name ) )
self.x = init_data[ 'x' ]
self.y = init_data[ 'y' ]
self.pointer = init_data[ 'p' ]
self.background_color = init_data[ 'c' ]
init_width = init_data[ 'w' ]
if( init_width > 0 ) :
self.width = init_width
else :
self.width = self.parent.width
init_height = init_data[ 'h' ]
if( init_height > 0 ) :
self.height = init_height
else :
self.height = self.parent.height
widgetのidプロパティの値は単なる文字型ではない
on_kv_postの引数であるbase_widgetの辞書型プロパティidsには
base_widgetにぶら下がっているwidgetやButton等がkvファイルでの出現順に
id値をキーとしてそのwidgetやButton等のインスタンスが格納されている
kvファイルでGaugeWidgetにidを記載しなかったため、
idsプロパティにGaugeWidgetのインスタンスは登録されていない
そこで、idに設定する値のリストを順に探索し、登録されていないid値をキーとして
自信のインスタンスをidsに登録する
続いて、登録したid値で初期値データを取得し、GaugeWidgetの各プロパティに設定する
この時、ViewAreaWidgetの横幅いっぱい、縦幅いっぱいにゲージ線を引くため
初期値データに小細工を仕掛けておく
GaugeWidget移動
ゲージ線およびボックス部分を動かすためには、マウスでつかまないといけない
マウスでつかむ目安にするため、ゲージ線およびボックス部分上にマウスが来た時に
マウスポインターを変更する
ゲージ線を動かす場合は、ボックス部分のサイズも追随して変更させる
ボックス部分を動かす場合は、4本のゲージ線も追随して動かす
マウスカーソル変更
まず、マウスポインターの位置がわからないと始まらない
マウスポインターの移動イベントをViewAreaWidgetに登録する
class ViewAreaBase( Widget ) :
def __init__( self, **kwargs ) :
super( ViewAreaBase, self ).__init__( **kwargs )
Window.bind( mouse_pos = self.on_mouse_pos )
def on_mouse_pos( self, window, pos ) :
if( len( window._mouse_buttons_down ) != 0 ) :
return False
if ( self._modal ) :
return False
if not ( self.collide_point( *pos ) ) :
Window.set_system_cursor( 'arrow' )
return True
cnv_pos = self.to_local( *pos, relative = True )
chkHit = False
for child in self.children :
if( child.collide_point( *cnv_pos ) ) :
Window.set_system_cursor( child.pointer )
chkHit = True
break
if not ( chkHit ) :
Window.set_system_cursor( 'hand' )
return True
まず、ViewAreaWidgetにon_mouse_posメソッドを定義する
続いて、コンストラクタでマウス移動イベントのコールバック関数に設定する
on_mouse_posメソッドのpos引数にマウスポインターの位置情報が格納される
マウスボタンが押下されている、PDFファイル選択ダイアログが表示されている場合は
何もしないで抜ける
一方、ViewAreaWidget外に出た場合は、矢印ポインターに変更して抜ける
on_mouse_pos内では、どのゲージ線、あるいはホックス部分のGaugeWidgetに
マウスポインターがあるのかを判別し、該当するGaugeWidgetに保持している
マウスポインター名をマウスポインターに設定している
移動描画
ゲージ線・ホックス部分を動かすためにViewAreaWidgetの
on_touch_down、on_touch_moveを改修する
class ViewAreaBase( Widget ) :
...
...
...
def on_touch_down( self, touch ) :
if not ( self.collide_point( touch.x, touch.y ) ):
return False
cnv_touch = self.to_local( touch.x, touch.y, True )
root_ids = self.parent.parent.ids
hit_key = ''
for key in Gauges.getNames() :
if( root_ids[ key ].collide_point( *cnv_touch ) ) :
hit_key = key
break
touch.ud[ TouchInfo.Target.p_name ] = hit_key
touch.ud[ TouchInfo.X.p_name ] = touch.x
touch.ud[ TouchInfo.Y.p_name ] = touch.y
return True
def on_touch_move( self, touch ) :
if( any( touch.ud ) and ( self.collide_point( touch.x, touch.y ) ) ) :
if( touch.ud[ TouchInfo.Target.p_name ] == '' ) :
move_x = touch.x - touch.ud[ TouchInfo.X.p_name ]
if( ( self.image_x + move_x < 0 ) and ( self.image_x + move_x > -( self.image_width - self.width ) ) ) :
self.image_x += move_x
touch.ud[ TouchInfo.X.p_name ] = touch.x
move_y = touch.y - touch.ud[ TouchInfo.Y.p_name ]
if( ( self.image_y + move_y < 0 ) and ( self.image_y + move_y > -( self.image_height - self.height ) ) ) :
self.image_y += move_y
touch.ud[ TouchInfo.Y.p_name ] = touch.y
else :
root_ids = self.parent.parent.ids
obj_gauge = root_ids[ touch.ud[ TouchInfo.Target.p_name ] ]
move_x = obj_gauge.x + touch.x - touch.ud[ TouchInfo.X.p_name ]
move_y = obj_gauge.y + touch.y - touch.ud[ TouchInfo.Y.p_name ]
self_x, self_y = self.to_local( *self.pos, relative = True )
left_gauge = root_ids[ Gauges.Left.f_name ]
right_gauge = root_ids[ Gauges.Right.f_name ]
top_gauge = root_ids[ Gauges.Top.f_name ]
bottom_gauge = root_ids[ Gauges.Bottom.f_name ]
scope_gauge = root_ids[ Gauges.Scope.f_name ]
match touch.ud[ TouchInfo.Target.p_name ] :
case Gauges.Left.f_name :
if( ( self_x < move_x ) and ( move_x + left_gauge.width < right_gauge.x ) ) :
left_gauge.x = move_x
scope_gauge.x = left_gauge.right
scope_gauge.width = right_gauge.x - left_gauge.right
case Gauges.Right.f_name :
if( ( left_gauge.right < move_x ) and ( move_x < self.width - right_gauge.width ) ) :
right_gauge.x = move_x
scope_gauge.width = right_gauge.x - left_gauge.right
case Gauges.Top.f_name :
if( ( bottom_gauge.top < move_y ) and ( move_y < self.height - top_gauge.height ) ) :
top_gauge.y = move_y
scope_gauge.height = top_gauge.y - bottom_gauge.top
case Gauges.Bottom.f_name :
if( ( self_y < move_y ) and ( move_y + bottom_gauge.height < top_gauge.y ) ) :
bottom_gauge.y = move_y
scope_gauge.y = obj_gauge.top
scope_gauge.height = top_gauge.y - bottom_gauge.top
case Gauges.Scope.f_name :
if( ( self_x + left_gauge.width < move_x ) and ( move_x + scope_gauge.width < self.width - right_gauge.width ) ) :
if( ( self_y + bottom_gauge.height < move_y ) and ( move_y + scope_gauge.height < self.height - top_gauge.height ) ) :
scope_gauge.x = move_x
scope_gauge.y = move_y
left_gauge.x = scope_gauge.x - left_gauge.width
right_gauge.x = scope_gauge.right
top_gauge.y = scope_gauge.top
bottom_gauge.y = scope_gauge.y - bottom_gauge.height
left_gauge.refresh()
right_gauge.refresh()
top_gauge.refresh()
bottom_gauge.refresh()
scope_gauge.refresh()
touch.ud[ TouchInfo.X.p_name ] = touch.x
touch.ud[ TouchInfo.Y.p_name ] = touch.y
return True
on_touch_downにおいて、ドラッグを始めたGaugeWidgetのidキー値も
touch.udに保持する
on_touch_moveにおいて、touch.udに保持してあるidキー値の
GaugeWidgetの移動処理、および関連するGaugeWidgetの設定を行う
なお、touch.udにidキー値が保持されていない場合は、PDF画像の移動処理を行う
ゲージ線・ホックス部分を再描画を行うrefreshメソッドを
GaugeWidgetクラスに定義する
class GaugeBase( Widget ) :
...
...
...
def refresh( self ) :
self.canvas.before.clear()
with self.canvas.before :
Color( rgba = self.background_color )
Rectangle( pos = self.to_parent( *self.parent.pos, relative = True ), size = self.size )
canvasをクリアした後に、GaugeWidgetの位置と大きさの範囲を
background_colorで塗りつぶす
ゲージ線を実装したので、次回はズーム機能を実装しよう
Discussion