๐Ÿงฎ Python (Tkinter)์œผ๋กœ Desktop Word Counter ์•ฑ ๋งŒ๋“ค๊ธฐ

๋ฐœํ–‰: (2025๋…„ 12์›” 30์ผ ์˜ค์ „ 11:18 GMT+9)
8 min read
์›๋ฌธ: Dev.to

Source: Dev.to

์œ„์— ์ œ๊ณต๋œ ํ…์ŠคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ฒˆ์—ญ์„ ์›ํ•˜๋Š” ๋ณธ๋ฌธ์„ ์ œ๊ณตํ•ด ์ฃผ์‹œ๋ฉด ํ•œ๊ตญ์–ด๋กœ ๋ฒˆ์—ญํ•ด ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

โœจ Features

  • ๐Ÿ“Š Live word, character, sentence & line count โ†’ ๐Ÿ“Š ์‹ค์‹œ๊ฐ„ ๋‹จ์–ด, ๋ฌธ์ž, ๋ฌธ์žฅ ๋ฐ ์ค„ ์ˆ˜
  • ๐Ÿ” Keyword density calculation โ†’ ๐Ÿ” ํ‚ค์›Œ๋“œ ๋ฐ€๋„ ๊ณ„์‚ฐ
  • โฑ Estimated reading time (200โ€ฏWPM) โ†’ โฑ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„ (200โ€ฏWPM)
  • ๐Ÿ“‚ Open & saveโ€ฏ.txt files โ†’ ๐Ÿ“‚ ์—ด๊ธฐ ๋ฐ ์ €์žฅโ€ฏ.txt ํŒŒ์ผ
  • ๐ŸŒ™ Lightโ€ฏ/โ€ฏDark mode toggle โ†’ ๐ŸŒ™ ๋ผ์ดํŠธโ€ฏ/โ€ฏ๋‹คํฌ ๋ชจ๋“œ ์ „ํ™˜
  • ๐Ÿงน Clear text with confirmation โ†’ ๐Ÿงน ํ…์ŠคํŠธ ์ง€์šฐ๊ธฐ ํ™•์ธ๊ณผ ํ•จ๊ป˜
  • โ“ Builtโ€‘in help dialog โ†’ โ“ ๋‚ด์žฅ ๋„์›€๋ง ๋Œ€ํ™”์ฐฝ
  • ๐Ÿ’ป Fully offline & crossโ€‘platform โ†’ ๐Ÿ’ป ์™„์ „ ์˜คํ”„๋ผ์ธ ๋ฐ ํฌ๋กœ์Šคโ€‘ํ”Œ๋žซํผ

๐Ÿงฐ ๊ธฐ์ˆ  ์Šคํƒ

  • Pythonโ€ฏ3
  • Tkinter (GUI)
  • ttk (ํ…Œ๋งˆ ์œ„์ ฏ)
  • svโ€‘ttk (ํ˜„๋Œ€์ ์ธ ๋ฐ์€/์–ด๋‘์šด ํ…Œ๋งˆ)
  • threading (๋…ผ๋ธ”๋กœํ‚น ์—…๋ฐ์ดํŠธ)

๐Ÿ“ฆ ์š”๊ตฌ ์‚ฌํ•ญ

Only one external dependency is needed:

pip install sv-ttk

๐Ÿ—๏ธ App Architecture

The app is split into logical sections:

  1. Helpers โ€“ status updates & resource paths
  2. Core Logic โ€“ text analysis & keyword density
  3. UI Sections โ€“ text input, statistics, actions
  4. Theme Engine โ€“ light/dark mode switching
  5. File Operations โ€“ open, save, clear

This keeps the code readable and extensible.

๐Ÿง  ํ…์ŠคํŠธ ๋ถ„์„ ์ž‘๋™ ๋ฐฉ์‹

MetricMethod
Words (๋‹จ์–ด)split()
Characters (๋ฌธ์ž)len(text)
Characters (no spaces) (๊ณต๋ฐฑ ์—†๋Š” ๋ฌธ์ž)๊ณต๋ฐฑ ๋ฐ ์ค„๋ฐ”๊ฟˆ์„ ์ œ๊ฑฐํ•œ ๋’ค len()
Sentences (๋ฌธ์žฅ)., !, ? ์˜ ์ถœํ˜„ ํšŸ์ˆ˜ ์„ธ๊ธฐ
Reading time (์ฝ๋Š” ์‹œ๊ฐ„)words / 200โ€ฏWPM (๋ฐ˜์˜ฌ๋ฆผ, ์ตœ์†Œโ€ฏ1โ€ฏ๋ถ„)
Keyword density (ํ‚ค์›Œ๋“œ ๋ฐ€๋„)(keyword occurrences / total words) * 100

