0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】記事に載っていない appconfig.py を作る

0
Posted at

はじめに

「趣味や仕事でサクッと使えるPython Tips玉手箱」で公開されている プログラム・ランチャー を自分の環境で動かしてみました。

ところが、この記事のソースコード(launcher.py)は appconfig.py を import しているのに、appconfig.py の中身が記載されていません(共通パッケージのzipに含まれているようです)。

この記事では次の内容をまとめます。

  • 記事に載っていない appconfig.py を自作する
  • 動かしたときに出た No module named 'tkinter' エラーの対処
  • run_command 関数が何をしているか
  • フォルダやWebサイトを開くボタンの登録例

launcher.py 本体は元記事の著作物のため、この記事には転載しません。元記事からダウンロードするか、ソースを参照してください。

動作環境

  • Windows
  • Python 3.12(python.org の公式インストーラー版)
  • customtkinter / tkinterdnd2
pip install customtkinter tkinterdnd2

ランチャーの概要

メニューと設定の2画面構成で、設定画面に次の形式で1行ずつ書くと、メニューにボタンが生成されます。

タイトル,コマンド1&コマンド2&コマンド3

入力内容は、ウィンドウを閉じるときにJSONファイルへ自動保存され、次回起動時に復元されます。この保存と読込を担当するのが appconfig.py です。

appconfig.py を作る

launcher.py から必要なインターフェースを読み取る

launcher.py で AppConfig が使われている箇所は次のとおりです。

from appconfig import AppConfig

conf = AppConfig("./config.json")        # ① 設定ファイルのパスを渡してインスタンス生成

conf.set("textbox", textbox.get('1.0', 'end-1c'))  # ② 値を設定
conf.write()                             # ③ ファイルへ保存

conf.read()                              # ④ ファイルから読込
txt = conf.get("textbox", "")            # ⑤ 値を取得(無ければ既定値 "")

つまり、必要なのは次の5つです。

項目 役割
AppConfig(path) 設定ファイルのパスを受け取る
set(key, value) メモリ上に値を保持する
write() JSONファイルに保存する
read() JSONファイルから読み込む
get(key, default) 値を返す(無ければ default)

完成したコード

初回起動時は config.json がまだ存在しないため、read() は ファイルが無くてもエラーにしない ようにしています。また、保存途中にアプリが落ちても既存の設定が壊れないよう、一時ファイルに書き込んでから置き換える方式にしました。

"""
appconfig.py
画面に入力された値をJson形式のファイルに保存/読込するクラス
"""

import json
import os
import tempfile


class AppConfig:
    """設定値をJsonファイルへ保存/読込するクラス"""

    def __init__(self, filepath="./config.json", encoding="utf-8"):
        self.filepath = filepath
        self.encoding = encoding
        self.data = {}

    def read(self):
        """
        設定ファイルを読み込む。
        ファイルが無い場合や中身が壊れている場合は空の設定にする。
        読み込みに成功したら True、それ以外は False を返す。
        """
        if not os.path.isfile(self.filepath):
            self.data = {}
            return False

        try:
            with open(self.filepath, "r", encoding=self.encoding) as f:
                loaded = json.load(f)
        except (json.JSONDecodeError, OSError, UnicodeDecodeError):
            self.data = {}
            return False

        # トップレベルが辞書でない場合は不正な形式として扱う
        if not isinstance(loaded, dict):
            self.data = {}
            return False

        self.data = loaded
        return True

    def write(self):
        """
        設定をJsonファイルに保存する。
        一時ファイルに書き込んでから置き換えるため、
        書き込み途中で異常終了しても既存の設定ファイルが壊れにくい。
        """
        dirpath = os.path.dirname(os.path.abspath(self.filepath))
        os.makedirs(dirpath, exist_ok=True)

        fd, tmp_path = tempfile.mkstemp(dir=dirpath, suffix=".tmp")
        try:
            with os.fdopen(fd, "w", encoding=self.encoding) as f:
                json.dump(self.data, f, ensure_ascii=False, indent=4)
            os.replace(tmp_path, self.filepath)
        except Exception:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)
            raise

    def get(self, key, default=None):
        """キーに対応する値を返す。キーが無ければ default を返す。"""
        return self.data.get(key, default)

    def set(self, key, value):
        """キーに値を設定する(write() を呼ぶまでファイルには保存されない)。"""
        self.data[key] = value

    def remove(self, key):
        """キーを削除する。キーが無くてもエラーにしない。"""
        self.data.pop(key, None)

    def clear(self):
        """設定をすべて消去する(メモリ上のみ)。"""
        self.data = {}

    def keys(self):
        """登録されているキーの一覧を返す。"""
        return list(self.data.keys())

    def __contains__(self, key):
        return key in self.data

配置場所

launcher.py の冒頭に次の行があります。

sys.path.append(os.path.join(os.path.dirname(__file__), '../libs'))

このため、appconfig.py は launcher フォルダと同じ階層にある libs フォルダ に置きます。

