🔡

Tkinter でルビを表示する

2024/06/30に公開

Python の GUI ライブラリである Tkinter でルビを表示したい場面に遭遇しました。Web であれば簡単に表現可能なのですが、残念ながら Tkinter にルビ機能は存在しないため、スペースを用いて擬似的に表現します。

親文字用、ルビ用の TextLabel を用意する

親文字を表示する parentLabel およびルビを表示する rubyLabel を用意します。rubyLabel のフォントサイズは parentLabel の半分にセットします。

import tkinter as tk

size = 24
parentLabel = tk.Label(Display.root, text="", font=(FONT, size))
rubyLabel = tk.Label(Display.root, text="", font=(FONT, size / 2))
parentLabel.place(x=140, y=80)
rubyLabel.place(x=140, y=80 - 12)

ルビ付きテキストをパースする

今回は以下のようにルビを表記するものとします。モノルビ(1 文字ずつルビを振る)、グループルビ(単語にまとめてルビを振る)の場合も表記は同じです。

[明日:あした]からスペースを[用:もち]いて[擬:ぎ][似:じ][的:てき]に[表:ひょう][現:げん]

これをパースし、辞書を要素とする配列に格納します。

def parse_assignment_text(cls, text):        
    words = []
    temp = { "parent": "", "ruby": None }

    def add(temp):
        if len(temp["parent"]) > 0:
            words.append({ "parent": temp["parent"], "ruby": temp["ruby"] or "" })
            temp["parent"] = ""
            temp["ruby"] = None

    for char in text:
        if char == "[" or char == "]":
            add(temp)
        elif char == ":":
            temp["ruby"] = ""
        elif temp["ruby"] is not None:
            temp["ruby"] += char
        else:
            temp["parent"] += char

    add(temp)
    return words

スペースを用いて表現する

あとは 1. で用意した parentLabel, rubyLabel にそれぞれ親文字、ルビを流し込んでいけばよいのですが、この際に、ルビが親文字の中央に収まるよう(中付き)に間隔を調整する必要があります。逆にルビが親文字をはみ出す場合は、親文字がルビの中央に収まるように配置します。この処理を図示すると、以下の通りになります。

ルビの間隔の処理

Unicode には半角スペース(U+0020)や全角スペース(U+3000)のほかにも、以下の種類のスペースが存在しています。半角スペースは名称に反して 1/3 em(全角の 1/3 の幅)として表現されることが多いため、1/2 em や 1/4 em を表現するには不十分です。したがって今回は Four-Per-Em Space(U+2005)を用いて体裁を整えます。

Unicode 名称
U+2004 Three-Per-Em Space 1/3 em、三分アキ
U+2005 Four-Per-Em Space 1/4 em、四分アキ
U+2006 Six-Per-Em Space 1/6 em、六分アキ

スペーシングの処理を以下に示します。

import math

text = "[明日:あした]からスペースを[用:もち]いて[擬:ぎ][似:じ][的:てき]に[表:ひょう][現:げん]"
parsed = parse_assignment_text(text)
parent = ""
ruby = ""

for word in parsed:
    parent_len = len(word["parent"])
    ruby_len = len(word["ruby"])

    parent_spaces = max((ruby_len - parent_len * 2) * 2, 0)
    ruby_spaces = max((parent_len * 2 - ruby_len) * 4, 0)

    parent += f"{"" * math.ceil(parent_spaces / 2)}{word["parent"]}{"" * math.floor(parent_spaces / 2)}"
    ruby += f"{"" * math.ceil(ruby_spaces / 2)}{word["ruby"]}{"" * math.floor(ruby_spaces / 2)}"

parentLabel["text"] = parent
rubyLabel["text"] = ruby

コードを実行すると、以下のようにルビを表現できます。突出処理等は実装できていませんが、簡易的な実装として考えていただければ幸いです。

Discussion