Bring back the native title bar so the app has a taskbar entry

Das randlose Fenster (overrideredirect) kostet unter Windows zwingend den
Taskleisten-Eintrag. Nachruesten liesse er sich nur per SetWindowLongW
(WS_EX_APPWINDOW) -- genau der Aufruf, der laut AGENTS.md schon
AV-Fehlalarme ausgeloest hat. Der User hat sich deshalb fuer die native
Titelleiste entschieden.

Damit kommen Taskleiste, Alt+Tab, Aero-Snap sowie Ziehen und
Groessenaendern am Rahmen nativ zurueck, und die Eigenbau-Loesungen dafuer
entfallen ersatzlos: ziehbare Kopfzeile, Anfasser unten rechts (ersetzt
durch self.minsize()), eigene ✕/—-Knoepfe und der <Unmap>-Handler, der
Minimieren ins Tray umgeleitet hat.

Schliessen legt weiterhin ins Tray statt zu beenden (wie die offizielle
VersaGUI). Minimieren geht jetzt normal in die Taskleiste, und Escape
minimiert ebenfalls statt ins Tray zu legen -- mit Taskleisten-Eintrag
waere "verschwindet spurlos" die unangenehmere Ueberraschung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-08-29 01:02:07 +02:00
parent 50868ab081
commit e1e3d0e466

View file

