Tkinter label height to fit content

  • Thread starter Bart Kastermans
  • Start date
B

Bart Kastermans

I have a label into which I am going to put content of different
sizes. I would like to know how high I need to make the label so that I
can size the window so it can stay the same for the different content
sizes. I have a strategy, but it seems more complicated then it should
be.

I want to set a label to a given width and wraplength:

l = Label(root)
l['width'] = 30
l['wraplength'] = 244
l['text'] = "testing this"

Now I want to query the label to find how many lines are
used. l['height'] stays at 0, so the best I have been able to come up
with is to use l.winfo_height() and convert the height given in pixels
to the number of lines used. Nothing in dir(l) seems to give me the
information directly, but this strategy is fragile to font changes and
other changes.

Any suggestions?
 
B

Bart Kastermans

rantingrick said:
Yeah, have you considered using the "linespace()" method of tk.Font
objects to calculate the height? Although i must say it "feels" as if
your doing something you should not need to do, however i cannot be
sure without knowing more about this GUI. Sounds a lot like trying to
put socks on a rooster.

http://infohost.nmt.edu/tcc/help/pubs/tkinter/std-attrs.html#fonts

Thx. That function should allow for a bit of robustness.

I get bits of information over RSS, these are of varying length. I
want to show 10 at a time, and scroll through them. Now when I
scroll the window grows and shrinks depending on their size, I want
to right from the start make it high enough to contain even the
biggest that will have to be shown. So the question is determining
the height parameter for the labels ahead of time. My strategy has
been to put all in labels and then try to get the information from
the label of how high it needs to be made at a certain width.
 
R

rantingrick

I get bits of information over RSS, these are of varying length.  I
want to show 10 at a time, and scroll through them.  Now when I
scroll the window grows and shrinks depending on their size, I want
to right from the start make it high enough to contain even the
biggest that will have to be shown.  So the question is determining
the height parameter for the labels ahead of time.  My strategy has
been to put all in labels and then try to get the information from
the label of how high it needs to be made at a certain width.

I see. However i might opt instead for a text widget with columns of
wrapped text. You could use the textwrap.py module to help (although
you'll have to work around it's shortcomings for paragraphs and such).
In any event it's difficult to offer good advice without seeing the
code directly.
 
R

rantingrick

Thx.  That function should allow for a bit of robustness.

Correction. The function is actually "tkFont.metrics(arg)" which takes
"linespace" as an optional argument.
 
B

Bart Kastermans

I build on the suggestion by rantingrick, but took it in a bit
different direction.

I now have working code that performs reasonable. The reason for
the class lines (as opposed to just a function) is b/c font.measure
appears not that fast. So I want to remember between different
calls to lines.count where the cutoff was, and then start looking
from there. This one step of "optimization" was enough to make it
run reasonable on my system.

There are one important thing skipped, Tkinter.Label takes whitespace
into account. This code does not yet. Also I just hacked this
together as an example solution to work further from.

import Tkinter as Tk
import tkFont
import random
import sys

def genstr (j):
rno = random.randint(4,50)
ret_val = str(j) + ":"
for i in range (0, rno):
ret_val += "hello" + str(i)
return ret_val

def gendata (lh):
ret_val = []
for i in range(0,lh):
ret_val.append (genstr (i))
return ret_val

data = gendata (100)

root = Tk.Tk()
font = tkFont.Font(family='times', size=13)

class lines:
def __init__ (self):
self.lastct = 1 # remember where the cutoff was last work from there

def count (self, text, cutoff = 400):
global font
no_lines = 1
start_idx = 0
idx = self.lastct

while True:
if idx > len (text):
idx = len (text)

# shrink from guessed value
while font.measure (text[start_idx:idx - 1]) > cutoff:
if idx <= start_idx:
print "error"
sys.exit ()
else:
idx -= 1
self.lastct = idx - start_idx # adjust since was too big

# increase from guessed value (note: if first shrunk then done)
while (idx < len (text)
and font.measure (text[start_idx:idx]) < cutoff):
idx += 1
self.lastct = idx - start_idx # adjust since was too small

# next line has been determined
print "*" + text[start_idx:idx-1] + "*"
if idx == len(text) and font.measure (text[start_idx:]) < cutoff:
return no_lines
elif idx == len(text):
return no_lines + 1
else:
no_lines += 1
start_idx = idx - 1
idx = start_idx + self.lastct

lin = lines()

# for testing speed compute for all data
for i in range(0,len(data)):
lin.count(data, 450)

