目次
はじめに
前回の記事で、PythonでADB File Controllerなるアプリを作りました。Macに接続したAndroidデバイスからADBコマンドを利用してファイル情報を取得して表示したり、ダウンロードしたりできるアプリです。
それなりに動くものは作れたものの、まだまだ改善したいことがあったので、今回は気になる箇所を修正して最終的にはアプリ化まで実施したので、変更点とアプリ化の方法を紹介します。
変更点①:アイコンを表示
TKinterのTreeViewにアイコン表示
ディレクトリ構造ツリーのアイテム名の前にアイコンを表示するようにしました。TkinterのTreeViewでアイコンを表示するには、PhotoImageでアイコンを取り込んで、treeに渡すことで簡単に表示することができます。
まず、表示したいアイコンを用意してプロジェクト内に配置します。私はAssetsというフォルダを用意して、その中にpngファイルをすべて置きました。
from tkinter import ttk, PhotoImage
folder_icon = PhotoImage(file=resource_path("Assets/folder_16.png"))
tree.insert(item_id, 'end', text=item_name, values=item_infos, image=folder_icon) アイテムの種類は、フォルダ(閉じる/開く) + シンボリックリンク + ファイルの計4種類を用意し、adb ls -lコマンドで得られるtypeから表示するアイコンを切り替える処理を行います。フォルダのアイコンについては、ツリーの開閉に合わせて動的にアイコンを切り替えようにしていますが、TreeViewでは動的な変更にも対応してくれます。具体的には、変更したいツリーアイテムのIDと変更後の画像をPhotoImageをtree.itemの引数に渡すことで切り替えられるので、開閉のイベント発生時に動的に変更するようにしました。
tree.item(item_id, image=self.folder_icon) 今回利用したアイコンは16 x 16サイズを利用したので問題にならなかったのですが、大きなサイズの画像をアイコンにしようとすると、そのままでは表示が見切れてしまいます。なので、目的のサイズに変更してから利用すれば良いと思います(たぶん)。TKinterで画像のサイズを変更する方法は、こちらの記事が参考になりそうです。
右クリックメニューにアイコン表示
つづいて、右クリックで表示されるメニューにもアイコンを表示しました。メニューにアイコンを表示するには、メニューのadd_commandの引数でPhotoImageを渡すことで実現できます。ここで、compound="left" の指定がないとラベルなしのアイコンだけの表示になります。
from tkinter import Menu, PhotoImage
context_menu = Menu(self, tearoff=0)
context_menu.add_command(label='Android -> PC', command=self.on_select_download, image=self.download_icon, compound="left") アイコン画像の入手
アイコンはこちらのサイトで公開されているものを利用させていただきました。
変更点②:フォルダの新規作成
右クリックのメニューに[make folder]を選択できるようにして、任意のフォルダ内に新しいフォルダを作成できるようにしました。フォルダの作成はmkdirコマンドで実施します。
adb -s <デバイスID> shell mkdir <dstパス> 変更点③:シンボリックリンクの表示改善
リンク先のディレクトリ判定
ls -lコマンドを実行すると、パーミッションが”lrw-r—r—”と表示されるアイテムがあります。この最初に表示されている文字がタイプを表しており、’l’はシンボリックリンクを意味します。Windowsでいうところのショートカットです。このシンボリックリンクを表示するにあたり、リンク先がディレクトリかファイルかを判定する必要がありました。ディレクトリの場合はツリーを展開できるようにする必要があり、逆にファイルの場合は展開ができないようにしたいためです。シンボリックリンクはそれだけではリンク先を判断することができないので、ls -ldコマンドを利用して判定を行います。
adb -s <デバイスID> shell ls -ld <dstパス>/ ここで工夫するのが、パスの最後に必ず”/”をつけることです。”/”をつけると、そのパスはディレクトリであることを明示的に指示することになります。これによって指定パスがディレクトリの場合には情報が表示され、逆にファイルの場合はエラーとなります。
% ls -ld bin/
drwxr-x--x 4 root shell 8192 2009-01-01 09:00 bin/
% ls -ld link/
ls: link/: No such file or directory 大量ファイルの一括判定
シンボリックリンクの判定処理を行うにあたって、フォルダ内に大量にシンボリックリンクが含まれるケースで躓くことになりました。というのも、フォルダ内のファイルを一個ずつls -ldコマンドで確認すると秒オーダーの時間が掛かってしまうという問題が見つかったためです。リンク数にもよりますが10秒を超えるケースもありました。これを解決する手段として考えたのが、ls -ldコマンドの引数に複数のパスを渡すというものです。lsコマンドには複数のパスを渡すことができ、これらを一括して処理してくれます。この工夫によって10秒→1秒未満にまで短縮することができました。
adb -s <デバイスID> shell ls -ld <dstパス1>/ <dstパス2>/ <dstパス3>/ このコマンドで得られる出力は、指定したパスの順番とは関係なく出力されます。そのため、出力結果からディレクトリ名/フォルダ名を抽出する必要があります。出力から名前を抽出するには、正規表現を使うと簡単です。
pattern = r'(.)(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+(.+)/'
folder_info = re.findall(pattern, "drwxr-x--x 4 root shell 8192 2009-01-01 09:00 bin/")
print(folder_info[0][8]) # ディレクトリ名を表示 pattern = r'ls: (.+)/:.*'
file_info = re.findall(pattern, "ls: link/: No such file or directory")
print(file_info[0]) # ファイル名を表示 Pyinstallerでアプリ化する
Pyinstallerコマンド
作成したアプリを、MacのDockに入れていつでも実行できるようにするためにappにしてみました。app化するにあたり、利用したのはPyinstallerです。Pyinstallerの導入方法についての説明は省略しますが、以下のコマンドでapp化することができました。
pyinstaller --add-data "Assets:Assets" --add-binary "resources/adb:resources" --noconsole --icon=icon.icns ADBFileController.py - —add-dataオプションでアイコン画像が保存されているAssetsフォルダをアプリに取り込みます。
- —add-binaryオプションでadb実行ファイルをアプリに取り込みます。
- —noconsoleオプションで実行ファイルをバンドルします。
- —iconオプションでアプリアイコンを指定します。
コマンドで生成されたADBFileController.appは、Macのアプリケーションフォルダに入れることで、Launchpad上で普通のアプリと同じ用に利用できるようになります。
adb実行ファイルの取り込み
今回のapp化では--add-binaryオプションでadbコマンドの実行ファイルをアプリに直接埋め込みました。理由は、アプリ化するとadbコマンドのパスが見つからず、実行できない状況となったためです。この解決方法として、adbコマンドへの絶対パスを指定するという方法もありますが、将来的にコードをGithubで配信するとなった場合に、私の環境に依存する情報は埋め込みたくありませんでした。
そこで、adbの実行ファイルをwhichコマンドで探し、プロジェクト内のresourcesフォルダにコピーしました。また、adbのパスは開発環境とapp実行時で変化するため、パスは動的に取得するように変更しました。
% which adb
/Users/<user>/Library/Android/sdk/platform-tools/adb import sys
def get_resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
adb_path = get_resource_path("resources/adb") 設定ファイルの保存先変更
app化するにあたって、設定保存ファイルの保存先を変更しました。以前の保存先はプロジェクト内のsettingsフォルダに保存していたのですが、app化した時にフォルダへのアクセスエラーが発生しました。当然、appではプロジェクトというものが無いため、フォルダを作れないことが原因となります。実は、この問題の原因に気がつくまでに結構な時間が掛かりました。というのも、app化するとコンソールが表示されないためエラーの発生箇所がわからないためです。そこで、問題箇所を特定するために、Macのログを確認することにしました。
% log show --predicate 'eventMessage contains "ADBFileController"' --info --last 1h ログを確認することで設定ファイルの扱いが原因であることが特定できたので、設定ファイルの保存先に一般的にアプリが利用する”/Users/<username>/Library/Application Support”直下にアプリディレクトリを作成して保存することにしました。
アプリアイコン用意
Macアプリで表示するアイコンとして1024 x 1024のpngを用意しました。Mac上で動くAndroidのファイルビューアなので、安直にドロイド君にリンゴを持たせておきました。Appleロゴをもたせたアイコンも作ったのですが、Appleのロゴ使用ガイドラインに抵触することを恐れて、普通のリンゴにしました。
icns作成
pyinstallerでアプリアイコンを設定するにはicnsを作成する必要がありました。icon.iconsetというフォルダを用意して、その中に16x16から1024x1024までの複数種類のサイズのアイコンを格納します。1024 x 1024のicon.pngを用意したので、ターミナルでコマンドを実行することで、複数種類のアイコンが生成できます。画像が用意できたら、iconutilコマンドでicnsを生成します。
sips -z 16 16 icon.png --out icon.iconset/icon_16x16.png
sips -z 32 32 icon.png --out icon.iconset/icon_16x16@2x.png
sips -z 32 32 icon.png --out icon.iconset/icon_32x32.png
sips -z 64 64 icon.png --out icon.iconset/icon_32x32@2x.png
sips -z 128 128 icon.png --out icon.iconset/icon_128x128.png
sips -z 256 256 icon.png --out icon.iconset/icon_128x128@2x.png
sips -z 256 256 icon.png --out icon.iconset/icon_256x256.png
sips -z 512 512 icon.png --out icon.iconset/icon_256x256@2x.png
sips -z 512 512 icon.png --out icon.iconset/icon_512x512.png
sips -z 1024 1024 icon.png --out icon.iconset/icon_512x512@2x.png iconutil -c icns icon.iconset 作成したicnsは、pyinstallerコマンドで—iconオプションに使用しました。
まとめ
今回はアプリの細かい表示仕様にこだわって改造を行った。特に苦労したのはシンボリックリンクの取り扱いで、ディレクトリ判定と対象のシンボリックリンクを効率よく処理する方法を解決する方法を見つけるまでに多大な時間がかかった。そのかいあってアプリの表示は個人的には大満足な仕上がりとなった。また、アプリ化に当たっても、出力した実行ファイルであれば動作するが、バンドル化した途端に動作しないという現象が発生して、原因を特定するのにも苦労した。結果として設定ファイルの保存先に問題が見つかったのだが、この過程でMacのログを確認する方法を覚えたし、一般的なアプリの設定ファイルの保存先も知ることができたので結果的には良かったと思う。
ここまでで作成したソースコードですが、まだまだ公開できたものではないので、コードを見直して最終的にはGithubで公開したいと思います。

