見出し画像

PythonでAndroidアプリ作成してみる #3 Kivy GUI深堀

こんにちは、Rcatです。
前回は任意レイアウトでのGUIをスマホにもっていくところまでを行いました。
ここからは今回の目的であるバックアップアプリを作っていきます。
まず、最初にguiレイアウトの方を深掘りして作ってしまいましょう。

本シリーズはこちら


はじめに

GUIレイアウトの検討

まずはパワーポイントでどういう感じのguiにしようか考えてみました。

それぞれの要素の説明

ボックスレイアウトを四角い図形を並べて表しています。
大体こんな感じのレイアウトで作成できればなというイメージです。とりあえずこの時点でのイメージなので、この先と変わる可能性はあります。

一番左側がメインメニューでボタンを押せば即バックアップできる便利な感じにしようと思います。

次の画面が設定ボタンを押した時の画面で、ここでフォルダーの割り当てを行っていきます。
プロファイルとはバックアップの設定のことで、いくつか保持できるようにしようと考えています。

最後にバックアップ開始時の画面で進行状況のログが表示されるようにしようかなと考えています。

GUIの作成

画面構成要素の決定

それでは早速KVファイルを作っていきましょう。
まずは画面を構成する部品を何で作るか決めます。
下記サイトを参考に部品を選びました
https://senablog.com/python-kivy-widget/

ざっとこんな感じでしょうか?
タイトルと情報に関してはラベル
プロファイルに関してはスピナー
バックアップの開始と設定についてはボタンを選びます。
ドロップダウンリストにはどうやら2種類あるみたいで、ドロップダウンとスピナーがあります。軽く見た感じスピナーの方が使いやすそうなので、今回はスピナーを使います。

KVファイルを作成

今回のKVファイルは次のようになっています。

<MainMenuScreen>:
    BoxLayout:
        orientation: 'vertical'
        padding: 5

        Label:
            text: "Rcat BackUP App"
            font_size: "30sp"
            size_hint_y: 0.15
        Spinner:
            size_hint_y: 0.05
            text: 'プロファイル'
            values: root.SpinnerItems

        Label:
            text: "プロファイル情報"
            font_size: "15sp"
            color:0,0,0,1
            id: ProfileInfo
            padding: 10 
            size_hint_y: 0.4
            canvas.before:
                Color:
                    rgba: 0.1,0.1,0.1, 1 
                Rectangle:
                    pos: self.pos
                    size: self.size

                Color:
                    rgba: 0.9,0.9,0.9, 1 
                Rectangle:
                    pos: self.x + 5, self.y + 5
                    size: self.width - 10, self.height - 10 
        Widget:
            size_hint_y: 0.1

        Button:
            text: "バックアップ開始"
            id: BackUpButton
            font_size: "20sp"
            size_hint_y: 0.1
            on_press:root.BackUpButton_Click()

        Button:
            text: "設定"
            id: ConfigButton
            font_size: "20sp"
            size_hint_y: 0.1  
            on_press:root.ConfigButton_Click()

細かい説明をしていると長くなりすぎてしまうので、前回紹介した箇所などは省きます。
今回はボックスレイアウト/垂直を使用して各マスの縦の大きさを調整することで、それっぽいレイアウトにしています
そこで使用しているのがsize_hint_yです。これは親要素のサイズに対して何%であるかで大きさを決めるもののようです。
例えばタイトルは15%、プロファイル情報は40%の高さサイズを与えています。
また、文字サイズについてパソコンでもスマホでも使用できるようにSPという単位を使いました。こうすることで、画面の解像度が違っても文字の大きさが大きく変わってしまうことはありません。
プロファイル情報については四角い枠で囲いたかったので、キャンバスというものを使ってみました。具体的には、塗りつぶししかできないので、最初に枠の色で全体を塗りつぶした後、一回り小さい四角形を背景色で塗りつぶすという方法をとっています。
最後にスピナーですが、選択肢はvaluesみたいです。
Pythonと連携する予定なので、前回のボタンを押したら文字が変化するラベル同様変数を割り当てておきます。

Python側を作成

Python側のコードは次のようになっています。
インポートは今までの分全部書き書いてあるので多分過剰です。

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import StringProperty,ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import ScreenManager, Screen

class MainMenuScreen(Screen):
	SpinnerItems  = ListProperty(["ねこ","こねこ","CAT"])

	def BackUpButton_Click(self):
		self.SpinnerItems.append("バックアップ開始")

	def ConfigButton_Click(self):
		self.SpinnerItems.append("設定開始")

#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=
class backpuappApp(App):
	def build(self):
		self.title = 'RcatApp'
		S = ScreenManager()
		S.add_widget(MainMenuScreen(name="mainmenu"))
		return S

