2025年10月18日 星期六

勤益科大成績計算系統

 

勤益科大成績計算系統


匯入程式前需修改 EXCEL如下 匯入才不會錯誤



成績管理系統。它包含了:

  1. 使用 score114.db 資料庫。

  2. 資料表欄位包含學號、姓名、班級名稱、入學畢業系科、平時成績(最多6次)、期中、期末、個人加減分、總成績

  3. 總成績計算邏輯

    • 平時成績平均 $ = (\text{平1} + \text{平2} + \dots + \text{平}N) / N$,其中 $N$ 是有分數輸入的平時成績次數(3到6次)。

    • $\text{總成績} = (\text{平時成績平均} \times \text{平時成績權重}) + (\text{期中} \times \text{期中權重}) + (\text{期末} \times \text{期末權重}) + \text{個人加減分}$

    • 備註:由於輸入檔案標題列提供了不同的平時成績權重 (平時成績30%, 平時成績30%...),但在描述中只提到了三個加權項目,我將平時成績權重、期中權重、期末權重設為系統的可變參數,預設為 $30\%$, $35\%, 35\%$。使用者可以在系統中設定這些權重。

  4. 系統功能:新增學生資料查詢所有學生資料修改學生資料刪除學生資料刪除整個資料庫

  5. 匯入學生基本資料功能(從 CSV/Excel 檔案的匯入)。

在匯入學生基本資料時,只匯入 學號姓名班級名稱入學畢業系科 這四個欄位,而其他成績欄位(平時成績、期中、期末、加減分、總成績)則設定為預設值None。這通常用於初始化學生名冊。


PYTHON程式

import tkinter as tk

from tkinter import messagebox, filedialog

from tkinter import ttk

import sqlite3

import pandas as pd

import math

import os


# 資料庫操作類

