TIP 749: Dark toplevel decoration on MS-Windows

Login
Bounty program for improvements to Tcl and certain Tcl packages.
    Author:         harald.oehlmann@elmicron.de
    State:          Withdrawn
    Type:           Project
    Created:        02-Apr-2026
    Tcl-Version:    9.1
    Tk-Branch:      tip-749-mswin-dark-toplevel
    Keywords:       Tk, MS-WIndows, toplevel
    Vote:           Pending
    Vote-Summary:   
    Votes-For:      
    Votes-Against:  
    Votes-Present:  

Abstract

This TIP proposes adding a switch to change a toplevels decoration to dark mode on MS-Windows.

This TIP was integrated in TIP 750 and withdrawn.

Rationale

When the windows content is dark, a dark toplevel decoration is beautiful.

To see an example, do the following on MS-Windows 11: Open the start menu and type "cmd". The cmd.exe shell window is dark (independent on the theme) and the window decoration is dark.

Compatibility to the MacOS implementation

TIP655 (implemented) and TIP750 (proposed) proposes the following commands:

On MacOS:

wm attributes $toplevel attributes -appearance auto|dak|light

The values may be abbreviated. See the discussion part below. It is announced to change this to:

wm attributes $toplevel attributes -appearance auto|light|dark

On MacOS and MS-Win:

winfo isdark $toplevel

Which returns a boolean to indicate dark mode.

Specification

The new attribute "-appearance" is added to "wm attribute" for the MS-Windows platform. The new attribute manages the dark mode of the Window decoration.

The permitted values:

  • "light": light Window decoration. This is the default value.
  • "dark": dark Window decoration.

Set mode

The appearance may be set by:

wm attributes $toplevel -appearance light|dark

Abbreviations for light or dark are allowed.

The command may fail if there are issues with the given toplevel window status.

Query mode

The appearance mode may be queried by one of the two following commands:

wm attributes $toplevel -appearance
wm attributes $toplevel

Reference Implementation

The implementation is in Tk branch tip-749-mswin-dark-toplevel.

The current implementation requires an "update idletasks" between toplevel creation and appearance setting:

toplevel .t
update idletasks
wm attributes .t -appearance dark

Any solution to avoid this is appreciated.

Credits

This proposal is by Alexandru. The solution sketch is by Emiliano. The MacOS version is authored by Marc. Details are in Tk ticket a2125a1b.

And the whole Tk community has to help, as the coordinator is again approaching something he has no idea about.

Backwards Compatibility

The shortcut for

wm attribute $t -a $color

for -alpha does not work any more.

Discussion

Black toplevel menu

The first level of an eventual menu does not change the color with the -darkmode switch. The issue is, that an eventual color "-bg black" is not honored on MS-Windows on the top menu. Tk Bug 11bd4a03 addresses this point.

Compatibility to MacOS

Emiliano

On windows, win32 apps get light window decorations by default. To check whether dark mode is active for the more modern APIs, this code can be used:

package require registry
proc isdarkmodeactive {} {
    set path [join {
        HKEY_CURRENT_USER
        SOFTWARE
        Microsoft
        Windows
        CurrentVersion
        Themes
        Personalize
    } \\]
    if {[catch {registry get $path AppsUseLightTheme} value]} {
        # key not found
        return 0
    }
    return [expr {$value == 0}]
}

Support more colors (Mason McParlane)

If anyone is interested in a script solution to dark mode I added it to TkCon http://github.com/bohagan1/TkCon/blob/a879ff32af1b836112894ba638e90d66802a9ef7/tkcon.tcl#L418. See below for code snippets. This detection solution works for macOS, Linux, and Windows and a routine for Windows that uses CFFI https://github.com/apnadkarni/tcl-cffi allows users to set the color. Having a single dark-mode toggle doesn't seem to be enough for Windows where controlling the specific color can really enhance the feel of apps.

## ::tkcon::DarkModeSetting - detects dark mode
# Outputs: true if dark mode is enabled, otherwise false
##
proc ::tkcon::DarkModeSetting {} {
     variable PRIV
     set darkmode 0
     catch {
         if {$PRIV(WIN32)} {
             package require registry
             set keypath {HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize}
             set darkmode [expr {[registry get $keypath AppsUseLightTheme] == 0}]

         } elseif {$PRIV(AQUA)} {
             set istyle [exec defaults read -g AppleInterfaceStyle]
             set darkmode [expr {$istyle eq "Dark"}]

         } else {
             set colorscheme_query {qdbus org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop
                 org.freedesktop.portal.Settings.Read "org.freedesktop.appearance" "color-scheme"
             }
             set darkmode [expr {1 == [exec {*}$colorscheme_query]}]
         }

     }
     return $darkmode
}

proc ::tkcon::HexToBGR {color} {
     if {[scan $color "#%2x%2x%2x" r g b] != 3} {
         return -code error "Invalid hex color format: $color"
     }
     return [expr {($b << 16) | ($g << 8) | $r}]
}

proc ::tkcon::SetWindowColor {window color} {
     variable PRIV
     if {!$PRIV(WIN32) || [catch {package require cffi}]} {
         return
     }

     cffi::alias load win32
     cffi::Wrapper create dwmapi [file join $::env(windir) system32 dwmapi.dll]
     cffi::Wrapper create user32 [file join $::env(windir) system32 user32.dll]

     cffi::alias define HRESULT {long nonnegative winerror}
     dwmapi stdcall DwmSetWindowAttribute HRESULT {
         hwnd pointer.HWND dwAttribute DWORD pvAttribute pointer cbAttribute DWORD
     }

     user32 stdcall GetParent pointer.HWND {
         hwnd pointer.HWND
     }

     proc ::tkcon::SetWindowColor {window color} {
         set DWMWA_CAPTION_COLOR 35
         set hwndptr [cffi::pointer make [winfo id $window] HWND]
         cffi::pointer safe $hwndptr
         set parentptr [GetParent $hwndptr]

         set colorptr [cffi::arena pushframe DWORD]
         cffi::memory set $colorptr DWORD [HexToBGR $color]

         set size [cffi::type size DWORD]
         DwmSetWindowAttribute $parentptr $DWMWA_CAPTION_COLOR $colorptr $size

         cffi::arena popframe
         cffi::pointer dispose $hwndptr
         cffi::pointer dispose $parentptr
     }

     tailcall ::tkcon::SetWindowColor $window $color
}

Support auto mode

An auto-Mode with the same syntax as MacOS may be added as follows:

wm attribute . -appearance auto

Will check the registry for the current mode and sets the mode

Here is the TCL equivalent:

proc isdarkmodeactive {} {
    set path [join {
        HKEY_CURRENT_USER
        SOFTWARE
        Microsoft
        Windows
        CurrentVersion
        Themes
        Personalize
    } \\]
    if {[catch {registry get $path AppsUseLightTheme} value]} {
        # key not found
        return 0
    }
    return [expr {$value == 0}]
}

Emiliano: According to what I've read online (docs are lacking) there is a Windows message sent to toplevels when the theme changes; it seem to be WM_SETTINGCHANGE, which Tk handles in the function WmProc(), in win/tkWinWM.c. While is not entirely clear what the message payload is when the dark mode is set, a bit of experimentation could lead to a solution.

Kevin: On initialization on Win10 or Win11, you can determine whether the system is in dark mode by querying the registry key HKEY_CURRENT_USERSoftwareMicrosoftWindowsCurrentVersionThemesPersonalizeAppsUseLightTheme. If it does not exist or is nonzero, the light theme should be used.

Of note, if SystemParametersInfo(SPI_GETHIGHCONTRAST, ...) indicates that the high contrast feature is on, then the high contrast request takes precedence over the Windows theme.

The app is responsible for making its title bar follow the theme. There is a function in , called DwmSetWindowAttribute, to carry this out. See nf-dwmapi-dwmsetwindowattribute for details (Making the app responsible means that light-mode-only legacy apps get compatible light-mode title bars).

The system colors whose names are found in the 'Windows' section of colors should follow the OS theme. I don't recall without sourcediving whether Tk uses Windows control messages (e.g., WM_CTLCOLORSTATIC, WM_CTLCOLORDLG, etc.) to manage these, or if Tk uses its own messaging (I'm not sure all of this can really handle theme changes without first making Tk support named colors, a feature that I've wanted for a long time but never had time to explore implementing).

Emiliano is correct that the basic notification for changes to the theme is WM_SETTINGCHANGE. Typically, the lParam is a string that is the name of the leaf node in the registry (so "AppsUseLightTheme" in this case). But apps that send WM_SETTINGCHANGE often get it wrong, so apps that handle WM_SETTINGCHANGE typically check and reload all the Windows system parameter settings that the app uses. (I've also seen that the WM_SETTINGCHANGE for the theme will set lParam to "ImmersiveColorSet".)

There's also a message, WM_THEMECHANGED. I'm not sure of the context in which it appears. When it arrives, the code should use OpenThemeData() to get the current theme, and then GetThemeColor to retrieve the colors. (I think this is where Tk's 'system*' colors come from on Windows, but I'm far from positive, and again, no time to sourcedive.)

Another possible approach to getting notified of the 'light/dark' change is to call RegNotifyChangeKeyValue with fAsynchronous set and an hEvent provided. Changes will be reported through the message pump, with the given hEvent.

Pre-Windows-10, only the light theme exists (although high contrast still needs to be handled, that goes all the way back to WinXP).

Copyright

This document has been placed in the public domain.

History