From 3be9d0910f8d551b0af8aa4f072a964cdcf4f2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Climent?= Date: Tue, 1 Aug 2023 11:24:30 +0200 Subject: [PATCH] version 2.0: ad auto dark mode and fix crashes (fix #17) --- README.md | 35 +++---- examples/Pyside2.py | 25 +++++ setup.py | 2 +- src/win32mica/__init__.py | 215 ++++++++++++++++++++------------------ 4 files changed, 151 insertions(+), 126 deletions(-) create mode 100644 examples/Pyside2.py diff --git a/README.md b/README.md index fd87944..9db85ed 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ python -m pip install win32mica ## Requirements: - Windows 11 - A window set to not have a transparent background and to have extended composition enabled* (It might work with other settings, but nothing is guaranteed.) - - The HWND (identifier) of that window. More info: [what is a hwnd?](https://stackoverflow.com/questions/1635645/what-is-hwnd-in-vc) + - The hWnd (identifier) of that window. More info: [what is a hWnd?](https://stackoverflow.com/questions/1635645/what-is-hwnd-in-vc) - OPTIONAL: The window must have semi-transparent widgets/controls in order to recreate the transparency effect on the controls. - OPTIONAL: Know if Windows has dark or light mode enabled. This can be checked with the [`darkdetect` module](https://pypi.org/project/darkdetect/) @@ -36,19 +36,23 @@ hwnd = qtwindow.winId().__int__() # On a PyQt/PySide window hwnd = tkwindow.frame() # On a tkinter window # You'll need to adjust this to your program -from win32mica import MICAMODE, ApplyMica +from win32mica import MicaMode, ApplyMica -mode = MICAMODE.DARK # Dark mode mica effect -mode = MICAMODE.LIGHT # Light mode mica effect +mode = MicaMode.DARK # Dark mode mica effect +mode = MicaMode.LIGHT # Light mode mica effect +mode = MicaMode.AUTO # Apply system theme, and change it if system theme changes # Choose one of them following your app color scheme -import darkdetect # You can pass the darkdetect return value directly, since the ColorMode accepts bool values (True -> dark, False -> light) -mode = darkdetect.isDark() +def callbackFunction(): + print("Theme has changed!") + +win32mica.ApplyMica(HWND=hwnd, ColorMode=mode, onThemeChange=callbackFunction) + +# Function arguments: +# HWND -- a handle to a window (it being an integer value) +# ColorMode -- MicaMode.DARK or MicaMode.LIGHT, depending on the preferred UI theme. A boolean value can also be passed, True meaning Dark and False meaning Light +# onThemeChange -- a function without arguments that will be called when the system theme changes. This parameter is effective only if the theme is set to MicaMode.AUTO -win32mica.ApplyMica(hwnd, mode) -# Will return 0x32 if the system does not support Mica textures (Windows 10 or less). Immersive dark mode will still be applied (if selected theme is MICAMODE.DARK) -# Will return 0x00 if mica is applied successfully -# If DwmSetWindowAttribute fails, the output code will be returned ``` You can check out the [examples folder](https://github.com/martinet101/win32mica/tree/main/examples) for detailed use in Tk and PySide/PyQt. @@ -60,14 +64,3 @@ You can check out the [examples folder](https://github.com/martinet101/win32mica _Those are PySide2 windows with custom widgets._ - -## Troubleshooting: - -For more information about possible errors/mistakes, make sure to add the following before using win32mica: - - -```python -# Add these lines at the very start of your script -import win32mica -win32mica.debugging = True -``` diff --git a/examples/Pyside2.py b/examples/Pyside2.py new file mode 100644 index 0000000..4d61cfa --- /dev/null +++ b/examples/Pyside2.py @@ -0,0 +1,25 @@ +import ctypes + +try: + import win32mica as mc + from PySide2 import QtWidgets, QtCore +except ImportError: + import os + os.system("pip install win32mica PySide2") + +root = QtWidgets.QApplication() +app = QtWidgets.QMainWindow() +app.setAttribute(QtCore.Qt.WA_TranslucentBackground) +app.setWindowTitle("Qt Dark") +app.setGeometry(100, 100, 300, 200) +mc.ApplyMica(app.winId(), mc.MICAMODE.DARK) +app.show() + +app2 = QtWidgets.QMainWindow() +app2.setAttribute(QtCore.Qt.WA_TranslucentBackground) +app2.setWindowTitle("Qt Light") +app2.setGeometry(400, 100, 300, 200) +mc.ApplyMica(app2.winId(), mc.MICAMODE.LIGHT) +app2.show() + +root.exec_() \ No newline at end of file diff --git a/setup.py b/setup.py index d9df58f..4570fa7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="win32mica", - version="1.9", + version="2.0", author="Martí Climent", author_email="marticlilop@gmail.com", description="Apply mica background (if supported) and immersive dark mode to Windows 11 Win32 apps made with python, such as Tkinter or PyQt/PySide apps", diff --git a/src/win32mica/__init__.py b/src/win32mica/__init__.py index 102fc6f..f7dfd6a 100644 --- a/src/win32mica/__init__.py +++ b/src/win32mica/__init__.py @@ -1,8 +1,14 @@ import ctypes, sys, threading, time, winreg class MICAMODE(): - DARK = True - LIGHT = False + DARK = 1 + LIGHT = 0 + AUTO = 2 + +class MicaMode(): + DARK = 1 + LIGHT = 0 + AUTO = 2 debugging = False @@ -29,126 +35,127 @@ def readRegedit(aKey, sKey, default, storage=winreg.HKEY_CURRENT_USER): print(e) return default - -def ApplyMica(HWND: int, ColorMode: bool = MICAMODE.LIGHT, darkModeMode: int = 20) -> int: +def nullFunction(): + pass + +def ApplyMica(HWND: int, ColorMode: bool = MicaMode.LIGHT, onThemeChange = nullFunction) -> int: """Apply the new mica effect on a window making use of the hidden win32api and return an integer depending on the result of the operation Keyword arguments: HWND -- a handle to a window (it being an integer value) - ColorMode -- MICAMODE.DARK or MICAMODE.LIGHT, depending on the preferred UI theme. A boolean value can also be passed, True meaning Dark and False meaning Light + ColorMode -- MicaMode.DARK or MicaMode.LIGHT, depending on the preferred UI theme. A boolean value can also be passed, True meaning Dark and False meaning Light + onThemeChange -- a function to call when the system theme changes. Will be called only if ColorMode is set to MicaMode.AUTO """ try: - HWND = int(HWND) - except ValueError: - HWND = int(str(HWND), 16) - - user32 = ctypes.windll.user32 - dwm = ctypes.windll.dwmapi - - class AccentPolicy(ctypes.Structure): - _fields_ = [ - ("AccentState", ctypes.c_uint), - ("AccentFlags", ctypes.c_uint), - ("GradientColor", ctypes.c_uint), - ("AnimationId", ctypes.c_uint) - ] - - class WindowCompositionAttribute(ctypes.Structure): - _fields_ = [ - ("Attribute", ctypes.c_int), - ("Data", ctypes.POINTER(ctypes.c_int)), - ("SizeOfData", ctypes.c_size_t) - ] - - class _MARGINS(ctypes.Structure): - _fields_ = [("cxLeftWidth", ctypes.c_int), - ("cxRightWidth", ctypes.c_int), - ("cyTopHeight", ctypes.c_int), - ("cyBottomHeight", ctypes.c_int) - ] - - DWM_UNDOCUMENTED_MICA_ENTRY = 1029 # Undocumented MICA (Windows 11 22523-) - DWM_UNDOCUMENTED_MICA_VALUE = 0x01 # Undocumented MICA (Windows 11 22523-) - - DWM_DOCUMENTED_MICA_ENTRY = 38 # Documented MICA (Windows 11 22523+) - DWM_DOCUMENTED_MICA_VALUE = 0x02 # Documented MICA (Windows 11 22523+) - DWMW_USE_IMMERSIVE_DARK_MODE = 20 - + try: + HWND = int(HWND) + except ValueError: + HWND = int(str(HWND), 16) + + user32 = ctypes.windll.user32 + dwm = ctypes.windll.dwmapi + + class AccentPolicy(ctypes.Structure): + _fields_ = [ + ("AccentState", ctypes.c_uint), + ("AccentFlags", ctypes.c_uint), + ("GradientColor", ctypes.c_uint), + ("AnimationId", ctypes.c_uint) + ] + + class WindowCompositionAttribute(ctypes.Structure): + _fields_ = [ + ("Attribute", ctypes.c_int), + ("Data", ctypes.POINTER(ctypes.c_int)), + ("SizeOfData", ctypes.c_size_t) + ] + + class _MARGINS(ctypes.Structure): + _fields_ = [("cxLeftWidth", ctypes.c_int), + ("cxRightWidth", ctypes.c_int), + ("cyTopHeight", ctypes.c_int), + ("cyBottomHeight", ctypes.c_int) + ] + + DWM_UNDOCUMENTED_MICA_ENTRY = 1029 # Undocumented MICA (Windows 11 22523-) + DWM_UNDOCUMENTED_MICA_VALUE = 0x01 # Undocumented MICA (Windows 11 22523-) + + DWM_DOCUMENTED_MICA_ENTRY = 38 # Documented MICA (Windows 11 22523+) + DWM_DOCUMENTED_MICA_VALUE = 0x02 # Documented MICA (Windows 11 22523+) + DWMW_USE_IMMERSIVE_DARK_MODE = 20 + - SetWindowCompositionAttribute = user32.SetWindowCompositionAttribute - DwmSetWindowAttribute = dwm.DwmSetWindowAttribute - DwmExtendFrameIntoClientArea = dwm.DwmExtendFrameIntoClientArea + SetWindowCompositionAttribute = user32.SetWindowCompositionAttribute + DwmSetWindowAttribute = dwm.DwmSetWindowAttribute + DwmExtendFrameIntoClientArea = dwm.DwmExtendFrameIntoClientArea + + MODE = 0x00 - if ColorMode == MICAMODE.DARK: # Apply dark mode def setMode(): - oldMode = -1 + nonlocal MODE + OldMode = -1 while True: - mode = readRegedit(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 0) - if oldMode != mode: - oldMode = mode - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(0x01)), ctypes.sizeof(ctypes.c_int)) + CurrentMode = readRegedit(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 0) + if OldMode != CurrentMode: + + OldMode = CurrentMode + if MODE == 0x01: + ModeToSet = 0x01 + elif MODE == 0x00: + ModeToSet = 0x00 + else: + ModeToSet = 0x00 if CurrentMode != 0 else 0x01 + try: + onThemeChange() + except: + pass + DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) time.sleep(0.5) - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(0x01)), ctypes.sizeof(ctypes.c_int)) + DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) time.sleep(0.5) - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(0x01)), ctypes.sizeof(ctypes.c_int)) + DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) time.sleep(0.5) - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(0x01)), ctypes.sizeof(ctypes.c_int)) + DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) time.sleep(0.5) time.sleep(0.1) - - - - threading.Thread(target=setMode, daemon=True, name="win32mica: ensure dark mode").start() - else: # Apply light mode - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(0x00)), ctypes.sizeof(ctypes.c_int)) - - - if sys.platform == "win32" and sys.getwindowsversion().build >= 22000: - - Acp = AccentPolicy() - Acp.GradientColor = int("00cccccc", base=16) - Acp.AccentState = 5 - Acp.AccentPolicy = 19 - - Wca = WindowCompositionAttribute() - Wca.Attribute = 20 - Wca.SizeOfData = ctypes.sizeof(Acp) - Wca.Data = ctypes.cast(ctypes.pointer(Acp), ctypes.POINTER(ctypes.c_int)) - - Mrg = _MARGINS(-1, -1, -1, -1) + if ColorMode == MicaMode.DARK: + MODE = 0x01 + elif ColorMode == MicaMode.LIGHT: + MODE = 0x00 + else: # ColorMode == MicaMode.AUTO + MODE = 0x02 - o = DwmExtendFrameIntoClientArea(HWND, ctypes.byref(Mrg)) - if debugging: - if o != 0: - print("Win32mica: Failed to DwmExtendFrameIntoClientArea", hex(o+0xffffffff)) - else: - print("Win32mica: DwmExtendFrameIntoClientArea Ok") - o = SetWindowCompositionAttribute(HWND, Wca) - if debugging: - if o != 0: - print("Win32mica: Failed to SetWindowCompositionAttribute", o) + threading.Thread(target=setMode, daemon=True, name="win32mica: theme thread").start() + + if sys.platform == "win32" and sys.getwindowsversion().build >= 22000: + + Acp = AccentPolicy() + Acp.GradientColor = int("00cccccc", base=16) + Acp.AccentState = 5 + Acp.AccentPolicy = 19 + + Wca = WindowCompositionAttribute() + Wca.Attribute = 20 + Wca.SizeOfData = ctypes.sizeof(Acp) + Wca.Data = ctypes.cast(ctypes.pointer(Acp), ctypes.POINTER(ctypes.c_int)) + + Mrg = _MARGINS(-1, -1, -1, -1) + + o = DwmExtendFrameIntoClientArea(HWND, ctypes.byref(Mrg)) + try: + o = SetWindowCompositionAttribute(HWND, Wca) + except ctypes.ArgumentError: + pass + + if sys.getwindowsversion().build < 22523: + return DwmSetWindowAttribute(HWND, DWM_UNDOCUMENTED_MICA_ENTRY, ctypes.byref(ctypes.c_int(DWM_UNDOCUMENTED_MICA_VALUE)), ctypes.sizeof(ctypes.c_int)) else: - print("Win32mica: SetWindowCompositionAttribute Ok") - - if ColorMode == MICAMODE.DARK: - Wca.Attribute = 1 - o = SetWindowCompositionAttribute(HWND, Wca) - if debugging: - if o != 0: - print("Win32mica: Failed to SetWindowCompositionAttribute (dark mode)", o) - else: - print("Win32mica: SetWindowCompositionAttribute OK (dark mode)", o) + return DwmSetWindowAttribute(HWND, DWM_DOCUMENTED_MICA_ENTRY, ctypes.byref(ctypes.c_int(DWM_DOCUMENTED_MICA_VALUE)), ctypes.sizeof(ctypes.c_int)) else: - if debugging: - print("Win32mica: No SetWindowCompositionAttribute (light mode)") - - if sys.getwindowsversion().build < 22523: # If mica is not a public API - return DwmSetWindowAttribute(HWND, DWM_UNDOCUMENTED_MICA_ENTRY, ctypes.byref(ctypes.c_int(DWM_UNDOCUMENTED_MICA_VALUE)), ctypes.sizeof(ctypes.c_int)) - else: # If mica is present in the public API - return DwmSetWindowAttribute(HWND, DWM_DOCUMENTED_MICA_ENTRY, ctypes.byref(ctypes.c_int(DWM_DOCUMENTED_MICA_VALUE)), ctypes.sizeof(ctypes.c_int)) - else: - print(f"Win32Mica Error: {sys.platform} version {sys.getwindowsversion().build} is not supported") - return 0x32 + print(f"Win32Mica Error: {sys.platform} version {sys.getwindowsversion().build} is not supported") + return 0x32 + except Exception as e: + print("Win32mica: "+str(type(e))+": "+str(e))