Pythonのみを使って、今TODOアプリケーション（分散型）を作るとしたら

[1]:https://github.com/The-town/qiita/tree/master/todo
[2]:https://docs.python.org/ja/3/library/configparser.html?highlight=configparser#configparser.ConfigParser
[3]:https://qiita.com/The-town/items/b7236e58ebae198cf7d1

## 本記事の概要

本記事はQiita夏祭り2020の 「 〇〇（言語）のみを使って、今△△（アプリ）を作るとしたら」 を
テーマにして作成した**TODOアプリケーション（分散型）**の紹介、実装方法説明を行うものです。

使用しているのは**Python3（ver 3.7）**であり、Windows10上で動作確認をしております。

コードは以下のGitHubに配置しております。ご自由にお使いください。

[コード][1]

[機能拡張を行いました！][3]

## TODOアプリケーション（分散型）とは

### 見た目

以下のようなアプリケーションです。

![todo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/e6a4ee6c-fc46-8887-242a-8efe3ede2043.png)

![todo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/9d55e97d-3c8c-57af-ca02-68efb4b1c506.gif)

TODOリストを表示する箇所が下部にあります。

特定のTODOを表示するためにプルダウンから文字列を選択でき、更新ボタンを押すことで表示されます。

TODOリストを選択すると、リストのさらにしたに詳細な内容を表示します。

### 機能

大きく3つの機能を実装しています。

* 複数のフォルダに格納されたTODOファイルを一覧表示する。
* 特定のフォルダに格納されたTODOのみを一覧表示する。
* TODOファイルの詳細内容を表示する。

#### 複数のフォルダに格納されたTODOファイルを一覧表示する

![function_一覧表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/59f74020-f277-09e3-d952-8851b630a09d.png)


この機能は指定した複数のフォルダに保存されているTODOファイルを検索し、画面上に一覧表示するものです。

例えば**F:\Document\Qiita**というフォルダの中に**pythonのみで作ったtodoアプリを投稿.txt**という**TODOファイル**を格納したとします。
このファイルにはTODOの詳細内容、今執筆中の記事を投稿するということと、いつまでに投稿する等と言った期限を書いています。
*__TODOファイル__の内容は自由に記載できます。*

一方で、**F:\Document\python\django**というフォルダの中にも別の**TODOファイル**があったとします。

こう言った状況で困るのは、**TODOファイル**が**分散**してしまうという事です。

もちろん**TODOフォルダ**を作りその中に**TODOファイル**を入れればいいのですが、せっかく自分らしくフォルダ階層を作成して整理しているわけですから、関連するファイルは例え**TODOファイル**でも同じフォルダに入れたいです。
また、**TODOファイル**に調べた内容を追記したりと、重要な情報を記載することもあるでしょう。

上記の問題を解消するために、あえて**TODOファイル**が分散してもよいようにしました。

#### 特定のフォルダに格納されたTODOのみを一覧表示する

![function_特定のフォルダを一覧表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/27192eab-7b2e-051a-5ba4-d8aa93bd8b5a.png)


この機能は特定のフォルダにある**TODOファイル**のみを一覧表示するものです。

**TODOファイル**が多くなってきたときに、全ての**TODOファイル**の中から目的の**TODOファイル**を探すのは面倒です。

そのため、特定のフォルダに保存されている**TODOファイル**のみを表示できるように、プルダウンからフォルダを選択できるようにしました。

#### TODOファイルの詳細内容を表示する

![function_詳細表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/df78a2b5-c037-6bdf-5052-bae5b9247fcc.png)


この機能は表示された**TODOファイル**のリストをクリックすると画面下部に詳細内容を表示するものです。

詳細内容は**TODOファイル**に記載されている内容です。

基本的にファイル名で具体的な**TODO**（何をするのか）を書いておけば事足りるかもしれません。
ただ、進捗状況や調査内容などが見れたほうがよいかと思い実装しました。

### 使い方

本記事冒頭に記載した[GitHub][1]からコードをダウンロードし、1か所のフォルダに配置します。

**display.py**を実行すればGUIが起動します。

> python display.py

設定ファイルは**config.ini**という名前のファイルで以下のフォーマットで記載します。

```config
[Dir_names]
#プルダウンに表示される名前=TODOファイルを格納しているフォルダの絶対パス
#例
qiita=F:\Document\800_IT自己学習\09_python\51_Qiita

[File_names]
#TODOリスト一覧に表示したいファイル名。ワイルドカード(*)を使用できます。
#例

#先頭にtodoという文字列があるファイル
todo=todo*

#拡張子がpyのファイル
python=*.py
```

