2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-07-05

本記事の概要

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

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

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

コード

機能拡張を行いました!

TODOアプリケーション(分散型)とは

見た目

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

todo.png

todo.gif

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

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

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

機能

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

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

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

function_一覧表示.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

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

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

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

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

function_詳細表示.png

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

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

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

使い方

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

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

python display.py

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

[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にしてください。詳細画面が表示されません。

実装について

ファイルの検索

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検索

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**というpythonに組み込まれているモジュールを使用し、設定ファイルから値を読み取ります。

search_fileは全体の検索、limit_search_fileは指定したフォルダのみを検索します。

todo表示

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のオブジェクトパーツ

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アプリをこれから使っていって便利に出来たらなと思います。

2
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?