iT邦幫忙

0

[Day26]抽籤/點名器 GUI—基礎版

  • 分享至 

  • xImage
  •  

今天做一個可直接上課/活動用的抽籤/點名器。設計目標很單純:
最小可用(MVP):匯入名單 → 抽一個 → 不重複 → 看得見剩餘與已抽中
整個工具只用 Python 標準庫(Tkinter、csv、random、pathlib),無需安裝任何套件。

為什麼這樣設計?

  • 不重複抽取:每次抽到的名字會從候選池移除,直到抽完才需重置。
  • 隨機來源:用 random.SystemRandom()(系統強隨機源)比單純 random.random() 更適合抽籤用途。
  • 名單相容性:支援 TXT(每行一個名字)與 CSV(取第一欄),快速套用你現成的名單。
  • 狀態透明:同畫面顯示「候選名單」與「中獎名單」,並有「剩餘|已抽」統計。
  • 零相依:在沒有網路/沒有 pip 的環境也能用(比賽、教室、會議室)。

功能清單(MVP)

  • 匯入名單:TXT/CSV(第一欄),自動略過重複
  • 手動新增姓名
  • 抽一個(不重複,直到抽完)
  • 「剩餘人數」、「已抽中人數」即時更新
  • 快捷鍵:Space / Enter 直接抽
  • 重置:把已抽中的人放回候選池,清空中獎清單

檔案格式說明
TXT
王小明
李小華
陳大雄
CSV(取第一欄,不當作表頭)
姓名,系級
王小明,資工三
李小華,資工四
程式碼(存成 raffle_gui_basic.py)

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import csv, random
from pathlib import Path