まだこの時点では触れられていないのですが、バックアップ画面と設定画面を推移する必要があるので、Screenを使用しています。そのため、メインクラスのビルド関数の中身が少々変わっておりますが、今は気にしないでください。
さて、画面を構成する方のクラスですが、前回のボタンを押したらラベルの文字が変わるのと大差ありません。
変化しているのはスピナーを使っているので、スピナーが使うプロパティを宣言しているところです。スピナーはリストプロパティを使用し、引数にリストを受け取ります。また、このリストに追加することで動的に内容を変更できます。今回はボタンを押すと、そのボタンの内容がリストに追加されるという意味不明なコードにしてみました。

実際に起動してみる

というわけでパソコンで起動してみます。
大体思い通りのレイアウトとなっているようです。ひとまずはこれで進めていきます。

スピナーを開いたのがこんな感じ。うまく動作しているようです。
ちなみにスピナーが下に開くことに気づいてからレイアウトを変更し、スピナーを上に持って行きました。
若干残念なのがこのスピナーから出てくる選択肢が他のウィジェットと見た目がほぼ一緒なので、違いが分かりづらいのが残念ですね。HTMLのセレクトボックス的だったらすごく良かったのですが…。

次にあまり進めすぎて、スマホにインストールしたら動かなかったとなると何が悪いのかわからなくなるので、とりあえず現状でインストールします。
表示されたのがこちら当初のレイアウトと比較してもそれほど悪くない感じです。

2画面目の作成

設定画面

さて、1つ目の画面ができたところで、次は設定画面を作っていきます。
とりあえず完成形はこのような感じになります。

簡単に説明すると、まず最初にクライアント名、パスワード、サーバーなどの入力情報があります。ここはボックスレイアウトの入れ子になっており、テーブルレイアウトを可能にしています。
その下にプロファイルです。
フォルダを選択すると、中央のリストにフォルダが追加されていきます。左側のチェックを外すことで、保存時に不要なフォルダを消すこともできます。ここもボックスレイアウトの入れ子でできています。

KVファイルの方はこのようになっています。
単純に長々しいだけで特に新しい要素はありません。

<ConfigMenuScreen>:
    BoxLayout:
        orientation: 'vertical'
        padding:5
        
        Label:
            id:title
            text: "設定"
            font_size: "30sp"
            size_hint_y: 0.15
        BoxLayout:
            orientation:"horizontal"
            size_hint_y: 0.05
            Label:
                text:"クライアント名"
                font_size: "15sp"
                size_hint_x: 0.4
            TextInput:
                id:ClientNameBox
                text: ''
        BoxLayout:
            orientation:"horizontal"
            size_hint_y: 0.05
            Label:
                text:"パスワード"
                font_size: "15sp"
                size_hint_x: 0.4
            TextInput:
                id:PasswdBox
                text: ''
        BoxLayout:
            orientation:"horizontal"
            size_hint_y: 0.05
            Label:
                text:"サーバー"
                font_size: "15sp"
                size_hint_x: 0.4
            TextInput:
                id:ServerAddrBox
                text: ''
        Spinner:
            size_hint_y: 0.05
            text: '新規作成'
            id:Profiles
            values: root.SpinnerItems
            on_text:root.Spinner_Change()
        BoxLayout:
            orientation: 'vertical'
            id:DirList
            size_hint_y: 0.4
        Button:
            text: "フォルダを選択"
            id: DirSelectButton
            font_size: "20sp"
            size_hint_y: 0.1
            on_release:root.DirSelectButton_Click()
        Button:
            text: "保存"
            id: SaveButton
            font_size: "20sp"
            size_hint_y: 0.1
            on_release:root.SaveButton_Click()
        Button:
            text: "キャンセル"
            id: ExitButton
            font_size: "20sp"
            size_hint_y: 0.1  
            on_release:root.ExitButton_Click()

スマホで表示するとこんな感じ。
上手く行っていますね!

フォルダ選択画面

フォルダを選択画面はこのような感じです。
標準で搭載されている"FileChooserIconView"を使用しています。
ちなみに本来はファイルを選ぶためのもので、フォルダを選ぶことはできません。
そのため、ちょっと工夫してフォルダを選択ボタンを設けることで、今表示されているフォルダを選択したことにできるようにしてます。
この画面でフォルダを選択すると、前画面のリストにフォルダが追加されます。

フォルダ選択画面のKVは以下の通りです。
ポイントとしてはAndroidをターゲットに考えているのでFileChooserIconViewのルートパスを内部ストレージに設定しているところです。まだ完全に理解はしていないですが、これはAndroidの時のみなので、Windowsの場合は普通にカレントディレクトリのルートドライブから始まります。余裕があればもう少し進めていきたいですね。