* **Dir\_names**には複数のフォルダ名を記載できます。
* **File\_names**には複数のファイル名を記載できます。
* ファイル名を記載する場合はワイルドカード（\*）を使用できます。
* ファイル名を複数記載した場合は**or**の検索になります。

**※注意点として、記載したフォルダ名配下に大量のフォルダ、ファイルがある場合、検索するのに時間がかかる場合があります。**
**極端な話ですがフォルダ名にC:\と記載すると、TODOリストを表示するまでにかなりの時間を要します。**

**TODOファイルの文字コードはUTF-8にしてください。詳細画面が表示されません。**

## 実装について

### ファイルの検索

```python:operate_file_1.py
import os
import fnmatch
#root:ディレクトリ名
#patterns:unixのシェル形式のワイルドカードへ対応。ファイル名のパターン
#yield_folders:サブディレクトリ配下のファイルを読み込むかどうか。

def all_files(root, patterns = "*", single_level=False, yield_folders=False):
#split:";"でパターンを複数指定できる。;はパターンに入れられない。
    patterns = patterns.split(";")

#os.walkで指定したディレクトリ以下のディレクトリパス名、ディレクトリ名、ファイル名を渡す。
    for path, subdirs, files in os.walk(root):
        if yield_folders:
            files.extend(subdirs)
        files.sort()
        for name in files:
            for pattern in patterns:
                #fnmatchはnameがpatternに一致する場合、Trueを返す。
                if fnmatch.fnmatch(name, pattern):
                    yield os.path.join(path, name)
                    break
        if single_level:
            break
```

Pythonクックブックに記載されていたコードになります。

> Pythonクックブック第2版（89ページ　2.16 ディレクトリツリーの探索） Alex Martelli, Anna Martelli Ravenscroft, David Ascher　著、鴨澤 眞夫、當山 仁健、吉田 聡、吉宗 貞紀、他　訳　出版社　オライリー・ジャパン　ISBN978-4-87311-276-3

実行していることは、受け取ったディレクトリ名、ファイル名から一致するものを探索し結果を返すということです。

### todo検索

```python:todo.py
from operate_file_1 import all_files
import configparser


class Todo:
    def __init__(self):

        self.rule_file = configparser.ConfigParser()
        self.rule_file.read("./config.ini", "UTF-8")

        self.dir_name_keys = list(self.rule_file["Dir_names"].keys())
        self.dir_names = [self.rule_file["Dir_names"][key] for key in self.rule_file["Dir_names"].keys()]
        self.patterns = [self.rule_file["File_names"][key] for key in self.rule_file["File_names"].keys()]

    def search_file(self):
        paths = {}
        for index, dir_name in enumerate(self.dir_names):
            paths[self.dir_name_keys[index]] = list(all_files(dir_name, ";".join(self.patterns)))
        return paths

    def limit_search_file(self, dir_name_key):
        paths = {}
        paths[dir_name_key] = list(all_files(self.rule_file["Dir_names"][dir_name_key], ";".join(self.patterns)))
        return paths

    def get_dir_name_keys(self):
        return self.dir_name_keys
```

こちらは**config.ini**から設定内容を読み取り、**operate_file_1.py**に記載した関数を使ってファイルを検索するクラスです。

**[configparser][2]**というpythonに組み込まれているモジュールを使用し、設定ファイルから値を読み取ります。

**search\_file**は全体の検索、**limit\_search\_file**は指定したフォルダのみを検索します。

### todo表示