PyTools/
├── launcher/
│   └── launcher.py
└── libs/
    └── appconfig.py

保存されるJSON

launcher.py が保存するのは textbox というキー1つだけで、config.json は次のようになります。日本語はそのまま読める形(ensure_ascii=False)で保存しています。

{
    "textbox": "フォルダコピー,cd ..\\folder_copy&python ui.py\nPDF結合,cd ..\\pdfjoiner&python main.py"
}

動作確認

次のケースを確認しました。

  • config.json が無いとき、read() は False を返し、get() は既定値を返す
  • 日本語を含む文字列を write() して read() し直すと、内容が一致する
  • JSONが壊れているとき、または中身が辞書でないときは、例外にならず空の設定になる
  • 保存後に .tmp ファイルが残らない

ハマりどころ: No module named 'tkinter'

実行すると、次のエラーが出ました。

  File ".../site-packages/customtkinter/__init__.py", line 6, in <module>
    from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar
ModuleNotFoundError: No module named 'tkinter'

原因は Python本体に tkinter が入っていなかったこと です。customtkinter は tkinter の上で動くため、その手前で止まります。appconfig.py とは無関係です。

tkinter は pip では入りません。Python のインストール時にオプションが外れていた場合は、次の手順で追加します。

  1. 「設定」→「アプリ」→「インストールされているアプリ」で Python 3.12 を選び「変更」
  2. Python Setup で Modify を選択
  3. tcl/tk and IDLE にチェックを入れる
  4. Next → Install

確認は次のコマンドです。小さな空ウィンドウが表示されれば成功です。

python -m tkinter

Microsoft Store 版のPythonで解決しない場合は、python.org の公式インストーラーから入れ直すのが確実です。元記事で紹介されている WinPython には、最初から tkinter が含まれています。

run_command は何をしているか

ボタンを押したときにコマンドを実行する部分は、次の2行です。

def run_command(cmd):
    subprocess.Popen(cmd.strip(), shell=True)
部分 意味
cmd.strip() 前後の空白や改行を取り除く
shell=True cmd.exe 経由で実行する。cd などの組み込みコマンドや、& による連結が使える
subprocess.Popen 終了を待たずに起動する。ランチャーの画面が固まらない

つまり、フォルダコピー,cd ..\folder_copy&python ui.py のボタンは、コマンドプロンプトに cd ..\folder_copy&python ui.py と入力したのと同じ動きになります。

注意点は2つです。

  • shell=True は文字列をそのままコマンドとして実行します。自分で書いた内容だけを使い、出所の分からない config.json は中身を確認せずに使わないでください。
  • Popen は結果を受け取らないため、失敗してもランチャーにエラーは出ません。動かないときは、同じコマンドをコマンドプロンプトで直接実行して原因を確認します。

登録例

フォルダを開く

explorer コマンドを使います。

ドキュメント,explorer C:\Users\ユーザー名\Documents
共有資料,explorer "D:\My Documents\共有資料"
共有サーバー,explorer \\server\share\docs

パスにスペースが含まれる場合は、ダブルクォーテーションで囲みます。相対パスはランチャーを起動したカレントディレクトリが基準になるため、確実に開きたいときはフルパスで書きます。

ファイルを開く

start を使うと、関連付けられたアプリで開きます。

議事録,start "" "D:\work\議事録.xlsx"

start の直後の "" はウィンドウタイトルの指定位置で、空のまま置くのが決まりです。

Webサイトを開く

URLを start に渡すと、既定のブラウザで開きます。

Google,start "" https://www.google.com

ブラウザを指定する場合は、次のように書きます。

Chrome,start chrome https://www.google.com
Edge,start msedge https://www.google.com
Firefox,start firefox https://www.google.com

URLに & が含まれる場合は、^& とエスケープし、URL全体をダブルクォーテーションで囲みます。

天気検索,start "" "https://www.google.com/search?q=天気^&hl=ja"

複数をまとめて開く

& でつなぐと、1つのボタンで複数を一度に開けます。

朝のチェック,start "" https://mail.google.com&start "" https://calendar.google.com
今日の作業,explorer C:\work\a&explorer C:\work\b

書式の注意

  • タイトルにカンマ , は使えません。最初のカンマでタイトルとコマンドを分けるためです。コマンド側にカンマがあっても問題ありません。
  • 1行につき1ボタンです。途中で改行しないでください。
  • 登録後は「編集内容を画面に反映」ボタンを押します。

まとめ

  • 元記事の launcher.py は appconfig.py を import していますが、中身は記載されていません。
  • AppConfig に必要なのは read / write / get / set の4つで、JSONの読み書きだけで実装できます。
  • No module named 'tkinter' は、Python側に tcl/tk が入っていないことが原因で、インストーラーの Modify で追加できます。
  • explorer と start を使えば、フォルダ・ファイル・Webサイトも登録できます。

ランチャーが便利だったので、自分の使うフォルダやサイトもまとめて登録しておくと、作業の起動がかなり楽になりました。

参考

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?