Python Forum

Full Version: tkinter text widget word wrap position
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hi,

I'm trying to figure out the index position on where the tkinter text widget cuts of the word when using WORD wrap. I thought the newline '\n' would indicate this, but I was wrong. If you see the example below (I replaced '\n' with '$'), you see the cut off word is not indicated by a '\n'.

Is there any way I can detect the index of the cut off position? I would like to use this to create a text editor with line numbers when using WORD wrap.

from tkinter import *
from decimal import Decimal

class app:
    def __init__(self, master):
        self.master = master

        master.title("PyEditor")
        master.geometry("720x180")

        master.grid_columnconfigure(1, weight=1)
        master.grid_rowconfigure(0, weight=1)

        self.lineframe = LineNumberFrame(master)
        self.lineframe.grid(row=0, column=0)
        
        self.edit_space = CustomText(master, wrap= WORD)
        self.edit_space.grid(row=0, column=1, sticky=NSEW)
        self.lineframe.attach(self.edit_space)
        self.edit_space.bind("<<TextModified>>", self._on_change)
        
        self.edit_scroll = Scrollbar(master, orient = VERTICAL, command = self._multi_scroll )
        
        self.edit_scroll.grid(row = 0, column = 2, sticky=NS)
        
        
        self.edit_space.configure(yscrollcommand = self._scroll1)
        self.lineframe.line_numbers.configure(yscrollcommand = self._scroll2)
        
        master.bind_all("<MouseWheel>", self._on_mousewheel)
        
    def _scroll1(self, *args):
        if self.lineframe.line_numbers.yview() != self.edit_space.yview():
            self.lineframe.line_numbers.yview_moveto(args[0])
            
        self.edit_scroll.set(*args)
        
    def _scroll2(self, *args):
        if self.edit_space.yview() != self.lineframe.line_numbers.yview():
            self.edit_space.yview_moveto(args[0])
            
        self.edit_scroll.set(*args)
        
    def _on_change(self, event):
        self.lineframe.redraw()
        
    def _multi_scroll(self, *args):
        self.edit_space.yview(*args)
        self.lineframe.line_numbers.yview(*args)
        
    
        
    def _on_mousewheel(self,event):
        self.edit_space.yview("scroll", -1 * (event.delta), "units")
        self.lineframe.line_numbers.yview("scroll", -1 * (event.delta), "units")

class LineNumberFrame(Frame):    
    def __init__(self, master: Tk = None):
        super().__init__(master, width = 1)
        self.master = master
        self.grid(row = 0, column = 0, sticky="nsew")
        self.create_widgets()
        
    def create_widgets(self):
        self.line_numbers = Text(self.master, width = 3)
        self.line_numbers.config(state = DISABLED)
        self.line_numbers.grid(row=0, column=0, sticky=NSEW)
        
    def attach(self, text_widget):
        self.text_widget = text_widget
        
    def redraw(self):
        self.line_numbers.config(state = NORMAL)
        self.line_numbers.delete("1.0", END)
        
                
        i = 1.0
        lastline = self.text_widget.index("end")
        
        is_first = True

        while True :
            if Decimal(i).compare(Decimal(lastline)) >= 0:
                break

            linenum = str(i).split(".")[0]          
            print("idx: " + self.text_widget.get(str(i) + " linestart", str(i) + " lineend+1c").replace("\n", "$"))

            if is_first == True:
                self.line_numbers.insert(END, linenum)
                is_first = False
            else:
                self.line_numbers.insert(END, "\n" + linenum)

            i = self.text_widget.index("%s+1line" % i)
            
        self.line_numbers.config(state = DISABLED)
        
        

class CustomText(Text):
    def __init__(self, *args, **kwargs):
        """A text widget that report on internal widget commands"""
        Text.__init__(self, *args, **kwargs)

        # create a proxy for the underlying widget
        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    def _proxy(self, command, *args):
        cmd = (self._orig, command) + args
        result = self.tk.call(cmd)
        
       

        if command in ("insert", "delete", "replace"):
            self.event_generate("<<TextModified>>")

        return result
        
        

root = Tk()
my_gui = app(root)

# Insert some text
my_gui.edit_space.insert(END, "A very long line to see how the word wrap works. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.\n")

for x in range(2,10):   
    my_gui.edit_space.insert(END, "Line " + str(x) +"\n")

root.mainloop()
Thanks
The text window described here has an expandable geometry, so the wrap position varies as the window is expanded and/or contracted.

There are some command attributes that have some control over padding (spacing1, spacing2 and spacing3), as well as some modifiers in the wrap command:
Quote:wrap
This option controls the display of lines that are too wide.
  • With the default behavior, wrap=tk.CHAR, any line that gets too long
    will be broken at any character.
  • Set wrap=tk.WORD and it will break the line after the last word that will
    fit.
  • If you want to be able to create lines that are too long to fit in the window,
    set wrap=tk.NONE and provide a horizontal scrollbar.

If you don't already have a copy, I recommend downloading John Shipman's tkinter manual here: https://reu.cct.lsu.edu/documents/Python...kinter.pdf

There are other commands that may help, starting on page 82
(Mar-17-2021, 10:15 AM)Larz60+ Wrote: [ -> ]The text window described here has an expandable geometry, so the wrap position varies as the window is expanded and/or contracted.

There are some command attributes that have some control over padding (spacing1, spacing2 and spacing3), as well as some modifiers in the wrap command:
Quote:wrap
This option controls the display of lines that are too wide.
  • With the default behavior, wrap=tk.CHAR, any line that gets too long
    will be broken at any character.
  • Set wrap=tk.WORD and it will break the line after the last word that will
    fit.
  • If you want to be able to create lines that are too long to fit in the window,
    set wrap=tk.NONE and provide a horizontal scrollbar.

If you don't already have a copy, I recommend downloading John Shipman's tkinter manual here: https://reu.cct.lsu.edu/documents/Python...kinter.pdf

There are other commands that may help, starting on page 82

I've tried several methods, like 'dlineinfo' and 'bbox', but none of them provide me with the necessary info...
I think dlineinfo provides what ypu need, the vertical position of each line, but you need to use place instead of grid to position the line numbers. Even without word wrap this would be necessary to support rich text.
(Mar-17-2021, 08:45 PM)deanhystad Wrote: [ -> ]I think dlineinfo provides what ypu need, the vertical position of each line, but you need to use place instead of grid to position the line numbers. Even without word wrap this would be necessary to support rich text.

Thx, I was looking at the wrong attribute of dlineinfo.

A question though, what makes 'place' exactly better than 'grid' for the line numbers in this case?
Grid does not let you specify location, only relative location. Line number 11 might be 13 or 26 pixels below line number 10 depending on if line 10 wraps. I suppose you could add an empty label to the line number grid, but you'd have to keep track of that somehow.
Ok, but I'm using a text widget in which I display the line numbers. So that's probably my problem...

Thanks!