# show only first 10
for i in range(0,min(len(data),10)):
l = Tk.Label(root)
l.pack()
l['text'] = data
print i
no = lin.count (data, 450)
print "computed lines", no
l['width'] = 50
l['justify'] = Tk.LEFT
l['anchor'] = 'w'
l['wraplength'] = 450
l['padx']=10
l['pady'] = 5
l['height'] = no
l['font'] = font
if i % 2 == 0:
l['background'] = 'grey80'
else:
l['background'] = 'grey70'

root.mainloop()
 
R

rantingrick

Hmm, i can replace all that code with this...

#
# Easy_as.py
#
import Tkinter as tk
from ScrolledText import ScrolledText
import tkFont
import random
# Create some puesdo data.
data = [
'{0}.{1}'.format(x, 'blah'*random.randint(4, 50))
for x in range(100)
]
##print data
# Create the main window and a scrolled text widget.
root = tk.Tk()
font = tkFont.Font(family='times', size=13)
textbox = ScrolledText(
root,
width=60,
height=20,
font=('Times', 10),
wrap=tk.WORD,
)
textbox.pack(
fill=tk.BOTH,
expand=True,
padx=5,
pady=5,
)
textbox.insert(1.0, '\n\n'.join(data))
# Start the event loop.
root.mainloop()
#
# End
#
 
R

rantingrick

Or if you prefer the alternating background approach...


##################
# Easy_as.py
##################
import Tkinter as tk
from ScrolledText import ScrolledText
import tkFont
import random
END = 'end'
INSERT = 'insert'
#
# Create some puesdo data.
data = [
'{0}.{1}'.format(x, 'blah'*random.randint(4, 50))
for x in range(100)
]
##print data
#
# Create the main window and a scrolled text widget.
root = tk.Tk()
font = tkFont.Font(family='times', size=13)
textbox = ScrolledText(
root,
width=60,
height=20,
font=('Times', 10),
wrap=tk.WORD,
)
textbox.pack(
fill=tk.BOTH,
expand=True,
padx=5,
pady=5,
)
#
# Add a tag to the very end of the widget and
# configure the tag only once!
textbox.tag_add('one', END)
textbox.tag_config('one', background='gray')
#
# Iterate over the lines stuffing them into the textbox.
idata = iter(data)
sidx = 1.0
while True:
try:
textbox.insert(END, idata.next()+"\n")
textbox.tag_add('one', sidx, INSERT)
textbox.insert(END, idata.next()+"\n")
print sidx, textbox.index(END)
sidx = textbox.index(INSERT)
except StopIteration:
break
#
# Start the event loop.
root.mainloop()
##################
# End
##################
 
B

Bart Kastermans

rantingrick said:
Hmm, i can replace all that code with this...

Because I stupidly forgot to repeat the original problem I had, and my
code doesn't show it (and doesn't show the correct use of the function I
wrote). The code shows that I now know how to compute the number of
lines and item will have; in the actual program I am developing I will
take the max of these numbers and make all items that height.

This means the code I should have shown is as follows (here I first
compute the maximum height needed for any item, and then show all items
using this height). Also in the actual program there will be scrolling
options to change the item shown by the different labels.

import Tkinter as Tk
import tkFont
import random
import sys

def genstr (j):
rno = random.randint(4,50)
ret_val = str(j) + ":"
for i in range (0, rno):
ret_val += "hello" + str(i)
return ret_val

def gendata (lh):
ret_val = []
for i in range(0,lh):
ret_val.append (genstr (i))
return ret_val

data = gendata (100)

root = Tk.Tk()
font = tkFont.Font(family='times', size=13)

class lines:
def __init__ (self):
self.lastct = 1 # remember where the cutoff was last work from there

def count (self, text, cutoff = 400):
global font
no_lines = 1
start_idx = 0
idx = self.lastct

while True:
if idx > len (text):
idx = len (text)

# shrink from guessed value
while font.measure (text[start_idx:idx - 1]) > cutoff:
if idx <= start_idx:
print "error"
sys.exit ()
else:
idx -= 1
self.lastct = idx - start_idx # adjust since was too big

# increase from guessed value (note: if first shrunk then done)
while (idx < len (text)
and font.measure (text[start_idx:idx]) < cutoff):
idx += 1
self.lastct = idx - start_idx # adjust since was too small

# next line has been determined
print "*" + text[start_idx:idx-1] + "*"
if idx == len(text) and font.measure (text[start_idx:]) < cutoff:
return no_lines
elif idx == len(text):
return no_lines + 1
else:
no_lines += 1
start_idx = idx - 1
idx = start_idx + self.lastct

lin = lines()

