An extension that provides slightly modified ttk widgets, with the ability to change the size and rotation angle of the image they display, without modifying the underlying Tk image. *** What you can do with it *** All widgets that accept the '''-image''' opton, will have additional options to: * specify the size of the image * specify wether the aspect ratio of the original image should be kept when resizing * atomatically adapt the size of the image to the available space in the widget * rotate the image by any angle *** Limitations, known bugs, notes *** * works only with photo images * does not apply to ttk::notebook and ttk::treeview * needs imgtools (http://tkimgtools.sourceforge.net) * custom options are not part of the options database. * [ image inuse ] won't work correctly * because the widget commands get renamed, error messages will show the wrong widget command * widgets will react a little differently if an image is changed or deleted: while normally ttk widgets don't adapt their size on such an event, with this extension they will. ---- ====== # # -- dift - Dynamic Images For Tk # # An extension that provides slightly modified ttk widgets, with the # ability to change the size and rotation angle of an image on the fly # without modifiying the underlying Tk image. # # Author: Simon Bachmann # # # Usage # ======= # # The extension provides a set of widgets that can dynamically change the size # and rotation of the image they display, without affecting the tk image that was # set with the -image option. For the rest, these widgets work exactly like their ttk # counterparts: # # ::dift::ttk::label # ::dift::ttk::button # ::dift::ttk::checkbutton # ::dift::ttk::menubutton # ::dift::ttk::radiobutton # # The additional options are: # # -imagewidth size # -imageheight size Reference width and height to use for image display. # The actual size of the displayed image depends also # on the -imageaspectratio option (see below). # Accepted values: # - integer greater than zero: size in pixels # - 'orig': use size of original image # - 'widget': use size of available space in widget. # Gets updated automatically if the size of the # widget changes. # Defaults to 'orig'. # # -imageaspectratio mode Specify how the image should be resized with respect # to the reference given with the size options above. # May be one of: # - 'ignore': scale image exactly to the reference size # without preserving the aspect ratio # - 'keep': keep the aspect ratio of the original image. # Make the image as large as possible, without # exceeding the bounds given by the size # options # - 'keepexpand': keep the aspect ratio. Make the image # large enough to cover all of the reference area # given by the size options, but not larger # Does not affect -imagerotation. # Defaults to 'ignore' # # # -imagerotation angle Turn the image clockwise by angle degrees. # Negative values for counter-clockwise rotation. # The size of the image will not change. # Defaults to 0° # # If more than one image is set for a particular widget (images bound to a statespec), # these options affect all images. # # For convenience, the ttk widgets that do not have a '-image' option are also provided. # (e.g. ::dift::ttk::labelframe). These commands are merely an alias to the ttk widgets. # package require Tk 8.6 package require imgtools 0.3 namespace eval ::dift { variable version 0.2 # # List of all widget creation commands, that we consider variable modWidgets [list ::ttk::label ::ttk::button ::ttk::checkbutton \ ::ttk::menubutton ::ttk::radiobutton] # # These ones don't get any special treatment variable unchangedWidgets [list ::ttk::scale ::ttk::panedwindow ::ttk::separator \ ::ttk::entry ::ttk::frame ::ttk::labelframe ::ttk::sizegrip ::ttk::combobox \ ::ttk::notebook ::ttk::progressbar ::ttk::treeview ::ttk::spinbox \ ::ttk::scrollbar] # # create a ::dift::ttk namespace to hold commands to create widgets namespace eval ::dift::ttk { namespace export * } # # Custom options # the dict contains the command description ([pathName configure -option]) # The values of the second dict (list) is: opt name, database name, database # class, default value. without current value, of course variable customOpts { -imagewidth -imageheight -imageaspectratio -imagerotation } variable customOptsDescr { -imagewidth {-imagewidth {} {} orig} -imageheight {-imageheight {} {} orig} -imageaspectratio {-imageaspectratio {} {} ignore} -imagerotation {-imagerotation {} {} 0} } # # In this dict we keep track of the images # first key: name of widget # second key: # 'orig' orignal images # 'mod' transformed images. if name(s) are equal to the ones in # orig, we did no tranformation # third key: ttk state for the image ('default' for default image) # value: name of the image. Empty String if no data available variable imgIndex [dict create] # # This list holds the names of all transformed (mod) images we created. # Needed to decide if an image can be deleted when no longer used variable modImgList {} # # This one saves our custom options # first key: name of widget # second key: name of option # value: value of option variable optDict [dict create] # # Here we keep our images # the counter is for the names namespace eval imgs {} variable imgCounter 0 # # To be able to calculate the available space for images at init, we need # an image to display. We'll use a single-pixel (black, fully transparent) # for that: image create photo ::dift::imgs::unitImg ::dift::imgs::unitImg put {#000000} ::dift::imgs::unitImg transparency set 0 0 1 # # Renamed commands (where possible) go here namespace eval cmds {} variable wCmdPrefix ::dift::cmds:: } # # -- initPackage # # Rename all commands that create widgets that can contain images # and do other setup stuff # proc ::dift::initPackage {} { variable modWidgets variable unchangedWidgets foreach w $modWidgets { if {[info command ::dift::$w] ne {}} { continue } proc ::dift::$w {pathName args} [list apply {{wCreator} { upvar pathName myPathName upvar args myArgs return [::dift::createWidget $wCreator $myPathName $myArgs] }} $w ] } foreach w $unchangedWidgets { if {[info command ::dift::$w] ne {}} { continue } proc ::dift::$w {pathName args} [list apply {{wCreator} { upvar pathName myPathName upvar args myArgs return [$wCreator $myPathName {*}$myArgs] }} $w ] } return } # # -- createWidget # # Gets called whenever the user creates a new widget with one of the commands # we renamed at init. Filters out the interesting options, creates the # widget as desired and renames the windget command. # proc ::dift::createWidget {wCreator pathName arglist} { variable imgIndex variable optDict variable customOptsDescr variable wCmdPrefix # # Set defaults dict set imgIndex $pathName orig {} dict set imgIndex $pathName mod {} dict set optDict $pathName -image {} dict set optDict $pathName -imagewidth \ [lindex [dict get $customOptsDescr -imagewidth] 3] dict set optDict $pathName -imageheight \ [lindex [dict get $customOptsDescr -imageheight] 3] dict set optDict $pathName -imageaspectratio \ [lindex [dict get $customOptsDescr -imageaspectratio] 3] dict set optDict $pathName -imagerotation \ [lindex [dict get $customOptsDescr -imagerotation] 3] # # Create widget without options first # rename the widget command if {[catch {$wCreator $pathName} procResult procOpts]} { cleanupWidget $pathName return -options $procOpts $procResult } rename ::$pathName ::$wCmdPrefix$pathName proc ::$pathName {subcmd args} { set w [lindex [info level 0] 0] ::dift::widgetCommand $w $subcmd $args } # # Configure the widget if {[catch {widgetConfigure $pathName $arglist} confResult confOpts]} { cleanupWidget $pathName return -options $confOpts $confResult } # # Bindings bind $pathName [list ::dift::cleanupWidget $pathName] bind $pathName [list ::dift::configureEvent $pathName] return -options $procOpts $procResult } # # -- cleanupWidget # # Get rid of everything related to widget pathName # proc ::dift::cleanupWidget {pathName} { variable optDict variable imgIndex variable wCmdPrefix catch {destroy $pathName} catch {rename ::$pathName {}} catch {rename ::$wCmdPrefix$pathName {}} # remove traces on orig imgs removeTraces $pathName [dict get $imgIndex $pathName orig] dict unset optDict $pathName catch {deleteModImgs $pathName} dict unset imgIndex $pathName return } # # -- updateImgView # # Updates the displayed image(s) of widget pathName # based on the current settings in optDict and state # of the widget. # Does take care to perform only those actions that are # needed. We cannot, however, detect if an original image was changed # since last update. Thus, we assume that all original images might # have changed. It is up to the caller to call this proc only # when needed. # # Ttk widgets react kinda strangely if an image gets modified or deleted. # While changes of the contents ofthe image are displayed ok, ttk widgets # don't adapt their size if the size of an image changes after it was # set with configure -image. Also, if more than one image is set (per-state) # and one of them is deleted, the widget will show no image for all states. # # We will not mimick this behaviour, as it would make it quite difficult to # implement the 'widget' size to the -imagewidth/height options correctly. # And, I suspect this behaviour is a bug. # # We will have the widget resize itself if an original image changes. # However, we'll keep the rule, that if for one state the image is deleted, # we'll display no image for all states. In this case, the entry in imgIndex # in the orig branch will be {} for the image in question, the mod images # will all be set to unitImg. # proc ::dift::updateImgView {pathName} { variable optDict variable imgIndex variable wCmdPrefix set imgOpt [dict get $optDict $pathName -image] # # No value for -image option: this means that the widget was (re)configured # with '-image {}'. We delete mod imgs, reconfigure the real widget and quit # Orig branch of imgIndex should already be ok (newOrigImgs was called). if {$imgOpt eq {}} { deleteModImgs $pathName if {[dict get $imgIndex $pathName orig] ne {}} { error "::dift::updateImgView: internal error. Empty img list but orig entry in dict is [dict get $imgIndex $pathName orig]" } ${wCmdPrefix}${pathName} configure -image {} return } # # Recreate the orig branch of imgIndex and check if all orig images exist set imgOpt [list default {*}$imgOpt] set origImgsExist yes # if -image option is not set, orig branch is the empty string dict set imgIndex $pathName orig {} foreach {state img} $imgOpt { if {$img ni [image names]} { set origImgsExist no dict set imgIndex $pathName orig $state {} } else { dict set imgIndex $pathName orig $state $img } } # # actual widget has no images set: set the unitImg if {[${wCmdPrefix}${pathName} cget -image] eq {}} { ${wCmdPrefix}${pathName} configure -image ::dift::imgs::unitImg #after idle [list ::dift::updateImgView $pathName] #return } # # some orig images don't exist: set all to unitPixel # otherwise: recalculate mod images if { ! $origImgsExist} { deleteModImgs $pathName foreach {state img} $imgOpt { dict set imgIndex $pathName mod $state ::dift::imgs::unitImg } } else { modifyImages $pathName } # # reconfigure the widget ${wCmdPrefix}${pathName} configure -image [getImgOptList $pathName] return } # # -- modifyImages # # Modify image of widget pathName according to opts # in optDict. Does not update the view in the widget, just creates # the modified images and writes their name to the imgIndex. # # This proc assumes that all original images exist # proc ::dift::modifyImages {pathName} { variable imgIndex variable imgCounter variable optDict variable modImgList # # We must wait to delte previously modified images until after transformation # as the previous imgs are needed to calculage available space in widget set delImgList [getModDeleteList $pathName] dict set imgIndex $pathName mod {} # # Transform where needed set imgList [list "default"] lappend imgList {*}[dict get $optDict $pathName -image] foreach {state img} $imgList { # # Create the modified image if needed and write its name to the index set sizespec [getSizeSpec $pathName $state] set origSizespec "[image width $img]x[image height $img]" set angle [dict get $optDict $pathName -imagerotation] if {$sizespec eq $origSizespec && $angle == 0} { # Nothing to do, just "link" to the original image dict set imgIndex $pathName mod $state $img continue } set newImg [image create photo "::dift::imgs::dti_image[incr imgCounter]"] lappend modImgList $newImg if {$sizespec ne $origSizespec} { ::imgtools::scale $img $sizespec -compositingrule set -shrink $newImg set img $newImg } if {$angle != 0} { ::imgtools::rotate $img $angle -clipping keepsize -compositingrule set \ -shrink -interpolation linear $newImg } dict set imgIndex $pathName mod $state $newImg } # # delete previous mod images foreach img $delImgList { image delete $img } return } # # -- getSizeSpec # # Generates a sizespec to be used as argument for Imgtools, # for the image specifed for state # proc ::dift::getSizeSpec {pathName state} { variable optDict variable imgIndex set width [dict get $optDict $pathName -imagewidth] set height [dict get $optDict $pathName -imageheight] lassign [getImgCavitySize $pathName] widgetWidth widgetHeight set origWidth [image width [dict get $imgIndex $pathName orig $state]] set origHeight [image height [dict get $imgIndex $pathName orig $state]] set ratioMode [dict get $optDict $pathName -imageaspectratio] set origRatio [expr {1.0 * [image width [dict get $imgIndex $pathName orig $state]] \ / [image height [dict get $imgIndex $pathName orig $state]]}] set width [expr {$width eq "orig" ? $origWidth : $width}] set width [expr {$width eq "widget" ? $widgetWidth : $width}] set height [expr {$height eq "orig" ? $origHeight : $height}] set height [expr {$height eq "widget" ? $widgetHeight : $height}] set widthFromHeight [expr {round(1.0 * $height * $origRatio)}] set heightFromWidth [expr {round(1.0 * $width / $origRatio)}] switch $ratioMode { "ignore" { set sizespec "${width}x${height}" } "keep" { if {$widthFromHeight > $width} { set sizespec "${width}x${heightFromWidth}" } else { set sizespec "${widthFromHeight}x${height}" } } "keepexpand" { if {$widthFromHeight < $width} { set sizespec "${width}x${heightFromWidth}" } else { set sizespec "${widthFromHeight}x${height}" } } } return $sizespec } # # -- getImgCavitySize # # Returns a list with the size of the available space in widget # to display images. # proc ::dift::getImgCavitySize {pathName} { #variable imgIndex variable wCmdPrefix set imglist [${wCmdPrefix}$pathName cget -image] if {$imglist eq {}} { # this should not happen! error "dift, getImgCavitySize: internal error. Got called for widget with no images set" } set img [getImgForState $pathName [$pathName state]] set imgWidth [image width $img] set imgHeight [image height $img] set horizDiff [expr {[winfo reqwidth $pathName] - $imgWidth}] set vertDiff [expr {[winfo reqheight $pathName] - $imgHeight}] # make sure we don't end up with a negative or zero size set cavityWidth [expr {[winfo width $pathName] - $horizDiff}] set cavityWidth [expr {$cavityWidth <= 0 ? 1 : $cavityWidth}] set cavityHeight [expr {[winfo height $pathName] - $vertDiff}] set cavityHeight [expr {$cavityHeight <= 0 ? 1 : $cavityHeight}] return [list $cavityWidth $cavityHeight] } # # -- getImgForState # returns the name of the image that is currently used for statespec # in widget $pathName # type may be orig or mod # proc ::dift::getImgForState {pathName statespec {type mod}} { variable wCmdPrefix # we can't use image names from imgIndex, because it might not # have been updated. if {$type eq "mod"} { set imgList [${wCmdPrefix}${pathName} cget -image] } elseif {$type eq "orig"} { set imgList [$pathName cget -image] } else { error "::dift::getImgForState: internal error. Invalid type $type" } set stateImgs [lassign $imgList default] set resImg {} dict for {state img} $stateImgs { if {[stateMatch $state $statespec]} { set resImg $img break } } if {$resImg eq {}} { set resImg $default } return $resImg } # # -- stateMatch # # returns true if statespec matches wstate, false othrewise # proc ::dift::stateMatch {statespec wstate} { set match true foreach state $statespec { if {[string index $state 0] eq "!"} { set state [string range $state 1 end] if {$state in $wstate} { set match false break } } else { if {$state ni $wstate} { set match false break } } } return $match } # # -- widgetCommand # # Widget command for all widgets are rerouted throught this proc # Filters out the parts that we're interested in, passes everything else # on to the original widget command # proc ::dift::widgetCommand {pathName subcmd arglist} { variable wCmdPrefix switch $subcmd { "cget" { return [widgetCget $pathName $arglist] } "configure" { if {[llength $arglist] <= 1} { return [widgetListOpts $pathName $arglist] } else { return [widgetConfigure $pathName $arglist] } } default { return [::$wCmdPrefix$pathName $subcmd {*}$arglist] } } error "switch is broken!" return } # # -- widgetCget # # Implements the cget widget subcommand # proc ::dift::widgetCget {pathName option} { variable optDict variable wCmdPrefix switch $option { "-image" - "-imagewidth" - "-imageheight" - "-imageaspectratio" - "-imagerotation" { return [dict get $optDict $pathName $option] } default { # we expand the option argument, so we can leave the task to # complain about wrong number of args to the original # widget command return [::$wCmdPrefix$pathName cget {*}$option] } } error "switch is broken!" return } # # -- widgetListOpts # # Implements the information retrieving part of the configure # subcommand (no options, only one option as argument) # proc ::dift::widgetListOpts {pathName arglist} { variable customOpts variable wCmdPrefix if {[llength $arglist] == 0} { set optDescr [::$wCmdPrefix$pathName configure] foreach opt $customOpts { lappend optDescr [getOptDescr $pathName $opt] } return $optDescr } elseif {$arglist in $customOpts} { return [getOptDescr $pathName $arglist] } else { return [::$wCmdPrefix$pathName configure {*}$arglist] } } # # -- getOptDescr # # returns a list describing the custom option passed as argument # the elements of the list are option name, database name, database # class, default value and the current value # proc ::dift::getOptDescr {pathName opt} { variable optDict variable customOptsDescr set result [dict get $customOptsDescr $opt] lappend result [dict get $optDict $pathName $opt] return $result } # # -- widgetConfigure # # The configure subcommand, when used for a real configure (i.e. at # least one opt-val pair passed) # intercept custom options, modify the value of the -image opt # proc ::dift::widgetConfigure {pathName arglist} { variable optDict variable wCmdPrefix # # Go through options set forwardOpts {} set updateImgs no foreach {opt value} $arglist { switch $opt { "-image" { # # Existence of states is checked by ttk # existence of image is checked in newOrigImgs if {$value ne {} && [llength $value ] % 2 != 1} { error "image specification must contain an odd number of elements" } newOrigImgs $pathName $value set updateImgs yes } "-imagewidth" - "-imageheight" { if {$value ne "orig" && $value ne "widget" \ && (! [string is integer -strict $value] || $value <= 0) } { error "bad size \"$value\": must be orig, widget or integer greater than zero" } dict set optDict $pathName $opt $value set updateImgs yes } "-imageaspectratio" { if {$value ni {ignore keep keepexpand}} { error "bad aspect ratio \"$value\": must be ignore, keep or keepexpand" } dict set optDict $pathName $opt $value set updateImgs yes } "-imagerotation" { if { ! [string is double -strict $value]} { #cleanupWidget $pathName error "bad angle of rotation \"$value\": expected floating-point number" } if {$value ne [dict get $optDict $pathName -imagerotation]} { set updateImgs yes } dict set optDict $pathName -imagerotation $value } default { lappend forwardOpts $opt $value } } } # # Configure the widget if {[llength $forwardOpts] == 0} { # we got only custom opts and need not to pass anything on to # the original widget command set result {} } elseif {[llength $forwardOpts] >= 2} { set result [::$wCmdPrefix$pathName configure {*}$forwardOpts] } else { error "dift::widgetConfigure: internal error, only 1 forwardOpt: $forwardOpts" } # # Update transformed images if {$updateImgs} { #after idle [list ::dift::updateImgView $pathName] updateImgView $pathName } return $result } # # -- getImgOptList # # build a list suitable to be used as value for the -image option, # based on the value saved in our optDict, with the transformed Images. # proc ::dift::getImgOptList {pathName} { variable imgIndex variable optDict set optval [dict get $optDict $pathName -image] set newOptval {} if {$optval eq {} } { return $newOptval } lappend newOptval [dict get $imgIndex $pathName mod default] foreach {state img} [lrange $optval 1 end] { lappend newOptval $state [dict get $imgIndex $pathName mod $state] } return $newOptval } # # -- deleteModIMgs # # delete all the modified images for pathName # no errors - even if no entries for pathName esist # Checks if an image in the mod branch of the dict really is a modifeed # one. If it's just a "link" to the original image (no transformation # needed) it does not delete it - of course! # Removes all entries in the mod branch of imgIndex. orig branch is left # untouched. # proc ::dift::deleteModImgs {pathName} { variable imgIndex variable modImgList if { ! [dict exists $imgIndex $pathName mod]} { return } set delList [getModDeleteList $pathName] foreach img $delList { set idx [lsearch -exact $modImgList $img] set modImgList [lreplace $modImgList $idx $idx] catch {image delete $img} } dict set imgIndex $pathName mod {} return } # # -- getModDeleteList # # Similar as deleteModImgs, but don't actually delete images. # just return a list of the images that should be deleted # proc ::dift::getModDeleteList {pathName} { variable imgIndex variable modImgList if { ! [dict exists $imgIndex $pathName mod]} { return } set delList {} dict for {state img} [dict get $imgIndex $pathName mod] { if {$img in $modImgList } { lappend delList $img } } return $delList } # # -- newOrigImgs # # Checks if the new images exist (error if not) # Adds a trace on the image command of each new image (detect changes!) # Removes traces from previous orig images that get replaced # writes new imglist to optDict and to the orig branch of imgIndex. # Does not delete mod images # does NOT transform images, nor uptdate the widget's view # proc ::dift::newOrigImgs {pathName imglist} { variable optDict variable imgIndex if {$imglist eq {}} { set modlist {} } else { set modlist [list default {*}$imglist] } foreach {state img} $modlist { if { $img ni [image names] } { error "image \"$img\" doesn't exist" } } # # remove traces from previous orig images, add trace for new image(s) if {[dict exists $imgIndex $pathName orig]} { removeTraces $pathName [dict get $imgIndex $pathName orig] } addTraces $pathName $modlist # # delete mod images # NOPE! We might need them to calculate size of new images! # deleteModImgs $pathName # # write new value to optDict and imgIndex dict set optDict $pathName -image $imglist dict set imgIndex $pathName orig $modlist return } # # -- removeTraces # # removes traces from orig images in imgIndex-style dict imgDict # imgDict must have the same format as the innermost dict in imgIndex # i.e. state-imageName pairs. # # does not throw errors if a trace does not exist # proc ::dift::removeTraces {pathName imgDict} { dict for {state img} $imgDict { catch {trace remove execution $img leave [list ::dift::origImgModified $pathName $img]} catch {trace remove command $img delete \ [list after 0 ::dift::origImgDeleted $pathName $img]} } return } # # -- addTraces # # adds traces to orig images # imgDict must have the same format as the innermost dict in imgIndex # i.e. state-imageName pairs. # imgDict may be an empty string # # may throw an error if trace fails # proc ::dift::addTraces {pathName imgDict} { dict for {state img} $imgDict { trace add execution $img leave [list ::dift::origImgModified $pathName $img] # we need an [after] callback here, because at the time the trace triggers, the # image still exists trace add command $img delete [list after 0 ::dift::origImgDeleted $pathName $img] } return } # # -- origImgModified # # Called by trace, whenever an original image is modified # Applies transformations and updates widget # proc ::dift::origImgModified {pathName imgName args} { #variable wCmdPrefix #modifyImages $pathName #::$wCmdPrefix$pathName configure -image [getImgOptList $pathName] #after idle [list ::dift::updateImgView $pathName] updateImgView $pathName return } # # -- origImgDeleted # # Called by trace, whenever an orignial image gets deleted. # updateImgView handles the case where orig images no longer exist. # No need to remove traces: for deleted image(s) it's gone anyway, # for the other ones, it doesn't hurt to keep them. # proc ::dift::origImgDeleted {pathName imgName args} { # since there's an after callack involved, this proc might be callde # after pathName has been destroyed. In this case, there's nothing # we have to do if { [winfo exists $pathName] } { updateImgView $pathName } return } # # -- configureEvent # # Updates displayed image after a event, if needed. # This is the case if at least one size was set to 'widget' # proc ::dift::configureEvent {pathName} { variable optDict if {[dict get $optDict $pathName -image] ne {}} { updateImgView $pathName } return } ::dift::initPackage package provide dift $::dift::version ====== <>Category Image Processing|Category Widget|Category GUI