- Get link
- X
- Other Apps
Recommended Posts
- Get link
- X
- Other Apps
파이썬으로 만드는 간단 메모장 (Tkinter GUI 예제)
이 글에서는 파이썬 Tkinter로 작동하는 가벼운 메모장 애플리케이션을 단계별로 만들어 봅니다. 파일 열기/저장, 새 파일, 선택/복사/붙여넣기, 찾기, 되돌리기/다시실행, 단어 수 세기, 상태표시줄 등 기본 기능을 구현합니다. 교육 및 실습용으로 설계되어 코드를 그대로 복사해 실행하면 즉시 동작합니다.
GUI 프로그래밍 초입에서 가장 좋은 예제는 “텍스트 에디터”입니다. 위젯 배치, 메뉴 구성, 파일 I/O, 대화창, 단축키 바인딩까지 핵심 개념을 한 번에 경험할 수 있기 때문입니다. 아래 가이드는 초급교육용블로그 포스팅용으로 구성되어 있으며, 중간중간 확장 팁도 제시합니다.
학습 목표
- Tkinter로 텍스트 위젯과 스크롤바 배치하기
- 메뉴바 구성 및 파일 다이얼로그 사용하기
- 편집 명령(잘라내기/복사/붙여넣기/되돌리기)과 단축키 바인딩
- 찾기/단어 수 세기/상태표시줄 구현
준비물
- Python 3.8+ (표준 배포판에 Tkinter 포함)
- OS: Windows/Mac/Linux 모두 가능
- 에디터/IDE: VS Code, PyCharm, IDLE 등
※ 일부 리눅스 배포판은 python3-tk 패키지 설치가 필요할 수 있습니다.
프로젝트 구조와 핵심 아이디어
단일 파일 notepad.py로 시작합니다. 메인 윈도우를 만들고, Text 위젯에 스크롤바를 붙이며, 메뉴바로 명령을 노출합니다. 명령은 함수로 분리하여 가독성과 재사용성을 높입니다. 상태표시줄은 현재 행/열 위치와 수정 여부를 보여주며, 키 입력/마우스 클릭 이벤트에 반응하도록 바인딩합니다.
찾기 기능은 간단한 Toplevel 대화창으로 구현합니다. 사용자가 문자열을 입력하면 본문에서 모든 일치 항목을 하이라이트합니다. 정규식 대신 기본 검색으로 시작하여 교육 난도를 낮추고, 하이라이트 태그를 통해 시각 피드백을 제공합니다.
핵심 위젯와 배치
- Text: 본문 입력/편집
- Scrollbar: 세로 스크롤
- Menu: 파일/편집/도구/도움말
- Label: 상태표시줄
실습 코드 (완성본)
아래 코드를 notepad.py로 저장하고 실행하세요. Windows는 파일을 더블클릭하거나 python notepad.py로 실행할 수 있습니다. Mac/Linux도 터미널에서 동일하게 실행합니다.
"""
파이썬 Tkinter 메모장 - 교육용 예제
기능: 새로 만들기, 열기, 저장/다른 이름으로 저장, 닫기
잘라내기/복사/붙여넣기/실행 취소/다시 실행/전체 선택
찾기(하이라이트), 단어 수 세기, 상태표시줄(행/열, 수정됨)
"""
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter import ttk
import os
APP_TITLE = "PyNote - 간단 메모장 (Tkinter)"
class Notepad(tk.Tk):
def __init__(self):
super().__init__()
self.title(APP_TITLE)
self.geometry("900x600")
self.filepath = None
self.modified = False
self._create_widgets()
self._create_menu()
self._bind_shortcuts()
self._update_title()
# ---------------- UI 구성 ----------------
def _create_widgets(self):
# 상단 프레임(툴바 확장 여지)
top = ttk.Frame(self)
top.pack(side=tk.TOP, fill=tk.X)
# 본문 텍스트 + 스크롤
self.text = tk.Text(self, wrap="word", undo=True)
self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
yscroll = ttk.Scrollbar(self, command=self.text.yview)
yscroll.pack(side=tk.RIGHT, fill=tk.Y)
self.text.configure(yscrollcommand=yscroll.set)
# 상태표시줄
self.status = ttk.Label(self, text="Ready", anchor=tk.W)
self.status.pack(side=tk.BOTTOM, fill=tk.X)
# 찾기 하이라이트 태그
self.text.tag_configure("search_hit", background="#fff3a3")
# 변경 감지
self.text.bind("<KeyRelease>", self._on_change)
self.text.bind("<ButtonRelease-1>", self._on_cursor_move)
# ttk 스타일(가독성)
style = ttk.Style(self)
try:
self.tk.call("source", "sun-valley.tcl") # 없으면 무시
style.theme_use("sun-valley-dark")
except tk.TclError:
pass
def _create_menu(self):
menubar = tk.Menu(self)
# 파일
filemenu = tk.Menu(menubar, tearoff=0)
filemenu.add_command(label="새로 만들기", command=self.new_file, accelerator="Ctrl+N")
filemenu.add_command(label="열기...", command=self.open_file, accelerator="Ctrl+O")
filemenu.add_command(label="저장", command=self.save_file, accelerator="Ctrl+S")
filemenu.add_command(label="다른 이름으로 저장...", command=self.save_as)
filemenu.add_separator()
filemenu.add_command(label="끝내기", command=self.exit_app, accelerator="Ctrl+Q")
menubar.add_cascade(label="파일", menu=filemenu)
# 편집
editmenu = tk.Menu(menubar, tearoff=0)
editmenu.add_command(label="실행 취소", command=lambda: self.text.event_generate("<<Undo>>"), accelerator="Ctrl+Z")
editmenu.add_command(label="다시 실행", command=lambda: self.text.event_generate("<<Redo>>"), accelerator="Ctrl+Y")
editmenu.add_separator()
editmenu.add_command(label="잘라내기", command=lambda: self.text.event_generate("<<Cut>>"), accelerator="Ctrl+X")
editmenu.add_command(label="복사", command=lambda: self.text.event_generate("<<Copy>>"), accelerator="Ctrl+C")
editmenu.add_command(label="붙여넣기", command=lambda: self.text.event_generate("<<Paste>>"), accelerator="Ctrl+V")
editmenu.add_separator()
editmenu.add_command(label="전체 선택", command=self.select_all, accelerator="Ctrl+A")
menubar.add_cascade(label="편집", menu=editmenu)
# 도구
toolsmenu = tk.Menu(menubar, tearoff=0)
toolsmenu.add_command(label="찾기...", command=self.find_text, accelerator="Ctrl+F")
toolsmenu.add_command(label="단어 수 세기", command=self.word_count)
menubar.add_cascade(label="도구", menu=toolsmenu)
# 도움말
helpmenu = tk.Menu(menubar, tearoff=0)
helpmenu.add_command(label="정보", command=self.show_about)
menubar.add_cascade(label="도움말", menu=helpmenu)
self.config(menu=menubar)
def _bind_shortcuts(self):
self.bind("<Control-n>", lambda e: self.new_file())
self.bind("<Control-o>", lambda e: self.open_file())
self.bind("<Control-s>", lambda e: self.save_file())
self.bind("<Control-f>", lambda e: self.find_text())
self.bind("<Control-a>", lambda e: self.select_all())
self.bind("<Control-q>", lambda e: self.exit_app())
# ---------------- 파일 명령 ----------------
def new_file(self):
if self._confirm_discard_changes():
self.text.delete("1.0", tk.END)
self.filepath = None
self.modified = False
self._update_title()
self._update_status("새 문서")
def open_file(self):
if not self._confirm_discard_changes():
return
path = filedialog.askopenfilename(
filetypes=[("텍스트 파일", "*.txt"), ("모든 파일", "*.*")]
)
if path:
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
self.text.delete("1.0", tk.END)
self.text.insert("1.0", content)
self.filepath = path
self.modified = False
self._update_title()
self._update_status(f"열기: {os.path.basename(path)}")
except Exception as e:
messagebox.showerror("오류", f"파일을 열 수 없습니다:\\n{e}")
def save_file(self):
if self.filepath is None:
return self.save_as()
try:
content = self.text.get("1.0", tk.END)
with open(self.filepath, "w", encoding="utf-8") as f:
f.write(content.rstrip("\\n") + "\\n")
self.modified = False
self._update_title()
self._update_status("저장 완료")
except Exception as e:
messagebox.showerror("오류", f"저장에 실패했습니다:\\n{e}")
def save_as(self):
path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("텍스트 파일", "*.txt"), ("모든 파일", "*.*")]
)
if path:
self.filepath = path
return self.save_file()
def exit_app(self):
if self._confirm_discard_changes():
self.destroy()
def _confirm_discard_changes(self):
if self.modified:
res = messagebox.askyesnocancel("변경 내용 저장", "변경사항을 저장하시겠습니까?")
if res: # 예 - 저장
self.save_file()
return True
elif res is None: # 취소
return False
else: # 아니오
return True
return True
# ---------------- 편집/도구 ----------------
def select_all(self):
self.text.tag_add(tk.SEL, "1.0", tk.END)
self.text.mark_set(tk.INSERT, "1.0")
self.text.see(tk.INSERT)
return "break"
def word_count(self):
content = self.text.get("1.0", tk.END).strip()
chars = len(content)
words = len(content.split()) if content else 0
lines = int(self.text.index('end-1c').split('.')[0])
messagebox.showinfo("단어 수 세기", f"줄: {lines}\\n단어: {words}\\n글자(공백 포함): {chars}")
def find_text(self):
# 찾기 대화상자
win = tk.Toplevel(self)
win.title("찾기")
win.transient(self)
win.resizable(False, False)
ttk.Label(win, text="검색어").grid(row=0, column=0, padx=8, pady=8)
entry = ttk.Entry(win, width=30)
entry.grid(row=0, column=1, padx=8, pady=8)
entry.focus()
def do_find(*_):
self.text.tag_remove("search_hit", "1.0", tk.END)
needle = entry.get()
if not needle:
return
start = "1.0"
while True:
idx = self.text.search(needle, start, stopindex=tk.END, nocase=True)
if not idx:
break
end = f"{idx}+{len(needle)}c"
self.text.tag_add("search_hit", idx, end)
start = end
self._update_status(f"찾기: '{needle}' 결과 하이라이트")
ttk.Button(win, text="찾기", command=do_find).grid(row=0, column=2, padx=8, pady=8)
win.bind('<Return>', do_find)
def show_about(self):
messagebox.showinfo("정보", "PyNote - Tkinter 교육용 메모장 예제\\n(c) 2025")
# ---------------- 상태 업데이트 ----------------
def _on_change(self, _event=None):
self.modified = True
self._update_title()
self._update_cursor_pos()
def _on_cursor_move(self, _event=None):
self._update_cursor_pos()
def _update_cursor_pos(self):
row, col = self.text.index(tk.INSERT).split('.')
mod = " • 수정됨" if self.modified else ""
self.status.config(text=f"행 {row}, 열 {int(col)+1}{mod}")
def _update_status(self, msg):
self.status.config(text=msg)
def _update_title(self):
name = os.path.basename(self.filepath) if self.filepath else "제목 없음"
mark = "*" if self.modified else ""
self.title(f"{mark}{name} - {APP_TITLE}")
if __name__ == "__main__":
app = Notepad()
app.mainloop()
실행이 안 될 때?
- 리눅스: sudo apt install python3-tk 등으로 Tk 패키지를 설치하세요.
- Mac: python 대신 python3로 실행해 보세요.
- 윈도우: “Windows Defender가 차단” 메시지가 뜨면 추가 정보 → 실행을 선택합니다.
코드 해설
1) 메인 윈도우와 텍스트 위젯
tk.Tk()로 창을 만들고 Text 위젯을 좌측에 확장 배치합니다. 스크롤바는 오른쪽에 붙이고, yscrollcommand와 command로 서로 연결합니다. 단어 단위 줄바꿈을 위해 wrap="word"를 사용합니다.
2) 메뉴와 단축키
파일/편집/도구/도움말 메뉴를 구성하고, 각 명령을 함수에 매핑합니다. 단축키는 self.bind("<Control-s>", ...)처럼 바인딩합니다. 편집 명령의 경우 <<Undo>>, <<Redo>>, <<Copy>> 등 Tkinter의 가상 이벤트를 재사용하면 구현이 간단해집니다.
3) 문서 변경 감지와 상태표시줄
키/마우스 이벤트에 반응하여 수정 상태와 커서 위치를 갱신합니다. 타이틀에 * 마크를 붙여 저장 필요 여부를 직관적으로 알립니다. 상태표시줄에는 “행/열”과 “수정됨”을 표시하여 작성 흐름을 돕습니다.
4) 찾기와 하이라이트
Toplevel 대화창에서 검색어를 입력받고, 본문 전체를 순회하며 일치 구간에 search_hit 태그를 부여합니다. 태그별 배경색으로 하이라이트되며, 다시 검색하면 이전 하이라이트를 제거하고 새로 표시합니다. 대소문자 무시 옵션(nocase=True)으로 사용성을 높였습니다.
5) 파일 I/O와 인코딩
열기/저장은 filedialog를 통해 경로를 선택하고, utf-8 인코딩으로 읽고 씁니다. 저장 시 마지막 개행을 정리해 텍스트 파일의 일관성을 유지합니다. 예외는 messagebox.showerror로 사용자에게 명확히 안내합니다.
확장 아이디어
- 최근 문서 목록, 자동 저장/백업
- 다크/라이트 테마 전환, 글꼴/크기 설정 대화창
- 찾아 바꾸기 (replace), 정규식 검색
- 줄 번호 표시, 미니맵/자동 들여쓰기
자주 묻는 질문(FAQ)
Q1. Mac에서 단축키가 안 먹어요.
일부 키보드 레이아웃/입력기에서 Control 대신 Command를 선호할 수 있습니다. 교육용 예제에서는 단순화를 위해 Control로 통일했지만, <Command-s> 등으로 추가 바인딩해도 됩니다.
Q2. 검색 하이라이트 색을 바꾸려면?
self.text.tag_configure("search_hit", background="...")에서 컬러 코드를 바꾸면 됩니다. 브랜드 컬러나 다크 테마에 맞게 조정하세요.
마무리
이상으로 파이썬 Tkinter로 만드는 기본 메모장을 완성했습니다. 파일 관리, 편집 명령, 찾기, 상태표시줄까지 GUI 입문에 필요한 요소를 한 바퀴 경험했을 것입니다. 여기서 제시한 확장 아이디어를 하나씩 추가해 보며, 자신만의 가벼운 에디터를 발전시켜 보세요.
© 2025 교육용 코드 예제 · 자유롭게 변형/활용 가능 (출처 표기 권장)
Comments
Post a Comment