Files
pfand_PKG/PfandApplication/pfand_scanner.py

760 lines
32 KiB
Python

# µScan V2.3.0 - Verbesserte Deutsche Version
import tkinter as tk
from tkinter import ttk, simpledialog, messagebox
import cv2
from PIL import Image, ImageTk
from pyzbar.pyzbar import decode
from datetime import datetime, timedelta
import threading
import queue
import json
import os
import time
import numpy as np
class PfandScanner:
def __init__(self, window, window_title):
self.window = window
self.window.title(window_title)
self.window.geometry("1600x900")
self.window.minsize(1200, 700)
self.window.configure(bg='#f0f0f0')
# Configure main window grid
self.window.columnconfigure(0, weight=1)
self.window.rowconfigure(0, weight=1)
# Style configuration
self.setup_styles()
self.data_file = "quantities.json"
self.products_file = "products.json"
self.load_data()
self.barcode_times = {}
self.prompted_barcodes = set()
self.current_barcodes = [] # Store current frame's barcodes for outlining
self.selected_device_index = tk.IntVar(value=0)
self.last_process_time = time.time()
# FPS Setting
self.process_interval = 0.15 # Improved processing speed
# Collapsible scan list state
self.scan_list_collapsed = tk.BooleanVar(value=False)
# Threading control
self.running = True
self.camera_thread = None
self.process_thread = None
self.init_gui()
self.init_camera()
self.queue = queue.Queue()
self.frame_queue = queue.Queue(maxsize=2) # Limit queue size to prevent memory issues
self.pfand_values = {
"EINWEG": 0.25,
"MEHRWEG": 0.15,
"DOSE": 0.25,
}
# Start threads
self.start_threads()
self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
def setup_styles(self):
self.style = ttk.Style()
self.style.theme_use('clam')
# Configure colors
self.colors = {
'primary': '#2c3e50',
'secondary': '#3498db',
'success': '#27ae60',
'warning': '#f39c12',
'danger': '#e74c3c',
'light': '#ecf0f1',
'dark': '#34495e'
}
# Configure custom styles
self.style.configure('Title.TLabel', font=('Segoe UI', 16, 'bold'), foreground=self.colors['primary'])
self.style.configure('Heading.TLabel', font=('Segoe UI', 12, 'bold'), foreground=self.colors['dark'])
self.style.configure('Info.TLabel', font=('Segoe UI', 10), foreground=self.colors['dark'])
self.style.configure('Success.TLabel', font=('Segoe UI', 10, 'bold'), foreground=self.colors['success'])
self.style.configure('Custom.TLabelFrame', relief='solid', borderwidth=1)
self.style.configure('Camera.TFrame', relief='solid', borderwidth=2)
def load_data(self):
# Load products
if os.path.exists(self.products_file):
try:
with open(self.products_file, 'r', encoding='utf-8') as f:
products_data = json.load(f)
self.products = products_data.get("products", [])
self.prices = products_data.get("prices", {})
except Exception as e:
print(f"Fehler beim Laden der Produkte: {e}")
self.products = []
self.prices = {}
else:
self.products = []
self.prices = {}
# Load quantities
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
self.quantities = json.load(f)
except Exception as e:
print(f"Fehler beim Laden der Mengen: {e}")
self.quantities = {}
else:
self.quantities = {}
def save_json(self):
"""Save quantities to quantities.json"""
try:
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(self.quantities, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"Fehler beim Speichern der Mengen: {e}")
def init_gui(self):
# Main container
self.main_frame = ttk.Frame(self.window, padding="10")
self.main_frame.grid(sticky="nsew")
self.main_frame.columnconfigure(1, weight=2) # Camera gets more space
self.main_frame.columnconfigure(0, weight=1) # Controls
self.main_frame.columnconfigure(2, weight=1) # Info
self.main_frame.rowconfigure(1, weight=1)
# Header
header_frame = ttk.Frame(self.main_frame)
header_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 10))
title_label = ttk.Label(header_frame, text="µScan", style='Title.TLabel')
title_label.pack(side="left")
status_label = ttk.Label(header_frame, text="v2.3.5", style='Info.TLabel')
status_label.pack(side="right")
# Control Panel (Left)
self.control_frame = ttk.LabelFrame(self.main_frame, text="Steuerung", padding="10")
self.control_frame.grid(row=1, column=0, padx=(0, 10), sticky="nsew")
# Camera View (Center)
self.camera_frame = ttk.LabelFrame(self.main_frame, text="Kameraansicht", padding="5")
self.camera_frame.grid(row=1, column=1, padx=5, sticky="nsew")
self.camera_frame.columnconfigure(0, weight=1)
self.camera_frame.rowconfigure(0, weight=1)
# Info Panel (Right) - now collapsible
self.info_frame = ttk.LabelFrame(self.main_frame, text="Scan-Ergebnisse", padding="10")
self.info_frame.grid(row=1, column=2, padx=(10, 0), sticky="nsew")
# Camera display
camera_container = ttk.Frame(self.camera_frame, relief='solid', borderwidth=2)
camera_container.grid(sticky="nsew", padx=5, pady=5)
camera_container.columnconfigure(0, weight=1)
camera_container.rowconfigure(0, weight=1)
self.camera_label = ttk.Label(camera_container, text="Kamera wird geladen...", anchor="center")
self.camera_label.grid(sticky="nsew")
self.init_device_selector()
self.init_controls()
self.init_treeview()
self.init_statistics()
def init_device_selector(self):
device_frame = ttk.LabelFrame(self.control_frame, text="Kamera-Einstellungen", padding="10")
device_frame.pack(fill="x", pady=(0, 10))
ttk.Label(device_frame, text="Kamera auswählen:", style='Heading.TLabel').pack(anchor="w")
self.device_combo = ttk.Combobox(device_frame, state="readonly")
self.device_combo.pack(fill="x", pady=(5, 10))
available_devices = self.list_video_devices()
self.device_combo['values'] = [f"Kamera {i}" for i in available_devices]
if available_devices:
self.device_combo.current(0)
self.device_combo.bind("<<ComboboxSelected>>", self.change_camera)
# Camera status
self.camera_status = ttk.Label(device_frame, text="Status: Initialisierung...", style='Info.TLabel')
self.camera_status.pack(anchor="w")
def init_controls(self):
# Focus controls
focus_frame = ttk.LabelFrame(self.control_frame, text="Fokus-Steuerung", padding="10")
focus_frame.pack(fill="x", pady=(0, 10))
self.autofocus_var = tk.BooleanVar(value=True)
self.autofocus_check = ttk.Checkbutton(
focus_frame, text="Auto-Fokus", variable=self.autofocus_var, command=self.toggle_autofocus)
self.autofocus_check.pack(anchor="w", pady=(0, 5))
ttk.Label(focus_frame, text="Manueller Fokus:").pack(anchor="w")
self.focus_slider = ttk.Scale(focus_frame, from_=0, to=255, orient="horizontal")
self.focus_slider.set(0)
self.focus_slider.pack(fill="x", pady=(0, 5))
# Image processing controls
process_frame = ttk.LabelFrame(self.control_frame, text="Bildverbesserung", padding="10")
process_frame.pack(fill="x", pady=(0, 10))
ttk.Label(process_frame, text="Helligkeit:").pack(anchor="w")
self.brightness_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
self.brightness_slider.set(50)
self.brightness_slider.pack(fill="x", pady=(0, 5))
ttk.Label(process_frame, text="Kontrast:").pack(anchor="w")
self.contrast_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
self.contrast_slider.set(50)
self.contrast_slider.pack(fill="x", pady=(0, 10))
# Scan options
scan_frame = ttk.LabelFrame(self.control_frame, text="Scan-Optionen", padding="10")
scan_frame.pack(fill="x")
self.outline_var = tk.BooleanVar(value=True)
ttk.Checkbutton(scan_frame, text="Barcodes umranden", variable=self.outline_var).pack(anchor="w")
self.beep_var = tk.BooleanVar(value=True)
ttk.Checkbutton(scan_frame, text="Ton bei Scan", variable=self.beep_var).pack(anchor="w")
def init_statistics(self):
"""Initialize statistics panel"""
stats_frame = ttk.LabelFrame(self.info_frame, text="Statistiken", padding="10")
stats_frame.pack(fill="x", pady=(0, 10))
self.total_scans_label = ttk.Label(stats_frame, text="Gesamte Scans: 0", style='Info.TLabel')
self.total_scans_label.pack(anchor="w")
self.total_value_label = ttk.Label(stats_frame, text="Gesamtwert: €0,00", style='Success.TLabel')
self.total_value_label.pack(anchor="w")
self.session_time_label = ttk.Label(stats_frame, text="Sitzungsdauer: 00:00", style='Info.TLabel')
self.session_time_label.pack(anchor="w")
self.session_start = datetime.now()
self.total_scans = 0
self.total_value = 0.0
def init_treeview(self):
# Header frame with collapse button
tree_header_frame = ttk.Frame(self.info_frame)
tree_header_frame.pack(fill="x", pady=(0, 5))
# Collapse button
self.collapse_button = ttk.Button(
tree_header_frame,
text="▼ Scan-Liste",
command=self.toggle_scan_list,
width=15
)
self.collapse_button.pack(side="left")
# Clear button
clear_button = ttk.Button(
tree_header_frame,
text="Liste löschen",
command=self.clear_scan_list
)
clear_button.pack(side="right")
# Treeview frame (collapsible)
self.tree_container = ttk.Frame(self.info_frame)
self.tree_container.pack(fill="both", expand=True)
self.tree = ttk.Treeview(
self.tree_container,
columns=("Zeit", "Barcode", "Typ", "Pfand"),
show="headings",
height=15
)
# Configure columns
self.tree.heading("Zeit", text="Zeit")
self.tree.heading("Barcode", text="Barcode")
self.tree.heading("Typ", text="Typ")
self.tree.heading("Pfand", text="Pfand")
self.tree.column("Zeit", width=100, minwidth=80)
self.tree.column("Barcode", width=120, minwidth=100)
self.tree.column("Typ", width=80, minwidth=60)
self.tree.column("Pfand", width=70, minwidth=50)
# Scrollbars
v_scrollbar = ttk.Scrollbar(self.tree_container, orient="vertical", command=self.tree.yview)
h_scrollbar = ttk.Scrollbar(self.tree_container, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
# Grid layout
self.tree.grid(row=0, column=0, sticky="nsew")
v_scrollbar.grid(row=0, column=1, sticky="ns")
h_scrollbar.grid(row=1, column=0, sticky="ew")
self.tree_container.columnconfigure(0, weight=1)
self.tree_container.rowconfigure(0, weight=1)
# Alternate row colors
self.tree.tag_configure('oddrow', background='#f9f9f9')
self.tree.tag_configure('evenrow', background='white')
def toggle_scan_list(self):
"""Toggle the visibility of the scan list"""
if self.scan_list_collapsed.get():
# Expand
self.tree_container.pack(fill="both", expand=True)
self.collapse_button.configure(text="▼ Scan-Liste")
self.scan_list_collapsed.set(False)
# Resize window columns
self.main_frame.columnconfigure(2, weight=1)
else:
# Collapse
self.tree_container.pack_forget()
self.collapse_button.configure(text="▶ Scan-Liste")
self.scan_list_collapsed.set(True)
# Make info panel smaller
self.main_frame.columnconfigure(2, weight=0, minsize=200)
def clear_scan_list(self):
"""Clear all items from the scan list"""
if messagebox.askyesno("Liste löschen", "Möchten Sie wirklich alle Einträge aus der Scan-Liste löschen?"):
for item in self.tree.get_children():
self.tree.delete(item)
def list_video_devices(self, max_devices=10):
available = []
for i in range(max_devices):
cap = cv2.VideoCapture(i)
if cap.isOpened():
available.append(i)
cap.release()
return available
def change_camera(self, event=None):
index = self.device_combo.current()
self.selected_device_index.set(index)
self.init_camera()
def init_camera(self):
if hasattr(self, 'cap') and self.cap and self.cap.isOpened():
self.cap.release()
device_index = self.selected_device_index.get()
self.cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW if os.name == 'nt' else 0)
if self.cap.isOpened():
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
self.cap.set(cv2.CAP_PROP_FPS, 30)
self.toggle_autofocus()
self.camera_status.configure(text="Status: Verbunden ✓", foreground=self.colors['success'])
else:
self.camera_status.configure(text="Status: Fehler ✗", foreground=self.colors['danger'])
def toggle_autofocus(self):
if self.cap:
if self.autofocus_var.get():
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 1)
self.focus_slider.state(['disabled'])
else:
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 0)
self.focus_slider.state(['!disabled'])
self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get())
def adjust_image(self, frame):
brightness = self.brightness_slider.get() / 50.0 - 1.0
contrast = self.contrast_slider.get() / 50.0
adjusted = cv2.convertScaleAbs(frame, alpha=contrast, beta=brightness * 127)
gray = cv2.cvtColor(adjusted, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
return cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
def draw_barcode_outline(self, frame, barcodes):
if not self.outline_var.get():
return frame
for barcode in barcodes:
# Get barcode polygon points
points = barcode.polygon
if len(points) == 4:
# Convert to numpy array
pts = np.array([[point.x, point.y] for point in points], np.int32)
pts = pts.reshape((-1, 1, 2))
# Draw colored outline based on barcode type
barcode_data = barcode.data.decode('utf-8')
if len(barcode_data) == 13:
color = (0, 255, 0) # Green for EINWEG
elif len(barcode_data) == 8:
color = (255, 0, 0) # Blue for MEHRWEG
else:
color = (0, 165, 255) # Orange for DOSE
# Draw outline
cv2.polylines(frame, [pts], True, color, 3)
# Add label
rect = cv2.boundingRect(pts)
label_pos = (rect[0], rect[1] - 10)
pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE"
deposit = self.pfand_values.get(pfand_type, 0.00)
label = f"{pfand_type}: €{deposit:.2f}"
# Draw label background
(text_width, text_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
cv2.rectangle(frame, (label_pos[0], label_pos[1] - text_height - 5),
(label_pos[0] + text_width, label_pos[1] + 5), color, -1)
# Draw label text
cv2.putText(frame, label, label_pos, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
return frame
def start_threads(self):
"""Start camera and processing threads"""
self.camera_thread = threading.Thread(target=self.camera_worker, daemon=True)
self.camera_thread.start()
self.process_thread = threading.Thread(target=self.process_worker, daemon=True)
self.process_thread.start()
# Start UI update loop
self.update_preview()
self.process_queue()
def camera_worker(self):
"""Worker thread for camera capture and processing"""
while self.running:
try:
if not hasattr(self, 'cap') or not self.cap.isOpened():
time.sleep(0.1)
continue
ret, frame = self.cap.read()
if ret:
if not self.autofocus_var.get():
self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get())
current_time = time.time()
if current_time - self.last_process_time >= self.process_interval:
processed_frame = self.adjust_image(frame)
barcodes = decode(processed_frame) or decode(frame)
self.current_barcodes = barcodes
for barcode in barcodes:
barcode_data = barcode.data.decode('utf-8')
self.queue.put(barcode_data)
self.last_process_time = current_time
# Draw barcode outlines
frame_with_outlines = self.draw_barcode_outline(frame.copy(), self.current_barcodes)
# Put frame in queue for UI thread
try:
if self.frame_queue.full():
self.frame_queue.get_nowait() # Discard old frame
self.frame_queue.put(frame_with_outlines, timeout=0.1)
except queue.Full:
pass # Skip this frame if queue is full
except Exception as e:
print(f"Fehler in der Kamera-Verarbeitung: {e}")
time.sleep(0.1)
def process_worker(self):
"""Worker thread for barcode processing"""
while self.running:
try:
barcode_data = self.queue.get(timeout=0.1)
now = datetime.now()
# Rate limiting
timestamps = self.barcode_times.get(barcode_data, [])
timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)]
if len(timestamps) >= 3:
continue
timestamps.append(now)
self.barcode_times[barcode_data] = timestamps
current_time = now.strftime("%H:%M:%S")
pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE"
deposit = self.pfand_values.get(pfand_type, 0.00)
# Update UI in main thread
self.window.after(0, self.add_to_treeview, current_time, barcode_data, pfand_type, deposit)
# Show product selection dialog
if barcode_data not in self.prompted_barcodes:
self.prompted_barcodes.add(barcode_data)
self.window.after(0, self.show_product_selection, barcode_data)
except queue.Empty:
pass
except Exception as e:
print(f"Fehler in der Warteschlangenverarbeitung: {e}")
def add_to_treeview(self, current_time, barcode_data, pfand_type, deposit):
"""Add item to treeview (called from main thread)"""
# Determine row color
row_count = len(self.tree.get_children())
tag = 'evenrow' if row_count % 2 == 0 else 'oddrow'
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}"), tags=(tag,))
# Update statistics
self.total_scans += 1
self.update_statistics()
# Sound notification
if self.beep_var.get():
self.window.bell()
def update_preview(self):
"""Update camera preview (called from main thread)"""
try:
# Get frame from queue
frame = self.frame_queue.get_nowait()
# Convert and display
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
img = Image.fromarray(cv2image)
# Resize to fit label while maintaining aspect ratio
label_width = self.camera_label.winfo_width()
label_height = self.camera_label.winfo_height()
if label_width > 1 and label_height > 1:
img.thumbnail((label_width, label_height), Image.Resampling.LANCZOS)
imgtk = ImageTk.PhotoImage(image=img)
self.camera_label.imgtk = imgtk
self.camera_label.configure(image=imgtk)
except queue.Empty:
pass # No new frame available
except Exception as e:
print(f"Fehler in der Video-Vorschau: {e}")
# Schedule next update
if self.running:
self.window.after(30, self.update_preview)
def update_statistics(self):
session_time = datetime.now() - self.session_start
hours, remainder = divmod(int(session_time.total_seconds()), 3600)
minutes, _ = divmod(remainder, 60)
time_str = f"{hours:02d}:{minutes:02d}"
self.session_time_label.configure(text=f"Sitzungsdauer: {time_str}")
self.total_scans_label.configure(text=f"Gesamte Scans: {self.total_scans}")
self.total_value_label.configure(text=f"Gesamtwert: €{self.total_value:.2f}")
def show_product_selection(self, barcode_data):
if hasattr(self, 'product_win') and self.product_win.winfo_exists():
return
self.product_win = tk.Toplevel(self.window)
self.product_win.title("Produkt auswählen")
self.product_win.geometry("500x400")
self.product_win.minsize(450, 350)
self.product_win.resizable(True, True)
# Center the window
self.product_win.transient(self.window)
self.product_win.grab_set()
# Configure grid weights for proper expansion
self.product_win.columnconfigure(0, weight=1)
self.product_win.rowconfigure(1, weight=1)
# Header frame
header_frame = ttk.Frame(self.product_win, padding="10")
header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 5))
header_frame.columnconfigure(0, weight=1)
ttk.Label(header_frame, text="Produkt für Barcode auswählen:",
style='Heading.TLabel').grid(row=0, column=0, sticky="w")
ttk.Label(header_frame, text=f"'{barcode_data}'",
style='Info.TLabel', font=('Courier', 10)).grid(row=1, column=0, sticky="w", pady=(0, 5))
# Main content frame with scrollable area
content_frame = ttk.Frame(self.product_win)
content_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
content_frame.columnconfigure(0, weight=1)
content_frame.rowconfigure(0, weight=1)
selected_product = tk.StringVar()
# Create a canvas with scrollbar for the product list
# Create a canvas with scrollbar for the product list
canvas = tk.Canvas(content_frame, bg='white', highlightthickness=0)
scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=scrollbar.set)
# Frame inside canvas
scrollable_frame = ttk.Frame(canvas)
window_id = canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
# Update scrollregion when frame contents change
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
# Resize inner frame width when canvas resizes
def on_canvas_configure(event):
canvas.itemconfig(window_id, width=event.width)
canvas.bind("<Configure>", on_canvas_configure)
# Place canvas + scrollbar
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
# Add products to scrollable frame
if self.products:
for i, product in enumerate(self.products):
current_quantity = self.quantities.get(product, 0)
price = self.prices.get(product, 0.00)
product_frame = ttk.Frame(scrollable_frame, padding="5")
product_frame.pack(fill="x", pady=2)
product_frame.columnconfigure(1, weight=1)
ttk.Radiobutton(
product_frame,
text=product,
variable=selected_product,
value=product
).grid(row=0, column=0, sticky="w")
info_text = f"Aktuell: {current_quantity}, Preis: €{price:.2f}"
ttk.Label(product_frame, text=info_text, style='Info.TLabel').grid(row=0, column=1, sticky="e", padx=(10, 0))
else:
no_products_label = ttk.Label(scrollable_frame, text="Noch keine Produkte definiert.",
style='Info.TLabel', justify="center")
no_products_label.pack(pady=20)
# Button frame at the bottom
button_frame = ttk.Frame(self.product_win, padding="10")
button_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=(5, 10))
# Configure button frame columns for proper spacing
button_frame.columnconfigure(0, weight=1)
button_frame.columnconfigure(1, weight=0)
button_frame.columnconfigure(2, weight=0)
button_frame.columnconfigure(3, weight=0)
# Helper functions for buttons
def confirm():
product = selected_product.get()
if product:
# Update quantity
self.quantities[product] = self.quantities.get(product, 0) + 1
# Update total value with actual product price
product_price = self.prices.get(product, 0.00)
self.total_value += product_price
self.save_json()
self.update_statistics()
self.product_win.destroy()
# Show confirmation message
messagebox.showinfo("Erfolgreich", f"Produkt '{product}' wurde hinzugefügt!")
else:
messagebox.showwarning("Keine Auswahl", "Bitte wählen Sie ein Produkt aus.")
def cancel():
self.product_win.destroy()
def add_new_product():
new_product = simpledialog.askstring("Neues Produkt", "Name des neuen Produkts:")
if new_product and new_product.strip():
new_product = new_product.strip()
if new_product not in self.products:
# Ask for price
price_str = simpledialog.askstring("Preis", f"Preis für '{new_product}' (€):")
try:
price = float(price_str.replace(',', '.')) if price_str else 0.00
except:
price = 0.00
self.products.append(new_product)
self.prices[new_product] = price
# Save products
try:
products_data = {"products": self.products, "prices": self.prices}
with open(self.products_file, 'w', encoding='utf-8') as f:
json.dump(products_data, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"Fehler beim Speichern der Produkte: {e}")
# Refresh the dialog
self.product_win.destroy()
self.window.after(0, self.show_product_selection, barcode_data)
else:
messagebox.showwarning("Produkt existiert", "Dieses Produkt ist bereits vorhanden.")
# Buttons with proper layout
ttk.Button(button_frame, text="Neues Produkt", command=add_new_product).grid(row=0, column=0, sticky="w", padx=5)
ttk.Button(button_frame, text="Abbrechen", command=cancel).grid(row=0, column=2, padx=5)
ttk.Button(button_frame, text="Bestätigen", command=confirm).grid(row=0, column=3, padx=5)
# Bind Enter key to confirm
self.product_win.bind('<Return>', lambda e: confirm())
self.product_win.bind('<Escape>', lambda e: cancel())
# Set focus to the window
self.product_win.focus_set()
def process_queue(self):
try:
# This method is now handled by the process_worker thread
pass
except Exception as e:
print(f"Fehler in der Warteschlangenverarbeitung: {e}")
# Schedule next check
if self.running:
self.window.after(100, self.process_queue)
def on_closing(self):
self.running = False
# Wait for camera thread to finish
if self.camera_thread and self.camera_thread.is_alive():
self.camera_thread.join(timeout=1.0)
if self.process_thread and self.process_thread.is_alive():
self.process_thread.join(timeout=1.0)
try:
if hasattr(self, 'cap') and self.cap and self.cap.isOpened():
self.cap.release()
cv2.destroyAllWindows()
except:
pass
self.window.destroy()
if __name__ != "__main__":
def launch_pfand_scanner():
scanner_window = tk.Toplevel()
PfandScanner(scanner_window, "µScan V2.3.5")
else:
# For standalone testing
root = tk.Tk()
app = PfandScanner(root, "µScan V2.3.5")
root.mainloop()