CyberKanban — Trello-like Kanban in Python (CTkinter + SQLite) with Drag-Drop
Demo :
Click Video πππ
Description :
Build a Trello-style Kanban in Python using CTkinter + SQLite. Drag-drop, edit, search, dark UI—full demo + code notes.
Features :
-
Enable Search Description (use meta description above)
-
Allow Reader Comments
-
Show Share Buttons
-
Use HTTPS
-
Insert a hero image (9:16 or 16:9) with “PYTHON KANBAN — CTkinter + SQLite”
-
Embed the Short + paste code highlights (key classes:
KanbanDB
,TaskCard
,Column
,App
) -
CTA: “Comment KANBAN for repo + full tutorial.”
Code :
# CyberKanban: Trello-like Kanban Board in Python (CTkinter + SQLite)
# Author: FuzzuTech Helper
# Requirements: customtkinter (pip install customtkinter)
# Run: python main.py
import sqlite3
import datetime
import sys
import os
import tkinter as tk
import customtkinter as ctk
APP_TITLE = "CyberKanban – CTkinter + SQLite"
DB_PATH = "kanban.db"
STATUSES = ["To Do", "In Progress", "Done"]
class KanbanDB:
def __init__(self, path):
self.path = path
self.conn = sqlite3.connect(self.path)
self._init_schema()
def _init_schema(self):
cur = self.conn.cursor()
cur.execute('''CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'To Do',
created_at TEXT NOT NULL
);''')
self.conn.commit()
def add_task(self, title, description, status="To Do"):
cur = self.conn.cursor()
cur.execute("INSERT INTO tasks (title, description, status, created_at) VALUES (?, ?, ?, ?)",
(title.strip(), description.strip(), status, datetime.datetime.now().isoformat(timespec="seconds")))
self.conn.commit()
return cur.lastrowid
def update_task(self, task_id, **fields):
if not fields: return
cols = ", ".join([f"{k}=?" for k in fields])
vals = list(fields.values()) + [task_id]
self.conn.execute(f"UPDATE tasks SET {cols} WHERE id=?", vals)
self.conn.commit()
def delete_task(self, task_id):
self.conn.execute("DELETE FROM tasks WHERE id=?", (task_id,))
self.conn.commit()
def fetch_by_status(self, status, search=""):
cur = self.conn.cursor()
if search:
like = f"%{search.strip()}%"
cur.execute("SELECT id, title, description, status, created_at FROM tasks WHERE status=? AND (title LIKE ? OR description LIKE ?) ORDER BY id DESC",
(status, like, like))
else:
cur.execute("SELECT id, title, description, status, created_at FROM tasks WHERE status=? ORDER BY id DESC",
(status,))
return cur.fetchall()
def get(self, task_id):
cur = self.conn.cursor()
cur.execute("SELECT id, title, description, status, created_at FROM tasks WHERE id=?", (task_id,))
return cur.fetchone()
class TaskCard(ctk.CTkFrame):
def __init__(self, master, app, task_tuple, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.app = app
self.task = task_tuple # (id, title, desc, status, created_at)
self.configure(corner_radius=16, border_width=1, border_color=self.app.palette["card_border"])
self._build_ui()
self._bind_drag()
def _build_ui(self):
id_, title, desc, status, created_at = self.task
self.grid_columnconfigure(0, weight=1)
badge = ctk.CTkLabel(self, text=f"#{id_}", text_color=self.app.palette["badge_text"])
badge.grid(row=0, column=0, sticky="w", padx=10, pady=(8,0))
self.title_lbl = ctk.CTkLabel(self, text=title, font=ctk.CTkFont(size=14, weight="bold"), wraplength=240, justify="left")
self.title_lbl.grid(row=1, column=0, sticky="w", padx=10, pady=(2,0))
if desc:
self.desc_lbl = ctk.CTkLabel(self, text=desc, font=ctk.CTkFont(size=12), wraplength=240, justify="left")
self.desc_lbl.grid(row=2, column=0, sticky="w", padx=10, pady=(2,6))
meta = ctk.CTkLabel(self, text=f"{status} • {created_at.split('T')[0]}", font=ctk.CTkFont(size=11), text_color=self.app.palette["muted"])
meta.grid(row=3, column=0, sticky="w", padx=10, pady=(0,6))
btn_row = ctk.CTkFrame(self, fg_color="transparent")
btn_row.grid(row=4, column=0, sticky="ew", padx=8, pady=(0,8))
btn_row.grid_columnconfigure((0,1,2), weight=1)
self.edit_btn = ctk.CTkButton(btn_row, text="✏️ Edit", command=self._edit, height=28, corner_radius=12)
self.edit_btn.grid(row=0, column=0, padx=4)
self.move_btn = ctk.CTkButton(btn_row, text="➡️ Move", command=self._move_next, height=28, corner_radius=12)
self.move_btn.grid(row=0, column=1, padx=4)
self.del_btn = ctk.CTkButton(btn_row, text="π️ Delete", fg_color=self.app.palette["danger"], hover_color=self.app.palette["danger_hover"],
command=self._delete, height=28, corner_radius=12)
self.del_btn.grid(row=0, column=2, padx=4)
def _edit(self):
id_, title, desc, status, _ = self.task
EditTaskDialog(self.app, id_, title, desc, status, on_save=self._on_edit_saved)
def _on_edit_saved(self, task_id, new_title, new_desc, new_status):
self.app.db.update_task(task_id, title=new_title, description=new_desc, status=new_status)
self.app.refresh_all()
def _move_next(self):
id_, title, desc, status, _ = self.task
idx = STATUSES.index(status)
new_status = STATUSES[(idx + 1) % len(STATUSES)]
self.app.db.update_task(id_, status=new_status)
self.app.toast(f"Moved to {new_status}")
self.app.refresh_all()
def _delete(self):
self.app.db.delete_task(self.task[0])
self.app.toast("Task deleted")
self.app.refresh_all()
# ---- Simple Drag & Drop ----
def _bind_drag(self):
self.bind("<Button-1>", self._start_drag)
self.bind("<B1-Motion>", self._on_drag)
self.bind("<ButtonRelease-1>", self._end_drag)
for child in self.winfo_children():
child.bind("<Button-1>", self._start_drag)
child.bind("<B1-Motion>", self._on_drag)
child.bind("<ButtonRelease-1>", self._end_drag)
self._drag_data = {"x":0, "y":0, "dragging": False}
def _start_drag(self, event):
self._drag_data["x"] = event.x
self._drag_data["y"] = event.y
self._drag_data["dragging"] = True
self.lift()
def _on_drag(self, event):
if not self._drag_data["dragging"]:
return
# Create a toplevel ghost during drag for smoothness
if not hasattr(self, "_ghost"):
self._ghost = tk.Toplevel(self)
self._ghost.overrideredirect(True)
self._ghost.attributes("-alpha", 0.9)
self._ghost.configure(bg="#222222")
lbl = tk.Label(self._ghost, text=self.title_lbl.cget("text"), fg="white", bg="#222222", font=("Arial", 12, "bold"), padx=10, pady=6)
lbl.pack()
# Position ghost near cursor
self._ghost.geometry(f"+{self.winfo_pointerx()+12}+{self.winfo_pointery()+12}")
# Highlight columns under cursor
self.app.highlight_column_under_cursor()
def _end_drag(self, event):
if not self._drag_data["dragging"]:
return
self._drag_data["dragging"] = False
# Determine drop target
target_status = self.app.get_column_under_cursor()
if target_status and target_status != self.task[3]:
self.app.db.update_task(self.task[0], status=target_status)
self.app.toast(f"Dropped into {target_status}")
self.app.refresh_all()
# cleanup ghost + remove highlights
if hasattr(self, "_ghost"):
self._ghost.destroy()
delattr(self, "_ghost")
self.app.clear_highlights()
class AddTaskDialog(ctk.CTkToplevel):
def __init__(self, app, on_add):
super().__init__(app)
self.app = app
self.on_add = on_add
self.title("New Task")
self.geometry("480x360+120+120")
self.resizable(False, False)
self.configure(bg=app.palette["bg"])
container = ctk.CTkFrame(self, corner_radius=16)
container.pack(fill="both", expand=True, padx=12, pady=12)
container.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(container, text="Title").grid(row=0, column=0, sticky="w", padx=10, pady=8)
self.title_entry = ctk.CTkEntry(container, placeholder_text="e.g., Recon for bug bounty target")
self.title_entry.grid(row=0, column=1, sticky="ew", padx=10, pady=8)
ctk.CTkLabel(container, text="Description").grid(row=1, column=0, sticky="nw", padx=10, pady=8)
self.desc_txt = ctk.CTkTextbox(container, height=140)
self.desc_txt.grid(row=1, column=1, sticky="nsew", padx=10, pady=8)
ctk.CTkLabel(container, text="Status").grid(row=2, column=0, sticky="w", padx=10, pady=8)
self.status_opt = ctk.CTkOptionMenu(container, values=STATUSES)
self.status_opt.set("To Do")
self.status_opt.grid(row=2, column=1, sticky="w", padx=10, pady=8)
btn_row = ctk.CTkFrame(container, fg_color="transparent")
btn_row.grid(row=3, column=0, columnspan=2, pady=12)
ctk.CTkButton(btn_row, text="Cancel", command=self.destroy).pack(side="left", padx=6)
ctk.CTkButton(btn_row, text="Add Task", command=self._submit).pack(side="left", padx=6)
self.title_entry.focus_set()
def _submit(self):
title = self.title_entry.get().strip()
desc = self.desc_txt.get("1.0", "end").strip()
status = self.status_opt.get()
if not title:
self.app.toast("Please enter a title")
return
self.on_add(title, desc, status)
self.destroy()
class EditTaskDialog(ctk.CTkToplevel):
def __init__(self, app, task_id, title, desc, status, on_save):
super().__init__(app)
self.app = app
self.task_id = task_id
self.on_save = on_save
self.title(f"Edit Task #{task_id}")
self.geometry("520x420+140+140")
self.resizable(False, False)
self.configure(bg=app.palette["bg"])
container = ctk.CTkFrame(self, corner_radius=16)
container.pack(fill="both", expand=True, padx=12, pady=12)
container.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(container, text="Title").grid(row=0, column=0, sticky="w", padx=10, pady=8)
self.title_entry = ctk.CTkEntry(container)
self.title_entry.insert(0, title)
self.title_entry.grid(row=0, column=1, sticky="ew", padx=10, pady=8)
ctk.CTkLabel(container, text="Description").grid(row=1, column=0, sticky="nw", padx=10, pady=8)
self.desc_txt = ctk.CTkTextbox(container, height=180)
self.desc_txt.insert("1.0", desc)
self.desc_txt.grid(row=1, column=1, sticky="nsew", padx=10, pady=8)
ctk.CTkLabel(container, text="Status").grid(row=2, column=0, sticky="w", padx=10, pady=8)
self.status_opt = ctk.CTkOptionMenu(container, values=STATUSES)
self.status_opt.set(status if status in STATUSES else "To Do")
self.status_opt.grid(row=2, column=1, sticky="w", padx=10, pady=8)
btn_row = ctk.CTkFrame(container, fg_color="transparent")
btn_row.grid(row=3, column=0, columnspan=2, pady=12)
ctk.CTkButton(btn_row, text="Cancel", command=self.destroy).pack(side="left", padx=6)
ctk.CTkButton(btn_row, text="Save Changes", command=self._submit).pack(side="left", padx=6)
self.title_entry.focus_set()
def _submit(self):
new_title = self.title_entry.get().strip()
new_desc = self.desc_txt.get("1.0", "end").strip()
new_status = self.status_opt.get()
if not new_title:
self.app.toast("Please enter a title")
return
self.on_save(self.task_id, new_title, new_desc, new_status)
self.destroy()
class Column(ctk.CTkFrame):
def __init__(self, master, app, status_name):
super().__init__(master, corner_radius=18, border_width=2)
self.app = app
self.status_name = status_name
self.configure(border_color=app.palette["col_border"], fg_color=app.palette["col_bg"])
self.header = ctk.CTkLabel(self, text=f"{status_name}", font=ctk.CTkFont(size=16, weight="bold"))
self.header.pack(anchor="w", padx=12, pady=(10,4))
self.body = ctk.CTkScrollableFrame(self, corner_radius=14, fg_color=app.palette["col_inner"])
self.body.pack(fill="both", expand=True, padx=10, pady=8)
def render_items(self, items):
# clear
for w in self.body.winfo_children():
w.destroy()
if not items:
empty = ctk.CTkLabel(self.body, text="No tasks", text_color=self.app.palette["muted"])
empty.pack(pady=10)
else:
for row in items:
card = TaskCard(self.body, self.app, row)
card.pack(fill="x", padx=6, pady=6)
class App(ctk.CTk):
def __init__(self):
super().__init__()
# Theme + palette
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("dark-blue")
self.palette = {
"bg": "#0b0f17",
"col_bg": "#111827",
"col_inner": "#0f172a",
"col_border": "#1f2a44",
"card_border": "#27324d",
"muted": "#94a3b8",
"danger": "#ef4444",
"danger_hover": "#dc2626",
"badge_text": "#a5b4fc"
}
self.title(APP_TITLE)
self.geometry("1180x680")
self.minsize(1000, 620)
self.db = KanbanDB(DB_PATH)
self.search_query = tk.StringVar()
self._build_layout()
self.refresh_all()
def _build_layout(self):
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
# Top bar
top = ctk.CTkFrame(self, corner_radius=0, fg_color=self.palette["bg"])
top.grid(row=0, column=0, sticky="ew")
top.grid_columnconfigure(3, weight=1)
title = ctk.CTkLabel(top, text="π§© CyberKanban", font=ctk.CTkFont(size=22, weight="bold"))
title.grid(row=0, column=0, padx=14, pady=10)
self.search_entry = ctk.CTkEntry(top, textvariable=self.search_query, placeholder_text="Search tasks… (title/description)")
self.search_entry.grid(row=0, column=1, padx=8, pady=10, sticky="ew")
self.search_entry.bind("<Return>", lambda e: self.refresh_all())
ctk.CTkButton(top, text="Search", command=self.refresh_all).grid(row=0, column=2, padx=6, pady=10)
add_btn = ctk.CTkButton(top, text="+ New Task", command=self.open_add_dialog, height=36, corner_radius=14)
add_btn.grid(row=0, column=4, padx=12, pady=10)
# Sub bar
sub = ctk.CTkFrame(self, fg_color=self.palette["col_bg"])
sub.grid(row=1, column=0, sticky="ew")
ctk.CTkLabel(sub, text="Tip: Drag a card and drop into a column to change status.",
text_color=self.palette["muted"]).pack(anchor="w", padx=12, pady=6)
# Columns Row
cols = ctk.CTkFrame(self, fg_color=self.palette["bg"])
cols.grid(row=2, column=0, sticky="nsew", padx=10, pady=10)
cols.grid_columnconfigure((0,1,2), weight=1)
cols.grid_rowconfigure(0, weight=1)
self.columns = {}
for i, name in enumerate(STATUSES):
col = Column(cols, self, name)
col.grid(row=0, column=i, sticky="nsew", padx=8, pady=8)
self.columns[name] = col
# Footer
foot = ctk.CTkFrame(self, fg_color=self.palette["bg"])
foot.grid(row=3, column=0, sticky="ew")
self.status_label = ctk.CTkLabel(foot, text="Ready", text_color=self.palette["muted"])
self.status_label.pack(side="left", padx=10, pady=6)
def open_add_dialog(self):
AddTaskDialog(self, self._on_add_task)
def _on_add_task(self, title, desc, status):
self.db.add_task(title, desc, status)
self.toast("Task added")
self.refresh_all()
def refresh_all(self):
q = self.search_query.get().strip()
total = 0
for name, col in self.columns.items():
items = self.db.fetch_by_status(name, q)
total += len(items)
col.render_items(items)
self.status_label.configure(text=f"Tasks: {total}")
self.clear_highlights()
# ---- Drag & Drop helpers ----
def _hit_test_column(self, x_root, y_root):
for name, col in self.columns.items():
bx = col.body.winfo_rootx()
by = col.body.winfo_rooty()
bw = col.body.winfo_width()
bh = col.body.winfo_height()
if bx <= x_root <= bx + bw and by <= y_root <= by + bh:
return name
return None
def highlight_column_under_cursor(self):
status = self.get_column_under_cursor()
for name, col in self.columns.items():
if name == status:
col.configure(border_color="#38bdf8")
else:
col.configure(border_color=self.palette["col_border"])
def clear_highlights(self):
for col in self.columns.values():
col.configure(border_color=self.palette["col_border"])
def get_column_under_cursor(self):
return self._hit_test_column(self.winfo_pointerx(), self.winfo_pointery())
# ---- UX helpers ----
def toast(self, message):
# subtle status pop
self.status_label.configure(text=message)
self.after(1600, lambda: self.status_label.configure(text="Ready"))
if __name__ == "__main__":
# Ensure working dir is script dir
try:
os.chdir(os.path.dirname(os.path.abspath(__file__)))
except Exception:
pass
app = App()
app.mainloop()
Comments
Post a Comment