当初は、自分の公開備忘録というコンセプトで作っていたのだが、いつのまにか開発実況となってしまった「kivy戦記」。

この名前の通り、開発中は勘所がなかなかわからず、とても苦労しました。

いまでも、自分としては勘所がつかめているのか自信がないのですが、これからもkivyを使った開発をちょくちょくやっていこうかと思います。

kivyも、現在は日本語環境で難がありますし、本来のターゲットであるスマホへのインストールも難しいらしく、まだまだ一般的と言えない状況があります。

こちらも、kivyの将来に賭けた身であり(もっとも、これよりよいGUI環境があればそっちに移るかも)、kivyのバージョンアップを信じて、これからも開発していきたいと思います。

 

ただ、これからの開発では、今回のようにいちいち実況するスタイルは、こちらも疲れますし、だいいち見ている方々も参考になりづらいのではないかと思って、非常に恐縮しています。

これ以降は、なにか皆様の参考になりそうなものをピックアップして、開発と並行してぼちぼちアップしていきたいと思います。

 

最後に一つ愚痴を言わせてください。

ああ、Unityがうらやましい。

 

なお、(とりあえずの)最終的なfilelist.pyは、こうなります。


import os
import sys
import json

from kivy.app import App
from kivy.lang import Builder
from kivy.utils import platform

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivy.uix.treeview import TreeViewLabel
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import ObjectProperty, StringProperty
from kivy.clock import Clock

from pprint import pprint

from kivy.core.text import LabelBase, DEFAULT_FONT
from kivy.resources import resource_add_path

# 日本語フォント設定
resource_add_path('./fonts')
LabelBase.register(DEFAULT_FONT, 'ipaexg.ttf')

sm = ScreenManager()
path_inf = 'path_inf.json'


class AgreeMsgPop(BoxLayout):
    message_text = StringProperty()
    prs_agree1 = ObjectProperty(None)
    prs_agree2 = ObjectProperty(None)

class PathReadInf:

    def __init__(self, **kwargs):
        self.path_json = None
        self.notfound = False
        self.err = False

        try:
            #raise BaseException('やっちまったな')
            with open(path_inf, 'r') as fd:

                self.path_json = json.load(fd)

        except FileNotFoundError:
            self.notfound = True

        except PermissionError as err:
            self.msg = '保存データを読み込もうとしましたが、読み込み許可がありません。\n'
            self.msg += '読みこもうとしたパス・ファイルは、' + path_inf + 'です。\n'
            self.msg += '保存データの読み込み許可を確認するか、パス・ファイルが適当かを確認してください。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_drive, \
                prs_agree2=kwargs['btn_ok_post'], \
                message_text=self.msg)
            self.popup = Popup(title='読み込み許可エラー', content=content)
            self.popup.open()

            self.err = True

        except IOError as err:
            self.msg = '保存データを読み込もうとしましたが、読み込めません。\n'
            self.msg += '読み込み先機器に問題があるようです。\n'
            self.msg += '読み込み先機器の状態を確認してください。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_drive, \
                prs_agree2=kwargs['btn_ok_post'], \
                message_text=self.msg)
            self.popup = Popup(title='I/Oエラー', content=content)
            self.popup.open()

            self.err = True

        except BaseException as err:
            self.msg = '保存データを読み込もうとしましたが、何らかのエラーが起きました。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_drive, \
                prs_agree2=kwargs['btn_ok_post'], \
                message_text=self.msg)
            self.popup = Popup(title='その他のエラー', content=content)
            self.popup.open()

            self.err = True

    def prs_err_drive(self):
        self.popup.dismiss()

class PathWriteInf:
    def __init__(self, **kwargs):

        self.err = True

        path_inf_dict = kwargs['path_inf_dict']
        self.pd = {}
        self.pd['begin_path'] = path_inf_dict['begin_path']

        try:
            #raise BaseException('やっちまったな')
            with open(path_inf, 'w') as fd:
                json.dump(self.pd, fd)
                self.err = False

        except PermissionError as err:
            self.msg = '保存データを書き込もうとしましたが、書き込み許可がありません。\n'
            self.msg += 'パスは、' + self.pd['begin_path'] + 'です。\n'
            self.msg += '保存先の書き込み許可を確認するか、パスが適当かを確認してください。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_agree, \
                prs_agree2=kwargs['btn_ok_post'], \
                message_text=self.msg)
            self.popup = Popup(title='書き込み許可エラー', content=content)
            self.popup.open()

        except IOError as err:
            self.msg = '保存データを書き込もうとしましたが、書き込めません。\n'
            self.msg += '書き込み先機器に問題があるようです。\n'
            self.msg += '書き込み先機器の状態を確認してください。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_agree, \
                prs_agree2=kwargs['btn_ok_post'], \
                message_text=self.msg)
            self.popup = Popup(title='I/Oエラー', content=content)
            self.popup.open()


        except BaseException as err:
            self.msg = '保存データを書き込もうとしましたが、何らかのエラーが起きました。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_agree, \
                prs_agree2=kwargs['btn_ok_post'], \
                message_text=self.msg)
            self.popup = Popup(title='その他のエラー', content=content)
            self.popup.open()

    def prs_err_agree(self):
        self.popup.dismiss()

class DriveBtn(BoxLayout):
    list_view = ObjectProperty(None)

    def prs_drive(self, button):
        sm.current_screen.popup.start_path = button.text
        sm.current_screen.popup.title = '読み込み中 ' +  button.text

class PathChoosePop(Popup):
    #current_dir = os.path.dirname(os.path.abspath(__file__))

    prs_load = ObjectProperty(None)
    prs_cancel = ObjectProperty(None)
    start_path = ObjectProperty(None)

    def __init__(self, **kwargs):
        super(PathChoosePop, self).__init__(**kwargs)
        self.title ='読み込み中 ' + self.start_path

        if platform == 'win':
            import win32api

            dv = win32api.GetLogicalDriveStrings()

            self.drive_list.data = []
            btn_list = dv.split('\000')[:-1]   #ドライブ名をリストに、最後につく余分な空白は削除
            for btn_list_idv in btn_list:
                self.drive_list.data.append({'value': btn_list_idv})

        else:
            self.drive_list.width = 0
            self.drive_list.size_hint_x = 0

    def prs_path(self, path, selection):
        if 0 < len(selection):
            self.title = '読み込み中 ' + selection[0]

    def isdir_choosepath(self, dirname, filename):
        # ディスレクトリならTrueを返す
        return os.path.isdir(os.path.join(dirname, filename))

class SetScn(Screen):
    bgnpath = ObjectProperty(None)

    def __init__(self, **kwargs):
        super(SetScn, self).__init__(**kwargs)

    def prs_chooser(self):
        begin_path_result = self.isdir_bgnpath()

        if begin_path_result == True:
            self.popup = PathChoosePop(prs_load=self.prs_popup_ok, \
                                       prs_cancel=self.prs_popup_cancel, \
                                       start_path=self.bgnpath.text)
            self.popup.open()

        else:
            self.msg = '入力した開始パスが存在しません\n\n'
            self.msg += self.bgnpath.text

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_popup_cancel, \
                prs_agree2=self.do_pass, \
                message_text=self.msg)

            self.popup = Popup(title='入力エラー', content=content)
            self.popup.open()

            self.bgnpath.text = sm.begin_path_previous

    def prs_ok(self):
        begin_path_result = self.isdir_bgnpath()

        if begin_path_result == True:

            self.pd = {}
            self.pd['begin_path'] = self.bgnpath.text

            pw = PathWriteInf( \
                path_inf_dict=self.pd, \
                btn_ok_post=self.file_err)
            if pw.err == False:
                self.manager.get_screen('all').selpath.text = self.bgnpath.text
                sm.begin_path_previous = self.bgnpath.text
                sm.current = 'all'

        else:
            self.msg = '入力した開始パスが存在しません\n\n'
            self.msg += self.bgnpath.text

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_agree, \
                prs_agree2=self.do_pass, \
                message_text=self.msg)

            self.popup = Popup(title='入力エラー', content=content)
            self.popup.open()

            self.bgnpath.text = sm.begin_path_previous

    def prs_cancel(self):
        self.bgnpath.text = sm.begin_path_previous
        sm.current = 'all'

    def prs_err_agree(self):
        self.popup.dismiss()

    def prs_popup_ok (self, path, filename):
        self.bgnpath.text = path
        #sm.begin_path_previous = path
        self.popup.dismiss()

    def prs_popup_cancel(self):
        self.popup.dismiss()

    def isdir_bgnpath(self):
        if os.path.isdir(self.bgnpath.text) == False:

            return False

        return True

    def file_err(self):
        sys.exit()

    def do_pass(self):
        pass

