🐈‍⬛

Python + Kivy で作ってみた【Ⅵ】

2023/07/04に公開

ゲージ線

照準器のように水平、垂直方向の直線を表示するために、
最初は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

クラス内に二つのクラスメソッドを定義した

  1. idに設定する値のリストを返すgetNames
  2. 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