max_ht = 0
for i in range(0,len(data)):
ct = lin.count(data, 450)
if ct > max_ht:
max_ht = ct

for i in range(0,min(len(data),5)):
l = Tk.Label(root)
l.pack()
l['text'] = data
l['width'] = 50
l['justify'] = Tk.LEFT
l['anchor'] = 'w'
l['wraplength'] = 450
l['padx']=10
l['pady'] = 5
l['height'] = max_ht
l['font'] = font
if i % 2 == 0:
l['background'] = 'grey80'
else:
l['background'] = 'grey70'

root.mainloop()


#
# Easy_as.py
#
import Tkinter as tk
from ScrolledText import ScrolledText
import tkFont
import random
# Create some puesdo data.
data = [
'{0}.{1}'.format(x, 'blah'*random.randint(4, 50))
for x in range(100)
]
##print data
# Create the main window and a scrolled text widget.
root = tk.Tk()
font = tkFont.Font(family='times', size=13)
textbox = ScrolledText(
root,
width=60,
height=20,
font=('Times', 10),
wrap=tk.WORD,
)
textbox.pack(
fill=tk.BOTH,
expand=True,
padx=5,
pady=5,
)
textbox.insert(1.0, '\n\n'.join(data))
# Start the event loop.
root.mainloop()
#
# End
#
 
R

rantingrick

Because I stupidly forgot to repeat the original problem I had, and my
code doesn't show it (and doesn't show the correct use of the function I
wrote).

Oh NOW i see! This new code you posted is like night and day compared
to the original </sarcasm> :eek:)

Anyway, i did not read the code to find the difference because i want
to know why you insist on using multiple labels for this when a
scrolled text will suffice. Are you trying to create some sort of
textual "animation frames" that you can flip through on demand? If not
i would use the scrolled text (and even the scrolled text can "feel"
like animation frames whist scrolling).

Take your input data and replace ALL single newlines with null strings
(thereby preserving paragraphs) and let the textbox control the line
wrapping. The benefits are enormous using my way; your way is limited
and requires re-inventing the wheel. Tell me WHY the textbox approach
is not a viable solution and THEN i'll listen to you.

Until then; you can lead a horse to water...
 
B

Bart Kastermans

rantingrick said:
Oh NOW i see! This new code you posted is like night and day compared
to the original </sarcasm> :eek:)

Quite, I should know better (I deal with students thinking what is in
their mind should be clear to me all the time):


##############################################
# run through all the data we have, compute the maximum height
max_ht = 0
for i in range(0,len(data)):
# lin.count is the line counting function
ct = lin.count(data, 450)
if ct > max_ht:
max_ht = ct

for i in range(0,min(len(data),5)):
# l is the Tkinter.Label with all formatting applied and data
# set for text
l['height'] = max_ht # use this maximum height for all Labels.

###############################################

Thinking on this some more, the first computation should surely be

max_ht = max (map (lambda x: lin.count(x, 450), data))


The second one could then be

labels = map (label_prepare_pack, data[:5])

where label_prepare_pack prepares (sets all attributes and data), packs,
and returns the new label. The first (for max_ht) is a clear
improvement to me, the second (for labels) uses too many side-effects to
be very appealing to me.
Anyway, i did not read the code to find the difference because i want
to know why you insist on using multiple labels for this when a
scrolled text will suffice. Are you trying to create some sort of
textual "animation frames" that you can flip through on demand?

I don't follow precisely, but I am indeed looking to flip through all of
data while showing only 5 or 10 at a time. The problem I wanted to
solve was that my window kept changing size while doing this.
If not
i would use the scrolled text (and even the scrolled text can "feel"
like animation frames whist scrolling).

Take your input data and replace ALL single newlines with null strings
(thereby preserving paragraphs) and let the textbox control the line
wrapping. The benefits are enormous using my way; your way is limited
and requires re-inventing the wheel. Tell me WHY the textbox approach
is not a viable solution and THEN i'll listen to you.

The reason I thought this, was that I didn't realize I could bind
actions to tags (to get different actions for different bits of text).
Now that I do know this I could use code like my lin.count to get the
maximum height still (to get a constant location for the i-th item as
the items are changed; more importantly to get a constant height for the
whole list of items), and then use tags to bind the corresponding
actions.
Until then; you can lead a horse to water...

I certainly appreciate your trying. I might not see the fresh stream
yet, but I do see liquid (possibly a shallow muddy pool, but big
progress from the dry sandy dunes before). I will keep both approaches
in mind as I further develop.

Again, thanks for the help, greatly appreciated.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,014
Latest member
BiancaFix3

Latest Threads

Top