class ScoreDB:

    def __init__(self, db_name="score114.db"):

        self.conn = None

        self.cursor = None

        self.db_name = db_name

        self.connect()

        self.create_table()


    def connect(self):

        """連接或創建 SQLite 資料庫檔案"""

        try:

            self.conn = sqlite3.connect(self.db_name)

            self.cursor = self.conn.cursor()

        except sqlite3.Error as e:

            print(f"無法連接資料庫: {e}") 

            messagebox.showerror("資料庫錯誤", f"無法連接資料庫: {e}")


    def create_table(self):

        """創建 scores 資料表"""

        self.cursor.execute("""

            CREATE TABLE IF NOT EXISTS scores (

                學號 TEXT PRIMARY KEY,

                姓名 TEXT,

                班級名稱 TEXT,

                入學畢業系科 TEXT,

                平1 REAL,

                平2 REAL,

                平3 REAL,

                平4 REAL,

                平5 REAL,

                平6 REAL,

                期中 REAL,

                期末 REAL,

                個人加減分 REAL,

                總成績 REAL

            )

        """)

        self.conn.commit()


    def disconnect(self):

        """斷開資料庫連接"""

        if self.conn:

            self.conn.close()


    def _calculate_total_score(self, scores_data, weights):

        """

        根據輸入的成績和平時成績次數計算總成績。

        總成績 = (平時平均 * 平時權重) + (期中 * 期中權重) + (期末 * 期末權重) + 個人加減分

        """

        p_scores = [scores_data.get(f'平{i}') for i in range(1, 7)]

        valid_p_scores = [float(s) for s in p_scores if s is not None and s != '']

        

        num_p_scores = len(valid_p_scores)

        avg_p = sum(valid_p_scores) / num_p_scores if num_p_scores > 0 else 0.0


        midterm = float(scores_data.get('期中') if scores_data.get('期中') is not None else 0.0)

        final = float(scores_data.get('期末') if scores_data.get('期末') is not None else 0.0)

        bonus = float(scores_data.get('個人加減分') if scores_data.get('個人加減分') is not None else 0.0)


        total_score = (avg_p * weights['P']) + \

                      (midterm * weights['M']) + \

                      (final * weights['F']) + \

                      bonus

        

        total_score = max(0.0, min(100.0, total_score))

        return round(total_score, 2)


    def insert_score(self, data, weights):

        """插入新學生資料並計算總成績"""

        try:

            # 確保資料中包含所有計算所需的鍵,即使是 None

            calc_data = {k: data.get(k) for k in ['平1', '平2', '平3', '平4', '平5', '平6', '期中', '期末', '個人加減分']}

            total_score = self._calculate_total_score(calc_data, weights)

            data['總成績'] = total_score

            

            columns = ', '.join(data.keys())

            placeholders = ', '.join('?' * len(data))

            values = tuple(data.values())


            query = f"INSERT INTO scores ({columns}) VALUES ({placeholders})"

            self.cursor.execute(query, values)

            self.conn.commit()

            return True

        except sqlite3.IntegrityError:

            messagebox.showerror("錯誤", f"學號 {data['學號']} 已存在。請使用修改功能。")

            return False

        except Exception as e:

            print(f"插入資料失敗: {e}")

            messagebox.showerror("錯誤", f"插入資料失敗: {e}")

            return False


    def get_all_scores(self):

        """查詢所有學生資料"""

        self.cursor.execute("SELECT * FROM scores ORDER BY 學號")

        return self.cursor.fetchall()


    def update_score(self, student_id, data, weights):

        """更新學生資料並重新計算總成績"""

        try:

            # 必須先確保 data 包含所有計算所需的鍵

            required_keys = ['平1', '平2', '平3', '平4', '平5', '平6', '期中', '期末', '個人加減分']

            calc_data = {k: data.get(k) for k in required_keys}

            

            total_score = self._calculate_total_score(calc_data, weights)

            data['總成績'] = total_score

            

            set_clauses = ', '.join(f"{col} = ?" for col in data.keys())

            values = list(data.values())

            values.append(student_id)


            query = f"UPDATE scores SET {set_clauses} WHERE 學號 = ?"

            self.cursor.execute(query, values)

            self.conn.commit()

            return True

        except Exception as e:

            print(f"更新資料失敗: {e}")

            messagebox.showerror("錯誤", f"更新資料失敗: {e}")

            return False


    def delete_score(self, student_id):

        """刪除單一學生資料"""

        try:

            self.cursor.execute("DELETE FROM scores WHERE 學號 = ?", (student_id,))

            self.conn.commit()

            return True

        except Exception as e:

            print(f"刪除資料失敗: {e}")

            messagebox.showerror("錯誤", f"刪除資料失敗: {e}")

            return False


    def delete_database(self):

        """刪除整個資料庫檔案"""

        self.disconnect() 

        try:

            if os.path.exists(self.db_name):

                os.remove(self.db_name)

                messagebox.showinfo("成功", f"資料庫檔案 {self.db_name} 已成功刪除。請重新啟動系統。")

                return True

            else:

                messagebox.showwarning("警告", "資料庫檔案不存在。")

                return False

        except OSError as e:

            messagebox.showerror("錯誤", f"無法刪除資料庫檔案: {e}")

            return False


    def import_from_csv(self, file_path, weights):

        """

        從 CSV/Excel 檔案匯入基本學生資料 (學號, 姓名, 班級名稱, 入學畢業系科)。

        成績相關欄位會被初始化為 None/0,並計算總成績 (初始為 0)。

        """

        # 僅需匯入的基本欄位

        base_cols = ['學號', '姓名', '班級名稱', '入學畢業系科']

        

        # 偵測檔案的實際標頭行(更穩健的策略)

        header_index = 0

        try:

            is_excel = file_path.lower().endswith(('.xlsx', '.xls'))

            if is_excel:

                df_test = pd.read_excel(file_path, header=None, nrows=5)

            else:

                # 嘗試使用 utf-8 和 big5/cp950 解碼 (常見的中文編碼)

                try:

                    df_test = pd.read_csv(file_path, header=None, nrows=5, encoding='utf-8')

                except UnicodeDecodeError:

                    df_test = pd.read_csv(file_path, header=None, nrows=5, encoding='big5')

            

            # 嘗試找出包含 '學號' 且包含所有四個基本欄位的行作為標頭

            for i in range(len(df_test)):

                current_header = df_test.iloc[i].astype(str).tolist()

                if '學號' in current_header and all(col in current_header for col in base_cols):

                    header_index = i

                    break

            

            if is_excel:

                df = pd.read_excel(file_path, header=header_index)

            else:

                # 再次使用正確的編碼和標頭讀取整個文件

                try:

                    df = pd.read_csv(file_path, header=header_index, encoding='utf-8')

                except UnicodeDecodeError:

                    df = pd.read_csv(file_path, header=header_index, encoding='big5')


        except Exception as e:

            print(f"檔案解析失敗: {e}")

            messagebox.showerror("匯入錯誤", f"無法解析檔案結構,請確保檔案為 CSV/Excel 格式: {e}")

            return False


        try:

            if df.empty or '學號' not in df.columns:

                 messagebox.showwarning("匯入警告", "匯入的檔案中沒有可用的學生資料或標頭不正確。")

                 return False


            # 只保留基本欄位並創建副本

            df_base = df[base_cols].copy()

            

            # 將 NaN/NaT (缺失值) 替換為 None

            df_base = df_base.where(pd.notna(df_base), None)


            successful_imports = 0

            failed_imports = 0


            for index, row in df_base.iterrows():

                try:

                    student_id = str(row['學號']).strip() if row['學號'] is not None else ''

                    if student_id == '':

                        continue


                    # 準備數據字典 (成績欄位初始化)

                    data = {

                        '學號': student_id,

                        '姓名': row['姓名'],

                        '班級名稱': row['班級名稱'],

                        '入學畢業系科': row['入學畢業系科'],

                        # 成績欄位初始化為 None

                        '平1': None, '平2': None, '平3': None, 

                        '平4': None, '平5': None, '平6': None,

                        '期中': None,

                        '期末': None,

                        '個人加減分': 0.0 

                    }

                    

                    # 計算總成績 (初始為 0)

                    total_score = self._calculate_total_score(data, weights)

                    data['總成績'] = total_score

                    

                    columns = ', '.join(data.keys())

                    placeholders = ', '.join('?' * len(data))

                    values = tuple(data.values())


                    # INSERT OR REPLACE:如果學號存在則更新 (覆蓋基本資料,成績不變),否則插入

                    query = f"INSERT OR REPLACE INTO scores ({columns}) VALUES ({placeholders})"

                    self.cursor.execute(query, values)

                    successful_imports += 1

                except Exception as e:

                    print(f"匯入學號 {row.get('學號', '未知')} 失敗: {e}")

                    failed_imports += 1


            self.conn.commit()

            messagebox.showinfo("匯入成功", f"成功匯入/更新 {successful_imports} 筆學生基本資料。成績欄位已初始化。")

            return True

        except Exception as e:

            print(f"匯入檔案時發生錯誤: {e}")

            messagebox.showerror("匯入錯誤", f"匯入檔案時發生錯誤: {e}")

            return False


