TclOO extension edit
RZ I'm missing some features in plain TclOO. So I added these features on top of it. Feel free to comment or use it.- private variables (see also TclOO private variables)
- options with cget/configure function
- components (object and widgets) with integration in cget/configure methods
- private components
Enhanced commands
- constructor
- Access to private variables, setup internal structures and calling next
- destructor
- Access to private variables, deleting components and calling next
- method
- Access to private variables
- variable
- Additional -private and -privateclear switches
New class commands
option <name> <value> <body>- Define new option. The <body> will on optionsetting in the current class context evaluated.
- Remove previously defined option.
component addprivate <name> createcommand ?optionlist?
- Define new component. If the name starts with '.' (dot) it is a widget. If name is '.' (only a dot) it will make the current object act as a widget. If the name starts with '::' (double colon) it is a object.
- The createcommand will be evaluated to create the component. It should return the component command. Component commands should also have cget/configure methods to access options. If the second word inside the createcomand start with %W then %W is replaced with the current object widget '$zz(.)'
- The optionlist is a "key value" list.
- If key is keep then value is used as an option list. All component options matching one of these entries will be added to the object options.
- If key is ignore then value is used as an option list. All already defined component options matching one of these entries will be deleted.
- If key and value is starting with '-' (minus sign) then component option names key is mapped to object option value.
component deleteprivate <name> ..
- Remove previously defined component's.
New object commands
- cget <option>
- Get option values.
- configure ?option value ..?
- Get and set options.
- component
- Return all component names.
- component <name>
- Return command of the given name.
- component args
- See above for the add* and del* command syntax.
Commands inside methods
- _zz_constructor
- Setup internal variables
- _zz_destructor
- Internal cleanup
- _zz_method
- Access to private variables
Variables
The public array variable {} is used to store options (-*) and components objects (:*) and component widgets (.*). The private array variable _ is used to store private component object _(:*) and private component widgets _(.*).Examples edit
Extending widgets
::zz::class create togglelabel { superclass zz::object component . {label %W -text test} {keep -*} constructor {args} {my configure {*}$args} method toggle {} { set myBg $(-background) set myFg $(-foreground) array set {} [list -foreground $myBg -background $myFg] } } togglelabel .l -foreground black -background white .l toggle
Private variables, components
::zz::class create zz1 { superclass zz::object option -xyz z1xyz {puts zz1-xyz=$(-xyz)} option -abc abc {puts zz1-abc=$(-abc)} component add . {toplevel %W} component add .l1 {label %W.l1 -text extern} {keep -text -bd -bd ignore -bd} constructor args { lappend (a) zz1 lappend _(my) zz1 my component addprivate .l2 {label $(.).l2 -text inside} {-text -text} grid $(.l1) $_(.l2) my configure {*}$args } destructor {} method parray {name} {puts zz1>;::parray $name} } ::zz::class create zz2 { superclass zz1 option -xyz z2xyz {puts zz2-xyz=$(-xyz)} component add .l2 {label $(.).l3 -text outside} {-text -text} destructor {} constructor args { lappend (a) zz2 lappend _(my) zz2 grid $(.l2) } method parray {name} {puts zz2>;::parray $name;next $name} } zz2 .z .z parray "" .z parray _
Code edit
catch {rename ::? {}} ## Helper function for msgcat::mc command inside classes. proc ::? {args} { if {[catch {set myNs [uplevel 1 self class]}]} { set myNs [uplevel 1 namespace current] } namespace eval $myNs ::msgcat::mc $args } #=============================================================================== namespace eval ::zz { ## Customized ::oo::define command. # # Constructor with private variables, next and initialization: # constructor arglist body # # Destructor with private variables, next and internal clean up: # destructor body # # Method with private variables: # method arglist body # # Definition of additional private variables: # variable -private <name> .. # Remove all private variables: # variable -privateclear # # New definition or overwrite of options: # option <-name> value ?body? # Remove of existing options: # option delete <-name> .. # # Handling of components. See method component for documentation. # component add <component> createcmd ?optionlist? # component addprivate <component> createcmd ?optionlist? # component delete <component> .. # component deleteprivate <component> .. # proc define {class args} { switch -- [lindex $args 0] { constructor {::oo::define $class {*}[lrange $args 0 1]\ "my _zz_method;next;my _zz_constructor\n[lindex $args 2]" } destructor {::oo::define $class [lindex $args 0]\ "my _zz_method\n[lindex $args 1] \nmy _zz_destructor;next" } method {::oo::define $class {*}[lrange $args 0 2]\ "my _zz_method\n[lindex $args 3]" } variable { upvar 0 ${class}::(vars) _ switch -- [lindex $args 1] { -private { foreach myVar [lrange $args 2 end] { if {[lsearch $_ $myVar] == -1} {lappend _ $myVar $myVar} } } -privateclear {set _ {_zz _zz}} default {::oo::define $class variable {*}$args} } } option { upvar 0 ${class}:: _ if {[lindex $args 1] eq {delete}} { set myName " $class\ -\ " foreach myOpt [lrange $args 2 end] { if {[string index $myOpt 0] ne {-}} { error [? {wrong option name: %1$s} $myOpt] } set myNr [lsearch $_(optionsets) $myName$myOpt] if {$myNr == -1} {error [? {option not found: %1$s} $myOpt]} set _(optionsets) [lreplace $_(optionsets) $myNr $myNr] set _(optioninit) [lreplace $_(optioninit) $myNr $myNr] } } else { lassign $args x myOpt myVal myBody if {[string index $myOpt 0] ne {-}} { error [? {wrong option name: %1$s} $myOpt] } set myName " $class\ -\ $myOpt" set myNr [lsearch $_(optionsets) $myName] if {$myNr == -1} { lappend _(optionsets) $myName $myBody lappend _(optioninit) $myOpt $myVal } else { lset _(optionsets) [incr myNr] $myBody lset _(optioninit) $myNr $myVal } } } component { upvar 0 ${class}::(complist) _ switch -- [lindex $args 1] { add - addprivate { lassign $args x myMode myName myCmd myOpts if {[string index $myName 0] ni {. :}} { default {error [? {wrong comp name %1$s} $myName]} } foreach myL $_ { if {[lindex $myL 1] eq $myName && [lindex $myL 0] eq $myMode} { error [? {comp name exists%1$s} $myName] } } lappend _ [list $myMode $myName $myCmd $myOpts] } delete - deleteprivate { if {[lindex $args 1] eq {delete}} { set myMode add } else { set myMode addprivate } foreach myName [lrange $args 2 end] { set myNr 0 foreach myL $_ { if {[lindex $myL 1] eq $myName && [lindex $myL 0] eq $myMode} { set _ [lreplace $_ $myNr $myNr] set myNr -1 break } incr myNr } if {$myNr != -1} {error [? {component not found: %1$s} $myName]} } } default {[? {wrong component command '%1$s', should be one of %2$s}\ [lindex $args 1] {add addprivate delete deleteprivate}] } } } default {tailcall ::oo::define $class {*}$args} } } } #------------------------------------------------------------------------------- ## Customized ::oo::class command. ::oo::class create ::zz::class { superclass ::oo::class self export createWithNamespace self unexport new ## Always create new classes with namespace. # See "oo::class create" command. self method create {args} { return [uplevel 1 [list [self] createWithNamespace [lindex $args 0] {*}$args]] } ## Build new class using ::zz::class with additional commands. constructor {args} { # Current class name. set myCls [self object] # Make ::zz::* methods in class definition available. foreach myName {constructor destructor method variable option component} { interp alias {} [self namespace]::$myName {} ::zz::define $myCls $myName } # Make ::oo::define methods available. foreach myName {renamemethod deletemethod forward unexport mixin superclass export filter} { interp alias {} [self namespace]::$myName {} ::oo::define $myCls $myName } ## Internal method \c _zz_trace to handle option setting. # Defined in each class to support access to private class parts. # If op is empty then eval command given in array (internal usage only!) # Otherwise call all option related bodies. set myBody "namespace upvar \[my varname { }\]$myCls {*}\$${myCls}::(vars)" append myBody { if {$op eq {}} {eval $array ; return};# eval body if {[string index $field 0] ne {-}} return;# no option # Ensure the option setting body of . comes last, TODO optimization set myC [self class] foreach myList [lsort -decreasing [array names $array *\ $field]] { lassign $myList myCls myCmp myOpt if {$myCls eq $myC} { my _zz_trace $zz($myList) {} {} } else { nextto $myCls $zz($myList) {} {} } } } ::oo::define $myCls method _zz_trace {array field op} $myBody # Internal class informations. Define class definition variables. array set ${myCls}:: [list vars {_zz _zz} optionsets {} optioninit {} complist {}] # Define internally used array variable. ::oo::define $myCls variable zz # Add ::zz::object to list of superclasses if {$myCls ne {::zz::object}} { ::oo::define $myCls {superclass ::zz::object} } # Define default constructor ::zz::define $myCls constructor args {} # Define default destructor. ::zz::define $myCls destructor {} # Read and evaluate the class definition. my eval {*}$args } ## Enable object creation with namespace and without "new" word. method unknown {args} { my createWithNamespace ::[lindex $args 0] {*}$args } } #------------------------------------------------------------------------------- ## Class to create objects. Define class methods with ::oo::define! ::zz::class create ::zz::object { ## Array variable to hold internal informations. # (-*) Value of option. # (.*) Component widget command. # (:*) Component object command. # ( <class> <comp> <option>) Used body when setting options. variable zz } #------------------------------------------------------------------------------- ## ::oo::define ::zz::object constructor {args} { } #------------------------------------------------------------------------------- ## ::oo::define ::zz::object destructor { # object foreach {myN myV} [array get zz :*] {$myV destroy} # widget if {[info exists zz(.)]} { if {[winfo exists $zz(.)]} {destroy $zz(.)} } else { foreach {myN myV} [array get zz .*] { if {[winfo exists $myV]} {destroy $myV} } } } ## Return value of configuration option. ::oo::define ::zz::object method cget {option} { if {[string index $option 0] ne {-} || ![info exists zz($option)]} { error [? {unknown option %1$s} $option] } return $zz($option) } #------------------------------------------------------------------------------- ## Work with configuration options. ::oo::define ::zz::object method configure {args} { set l [llength $args] if {$l == 0} { set myRet {} foreach myOpt [lsort [array names zz -*]] { lappend myRet $myOpt $zz($myOpt) } return $myRet } elseif {$l == 1} {;# same as cget() function if {[string index $args 0] ne {-} || ![info exists zz($args)]} { error [? {unknown option %1$s} $args] } return $zz($args) } elseif {$l%2 == 0} { foreach {o v} $args { if {[string index $o 0] ne {-} || ![info exists zz($o)]} { error [? {unknown option %1$s} $o] } set myOld $zz($o) if {[catch {set zz($o) $v} myMsg]} { catch {set zz($o) $myOld} error [? {error in configure %1$s: %2$s} $o $myMsg] } } } else { error [? {wrong configure: %s} $args] } } #------------------------------------------------------------------------------- ## Component command. # <component> names starting with . are treated as widgets. # <component> names starting with : are treated as objects. # # Get list of available public components: # component # Get command of available public component: # component <component> # Add new public component: # component add <component> createcmd ?optionlist? # Add new private component: # component addprivate <component> createcmd ?optionlist? # Delete existing public component: # component delete <component> .. # Delete existing private component: # component deleteprivate <component> .. # # \note Defined with ::zz::define to access private variable _zz. ::zz::define ::zz::object method component {args} { # Return public component names if {$args eq {}} {return [array names zz {[.:]*}]} set myMode [lindex $args 0] # Return public component command if {[string index $myMode 0] in {. :}} { if {[info exists zz($myMode)]} { return $zz($myMode) } error [? {unknown component %1$s} $myMode] } # Add and delete components set myCls [uplevel 1 self class] set args [lrange $args 1 end] switch -- $myMode { add - addprivate {;# Add new component if {$myMode eq {add}} { set myVar [my varname zz] } else { set myVar [my varname { }]${myCls}::_zz } lassign $args myComp myCmd myOpts set myCompvar ${myVar}($myComp) if {[info exists $myCompvar]} { error [? {comp %1$s already exists} $myComp] } set myCopts {} set myCvals {} switch -- [string index $myComp 0] { . { set myCmd [string map [list %W [namespace tail [self]]] $myCmd] if {$myComp eq {.}} { set mySelf [self] rename $mySelf ::zz::self set w [uplevel 1 $myCmd] set myW ::${w}__zz__ set myBind [list $w destroy] rename $w $myW rename ::zz::self $mySelf } else { set w [uplevel 1 $myCmd] set myW $w set myBind "array unset \{$myVar\} \{ $myCls $myComp -*\} \; unset -nocomplain \{$myCompvar\}" } set w [string trimleft $w :] bindtags $w [list zz$myW {*}[bindtags $w]] bind zz$myW <Destroy> $myBind set $myCompvar $w } : { set $myCompvar [uplevel 1 $myCmd] foreach myList [$myCmd configure] { lappend myCopts [lindex $myList 0] lappend myCvals [lindex $myList end] } set myW [set $myCompvar] } default {error [? {wrong comp name %1$s} $myComp]} } foreach myList [$myW configure] { lappend myCopts [lindex $myList 0] lappend myCvals [lindex $myList end] } # Get all component options array set myFound {} foreach {myFrom myTo} $myOpts { if {[string index $myFrom 0] eq {-}} {;# -copt -opt if {[string index $myTo 0] ne {-}} { error [? {wrong option name: %1$s} $myTo] } set myNr [lsearch $myCopts $myFrom] if {$myNr == -1} { error [? {option not found: %1$s} $myFrom] } append myFound($myTo) "\n$myW configure $myFrom \$zz($myTo)" if {[lsearch $myCopts $myTo] == -1} { lappend myCopts $myTo lappend myCvals [lindex $myCvals $myNr] } } elseif {$myFrom eq {keep}} {;# keep -* foreach myT $myTo { foreach myO [lsearch -inline -glob -all $myCopts $myT] { append myFound($myO) "\n$myW configure $myO \$zz($myO)" } } } elseif {$myFrom eq {ignore}} {;# ignore -* foreach myT $myTo { foreach myO [array names myFound $myT] {unset myFound($myO)} } } else { error [? {wrong from part name: %1$s} $myFrom] } } # Set options foreach myOpt [array names myFound] { set zz(\ $myCls\ $myComp\ $myOpt) $myFound($myOpt) if {![info exists zz($myOpt)]} { set zz($myOpt) [lindex $myCvals [lsearch $myCopts $myOpt]] } } return [set $myCompvar] } delete - deleteprivate {;# Delete existing component if {$myMode eq {delete}} { set myVar [my varname zz] } else { set myVar [my varname { }]${myCls}::_zz } foreach myComp $args { set myCompvar ${myVar}($myComp) if {![info exists $myCompvar]} return # Remove option info array unset $myVar " $myCls $myComp -*" unset $myCompvar # Remove widget/object if {[string index $myComp 0] eq {:}} { catch {[set $myCompvar] destroy} continue } set w [set $myCompvar] if {[winfo exists $w]} { set myTags [bindtags $w] set i [lsearch $myTags "::zz::$w"] if {$i >= 0} { bindtags $w [lreplace $myTags $i $i] } bind ::zz::$w <Destroy> {} destroy $w } } } default {[? {wrong command '%1$s', should be one of %2$s}\ [lindex $args 1] {add addprivate delete deleteprivate}] } } } #------------------------------------------------------------------------------- ## Function for use in constructor. ::oo::define ::zz::object method _zz_constructor {} { set myCls [uplevel 1 self class] array set zz [set ${myCls}::(optionsets)] array set zz [set ${myCls}::(optioninit)] foreach myList [set ${myCls}::(complist)] { uplevel 1 [list my component {*}$myList] } # Start option variable trace in outermost class if {[info object class [self object]] eq $myCls} { trace add var [my varname zz] write [list [namespace which my] _zz_trace] } } #------------------------------------------------------------------------------- ## Function for use in destructor. ::oo::define ::zz::object method _zz_destructor {} { set myCls [uplevel 1 self class] set myVar [my varname { }]${myCls}::_zz # object foreach {myN myV} [array get $myVar :*] {$myV destroy} # widget if {[info exists ${myVar}(.)]} { set myV [set ${myVar}(.)] if {[winfo exists $myV]} {destroy $myV} } else { foreach {myN myV} [array get $myVar .*] { if {[winfo exists $myV]} {destroy $myV} } } } #------------------------------------------------------------------------------- ## Function to access private variables. ::oo::define ::zz::object method _zz_method {} { set myCls [uplevel 1 self class] set myNs [my varname { }]$myCls namespace eval $myNs {} uplevel 1 [list namespace upvar $myNs {*}[set ${myCls}::(vars)]] } #------------------------------------------------------------------------------- ## Function to access private variables. ::oo::define ::zz::object method _zz_varname {name} { return [my varname { }][uplevel 1 self class]::$name } #-------------------------------------------------------------------------------
Comments edit
DKF: My main comment is this: have you put this in a repository somewhere? It's much easier to develop when you've got proper history mechanisms available. If you prefer fossil, check out http://chiselapp.com (run by Roy Keene), if you prefer git, there's github of course, and for subversion you're probably better with google code.Aside from that, a very useful technique for doing the configure is to evaluate the user's script in a namespace (that's what oo::define really is doing, with some small extra tricks). It's great, because it takes very little code to do right. I'd also commend using forwarded methods as a technique for exposing methods from underlying widgets; by putting the contained implementation widgets in the instance namespace, you get automatic cleanup and concealment and organisation for almost nothing.RZ This is so far just a proof of concept. If it is working I will put it into some fossil repository and remove the code from here. TclOO is still a great tool but I hope to get private variables directly in it in time ;) Options, cget/configure and components would be fine too. But this is more tricky and can be evaluated in scripted extensions.Do you mean by configure the option setting part? Here I have used the _zz_trace function to evaluate code in the correct namespace. This is necessary to access private variables. Is there a better solution for this task?I'm at loss with your hint to use forward. For which part should I use it?To make cleanup easy I have put all private variables on the same place as normal variables. But I have used here for each class a separate sub-namespace. This prevent collisions because normal variables could not contain the : sign.Component widgets and objects need still deletion by hand. Therefore the destructor and _zz_destructor functions.DKF: The little megawidget framework inside Tk (see library/megawidget.tcl) puts the real Tk widgets it wraps inside its instance namespace and forwards some methods on to them. For example, if you embedded a button and wanted to expose its flash method, you might do:oo::define megabuttonclass { forward flash buttonWidget flash }Where buttonWidget is what the button has been renamed to inside the instance. This is a class-level forwarding that forwards to something in an instance (technically, the forwarding target command is resolved with respect to the instance namespace); you can do a lot of clever stuff with this. TclOO is an extremely heavy user of Tcl's namespace and stack frame facilities; because of this, it required almost no core changes.RZ Thank you for the example.