class ResultPts(BoxLayout):
    start_path = StringProperty(None)
    tree_view = ObjectProperty(None)
    dir_cnt = 0

    def __init__(self, **kwargs):
        super(ResultPts, self).__init__(**kwargs)
        #Clock.schedule_once(self._after_kv_applied)

    def get_tree(self):
        self.init_path = ''
        self.tree_view.bind(minimum_height=self.tree_view.setter('height'))

        ResultPts.dir_cnt = 1000
        self.dir_cnt_limit = ResultPts.dir_cnt

        self.del_tree()
        self.tree_height = self.get_each_tree(self.start_path, None)
        self.path_msg = '只今のパス:'+self.start_path
        if ResultPts.dir_cnt < 0:
            self.path_msg += ' ('+str(self.dir_cnt_limit)+'個を越えたので中断しています)'
        self.tree_view.root_options = {'text':self.path_msg, 'font_size': 20}

    def del_tree(self):
        for child_widget_each in [child_widget for child_widget in self.tree_view.children]:
            self.tree_view.remove_node(child_widget_each)

    def get_each_tree(self, this_dir, parent):
        try:
            #raise BaseException('やっちまったな')

            ls = os.listdir(path=this_dir)
            # 指定ディレクトリ内の数でカウントダウンする
            for ls_idv in ls:
                ResultPts.dir_cnt -= 1

            # 指定ディレクトリ内のファイル・ディレクトリを一度フルパスにしてから、ディレクトリであればリストに入れる
            ls_dir = [ls_idv for ls_idv in ls if os.path.isdir(os.path.join(this_dir, ls_idv))]
            ls_dir.sort()

            # 指定ディレクトリ内のファイル・ディレクトリを一度フルパスにしてから、ファイルであればリストに入れる
            ls_file = [ls_idv for ls_idv in ls if os.path.isfile(os.path.join(this_dir, ls_idv))]
            ls_file.sort()

            if ResultPts.dir_cnt >= 0:
                for ls_dir_idv in ls_dir:
                    tree_node = self.tree_view.add_node(TreeViewLabel(text=ls_dir_idv, is_open=False), parent)
                    self.get_each_tree(os.path.join(this_dir, ls_dir_idv), tree_node)

            if ls_file:
                for ls_file_idv in ls_file:
                    self.tree_view.add_node(TreeViewLabel(text=ls_file_idv, is_open=False), parent)
            else:
                # ディレクトリの中にファイルが1つも入っていない時の処理
                self.tree_view.add_node(TreeViewLabel(text='(empty)', is_open=False), parent)

        except PermissionError as err:
            pass

        except IOError as err:
            self.msg = 'ファイルデータを読み込もうとしましたが、読み込めません。\n'
            self.msg += '読み込み先機器に問題があるようです。\n'
            self.msg += '読み込み先機器の状態を確認してください。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_agree, \
                prs_agree2=self.file_err, \
                message_text=self.msg)
            self.popup = Popup(title='I/Oエラー', content=content)
            self.popup.open()

        except BaseException as err:
            self.msg = 'ファイルデータを読み込もうとしましたが、何らかのエラーが起きました。\n\n'
            self.msg += str(err)

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_agree, \
                prs_agree2=self.file_err, \
                message_text=self.msg)
            self.popup = Popup(title='その他のエラー', content=content)
            self.popup.open()


    def prs_err_agree(self):
        self.popup.dismiss()

    def file_err(self):
        #
        # ファイル読み込み処理で異常だった時の処理
        #
        sys.exit()

