Add GUI functionality and update documentation

This commit is contained in:
2025-12-07 16:00:01 -05:00
parent a383a7c461
commit 294ad26ce6
5 changed files with 235 additions and 14 deletions

View File

@@ -28,7 +28,8 @@ CustomBingo/
│ ├── main.py
│ ├── card_generator.py
│ ├── spreadsheet_reader.py
── pdf_generator.py
── pdf_generator.py
│ └── gui.py
├── tests/
├── data/
├── requirements.txt
@@ -82,8 +83,15 @@ CustomBingo/
3. Test with various input spreadsheets
4. Verify PDF output quality
### Step 8: Documentation and Polish
1. Create README.md with usage instructions
### Step 8: GUI Development
1. Create `gui.py` module with tkinter interface
2. Implement input fields for card title, column headers, spreadsheet path, and number of cards
3. Add file dialogs for source spreadsheet and output PDF location
4. Integrate with existing card generation functions
5. Add validation, status updates, and error handling
### Step 9: Documentation and Polish
1. Update README.md to include GUI usage instructions
2. Add command-line help
3. Test on target platform (Windows PowerShell)
4. Final review and debugging
@@ -94,6 +102,7 @@ CustomBingo/
3. Customizable column headers
4. Multiple card layouts
5. Web interface option
6. Enhanced GUI features (theme customization, preview, etc.)
## Dependencies
- pandas: For reading spreadsheet files

View File