@ -56,8 +56,9 @@ WARN_RED = "#e0895a"
POLL_MS = 1500 POLL_MS = 1500
CARD_W, CARD_H = 150, 114 CARD_W, CARD_H = 150, 114
# Untergrenze beim Ziehen am Anfasser -- knapp unter der Groesse, die das # Untergrenze fuers Verkleinern (self.minsize) -- knapp unter der Groesse,
# 4x5-Grid + Encoder mindestens brauchen, damit nichts abgeschnitten wird. # die das 4x5-Grid + Encoder mindestens brauchen, damit nichts
# abgeschnitten wird.
MIN_W, MIN_H = 700, 500 MIN_W, MIN_H = 700, 500
SERIAL_POLL_S = 1.5 SERIAL_POLL_S = 1.5
SERIAL_IDLE_S = 3.0 SERIAL_IDLE_S = 3.0
@ -130,8 +131,8 @@ class VersaPadViewer(tk.Tk):
self.configure(bg=BG) self.configure(bg=BG)
# Hoehe muss Kopfzeile + Tabs + 5 Kartenreihen + Encoder + Fusszeile # Hoehe muss Kopfzeile + Tabs + 5 Kartenreihen + Encoder + Fusszeile
# fassen -- seit die Karten eine Notizzeile haben (CARD_H 84 -> 114) # fassen -- seit die Karten eine Notizzeile haben (CARD_H 84 -> 114)
# reichten die alten 760px nicht mehr: Encoder und der Groessen- # reichten die alten 760px nicht mehr: der Encoder-Bereich lag
# Anfasser lagen unterhalb des Fensterrands und waren unerreichbar. # unterhalb des Fensterrands. Wer CARD_H aendert, muss hier mit.
self.geometry("790x960") self.geometry("790x960")
self.profile = 0 self.profile = 0
self._mtimes = {} self._mtimes = {}
@ -165,32 +166,23 @@ class VersaPadViewer(tk.Tk):
) )
self._tray_icon.run_detached() self._tray_icon.run_detached()
# Titelleiste ausgeblendet (reines Tk `overrideredirect`, KEINE # Normales Fenster MIT Windows-Titelleiste. Bis 2026-08-28 lief es
# ctypes/WinAPI-Fenstertricks -- siehe Speicher-Notiz "keine # randlos (`overrideredirect(True)`) -- das kostet unter Windows aber
# Selbstversteck-Fenstertricks": das Nachruesten eines Taskleisten- # zwingend den Taskleisten-Eintrag, und der wurde ausdruecklich
# Icons per SetWindowLongW hat frueher AV-Fehlalarme ausgeloest). # gebraucht. Nachruesten liesse er sich nur per `SetWindowLongW`
# Ersatz fuer die fehlende Systemleiste: Kopfzeile ist ziehbar, und # (WS_EX_APPWINDOW) -- genau der Aufruf, der laut Speicher-Notiz
# die Buttons rechts uebernehmen Minimieren/Schliessen (beides ins # "keine Selbstversteck-Fenstertricks" schon AV-Fehlalarme
# Tray, wie vorher schon das X der echten Titelleiste). # ausgeloest hat und deshalb nicht in Frage kommt. Mit der echten
self.overrideredirect(True) # Titelleiste kommen Taskleiste, Alt+Tab, Aero-Snap, Ziehen und
# Groessenaendern am Rahmen nativ zurueck; die Eigenbau-Loesungen
# dafuer (ziehbare Kopfzeile, Anfasser unten rechts, eigene
# ✕/—-Knoepfe) sind damit ersatzlos entfallen.
self.minsize(MIN_W, MIN_H)
self.toggles_row = header = tk.Frame(self, bg=BG) self.toggles_row = header = tk.Frame(self, bg=BG)
header.pack(fill="x", padx=20, pady=(10, 6)) header.pack(fill="x", padx=20, pady=(10, 6))
title = tk.Label(header, text="VersaPad", bg=BG, fg=TEXT, tk.Label(header, text="VersaPad", bg=BG, fg=TEXT,
font=("Segoe UI", 11, "bold")) font=("Segoe UI", 11, "bold")).pack(side="left", anchor="w", padx=(0, 16))
title.pack(side="left", anchor="w", padx=(0, 16))
for widget in (header, title):
widget.bind("<Button-1>", self._start_move)
widget.bind("<B1-Motion>", self._on_move)
for text in ("", ""):
btn = tk.Label(header, text=text, bg=BG, fg=TEXT_DIM,
font=("Segoe UI", 11), cursor="hand2", padx=6)
btn.pack(side="right")
btn.bind("<Button-1>", lambda e: self._hide_to_tray())
btn.bind("<Enter>", lambda e, b=btn: b.configure(fg=TEXT))
btn.bind("<Leave>", lambda e, b=btn: b.configure(fg=TEXT_DIM))
info_btn = tk.Label(header, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 12), info_btn = tk.Label(header, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 12),
cursor="hand2", padx=6) cursor="hand2", padx=6)
@ -264,29 +256,18 @@ class VersaPadViewer(tk.Tk):
self.enc_frame = tk.Frame(self, bg=BG) self.enc_frame = tk.Frame(self, bg=BG)
self.enc_frame.pack(fill="x", padx=20) self.enc_frame.pack(fill="x", padx=20)
# Fusszeile + Anfasser zum Groessenaendern: mit ausgeblendeter
# Titelleiste (overrideredirect) entfernt Windows auch die
# Fensterraender, an denen man sonst zieht -- ohne diesen Griff
# liesse sich das Fenster gar nicht mehr skalieren.
footer_row = tk.Frame(self, bg=BG) footer_row = tk.Frame(self, bg=BG)
footer_row.pack(fill="x", padx=20, pady=(14, 8)) footer_row.pack(fill="x", padx=20, pady=(14, 8))
self.footer = tk.Label(footer_row, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8)) self.footer = tk.Label(footer_row, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8))
self.footer.pack(side="left", anchor="w") self.footer.pack(side="left", anchor="w")
# Der Griff haengt per place() an der FENSTER-Ecke, nicht am Ende des # Schliessen legt weiterhin ins Tray statt zu beenden (wie die
# gepackten Inhalts: sonst wandert er mit dem Inhalt aus dem Bild, # offizielle VersaGUI -- das Programm soll im Hintergrund
# sobald das Fenster kleiner als der Inhalt ist -- also genau dann, # weiterlaufen). Minimieren geht jetzt aber ganz normal in die
# wenn man ihn zum Vergroessern braucht. # Taskleiste; frueher hat ein <Unmap>-Handler auch das ins Tray
grip = tk.Label(self, text="", bg=BG, fg=TEXT_DIM, # umgeleitet, was ohne Taskleisten-Eintrag sinnvoll war und mit
font=("Segoe UI", 11), cursor="sizing") # einem das Fenster nur unauffindbar machen wuerde.
grip.place(relx=1.0, rely=1.0, anchor="se", x=-3, y=-1)
grip.bind("<Button-1>", self._start_resize)
grip.bind("<B1-Motion>", self._on_resize)
grip.bind("<Enter>", lambda e: grip.configure(fg=TEXT))
grip.bind("<Leave>", lambda e: grip.configure(fg=TEXT_DIM))
self.protocol("WM_DELETE_WINDOW", self._hide_to_tray) self.protocol("WM_DELETE_WINDOW", self._hide_to_tray)
self.bind("<Unmap>", self._on_unmap)
self._bind_shortcuts() self._bind_shortcuts()
self._serial_thread.start() self._serial_thread.start()
@ -388,25 +369,6 @@ class VersaPadViewer(tk.Tk):
def _on_toggle_topmost(self): def _on_toggle_topmost(self):
self.attributes("-topmost", self.always_on_top.get()) self.attributes("-topmost", self.always_on_top.get())
# ── Fenster ziehen (Ersatz fuer die ausgeblendete Titelleiste) ──
def _start_move(self, event):
self._drag_origin = (event.x_root - self.winfo_x(), event.y_root - self.winfo_y())
def _on_move(self, event):
dx, dy = self._drag_origin
self.geometry(f"+{event.x_root - dx}+{event.y_root - dy}")
def _start_resize(self, event):
self._resize_origin = (event.x_root, event.y_root,
self.winfo_width(), self.winfo_height())
def _on_resize(self, event):
x0, y0, w0, h0 = self._resize_origin
width = max(MIN_W, w0 + (event.x_root - x0))
height = max(MIN_H, h0 + (event.y_root - y0))
self.geometry(f"{width}x{height}")
# ── Live-Sync mit dem Board ──────────────────────────────────── # ── Live-Sync mit dem Board ────────────────────────────────────
def _on_toggle_sync(self): def _on_toggle_sync(self):
@ -452,13 +414,7 @@ class VersaPadViewer(tk.Tk):
text = SERIAL_STATUS_TEXT.get(error, error or "Fehler") text = SERIAL_STATUS_TEXT.get(error, error or "Fehler")
self.sync_status.configure(text=text, fg=WARN_RED) self.sync_status.configure(text=text, fg=WARN_RED)
# ── Tray-Icon (kein Taskleisten-Eintrag beim Minimieren, wie VersaGUI) ── # ── Tray-Icon (Schliessen beendet nicht, wie bei der VersaGUI) ──
def _on_unmap(self, event):
"""Minimieren faengt Windows normalerweise als Taskleisten-Icon ab --
wir wollen stattdessen: Fenster komplett weg, nur noch Tray-Icon."""
if event.widget is self and self.state() == "iconic":
self._hide_to_tray()
def _hide_to_tray(self): def _hide_to_tray(self):
self.withdraw() self.withdraw()
@ -636,7 +592,11 @@ class VersaPadViewer(tk.Tk):
haelt dessen grab_set() die Tastatur -- die Kuerzel hier koennen ihm haelt dessen grab_set() die Tastatur -- die Kuerzel hier koennen ihm
also nicht dazwischenfunken.""" also nicht dazwischenfunken."""
for seq, handler in ( for seq, handler in (
("<Escape>", lambda e: self._hide_to_tray()), # Minimieren statt ins Tray: seit das Fenster eine Titelleiste
# und damit einen Taskleisten-Eintrag hat, waere "verschwindet
# spurlos" die unangenehmere Ueberraschung. Ins Tray legt
# weiterhin das ✕ der Titelleiste.
("<Escape>", lambda e: self.iconify()),
("<F2>", lambda e: self._rename_tab(self.profile)), ("<F2>", lambda e: self._rename_tab(self.profile)),
("<Control-e>", lambda e: self._toggle_editing_shortcut()), ("<Control-e>", lambda e: self._toggle_editing_shortcut()),
("<Control-E>", lambda e: self._toggle_editing_shortcut()), ("<Control-E>", lambda e: self._toggle_editing_shortcut()),