MG Apr 25th 2010 - After putting it off for a while, I've finally had to give in and add a spellchecker to one of my apps. Being too lazy to actually write the spellchecking code myself, it uses Aspell for it (path near the top; you could use something else that returned data in the same format, like ispell). Here's the code, for a slightly MS-Wordish looking spellcheck widget (using ttk themed windows, and pretty much my first mildly complex use of grid instead of pack, which seems to have worked OK). Screenshot taken on Win XP.
DDG Hunspell can be used as well, just replace aspell -a with hunspell -a. Further package require Tk added to avoid errors if using tclsh on Linux. Still problems of lcoing the pipe after finishing the apllication.
The command
spellcheck::spellcheck $string
launches the widget and spellchecks the given string, and returns a new string with any replacements/fixes made when the Done button is clicked (or the original string, if Cancel is clicked). If the options(grab) variable is set true, a local grab is performed when the spellcheck widget is up, stopping button/keypresses in other windows.
package require Tk namespace eval ::spellcheck { variable options; variable spellcheck; # DDG - check for spell checker if {[auto_execok "aspell"] eq "" && [auto_execok "hunspell"] eq ""} { error "Either aspell or hunspell must be installed and the binaries must be in the path!" } if {[auto_execok "aspell"] ne ""} { set options(aspell) "aspell -a" } else { set options(aspell) "hunspell -a" } # on Windows you might manually set the path #set options(aspell) "e:/progra~1/aspell/bin/aspell.exe -a" # set options(hunspell) "e:/hunspell/hunspell.exe -a" # should work as well set options(grab) 0 } proc ::spellcheck::spellcheck {string} { variable spellcheck; variable options; array unset spellcheck; set win .spellcheck if { [winfo exists $win] } { raise $win bell -displayof $win; return; } set spellcheck(win) $win set spellcheck(string) $string toplevel $win wm title $win "Spellchecker" pack [set frame [::ttk::frame $win.frame]] -side left -anchor nw -expand 1 -fill both -pady 8 if { $options(grab) } { grab set $win bind $frame <Destroy> [list grab release $win];# don't think this is necessary, but better safe than sorry } # DDG: can't pack and then grid, so removed pack ::ttk::label $frame.top_label -text "Words:" set left [::ttk::frame $frame.left] pack [set top [::ttk::frame $left.top]] -expand 1 -fill both pack [set input [text $top.txt -width 50 -height 7 -wrap word -yscrollcommand "$top.sb set"]] -side left -expand 1 -fill both pack [::ttk::scrollbar $top.sb -command "$input yview"] -side left -fill y ::ttk::label $frame.bottom_label -text "Suggestions:" # DDG: fix, can't first pack and then grid with modern Tk set bottom [::ttk::frame $frame.bottom] pack [set tree [::ttk::treeview $bottom.tree -style "Spell.Treeview" -columns Suggestion -show {} -yscrollcommand "$bottom.sb set" -height 6 -selectmode browse]] -side left -anchor nw -expand 1 -fill both $tree column Suggestion -width 50 pack [::ttk::scrollbar $bottom.sb -command "$tree yview"] -side left -fill y set right [::ttk::frame $frame.right] foreach {btn letter} [list "Done" d "Cancel" c "Replace" r "Ignore" i] { grid [::ttk::frame $right.frame$btn] -sticky ew -padx 2 -pady 8 pack [::ttk::frame $right.frame$btn.sub] -side left -anchor center pack [::ttk::button $right.frame$btn.sub.b -text $btn -underline 0] -anchor center bind $win <KeyPress-$letter> [list $right.frame$btn.sub.b invoke] } $right.frameDone.sub.b configure -command [list ::spellcheck::finish 1] $right.frameCancel.sub.b configure -command [list ::spellcheck::finish 0] $right.frameReplace.sub.b configure -command [list ::spellcheck::replaceWord 1] $right.frameIgnore.sub.b configure -command [list ::spellcheck::replaceWord 0] $right.frameReplace.sub.b state disabled $right.frameIgnore.sub.b state disabled grid $frame.top_label -sticky w -padx 6 grid $left $right -sticky nsew -pady 4 -padx 6 grid $frame.bottom_label -sticky w -padx 6 grid $bottom -sticky nsew -pady 4 -padx 6 grid rowconfigure $frame $left -weight 1 grid rowconfigure $frame $bottom -weight 1 grid columnconfigure $frame 0 -weight 1 set spellcheck(replace) $right.frameReplace.sub.b set spellcheck(ignore) $right.frameIgnore.sub.b set spellcheck(tree) $tree set spellcheck(input) $input $tree tag configure wrong -font [list {*}[font actual [ttk::style lookup Treeview -font]] -slant italic] $tree state disabled $input tag configure checking -background darkblue $input tag configure wrong -underline 1 -foreground red $input insert end $string $tree insert {} end -values [list "Please wait. Checking spelling..."] -tags wrong bind $tree <ButtonPress-3> {if {[%W instate !disabled]} {::spellcheck::rightClickTree %X %Y %x %y}} bind $tree <Double-1> {if {[%W instate !disabled]} {::spellcheck::doubleClickTree %x %y}} wm minsize $win [winfo reqwidth $win] [winfo reqheight $win] set lineNum 1 set total 0 set wroung 0 foreach line [split $string "\n"] { set corrections [checkSpelling $line] foreach {count corrections} $corrections {break;} if { $count == 0 } { $tree delete [$tree children {}] $tree insert {} end -values [list "Unable to spell-check text: $spellcheck(error)"] -tags wrong set total -1 $right.frameDone.sub.b state disabled break; } else { foreach x $corrections { incr total foreach {start word suggestions} $x {break;} incr start -1 $input tag add wrong "$lineNum.0 + $start chars" "$lineNum.0 + [expr {$start + [string length $word]}] chars" set spellcheck(suggestions,$word) $suggestions } } incr lineNum } $input configure -state disabled if { $total == 0 } { tk_messageBox -icon info -parent $top -message "All words are spelled correctly." destroy $win; return $spellcheck(string); } elseif { $total > 0 } { $tree delete [$tree children {}] $tree insert {} end -values [list "Click a misspelled word to begin"] -tags wrong $input tag bind wrong <Button-1> [list ::spellcheck::suggest] $input tag bind wrong <Enter> [list $input configure -cursor hand2] $input tag bind wrong <Leave> [list $input configure -cursor xterm] } vwait ::spellcheck::spellcheck(string) destroy $win; return $spellcheck(string); };# ::spellcheck::spellcheck proc ::spellcheck::doubleClickTree {x y} { variable spellcheck; set what [lindex [$spellcheck(tree) identify $x $y] 0] if { $what eq "cell" } { ::spellcheck::replaceWord 1 } };# ::spellcheck::doubleClickTree proc ::spellcheck::rightClickTree {X Y x y} { variable spellcheck; event generate $spellcheck(tree) <ButtonPress-1> -rootx $X -rooty $Y -x $x -y $y set m .m catch {destroy $m} menu $m -tearoff 0 $m add command -label "Use this word" -command [list ::spellcheck::replaceWord 1] $m add command -label "Copy word to Clipboard" -command [list ::spellcheck::copyTreeWord] tk_popup $m $X $Y };# ::spellcheck::rightClickTree proc ::spellcheck::copyTreeWord {} { variable spellcheck; set word [lindex [$spellcheck(tree) item [$spellcheck(tree) selection] -value] 0] clipboard clear -displayof $spellcheck(tree) clipboard append -displayof $spellcheck(tree) $word };# ::spellcheck::copyTreeWord proc ::spellcheck::suggest {{index "current"}} { variable spellcheck; $spellcheck(input) tag remove checking 1.0 end # Get word set range [$spellcheck(input) tag prevrange "wrong" "$index + 1 char"] $spellcheck(input) tag add "checking" {*}$range set word [$spellcheck(input) get checking.first checking.last] $spellcheck(tree) delete [$spellcheck(tree) children {}] if { ![info exists spellcheck(suggestions,$word)] || ![llength $spellcheck(suggestions,$word)] } { $spellcheck(tree) insert {} end -values [list "None"] -tags wrong $spellcheck(tree) state disabled $spellcheck(replace) state disabled } else { $spellcheck(tree) state !disabled foreach x $spellcheck(suggestions,$word) { $spellcheck(tree) insert {} end -values [list "$x"] } set first [lindex [$spellcheck(tree) children {}] 0] $spellcheck(tree) see $first $spellcheck(tree) selection set $first $spellcheck(tree) focus $first $spellcheck(replace) state !disabled } $spellcheck(ignore) state !disabled return; };# ::spellcheck::suggest proc ::spellcheck::replaceWord {replace} { variable spellcheck; $spellcheck(input) configure -state normal set index [$spellcheck(input) index checking.first] if { $replace } { set orig [$spellcheck(input) get checking.first checking.last] set new [lindex [$spellcheck(tree) item [$spellcheck(tree) selection] -values] 0] $spellcheck(input) delete checking.first checking.last $spellcheck(input) insert $index $new } else { $spellcheck(input) tag remove wrong checking.first checking.last $spellcheck(input) tag remove checking checking.first checking.last } $spellcheck(input) configure -state disabled $spellcheck(replace) state disabled $spellcheck(ignore) state disabled if { ![llength [set next [$spellcheck(input) tag nextrange wrong $index end]]] && ![llength [set next [$spellcheck(input) tag nextrange wrong 1.0 $index]]] } { # All words now spelled correctly set ans [tk_messageBox -parent $spellcheck(win) -icon info -title "Spellcheck" -type yesno \ -message "All words are now spelled correctly. Finished?"] if { $ans eq "yes" } { ::spellcheck::finish 1 } return; } else { $spellcheck(tree) delete [$spellcheck(tree) children {}] $spellcheck(tree) insert {} end -values [list "Click a misspelled word to begin"] -tags wrong suggest [lindex $next 0] } return; };# ::spellcheck::replaceWord proc ::spellcheck::finish {use} { variable spellcheck; if { $use } { set spellcheck(string) [$spellcheck(input) get 1.0 end-1c] } else { set spellcheck(string) $spellcheck(string);# no change, but set to itself to trigger vwait } return; };# ::spellcheck::finish proc ::spellcheck::checkSpelling {string} { variable options; variable spellcheck; if { [catch {open "|$options(aspell)" r+} pipe] } { set spellcheck(error) $pipe return [list 0 [list]]; } set spellcheck(error) "" fconfigure $pipe -buffering line gets $pipe ;# skip version line fconfigure $pipe -blocking 0 set return [list] puts $pipe "^$string" after 500 [list ::spellcheck::checkSpellingSub $pipe] vwait ::spellcheck::spellcheck(result) return $::spellcheck::spellcheck(result); };# ::spellcheck::checkSpelling proc ::spellcheck::checkSpellingSub {pipe} { set i 0 while { [set count [gets $pipe line]] >= 0 } { incr i if { $count == 0 || $line eq "*" } { continue; # nothing of interest } set list [split $line " "] if { [lindex $list 0] eq "#" } { # No suggestions lappend return [list [lindex $list 2] [lindex $list 1]] } else { lappend return [list [string range [lindex $list 3] 0 end-1] [lindex $list 1] [split [string map [list "," ""] [join [lrange $list 4 end] " "]] " "]] } } close $pipe set ::spellcheck::spellcheck(result) [list $i $return]; return; };# ::spellcheck::checkSpellingSub # Stop disabled treeview widgets responding to input foreach x [list Control-Button-1 Shift-Button-1 Key-space Key-Return Key-Left Key-Right \ Key-Down Key-Up B1-Motion Double-Button-1 ButtonRelease-1 Button-1] { bind Treeview <$x> [format {if { ![%%W instate disabled] } { %s }} [bind Treeview <$x>]] } # Handy debugging proc proc winover {} { return [winfo containing {*}[winfo pointerxy .]]; } catch { console eval {.console configure -font [list "Courier New" 10]} console show } # Run it set myTestString "The quik brown fox jumps over the layzee dog\nJackdaws lov my big sphincks of quortz\nFoo bar baz boing sprocket kablooey." puts "Input: $myTestString" puts "\nOutput: [::spellcheck::spellcheck $myTestString]"
The code for actually passing the words to aspell and reading them is slightly modified from an example on open, which did the same with ispell. Any comments/suggestions appreciated.
DDG - 2022-01-19 - modern Tcl/Tk 8.6/8.7 complains about pack and then grid the same widget, I as well used automatic setting for the spell checker. BTW: It is not a real widget which you could embed into your application, but the could here could be used as the base to crate one.