TkX

TkX is a megawidget package based on Ttk.

I'm working on redefining how one interacts with Tk, and I'm looking for some ideas from the hive mind. As someone who has written a lot of Tk code, I've always found it cumbersome in a lot of ways while still being infinitely easier than other toolkits I've used. TkX started as a megawidget framework that would eventually be TIP'd as the megawidget framework to be included with Tk as the default. That part is actually done, but now I'm looking to go one further and make interacting with Tk better.

Here are the main issues I seek to solve:

Naming Widgets

Naming widgets is a pain in the ass when, most times, there are only a few you might actually care about in your GUI. I'm specifically referring to things like frames, toolbars, scrollbars, etc... Things that it doesn't matter what the widget is called because you'll never refer to it by its path again.

Widget Tagging

Conversely, there are widgets that you do sometimes want to remember for later. Every Tk app basically has some similar form of this piece of code:

global widgets

set f [frame $top.f1]
pack $f

set widgets(OKButton) [button $f.b1 -text OK]
pack $widgets(OKButton) -side left

We use some array or some dict or something somewhere that stores a reference to a widget path by some name. We basically invent some system of tagging widgets every time we write a GUI app. If you don't know what I'm talking about, you haven't written enough Tk.

These two issues (which are just a start) actually support each other. With widget tagging, we don't need to worry about naming. We simply tag the widgets we care about and forget the ones we don't and let Tk handle naming them for us. I also wanted to include geometry management as something I want to work on, but what I propose is not necessarily a fundamental change in what is already there. Just a re-imagining. With those things in mind, here is what TkX proposes:

Proposed Syntax

tkx::new toplevel . {-tags Top}

tkx::new ttk::frame Top {-tags Toolbar} \
    grid {-row 0 -column 0 -sticky ew}
    tkx::new ttk::button Toolbar {-text Help -command exit} \
        pack {-side left}
    tkx::new ttk::button Toolbar {-text Quit -command exit} \
        pack {-side left}

tkx::new {tkx::scrolled text} Top {-background white -foreground gray} \
    grid {-row 1 -column 0 -sticky nesw -weight 1}

TkX provides a tkx::new command through which all widget creation passes. This is mostly because I don't want to step on what Tk already does and/or propose a complete rewrite of Tk. This package is not meant to create an incompatibility, but simply to propose a new way going forward. The syntax is basically:

tkx::new widgetCommand parentWidget ?widgetOptions? ?geometryManager? ?geometryOptions?

I'm not married to this syntax, which is why I'm creating this page. I want to get some opinions from the people who knew Tk best about how this should all look. Here are the ideas I've had so far.

Syntax Options

tkx::new text $parent -background white -foreground black \
    grid -row 0 -column 0

tkx::new text $parent -background white -foreground black -- \
    grid -row 0 -column 0

tkx::new text $parent {-background white -foreground black} \
    grid {-row 0 -column 0}

tkx::new text $parent {
    -background white -foreground black
} grid {
    -row 0 -column 0
}

They are all variations of the same theme but with small differences. The first version takes a little more parsing and would suffer a small performance hit because of it. The second would also require a bit of parsing but less than the first. The third and fourth are identical except for white spacing. Either one could be used, but the question comes down to how we would document things for newbies.

The reason for a lot of this is to remove some of the more confusing parts of Tk and make it easier for newbies to grasp. That is a major goal in the new design, so keep that in mind while you're looking at the variations. The last one is the most verbose, but it also makes it really easy for a newbie to not have to use \ at the line end or anything like that. It simply looks like blocks.

I'm going to stop now and open the floor for discussion. This code has already been written, and I'm almost ready to release. I just need to decide on syntax here and then put it out for iteration and evolution.

Damon Courtney (July 22nd, 2010)


AMG: One of your examples creates a new toplevel tagged "Top" that is a child of "." . Is this because "." is special-cased to be a pre-recognized tag? Or is it because widget paths can be specified as an alternative to tags when giving the parent name?

Damon Courtney: You can use a widget path or tag interchangeably, just like in the Tk canvas with item IDs and tags. It doesn't matter. I should point out that TkX also provides the following commands for use within the system:

tkx::tag command ?options ...?
tkx::cget tagOrWidget option
tkx::configure tagOrWidget ?option? ?value ?option value ...?

The tkx::tag command has commands similar to tagging in the Text and Canvas widgets for adding and removing tags from widgets. The tkx::new command simply adds a simple way into this interface by accepting a -tags option on all widgets and then doing the tagging for you.

AMG: What happens creating a widget parented to a tag that matches multiple widgets? That's probably an error. But are there cases where it's valid to have multiple widgets share the same tag? It might be nice to do a single tkx::configure to set the options for multiple widgets, although this can sometimes fail when the widgets are different types.

Damon Courtney: The tkx::tag command does allow you to tag multiple widgets (and menu entries) with a single tag, and the configure command will configure all the widgets associated with a tag just like the Canvas widget. If there is a widget that doesn't accept a particular option, it will simply ignore it and move on. In the case of using a tag as the widget parent, it's going to pick up the first widget that has that tag. Generally speaking, you would always use a unique tag for a widget name and then some generic name for other uses. At least that's how I do it. 0-]


HaO: An issue which often results in cluttered code is tabbing order and clipping order. If they should be different, one could (see example on page tk_focusNext):

  • use raise
  • create and pack widgets in different order

It would be helpful, if this could be solved in a more straitforward way be a megawidget framework.

Damon Courtney: With the geometry attached to each widget at its point of creation, the widgets would basically have a tab order of whatever order they were created and inserted into the view. Of course, with things like the grid manager, this doesn't necessarily mean that they are in clipping order. That being said, I've usually found it easiest to specify the tab order after the whole window is created. What about something like:

tkx::focusOrder toplevel ?widget ...?

Which would do all the raises and lowers to put everything in the right order. The other option is to add a fake global option to every widget that lets you specify where in the tab order it falls, but I think this is more prone to error since moving the widget means you always have to update the value. Which is what happens using grid a lot, for example. You move a widget, and you forget to change the row or column.

HaO: I would appreciate the focusOrder method.


sbron: This way you still have to create lots of tags for toplevels and frames just to be able to put other widgets inside, but which will never be used again anywhere else in the program. How about adding something resembling the -children option of the ttk::style layout command? You would not normally have to specify a parentWidget. It defaults to the widget in who's "children" block the new widgets are created, or . if the widget is created outside a "children" block. It should still be possible to specify a parentWidget (perhaps only allowed for widgets created outside of a "children" block of another widget) using the -parent option.

That way you could code your first example something like this:

tkx::new toplevel children {
    tkx::new ttk::frame grid {-row 0 -column 0 -sticky ew} children {
        tkx::new ttk::button {-text Help -command exit} pack {-side left}
        tkx::new ttk::button {-text Quit -command exit} pack {-side left}
    }
    tkx::new {tkx::scrolled text} {-background white -foreground gray} \
      grid {-row 1 -column 0 -sticky nesw -weight 1}
}

(The syntax may have to be changed a little to be able to parse the code unambiguously, but I think you get the basic idea.)

DC: I had actually started working on something like this. I'm not entirely sure how I want to do it yet, but I definitely see value in a more structured approach to subwidget creation.


AMG: sbron, looks good to me. Changing the subject: thanks for (accidentally) drawing my attention to tkx::scrolled, which I failed to notice in DC's first example. What can you guys tell me about tkx::scrolled?

DC: tkx::scrolled is a widget I've already implemented in the new TkX megawidget system. It basically just applies scrollbars to the given widget and returns a single widget path that acts as the original widget. It embeds the given widget inside a frame with scrollbars, but you still manipulate it as you would a regular widget. It delegates all the methods and options on to the underlying widget. So, instead of doing the standard method now, which is:

SomeScrolledWindowFrame .f
text .f.t
.f setwidget .f.t

