A Text Widget With Line Numbers

Here is a prototype of a tkinter text widget that displays line numbers and copes well with wrapped lines.

I have taken a lazy way out and update the line numbers using a periodic interrupt. This saves having to monitor every action on the text widget to see if the line numbers have changed.

The line numbers are displayed in a separate Text widget, so normal editing operations on the main widget do not have to take account of line numbers.

Screenshot showing three instances of the widget in a PanedWindow

linenumbers.png

   1 """A wrapper to show line numbers for Tkinter Text widgets.
   2 
   3 Features:
   4 
   5  - Line numbers are displayed in a seperate Text widget.
   6   
   7  - Line number display is entirely automatic.
   8   
   9  - Text in the main Text widget can be manipulated normally,
  10    without regard to line numbers.
  11 
  12  - Line numbers are displayed correctly for wrapped lines.
  13   
  14 Drawbacks:
  15 
  16  - The height of each line in the main Text widget must all
  17    be the same.
  18 
  19  - The height of the lines in the line number display Text
  20    widget must be the same as for the main Text widget.
  21 
  22  - There is a slight delay in line numbers catching up with reality.
  23    This is most noticable when fast scrolling is taking place.
  24 
  25 """
  26 
  27 __version__ = 0.2
  28 __date__    = "2009-07-25"
  29 __author__  = "robert@pytrash.co.uk"
  30 __licence__ = "Public Domain"
  31 
  32 
  33 __changelog__ = (
  34 
  35 ('2009-07-25', '0.2', 'PyTrash',
  36 
  37 """Fixed bugs, improved efficiency, added PanedWindow to demo."""),
  38 
  39 ('2009-07-24', '0.1', 'PyTrash',
  40 
  41 """Initial version."""),
  42  
  43 )
  44 
  45 
  46 from Tkinter import *
  47 
  48 root = Tk()
  49 
  50 class EditorClass(object):
  51 
  52     UPDATE_PERIOD = 100 #ms
  53 
  54     editors = []
  55     updateId = None
  56 
  57     def __init__(self, master):
  58         
  59         self.__class__.editors.append(self)
  60 
  61         self.lineNumbers = ''
  62 
  63         # A frame to hold the three components of the widget.
  64         self.frame = Frame(master, bd=2, relief=SUNKEN)
  65 
  66         # The widgets vertical scrollbar
  67         self.vScrollbar = Scrollbar(self.frame, orient=VERTICAL)
  68         self.vScrollbar.pack(fill='y', side=RIGHT)
  69 
  70         # The Text widget holding the line numbers.
  71         self.lnText = Text(self.frame,
  72                 width = 4,
  73                 padx = 4,
  74                 highlightthickness = 0,
  75                 takefocus = 0,
  76                 bd = 0,
  77                 background = 'lightgrey',
  78                 foreground = 'magenta',
  79                 state='disabled'
  80         )
  81         self.lnText.pack(side=LEFT, fill='y')
  82 
  83         # The Main Text Widget
  84         self.text = Text(self.frame,
  85                 width=16,
  86                 bd=0,
  87                 padx = 4,
  88                 undo=True,
  89                 background = 'white'
  90         )
  91         self.text.pack(side=LEFT, fill=BOTH, expand=1)
  92 
  93         self.text.config(yscrollcommand=self.vScrollbar.set)
  94         self.vScrollbar.config(command=self.text.yview)
  95 
  96         if self.__class__.updateId is None:
  97             self.updateAllLineNumbers()
  98    
  99     def getLineNumbers(self):
 100         
 101         x = 0
 102         line = '0'
 103         col= ''
 104         ln = ''
 105         
 106         # assume each line is at least 6 pixels high
 107         step = 6
 108         
 109         nl = '\n'
 110         lineMask = '    %s\n'
 111         indexMask = '@0,%d'
 112         
 113         for i in range(0, self.text.winfo_height(), step):
 114             
 115             ll, cc = self.text.index( indexMask % i).split('.')
 116 
 117             if line == ll:
 118                 if col != cc:
 119                     col = cc
 120                     ln += nl
 121             else:
 122                 line, col = ll, cc
 123                 ln += (lineMask % line)[-5:]
 124 
 125         return ln
 126 
 127     def updateLineNumbers(self):
 128 
 129         tt = self.lnText
 130         ln = self.getLineNumbers()
 131         if self.lineNumbers != ln:
 132             self.lineNumbers = ln
 133             tt.config(state='normal')
 134             tt.delete('1.0', END)
 135             tt.insert('1.0', self.lineNumbers)
 136             tt.config(state='disabled')
 137         
 138     @classmethod
 139     def updateAllLineNumbers(cls):
 140 
 141         if len(cls.editors) < 1:
 142             cls.updateId = None
 143             return
 144         
 145         for ed in cls.editors:
 146             ed.updateLineNumbers()
 147         
 148         cls.updateId = ed.text.after(
 149             cls.UPDATE_PERIOD,
 150             cls.updateAllLineNumbers)
 151 
 152 
 153 def demo(noOfEditors, noOfLines):
 154 
 155     pane = PanedWindow(root, orient=HORIZONTAL, opaqueresize=True)
 156 
 157     for e in range(noOfEditors):
 158         ed = EditorClass(root)
 159         pane.add(ed.frame)
 160 
 161     s = 'line ................................... %s'
 162     s = '\n'.join( s%i for i in xrange(1, noOfLines+1) )
 163     
 164     for ed in EditorClass.editors:
 165         ed.text.insert(END, s)
 166 
 167     pane.pack(fill='both', expand=1)
 168 
 169     root.title("Example - Line Numbers For Text Widgets")
 170 
 171 
 172 if __name__ == '__main__':
 173 
 174     demo(3, 9999)
 175     mainloop()

tkinter: A_Text_Widget_with_Line_Numbers (last edited 2010-07-26 11:59:11 by localhost)