Prerequisites
1. Tkinter
Tkinter is Python's standard built-in GUI toolkit, integrated directly into the Python interpreter. It provides a lightweight way to create graphical user interfaces without requiring external installations. We'll use Tkinter to build the core interface of our roll call app, including buttons, labels, and interactive controls.
2. PIL (Pillow)
The Pillow library (a maintained fork of the original PIL) is a powerful image processing tool for Python. It supports a wide range of image formats and enables operations like resizing, cropping, and displaying images in GUI applications. To install Pillow, run the following command:
pip install pillow
We'll use Pillow's Image and ImageTk modules to load and display custom images in our app interface.
Application Features
- Autto-Load Names: On startup, the app automatically detects and loads names from a text file in the same directory, where each name is on a separate line.
- Sequential Roll Call: Cycles through names in the order they appear in the list. Click "Start" to begin scrolling, and "Stop" to select the current name.
- Random Roll Call: Randomly selects names from the list, ensuring unbiased selection each time the "Stop" button is pressed.
- Manual Namee Import: Allows users to select a custom TXT file (one name per line) to load their own list of names.
- PyQt5 Version: An alternative implementation using PyQt5 is available with a more modern UI, though it results in a larger application package size.
Implementation Code
import random
import time
import threading
from tkinter import *
from tkinter import ttk, messagebox, filedialog
from base64 import b64decode
from PIL import Image, ImageTk
from io import BytesIO
class RollCallApp:
def __init__(self):
self.root = Tk()
self.is_running = False
self.update_interval = 0.05
self.name_list = []
self.current_index = 0
# Configure main window
self.root.title("Class Roll Call - v1.0")
window_width = 680
window_height = 350
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
x_pos = (screen_width - window_width) // 2
y_pos = (screen_height - window_height) // 2
self.root.geometry(f"{window_width}x{window_height}+{x_pos}+{y_pos}")
self.root.resizable(False, False)
self._setup_widgets()
self._load_default_names()
self.root.mainloop()
def _setup_widgets(self):
# Name display label
self.name_var = StringVar()
self.name_display = ttk.Label(self.root, textvariable=self.name_var,
font=('Arial', 100, 'bold'), foreground='#1E90FF')
# Control buttons
self.start_btn = ttk.Button(self.root, text="Start", command=self._toggle_roll_call)
self.load_btn = ttk.Button(self.root, text="Load Custom Name List", command=self._load_custom_names)
# Roll call mode selection
self.mode_frame = ttk.LabelFrame(self.root, text="Roll Call Mode")
self.mode_var = IntVar(value=1)
self.sequential_radio = ttk.Radiobutton(self.mode_frame, text="Sequential Roll Call",
variable=self.mode_var, value=1)
self.random_radio = ttk.Radiobutton(self.mode_frame, text="Random Roll Call",
variable=self.mode_var, value=2)
# Name count label
self.count_label = ttk.Label(self.root, font=('Arial', 20), foreground='#FF7F50')
self._update_count_label()
# Load header image
self._setup_header_image()
# Layout widgets
self._place_widgets()
def _setup_header_image(self):
# Base64 encoded image data
img_data = b'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'
img = Image.open(BytesIO(b64decode(img_data)))
self.header_img = ImageTk.PhotoImage(img.resize((680, 100), Image.Resampling.LANCZOS))
self.header_label = ttk.Label(self.root, image=self.header_img)
def _place_widgets(self):
# Header image
self.header_label.pack(fill=X, pady=10)
# Name display
self.name_display.pack(pady=20)
# Count label
self.count_label.pack(pady=5)
# Mode frame
self.mode_frame.pack(pady=10, fill=X, padx=20)
self.sequential_radio.pack(side=LEFT, padx=20)
self.random_radio.pack(side=LEFT, padx=20)
# Buttons
btn_frame = ttk.Frame(self.root)
btn_frame.pack(pady=10)
self.start_btn.pack(side=LEFT, padx=20)
self.load_btn.pack(side=LEFT, padx=20)
def _load_default_names(self):
# Try to load names from a local text file
try:
with open("names.txt", "r", encoding="utf-8") as f:
self.name_list = [line.strip() for line in f if line.strip()]
if not self.name_list:
messagebox.showwarning("Warning", "No valid names found in the default file.")
self._update_count_label()
except FileNotFoundError:
messagebox.info("Info", "Default names.txt not found. Use 'Load Custom Name List' to import your own list.")
def _load_custom_names(self):
file_path = filedialog.askopenfilename(filetypes=[("Text Files", "*.txt")])
if not file_path:
return
try:
with open(file_path, "r", encoding="utf-8") as f:
self.name_list = [line.strip() for line in f if line.strip()]
if not self.name_list:
messagebox.showwarning("Warning", "No valid names found in the selected file.")
self._update_count_label()
except Exception as e:
messagebox.showerror("Error", f"Failed to load file: {str(e)}")
def _update_count_label(self):
self.count_label.config(text=f"Total Names: {len(self.name_list)}")
def _toggle_roll_call(self):
if not self.name_list:
messagebox.showwarning("Warning", "Please load a name list first.")
return
self.is_running = not self.is_running
if self.is_running:
self.start_btn.config(text="Stop")
# Start a new thread to avoid freezing the GUI
threading.Thread(target=self._roll_call_loop, daemon=True).start()
else:
self.start_btn.config(text="Start")
def _roll_call_loop(self):
while self.is_running:
if self.mode_var.get() == 1:
# Sequential mode
self.name_var.set(self.name_list[self.current_index])
self.current_index = (self.current_index + 1) % len(self.name_list)
else:
# Random mode
self.name_var.set(random.choice(self.name_list))
time.sleep(self.update_interval)
# Update GUI
self.root.update_idletasks()