📄 combobox.tcl
字号:
# import the widgets and options arrays into this proc so # we don't have to use fully qualified names, which is a # pain. upvar ::combobox::${w}::widgets widgets upvar ::combobox::${w}::options options # this is our widget -- a frame of class Combobox. Naturally, # it will contain other widgets. We create it here because # we need it in order to set some default options. set widgets(this) [frame $w -class Combobox -takefocus 0] set widgets(entry) [entry $w.entry -takefocus 1] set widgets(button) [label $w.button -takefocus 0] # this defines all of the default options. We get the # values from the option database. Note that if an array # value is a list of length one it is an alias to another # option, so we just ignore it foreach name [array names widgetOptions] { if {[llength $widgetOptions($name)] == 1} continue set optName [lindex $widgetOptions($name) 0] set optClass [lindex $widgetOptions($name) 1] set value [option get $w $optName $optClass] set options($name) $value } # a couple options aren't available in earlier versions of # tcl, so we'll set them to sane values. For that matter, if # they exist but are empty, set them to sane values. if {[string length $options(-disabledforeground)] == 0} { set options(-disabledforeground) $options(-foreground) } if {[string length $options(-disabledbackground)] == 0} { set options(-disabledbackground) $options(-background) } # if -value is set to null, we'll remove it from our # local array. The assumption is, if the user sets it from # the option database, they will set it to something other # than null (since it's impossible to determine the difference # between a null value and no value at all). if {[info exists options(-value)] \ && [string length $options(-value)] == 0} { unset options(-value) } # we will later rename the frame's widget proc to be our # own custom widget proc. We need to keep track of this # new name, so we'll define and store it here... set widgets(frame) ::combobox::${w}::$w # gotta do this sooner or later. Might as well do it now pack $widgets(button) -side right -fill y -expand no pack $widgets(entry) -side left -fill both -expand yes # I should probably do this in a catch, but for now it's # good enough... What it does, obviously, is put all of # the option/values pairs into an array. Make them easier # to handle later on... array set options $args # now, the dropdown list... the same renaming nonsense # must go on here as well... set widgets(dropdown) [toplevel $w.top] set widgets(listbox) [listbox $w.top.list] set widgets(vsb) [scrollbar $w.top.vsb] pack $widgets(listbox) -side left -fill both -expand y # fine tune the widgets based on the options (and a few # arbitrary values...) # NB: we are going to use the frame to handle the relief # of the widget as a whole, so the entry widget will be # flat. This makes the button which drops down the list # to appear "inside" the entry widget. $widgets(vsb) configure \ -borderwidth 1 \ -command "$widgets(listbox) yview" \ -highlightthickness 0 $widgets(button) configure \ -background $options(-buttonbackground) \ -highlightthickness 0 \ -borderwidth $options(-elementborderwidth) \ -relief raised \ -width [expr {[winfo reqwidth $widgets(vsb)] - 2}] $widgets(entry) configure \ -borderwidth 0 \ -relief flat \ -highlightthickness 0 $widgets(dropdown) configure \ -borderwidth $options(-elementborderwidth) \ -relief sunken $widgets(listbox) configure \ -selectmode browse \ -background [$widgets(entry) cget -bg] \ -yscrollcommand "$widgets(vsb) set" \ -exportselection false \ -borderwidth 0# trace variable ::combobox::${w}::entryTextVariable w \# [list ::combobox::EntryTrace $w] # do some window management foo on the dropdown window wm overrideredirect $widgets(dropdown) 1 wm transient $widgets(dropdown) [winfo toplevel $w] wm group $widgets(dropdown) [winfo parent $w] wm resizable $widgets(dropdown) 0 0 wm withdraw $widgets(dropdown) # this moves the original frame widget proc into our # namespace and gives it a handy name rename ::$w $widgets(frame) # now, create our widget proc. Obviously (?) it goes in # the global namespace. All combobox widgets will actually # share the same widget proc to cut down on the amount of # bloat. proc ::$w {command args} \ "eval ::combobox::WidgetProc $w \$command \$args" # ok, the thing exists... let's do a bit more configuration. if {[catch "::combobox::Configure [list $widgets(this)] [array get options]" error]} { catch {destroy $w} error "internal error: $error" } return ""}# ::combobox::HandleEvent --## this proc handles events from the entry widget that we want# handled specially (typically, to allow navigation of the list# even though the focus is in the entry widget)## Arguments:## w widget pathname# event a string representing the event (not necessarily an# actual event)# args additional arguments required by particular eventsproc ::combobox::HandleEvent {w event args} { upvar ::combobox::${w}::widgets widgets upvar ::combobox::${w}::options options upvar ::combobox::${w}::oldValue oldValue # for all of these events, if we have a special action we'll # do that and do a "return -code break" to keep additional # bindings from firing. Otherwise we'll let the event fall # on through. switch $event { "<MouseWheel>" { if {[winfo ismapped $widgets(dropdown)]} { set D [lindex $args 0] # the '120' number in the following expression has # it's genesis in the tk bind manpage, which suggests # that the smallest value of %D for mousewheel events # will be 120. The intent is to scroll one line at a time. $widgets(listbox) yview scroll [expr {-($D/120)}] units } } "<Any-KeyPress>" { # if the widget is editable, clear the selection. # this makes it more obvious what will happen if the # user presses <Return> (and helps our code know what # to do if the user presses return) if {$options(-editable)} { $widgets(listbox) see 0 $widgets(listbox) selection clear 0 end $widgets(listbox) selection anchor 0 $widgets(listbox) activate 0 } } "<FocusIn>" { set oldValue [$widgets(entry) get] } "<FocusOut>" { if {![winfo ismapped $widgets(dropdown)]} { # did the value change? set newValue [$widgets(entry) get] if {$oldValue != $newValue} { CallCommand $widgets(this) $newValue } } } "<1>" { set editable [::combobox::GetBoolean $options(-editable)] if {!$editable} { if {[winfo ismapped $widgets(dropdown)]} { $widgets(this) close return -code break; } else { if {$options(-state) != "disabled"} { $widgets(this) open return -code break; } } } } "<Double-1>" { if {$options(-state) != "disabled"} { $widgets(this) toggle return -code break; } } "<Tab>" { if {[winfo ismapped $widgets(dropdown)]} { ::combobox::Find $widgets(this) 0 return -code break; } else { ::combobox::SetValue $widgets(this) [$widgets(this) get] } } "<Escape>" {# $widgets(entry) delete 0 end# $widgets(entry) insert 0 $oldValue if {[winfo ismapped $widgets(dropdown)]} { $widgets(this) close return -code break; } } "<Return>" { # did the value change? set newValue [$widgets(entry) get] if {$oldValue != $newValue} { CallCommand $widgets(this) $newValue } if {[winfo ismapped $widgets(dropdown)]} { ::combobox::Select $widgets(this) \ [$widgets(listbox) curselection] return -code break; } } "<Next>" { $widgets(listbox) yview scroll 1 pages set index [$widgets(listbox) index @0,0] $widgets(listbox) see $index $widgets(listbox) activate $index $widgets(listbox) selection clear 0 end $widgets(listbox) selection anchor $index $widgets(listbox) selection set $index } "<Prior>" { $widgets(listbox) yview scroll -1 pages set index [$widgets(listbox) index @0,0] $widgets(listbox) activate $index $widgets(listbox) see $index $widgets(listbox) selection clear 0 end $widgets(listbox) selection anchor $index $widgets(listbox) selection set $index } "<Down>" { if {[winfo ismapped $widgets(dropdown)]} { ::combobox::tkListboxUpDown $widgets(listbox) 1 return -code break; } else { if {$options(-state) != "disabled"} { $widgets(this) open return -code break; } } } "<Up>" { if {[winfo ismapped $widgets(dropdown)]} { ::combobox::tkListboxUpDown $widgets(listbox) -1 return -code break; } else { if {$options(-state) != "disabled"} { $widgets(this) open return -code break; } } } } return ""}# ::combobox::DestroyHandler {w} --# # Cleans up after a combobox widget is destroyed## Arguments:## w widget pathname## Results:## The namespace that was created for the widget is deleted,# and the widget proc is removed.proc ::combobox::DestroyHandler {w} { catch { # if the widget actually being destroyed is of class Combobox, # remove the namespace and associated proc. if {[string compare [winfo class $w] "Combobox"] == 0} { # delete the namespace and the proc which represents # our widget namespace delete ::combobox::$w rename $w {} } } return ""}# ::combobox::Find## finds something in the listbox that matches the pattern in the# entry widget and selects it## N.B. I'm not convinced this is working the way it ought to. It# works, but is the behavior what is expected? I've also got a gut# feeling that there's a better way to do this, but I'm too lazy to# figure it out...## Arguments:## w widget pathname# exact boolean; if true an exact match is desired## Returns:## Empty stringproc ::combobox::Find {w {exact 0}} { upvar ::combobox::${w}::widgets widgets upvar ::combobox::${w}::options options ## *sigh* this logic is rather gross and convoluted. Surely ## there is a more simple, straight-forward way to implement ## all this. As the saying goes, I lack the time to make it ## shorter... # use what is already in the entry widget as a pattern set pattern [$widgets(entry) get] if {[string length $pattern] == 0} { # clear the current selection $widgets(listbox) see 0 $widgets(listbox) selection clear 0 end $widgets(listbox) selection anchor 0 $widgets(listbox) activate 0 return } # we're going to be searching this list... set list [$widgets(listbox) get 0 end] # if we are doing an exact match, try to find, # well, an exact match set exactMatch -1 if {$exact} { set exactMatch [lsearch -exact $list $pattern] } # search for it. We'll try to be clever and not only # search for a match for what they typed, but a match for # something close to what they typed. We'll keep removing one # character at a time from the pattern until we find a match # of some sort. set index -1 while {$index == -1 && [string length $pattern]} { set index [lsearch -glob $list "$pattern*"] if {$index == -1} { regsub {.$} $pattern {} pattern } } # this is the item that most closely matches... set thisItem [lindex $list $index] # did we find a match? If so, do some additional munging... if {$index != -1} { # we need to find the part of the first item that is # unique WRT the second... I know there's probably a # simpler way to do this... set nextIndex [expr {$index + 1}] set nextItem [lindex $list $nextIndex] # we don't really need to do much if the next # item doesn't match our pattern... if {[string match $pattern* $nextItem]} { # ok, the next item matches our pattern, too # now the trick is to find the first character # where they *don't* match... set marker [string length $pattern] while {$marker <= [string length $pattern]} { set a [string index $thisItem $marker] set b [string index $nextItem $marker] if {[string compare $a $b] == 0} { append pattern $a incr marker } else { break } } } else { set marker [string length $pattern] } } else { set marker end set index 0 } # ok, we know the pattern and what part is unique; # update the entry widget and listbox appropriately
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -