1
1

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とTkinterで

1
Posted at

こんな画面

image.png

機能

  • カンマつき単位つきの数式を計算してくれる
  • 計算結果をクリップボードにセットしてくれる
  • 計算結果は3桁カンマのフォーマット
  • 入力の初期値はクリップボードのテキスト

使い方

  • メールやチャットでちょっとした計算をしたいときに数式をクリップボードにコピー
  • TextCalc.pywを実行
  • エンター(すると計算結果がクリップボードにセット)
  • Windowsのクリップボード履歴で計算結果が履歴になるからこのアプリに履歴管理機能なんて要らない

ソース

textcalc.pyw
import sys
import re
import tkinter as tk
from tkinter import ttk
from tkinter import font as tkfont
from decimal import Decimal, InvalidOperation

# --- 数式の抽出と評価ロジック ---

MULTIPLY_CHARS = ['*', 'x', 'X', '×', '', '', '']
DIVIDE_CHARS   = ['/', '', '÷']
MULTIPLY_SET = set(MULTIPLY_CHARS)
DIVIDE_SET   = set(DIVIDE_CHARS)
LEFT_PARENS  = set(['(', ''])
RIGHT_PARENS = set([')', ''])

def extract_numbers_and_ops(expr: str) -> str:
    tokens = []
    i = 0
    while i < len(expr):
        ch = expr[i]
        if ch.isdigit():
            start = i
            i += 1
            while i < len(expr) and (expr[i].isdigit() or expr[i] in ",._·"):
                i += 1
            tokens.append(expr[start:i])
            continue
        elif ch in "+-":
            tokens.append(ch)
        elif ch in LEFT_PARENS:
            tokens.append('(')
        elif ch in RIGHT_PARENS:
            tokens.append(')')
        elif ch in MULTIPLY_SET:
            tokens.append('*')
        elif ch in DIVIDE_SET:
            tokens.append('/')
        i += 1
    return "".join(tokens)

def normalize_number_token(token: str) -> str:
    return token.replace(",", "").replace("_", "").replace("·", "")

def eval_text_expression(expr: str) -> Decimal:
    cleaned = extract_numbers_and_ops(expr)
    if not cleaned:
        raise ValueError("式が見つかりません")

    def repl(m: re.Match) -> str:
        return normalize_number_token(m.group(0))

    cleaned = re.sub(r"[0-9][0-9,._·]*", repl, cleaned)

    try:
        val = eval(cleaned, {"__builtins__": None}, {})
    except Exception as e:
        raise ValueError(f"無効な式です: {cleaned}") from e

    try:
        return Decimal(str(val))
    except InvalidOperation as e:
        raise ValueError(f"結果を数値に変換できません: {val}") from e

def format_with_commas(value: Decimal) -> str:
    if value == value.to_integral():
        return f"{int(value):,}"
    else:
        s = f"{value:f}".rstrip("0").rstrip(".")
        parts = s.split(".")
        parts[0] = f"{int(parts[0]):,}"
        return ".".join(parts)

# --- クリップボード(tkinter 経由) ---

def get_clipboard_text(root: tk.Tk) -> str:
    try:
        return root.clipboard_get()
    except tk.TclError:
        return ""

def set_clipboard_text(root: tk.Tk, text: str) -> bool:
    try:
        root.clipboard_clear()
        root.clipboard_append(text)
        root.update()
        return True
    except tk.TclError as e:
        print("[ERROR] クリップボード設定に失敗しました:", e, file=sys.stderr)
        return False

# --- GUI アプリ ---

def main():
    root = tk.Tk()
    root.title("TextCalc")
    root.geometry("520x200")
    root.resizable(False, False)

    base_font_size = 20
    app_font = ("Meiryo", base_font_size)

    style = ttk.Style(root)
    style.configure("TLabel", font=app_font)
    style.configure("TButton", font=app_font)
    style.configure("TEntry", font=app_font)

    clip_text = get_clipboard_text(root)
    first_line = clip_text.splitlines()[0].strip() if clip_text else ""

    frame = ttk.Frame(root, padding=8)
    frame.pack(fill="both", expand=True)

    entry_var = tk.StringVar(value=first_line)
    entry = ttk.Entry(frame, textvariable=entry_var, font=app_font)
    entry.pack(fill="x", expand=False, pady=(0, 8))

    btn_frame = ttk.Frame(frame)
    btn_frame.pack(fill="x", pady=(4, 0))

    status_var = tk.StringVar(value="")

    # ステータス用フォント(サイズ可変)
    status_font = tkfont.Font(family="Meiryo", size=base_font_size)
    lbl_status = ttk.Label(frame, textvariable=status_var, foreground="#006000")
    lbl_status.pack(anchor="w", pady=(12, 0))
    lbl_status.configure(font=status_font)

    def adjust_status_font(text: str):
        """
        ラベルの幅に合わせてフォントサイズを自動縮小(最小9pt)。
        溢れていない場合は base_font_size のまま。
        """
        if not text:
            return

        # まずベースサイズに戻して測定
        status_font.configure(size=base_font_size)
        root.update_idletasks()  # レイアウト確定

        label_width = lbl_status.winfo_width()
        if label_width <= 0:
            label_width = lbl_status.winfo_reqwidth()
        if label_width <= 0:
            return

        text_width = status_font.measure(text)

        # 溢れていないなら縮小しない
        if text_width <= label_width:
            return

        size = base_font_size
        while text_width > label_width and size > 9:
            size -= 1
            status_font.configure(size=size)
            text_width = status_font.measure(text)

    def do_convert():
        text = entry_var.get()
        if not text.strip():
            status_var.set("入力が空です")
            adjust_status_font(status_var.get())
            return

        try:
            result = eval_text_expression(text)
        except Exception as e:
            msg = f"エラー: {e}"
            status_var.set(msg)
            adjust_status_font(msg)
            return

        formatted = format_with_commas(result)
        new_text = f"{text} = {formatted}"

        status_var.set(new_text)
        adjust_status_font(new_text)
        set_clipboard_text(root, new_text)

    def on_entry_return(event):
        do_convert()
        return "break"

    entry.bind("<Return>", on_entry_return)

    btn_convert = ttk.Button(btn_frame, text="変換してコピー", command=do_convert)
    btn_convert.pack(side="left")

    entry.focus_set()
    root.mainloop()

if __name__ == "__main__":
    main()

解説

  • pywの拡張子つけるとWindowsでダブルクリックするだけでコマンドプロンプト画面なしにPythonが実行される
  • Tkinterは標準モジュール。他にも追加インストール必要なモジュールは一切使ってない
  • TkinterはMacOSやLinuxでも動くらしい(未検証)
  • GUIつきアプリだけど実行時のメモリ使用量は11MBとめちゃくちゃ小さい
  • スクロールをさせたくなかったので、文字が溢れたら縮小という面倒くさめのロジックを足してる
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?