class FrontScn(Screen):
    selpath = ObjectProperty(None)

    def initial(self, dt):
        #
        # SetScnの最初期内容
        #
        self.init_path = ''

        thisos = os.name
        #thisos = 'ms-dos'
        if thisos == 'posix':
            self.init_path = os.environ.get('HOME')
        elif thisos == 'nt':
            self.init_path = os.environ.get('HOMEDRIVE') + os.environ.get('HOMEPATH')
        else:
            self.msg = '内部OS判定(os.name)が、"' + thisos + '”でした。\n'
            self.msg += 'サポートしているOSは、UNIX系統と、Windowsです。\n'
            self.msg += 'したがってこのOSはサポート外です。すみません。'

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_oserr_agree, \
                prs_agree2=self.do_pass, \
                message_text=self.msg)

            self.popup = Popup(title='OSサポートエラー', content=content)
            self.popup.open()
            return

        #
        # FrontScn用の初期設定 ファイルパス読み込み
        #
        pr = PathReadInf(btn_ok_post=self.file_err)

        if pr.path_json != None:
            #
            # PathReadInf()で正常だった時の処理
            #
            path_inf_begin = pr.path_json['begin_path']
            self.manager.get_screen('set').bgnpath.text = path_inf_begin
            self.selpath.text = path_inf_begin
            sm.file_name_previous = path_inf_begin
            sm.begin_path_previous = path_inf_begin

        else:
            if pr.notfound == True:
                #
                # PathReadInf()で見つからなからった時の処理
                #
                self.manager.get_screen('set').bgnpath.text = self.init_path
                self.selpath.text = self.init_path
                sm.file_name_previous = self.init_path
                sm.begin_path_previous = self.init_path

                sm.current = 'set'

        if pr.err == False:
            #
            # FrontScnのtree処理
            #
            rp = self.ids.result_pts   # kv言語上で書いたResultPtsを取得
            rp.start_path = self.selpath.text
            rp.get_tree()

    def prs_chooser(self):
        self.popup = PathChoosePop(prs_load=self.prs_popup_load, \
                                   prs_cancel=self.prs_popup_cancel, \
                                   start_path=self.selpath.text)
        self.popup.open()

    def prs_exec(self):
        file_name_result = self.isdir_selpath()

        if file_name_result == True:
            sm.file_name_previous = self.selpath.text
            #
            # FrontScnのtree処理
            #
            rp = self.ids.result_pts   # kv言語上で書いたResultPtsを取得
            rp.start_path = self.selpath.text
            rp.get_tree()

        else:
            self.msg = '入力した開始パスが存在しません\n\n'
            self.msg += self.selpath.text

            content = AgreeMsgPop ( \
                prs_agree1=self.prs_err_agree, \
                prs_agree2=self.do_pass, \
                message_text=self.msg)
            self.popup = Popup(title='入力エラー', content=content)
            self.popup.open()

            self.selpath.text = sm.file_name_previous

    def prs_set(self):
        #
        # SetScnへの画面遷移時の内容
        #
        sm.current = 'set'

    def prs_popup_load(self, path, filename):
        self.selpath.text = path
        sm.file_name_previous = path
        self.popup.dismiss()

    def prs_popup_cancel(self):
        self.popup.dismiss()

    def prs_err_agree(self):
        self.popup.dismiss()

    def prs_oserr_agree(self):
        self.popup.dismiss()
        sys.exit()

    def isdir_selpath(self):
        if os.path.isdir(self.selpath.text) == False:

            return False

        return True

    def file_err(self):
        #
        # ファイル読み込み処理で異常だった時の処理
        #
        sys.exit()

    def do_pass(self):
        pass