<FileSelectScreen>:
    BoxLayout:
        orientation:'vertical'
        Label:
            id:title
            text: "フォルダを選択"
            font_size: "30sp"
            size_hint_y: 0.1
        Label:
            id:cd
            text: root.DirPath
            font_size: "10sp"
            size_hint_y: 0.05
        FileChooserIconView:
            id:GetOpenDirName
            size_hint_y: 0.65
            rootpath:'/storage/emulated/0/' if platform == 'android' else '/'
        Button:
            text: "このフォルダを選択"
            id: SelectButton
            font_size: "20sp"
            size_hint_y: 0.1
            on_release:root.SelectButton_Click()
        Button:
            text: "キャンセル"
            id: ExitButton
            font_size: "20sp"
            size_hint_y: 0.1  
            on_release:root.ExitButton_Click()

次にPythonの方です
とはいえ、長くなりすぎてしまうのでポイントに絞って解説します。
そのため、これから書くコードは一部が省略されている。
全体が欲しい方はアプリの配布が始まり次第、ソースコード配布を受けてほしい

S = ScreenManager()

class ConfigMenuScreen(Screen):
	SpinnerItems = ListProperty(["新規作成"])

#-------------------------------------------------------------------------------------------
	def on_enter(self):
		"画面切り替え時のイベント(kvファイルではなくここに書くだけでいい)"
		global SCREEN_TMPDATA
		if SCREEN_TMPDATA:
			self.getpath = SCREEN_TMPDATA
			self.SelectedProfile_Dirs.append(SCREEN_TMPDATA)
			self.DirListUpdate(self.SelectedProfile_Dirs)
			SCREEN_TMPDATA = None

	def DirSelectButton_Click(self):
		"フォルダ選択ボタンクリック時"
		S.current="getopendirname"

	def DirListUpdate(self,Dirlistdata):
		"フォルダリスト更新関数"
		self.rows = []
		self.ids.DirList.clear_widgets()
		if Dirlistdata:
			for d in Dirlistdata:
				NewRow = DirList_row()
				NewRow.text = d
				self.rows.append(NewRow)
				self.ids.DirList.add_widget(NewRow)

	def SaveButton_Click(self):
		"保存ボタンクリック時"
		ActiveDirs = []
		for r in self.rows:
			if r.ids.chk.active:
				ActiveDirs.append(r.ids.path.text)
		if ActiveDirs:
			Config.AddProfile(self.SelectedProfile_Name,ActiveDirs)
		else:
			if self.SelectedProfile_Name in Config.Config["profiles"]: 
				del Config.Config["profiles"][self.SelectedProfile_Name]
				self.SpinnerItems.remove(self.SelectedProfile_Name)

		Config.SaveConfig()
		Config.LoadConfig()
		S.current="mainmenu"

class FileSelectScreen(Screen):
	DirPath = StringProperty()

	def __init__(self, **kw):
		super().__init__(**kw)
		self.DirPath = ""
#-------------------------------------------------------------------------------------------
	def on_enter(self):
		"画面切り替え時のイベント(kvファイルではなくここに書くだけでいい)"
		global SCREEN_TMPDATA
		SCREEN_TMPDATA = None
		
#-------------------------------------------------------------------------------------------
	def SelectButton_Click(self):
		global SCREEN_TMPDATA
		self.DirPath = self.ids.GetOpenDirName.path
		SCREEN_TMPDATA = self.DirPath
		S.current="configmenu"
#-------------------------------------------------------------------------------------------
	def ExitButton_Click(self):
		S.current="configmenu"


class backpuappApp(App):

	def build(self):
		self.title = 'RcatApp'
		S.add_widget(MainMenuScreen(name="mainmenu"))
		S.add_widget(ConfigMenuScreen(name="configmenu"))
		S.add_widget(FileSelectScreen(name="getopendirname"))
		return S

まず一番気になるであろう画面推移でのデータのやり取りについてですが、推移用のグローバル変数を用意しています。そして、画面推移には専用のイベントがあります。このイベントをソースの方に入れておくことで、画面が変わった瞬間に値を取りに行くということができるわけです。
なお、画面推移に関しては全体で扱えるようにグローバルの一番最初でオブジェクトの宣言を変更しました。
画面推移を行うにはScreenManager.current="画面の名前"を実行します。
フォルダを選択する仕組みは本来ダブルクリックでファイルを選択するところ、ボタンを自作して無理やり終了するという仕組みです。
終了ボタンを押した時、カレントディレクトリを取得することで、擬似的にフォルダを選択するといったことを行っています。

まとめ

今回はメインメニューおよび設定画面の作成を行いました。
調べながらなので仕方ないですが、1画面で1日くらい時間がかかっているので正直すごく大変です。コードが回りくどいというか、JavaScriptとHTMLがすごく優秀だなと感じてしまいます。
とはいえ、大体やりたいことができてきたので、そろそろ機能面の方に入っていこうと思います。
次回はバックアップ機能の検討の方を進めていこうと思います。では、またお会いしましょう。

情報が役に立ったと思えば、僅かでも投げ銭していただけるとありがたいです。