์ž…๋ ฅํ•˜๋Š” ์ฆ‰์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค.

๐Ÿงฉ ์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ

์•„๋ž˜ ๋ชจ๋“  ๋‚ด์šฉ์„ ํ•˜๋‚˜์˜ wordcounter_pro.py ํŒŒ์ผ์— ๋ณต์‚ฌํ•˜๊ณ  ์‹คํ–‰ํ•˜์„ธ์š”.

import sys
import os
import threading
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sv_ttk

# =========================
# Helpers
# =========================
def resource_path(file_name):
    base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, file_name)

def set_status(msg):
    status_var.set(msg)
    root.update_idletasks()

# =========================
# App Setup
# =========================
root = tk.Tk()
root.title("WordCounter Pro")
root.geometry("1050x650")

sv_ttk.set_theme("light")

# =========================
# Globals
# =========================
dark_mode_var = tk.BooleanVar(value=False)

stats_vars = {
    "words": tk.StringVar(value="0"),
    "characters": tk.StringVar(value="0"),
    "characters_no_space": tk.StringVar(value="0"),
    "lines": tk.StringVar(value="0"),
    "sentences": tk.StringVar(value="0"),
    "reading_time": tk.StringVar(value="0 min"),
}

keyword_var = tk.StringVar()
keyword_density_var = tk.StringVar(value="0%")

# =========================
# Theme Toggle
# =========================
def toggle_theme():
    style.theme_use("clam")
    bg = "#2E2E2E" if dark_mode_var.get() else "#FFFFFF"
    fg = "white" if dark_mode_var.get() else "black"

    root.configure(bg=bg)
    for w in ["TFrame", "TLabel", "TLabelframe", "TLabelframe.Label", "TCheckbutton"]:
        style.configure(w, background=bg, foreground=fg)

    text_area.configure(
        bg="#1e1e1e" if dark_mode_var.get() else "white",
        fg="white" if dark_mode_var.get() else "black",
        insertbackground="white" if dark_mode_var.get() else "black"
    )

    set_status(f"Theme switched to {'Dark' if dark_mode_var.get() else 'Light'} mode")

# =========================
# Core Logic
# =========================
def count_text():
    content = text_area.get("1.0", tk.END).strip()

    if not content:
        for key in stats_vars:
            stats_vars[key].set("0" if key != "reading_time" else "0 min")
        keyword_density_var.set("0%")
        return

    words_list = content.split()
    words = len(words_list)
    characters = len(content)
    characters_no_space = len(content.replace(" ", "").replace("\n", ""))
    lines = len(content.splitlines())
    sentences = sum(content.count(x) for x in ".!?")

    reading_minutes = max(1, round(words / 200))
    stats_vars["reading_time"].set(f"{reading_minutes} min")

    stats_vars["words"].set(words)
    stats_vars["characters"].set(characters)
    stats_vars["characters_no_space"].set(characters_no_space)
    stats_vars["lines"].set(lines)
    stats_vars["sentences"].set(sentences)

    keyword = keyword_var.get().strip().lower()
    if keyword:
        occurrences = sum(1 for w in words_list if w.lower() == keyword)
        density = (occurrences / words) * 100
        keyword_density_var.set(f"{density:.2f}%")
    else:
        keyword_density_var.set("0%")

    set_status("Text analyzed")

def delayed_count(event=None):
    threading.Timer(0.1, count_text).start()

# =========================
# File Operations
# =========================
def open_file():
    path = filedialog.askopenfilename(
        filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
    )
    if not path:
        return
    try:
        with open(path, "r", encoding="utf-8") as f:
            text_area.delete("1.0", tk.END)
            text_area.insert("1.0", f.read())
        count_text()
        set_status(f"Loaded: {os.path.basename(path)}")
    except Exception as e:
        messagebox.showerror("Error", str(e))

def save_file():
    path = filedialog.asksaveasfilename(
        defaultextension=".txt",
        filetypes=[("Text Files", "*.txt")]
    )
    if not path:
        return
    try:
        with open(path, "w", encoding="utf-8") as f:
            f.write(text_area.get("1.0", tk.END))
    set_status(f"Saved to {path}")
    except Exception as e:
        messagebox.showerror("Error", str(e))

def clear_text():
    if messagebox.askyesno("Clear", "Clear all text?"):
        text_area.delete("1.0", tk.END)
        count_text()
        set_status("Text cleared")