@@ -10,6 +10,7 @@ CustomBingo is a Python application that reads a spreadsheet with 5 columns (B,
- Auto-fit text in grid cells with horizontal and vertical centering
- Support for multi-line text in cells
- Customizable number of cards to generate
- Graphical User Interface (GUI) for easy operation
## Installation
@@ -58,6 +59,27 @@ uv run python src/custom_bingo/main.py -i data/ChristmasSongsBingo.csv -o output
# Generate a BINGO card with custom title and sub-headers
uv run python src/custom_bingo/main.py -i data/ChristmasSongsBingo.csv -o output.pdf -t "Holiday Music Bingo" -s "Traditional,Upbeat,Holy,Classic,Festive"
## GUI Usage
The application also includes a graphical user interface for easier operation:
```bash
# Run the GUI application directly
python -m src.custom_bingo.gui
# Or using the installed command (after installation)
custombingo-gui
```
The GUI provides input fields for:
- Card title
- 5 column headers (B, I, N, G, O)
- Source spreadsheet file
- Number of cards to generate
- Output PDF location
Click the "Generate PDF" button to create your BINGO cards.
```
## Input Format

View File

@@ -18,6 +18,7 @@ dev = [
[project.scripts]
custombingo = "custom_bingo.main:main"
custombingo-gui = "custom_bingo.gui:main"
[tool.setuptools]
packages = ["custom_bingo"]

165
src/custom_bingo/gui.py Normal file
View File

@@ -0,0 +1,165 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
from custom_bingo.card_generator import generate_bingo_cards
from custom_bingo.spreadsheet_reader import read_spreadsheet
from custom_bingo.pdf_generator import export_cards_to_pdf
class BingoCardGeneratorUI:
def __init__(self, root):
self.root = root
self.root.title("Custom Bingo Card Generator")
self.root.geometry("600x700")
# Create main frame with padding
main_frame = ttk.Frame(root, padding="20")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights for responsive design
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# Card Title
ttk.Label(main_frame, text="Card Title:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.title_var = tk.StringVar(value="BINGO")
title_entry = ttk.Entry(main_frame, textvariable=self.title_var, width=50)
title_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 0))
# Column Headers
ttk.Label(main_frame, text="Column Headers:").grid(row=1, column=0, sticky=tk.W, pady=5)
header_frame = ttk.Frame(main_frame)
header_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 0))
# Configure column weights for the header frame
for i in range(5):
header_frame.columnconfigure(i, weight=1)
self.header_vars = []
columns = ['B', 'I', 'N', 'G', 'O']
for i, col in enumerate(columns):
ttk.Label(header_frame, text=f"{col}:").grid(row=0, column=i*2, sticky=tk.W)
var = tk.StringVar(value=col)
entry = ttk.Entry(header_frame, textvariable=var, width=10)
entry.grid(row=0, column=i*2+1, sticky=(tk.W, tk.E), padx=(0, 5))
self.header_vars.append(var)
# Source Spreadsheet
ttk.Label(main_frame, text="Source Spreadsheet:").grid(row=2, column=0, sticky=tk.W, pady=5)
spreadsheet_frame = ttk.Frame(main_frame)
spreadsheet_frame.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 0))
self.spreadsheet_var = tk.StringVar()
spreadsheet_entry = ttk.Entry(spreadsheet_frame, textvariable=self.spreadsheet_var, width=40)
spreadsheet_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
spreadsheet_frame.columnconfigure(0, weight=1)
browse_button = ttk.Button(spreadsheet_frame, text="Browse...", command=self.browse_spreadsheet)
browse_button.grid(row=0, column=1)
# Number of Cards
ttk.Label(main_frame, text="Number of Cards:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.number_var = tk.StringVar(value="1")
number_spinbox = ttk.Spinbox(main_frame, from_=1, to=100, textvariable=self.number_var, width=10)
number_spinbox.grid(row=3, column=1, sticky=tk.W, pady=5, padx=(10, 0))
# Output PDF Location
ttk.Label(main_frame, text="Output PDF:").grid(row=4, column=0, sticky=tk.W, pady=5)
output_frame = ttk.Frame(main_frame)
output_frame.grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 0))
self.output_var = tk.StringVar()
output_entry = ttk.Entry(output_frame, textvariable=self.output_var, width=40)
output_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
output_frame.columnconfigure(0, weight=1)
output_button = ttk.Button(output_frame, text="Save As...", command=self.save_pdf)
output_button.grid(row=0, column=1)
# Generate Button
self.generate_button = ttk.Button(main_frame, text="Generate PDF", command=self.generate_pdf)
self.generate_button.grid(row=5, column=0, columnspan=2, pady=20)
# Status label
self.status_var = tk.StringVar(value="Ready")
self.status_label = ttk.Label(main_frame, textvariable=self.status_var, foreground="blue")
self.status_label.grid(row=6, column=0, columnspan=2, pady=10)
def browse_spreadsheet(self):
filename = filedialog.askopenfilename(
title="Select Spreadsheet",
filetypes=[
("Excel files", "*.xlsx *.xls"),
("CSV files", "*.csv"),
("All files", "*.*")
]
)
if filename:
self.spreadsheet_var.set(filename)
def save_pdf(self):
filename = filedialog.asksaveasfilename(
title="Save PDF As",
defaultextension=".pdf",
filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
)
if filename:
self.output_var.set(filename)
def generate_pdf(self):
try:
# Validate inputs
title = self.title_var.get().strip()
if not title:
messagebox.showerror("Validation Error", "Card title cannot be empty.")
return
spreadsheet_path = self.spreadsheet_var.get().strip()
if not spreadsheet_path or not os.path.exists(spreadsheet_path):
messagebox.showerror("Validation Error", "Please select a valid source spreadsheet.")
return
output_path = self.output_var.get().strip()
if not output_path:
messagebox.showerror("Validation Error", "Please specify an output PDF location.")
return
try:
number_of_cards = int(self.number_var.get())
if number_of_cards < 1:
raise ValueError
except ValueError:
messagebox.showerror("Validation Error", "Number of cards must be a positive integer.")
return
# Get column headers
sub_headers = [var.get().strip() for var in self.header_vars]
# Update status
self.status_var.set("Processing...")
self.root.update()
# Generate the cards
data = read_spreadsheet(spreadsheet_path)
cards = generate_bingo_cards(data, number_of_cards)
export_cards_to_pdf(cards, output_path, title=title, sub_headers=sub_headers)
# Show success message
self.status_var.set(f"Successfully generated {number_of_cards} card(s) at {output_path}")
messagebox.showinfo("Success", f"Successfully generated {number_of_cards} BINGO card(s) at {output_path}")
except Exception as e:
self.status_var.set(f"Error: {str(e)}")
messagebox.showerror("Error", f"An error occurred: {str(e)}")
def main():
root = tk.Tk()
app = BingoCardGeneratorUI(root)
root.mainloop()
if __name__ == "__main__":
main()

View File

@@ -25,20 +25,35 @@ def wrap_text_to_fit_box(text: str, available_width: float, available_height: fl
if not text:
return [], max_font_size
# Import here to avoid circular imports if needed
from io import BytesIO
from reportlab.pdfgen import canvas
# Try different font sizes to find the best fit
for font_size in range(max_font_size, 0, -1):
# Split the text into lines that fit the width, with even more conservative padding
lines = simpleSplit(text, font_name, font_size, available_width - 8) # Subtract 8 for more padding
# Split the text into lines that fit the width
lines = simpleSplit(text, font_name, font_size, available_width * 0.95) # Use 95% of width for safety
# Check if the text height fits in the available height
line_height = font_size * 1.2 # Add some spacing between lines
total_height = len(lines) * line_height
if total_height <= available_height - 8: # Subtract 8 for more vertical padding
if total_height <= available_height * 0.95: # Use 95% of height for safety
# Also check that each individual line fits within the width
all_lines_fit = True
# Create a temporary canvas for text width measurement
temp_buffer = BytesIO()
temp_canvas = canvas.Canvas(temp_buffer)
for line in lines:
line_width = temp_canvas.stringWidth(line, font_name, font_size)
if line_width > available_width * 0.95: # Each line must fit within width
all_lines_fit = False
break
if all_lines_fit:
return lines, font_size
# If we can't fit the text with reasonable font size, use smallest possible
lines = simpleSplit(text, font_name, 1, available_width - 8)
lines = simpleSplit(text, font_name, 1, available_width * 0.95)
return lines, 1
@@ -64,20 +79,29 @@ def draw_multiline_text(c: canvas.Canvas, text: str, x: float, y: float, width:
# Try different font sizes to ensure the text fits properly in both dimensions
for font_size in range(original_max_font_size, 0, -1):
# Split the text using the current font size
lines = simpleSplit(text, "Helvetica", font_size, width * 0.8) # Use 80% of width for safety
lines = simpleSplit(text, "Helvetica", font_size, width * 0.95) # Use 95% of width initially
# Calculate actual line height including space between lines
line_height = font_size * 1.2 # Include space between lines
total_text_height = len(lines) * line_height
# Check if both width and height constraints are satisfied
if total_text_height <= height * 0.8: # Use 80% of height for safety
# Check if the height constraint is satisfied first
if total_text_height <= height * 0.95: # Use 95% of height for safety
# Also check that each individual line fits within the width
all_lines_fit = True
for line in lines:
line_width = c.stringWidth(line, "Helvetica", font_size)
if line_width > width * 0.95: # Each line must fit within width
all_lines_fit = False
break
if all_lines_fit:
# Text fits in the box with this font size
break
else:
# If even font size 1 doesn't fit, use size 1
font_size = 1
lines = simpleSplit(text, "Helvetica", font_size, width * 0.8)
lines = simpleSplit(text, "Helvetica", font_size, width * 0.95)
c.setFont("Helvetica", font_size)