# Tkinter 應用程式類

class ScoreManagerApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("成績管理系統 (score114.db)")

        self.db = ScoreDB()

        

        self.weights = {'P': 0.3, 'M': 0.35, 'F': 0.35} 


        self.create_widgets()

        self.display_data()


    def create_widgets(self):

        # --- 權重設定區 ---

        weight_frame = tk.LabelFrame(self, text="成績權重設定 (P+M+F=1.0)", padx=5, pady=5)

        weight_frame.pack(padx=10, pady=5, fill="x")


        tk.Label(weight_frame, text="平時權重 (P):").grid(row=0, column=0, padx=5, pady=2)

        self.p_weight_var = tk.StringVar(value=str(self.weights['P']))

        tk.Entry(weight_frame, textvariable=self.p_weight_var, width=8).grid(row=0, column=1, padx=5, pady=2)


        tk.Label(weight_frame, text="期中權重 (M):").grid(row=0, column=2, padx=5, pady=2)

        self.m_weight_var = tk.StringVar(value=str(self.weights['M']))

        tk.Entry(weight_frame, textvariable=self.m_weight_var, width=8).grid(row=0, column=3, padx=5, pady=2)


        tk.Label(weight_frame, text="期末權重 (F):").grid(row=0, column=4, padx=5, pady=2)

        self.f_weight_var = tk.StringVar(value=str(self.weights['F']))

        tk.Entry(weight_frame, textvariable=self.f_weight_var, width=8).grid(row=0, column=5, padx=5, pady=2)


        tk.Button(weight_frame, text="更新權重", command=self.update_weights).grid(row=0, column=6, padx=10, pady=2)


        # --- 資料輸入區 ---

        input_frame = tk.LabelFrame(self, text="學生資料輸入/修改", padx=10, pady=10)

        input_frame.pack(padx=10, pady=10, fill="x")


        fields = [

            ("學號:", "學號"), ("姓名:", "姓名"), ("班級名稱:", "班級名稱"), ("入學畢業系科:", "入學畢業系科"),

            ("平1:", "平1"), ("平2:", "平2"), ("平3:", "平3"), ("平4:", "平4"), ("平5:", "平5"), ("平6:", "平6"),

            ("期中:", "期中"), ("期末:", "期末"), ("個人加減分:", "個人加減分")

        ]

        self.entries = {}

        for i, (label_text, key) in enumerate(fields):

            row, col = divmod(i, 4)

            tk.Label(input_frame, text=label_text).grid(row=row, column=col*2, sticky="w", padx=5, pady=2)

            var = tk.StringVar()

            entry = tk.Entry(input_frame, textvariable=var, width=12)

            entry.grid(row=row, column=col*2 + 1, padx=5, pady=2)

            self.entries[key] = entry

            

        # --- 操作按鈕區 ---

        btn_frame = tk.Frame(self)

        btn_frame.pack(padx=10, pady=5, fill="x")


        tk.Button(btn_frame, text="新增學生資料", command=self.add_student, bg="#D9EAD3").pack(side=tk.LEFT, padx=5)

        tk.Button(btn_frame, text="修改學生資料", command=self.update_student, bg="#FFF2CC").pack(side=tk.LEFT, padx=5)

        tk.Button(btn_frame, text="刪除學生資料", command=self.delete_student, bg="#F4CCCC").pack(side=tk.LEFT, padx=5)

        tk.Button(btn_frame, text="清空輸入欄位", command=self.clear_entries).pack(side=tk.LEFT, padx=20)

        tk.Button(btn_frame, text="匯入學生資料 (CSV/Excel)", command=self.import_data, bg="#B4C6E7").pack(side=tk.LEFT, padx=5)

        tk.Button(btn_frame, text="!!! 刪除整個資料庫 !!!", command=self.delete_db_confirmation, bg="#EA9999").pack(side=tk.LEFT, padx=20)


        # --- 資料顯示區 (Treeview) ---

        data_frame = tk.Frame(self)

        data_frame.pack(padx=10, pady=10, fill="both", expand=True)


        columns = ("學號", "姓名", "班級名稱", "系科", "平1", "平2", "平3", "平4", "平5", "平6", "期中", "期末", "加減分", "總成績")

        self.tree = ttk.Treeview(data_frame, columns=columns, show="headings")

        

        for col in columns:

            self.tree.heading(col, text=col)

            width = 60

            if col in ["學號", "班級名稱", "系科"]:

                width = 80

            elif col == "姓名":

                width = 50

            elif col == "總成績":

                width = 70

            self.tree.column(col, width=width, anchor=tk.CENTER)


        self.tree.pack(side="left", fill="both", expand=True)


        scrollbar = ttk.Scrollbar(data_frame, orient="vertical", command=self.tree.yview)

        scrollbar.pack(side="right", fill="y")

        self.tree.configure(yscrollcommand=scrollbar.set)

        

        self.tree.bind('<<TreeviewSelect>>', self.item_selected)


    def update_weights(self):

        """更新並驗證成績權重,並重新計算所有總成績"""

        try:

            p = float(self.p_weight_var.get())

            m = float(self.m_weight_var.get())

            f = float(self.f_weight_var.get())


            if not (0.0 <= p <= 1.0 and 0.0 <= m <= 1.0 and 0.0 <= f <= 1.0):

                messagebox.showerror("權重錯誤", "所有權重必須在 0.0 到 1.0 之間。")

                return


            if abs(p + m + f - 1.0) > 0.001:

                messagebox.showwarning("權重警告", f"權重總和不等於 1.0 ({p+m+f:.2f})。")


            self.weights = {'P': p, 'M': m, 'F': f}

            messagebox.showinfo("成功", "成績權重已更新。")

            

            self.recalculate_all_scores()

            self.display_data()

        except ValueError:

            messagebox.showerror("輸入錯誤", "權重必須為有效的數字 (0.0 到 1.0)。")


    def recalculate_all_scores(self):

        """根據新的權重重新計算所有學生的總成績"""

        all_students = self.db.get_all_scores()

        

        for student in all_students:

            # 資料庫欄位順序: 0學號, 1姓名, 2班級名稱, 3入學畢業系科, 4平1, ..., 9平6, 10期中, 11期末, 12個人加減分, 13總成績

            

            # 建立用於 UPDATE 的字典 (包含所有欄位,除了 PK 和 總成績)

            update_data = {

                '姓名': student[1], '班級名稱': student[2], '入學畢業系科': student[3],

                '平1': student[4], '平2': student[5], '平3': student[6], 

                '平4': student[7], '平5': student[8], '平6': student[9],

                '期中': student[10],

                '期末': student[11],

                '個人加減分': student[12]

            }

            

            self.db.update_score(student[0], update_data, self.weights)

        

    def get_input_data(self):

        """從輸入欄位獲取資料並格式化"""

        data = {

            '學號': self.entries['學號'].get().strip(),

            '姓名': self.entries['姓名'].get().strip(),

            '班級名稱': self.entries['班級名稱'].get().strip(),

            '入學畢業系科': self.entries['入學畢業系科'].get().strip()

        }

        

        if not data['學號'] or not data['姓名']:

            messagebox.showerror("輸入錯誤", "學號和姓名為必填。")

            return None


        # 處理成績欄位

        try:

            for i in range(1, 7):

                key = f'平{i}'

                val = self.entries[key].get().strip()

                data[key] = float(val) if val else None


            for key in ['期中', '期末']:

                val = self.entries[key].get().strip()

                data[key] = float(val) if val else None

            

            val_bonus = self.entries['個人加減分'].get().strip()

            data['個人加減分'] = float(val_bonus) if val_bonus else 0.0


            return data

        except ValueError:

            messagebox.showerror("輸入錯誤", "平時/期中/期末/加減分必須為有效數字或留空。")

            return None


    def clear_entries(self):

        """清空所有輸入欄位"""

        for entry in self.entries.values():

            entry.delete(0, tk.END)


    def display_data(self):

        """從資料庫獲取資料並顯示在 Treeview 中"""

        for row in self.tree.get_children():

            self.tree.delete(row)

        

        scores = self.db.get_all_scores()

        for score in scores:

            display_score = []

            for i, s in enumerate(score):

                if s is None:

                    display_score.append('')

                elif isinstance(s, (float, int)) and i > 3: 

                    if math.modf(s)[0] == 0.0:

                        display_score.append(str(int(s)))

                    else:

                        display_score.append(f"{s:.2f}")

                else:

                    display_score.append(str(s))

            

            self.tree.insert('', tk.END, values=display_score)


    def add_student(self):

        """新增學生資料"""

        data = self.get_input_data()

        if data:

            if self.db.insert_score(data, self.weights):

                messagebox.showinfo("成功", f"學生 {data['姓名']} 資料新增成功。")

                self.clear_entries()

                self.display_data()


    def update_student(self):

        """修改學生資料"""

        selected_item = self.tree.focus()

        if not selected_item:

            messagebox.showwarning("警告", "請先從列表中選擇要修改的學生。")

            return


        student_id = self.tree.item(selected_item, 'values')[0]

        data = self.get_input_data()


        if data:

            if data['學號'] != student_id:

                 messagebox.showwarning("警告", "修改功能不允許變更學號。請先清空欄位後再點選。")

                 return

                 

            if self.db.update_score(student_id, data, self.weights):

                messagebox.showinfo("成功", f"學號 {student_id} 資料更新成功。")

                self.clear_entries()

                self.display_data()


    def delete_student(self):

        """刪除學生資料"""

        selected_item = self.tree.focus()

        if not selected_item:

            messagebox.showwarning("警告", "請先從列表中選擇要刪除的學生。")

            return

        

        student_id = self.tree.item(selected_item, 'values')[0]

        name = self.tree.item(selected_item, 'values')[1]


        if messagebox.askyesno("確認刪除", f"確定要刪除學生 {name} ({student_id}) 的資料嗎?"):

            if self.db.delete_score(student_id):

                messagebox.showinfo("成功", "學生資料已刪除。")

                self.clear_entries()

                self.display_data()


    def delete_db_confirmation(self):

        """確認並刪除整個資料庫"""

        if messagebox.askyesno("極度警告", "確定要刪除整個資料庫檔案 (score114.db) 嗎?此操作不可逆!"):

            if self.db.delete_database():

                self.quit()


    def item_selected(self, event):

        """點擊 Treeview 項目時,將資料填入輸入欄位"""

        selected_item = self.tree.focus()

        if selected_item:

            values = self.tree.item(selected_item, 'values')

            keys = [

                "學號", "姓名", "班級名稱", "入學畢業系科", 

                "平1", "平2", "平3", "平4", "平5", "平6", 

                "期中", "期末", "個人加減分", "總成績"

            ]

            

            self.clear_entries()

            

            for key_index, key in enumerate(keys):

                if key in self.entries:

                    value = values[key_index]

                    if value and value != 'None' and key != '總成績': 

                        self.entries[key].insert(0, value)


    def import_data(self):

        """匯入學生資料"""

        file_path = filedialog.askopenfilename(

            defaultextension=".csv",

            filetypes=[("CSV files", "*.csv"), ("Excel files", "*.xlsx"), ("All files", "*.*")]

        )

        if file_path:

            if self.db.import_from_csv(file_path, self.weights):

                self.display_data()


    def on_closing(self):

        """關閉應用程式時斷開資料庫連接"""

        if messagebox.askokcancel("退出", "確定要退出成績管理系統嗎?"):

            self.db.disconnect()

            self.destroy()


if __name__ == "__main__":

    app = ScoreManagerApp()

    app.protocol("WM_DELETE_WINDOW", app.on_closing)

    app.mainloop()



 PYTHON程式



import tkinter as tk
from tkinter import messagebox, filedialog
from tkinter import ttk
import sqlite3
import pandas as pd
import math
import os

# 資料庫操作類
class ScoreDB:
    def __init__(self, db_name="score114.db"):
        self.conn = None
        self.cursor = None
        self.db_name = db_name
        self.connect()
        self.create_table()

    def connect(self):
        """連接或創建 SQLite 資料庫檔案"""
        try:
            self.conn = sqlite3.connect(self.db_name)
            self.cursor = self.conn.cursor()
        except sqlite3.Error as e:
            print(f"無法連接資料庫: {e}") 
            messagebox.showerror("資料庫錯誤", f"無法連接資料庫: {e}")

    def create_table(self):
        """創建 scores 資料表"""
        self.cursor.execute("""
            CREATE TABLE IF NOT EXISTS scores (
                學號 TEXT PRIMARY KEY,
                姓名 TEXT,
                班級名稱 TEXT,
                入學畢業系科 TEXT,
                平1 REAL,
                平2 REAL,
                平3 REAL,
                平4 REAL,
                平5 REAL,
                平6 REAL,
                期中 REAL,
                期末 REAL,
                個人加減分 REAL,
                總成績 REAL
            )
        """)
        self.conn.commit()

    def disconnect(self):
        """斷開資料庫連接"""
        if self.conn:
            self.conn.close()

    def _calculate_total_score(self, scores_data, weights):
        """
        根據輸入的成績和平時成績次數計算總成績。
        總成績 = (平時平均 * 平時權重) + (期中 * 期中權重) + (期末 * 期末權重) + 個人加減分
        """
        p_scores = [scores_data.get(f'平{i}') for i in range(1, 7)]
        valid_p_scores = [float(s) for s in p_scores if s is not None and s != '']
        
        num_p_scores = len(valid_p_scores)
        avg_p = sum(valid_p_scores) / num_p_scores if num_p_scores > 0 else 0.0

        midterm = float(scores_data.get('期中') if scores_data.get('期中') is not None else 0.0)
        final = float(scores_data.get('期末') if scores_data.get('期末') is not None else 0.0)
        bonus = float(scores_data.get('個人加減分') if scores_data.get('個人加減分') is not None else 0.0)

        total_score = (avg_p * weights['P']) + \
                      (midterm * weights['M']) + \
                      (final * weights['F']) + \
                      bonus
        
        total_score = max(0.0, min(100.0, total_score))
        return round(total_score, 2)

    def insert_score(self, data, weights):
        """插入新學生資料並計算總成績"""
        try:
            calc_data = {k: data.get(k) for k in ['平1', '平2', '平3', '平4', '平5', '平6', '期中', '期末', '個人加減分']}
            total_score = self._calculate_total_score(calc_data, weights)
            data['總成績'] = total_score
            
            columns = ', '.join(data.keys())
            placeholders = ', '.join('?' * len(data))
            values = tuple(data.values())

            query = f"INSERT INTO scores ({columns}) VALUES ({placeholders})"
            self.cursor.execute(query, values)
            self.conn.commit()
            return True
        except sqlite3.IntegrityError:
            messagebox.showerror("錯誤", f"學號 {data['學號']} 已存在。請使用修改功能。")
            return False
        except Exception as e:
            print(f"插入資料失敗: {e}")
            messagebox.showerror("錯誤", f"插入資料失敗: {e}")
            return False

    def get_all_scores(self):
        """查詢所有學生資料"""
        self.cursor.execute("SELECT * FROM scores ORDER BY 學號")
        return self.cursor.fetchall()

    def update_score(self, student_id, data, weights):
        """更新學生資料並重新計算總成績"""
        try:
            required_keys = ['平1', '平2', '平3', '平4', '平5', '平6', '期中', '期末', '個人加減分']
            calc_data = {k: data.get(k) for k in required_keys}
            
            total_score = self._calculate_total_score(calc_data, weights)
            data['總成績'] = total_score
            
            set_clauses = ', '.join(f"{col} = ?" for col in data.keys())
            values = list(data.values())
            values.append(student_id)

            query = f"UPDATE scores SET {set_clauses} WHERE 學號 = ?"
            self.cursor.execute(query, values)
            self.conn.commit()
            return True
        except Exception as e:
            print(f"更新資料失敗: {e}")
            messagebox.showerror("錯誤", f"更新資料失敗: {e}")
            return False

    def delete_score(self, student_id):
        """刪除單一學生資料"""
        try:
            self.cursor.execute("DELETE FROM scores WHERE 學號 = ?", (student_id,))
            self.conn.commit()
            return True
        except Exception as e:
            print(f"刪除資料失敗: {e}")
            messagebox.showerror("錯誤", f"刪除資料失敗: {e}")
            return False

    def delete_database(self):
        """刪除整個資料庫檔案"""
        self.disconnect() 
        try:
            if os.path.exists(self.db_name):
                os.remove(self.db_name)
                messagebox.showinfo("成功", f"資料庫檔案 {self.db_name} 已成功刪除。請重新啟動系統。")
                return True
            else:
                messagebox.showwarning("警告", "資料庫檔案不存在。")
                return False
        except OSError as e:
            messagebox.showerror("錯誤", f"無法刪除資料庫檔案: {e}")
            return False

    def import_from_csv(self, file_path, weights):
        """從 CSV/Excel 檔案匯入基本學生資料 (學號, 姓名, 班級名稱, 入學畢業系科)。"""
        base_cols = ['學號', '姓名', '班級名稱', '入學畢業系科']
        header_index = 0
        try:
            is_excel = file_path.lower().endswith(('.xlsx', '.xls'))
            
            # 嘗試檢測標頭行
            if is_excel:
                df_test = pd.read_excel(file_path, header=None, nrows=5)
            else:
                try:
                    df_test = pd.read_csv(file_path, header=None, nrows=5, encoding='utf-8')
                except UnicodeDecodeError:
                    df_test = pd.read_csv(file_path, header=None, nrows=5, encoding='big5')
            
            for i in range(len(df_test)):
                current_header = df_test.iloc[i].astype(str).tolist()
                if '學號' in current_header and all(col in current_header for col in base_cols):
                    header_index = i
                    break
            
            # 讀取整個檔案
            if is_excel:
                df = pd.read_excel(file_path, header=header_index)
            else:
                try:
                    df = pd.read_csv(file_path, header=header_index, encoding='utf-8')
                except UnicodeDecodeError:
                    df = pd.read_csv(file_path, header=header_index, encoding='big5')

        except Exception as e:
            print(f"檔案解析失敗: {e}")
            messagebox.showerror("匯入錯誤", f"無法解析檔案結構,請確保檔案為 CSV/Excel 格式: {e}")
            return False

        try:
            if df.empty or '學號' not in df.columns:
                 messagebox.showwarning("匯入警告", "匯入的檔案中沒有可用的學生資料或標頭不正確。")
                 return False

            df_base = df[base_cols].copy()
            df_base = df_base.where(pd.notna(df_base), None)

            successful_imports = 0
            failed_imports = 0

            for index, row in df_base.iterrows():
                try:
                    student_id = str(row['學號']).strip() if row['學號'] is not None else ''
                    if student_id == '':
                        continue

                    # 準備數據字典 (成績欄位初始化)
                    data = {
                        '學號': student_id,
                        '姓名': row['姓名'],
                        '班級名稱': row['班級名稱'],
                        '入學畢業系科': row['入學畢業系科'],
                        '平1': None, '平2': None, '平3': None, 
                        '平4': None, '平5': None, '平6': None,
                        '期中': None,
                        '期末': None,
                        '個人加減分': 0.0 
                    }
                    
                    total_score = self._calculate_total_score(data, weights)
                    data['總成績'] = total_score
                    
                    columns = ', '.join(data.keys())
                    placeholders = ', '.join('?' * len(data))
                    values = tuple(data.values())

                    query = f"INSERT OR REPLACE INTO scores ({columns}) VALUES ({placeholders})"
                    self.cursor.execute(query, values)
                    successful_imports += 1
                except Exception as e:
                    print(f"匯入學號 {row.get('學號', '未知')} 失敗: {e}")
                    failed_imports += 1

            self.conn.commit()
            messagebox.showinfo("匯入成功", f"成功匯入/更新 {successful_imports} 筆學生基本資料。成績欄位已初始化。")
            return True
        except Exception as e:
            print(f"匯入檔案時發生錯誤: {e}")
            messagebox.showerror("匯入錯誤", f"匯入檔案時發生錯誤: {e}")
            return False

