760 lines
32 KiB
Python
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() |