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

Popular posts from this blog

πŸš€ Simple Login & Registration System in Python Tkinter πŸ“±

πŸš€ Create a Python Screen Recorder with Audio (Complete Code)

πŸ“‘ Fuzzu Packet Sniffer – Python GUI for Real-Time IP Monitoring | Tkinter + Scapy