Tk & Ttk Integration

Tk and Ttk (tile) use different methods for theming. While Tk uses the resource option database to set resources like colors and fonts for each widget class, Ttk uses styles. Tk can't switch theme on the fly while Ttk does this. Since we still have to live with a few widgets from Tk in our Ttk adapted applications, these widgets must be integrated with Ttk's style engine. I've had several attempts to do this but the one I present was the only robust I could find.

In this example I will only treat the Menu class widget. This is not possible to theme on Mac OS X, and on Windows the menu bar isn't themed either. The idea is to associate a style (ttk::style depending on version) with a Tk class of widget, like:

 style configure Text -background white

or

 style map Menu \
     -background {active SystemHighlight} \
     -foreground {active SystemHighlightText disabled SystemGrayText}

Surprisingly, this works. But this is, of course, only used as a place holder for the actual options since it doesn't affect any widgets directly. Since styles are only defined within a theme, this has to be done for each particular theme.

Each newly created Tk widget gets its values from the option database, so this must also be set for each selected theme. Secondly, to change Tk widget options on the fly when a new theme has been picked, we must also bind to <<ThemeChanged>> events for each widget class, and manually configure each widget of this class.

The example below show all this for the Menu class. For a complete treatment, see my Coccinella code: [L1 ]


    namespace eval ttk {

        foreach name [ttk::availableThemes] {

            # @@@ We could be more economical here and load theme only when needed.
            if {[catch {package require ttk::theme::$name}]} {
                continue
            }

            # Set only the switches that are not in [style configure .]
            # or [style map .].
            
            switch $name {
                winnative {
                    style theme settings $name {
                        style map Menu \
                          -background {active SystemHighlight} \
                          -foreground {active SystemHighlightText disabled SystemGrayText}
                    }
                }
                xpnative {
                    style theme settings $name {
                        style map Menu \
                          -background {active SystemHighlight} \
                          -foreground {active SystemHighlightText disabled SystemGrayText}
                    }
                }
            }
        }
    }

    namespace eval ::ttkutils {
        
        # There are two things that need to be set for each class of widget:
        #   1) existing widgets need to be configured
        #   2) the resources must be set for new widgets
        
        if {[lsearch [bindtags .] ThemeChanged] < 0} {
            bindtags . [linsert [bindtags .] 1 ThemeChanged]
        }
        bind ThemeChanged <<ThemeChanged>> {ttkutils::ThemeChanged }
        bind Menu         <<ThemeChanged>> {ttkutils::MenuThemeChanged %W }    
        
    }

    proc ttkutils::ThemeChanged {} {
        
        # We configure the resource database here as well since it saves code.
        # Seems X11 has some system option db that must be overridden.
        if {[tk windowingsystem] eq "x11"} {
            set priority 60
        } else {
            set priority startupFile
        }
      
        # @@@ I could think of an alternative here:
        # style theme settings default {
        #    array set style [style configure .]
        #    array set map   [style map .]
        # }
        # etc. and then cache all in style(name) and map(name).

        array set style [list -foreground black]
        array set style [style configure .]
        array set map   [style map .]

        array set menuMap [style map .]
        array set menuMap [style map Menu]

        if {[info exists style(-background)]} {
            set color $style(-background)
            option add *Menu.background             $color $priority
            option add *Menu.activeBackground       $color $priority
            
            if {[info exists menuMap(-background)]} {
                foreach {state col} $menuMap(-background) {
                    if {[lsearch $state active] >= 0} {
                        option add *Menu.activeBackground $col $priority
                        break
                    }
                }
            }
        }
        if {[info exists style(-foreground)]} {
            set color $style(-foreground)
            option add *Menu.foreground             $color $priority
            option add *Menu.activeForeground       $color $priority
            option add *Menu.disabledForeground     $color $priority

            if {[info exists menuMap(-foreground)]} {
                foreach {state col} $menuMap(-foreground) {
                    if {[lsearch $state active] >= 0} {
                        option add *Menu.activeForeground $col $priority
                    }
                    if {[lsearch $state disabled] >= 0} {
                        option add *Menu.disabledForeground $col $priority
                    }
                }
            }
        }
    }

    proc ttkutils::MenuThemeChanged {win} {

        if {[winfo class $win] ne "Menu"} {
            return
        }
            
        # Some themes miss this one.
        array set style [list -foreground black]
        array set style [style configure .]    
        array set style [style configure Menu]    
        array set map   [style map .]
        array set map   [style map Menu]
        
        if {[info exists style(-background)]} {
            set color $style(-background)
            $win configure -background $color
            $win configure -activebackground $color
            if {[info exists map(-background)]} {
                foreach {state col} $map(-background) {
                    if {[lsearch $state active] >= 0} {
                        $win configure -activebackground  $col
                        break
                    }
                }
            }
        }
        if {[info exists style(-foreground)]} {
            set color $style(-foreground)
            $win configure -foreground $color
            $win configure -activeforeground $color
            $win configure -disabledforeground $color
            if {[info exists map(-foreground)]} {
                foreach {state col} $map(-foreground) {
                    if {[lsearch $state active] >= 0} {
                        $win configure -activeforeground  $col
                    }
                    if {[lsearch $state disabled] >= 0} {
                        $win configure -disabledforeground  $col
                    }
                }
            }
        }
    }