```python:display.py
import tkinter as tk
import os
import datetime
from todo import Todo
from gui_object import Frame, Label, Listbox, Text, Button, Combobox


class TodoDisplay:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("todo")

        self.todo = Todo()
        self.todo_list_box_dict = {}

        self.todo_list_frame = Frame(self.root)
        self.todo_list_frame.grid(column=0, row=1)
        self.todo_detail_frame = Frame(self.root)
        self.todo_detail_frame.grid(column=0, row=2)
        self.function_frame = Frame(self.root)
        self.function_frame.grid(column=0, row=0)

        self.listbox = Listbox(master=self.todo_list_frame, master_of_detail_text=self.todo_detail_frame)

        self.refresh_button = Button(master=self.function_frame)
        self.refresh_button.grid(column=1, row=0)
        self.refresh_button["text"] = "更新"
        self.refresh_button["command"] = self.refresh

        self.combbox = Combobox(master=self.function_frame)
        self.combbox.grid(column=0, row=0)
        self.set_value_for_combbox()

    def display_todo(self):
        todo_list_box_id = 0
        self.todo_list_box_dict = {}

        if (self.combbox.get() == "all") or (self.combbox.get() == ""):
            paths = self.todo.search_file()
        else:
            paths = self.todo.limit_search_file(self.combbox.get())

        for key in paths.keys():
            for path in paths[key]:
                create_time, update_time = self.get_timestamp_of_path(path)
                insert_statement = " ".join(["作成", create_time, "更新", update_time, path.split("\\")[-1].split(".")[0]])
                self.listbox.insert(todo_list_box_id, insert_statement)
                self.todo_list_box_dict[todo_list_box_id] = path
                todo_list_box_id = todo_list_box_id + 1

        self.listbox.set_todo_list(self.todo_list_box_dict)

    def get_timestamp_of_path(self, path):
        stat_result = os.stat(path)
        create_time = datetime.datetime.fromtimestamp(stat_result.st_ctime).strftime("%Y/%m/%d %H:%M:%S")
        update_time = datetime.datetime.fromtimestamp(stat_result.st_mtime).strftime("%Y/%m/%d %H:%M:%S")

        return create_time, update_time

    def refresh(self, event=None):
        self.listbox.delete(0, "end")
        self.display_todo()

    def set_value_for_combbox(self):
        self.combbox["value"] = ["all"] + [dir_name.split("\\")[-1] for dir_name in self.todo.get_dir_name_keys()]

    def mainloop(self):
        self.root.mainloop()


if __name__ == "__main__":
    todo_display = TodoDisplay()
    todo_display.display_todo()
    todo_display.mainloop()
```

**Tkinter**のオブジェクトを使用して、**todo.py**で検索した**TODOファイル**の一覧を表示しています。


### GUIのオブジェクトパーツ

```python:gui_object.py
from tkinter import *
import tkinter as tk
import tkinter.ttk as ttk


class Frame(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master)
        self.grid(column=0, row=0)
        self["width"] = 100
        self["height"] = 100
        self["padx"] = 20
        self["pady"] = 20


class Button(tk.Button):
    def __init__(self, master=None):
        tk.Button.__init__(self, master)

        self["height"] = 2
        self["width"] = 20
        self["font"] = ("Helvetica", 15)


class RefreshButton(Button):
    def __init__(self, master=None,):
        Button.__init__(self, master)


class Combobox(ttk.Combobox):
    def __init__(self, master=None):
        ttk.Combobox.__init__(self, master)

        self["font"] = ("Helvetica", 20)


class Text(tk.Text):
    def __init__(self, master=None):
        tk.Text.__init__(self, master)
        self["width"] = 100
        self["height"] = 10


class Listbox(tk.Listbox):
    def __init__(self, master=None, master_of_detail_text=None):
        scrollbar = Scrollbar(master)
        scrollbar.pack(side=RIGHT, fill=Y)
        tk.Listbox.__init__(self, master, yscrollcommand=scrollbar.set, selectmode=EXTENDED)

        self.pack(side=LEFT, fill=BOTH)
        self["width"] = 100
        self["height"] = 10
        self["font"] = ("Helvetica", 12)
        self.master = master
        self.master_of_detail_text = master_of_detail_text
        self.text = Text(self.master_of_detail_text)

        scrollbar["command"] = self.yview
        self.bind("<Double-Button-1>", self.show_detail)
        self.bind("<Return>", self.show_detail)

        self.todo_list = {}

    def show_detail(self, event=None):
        self.text.destroy()
        self.text = Text(self.master_of_detail_text)
        self.text.insert(END, self.read_detail_of_todo(self.index(ACTIVE)))
        self.text.pack()

    def set_todo_list(self, todo_list_dict):
        self.todo_list = todo_list_dict

    def read_detail_of_todo(self, index):
        path = self.todo_list[index]
        with open(path, encoding="utf_8") as f:
            return f.read()
```

**display.py**で使用している**Tkinter**の各種オブジェクトについての設定をしています。

主に見た目などを調整しています。


## 終わりに

決められたテーマでアプリケーションを作るのが楽しく、幸せな時間でした。

このTODOアプリをこれから使っていって便利に出来たらなと思います。