update
This commit is contained in:
236
AzA march 2026/aza_empfang_region_snapshot.py
Normal file
236
AzA march 2026/aza_empfang_region_snapshot.py
Normal file
@@ -0,0 +1,236 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Vollbild-Overlay zur Bereichswahl fuer Snapshot in der Empfang-Desktop-WebView.
|
||||
|
||||
- Nur nach expliziter Nutzeraktion (Klick auf Snapshot) gestartet.
|
||||
- Keine Netz-/Log-Ausgabe von Bildinhalten oder Praxisdaten.
|
||||
- Windows: virtuelle Bildschirmflaeche; PIL ImageGrab fuer den gewaehlten Bereich.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
import sys
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from typing import Callable
|
||||
|
||||
_LOCK = threading.Lock()
|
||||
_ACTIVE = False
|
||||
|
||||
# Sehr kleine Auswahlen ignorieren (Klicks ohne Zieh-Bewegung).
|
||||
_MIN_SIDE_PX = 6
|
||||
|
||||
|
||||
def _virtual_screen_win32() -> tuple[int, int, int, int] | None:
|
||||
if sys.platform != "win32":
|
||||
return None
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
SM_XVIRTUALSCREEN = 76
|
||||
SM_YVIRTUALSCREEN = 77
|
||||
SM_CXVIRTUALSCREEN = 78
|
||||
SM_CYVIRTUALSCREEN = 79
|
||||
x = int(user32.GetSystemMetrics(SM_XVIRTUALSCREEN))
|
||||
y = int(user32.GetSystemMetrics(SM_YVIRTUALSCREEN))
|
||||
w = int(user32.GetSystemMetrics(SM_CXVIRTUALSCREEN))
|
||||
h = int(user32.GetSystemMetrics(SM_CYVIRTUALSCREEN))
|
||||
if w <= 0 or h <= 0:
|
||||
return None
|
||||
return (x, y, w, h)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _grab_bbox_to_png_data_url(left: int, top: int, right: int, bottom: int) -> str | None:
|
||||
"""Liefert data:image/png;base64,... oder None bei Fehler."""
|
||||
try:
|
||||
from PIL import ImageGrab
|
||||
except Exception:
|
||||
return None
|
||||
L = min(left, right)
|
||||
T = min(top, bottom)
|
||||
R = max(left, right)
|
||||
B = max(top, bottom)
|
||||
if R - L < _MIN_SIDE_PX or B - T < _MIN_SIDE_PX:
|
||||
return None
|
||||
try:
|
||||
try:
|
||||
img = ImageGrab.grab(bbox=(L, T, R, B), all_screens=True) # type: ignore[call-arg]
|
||||
except TypeError:
|
||||
img = ImageGrab.grab(bbox=(L, T, R, B))
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG", optimize=True)
|
||||
raw = buf.getvalue()
|
||||
if not raw or len(raw) < 32:
|
||||
return None
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
return f"data:image/png;base64,{b64}"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _run_overlay(on_done: Callable[[str | None], None]) -> None:
|
||||
global _ACTIVE
|
||||
root = tk.Tk()
|
||||
root.title("")
|
||||
root.configure(cursor="crosshair", bg="#101820")
|
||||
try:
|
||||
root.attributes("-alpha", 0.28)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
root.attributes("-topmost", True)
|
||||
except Exception:
|
||||
pass
|
||||
root.overrideredirect(True)
|
||||
|
||||
box = _virtual_screen_win32()
|
||||
if box:
|
||||
vx, vy, vw, vh = box
|
||||
root.geometry(f"{vw}x{vh}+{vx}+{vy}")
|
||||
else:
|
||||
try:
|
||||
root.attributes("-fullscreen", True)
|
||||
except Exception:
|
||||
root.state("zoomed")
|
||||
|
||||
canvas = tk.Canvas(root, highlightthickness=0, bg="#000010", cursor="crosshair")
|
||||
canvas.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
hint = tk.Label(
|
||||
root,
|
||||
text="Bereich mit der Maus aufziehen. Esc oder Rechtsklick: Abbrechen.",
|
||||
fg="#e8eef4",
|
||||
bg="#1a2530",
|
||||
font=("Segoe UI", 10),
|
||||
padx=10,
|
||||
pady=6,
|
||||
)
|
||||
hint.place(x=8, y=8)
|
||||
|
||||
sel: dict[str, int | None] = {"x0": None, "y0": None, "rid": None}
|
||||
|
||||
def finish(data_url: str | None) -> None:
|
||||
global _ACTIVE
|
||||
try:
|
||||
root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
with _LOCK:
|
||||
_ACTIVE = False
|
||||
try:
|
||||
on_done(data_url)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_press(event: tk.Event) -> None: # type: ignore[name-defined]
|
||||
if int(getattr(event, "num", 1) or 1) == 3:
|
||||
finish(None)
|
||||
return
|
||||
sel["x0"] = int(event.x_root)
|
||||
sel["y0"] = int(event.y_root)
|
||||
if sel["rid"] is not None:
|
||||
try:
|
||||
canvas.delete(sel["rid"])
|
||||
except Exception:
|
||||
pass
|
||||
sel["rid"] = None
|
||||
|
||||
def _scr_to_cv(sx: int, sy: int) -> tuple[int, int]:
|
||||
return int(sx - canvas.winfo_rootx()), int(sy - canvas.winfo_rooty())
|
||||
|
||||
def on_motion(event: tk.Event) -> None: # type: ignore[name-defined]
|
||||
x0, y0 = sel["x0"], sel["y0"]
|
||||
if x0 is None or y0 is None:
|
||||
return
|
||||
x1, y1 = int(event.x_root), int(event.y_root)
|
||||
if sel["rid"] is not None:
|
||||
try:
|
||||
canvas.delete(sel["rid"])
|
||||
except Exception:
|
||||
pass
|
||||
cx0, cy0 = _scr_to_cv(int(x0), int(y0))
|
||||
cx1, cy1 = _scr_to_cv(x1, y1)
|
||||
sel["rid"] = canvas.create_rectangle(
|
||||
cx0,
|
||||
cy0,
|
||||
cx1,
|
||||
cy1,
|
||||
outline="#4af",
|
||||
width=2,
|
||||
dash=(4, 4),
|
||||
)
|
||||
|
||||
def on_release(event: tk.Event) -> None: # type: ignore[name-defined]
|
||||
x0, y0 = sel["x0"], sel["y0"]
|
||||
if x0 is None or y0 is None:
|
||||
return
|
||||
x1, y1 = int(event.x_root), int(event.y_root)
|
||||
data = _grab_bbox_to_png_data_url(x0, y0, x1, y1)
|
||||
finish(data)
|
||||
|
||||
def on_esc(_event: tk.Event | None = None) -> None: # type: ignore[name-defined]
|
||||
finish(None)
|
||||
|
||||
canvas.bind("<ButtonPress-1>", on_press)
|
||||
canvas.bind("<B1-Motion>", on_motion)
|
||||
canvas.bind("<ButtonRelease-1>", on_release)
|
||||
canvas.bind("<ButtonPress-3>", lambda e: finish(None))
|
||||
root.bind("<Escape>", on_esc)
|
||||
try:
|
||||
root.focus_force()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
root.mainloop()
|
||||
except Exception:
|
||||
with _LOCK:
|
||||
_ACTIVE = False
|
||||
try:
|
||||
on_done(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def start_region_snapshot_capture(
|
||||
on_done: Callable[[str | None], None],
|
||||
) -> str:
|
||||
"""
|
||||
Startet die Bereichswahl in einem eigenen Thread (eigenes Tk-Fenster).
|
||||
|
||||
Returns:
|
||||
"started" | "busy"
|
||||
"""
|
||||
global _ACTIVE
|
||||
with _LOCK:
|
||||
if _ACTIVE:
|
||||
return "busy"
|
||||
_ACTIVE = True
|
||||
|
||||
def _thread_main() -> None:
|
||||
global _ACTIVE
|
||||
try:
|
||||
_run_overlay(on_done)
|
||||
except Exception:
|
||||
with _LOCK:
|
||||
_ACTIVE = False
|
||||
try:
|
||||
on_done(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
threading.Thread(target=_thread_main, daemon=True).start()
|
||||
except Exception:
|
||||
with _LOCK:
|
||||
_ACTIVE = False
|
||||
return "busy"
|
||||
return "started"
|
||||
Reference in New Issue
Block a user