سیستم مدیریت کتابخانه و کتابداری با پایتون و Tkinter (تم ترمینال مدرن)
نرمافزار دسکتاپ سبک و مدرن برای مدیریت کاتالوگ و امانت کتاب، توسعهیافته با رویکرد برنامهنویسی تابعی (Functional) و رابط کاربری اختصاصی سبز و مشکی بدون نیاز به پکیجهای خارجی.

داستان این پروژه
درباره پروژه:
پروژه Library.SYS یک برنامه دسکتاپ مدرن و مینیمال برای مدیریت چرخه کتابها، کاتالوگ و وضعیت امانت در محیطهای آموزشی و کتابخانهها است. این نرمافزار با تمرکز بر سرعت، سادگی و تجربه کاربری (UX) روان طراحی شده و دارای تم تاریک اختصاصی با رنگبندی الهامگرفته از ترمینال/ماتریکس (Green & Black) است.
امکانات و قابلیتهای کلیدی:
مدیریت کامل اطلاعات (CRUD): ثبت کتاب جدید با اعتبارسنجی یکتا بودن شابک (ISBN)، ویرایش سریع مشخصات، و حذف با اخذ تأییدیه.
سیستم گردش و امانت (Circulation): قابلیت جابهجایی آنی وضعیت کتاب بین «موجود (Available)» و «امانتدادهشده (Borrowed)».
کارتهای آماری تعاملی (Interactive KPI Cards): نمایش زنده آمار کل کتابها، موجودی و موارد امانتی با قابلیت فیلتر سریع جدول از طریق کلیک روی کارتها.
جستوجوی آنی (Live Search): فیلتر لحظهای و هوشمند بر اساس عنوان، نویسنده، سال چاپ و شابک.
معماری چندصفحهای (Tab Navigation): ناوبری نرم میان بخشهای داشبورد، مدیریت کاتالوگ، لاگ امانت و تنظیمات.
خروجی دادهها (Data Export): امکان دریافت گزارش کامل اطلاعات کاتالوگ در قالب فایل استاندارد CSV / Excel.
ویژگیهای فنی و پیادهسازی:
توسعهیافته بهصورت ۱۰۰٪ تابعی (Functional Programming) و با مدیریت شفاف وضعیت دادهها (State Management).
بدون وابستگی به کتابخانههای سنگین خارجی (Zero-Dependency)؛ ساختهشده کاملاً با کتابخانههای استاندارد پایتون (tkinter, ttk, csv).
بازطراحی و استایلدهی کامل المانهای رابط کاربری، فونتهای یکپارچه مونواسپیس و جدول سفارشیسازیشده با موتور قالب clam.
نگاهی نزدیکتر
نگاهی به کد
import csv
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
# ==========================================
# 🟢 BLACK & GREEN PALETTE ONLY
# ==========================================
COLOR_BG_DARKEST = "#0A0D0B" # Absolute Deep Black
COLOR_SIDEBAR_BG = "#111713" # Carbon Black
COLOR_CARD_BG = "#161F19" # Dark Slate Green/Black
COLOR_CARD_BORDER = "#1F2E24" # Subtle Emerald Border
COLOR_ACCENT_GREEN = "#00FF66" # Vivid Neon / Terminal Green
COLOR_MUTED_GREEN = "#10B981" # Emerald Green
COLOR_DARK_GREEN = "#064E3B" # Deep Forest Green
COLOR_TEXT_WHITE = "#E5E7EB" # Bright Off-white
COLOR_TEXT_MUTED = "#6EE7B7" # Soft Mint Gray
COLOR_INPUT_BG = "#0E1410" # Input Box Dark Surface
# ==========================================
# 📦 IN-MEMORY STATE (Functional)
# ==========================================
books_db = [
{"id": 1, "title": "Clean Code", "author": "Robert C. Martin", "year": "2008", "isbn": "9780132350884", "status": "Available"},
{"id": 2, "title": "The Pragmatic Programmer", "author": "Andrew Hunt", "year": "1999", "isbn": "9780201616224", "status": "Borrowed"},
{"id": 3, "title": "Design Patterns", "author": "Erich Gamma", "year": "1994", "isbn": "9780201633610", "status": "Available"},
{"id": 4, "title": "Introduction to Algorithms", "author": "Thomas H. Cormen", "year": "2009", "isbn": "9780262033848", "status": "Available"},
{"id": 5, "title": "Refactoring", "author": "Martin Fowler", "year": "2018", "isbn": "9780134757599", "status": "Borrowed"},
]
next_id = 6
active_filter = "ALL" # "ALL", "Available", "Borrowed"
# ==========================================
# ⚙️ HELPER FUNCTIONS
# ==========================================
def get_selected_id(tree):
selected = tree.selection()
if not selected:
return None
val = tree.item(selected[0], "values")[0]
return int(val.replace("#", ""))
def update_dashboard_stats(stat_vars):
total = len(books_db)
borrowed = sum(1 for b in books_db if b["status"] == "Borrowed")
available = total - borrowed
stat_vars["total"].set(str(total))
stat_vars["available"].set(str(available))
stat_vars["borrowed"].set(str(borrowed))
def refresh_table(tree, stat_vars, search_box=None):
for row in tree.get_children():
tree.delete(row)
q = ""
if search_box:
raw_val = search_box.get()
if raw_val != "Search catalog...":
q = raw_val.strip().lower()
for b in books_db:
# Status Filter Check
if active_filter != "ALL" and b["status"] != active_filter:
continue
# Search Query Check
if (q in b["title"].lower() or
q in b["author"].lower() or
q in b["isbn"].lower() or
q in b["status"].lower()):
tag = "available" if b["status"] == "Available" else "borrowed"
tree.insert("", "end", values=(
f"#{b['id']}",
b["title"],
b["author"],
b["year"],
b["isbn"],
b["status"]
), tags=(tag,))
update_dashboard_stats(stat_vars)
# ==========================================
# 🟢 ACTION HANDLERS (Add, Edit, Delete, etc.)
# ==========================================
def handle_filter_click(filter_type, tree, stat_vars, search_box):
global active_filter
active_filter = filter_type
refresh_table(tree, stat_vars, search_box)
def open_book_modal(parent, title_text, initial_data, on_save_callback):
modal = tk.Toplevel(parent)
modal.title(title_text)
modal.geometry("400x380")
modal.configure(bg=COLOR_CARD_BG)
modal.resizable(False, False)
modal.transient(parent)
modal.grab_set()
modal.update_idletasks()
x = parent.winfo_x() + (parent.winfo_width() // 2) - 200
y = parent.winfo_y() + (parent.winfo_height() // 2) - 190
modal.geometry(f"+{x}+{y}")
tk.Label(modal, text=title_text, font=("Consolas", 13, "bold"),
bg=COLOR_CARD_BG, fg=COLOR_ACCENT_GREEN).pack(anchor="w", padx=25, pady=(20, 15))
fields = {}
keys = [("Title", "title"), ("Author", "author"), ("Year", "year"), ("ISBN", "isbn")]
for label_name, key in keys:
frame = tk.Frame(modal, bg=COLOR_CARD_BG)
frame.pack(fill="x", padx=25, pady=4)
tk.Label(frame, text=label_name, font=("Consolas", 9), fg=COLOR_TEXT_MUTED,
bg=COLOR_CARD_BG, width=8, anchor="w").pack(side="left")
entry = tk.Entry(frame, font=("Consolas", 10), bg=COLOR_INPUT_BG, fg=COLOR_TEXT_WHITE,
insertbackground=COLOR_ACCENT_GREEN, relief="flat",
highlightbackground=COLOR_CARD_BORDER, highlightthickness=1)
entry.pack(side="right", fill="x", expand=True, ipady=4)
if initial_data and key in initial_data:
entry.insert(0, initial_data[key])
fields[key] = entry
def save_action():
data = {k: v.get().strip() for k, v in fields.items()}
if not data["title"] or not data["author"]:
messagebox.showwarning("Validation Error", "Title and Author are required!", parent=modal)
return
if on_save_callback(data, modal):
modal.destroy()
btn_frame = tk.Frame(modal, bg=COLOR_CARD_BG)
btn_frame.pack(fill="x", padx=25, pady=(25, 10))
cancel_btn = tk.Button(btn_frame, text="Cancel", font=("Consolas", 9), relief="flat",
bg=COLOR_INPUT_BG, fg=COLOR_TEXT_MUTED, padx=14, pady=5, cursor="hand2", command=modal.destroy)
cancel_btn.pack(side="right", padx=(8, 0))
save_btn = tk.Button(btn_frame, text="Save Record", font=("Consolas", 9, "bold"), relief="flat",
bg=COLOR_ACCENT_GREEN, fg=COLOR_BG_DARKEST, activebackground=COLOR_MUTED_GREEN,
padx=16, pady=5, cursor="hand2", command=save_action)
save_btn.pack(side="right")
def handle_add(parent, tree, stat_vars, search_box):
global next_id
def save_cb(data, modal):
global next_id
if data["isbn"] and any(b["isbn"] == data["isbn"] for b in books_db):
messagebox.showerror("Error", "ISBN already exists!", parent=modal)
return False
books_db.append({
"id": next_id,
"title": data["title"],
"author": data["author"],
"year": data["year"] or "N/A",
"isbn": data["isbn"] or "N/A",
"status": "Available"
})
next_id += 1
refresh_table(tree, stat_vars, search_box)
return True
open_book_modal(parent, "[+] Add New Book", None, save_cb)
def handle_edit(parent, tree, stat_vars, search_box):
b_id = get_selected_id(tree)
if b_id is None:
messagebox.showinfo("Select", "Please select a book from the table first.")
return
book = next(b for b in books_db if b["id"] == b_id)
def save_cb(data, modal):
if data["isbn"] and any(b["isbn"] == data["isbn"] and b["id"] != b_id for b in books_db):
messagebox.showerror("Error", "Another book has this ISBN!", parent=modal)
return False
book["title"] = data["title"]
book["author"] = data["author"]
book["year"] = data["year"] or "N/A"
book["isbn"] = data["isbn"] or "N/A"
refresh_table(tree, stat_vars, search_box)
return True
open_book_modal(parent, f"[*] Edit Book #{b_id}", book, save_cb)
def handle_delete(tree, stat_vars, search_box):
b_id = get_selected_id(tree)
if b_id is None:
messagebox.showinfo("Select", "Please select a book from the table first.")
return
if messagebox.askyesno("Confirm Delete", f"Delete book #{b_id} from database?"):
global books_db
books_db = [b for b in books_db if b["id"] != b_id]
refresh_table(tree, stat_vars, search_box)
def handle_toggle_status(tree, stat_vars, search_box):
b_id = get_selected_id(tree)
if b_id is None:
messagebox.showinfo("Select", "Please select a book first.")
return
book = next(b for b in books_db if b["id"] == b_id)
book["status"] = "Borrowed" if book["status"] == "Available" else "Available"
refresh_table(tree, stat_vars, search_box)
def handle_export_csv():
file_path = filedialog.asksaveasfilename(defaultextension=".csv",
filetypes=[("CSV Files", "*.csv")],
initialfile="library_catalog.csv")
if file_path:
with open(file_path, mode="w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["id", "title", "author", "year", "isbn", "status"])
writer.writeheader()
writer.writerows(books_db)
messagebox.showinfo("Export Successful", f"Saved {len(books_db)} books to CSV!")
# ==========================================
# 📑 NAVIGATION TABS SWITCHER
# ==========================================
def switch_view(view_name, views, nav_buttons):
for name, frame in views.items():
if name == view_name:
frame.pack(fill="both", expand=True)
else:
frame.pack_forget()
for name, btn in nav_buttons.items():
if name == view_name:
btn.config(bg=COLOR_CARD_BG, fg=COLOR_ACCENT_GREEN)
else:
btn.config(bg=COLOR_SIDEBAR_BG, fg=COLOR_TEXT_MUTED)
# ==========================================
# 🖥️ APPLICATION UI BUILDER
# ==========================================
def create_app():
root = tk.Tk()
root.title("Library Terminal Manager")
root.geometry("1100x650")
root.minsize(980, 580)
root.configure(bg=COLOR_BG_DARKEST)
stat_vars = {
"total": tk.StringVar(value="0"),
"available": tk.StringVar(value="0"),
"borrowed": tk.StringVar(value="0"),
}
# 1. SIDEBAR (Pure Black / Forest Accents)
sidebar = tk.Frame(root, bg=COLOR_SIDEBAR_BG, width=220)
sidebar.pack(side="left", fill="y")
sidebar.pack_propagate(False)
brand = tk.Frame(sidebar, bg=COLOR_SIDEBAR_BG)
brand.pack(fill="x", padx=20, pady=(25, 30))
tk.Label(brand, text=">_ LIBRARY.SYS", font=("Consolas", 12, "bold"), fg=COLOR_ACCENT_GREEN, bg=COLOR_SIDEBAR_BG).pack(anchor="w")
tk.Label(brand, text="Catalog & Circulation", font=("Consolas", 8), fg=COLOR_TEXT_MUTED, bg=COLOR_SIDEBAR_BG).pack(anchor="w")
views = {}
nav_buttons = {}
main_container = tk.Frame(root, bg=COLOR_BG_DARKEST)
main_container.pack(side="right", fill="both", expand=True, padx=20, pady=20)
# 2. DEFINING SUB-PAGES (For all sidebar buttons to work)
# Page A: Books Directory (Main)
books_view = tk.Frame(main_container, bg=COLOR_BG_DARKEST)
views["Books"] = books_view
# Page B: Dashboard Stats View
dash_view = tk.Frame(main_container, bg=COLOR_BG_DARKEST)
views["Dashboard"] = dash_view
tk.Label(dash_view, text="DASHBOARD OVERVIEW", font=("Consolas", 16, "bold"), fg=COLOR_ACCENT_GREEN, bg=COLOR_BG_DARKEST).pack(anchor="w", pady=(10, 20))
tk.Label(dash_view, text="Real-time analytics and statistics will show here.", font=("Consolas", 10), fg=COLOR_TEXT_MUTED, bg=COLOR_BG_DARKEST).pack(anchor="w")
# Page C: Borrowing Log View
borrow_view = tk.Frame(main_container, bg=COLOR_BG_DARKEST)
views["Borrowing"] = borrow_view
tk.Label(borrow_view, text="BORROWING & RETURNS LOG", font=("Consolas", 16, "bold"), fg=COLOR_ACCENT_GREEN, bg=COLOR_BG_DARKEST).pack(anchor="w", pady=(10, 20))
tk.Label(borrow_view, text="Click 'Toggle Status' in the Books table to lend or return copies.", font=("Consolas", 10), fg=COLOR_TEXT_MUTED, bg=COLOR_BG_DARKEST).pack(anchor="w")
# Page D: Settings View
settings_view = tk.Frame(main_container, bg=COLOR_BG_DARKEST)
views["Settings"] = settings_view
tk.Label(settings_view, text="SYSTEM SETTINGS", font=("Consolas", 16, "bold"), fg=COLOR_ACCENT_GREEN, bg=COLOR_BG_DARKEST).pack(anchor="w", pady=(10, 20))
tk.Button(settings_view, text="Export Library Database (CSV)", font=("Consolas", 10, "bold"),
bg=COLOR_CARD_BG, fg=COLOR_ACCENT_GREEN, activebackground=COLOR_MUTED_GREEN, relief="flat",
padx=15, pady=8, cursor="hand2", command=handle_export_csv).pack(anchor="w", pady=10)
# Create Navigation Buttons
nav_list = [("📊 Dashboard", "Dashboard"), ("📚 Books", "Books"), ("🔄 Borrowing", "Borrowing"), ("⚙️ Settings", "Settings")]
for text, key in nav_list:
btn = tk.Button(sidebar, text=text, font=("Consolas", 10, "bold"),
bg=COLOR_SIDEBAR_BG, fg=COLOR_TEXT_MUTED, activebackground=COLOR_CARD_BG, activeforeground=COLOR_ACCENT_GREEN,
anchor="w", padx=16, pady=10, relief="flat", cursor="hand2")
btn.config(command=lambda k=key: switch_view(k, views, nav_buttons))
btn.pack(fill="x", padx=10, pady=3)
nav_buttons[key] = btn
# Bottom badge
tk.Label(sidebar, text="● SYSTEM ONLINE\nGreen & Black v2.0", font=("Consolas", 8),
fg=COLOR_MUTED_GREEN, bg=COLOR_SIDEBAR_BG, justify="left").pack(side="bottom", anchor="w", padx=20, pady=20)
# ==========================================
# 📚 BUILDING MAIN BOOKS VIEW
# ==========================================
# Header
header = tk.Frame(books_view, bg=COLOR_BG_DARKEST)
header.pack(fill="x", pady=(0, 15))
tk.Label(header, text="CATALOG INVENTORY", font=("Consolas", 16, "bold"), fg=COLOR_ACCENT_GREEN, bg=COLOR_BG_DARKEST).pack(anchor="w")
tk.Label(header, text="Search, update, toggle and organize records", font=("Consolas", 9), fg=COLOR_TEXT_MUTED, bg=COLOR_BG_DARKEST).pack(anchor="w")
# 3 Interactive Dashboard Cards (Clicking them filters the table!)
stats_bar = tk.Frame(books_view, bg=COLOR_BG_DARKEST)
stats_bar.pack(fill="x", pady=(0, 15))
card_defs = [
("TOTAL BOOKS (Click to show all)", stat_vars["total"], COLOR_TEXT_WHITE, "ALL"),
("AVAILABLE (Click to filter)", stat_vars["available"], COLOR_ACCENT_GREEN, "Available"),
("BORROWED (Click to filter)", stat_vars["borrowed"], COLOR_TEXT_MUTED, "Borrowed"),
]
for title, var, color, f_type in card_defs:
card = tk.Button(stats_bar, bg=COLOR_CARD_BG, relief="flat", highlightbackground=COLOR_CARD_BORDER,
highlightthickness=1, cursor="hand2", activebackground=COLOR_DARK_GREEN)
card.pack(side="left", fill="x", expand=True, padx=(0, 10) if f_type != "Borrowed" else 0)
inner = tk.Frame(card, bg=COLOR_CARD_BG)
inner.pack(fill="both", padx=12, pady=8)
tk.Label(inner, text=title, font=("Consolas", 8, "bold"), fg=COLOR_TEXT_MUTED, bg=COLOR_CARD_BG).pack(anchor="w")
tk.Label(inner, textvariable=var, font=("Consolas", 18, "bold"), fg=color, bg=COLOR_CARD_BG).pack(anchor="w")
# Action Bar (Search + Buttons)
action_bar = tk.Frame(books_view, bg=COLOR_BG_DARKEST)
action_bar.pack(fill="x", pady=(0, 12))
search_box = tk.Entry(action_bar, font=("Consolas", 10), bg=COLOR_INPUT_BG, fg=COLOR_TEXT_MUTED,
insertbackground=COLOR_ACCENT_GREEN, relief="flat",
highlightbackground=COLOR_CARD_BORDER, highlightthickness=1, width=28)
search_box.pack(side="left", ipady=6, padx=(0, 10))
search_box.insert(0, "Search catalog...")
# Wire cards click filter
for btn_card, (_, _, _, f_type) in zip(stats_bar.winfo_children(), card_defs):
btn_card.config(command=lambda ft=f_type: handle_filter_click(ft, tree, stat_vars, search_box))
def on_search_focus_in(e):
if search_box.get() == "Search catalog...":
search_box.delete(0, "end")
search_box.config(fg=COLOR_TEXT_WHITE)
def on_search_focus_out(e):
if not search_box.get():
search_box.insert(0, "Search catalog...")
search_box.config(fg=COLOR_TEXT_MUTED)
search_box.bind("<FocusIn>", on_search_focus_in)
search_box.bind("<FocusOut>", on_search_focus_out)
search_box.bind("<KeyRelease>", lambda e: refresh_table(tree, stat_vars, search_box))
# All Action Buttons on Right
del_btn = tk.Button(action_bar, text="Delete", font=("Consolas", 9, "bold"), relief="flat",
bg=COLOR_CARD_BG, fg="#F87171", activebackground="#7F1D1D", padx=12, pady=5,
cursor="hand2", command=lambda: handle_delete(tree, stat_vars, search_box))
del_btn.pack(side="right", padx=(6, 0))
toggle_btn = tk.Button(action_bar, text="Toggle Status", font=("Consolas", 9, "bold"), relief="flat",
bg=COLOR_CARD_BG, fg=COLOR_TEXT_MUTED, activebackground=COLOR_DARK_GREEN,
padx=12, pady=5, cursor="hand2", command=lambda: handle_toggle_status(tree, stat_vars, search_box))
toggle_btn.pack(side="right", padx=(6, 0))
edit_btn = tk.Button(action_bar, text="Edit", font=("Consolas", 9, "bold"), relief="flat",
bg=COLOR_CARD_BG, fg=COLOR_ACCENT_GREEN, activebackground=COLOR_DARK_GREEN,
padx=14, pady=5, cursor="hand2", command=lambda: handle_edit(root, tree, stat_vars, search_box))
edit_btn.pack(side="right", padx=(6, 0))
add_btn = tk.Button(action_bar, text="+ Add Book", font=("Consolas", 9, "bold"), relief="flat",
bg=COLOR_ACCENT_GREEN, fg=COLOR_BG_DARKEST, activebackground=COLOR_MUTED_GREEN,
padx=16, pady=5, cursor="hand2", command=lambda: handle_add(root, tree, stat_vars, search_box))
add_btn.pack(side="right", padx=(6, 0))
# 4. GREEN & BLACK STYLED TREEVIEW TABLE
table_card = tk.Frame(books_view, bg=COLOR_CARD_BG, highlightbackground=COLOR_CARD_BORDER, highlightthickness=1)
table_card.pack(fill="both", expand=True)
style = ttk.Style()
style.theme_use("clam")
style.configure("Treeview",
background=COLOR_CARD_BG,
foreground=COLOR_TEXT_WHITE,
rowheight=32,
fieldbackground=COLOR_CARD_BG,
font=("Consolas", 9),
borderwidth=0)
style.configure("Treeview.Heading",
background=COLOR_INPUT_BG,
foreground=COLOR_ACCENT_GREEN,
font=("Consolas", 8, "bold"),
relief="flat",
borderwidth=0)
style.map("Treeview",
background=[("selected", COLOR_DARK_GREEN)],
foreground=[("selected", COLOR_ACCENT_GREEN)])
cols = ("id", "title", "author", "year", "isbn", "status")
tree = ttk.Treeview(table_card, columns=cols, show="headings", selectmode="browse")
tree.heading("id", text="ID")
tree.heading("title", text="TITLE")
tree.heading("author", text="AUTHOR")
tree.heading("year", text="YEAR")
tree.heading("isbn", text="ISBN")
tree.heading("status", text="STATUS")
tree.column("id", width=65, anchor="center")
tree.column("title", width=290, anchor="w")
tree.column("author", width=200, anchor="w")
tree.column("year", width=80, anchor="center")
tree.column("isbn", width=170, anchor="center")
tree.column("status", width=110, anchor="center")
tree.tag_configure("available", foreground=COLOR_ACCENT_GREEN)
tree.tag_configure("borrowed", foreground=COLOR_TEXT_MUTED)
scroll = ttk.Scrollbar(table_card, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=scroll.set)
tree.pack(side="left", fill="both", expand=True, padx=1, pady=1)
scroll.pack(side="right", fill="y", pady=1)
# Initial view & data load
switch_view("Books", views, nav_buttons)
refresh_table(tree, stat_vars, search_box)
return root
if __name__ == "__main__":
app = create_app()
app.mainloop()


