勤益科大成績計算系統
成績管理系統。它包含了:
使用
score114.db資料庫。資料表欄位包含學號、姓名、班級名稱、入學畢業系科、平時成績(最多6次)、期中、期末、個人加減分、總成績。
總成績計算邏輯:
平時成績平均 $ = (\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\%$。使用者可以在系統中設定這些權重。
系統功能:新增學生資料、查詢所有學生資料、修改學生資料、刪除學生資料、刪除整個資料庫。
匯入學生基本資料功能(從 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()





沒有留言:
張貼留言