fixed shit. now threading works fine and it looks better
This commit is contained in:
@@ -43,23 +43,29 @@ class PfandScanner:
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
self.update_preview()
|
||||
# Start threads
|
||||
self.start_threads()
|
||||
|
||||
self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
self.process_queue()
|
||||
|
||||
def setup_styles(self):
|
||||
"""Setup custom styles for a modern look"""
|
||||
self.style = ttk.Style()
|
||||
self.style.theme_use('clam')
|
||||
|
||||
@@ -84,7 +90,6 @@ class PfandScanner:
|
||||
self.style.configure('Camera.TFrame', relief='solid', borderwidth=2)
|
||||
|
||||
def load_data(self):
|
||||
"""Load products from products.json and quantities from quantities.json"""
|
||||
# Load products
|
||||
if os.path.exists(self.products_file):
|
||||
try:
|
||||
@@ -132,10 +137,10 @@ class PfandScanner:
|
||||
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 V2.3.0", style='Title.TLabel')
|
||||
title_label = ttk.Label(header_frame, text="µScan", style='Title.TLabel')
|
||||
title_label.pack(side="left")
|
||||
|
||||
status_label = ttk.Label(header_frame, text="Erweiterte Barcode-Scanner", style='Info.TLabel')
|
||||
status_label = ttk.Label(header_frame, text="v2.3.5", style='Info.TLabel')
|
||||
status_label.pack(side="right")
|
||||
|
||||
# Control Panel (Left)
|
||||
@@ -417,52 +422,133 @@ class PfandScanner:
|
||||
|
||||
return frame
|
||||
|
||||
def update_preview(self):
|
||||
try:
|
||||
if not hasattr(self, 'cap') or not self.cap.isOpened():
|
||||
self.window.after(100, self.update_preview)
|
||||
return
|
||||
|
||||
ret, frame = self.cap.read()
|
||||
if ret:
|
||||
if not self.autofocus_var.get():
|
||||
self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get())
|
||||
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()
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
for barcode in barcodes:
|
||||
barcode_data = barcode.data.decode('utf-8')
|
||||
self.queue.put(barcode_data)
|
||||
self.last_process_time = current_time
|
||||
ret, frame = self.cap.read()
|
||||
if ret:
|
||||
if not self.autofocus_var.get():
|
||||
self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get())
|
||||
|
||||
# Draw barcode outlines
|
||||
frame_with_outlines = self.draw_barcode_outline(frame.copy(), self.current_barcodes)
|
||||
|
||||
# Convert and display
|
||||
cv2image = cv2.cvtColor(frame_with_outlines, 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)
|
||||
|
||||
# Update session time
|
||||
self.update_statistics()
|
||||
|
||||
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}")
|
||||
|
||||
self.window.after(10, self.update_preview)
|
||||
|
||||
# Schedule next update
|
||||
if self.running:
|
||||
self.window.after(30, self.update_preview)
|
||||
|
||||
def update_statistics(self):
|
||||
session_time = datetime.now() - self.session_start
|
||||
@@ -481,76 +567,96 @@ class PfandScanner:
|
||||
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
|
||||
# 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="20")
|
||||
header_frame.grid(row=0, column=0, sticky="ew")
|
||||
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').pack(pady=(0, 5))
|
||||
ttk.Label(header_frame, text=f"'{barcode_data}'", style='Info.TLabel', font=('Courier', 10)).pack(pady=(0, 10))
|
||||
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
|
||||
content_frame = ttk.Frame(self.product_win, padding="20")
|
||||
content_frame.grid(row=1, column=0, sticky="nsew")
|
||||
# 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:
|
||||
# Create scrollable frame for products
|
||||
canvas = tk.Canvas(content_frame, bg='white')
|
||||
scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview)
|
||||
scrollable_frame = ttk.Frame(canvas)
|
||||
|
||||
scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
||||
)
|
||||
|
||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
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=f"{product}",
|
||||
text=product,
|
||||
variable=selected_product,
|
||||
value=product
|
||||
).pack(side="left")
|
||||
).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').pack(side="right")
|
||||
|
||||
canvas.grid(row=0, column=0, sticky="nsew")
|
||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
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:
|
||||
ttk.Label(content_frame, text="Noch keine Produkte definiert.", style='Info.TLabel').pack(pady=20)
|
||||
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
|
||||
button_frame = ttk.Frame(self.product_win, padding="20")
|
||||
button_frame.grid(row=2, column=0, sticky="ew")
|
||||
|
||||
# Configure button frame columns
|
||||
# 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:
|
||||
@@ -573,7 +679,6 @@ class PfandScanner:
|
||||
def cancel():
|
||||
self.product_win.destroy()
|
||||
|
||||
# Add new product button
|
||||
def add_new_product():
|
||||
new_product = simpledialog.askstring("Neues Produkt", "Name des neuen Produkts:")
|
||||
if new_product and new_product.strip():
|
||||
@@ -603,66 +708,39 @@ class PfandScanner:
|
||||
else:
|
||||
messagebox.showwarning("Produkt existiert", "Dieses Produkt ist bereits vorhanden.")
|
||||
|
||||
# Buttons with better layout
|
||||
# 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=1, padx=5)
|
||||
ttk.Button(button_frame, text="Bestätigen", command=confirm).grid(row=0, column=2, 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:
|
||||
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:
|
||||
return
|
||||
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)
|
||||
|
||||
# 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
|
||||
# Note: Total value is updated in product selection dialog
|
||||
|
||||
# Sound notification
|
||||
if self.beep_var.get():
|
||||
self.window.bell()
|
||||
|
||||
# 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:
|
||||
# This method is now handled by the process_worker thread
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Fehler in der Warteschlangenverarbeitung: {e}")
|
||||
finally:
|
||||
|
||||
# Schedule next check
|
||||
if self.running:
|
||||
self.window.after(100, self.process_queue)
|
||||
|
||||
def on_closing(self):
|
||||
"""Clean shutdown of the application"""
|
||||
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()
|
||||
@@ -674,9 +752,9 @@ class PfandScanner:
|
||||
if __name__ != "__main__":
|
||||
def launch_pfand_scanner():
|
||||
scanner_window = tk.Toplevel()
|
||||
PfandScanner(scanner_window, "µScan V2.3.0 - Verbesserte Deutsche Version")
|
||||
PfandScanner(scanner_window, "µScan V2.3.5")
|
||||
else:
|
||||
# For standalone testing
|
||||
root = tk.Tk()
|
||||
app = PfandScanner(root, "µScan V2.3.0 - Verbesserte Deutsche Version")
|
||||
app = PfandScanner(root, "µScan V2.3.5")
|
||||
root.mainloop()
|
||||
Reference in New Issue
Block a user