Implement native borderless window integration

This commit is contained in:
lm 2025-10-19 18:26:36 +02:00
parent f2db62036b
commit 01a9b2707c
2 changed files with 54 additions and 17 deletions

View File

@ -2,7 +2,11 @@
from __future__ import annotations
import ctypes
import platform
import tkinter as tk
from ctypes import wintypes
from importlib import resources
from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
from .i18n import I18nMixin
@ -83,17 +87,15 @@ class ICRAApp(
self._is_maximized = True
self.root.geometry(f"{screen_width}x{screen_height}+0+0")
self.root.configure(bg="#f2f2f7")
self._window_icon_ref = None
self._apply_window_icon()
self._init_window_chrome()
def _ensure_taskbar_entry(self) -> None:
"""Force the borderless window to show up in the Windows taskbar."""
try:
import platform
if platform.system() != "Windows":
return
import ctypes
hwnd = self.root.winfo_id()
if not hwnd:
self.root.after(50, self._ensure_taskbar_entry)
@ -129,15 +131,25 @@ class ICRAApp(
except Exception:
pass
def _apply_window_icon(self) -> None:
try:
icon_resource = resources.files("app.assets").joinpath("logo.png")
with resources.as_file(icon_resource) as icon_path:
icon = tk.PhotoImage(file=str(icon_path))
self.root.iconphoto(False, icon)
self._window_icon_ref = icon
except Exception:
self._window_icon_ref = None
def _init_window_chrome(self) -> None:
"""Configure a borderless window while retaining a taskbar entry."""
try:
import platform
system = platform.system()
if system == "Windows":
self.root.after(0, self._apply_windows_borderless_style)
self.root.after(0, self._ensure_taskbar_entry)
self.root.bind("<Map>", lambda _e: self._apply_windows_borderless_style(), add="+")
self.root.bind("<Map>", lambda _e: self._ensure_taskbar_entry(), add="+")
else:
self.root.overrideredirect(True)
except Exception:
@ -145,9 +157,6 @@ class ICRAApp(
def _apply_windows_borderless_style(self) -> None:
try:
import platform
import ctypes
if platform.system() != "Windows":
return
@ -160,20 +169,24 @@ class ICRAApp(
GWL_STYLE = -16
WS_CAPTION = 0x00C00000
WS_THICKFRAME = 0x00040000
WS_MAXIMIZEBOX = 0x00010000
WS_MINIMIZEBOX = 0x00020000
WS_SYSMENU = 0x00080000
WS_BORDER = 0x00800000
SWP_NOSIZE = 0x0001
SWP_NOMOVE = 0x0002
SWP_NOZORDER = 0x0004
SWP_FRAMECHANGED = 0x0020
style = user32.GetWindowLongW(hwnd, GWL_STYLE)
new_style = style & ~(
WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU
)
set_window_long = getattr(user32, "SetWindowLongPtrW", user32.SetWindowLongW)
get_window_long = getattr(user32, "GetWindowLongPtrW", user32.GetWindowLongW)
ptr_type = ctypes.c_longlong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_long
get_window_long.restype = ptr_type # type: ignore[attr-defined]
get_window_long.argtypes = [wintypes.HWND, ctypes.c_int] # type: ignore[attr-defined]
set_window_long.restype = ptr_type # type: ignore[attr-defined]
set_window_long.argtypes = [wintypes.HWND, ctypes.c_int, ptr_type] # type: ignore[attr-defined]
style = get_window_long(hwnd, GWL_STYLE)
new_style = style & ~(WS_CAPTION | WS_THICKFRAME | WS_BORDER)
if new_style != style:
user32.SetWindowLongW(hwnd, GWL_STYLE, new_style)
set_window_long(hwnd, GWL_STYLE, ptr_type(new_style))
user32.SetWindowPos(
hwnd,
0,
@ -183,6 +196,18 @@ class ICRAApp(
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED,
)
try:
dwmapi = ctypes.windll.dwmapi # type: ignore[attr-defined]
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
value = ctypes.c_int(1)
dwmapi.DwmSetWindowAttribute(
hwnd,
ctypes.c_uint(DWMWA_USE_IMMERSIVE_DARK_MODE),
ctypes.byref(value),
ctypes.sizeof(value),
)
except Exception:
pass
except Exception:
pass

View File

@ -446,6 +446,11 @@ class UIBuilderMixin:
max_btn.bind("<Leave>", lambda _e: max_btn.configure(bg=bar_bg))
self._max_button = max_btn
min_btn = tk.Button(title_bar, text="", command=self._minimize_window, **btn_kwargs)
min_btn.pack(side=tk.RIGHT, padx=0, pady=4)
min_btn.bind("<Enter>", lambda _e: min_btn.configure(bg="#2c2c32"))
min_btn.bind("<Leave>", lambda _e: min_btn.configure(bg=bar_bg))
for widget in (title_bar, title_label):
widget.bind("<ButtonPress-1>", self._start_window_drag)
widget.bind("<B1-Motion>", self._perform_window_drag)
@ -558,6 +563,13 @@ class UIBuilderMixin:
else:
self._restore_window()
def _minimize_window(self) -> None:
try:
self._remember_window_geometry()
self.root.iconify()
except Exception:
pass
def _update_maximize_button(self) -> None:
button = getattr(self, "_max_button", None)
if button is None: