Build an Interactive Class Roll Call Application with Python Tkinter

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()

Tags: python tkinter GUI Development PIL/Pillow Interactive Application

Posted on Sun, 04 Oct 2026 16:42:34 +0000 by uptime