class RaffleBasic:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("抽籤/點名器 - 基礎版")

        # 狀態
        self.pool = []      # 尚未抽到的名單
        self.winners = []   # 已抽中的名單(依抽出順序)
        self.rng = random.SystemRandom()

        self._build_ui()
        self._refresh_counts()

    def _build_ui(self):
        main = ttk.Frame(self.root, padding=16); main.grid(sticky="nsew")
        self.root.rowconfigure(0, weight=1); self.root.columnconfigure(0, weight=1)
        main.columnconfigure(0, weight=1); main.columnconfigure(1, weight=1)
        main.rowconfigure(2, weight=1)

        # 控制列
        top = ttk.Frame(main); top.grid(row=0, column=0, columnspan=2, sticky="we", pady=(0,8))
        ttk.Button(top, text="匯入名單", command=self.import_list).grid(row=0, column=0, padx=4)
        ttk.Button(top, text="重置", command=self.reset_all).grid(row=0, column=1, padx=4)

        # 顯示當前抽中
        display = ttk.Frame(main); display.grid(row=1, column=0, columnspan=2, sticky="we", pady=8)
        display.columnconfigure(0, weight=1)
        ttk.Label(display, text="抽中:", font=("Segoe UI", 11)).grid(row=0, column=0, sticky="w")
        self.var_current = tk.StringVar(value="—")
        ttk.Label(display, textvariable=self.var_current, font=("Segoe UI", 20)).grid(row=1, column=0, sticky="w")

        # 左:候選
        left = ttk.Frame(main); left.grid(row=2, column=0, sticky="nsew", padx=(0,8))
        left.columnconfigure(0, weight=1); left.rowconfigure(1, weight=1)
        ttk.Label(left, text="候選名單").grid(row=0, column=0, sticky="w")
        self.list_pool = tk.Listbox(left, height=12)
        self.list_pool.grid(row=1, column=0, sticky="nsew")

        # 右:已抽中
        right = ttk.Frame(main); right.grid(row=2, column=1, sticky="nsew")
        right.columnconfigure(0, weight=1); right.rowconfigure(1, weight=1)
        ttk.Label(right, text="中獎名單(最新在最上方)").grid(row=0, column=0, sticky="w")
        self.list_win = tk.Listbox(right, height=12)
        self.list_win.grid(row=1, column=0, sticky="nsew")

        # 底部:新增+抽取+統計
        bottom_left = ttk.Frame(main); bottom_left.grid(row=3, column=0, sticky="we", pady=(8,0))
        ttk.Label(bottom_left, text="新增姓名").grid(row=0, column=0)
        self.entry_new = ttk.Entry(bottom_left, width=18); self.entry_new.grid(row=0, column=1, padx=4)
        ttk.Button(bottom_left, text="加入", command=self.add_name).grid(row=0, column=2, padx=4)

        bottom_right = ttk.Frame(main); bottom_right.grid(row=3, column=1, sticky="we", pady=(8,0))
        ttk.Button(bottom_right, text="抽一個 (Space/Enter)", command=self.draw_one).grid(row=0, column=0, padx=4)
        self.lbl_counts = ttk.Label(bottom_right, text="剩餘:0|已抽:0")
        self.lbl_counts.grid(row=0, column=1, sticky="e")

        # 快捷鍵
        self.root.bind("<space>", lambda e: self.draw_one())
        self.root.bind("<Return>", lambda e: self.draw_one())

    # ---- 功能 ----
    def _refresh_counts(self):
        self.lbl_counts.config(text=f"剩餘:{len(self.pool)}|已抽:{len(self.winners)}")

    def _reload_pool(self):
        self.list_pool.delete(0, tk.END)
        for n in self.pool:
            self.list_pool.insert(tk.END, n)

    def import_list(self):
        path = filedialog.askopenfilename(
            title="匯入名單(TXT 或 CSV)",
            filetypes=[("文字檔或 CSV", "*.txt *.csv"), ("所有檔案", "*.*")]
        )
        if not path: return
        p = Path(path)
        names = []
        try:
            if p.suffix.lower() == ".csv":
                with p.open("r", encoding="utf-8-sig", newline="") as f:
                    for row in csv.reader(f):
                        if row and row[0].strip():
                            names.append(row[0].strip())
            else:
                with p.open("r", encoding="utf-8") as f:
                    for line in f:
                        name = line.strip()
                        if name:
                            names.append(name)
        except Exception as e:
            messagebox.showerror("匯入失敗", str(e)); return

        # 去重(不重複加入既有 pool / winners)
        existing = set(self.pool) | set(self.winners)
        added = [n for n in names if n not in existing]
        if not added:
            messagebox.showinfo("提示", "沒有可新增的項目(可能全都重複)"); return
        self.pool.extend(added)
        self._reload_pool(); self._refresh_counts()
        messagebox.showinfo("完成", f"已匯入 {len(added)} 筆(略過重複)")

    def add_name(self):
        name = self.entry_new.get().strip()
        if not name: return
        if name in self.pool or name in self.winners:
            messagebox.showwarning("重複", f"「{name}」已存在"); return
        self.pool.append(name)
        self.entry_new.delete(0, tk.END)
        self._reload_pool(); self._refresh_counts()

    def reset_all(self):
        if messagebox.askyesno("重置", "清空中獎名單並回復所有候選?"):
            self.pool += self.winners
            self.winners.clear()
            self.var_current.set("—")
            self._reload_pool()
            self.list_win.delete(0, tk.END)
            self._refresh_counts()

    def draw_one(self):
        if not self.pool:
            messagebox.showinfo("提示", "候選名單已抽完,請重置或新增成員"); return
        idx = self.rng.randrange(len(self.pool))
        name = self.pool.pop(idx)
        self.winners.append(name)
        self.var_current.set(name)
        self._reload_pool()
        self.list_win.insert(0, name)
        self._refresh_counts()
        self.root.bell()

def main():
    root = tk.Tk()
    app = RaffleBasic(root)
    root.mainloop()

if __name__ == "__main__":
    main()

使用方式

python raffle_gui_basic.py

操作流程
點「匯入名單」選 TXT/CSV 檔(或在左下角用「新增姓名」補人)。
直接按「抽一個」或鍵盤 Space/Enter。
「中獎名單」會把最新的放最上方,方便投影查看。
抽完會提示「候選名單已抽完」,可按「重置」把人放回去再抽。

實作:
https://ithelp.ithome.com.tw/upload/images/20251017/20169368X5gQXW4Nax.png
https://ithelp.ithome.com.tw/upload/images/20251017/201693681QmtsYHohC.png
常見問題(FAQ)

1.匯入 CSV 亂碼?
用 UTF-8 或 UTF-8 with BOM(utf-8-sig)匯出最安全。
2.名單重複?
會自動略過已存在於候選或已中獎清單中的名字。
3.抽到空白/雜訊?
TXT 建議每行只放姓名;CSV 以第一欄為姓名,其餘欄位會被忽略。
4.抽到一半想加人?
直接在「新增姓名」輸入框加入;不影響已抽中的結果。
5.怎麼保證公平?
使用 random.SystemRandom() 取自系統熵源,適合抽籤/點名用途。

Day 27 預告
復原上一抽(撤回剛抽出的名字)
抽五個(批次)
雙擊候選清單刪除
匯出中獎 CSV(含順序)


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言