You just do:

tkx::scrolled text .t

And you get .t as your widget path, which is now actually the frame, but will responds to everything just as if it were a text widget.


NEM 2010-07-29 (In reply to sbron): See Compositional Tk for an earlier attempt at doing this. Personally, in a new Tk framework, I would like to see a clean separation of the following aspects of creation:

  1. the logical structure of the UI;
  2. the behaviour of the UI;
  3. the data model underlying the UI;
  4. the layout of the UI;
  5. the style/theme of the UI.

This is an MVC separation, plus the view part is then broken down into structure, layout and style. Something like this (off the top of my head):

view PersonView {
    field name -label "Name:" -property name
    field addr -label "Address:" -property address
    choice sex -label "Sex:" -property sex
    button ok -label "OK" -command [list $model commit]
    button cancel -label "Cancel" -command [list $model rollback]
}  
layout PersonLayout -manager grid {
    row name -sticky ew
    row addr -sticky ew
    row sex -sticky ew
    row ok cancel -sticky e -padx 4
}
set person [person ...] ;# create person object
pack [PersonView pv -model $person -layout PersonLayout]

Here $person is an ordinary object which becomes the model for the view. The -property options are delegated to this model object (i.e., the view automatically calls cget/configure on the model to get/set these properties; or perhaps something analagous to trace). The rest of the puzzle I think we already have: bind/bindtags is more than powerful enough for the controller part, and ttk handles the style/theme aspects. For interactive use, the nesting would be optional, with alternative syntax being:

view PersonView
PersonView add field name -label "Name:" -property name
PersonView add button ...
layout PersonLayout -manager grid
PersonLayout row name -sticky ew
...

(This is rather off the top of my head at the moment, but indicates the general idea).

NEM Re-saving due to wiki crash on previous save...


DC: This is more like something that no longer resembles Tk at all. Not that it's a bad thing, but it's not my goal. I welcome someone else to write this package though. 0-] One thing I would change though is that all -command options should be abandoned. The -command of a button should be made to generate an event that some other part of the code can bind to if they want to do something with that event. That would be the best way to separate the view from the controller without having to embed calls directly to the controller.

NEM I don't think it's that radical a change, just a refactoring. I'll try and knock something up. I'm not sure about removing -command options. Firstly, most -command options would rarely change from controller to controller. Secondly, I think of the -command (or actions) as being the (imperative) model of the button. The controller is the binding that invokes the command on a click event. Otherwise, what is the model of a button?

escargo 30 July 2010 - I can sort of see this from two different points of view. Having a -command option allows the specification of additional parameters to be passed to the command handler if the command is the invocation of proc. I do like the notion of replacing the command option with a generated event, but only if there is a way for the widget to store information that could be easily retrieved by the event handler. That way an event handler could be shared by multiple widgets, each of which could provide additional context. (Obviously the identity of the widget needs to be available to the event handler.)

If you don't have event handlers as such, another mechanism that might work as well is for the widgets to have state variables easily accessible via trace commands, so that traces get triggered on state variable changes (such states as a button being pressed). That might be a bit more opaque, and it might still require the widget to carry custom information. This also makes it harder to share handlers, since instead of multiple widgets invoking the same event, each state variable would need its own trace.

escargo 30 July 2010 - I can sort of see this from two different points of view. Having a -command option allows the specification of additional parameters to be passed to the command handler if the command is the invocation of proc. I do like the notion of replacing the command option with a generated event, but only if there is a way for the widget to store information that could be easily retrieved by the event handler. That way an event handler could be shared by multiple widgets, each of which could provide additional context. (Obviously the identity of the widget needs to be available to the event handler.)

If you don't have event handlers as such, another mechanism that might work as well is for the widgets to have state variables easily accessible via trace commands, so that traces get triggered on state variable changes (such states as a button being pressed). That might be a bit more opaque, and it might still require the widget to carry custom information. This also makes it harder to share handlers, since instead of multiple widgets invoking the same event, each state variable would need its own trace.