# Tkinter 應用程式類
class ScoreManagerApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("成績管理系統 (score114.db)")
        self.db = ScoreDB()
        
        self.weights = {'P': 0.3, 'M': 0.35, 'F': 0.35} 

        self.create_widgets()
        self.display_data()

    def create_widgets(self):
        # ... (權重設定區塊保持不變) ...
        weight_frame = tk.LabelFrame(self, text="成績權重設定 (P+M+F=1.0)", padx=5, pady=5)
        weight_frame.pack(padx=10, pady=5, fill="x")

        tk.Label(weight_frame, text="平時權重 (P):").grid(row=0, column=0, padx=5, pady=2)
        self.p_weight_var = tk.StringVar(value=str(self.weights['P']))
        tk.Entry(weight_frame, textvariable=self.p_weight_var, width=8).grid(row=0, column=1, padx=5, pady=2)

        tk.Label(weight_frame, text="期中權重 (M):").grid(row=0, column=2, padx=5, pady=2)
        self.m_weight_var = tk.StringVar(value=str(self.weights['M']))
        tk.Entry(weight_frame, textvariable=self.m_weight_var, width=8).grid(row=0, column=3, padx=5, pady=2)

        tk.Label(weight_frame, text="期末權重 (F):").grid(row=0, column=4, padx=5, pady=2)
        self.f_weight_var = tk.StringVar(value=str(self.weights['F']))
        tk.Entry(weight_frame, textvariable=self.f_weight_var, width=8).grid(row=0, column=5, padx=5, pady=2)

        tk.Button(weight_frame, text="更新權重", command=self.update_weights).grid(row=0, column=6, padx=10, pady=2)

        # ... (資料輸入區塊保持不變) ...
        input_frame = tk.LabelFrame(self, text="學生資料輸入/修改", padx=10, pady=10)
        input_frame.pack(padx=10, pady=10, fill="x")

        fields = [
            ("學號:", "學號"), ("姓名:", "姓名"), ("班級名稱:", "班級名稱"), ("入學畢業系科:", "入學畢業系科"),
            ("平1:", "平1"), ("平2:", "平2"), ("平3:", "平3"), ("平4:", "平4"), ("平5:", "平5"), ("平6:", "平6"),
            ("期中:", "期中"), ("期末:", "期末"), ("個人加減分:", "個人加減分")
        ]
        self.entries = {}
        for i, (label_text, key) in enumerate(fields):
            row, col = divmod(i, 4)
            tk.Label(input_frame, text=label_text).grid(row=row, column=col*2, sticky="w", padx=5, pady=2)
            var = tk.StringVar()
            entry = tk.Entry(input_frame, textvariable=var, width=12)
            entry.grid(row=row, column=col*2 + 1, padx=5, pady=2)
            self.entries[key] = entry
            
        # ... (操作按鈕區塊保持不變) ...
        btn_frame = tk.Frame(self)
        btn_frame.pack(padx=10, pady=5, fill="x")

        tk.Button(btn_frame, text="新增學生資料", command=self.add_student, bg="#D9EAD3").pack(side=tk.LEFT, padx=5)
        tk.Button(btn_frame, text="修改學生資料", command=self.update_student, bg="#FFF2CC").pack(side=tk.LEFT, padx=5)
        tk.Button(btn_frame, text="刪除學生資料", command=self.delete_student, bg="#F4CCCC").pack(side=tk.LEFT, padx=5)
        tk.Button(btn_frame, text="清空輸入欄位", command=self.clear_entries).pack(side=tk.LEFT, padx=20)
        tk.Button(btn_frame, text="匯入學生資料 (CSV/Excel)", command=self.import_data, bg="#B4C6E7").pack(side=tk.LEFT, padx=5)
        tk.Button(btn_frame, text="!!! 刪除整個資料庫 !!!", command=self.delete_db_confirmation, bg="#EA9999").pack(side=tk.LEFT, padx=20)

        # --- 資料顯示區 (Treeview) ---
        data_frame = tk.Frame(self)
        data_frame.pack(padx=10, pady=10, fill="both", expand=True)

        # 1. 修改欄位定義:最左邊新增 "編號"
        columns = ("編號", "學號", "姓名", "班級名稱", "系科", "平1", "平2", "平3", "平4", "平5", "平6", "期中", "期末", "加減分", "總成績")
        self.tree = ttk.Treeview(data_frame, columns=columns, show="headings")
        
        # 2. 設定欄位標題與寬度
        for col in columns:
            self.tree.heading(col, text=col)
            width = 60
            if col == "編號":
                width = 40
            elif col in ["學號", "班級名稱", "系科"]:
                width = 80
            elif col == "姓名":
                width = 50
            elif col == "總成績":
                width = 70
            self.tree.column(col, width=width, anchor=tk.CENTER)
            # 確保編號欄位是最左邊的
            if col == "編號":
                self.tree.column(col, anchor=tk.CENTER)

        self.tree.pack(side="left", fill="both", expand=True)

        scrollbar = ttk.Scrollbar(data_frame, orient="vertical", command=self.tree.yview)
        scrollbar.pack(side="right", fill="y")
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        self.tree.bind('<<TreeviewSelect>>', self.item_selected)

    def update_weights(self):
        # ... (update_weights 保持不變) ...
        try:
            p = float(self.p_weight_var.get())
            m = float(self.m_weight_var.get())
            f = float(self.f_weight_var.get())

            if not (0.0 <= p <= 1.0 and 0.0 <= m <= 1.0 and 0.0 <= f <= 1.0):
                messagebox.showerror("權重錯誤", "所有權重必須在 0.0 到 1.0 之間。")
                return

            if abs(p + m + f - 1.0) > 0.001:
                messagebox.showwarning("權重警告", f"權重總和不等於 1.0 ({p+m+f:.2f})。")

            self.weights = {'P': p, 'M': m, 'F': f}
            messagebox.showinfo("成功", "成績權重已更新。")
            
            self.recalculate_all_scores()
            self.display_data()
        except ValueError:
            messagebox.showerror("輸入錯誤", "權重必須為有效的數字 (0.0 到 1.0)。")

    def recalculate_all_scores(self):
        # ... (recalculate_all_scores 保持不變) ...
        all_students = self.db.get_all_scores()
        
        for student in all_students:
            update_data = {
                '姓名': student[1], '班級名稱': student[2], '入學畢業系科': student[3],
                '平1': student[4], '平2': student[5], '平3': student[6], 
                '平4': student[7], '平5': student[8], '平6': student[9],
                '期中': student[10],
                '期末': student[11],
                '個人加減分': student[12]
            }
            self.db.update_score(student[0], update_data, self.weights)
        
    def get_input_data(self):
        # ... (get_input_data 保持不變) ...
        data = {
            '學號': self.entries['學號'].get().strip(),
            '姓名': self.entries['姓名'].get().strip(),
            '班級名稱': self.entries['班級名稱'].get().strip(),
            '入學畢業系科': self.entries['入學畢業系科'].get().strip()
        }
        
        if not data['學號'] or not data['姓名']:
            messagebox.showerror("輸入錯誤", "學號和姓名為必填。")
            return None

        try:
            for i in range(1, 7):
                key = f'平{i}'
                val = self.entries[key].get().strip()
                data[key] = float(val) if val else None

            for key in ['期中', '期末']:
                val = self.entries[key].get().strip()
                data[key] = float(val) if val else None
            
            val_bonus = self.entries['個人加減分'].get().strip()
            data['個人加減分'] = float(val_bonus) if val_bonus else 0.0

            return data
        except ValueError:
            messagebox.showerror("輸入錯誤", "平時/期中/期末/加減分必須為有效數字或留空。")
            return None

    def clear_entries(self):
        # ... (clear_entries 保持不變) ...
        for entry in self.entries.values():
            entry.delete(0, tk.END)

    def display_data(self):
        """從資料庫獲取資料並顯示在 Treeview 中,新增編號欄位"""
        for row in self.tree.get_children():
            self.tree.delete(row)
        
        scores = self.db.get_all_scores()
        
        # 3. 新增計數器
        row_number = 1
        for score in scores:
            display_score = []
            
            # 第一個元素是編號
            display_score.append(str(row_number)) 
            
            # 接下來是資料庫的欄位 (學號, 姓名, ..., 總成績)
            for i, s in enumerate(score):
                if s is None:
                    display_score.append('')
                elif isinstance(s, (float, int)) and i > 3: 
                    if math.modf(s)[0] == 0.0:
                        display_score.append(str(int(s)))
                    else:
                        display_score.append(f"{s:.2f}")
                else:
                    display_score.append(str(s))
            
            self.tree.insert('', tk.END, values=display_score)
            row_number += 1 # 編號遞增

    def add_student(self):
        # ... (add_student 保持不變) ...
        data = self.get_input_data()
        if data:
            if self.db.insert_score(data, self.weights):
                messagebox.showinfo("成功", f"學生 {data['姓名']} 資料新增成功。")
                self.clear_entries()
                self.display_data()

    def update_student(self):
        # ... (update_student 保持不變) ...
        selected_item = self.tree.focus()
        if not selected_item:
            messagebox.showwarning("警告", "請先從列表中選擇要修改的學生。")
            return

        # 注意:Treeview item 的 values[0] 現在是 "編號",學號是 values[1]
        student_id = self.tree.item(selected_item, 'values')[1]
        data = self.get_input_data()

        if data:
            if data['學號'] != student_id:
                 messagebox.showwarning("警告", "修改功能不允許變更學號。請先清空欄位後再點選。")
                 return
                 
            if self.db.update_score(student_id, data, self.weights):
                messagebox.showinfo("成功", f"學號 {student_id} 資料更新成功。")
                self.clear_entries()
                self.display_data()

    def delete_student(self):
        # ... (delete_student 保持不變) ...
        selected_item = self.tree.focus()
        if not selected_item:
            messagebox.showwarning("警告", "請先從列表中選擇要刪除的學生。")
            return
        
        # 注意:Treeview item 的 values[0] 是 "編號",學號是 values[1],姓名是 values[2]
        values = self.tree.item(selected_item, 'values')
        student_id = values[1]
        name = values[2]

        if messagebox.askyesno("確認刪除", f"確定要刪除學生 {name} ({student_id}) 的資料嗎?"):
            if self.db.delete_score(student_id):
                messagebox.showinfo("成功", "學生資料已刪除。")
                self.clear_entries()
                self.display_data()

    def delete_db_confirmation(self):
        # ... (delete_db_confirmation 保持不變) ...
        if messagebox.askyesno("極度警告", "確定要刪除整個資料庫檔案 (score114.db) 嗎?此操作不可逆!"):
            if self.db.delete_database():
                self.quit()

    def item_selected(self, event):
        """點擊 Treeview 項目時,將資料填入輸入欄位"""
        selected_item = self.tree.focus()
        if selected_item:
            values = self.tree.item(selected_item, 'values')
            
            # Treeview 的欄位順序現在是: 編號 (0), 學號 (1), 姓名 (2), ... 總成績 (14)
            # 資料庫欄位對應的索引從 1 開始
            keys = [
                "學號", "姓名", "班級名稱", "入學畢業系科", 
                "平1", "平2", "平3", "平4", "平5", "平6", 
                "期中", "期末", "個人加減分", "總成績"
            ]
            
            self.clear_entries()
            
            for key_index, key in enumerate(keys):
                # 實際值在 values 中的索引是 key_index + 1
                value_index = key_index + 1 
                
                if key in self.entries:
                    value = values[value_index]
                    # 填入輸入欄位 (排除 "總成績" - 不應該手動修改)
                    if value and value != 'None' and key != '總成績': 
                        self.entries[key].insert(0, value)

    def import_data(self):
        # ... (import_data 保持不變) ...
        file_path = filedialog.askopenfilename(
            defaultextension=".csv",
            filetypes=[("CSV files", "*.csv"), ("Excel files", "*.xlsx"), ("All files", "*.*")]
        )
        if file_path:
            if self.db.import_from_csv(file_path, self.weights):
                self.display_data()

    def on_closing(self):
        # ... (on_closing 保持不變) ...
        if messagebox.askokcancel("退出", "確定要退出成績管理系統嗎?"):
            self.db.disconnect()
            self.destroy()

if __name__ == "__main__":
    app = ScoreManagerApp()
    app.protocol("WM_DELETE_WINDOW", app.on_closing)
    app.mainloop()

沒有留言:

張貼留言

ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

 ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...