【Python】pythonファイルを一覧化・選択・実行できるツールをtkinterで作成したこと＆引っ掛かった部分について

#1.この記事について
##きっかけ
最近pythonで実装したツールをいくつか作った。
コンソール上からの起動にもダブルクリック起動にも対応できるようにしたので、
windowsショートカットに設定して（後述）、便利に起動できるようにしたかった（いずれスクレイピングツールも作成予定なので、その準備のためにも）。

ただ、拡張子pyのファイルひとつひとつにショートカットを設定しているといずれバッティングを起こす上に不用意な起動をしかねないので、
設定したフォルダ下のpythonファイルを一覧化表示し、そこから選択して実行できるGUIツールを作成することにした。

pythonのGUI作成にはtkinterという便利なライブラリがあると聞いたので、それを利用する。

##前提
拡張子pyまたはpywのファイルをpython.exe（あるいはpythonw.exe）と関連付けしてある状態であること。
関連付けとは　→　[http://www.first-pclife.com/pckiso/kanrenduketoha.html](http://www.first-pclife.com/pckiso/kanrenduketoha.html)


######[※最新のツールとソースコードはGitHubに上げてます（後述）](#5終わりに)

#2.やりたいこと
・指定したフォルダの下のpythonファイルを再帰的に検索し、リストなどのデータ型に格納
・データを表に一覧化表示するGUIを作成
・表から選択したファイルを実行する関数を実装
・上記条件のツールをwindowsショートカットに設定して起動できるようにすること

#3.使用したツール・環境
・Windows10
・Python 3.7.0

#4.作成したコードと解説

##外観
起動したらこんな感じ。

![Pythonプログラム実行アシスト 2020_01_02 9_18_02 - コピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541790/67d04f80-10c2-d4e1-ee28-210b72309d5d.png)

実行ボタン押下で指定したpythonファイルを実行する（ダブルクリック起動と同じ挙動）。

##コード

```py:CallPythonFile.py
import glob
import os
import subprocess
import tkinter as tk
import tkinter.ttk as ttk


def main():
    # ルートフレームの作成
    root = tk.Tk()
    root.title(u"Pythonプログラム実行アシスト")
    # rootウィンドウの大きさ設定
    root.geometry("1200x500")

    # 選択可能なpythonファイルを辞書の値として設定
    # 指定フォルダ直下のすべてのpythonファイル（再帰的に検索）
    myPath = r'C:\Users\UserName\Test\ToolBox'
    myDic = {}
    if os.path.isdir(myPath):
        for file in glob.glob(myPath + '\\**', recursive=True):
            nameRoot, ext = os.path.splitext(os.path.basename(file))
            if os.path.isfile(file) and ext in ('.py', '.pyw') and nameRoot != '__init__':
                myDic[os.path.abspath(file)] = os.path.basename(file)

    # Frame設定
    frame1 = ttk.Frame(root, width=1200, height=300, padding=10)
    frame1.grid()
    frame1.columnconfigure(0, weight=1)
    frame1.grid_propagate(False)

    # //////ツリービューの設定
    tree = ttk.Treeview(frame1, height=8, selectmode="browse")

    # 列インデックスの作成
    tree["columns"] = (1, 2)

    # 表スタイルの設定
    tree["show"] = "headings"

    # 各列の設定
    tree.column(1, width=250)
    tree.column(2, width=1000)

    # 各列のヘッダー設定
    tree.heading(1, text="ファイル名")
    tree.heading(2, text="パス")

    # レコードの作成
    for k, v in sorted(myDic.items(), key=lambda x: x[1]):
        tree.insert("", "end", values=(str(v), str(k)))

    # ツリービューの配置
    tree.grid(row=0, column=0, columnspan=2, padx=5, pady=5, sticky=tk.N + tk.S + tk.E + tk.W)
    # //////////////////////

    # スクロールバーの設定
    hscrollbar = ttk.Scrollbar(frame1, orient=tk.HORIZONTAL, command=tree.xview)
    vscrollbar = ttk.Scrollbar(frame1, orient=tk.VERTICAL, command=tree.yview)
    tree.configure(xscrollcommand=hscrollbar.set)
    tree.configure(yscrollcommand=vscrollbar.set)
    vscrollbar.grid(row=0, column=1, columnspan=1, padx=5, pady=5, sticky=tk.NS)
    hscrollbar.grid(row=1, column=0, columnspan=1, padx=5, pady=5, sticky=tk.EW)

    # ボタンの作成
    button = tk.Button(text="実行", command=lambda: RunPythonFile(root, tree))
    # ボタンの配置
    button.grid(padx=5, pady=5, )

    root.mainloop()


def RunPythonFile(root, tree):
    # 表から選んだファイルパスを取得
    selectedItems = tree.selection()
    filePath = tree.item(selectedItems[0])['values'][1]

    # フレーム閉じる
    root.destroy()

    # pyファイル呼び出し実行
    subprocess.check_call(['python', filePath])


if __name__ == '__main__':
    main()

```

##解説

```
for file in glob.glob(myPath + '\\**', recursive=True):
```
globメソッドを使えば結構楽に再帰的検索が実装できる。
使用の際は親フォルダのパスに＊＊を付与し、recursiveをtrueにする。
<br>

```
if os.path.isfile(file) and ext in ('.py', '.pyw') and nameRoot != '__init__':
```
フォルダは除いて、拡張子がpyあるいはpywであるもののみ抽出。
init.pyはいらないので除く。
<br>

```
tree = ttk.Treeview(frame1, height=8, selectmode="browse")
```
スクロールバーと連携するため、frameオブジェクトの子オブジェクトとしておく。
selectmode="browse"にすると、作成された表から一項目しか選択できないようにできるので便利。

<br>

```
    for k, v in sorted(myDic.items(), key=lambda x: x[1]):
        tree.insert("", "end", values=(str(v), str(k)))
```
valueの昇順ソートをしたもの（戻り値はリスト）に対してループして
項目を取り出している。
こちらを参考にさせていただきました。
[https://qiita.com/shizuma/items/40f1fe4702608db40ac3](https://qiita.com/shizuma/items/40f1fe4702608db40ac3)

<br>

```
button = tk.Button(text="実行", command=lambda: RunPythonFile(root, tree))
```
無名関数lambdaをかませない場合（commandに直接関数を指定する場合）、GUI表示時に関数が実行してしまうのでNG。

<br>

```
filePath = tree.item(selectedItems[0])['values'][1]
```
選んだすべての項目selectedItemsの最初の項目が実際に選んだ項目。
複数選択ありなら[1]以降も存在するが、selectmode="browse"をしてあるので一つだけ。
['values'][1]は、valueのリスト（ファイル名とファイルパス）からファイルパスを取り出している。

<br>

##引っ掛かった部分

```
    # スクロールバーの設定
    hscrollbar = ttk.Scrollbar(frame1, orient=tk.HORIZONTAL, command=tree.xview)
    vscrollbar = ttk.Scrollbar(frame1, orient=tk.VERTICAL, command=tree.yview)
    tree.configure(xscrollcommand=hscrollbar.set)
    tree.configure(yscrollcommand=vscrollbar.set)
    vscrollbar.grid(row=0, column=1, columnspan=1, padx=5, pady=5, sticky=tk.NS)
    hscrollbar.grid(row=1, column=0, columnspan=1, padx=5, pady=5, sticky=tk.EW)
```
スクロールバーの設定はかなり大変だった。
最初は各コンポーネントを配置するのにpack()メソッドを使っていたけど、
それではどうにもうまくいかない。

次にgird()メソッドを試してみた。
しかし、やっぱりずれる（x軸スクロールはうまくいってもy軸はできない。→　y軸がうまくいったがx軸が。。。）

最終的に、下記の記事を見つけた。
[Python 3.x - python ttk treeviewで背景色が変更にならない｜teratail](https://ja.stackoverflow.com/questions/56104/treeview%E5%86%85%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E5%9B%BA%E5%AE%9A%E5%B9%85%E3%81%AB%E3%81%97-%E6%A8%AA%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%90%E3%83%BC%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%97%E3%81%9F%E3%81%84)

こちらを参考にさせていただいて、どうにか次のように実装したらうまく行った。

```
    # Frame設定
    frame1 = ttk.Frame(root, width=1200, height=300, padding=10)
    frame1.grid()
    frame1.columnconfigure(0, weight=1)
    frame1.grid_propagate(False)

```
packメソッドでうまく実装する方法を御存知の方がいたらぜひ教えてほしいです。

##windowsショートカット登録

①下記のフォルダにCallPythonFile.pyのショートカットを格納する

```
C:\ProgramData\Microsoft\Windows\Start Menu\Programs
```

②ショートカットを右クリして、
「ショートカットキー」項目に好きなキーを指定する。

③キーを押下して、GUIが表示されることを確認する。

#5.終わりに

最新のツールとソースコードはこちら↓
[https://github.com/dede-20191130/CreateToolAndTest/tree/master/Tool_Python/CallPythonFile](https://github.com/dede-20191130/CreateToolAndTest/tree/master/Tool_Python/CallPythonFile)

なにか補足がありましたらコメントください。