Welcome, guest | Sign In | My Account | Store | Cart

The Text widget resize to its contents. Based on this: https://stackoverflow.com/questions/11544187/tkinter-resize-text-to-contents

But here there is one important improvement: The widget resize first, before a character inclusion.

For this trick I have had to use tag bindings.

Here there is a practical application of this widget:

https://code.activestate.com/recipes/578885-draggable-desktop-note-or-sticky-notes-in-tkinter/

Python, 152 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# Version: 1.2
# Author: Miguel Martinez Lopez
# Uncomment the next line to see my email
# print "Author's email: ", "61706c69636163696f6e616d656469646140676d61696c2e636f6d".decode("hex")

try:
    from Tkinter import Frame, Text
    from Tkconstants import *
    import tkFont
except ImportError:
    from tkinter import Frame, Text
    from tkinter.constants import *
    from tkinter import font as tkFont

class AutoResizedText(Frame):
    def __init__(self, master, width=0, height=0, family=None, size=None,*args, **kwargs):
        
        Frame.__init__(self, master, width = width, height= height)
        self.pack_propagate(False)

        self._min_width = width
        self._min_height = height

        self._textarea = Text(self, *args, **kwargs)
        self._textarea.pack(expand=True, fill='both')

        if family != None and size != None:
            self._font = tkFont.Font(family=family,size=size)
        else:
            self._font = tkFont.Font(family=self._textarea.cget("font"))

        self._textarea.config(font=self._font)

        # I want to insert a tag just in front of the class tag
        # It's not necesseary to guive to this tag extra priority including it at the beginning
        # For this reason I am making this search
        self._autoresize_text_tag = "autoresize_text_"+str(id(self))
        list_of_bind_tags = list(self._textarea.bindtags())
        list_of_bind_tags.insert(list_of_bind_tags.index('Text'), self._autoresize_text_tag)

        self._textarea.bindtags(tuple(list_of_bind_tags))
        self._textarea.bind_class(self._autoresize_text_tag, "<KeyPress>",self._on_keypress)

    def _on_keypress(self, event):
        self._textarea.focus_set()
        
        if event.keysym == 'BackSpace':
            self._textarea.delete("%s-1c" % INSERT)
            new_text = self._textarea.get("1.0", END)
        elif event.keysym == 'Delete':
            self._textarea.delete("%s" % INSERT)
            new_text = self._textarea.get("1.0", END)
        # We check whether it is a punctuation or normal key
        elif len(event.char) == 1:
            if event.keysym == 'Return':
                # In this situation ord(event.char)=13, which is the CARRIAGE RETURN character
                # We want instead the new line character with ASCII code 10
                new_char = '\n'
            else:
                new_char = event.char


            old_text = self._textarea.get("1.0", END)
            new_text = self._insert_character_into_message(old_text, self._textarea.index(INSERT), new_char)

        else:
            # If it is a special key, we continue the binding chain
            return
    
        # Tk Text widget always adds a newline at the end of a line
        # This last character is also important for the Text coordinate system
        new_text = new_text[:-1]

        self._fit_to_size_of_text(new_text)

        # Finally we insert the new character
        if event.keysym != 'BackSpace' and event.keysym != 'Delete':
            self._textarea.insert(INSERT, new_char)
        
        return "break"

    def _insert_character_into_message(self, message, coordinate, char):
        target_row, target_column = map( int, coordinate.split('.'))

        this_row = 1
        this_column = 0
        index = 0

        for ch in message:
            if this_row == target_row and this_column == target_column:
                message = message[:index] + char + message[index:]
                return message

            index += 1

            if ch == '\n':
                this_row += 1
                this_column = 0
            else:
                this_column += 1

    def _fit_to_size_of_text(self, text):
        number_of_lines = 0
        widget_width = 0

        for line in text.split("\n"):
            widget_width = max(widget_width,self._font.measure(line))
            number_of_lines += 1

        # We need to add this extra space to calculate the correct width
        widget_width += 2*self._textarea['bd'] + 2*self._textarea['padx'] + self._textarea['insertwidth']

        if widget_width < self._min_width:
            widget_width = self._min_width

        self._textarea.configure(height=number_of_lines)
        widget_height = max(self._textarea.winfo_reqheight(), self._min_height)

        self.config(width=widget_width, height=widget_height)

        # If we don't call update_idletasks, the window won't be resized before an insertion
        self.update_idletasks()

    @property
    def tag(self):
        return self._autoresize_text_tag

    def focus(self):
        self._textarea.focus()
        
    def bind(self, event, handler, add=None):
        self._textarea.bind(event, handler, add)

    def get(self, start, end=None):
        return self._textarea.get(start, end)

    def update(self, text):
        self._textarea.delete('1.0', 'end')        
        self._fit_to_size_of_text(text)
        self._textarea.insert('1.0', text)

if __name__ == '__main__':
    try:
        from Tkinter import Tk
    except ImportError:
        from tkinter import Tk

    root = Tk()
    auto_text = AutoResizedText(root, family="Arial",size=15, width = 100, height = 50)
    auto_text.pack()
    auto_text.focus()
    root.mainloop()

1 comment

Amirhosein Nikfal 7 years, 5 months ago  # | flag

Nice, Thank you.