# =========================
# Help Window
# =========================
def show_help():
    win = tk.Toplevel(root)
    win.title("WordCounter Pro - Help")
    win.geometry("480x360")
    win.configure(bg="#2e2e2e")
    win.resizable(False, False)
    win.transient(root)
    win.grab_set()

    frame = tk.Frame(win, bg="#2e2e2e")
    frame.pack(fill="both", expand=True, padx=12, pady=12)

    text = tk.Text(
        frame,
        bg="#2e2e2e",
        fg="white",
        insertbackground="white",
        wrap="word",
        relief="flat",
        borderwidth=0,
    )
    text.pack(fill="both", expand=True)

    help_content = (
        "WordCounter Pro helps you analyse text quickly.\n\n"
        "โ€ข Type or paste text into the main area.\n"
        "โ€ข Statistics update live.\n"
        "โ€ข Use the keyword field to see density.\n"
        "โ€ข Open/Save files via the menu.\n"
        "โ€ข Toggle Light/Dark mode with the checkbox.\n"
        "โ€ข Clear text with confirmation.\n"
    )
    text.insert("1.0", help_content)
    text.configure(state="disabled")

# =========================
# UI Construction
# =========================
style = ttk.Style()
style.theme_use("clam")

# Top frame โ€“ menu & theme toggle
top_frame = ttk.Frame(root, padding=10)
top_frame.pack(fill="x")

menu_bar = tk.Menu(root)
file_menu = tk.Menu(menu_bar, tearoff=0)
file_menu.add_command(label="Open", command=open_file)
file_menu.add_command(label="Save As...", command=save_file)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=root.quit)
menu_bar.add_cascade(label="File", menu=file_menu)

help_menu = tk.Menu(menu_bar, tearoff=0)
help_menu.add_command(label="Help", command=show_help)
menu_bar.add_cascade(label="Help", menu=help_menu)

root.config(menu=menu_bar)

theme_check = ttk.Checkbutton(
    top_frame,
    text="Dark Mode",
    variable=dark_mode_var,
    command=toggle_theme,
)
theme_check.pack(side="right")

# Main content โ€“ text area & stats
main_pane = ttk.PanedWindow(root, orient=tk.HORIZONTAL)
main_pane.pack(fill="both", expand=True, padx=10, pady=10)

# Text input
text_frame = ttk.Labelframe(main_pane, text=" Text Input ")
text_frame.pack(fill="both", expand=True, side="left", padx=5, pady=5)

text_area = tk.Text(
    text_frame,
    wrap="word",
    undo=True,
    font=("Helvetica", 12),
    relief="flat",
    borderwidth=2,
)
text_area.pack(fill="both", expand=True, padx=5, pady=5)
text_area.bind("", delayed_count)

# Stats panel
stats_frame = ttk.Labelframe(main_pane, text=" Statistics ")
stats_frame.pack(fill="both", expand=False, side="right", padx=5, pady=5)

def add_stat(label, var):
    row = ttk.Frame(stats_frame)
    row.pack(fill="x", pady=2)
    ttk.Label(row, text=label + ":").pack(side="left")
    ttk.Label(row, textvariable=var).pack(side="right")

add_stat("Words", stats_vars["words"])
add_stat("Characters", stats_vars["characters"])
add_stat("Characters (no spaces)", stats_vars["characters_no_space"])
add_stat("Lines", stats_vars["lines"])
add_stat("Sentences", stats_vars["sentences"])
add_stat("Reading Time", stats_vars["reading_time"])

# Keyword density
keyword_frame = ttk.Labelframe(stats_frame, text=" Keyword Density ")
keyword_frame.pack(fill="x", pady=8, padx=4)

ttk.Label(keyword_frame, text="Keyword:").grid(row=0, column=0, sticky="w", padx=2, pady=2)
keyword_entry = ttk.Entry(keyword_frame, textvariable=keyword_var)
keyword_entry.grid(row=0, column=1, sticky="e", padx=2, pady=2)

ttk.Label(keyword_frame, text="Density:").grid(row=1, column=0, sticky="w", padx=2, pady=2)
ttk.Label(keyword_frame, textvariable=keyword_density_var).grid(row=1, column=1, sticky="e", padx=2, pady=2)

# Bottom status bar
status_var = tk.StringVar(value=
"Ready")
status_bar = ttk.Label(root, textvariable=status_var, relief="sunken", anchor="w")
status_bar.pack(fill="x", side="bottom")

# Initial theme sync
toggle_theme()

root.mainloop()

WordCounterโ€ฏPro โ€“ Tkinter UI (Python)

# =========================
# Help Window
# =========================
def show_help():
    help_win = tk.Toplevel(root)
    help_win.title("Quick Help")
    help_win.geometry("420x340")
    help_win.resizable(False, False)

    text = tk.Text(
        help_win,
        bg="#2e2e2e",
        fg="#f0f0f0",
        font=("Segoe UI", 11),
        wrap="word",
        borderwidth=0
    )
    text.pack(fill="both", expand=True)

    help_text = """๐Ÿ“Š WordCounter Pro โ€” Quick Help

โ€ข Type or paste text to see live statistics
โ€ข Counts words, characters, sentences, and lines
โ€ข Enter a keyword to calculate density (%)
โ€ข Reading time based on 200 words per minute
โ€ข Open and save .txt files
โ€ข Toggle Dark Mode for comfort

All processing is done locally.
"""
    text.insert("1.0", help_text)
    text.config(state="disabled")

# =========================
# Styles
# =========================
style = ttk.Style()
style.theme_use("clam")
style.configure(
    "Action.TButton",
    font=("Segoe UI", 11, "bold"),
    foreground="white",
    background="#4CAF50",
    padding=8
)
style.map("Action.TButton", background=[("active", "#45a049")])

# =========================
# Status Bar
# =========================
status_var = tk.StringVar(value="Ready")
ttk.Label(root, textvariable=status_var, anchor="w").pack(side=tk.BOTTOM, fill="x")

# =========================
# Main UI
# =========================
main = ttk.Frame(root, padding=20)
main.pack(expand=True, fill="both")

ttk.Label(main, text="WordCounter Pro", font=("Segoe UI", 22, "bold")).pack()
ttk.Label(main, text="Advanced word & text analysis tool",
          font=("Segoe UI", 11)).pack(pady=(0, 10))

# =========================
# Text Area
# =========================
text_frame = ttk.LabelFrame(main, text="Text Input", padding=10)
text_frame.pack(fill="both", pady=10)

text_area = tk.Text(
    text_frame,
    font=("Segoe UI", 11),
    wrap="word",
    height=10
)
text_area.pack(fill="both")
text_area.bind("", delayed_count)

# =========================
# Statistics
# =========================
stats_frame = ttk.LabelFrame(main, text="Statistics", padding=15)
stats_frame.pack(fill="x", pady=8)

for i, (label, var) in enumerate(stats_vars.items()):
    ttk.Label(stats_frame, text=label.replace("_", " ").title() + ":",
              font=("Segoe UI", 10, "bold")).grid(row=0, column=i * 2, padx=4, sticky="e")
    ttk.Label(stats_frame, textvariable=var,
              font=("Segoe UI", 10)).grid(row=0, column=i * 2 + 1, padx=6, sticky="w")

# =========================
# Keyword Density
# =========================
keyword_frame = ttk.LabelFrame(main, text="Keyword Density", padding=12)
keyword_frame.pack(fill="x", pady=8)

ttk.Label(keyword_frame, text="Keyword:",
          font=("Segoe UI", 10, "bold")).pack(side="left", padx=(0, 6))

keyword_entry = ttk.Entry(keyword_frame, textvariable=keyword_var, width=22)
keyword_entry.pack(side="left")
keyword_entry.bind("", delayed_count)

ttk.Label(keyword_frame, text="Density:",
          font=("Segoe UI", 10, "bold")).pack(side="left", padx=(15, 6))

ttk.Label(keyword_frame, textvariable=keyword_density_var,
          font=("Segoe UI", 10)).pack(side="left")

# =========================
# Actions
# =========================
actions = ttk.Frame(main)
actions.pack(pady=12)

ttk.Button(actions, text="๐Ÿ“‚ Open", command=open_file,
           style="Action.TButton").pack(side="left", padx=6)
ttk.Button(actions, text="๐Ÿ’พ Save", command=save_file,
           style="Action.TButton").pack(side="left", padx=6)
ttk.Button(actions, text="๐Ÿงน Clear", command=clear_text,
           style="Action.TButton").pack(side="left", padx=6)
ttk.Button(actions, text="โ“ Help", command=show_help,
           style="Action.TButton").pack(side="left", padx=6)

ttk.Checkbutton(
    actions,
    text="Dark Mode",
    variable=dark_mode_var,
    command=toggle_theme
).pack(side="left", padx=14)

# =========================
# Run App
# ========```

=================
root.mainloop()

๐Ÿš€ ํ™•์žฅ ์•„์ด๋””์–ด

  • CSV๋กœ ํ†ต๊ณ„ ๋‚ด๋ณด๋‚ด๊ธฐ
  • ํ‚ค์›Œ๋“œ ๋ฐœ์ƒ ํšŸ์ˆ˜ ๊ฐ•์กฐ
  • ์‹ค์‹œ๊ฐ„ ๊ฐ€๋…์„ฑ ์ ์ˆ˜
  • PyInstaller๋กœ .exe ํŒจํ‚ค์ง•
  • ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํŒŒ์ผ ๋กœ๋“œ ์ถ”๊ฐ€

๐Ÿง  ์ตœ์ข… ์ƒ๊ฐ

Tkinter๊ฐ€ ๊ตฌ์‹์ผ ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ttkโ€ฏ+โ€ฏsvโ€‘ttk๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Python๋งŒ์œผ๋กœ๋„ ํ˜„๋Œ€์ ์ด๊ณ  ์œ ์šฉํ•œ ๋ฐ์Šคํฌํ†ฑ ๋„๊ตฌ๋ฅผ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์ด ๋งˆ์Œ์— ๋“œ์…จ๋‹ค๋ฉด ์ž์œ ๋กญ๊ฒŒ:

  • ํฌํฌํ•˜๊ธฐ
  • ํ™•์žฅํ•˜๊ธฐ
  • ํŒจํ‚ค์ง•๋œ ๋ฐ์Šคํฌํ†ฑ ์•ฑ์œผ๋กœ ๋งŒ๋“ค๊ธฐ

์ฆ๊ฑฐ์šด ๊ฐœ๋ฐœ ๋˜์„ธ์š”! ๐Ÿ› ๏ธ

Back to Blog

๊ด€๋ จ ๊ธ€

๋” ๋ณด๊ธฐ ยป

Python๊ณผ Tkinter๋กœ ๊ฐ„๋‹จํ•œ ํŒŒ์ผ ํƒ์ƒ‰๊ธฐ ๋งŒ๋“ค๊ธฐ โ€“ FileMate Explorer

๐Ÿ“‚ FileMate Explorer โ€“ ๊ฐ€๋ฒผ์šด ํŒŒ์ด์ฌ ํŒŒ์ผ ๊ด€๋ฆฌ์ž. ์™„์ „ํžˆ ํŒŒ์ด์ฌ์œผ๋กœ ๋งŒ๋“  ๊ฐ€๋ฒผ์šด ํŒŒ์ผ ํƒ์ƒ‰๊ธฐ๋ฅผ ์›ํ•˜์…จ๋‚˜์š”? Tkinter ๊ธฐ๋ฐ˜ FileMate Explorer๋ฅผ ๋งŒ๋‚˜๋ณด์„ธ์š”.

๐Ÿš€ FileMate Pro: Tkinter๋ฅผ ํ™œ์šฉํ•œ Python GUI ํŒŒ์ผ ๋งค๋‹ˆ์ €

FileMate Pro โ€“ ๊ฐ„๋‹จํ•œ Tkinter ํŒŒ์ผ ๊ด€๋ฆฌ์ž Python์—์„œ GUI๋กœ ํŒŒ์ผ์„ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋ณต์žกํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. FileMate Pro๋Š” ๊ฐ€๋ณ๊ณ  ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ โ€ฆ

๐Ÿงฎ Python๊ณผ Tkinter๋ฅผ ์‚ฌ์šฉํ•œ ํ˜„๋Œ€ ํ†ต๊ณ„ ๊ณ„์‚ฐ๊ธฐ ๋งŒ๋“ค๊ธฐ

StatMate โ€“ ํ˜„๋Œ€์ ์ธ ๋ฐ์Šคํฌํ†ฑ ํ†ต๊ณ„ ๊ณ„์‚ฐ๊ธฐ Python์€ ๋ฐ์ดํ„ฐ ์‚ฌ์ด์–ธ์Šค ์ƒํƒœ๊ณ„๋กœ ์ž์ฃผ ์ฐฌ์‚ฌ๋ฅผ ๋ฐ›์ง€๋งŒ, Pandas๋‚˜ NumPy ์—†์ด๋„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

Tkinter๋ฅผ ์‚ฌ์šฉํ•œ 2D ๊ฒŒ์ž„ ์‹œ์ž‘ํ•˜๊ธฐ (ํŒŒํŠธ 9): ์นด์šดํ„ฐ ํ‘œ์‹œ

์นด์šดํ„ฐ ํ‘œ์‹œ ์ด ๊ธฐ์‚ฌ์—์„œ๋Š” ํ™”๋ฉด์— ๋‚จ์•„ ์žˆ๋Š” ์•…๋งˆ ์ˆ˜๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์นด์šดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋จผ์ €, ์นด์šดํ„ฐ๋ผ๋Š” ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ๋‚จ์€ ์ˆ˜๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.