Add GUI functionality and update documentation
This commit is contained in:
@@ -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
|
||||
|
||||
22
README.md
22
README.md
@@ -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
|
||||
|
||||
@@ -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
165
src/custom_bingo/gui.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user