Theming a python to tcl/tk (hence without tkinter) application

As a foreword, tkinter users should have a look at ttkbootstrap which is unique to tkinter and is an awesome flat theme for tk.

But, I will instead admit that tcl/tk default is ugly by default ...


Except for windows ...


I am not a designer, but I am not blind either. I noticed that flat (aka without relief) designs age fairly well compared to the others.

So, we want themes, and why ? Because they can help provide a consistent cross platform look and feel. Here is an exemple on windows linux, freeBSD with the breeze theme :
The most part of the code has been publish in the post about making python and tcl/tk talk bidirectionnaly without tkinter. I am gonna focus on the theme part of the code which is mainly. Python users may want to look this posts since the available themes for python that can be installed with
pip install ttkthemes
have a good documentation on how to use ttkthemes and how they look but does not include all the available themes and where to get them.

The same themes as pypi ttkthemes can be obtained on debian with
apt install tcl-ttkthemes
and they are stored in /usr/share/tcltk/ttkthemes/themes/. From there we can use tcl that has its good sides to build a combobox for choosing a theme :
set themes_dir [ glob -directory  /usr/share/tcltk/ttkthemes/themes/ -type d * ]
set themes [ list ]
foreach dir $themes_dir {
    lappend themes [ file tail $dir ]
}
ttk::combobox .cb -textvariable theme -values [ lsort $themes ] -width 12
.cb state readonly
bind .cb <<ComboboxSelected>> { 
    set theme [%W get]
    catch {
    source /usr/share/tcltk/ttkthemes/themes/$theme/$theme.tcl }} pass
    ttk::style theme use $theme
}
catch { code } result
is the equivalent of try:/except, except you capture the exception in result and catch return 1 on error. the bind part call a proc to change the theme. With this and a few more widgets and bindings we can appreciate that definitively the breeze theme that is not provided by default is the best one ^_^

The essential



A ttk theme is fairly easy to use in tcl:
  1. spot its tcl file. ex aquativo.tcl
  2. source it ONCE
     source /usr/share/tcltk/ttkthemes/themes/aquativo/aquativo.tcl 
  3. use it :
    ttk::style theme use aquativo
  4. use only the ttk:: widgets in your tcl code
And that's all.

Annexe: full code



#!/usr/bin/env python
from subprocess import Popen, PIPE
from time import sleep, time, localtime
import os

# let's talk to tk/tcl directly through p.stdin
p = Popen(['wish'], stdin=PIPE, stdout=PIPE)

os.set_blocking(p.stdout.fileno(), False)
os.set_blocking(p.stdin.fileno(), False)

def puts(s):
    for l in s.split("\n"):
        p.stdin.flush()
        p.stdin.write((l + "\n").encode())
        p.stdin.flush()

def gets():
    ret=p.stdout.read()
    p.stdout.flush()
    return ret

WIDTH=HEIGHT=500

puts(f"""

canvas .c -width {WIDTH} -height {HEIGHT} -bg white
pack .c
.c configure -background white
ttk::frame .h
pack .h -fill both -expand true -padx 0 -pady 0
ttk::frame .g 
pack .g -in .h -fill x -expand true -padx 0

ttk::button  .ba -command {{  puts ch-=1 }} -text <<
ttk::button .bb -command {{  puts cm-=1 }} -text <
ttk::button .bc -command {{  puts ch+=1 }} -text >>
ttk::button .bd -command {{  puts cm+=1 }} -text >
pack .ba .bb  -side left -anchor w -in .g
pack .bc .bd  -side right -anchor e -in .g

set themes_dir [ glob -directory  /usr/share/tcltk/ttkthemes/themes/ -type d * ]
set themes [ list ]
foreach dir $themes_dir {{
    lappend themes [ file tail $dir ]
}}
ttk::combobox .cb -textvariable theme -values [ lsort $themes ] -width 12
.cb state readonly
bind .cb <<ComboboxSelected>> {{ 
    set theme [%W get]
    catch {{
    source /usr/share/tcltk/ttkthemes/themes/$theme/$theme.tcl }} pass
    ttk::style theme use $theme
}}
pack .cb  -in .g
ttk::frame .f 
pack .f -in .h -expand 1 -anchor s -padx 0 
set h 0
set cm 0
set s 0

ttk::label .l -text "sample of label"
ttk::entry .i -text "sample of input" -textvariable theme
pack .l .i -anchor w -in .f -padx 5 -pady 5
ttk::scale .s  -from 0 -to 24 -variable h 
ttk::button .bt -text Quit -command "destroy ."
ttk::spinbox .sb -from -60 -to 60 -textvariable cm -width 5 -command {{ puts "cm=$cm" }}
ttk::progressbar .pb -maximum 60 -variable s
pack .s .sb .pb  -in .f -side left -anchor se -padx 5 -pady 5 
pack .bt -in .h -anchor s -pady 5 -padx 5
""")

# Constant are CAPitalized in python by convention
from cmath import  pi as PI, e as E
ORIG=complex(WIDTH/2, HEIGHT/2)

# correcting python notations j => I
I = complex("j")
rad_per_sec = 2.0 * PI /60.0
rad_per_min = rad_per_sec / 60
rad_per_hour= rad_per_min / 12

origin_vector_hand = WIDTH/2 *  I

size_of_sec_hand = .9
size_of_min_hand = .8
size_of_hour_hand= .65

rot_sec = lambda sec  : -E ** (I * sec  * rad_per_sec)
rot_min = lambda min  : -E ** (I * min  * rad_per_min)
rot_hour= lambda hour : -E ** (I * hour * rad_per_hour)

to_real = lambda c1,c2 : "%f %f %f %f" % (c1.real,c1.imag,c2.real, c2.imag)
for n in range(60):
    direction= origin_vector_hand * rot_sec(n)
    start=.9 if n%5 else .85
    puts(f".c create line {to_real(ORIG+start*direction,ORIG+.95*direction)}")
    sleep(.01)

diff_offset_in_sec = (time() % (24*3600)) - \
    localtime()[3]*3600 -localtime()[4] * 60.0 \
    - localtime()[5]
ch=cm=0
n=0
while True:
    n+=1
    # eventually parsing tcl output
    t = time()
    s= t%60
    m = m_in_sec = t%(60 * 60) + cm * 60
    h = h_in_sec = (t- diff_offset_in_sec)%(24*60*60) + ch * 3600 + cm * 60 

    if back := gets():
        back = back.decode()
        exec(back)

    puts(".c delete second")
    puts(".c delete minute")
    puts(".c delete hour")
    if n%10==0:
        puts(f"set s {int(s%60)}")
        puts(f"set cm {cm}")
        puts(f"set h {h/3600}")
    n%=100
    c0=ORIG+ -.1 * origin_vector_hand * rot_sec(s)
    c1=ORIG+ size_of_sec_hand * origin_vector_hand * rot_sec(s)
    puts( f".c create line {to_real(c0,c1)} -tag second -fill blue -smooth true")
    c1=ORIG+size_of_min_hand * origin_vector_hand * rot_min(m)
    puts(f".c create line {to_real(ORIG, c1)} -tag minute -fill green -smooth true")
    c1=ORIG+size_of_hour_hand * origin_vector_hand * rot_hour(h)
    puts(f".c create line {to_real(ORIG,c1)} -tag hour -fill red -smooth true")
    puts("flush stdout")
    sleep(.15)

No comments: