diff --git a/README.md b/README.md index 9db85ed..9a3ad71 100644 --- a/README.md +++ b/README.md @@ -25,33 +25,43 @@ python -m pip install win32mica ## Usage: ```python -##################################################################### -# # -# Those examples are oversimplified, please see the examples folder # -# for detailed usage with each UI library. # -# # -##################################################################### - -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 - -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 - -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 +###################################################################### +# # +# Those examples are oversimplified, please see the examples/ folder # +# for detailed usage with each UI library. # +# # +###################################################################### + +hwnd = window.winId().__int__() # Get the hWnd of your window + +from win32mica import ApplyMica, MicaTheme, MicaStyle + +mode = MicaTheme.DARK # Dark mode mica effect +mode = MicaTheme.LIGHT # Light mode mica effect +mode = MicaTheme.AUTO # Apply system theme, and change it if system theme changes + +style = MicaStyle.DEFAULT # Default backdrop effect +style = MicaStyle.ALT # Alt backdrop effect + +def callbackFunction(NewTheme): + if newTheme == MicaTheme.DARK: + print("Theme has changed to dark!") + else: + print("Theme has changed to light!") + +win32mica.ApplyMica(HWND=hwnd, Theme=mode, Style=style, OnThemeChange=callbackFunction) + +# Parameters +# ---------- +# HWND : int +# The handle to the window on which the effect has to be applied +# Theme : MicaTheme, int +# The theme of the backdrop effect: MicaTheme.DARK, MicaTheme.LIGHT, MicaTheme.AUTO +# Style : MicaStyle, int +# The style of the mica backdrop effect: MicaStyle.DEFAULT, MicaStyle.ALT +# OnThemeChange : function +# A callback function that receives one parameter to call when the system theme changes (will only work if Theme is set to MicaTheme.AUTO) +# The passed parameter will be either MicaTheme.DARK or MicaTheme.LIGHT, corresponding to the new system theme ``` diff --git a/examples/Pyside2.py b/examples/Pyside2.py deleted file mode 100644 index 4d61cfa..0000000 --- a/examples/Pyside2.py +++ /dev/null @@ -1,25 +0,0 @@ -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/examples/Pyside6.py b/examples/Pyside6.py index d5673f0..6a62876 100644 --- a/examples/Pyside6.py +++ b/examples/Pyside6.py @@ -1,25 +1,68 @@ import ctypes -try: - import win32mica as mc - from PySide6 import QtWidgets, QtCore -except ImportError: - import os - os.system("pip install win32mica PySide2") +from win32mica import ApplyMica, MicaTheme, MicaStyle +from PySide6 import QtWidgets, QtCore 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) +ApplyMica(app.winId(), MicaTheme.DARK, MicaStyle.DEFAULT) 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) +ApplyMica(app2.winId(), MicaTheme.LIGHT, MicaStyle.DEFAULT) app2.show() +app3 = QtWidgets.QMainWindow() +app3.setAttribute(QtCore.Qt.WA_TranslucentBackground) +app3.setWindowTitle("Qt Dark Alt") +app3.setGeometry(100, 330, 300, 200) +ApplyMica(app3.winId(), MicaTheme.DARK, MicaStyle.ALT) +app3.show() + +app4 = QtWidgets.QMainWindow() +app4.setAttribute(QtCore.Qt.WA_TranslucentBackground) +app4.setWindowTitle("Qt Light Alt") +app4.setGeometry(400, 330, 300, 200) +ApplyMica(app4.winId(), MicaTheme.LIGHT, MicaStyle.ALT) +app4.show() + +app5 = QtWidgets.QMainWindow() +app5.setAttribute(QtCore.Qt.WA_TranslucentBackground) +app5.setWindowTitle("Qt Auto") +app5.setGeometry(700, 100, 300, 200) + +label = QtWidgets.QLabel("Change the system theme\nfrom the settings!") +def ApplyStyleSheet(theme): + if theme == MicaTheme.DARK: + label.setStyleSheet("color: white") + else: + label.setStyleSheet("color: black") + +ApplyMica(app5.winId(), MicaTheme.AUTO, MicaStyle.DEFAULT, OnThemeChange=ApplyStyleSheet) +app5.show() +app5.setCentralWidget(label) + +app6 = QtWidgets.QMainWindow() +app6.setAttribute(QtCore.Qt.WA_TranslucentBackground) +app6.setWindowTitle("Qt Auto Alt") +app6.setGeometry(700, 330, 300, 200) + +label2 = QtWidgets.QLabel("Change the system theme\nfrom the settings!") +def ApplyStyleSheet(theme): + if theme == MicaTheme.DARK: + label2.setStyleSheet("color: white") + else: + label2.setStyleSheet("color: black") + +ApplyMica(app6.winId(), MicaTheme.AUTO, MicaStyle.ALT, OnThemeChange=ApplyStyleSheet) +app6.show() +app6.setCentralWidget(label2) + + root.exec_() \ No newline at end of file diff --git a/examples/Tkinter.py b/examples/Tkinter.py deleted file mode 100644 index 02a7ba9..0000000 --- a/examples/Tkinter.py +++ /dev/null @@ -1,29 +0,0 @@ - -import tkinter as tk -from ctypes import windll -try: - import win32mica as mc -except ImportError: - import os - os.system("pip install win32mica") - - -app=tk.Tk() -app.title("Tk Dark") -app.configure(bg="#000000") # Please use BLACK as background color, otherwhise render issues might appear -app.wm_attributes("-transparent", "#000000") -app.update() -HWND=windll.user32.GetParent(app.winfo_id()) -mc.ApplyMica(HWND, ColorMode=mc.MICAMODE.DARK) - - -app2=tk.Tk() -app2.title("Tk Light") -app2.configure(bg="#ffffff") # Please use WHITE as background color, otherwhise render issues might appear -app2.wm_attributes("-transparent", "#ffffff") -app2.update() -HWND=windll.user32.GetParent(app2.winfo_id()) -mc.ApplyMica(HWND, ColorMode=mc.MICAMODE.LIGHT) - - -app.mainloop() diff --git a/img/demo.mp4 b/img/demo.mp4 new file mode 100644 index 0000000..fa13b65 Binary files /dev/null and b/img/demo.mp4 differ diff --git a/setup.py b/setup.py index 4570fa7..7b454f3 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="win32mica", - version="2.0", + version="3.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 f7dfd6a..86dd3fc 100644 --- a/src/win32mica/__init__.py +++ b/src/win32mica/__init__.py @@ -1,57 +1,81 @@ -import ctypes, sys, threading, time, winreg +import ctypes +import sys +import threading +import time +import winreg -class MICAMODE(): - DARK = 1 +class MicaTheme: LIGHT = 0 - AUTO = 2 - -class MicaMode(): DARK = 1 - LIGHT = 0 AUTO = 2 + +class MicaStyle: + DEFAULT = 3 + ALT = 4 - -debugging = False - -def readRegedit(aKey, sKey, default, storage=winreg.HKEY_CURRENT_USER): +def __read_registry(aKey, sKey, default, storage=winreg.HKEY_CURRENT_USER): registry = winreg.ConnectRegistry(None, storage) reg_keypath = aKey try: reg_key = winreg.OpenKey(registry, reg_keypath) - except FileNotFoundError as e: + except FileNotFoundError: return default except Exception as e: print(e) return default - for i in range(1024): try: value_name, value, _ = winreg.EnumValue(reg_key, i) if value_name == sKey: return value - except OSError as e: + except OSError: return default except Exception as e: print(e) return default -def nullFunction(): +def __null_function(NewTheme) -> None: 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 - onThemeChange -- a function to call when the system theme changes. Will be called only if ColorMode is set to MicaMode.AUTO + +def ApplyMica( + HWND: int, + Theme: bool = MicaTheme.LIGHT, + Style: bool = MicaStyle.DEFAULT, + OnThemeChange = __null_function, + ) -> int: + """Applies the mica backdrop effect on a specific hWnd + + Parameters + ---------- + HWND : int + The handle to the window on which the effect has to be applied + Theme : MicaTheme, int + The theme of the backdrop effect: MicaTheme.DARK, MicaTheme.LIGHT, MicaTheme.AUTO + Style : MicaStyle, int + The style of the mica backdrop effect: MicaStyle.DEFAULT, MicaStyle.ALT + OnThemeChange : function + A callback function that receives one parameter to call when the system theme changes (will only work if Theme is set to MicaTheme.AUTO) + The passed parameter will be either MicaTheme.DARK or MicaTheme.LIGHT, corresponding to the new system theme + + Returns + ------- + int + the integer result of the win32 api call to apply the mica backdrop effect. This value will equal to 0x32 if the system is not compatible with the mica backdrop """ + + if HWND == 0: + raise ValueError("The parameter HWND cannot be zero") + if Theme not in (MicaTheme.DARK, MicaTheme.LIGHT, MicaTheme.AUTO): + raise ValueError("The parameter ColorMode has an invalid value") + if Style not in (MicaStyle.DEFAULT, MicaStyle.ALT): + raise ValueError("The parameter Style has an invalid value") + try: try: HWND = int(HWND) except ValueError: HWND = int(str(HWND), 16) - + user32 = ctypes.windll.user32 dwm = ctypes.windll.dwmapi @@ -60,76 +84,98 @@ class AccentPolicy(ctypes.Structure): ("AccentState", ctypes.c_uint), ("AccentFlags", ctypes.c_uint), ("GradientColor", ctypes.c_uint), - ("AnimationId", 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) + ("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+) + _fields_ = [ + ("cxLeftWidth", ctypes.c_int), + ("cxRightWidth", ctypes.c_int), + ("cyTopHeight", ctypes.c_int), + ("cyBottomHeight", ctypes.c_int), + ] + + DWM_UNDOCUMENTED_MICA_ENTRY = 1029 + DWM_UNDOCUMENTED_MICA_VALUE = 0x01 if Style == MicaStyle.DEFAULT else 0x04 + + DWM_DOCUMENTED_MICA_ENTRY = 38 + DWM_DOCUMENTED_MICA_VALUE = 0x02 if Style == MicaStyle.DEFAULT else 0x04 DWMW_USE_IMMERSIVE_DARK_MODE = 20 - SetWindowCompositionAttribute = user32.SetWindowCompositionAttribute - DwmSetWindowAttribute = dwm.DwmSetWindowAttribute + DwmSetWindowAttribute = dwm.DwmSetWindowAttribute DwmExtendFrameIntoClientArea = dwm.DwmExtendFrameIntoClientArea - - MODE = 0x00 - def setMode(): - nonlocal MODE - OldMode = -1 + THEME = 0x00 + + def __apply_theme(): + nonlocal THEME + OldTheme = -1 while True: - 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 + CurrentTheme = __read_registry( + r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + "AppsUseLightTheme", + 0, + ) + if OldTheme != CurrentTheme: + OldTheme = CurrentTheme + if THEME == 0x01: + ThemeToSet = 0x01 + elif THEME == 0x00: + ThemeToSet = 0x00 else: - ModeToSet = 0x00 if CurrentMode != 0 else 0x01 + ThemeToSet = 0x00 if CurrentTheme != 0 else 0x01 try: - onThemeChange() + OnThemeChange(ThemeToSet) except: pass - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) + DwmSetWindowAttribute( + HWND, + DWMW_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(ctypes.c_int(ThemeToSet)), + ctypes.sizeof(ctypes.c_int), + ) time.sleep(0.5) - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) + DwmSetWindowAttribute( + HWND, + DWMW_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(ctypes.c_int(ThemeToSet)), + ctypes.sizeof(ctypes.c_int), + ) time.sleep(0.5) - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) + DwmSetWindowAttribute( + HWND, + DWMW_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(ctypes.c_int(ThemeToSet)), + ctypes.sizeof(ctypes.c_int), + ) time.sleep(0.5) - DwmSetWindowAttribute(HWND, DWMW_USE_IMMERSIVE_DARK_MODE, ctypes.byref(ctypes.c_int(ModeToSet)), ctypes.sizeof(ctypes.c_int)) + DwmSetWindowAttribute( + HWND, + DWMW_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(ctypes.c_int(ThemeToSet)), + ctypes.sizeof(ctypes.c_int), + ) time.sleep(0.5) time.sleep(0.1) + + if Theme == MicaTheme.LIGHT: + THEME = 0x00 + elif Theme == MicaTheme.DARK: + THEME = 0x01 + elif Theme == MicaTheme.AUTO: + THEME = 0x02 - if ColorMode == MicaMode.DARK: - MODE = 0x01 - elif ColorMode == MicaMode.LIGHT: - MODE = 0x00 - else: # ColorMode == MicaMode.AUTO - MODE = 0x02 - - threading.Thread(target=setMode, daemon=True, name="win32mica: theme thread").start() + threading.Thread(target=__apply_theme, daemon=True, name="Win32mica helper").start() if sys.platform == "win32" and sys.getwindowsversion().build >= 22000: - Acp = AccentPolicy() Acp.GradientColor = int("00cccccc", base=16) Acp.AccentState = 5 @@ -141,7 +187,7 @@ def setMode(): 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) @@ -149,13 +195,23 @@ def setMode(): 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)) + return DwmSetWindowAttribute( + HWND, + DWM_UNDOCUMENTED_MICA_ENTRY, + ctypes.byref(ctypes.c_int(DWM_UNDOCUMENTED_MICA_VALUE)), + ctypes.sizeof(ctypes.c_int), + ) else: - return DwmSetWindowAttribute(HWND, DWM_DOCUMENTED_MICA_ENTRY, ctypes.byref(ctypes.c_int(DWM_DOCUMENTED_MICA_VALUE)), ctypes.sizeof(ctypes.c_int)) + 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") + 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)) - - + print("Win32mica: " + str(type(e)) + ": " + str(e))