📑

tkinter/customtkinterでMarkdown記法のファイルを表示するアイデア

2023/12/10に公開

記事の要約

pythonのtkinter/customtkinterでMarkdown形式のファイルを読み込みたいとき、それほど高機能でなくてもいい場合は自分でパーサーを書くのが一番シンプル。

あらすじ

  1. 現在私が趣味で作っているソフトはcustomtkinterでGUIを組んでいて、「使い方」タブがある
  2. 使い方はソフトの機能追加に合わせて更新したい、かつ多言語対応もしたいのでできれば大本のファイルをMarkdownで記述して、それをcustomtkinterに描画したい
  3. しかし、Markdownをtkinterに読み込むライブラリは(検索した感じでは)なく、多くはHTMLに一度変換してtkinterに表示する方法を取っている
  4. 自分でパーサーを書いたらまぁまぁうまくいった

提案

tkinterにMarkdown記法のファイルを読み込む方法は、あらすじで説明しているようにhtmlに一度変換するものがほとんどです。
ですが、これでは余計なライブラリのimportが必要で、またhtmlをtkinterに表示するライブラリは完全なhtmlレンダリングができるものは(当然)ありません。
よって、今回の私のように完全でなくても一部のMarkdown記法の再現ができればよいといった場面であれば、自前のパーサーを書いて表示させるのが最もシンプルであると思います。

tkinterでのサンプルコード

このclassをMarkdown2Tkinter(file_path, target_frame)として呼び出せばMarkdownファイルが描画されます。

class Markdown2Tkinter:
    def __init__(self, path, frame):
        self.markdown2Tkinter(path, frame)

    def markdown2Tkinter(self, path, frame):
            # markdownファイルの読み込み
            with open(path, 'r', encoding='utf-8') as f:
                text = f.read()

            # textを改行で分割
            text = text.split('\n')

            tkObject = []

            # textを1行ずつlineMdToTkinterに渡す
            for line in text:
                tkObject.append(self.line2Tkinter(line, frame))

            # tkObjectの要素を1つずつframeに追加
            for i in tkObject:
                i.pack(anchor="nw")

    def line2Tkinter(self, line, frame):
         # 構造化パターンマッチングを使って、textを解析
        match StrRe(line):
            # lineが#+半角スペースで始まる場合
            case "#+ ":
                # lineが#で始まる場合、#の数を取得
                count = line.count("#")

                # lineから#を削除
                line = line.replace("#", "")

                # lineの前後の空白を削除
                line = line.strip()

                # labelを作成
                label = Label(frame, text=line, font=("Noto Sans CJK JP", 15 + (6 - count) *3), justify="left")
                return label
            
            # lineが半角スペース*+*+半角スペースで始まる場合
            case "^ *\* ":
                # lineが*で始まる場合、*を・に置換
                line = line.replace("*", "・")

                # labelを作成
                label = Label(frame, text=line, font=("Noto Sans CJK JP", 15), justify="left")
                return label

            case "---":
                # lineが---で始まる場合、lineを作成
                canvas = Canvas(frame, width=500, height=1, background="gray30", highlightthickness=0)
                return canvas

            case r"!\[":
                # lineが![で始まる場合、()の中身を取得
                line = re.findall(r'\((.*?)\)', line)[0].replace(".\\", "")
                line = os.path.join(os.path.dirname(__file__), line)

                # lineから画像を読み込み
                readImage = Image.open(line)
                global image
                image = ImageTk.PhotoImage(readImage)

                # canvasを作成
                canvas = Canvas(frame, width=readImage.width, height=readImage.height, highlightthickness=0)

                # canvasに画像を描画
                canvas.create_image(0, 0, image=image, anchor="nw")
                return canvas

            case _:
                # labelを作成
                label = Label(frame, text=line, font=("Noto Sans CJK JP", 15), justify="left")
                return label

class StrRe(str):
    def __init__(self, var):
        self.var = var
        pass

    def __eq__(self, pattern):
        return True if re.search(pattern, self.var) is not None else False

global image

描画結果

今回は下部に記載するMarkdownファイルの表示を目標とすると、今回のサンプルコードを使った場合、この画像の様になります。
サンプル画像

また、実際のアプリ制作の際はcustomtkinter等を使うと思いますので、customtkinterで多少見栄えよくしたサンプル画像も載せておきます。

使用したサンプルMarkdownファイル

# TEST用Markdownファイル
テスト用のMarkdownファイルです。
---
* テスト
    * テスト
* テスト

![fox](./fox_3d.png)

使用しているライブラリ

画像の出自

https://github.com/microsoft/fluentui-emoji

参考にしたページ

https://qiita.com/Kasiri-git/items/53432af284832c9cc5b0

Discussion