Sein Fund mitten im Lauf: "pack doch die maus nicht immer auf das programm, weil ich kann dir sonst nichts schreiben". Im Testlauf oeffnen Fenster und Dialoge jetzt bei x = Bildschirmbreite + 20 - auf dem zweiten Monitor, wenn einer da ist -, und kein Dialog ruft mehr grab_set. Unsichtbar machen (withdraw) waere falsch: dann sieht niemand mehr, was der Pruefstand tut - und ein weggenommenes Fenster baut seine Dialoge nicht mehr richtig auf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
150 lines
7.5 KiB
Python
150 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Farben nach dem Windows-Theme (hell/dunkel) - inklusive Titelleiste.
|
|
|
|
Native tk-Menues und -Popups lassen sich unter Windows nicht einfaerben, darum
|
|
baut das Programm Menueleiste und Dropdowns selbst; hier stehen nur die Farben.
|
|
"""
|
|
|
|
import sys
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
import config
|
|
|
|
|
|
def windows_dark_mode():
|
|
"""True, wenn Windows auf das dunkle App-Theme eingestellt ist."""
|
|
if not sys.platform.startswith("win"):
|
|
return False
|
|
try:
|
|
import winreg
|
|
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize") as k:
|
|
return winreg.QueryValueEx(k, "AppsUseLightTheme")[0] == 0
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
class ThemeMixin:
|
|
"""Farben setzen. Erwartet ein tk.Tk als Basis."""
|
|
|
|
# ----------------------------------------------------------------- Theme
|
|
|
|
def _apply_theme(self):
|
|
"""Farben passend zum Windows-Theme (hell/dunkel) setzen."""
|
|
dark = windows_dark_mode()
|
|
self._dark_mode = dark
|
|
if dark:
|
|
self.col_bg = "#23272e" # Fenster-Hintergrund
|
|
self.col_fg = "#e6e6e6" # Schrift
|
|
self.col_field = "#2f343c" # Eingabefelder
|
|
self.col_sel = "#3a4a44" # aktiv/hover
|
|
self.col_accent = "#5fb98f" # Akzent (Gruen, hell)
|
|
self.col_line = "#3a3f47"
|
|
self.col_scroll = "#4a505a" # Scroll-Griff
|
|
self.col_muted = "#8a929c" # gedaempfte Schrift (z.B. "powered by")
|
|
else:
|
|
self.col_bg = "#f3f3f3"
|
|
self.col_fg = "#1a1a1a"
|
|
self.col_field = "#ffffff"
|
|
self.col_sel = "#cfe3d8"
|
|
self.col_accent = "#1f5c3d"
|
|
self.col_line = "#cccccc"
|
|
self.col_scroll = "#b8b8b8"
|
|
self.col_muted = "#777777" # gedaempfte Schrift (z.B. "powered by")
|
|
|
|
self.configure(bg=self.col_bg)
|
|
style = ttk.Style(self)
|
|
try:
|
|
style.theme_use("clam")
|
|
except tk.TclError:
|
|
pass
|
|
bg, fg, field, sel, accent = (self.col_bg, self.col_fg, self.col_field,
|
|
self.col_sel, self.col_accent)
|
|
style.configure(".", background=bg, foreground=fg, fieldbackground=field,
|
|
bordercolor=self.col_line, lightcolor=bg, darkcolor=bg)
|
|
style.configure("TFrame", background=bg)
|
|
style.configure("TLabel", background=bg, foreground=fg)
|
|
style.configure("TLabelframe", background=bg, bordercolor=self.col_line)
|
|
style.configure("TLabelframe.Label", background=bg, foreground=accent)
|
|
style.configure("TButton", background=field, foreground=fg, bordercolor=self.col_line)
|
|
style.map("TButton", background=[("active", sel)], foreground=[("active", fg)])
|
|
style.configure("Menu.TMenubutton", background=bg, foreground=fg, relief="flat",
|
|
arrowcolor=bg, padding=(10, 3))
|
|
style.map("Menu.TMenubutton", background=[("active", sel)], foreground=[("active", fg)])
|
|
style.configure("TCheckbutton", background=bg, foreground=fg)
|
|
style.map("TCheckbutton", background=[("active", bg)], foreground=[("active", fg)])
|
|
style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg)
|
|
# Bei state="readonly" zeichnet die Combobox ihren Text als MARKIERUNG,
|
|
# sobald sie den Fokus hat - ohne diese beiden Farben steht der Wert
|
|
# unsichtbar im Feld.
|
|
style.configure("TCombobox", fieldbackground=field, foreground=fg,
|
|
background=field, arrowcolor=fg,
|
|
selectbackground=field, selectforeground=fg)
|
|
style.map("TCombobox", fieldbackground=[("readonly", field)],
|
|
foreground=[("readonly", fg)], background=[("readonly", field)],
|
|
selectbackground=[("readonly", field)], selectforeground=[("readonly", fg)])
|
|
# Die aufgeklappte Liste ist ein klassisches tk-Listbox-Widget
|
|
self.option_add("*TCombobox*Listbox.background", field)
|
|
self.option_add("*TCombobox*Listbox.foreground", fg)
|
|
self.option_add("*TCombobox*Listbox.selectBackground", sel)
|
|
self.option_add("*TCombobox*Listbox.selectForeground", fg)
|
|
for sbs in ("Vertical.TScrollbar", "Horizontal.TScrollbar", "TScrollbar"):
|
|
style.configure(sbs, troughcolor=bg, background=self.col_scroll,
|
|
bordercolor=bg, lightcolor=bg, darkcolor=bg,
|
|
arrowcolor=fg, relief="flat", gripcount=0)
|
|
style.map(sbs, background=[("active", accent)],
|
|
troughcolor=[("disabled", bg)])
|
|
style.configure("TSeparator", background=self.col_line)
|
|
# Reiter (Campingrechnung / Montage): ohne eigene Farben zeichnet clam den
|
|
# AKTIVEN Reiter dunkler als die ruhenden - man sieht dann nicht, wo man
|
|
# ist. Der offene Reiter bekommt darum den Hintergrund des Blattes und die
|
|
# Akzentfarbe, die ruhenden bleiben zurueckgenommen.
|
|
style.configure("TNotebook", background=bg, bordercolor=self.col_line,
|
|
tabmargins=(2, 4, 2, 0))
|
|
style.configure("TNotebook.Tab", background=field, foreground=self.col_muted,
|
|
bordercolor=self.col_line, padding=(16, 7),
|
|
font=("Segoe UI", 10))
|
|
style.map("TNotebook.Tab",
|
|
background=[("selected", bg), ("active", sel)],
|
|
foreground=[("selected", accent), ("active", fg)],
|
|
font=[("selected", ("Segoe UI", 10, "bold"))],
|
|
expand=[("selected", (1, 1, 1, 0))])
|
|
# Klassische tk-Widgets (Canvas, Toplevel, Text) erben diese Defaults
|
|
self.option_add("*background", bg)
|
|
self.option_add("*foreground", fg)
|
|
|
|
def _style_dialog(self, d):
|
|
"""Toplevel-Dialog an das Theme angleichen.
|
|
|
|
Im Pruefstand wird derselbe Dialog gebaut und bedient, aber nicht
|
|
gezeigt - und sein grab_set faellt weg. Sonst haelt jeder Dialog Maus
|
|
und Tastatur fest, waehrend nebenan jemand schreibt.
|
|
"""
|
|
d.configure(bg=self.col_bg)
|
|
if config.TESTLAUF:
|
|
# Wie das Hauptfenster: neben den Bildschirm, und ohne grab_set -
|
|
# ein Dialog, der Maus und Tastatur festhaelt, blockiert den
|
|
# Menschen, der nebenher schreibt.
|
|
d.geometry(f"+{d.winfo_screenwidth() + 20}+40")
|
|
d.grab_set = lambda *a, **k: None
|
|
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
|
|
d.after(20, lambda: self._dark_titlebar(d))
|
|
|
|
def _dark_titlebar(self, win):
|
|
"""Windows-Titelleiste des Fensters dunkel machen (nur im Dark-Mode)."""
|
|
if not (sys.platform.startswith("win") and getattr(self, "_dark_mode", False)):
|
|
return
|
|
try:
|
|
import ctypes
|
|
win.update_idletasks()
|
|
hwnd = ctypes.windll.user32.GetParent(win.winfo_id())
|
|
val = ctypes.c_int(1)
|
|
for attr in (20, 19): # DWMWA_USE_IMMERSIVE_DARK_MODE (Win11=20, frühes Win10=19)
|
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
|
hwnd, attr, ctypes.byref(val), ctypes.sizeof(val))
|
|
# Frame neu zeichnen lassen (ohne Flackern/Fokusverlust)
|
|
SWP = 0x0001 | 0x0002 | 0x0004 | 0x0020 # NOSIZE|NOMOVE|NOZORDER|FRAMECHANGED
|
|
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP)
|
|
except Exception as e: # noqa: BLE001
|
|
print("[theme] Titelleiste:", e)
|