class FilelistApp(App):
    def build(self):
        allscn = FrontScn()
        setscn = SetScn()

        Clock.schedule_once(allscn.initial, 0)

        sm.add_widget(allscn)
        sm.add_widget(setscn)

        return sm


if __name__ == '__main__':
    FilelistApp().run()

 

filelist.kv


<AgreeMsgPop>
    orientation: 'vertical'

    Label:
        size_hint_y: 0.9
        text: root.message_text

    BoxLayout:
        size_hint_y: 0.1
        orientation: 'horizontal'

        Button:
            size_hint_x: 10
            text: 'OK'
            on_release:
                root.prs_agree1()
                root.prs_agree2()

<DriveBtn>:
    canvas:
        Color:
            rgba: 1, 1, 1, 1
        Rectangle:
            size: self.size
            pos: self.pos
    value: ''
    Button:
        text: root.value
        background_normal: ''
        background_color: 0.5, 0.5, 0.75, 1
        color: 1, 1 ,1 ,1
        on_press: root.prs_drive(self)

<PathChoosePop>:
    id: path_choose_pop
    size_hint: 0.9, 0.9
    drive_list: drive_list
    auto_dismiss: False

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'
        BoxLayout:
            orientation: 'horizontal'

            RecycleView:
                id: drive_list
                size_hint_x: 0.3
                scroll_type: ['bars', 'content']
                scroll_wheel_distance: sp(30) #スクロール速度
                bar_width: sp(10)
                viewclass: 'DriveBtn'
                RecycleBoxLayout:
                    default_size: None, sp(40)
                    default_size_hint: 1, None
                    size_hint_y: None
                    height: self.minimum_height
                    orientation: 'vertical'
                    spacing: sp(2)

            FileChooserListView:
                id: list_view
                size_hint_x: 0.7
                dirselect: True
                path: root.start_path
                filters: [root.isdir_choosepath]
                on_touch_down: path_choose_pop.prs_path(list_view.path, list_view.selection)

        BoxLayout:
            size_hint_y : None
            height : 30
            Button:
                text: 'キャンセル'
                on_release: root.prs_cancel()

            Button:
                text: '読み込み'
                on_release: root.prs_load(list_view.path, list_view.selection)

<SetScn>:
    name: 'set'
    bgnpath: bgnpath

    BoxLayout:
        orientation: 'vertical'

        BoxLayout:
            size_hint_y: 0.1
            orientation: 'horizontal'

            BoxLayout:
                Label:
                    size_hint_x: 0.1
                    text: '開始パス'
                TextInput:
                    id: bgnpath
                    size_hint_x: 0.8
                Button:
                    size_hint_x: 0.1
                    text: '…'
                    on_release: root.prs_chooser()

        BoxLayout:
            size_hint_y: 0.8

        BoxLayout:
            size_hint_y: 0.1
            Button:
                size_hint_x: 0.5
                text: 'キャンセル'
                on_release: root.prs_cancel()        
            Button:
                size_hint_x: 0.5
                text: 'OK'
                on_release: root.prs_ok()

<ResultPts>
    tree_view: tree_view
    ScrollView:
        do_scroll_x: False
        TreeView:
            id: tree_view
            hide_root: False
            size_hint_y: None

<FrontScn>:
    name: 'all'
    selpath: selpath

    BoxLayout:
        orientation: 'vertical'

        BoxLayout:
            size_hint_y: 0.1
            orientation: 'horizontal'

            BoxLayout:
                Button:
                    size_hint_x: 0.1
                    text: '設定'
                    on_release: root.prs_set()
                TextInput:
                    id: selpath
                    size_hint_x: 0.7
                    text: 'ファイルパス'
                Button:
                    size_hint_x: 0.1
                    text: '…'
                    on_release: root.prs_chooser()
                Button:
                    size_hint_x: 0.1
                    text: '実行'
                    on_release: root.prs_exec()

        BoxLayout:
            size_hint_y: 0.9
            ResultPts:
                id: result_pts

FrontScn: