๐งฎ Python (Tkinter)์ผ๋ก Desktop Word Counter ์ฑ ๋ง๋ค๊ธฐ
Source: Dev.to
์์ ์ ๊ณต๋ ํ ์คํธ๊ฐ ์์ต๋๋ค. ๋ฒ์ญ์ ์ํ๋ ๋ณธ๋ฌธ์ ์ ๊ณตํด ์ฃผ์๋ฉด ํ๊ตญ์ด๋ก ๋ฒ์ญํด ๋๋ฆฌ๊ฒ ์ต๋๋ค.
โจ Features
- ๐ Live word, character, sentence & line count โ ๐ ์ค์๊ฐ ๋จ์ด, ๋ฌธ์, ๋ฌธ์ฅ ๋ฐ ์ค ์
- ๐ Keyword density calculation โ ๐ ํค์๋ ๋ฐ๋ ๊ณ์ฐ
- โฑ Estimated reading time (200โฏWPM) โ โฑ ์์ ์ฝ๊ธฐ ์๊ฐ (200โฏWPM)
- ๐ Open & saveโฏ
.txtfiles โ ๐ ์ด๊ธฐ ๋ฐ ์ ์ฅโฏ.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:
- Helpers โ status updates & resource paths
- Core Logic โ text analysis & keyword density
- UI Sections โ text input, statistics, actions
- Theme Engine โ light/dark mode switching
- File Operations โ open, save, clear
This keeps the code readable and extensible.
๐ง ํ ์คํธ ๋ถ์ ์๋ ๋ฐฉ์
| Metric | Method |
|---|---|
| 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๋ง์ผ๋ก๋ ํ๋์ ์ด๊ณ ์ ์ฉํ ๋ฐ์คํฌํฑ ๋๊ตฌ๋ฅผ ๋น ๋ฅด๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
์ด ๊ธ์ด ๋ง์์ ๋์ จ๋ค๋ฉด ์์ ๋กญ๊ฒ:
- ํฌํฌํ๊ธฐ
- ํ์ฅํ๊ธฐ
- ํจํค์ง๋ ๋ฐ์คํฌํฑ ์ฑ์ผ๋ก ๋ง๋ค๊ธฐ
์ฆ๊ฑฐ์ด ๊ฐ๋ฐ ๋์ธ์! ๐ ๏ธ