Tkinter : Getting screen text unit width. (Not pixels) - python

I'm using Tkinter to design a UI for an application. I'm using grid geometry and while specifying button width (or any widget width), I realized that width should be specified in text units and not pixels. Since I want to make it platform independent and screen size independent Is there any method to get max text unit width ? So that I can do math on basis of that.
For example:
I've 10 buttons in a row, which should be of equal width. If I hard code a width value specific to current screen value, it would not work on diff screen wise.
Thanks.

Using the tkinter.font package you can create Tk font objects to define a font and call the measure method to obtain the screen width of text using that font.
import tkinter as tk
import tkinter.font as tkfont
root = tk.Tk()
font = tkfont.Font(family="Consolas", size=10, weight="normal")
m_len = font.measure("m")
For monospace fonts, any character will do. For proportional text, if you give the whole string you get the screen length of the string. Otherwise you typically get the size of m or n as either a maximum or an average character width.
However, buttons are typically all the same size on many UI styles. Varying the button sizes is likely to look quite poor. You can specify a negative width to set a minimum width for the widget which can be helpful.

If you want to make a GUI that is "platform independent and screen size independent", you definitely do not want to be measuring sizes yourself. Unless, by saying you want something platform independent, you're saying you want a button to be X pixels regardless of pixel density or screen resolution (which seems like a very bad idea).
The whole reason tkinter supports measuring in character units, along with options for widgets to stretch and shrink, is to support platform independence. When you start working at the pixel level, you will have many, many problems when you run the code on other platforms, or on other displays, or with other fonts.
That being said, the measure method of a font can tell you exactly how many pixels a given string will require in a given font. If you want to know how wide "one character" is, you can use the measure method on the string "0", which is what tkinter uses as a base when computing widths based on characters.
If you want buttons to be exactly the same size, using character widths will give you that, because it isn't the width of 10 actual characters in that widget, but ten average character widths. In that case, "10 characters" will be the same for every widget, no matter what the contents of that widget.

You must be having a root variable like-
import tkinter as tk
root = tk.Tk()
You can use root.winfo_sccreenwidth() for width
width = root.winfo_screenwidth() #width of screen
height = root.winfo_screenheight() # height of screen
I am not sure about any way to get the screenwidth in text units, it may not even be possible.
A solution would be to be able to specify the button width in pixels, you can do this by following a simple example given here.
What you do is - put the Button widget inside a frame and specify the height and width for the frame , and then make frame not propagate the size using grid_propagate(False) for the frame and then making the button expand upto the frame (maybe by using grid sticky="we" )

Related

Label text position in Bokeh

I would like to stick MyText Label to the bottom right part of my figure
for a given text and a given font size (as shown on the picture for 'this is super fun', font size of '20px' and with tiny characters. I found the good position by dichotomy ).
What is the function position I need to pass to x ?
This should depends on len(MyText), text_font_size and figure width ...
from bokeh.models import ColumnDataSource, Label, LabelSet, Range1d
from bokeh.plotting import figure, output_file, show
width,height=400,300
p = figure(plot_width=width, plot_height=height)
MyText='this is super fun'
my_font_size = "20px"
labels = Label(x=width/2+25, y=0,x_units='screen', y_units='screen', text=MyText,text_font_size=my_font_size)
p.add_layout(labels)
show(p)
I don't think there is any 100% robust way to do this, actually.
You can set the text_align to "right" which helps:
p = figure(plot_width=width, plot_height=height)
labels = Label(x=width-50, y=0,
x_units="screen", y_units='screen', text_align="right",
text=MyText,text_font_size=my_font_size)
Note the -50 above is to account (roughly) for the width of the space to the right of the "plot area" (i.e where the toolbar is). However if you add a y-axis on the left side, you'd need to account for that too, and if you allow zooming, then left space can grow and shrink to accommodate bigger or smaller axis labels, which means you can't reliably account for that space with a single constant up front. You could set min_border values to be larger, which might mitigate the problem for some range of zooming/panning.
Also the above assumes the plot sizing mode is not "responsive". If the plot itself can resize then no constant value in screen units will ever work.
If you can fix your x range start/end (or add an "extra" range), then you could right-align to the range end value using "data" units. But if you allow zooming or panning then the label will move to stay fixed at that data position.
The main issue is that the "inner_width" is only computed in the browser. It's not available to the Python code because it doesn't exist outside the browser. What's really needed is some special convention or confguration to designate "inner_width" as a symbolic concept that updates to whatever is necessary, regardless of panning or zooming or resizing. I'd suggest making a GitHub issue to propose this feature.
In the mean time, I think any solution will involve some trial and error with a fixed font size in "px" and also ideally limiting panning/zooming if possible.

Color lines seperately in tkinter?

I'm trying to write a music program that would display Chordpro files in python. Similar to this image, I want the chords, comments, and lyrics to each have different colours. I've tried these widgets:
I tried separating chords, comments and lyrics into multiple strings that could overlap on a canvas (with a different colour for each string) to make the full song, but sadly whitespace overrides previously rendered text, so I could only see the last layer.
Label/Message doesn't have functionality for multiple colours unless make a label for each line, which is very tedious, considering I want the font size to be adjustable too.
Text is editable, which I don't want.
Is there some kind of module or other tkinter widget that would allow separately coloured lines?
Just draw the separate lines at different y positions (heights) on the canvas. It's the first two parameters of the create_text() function. E.g.
self.canvas = Canvas(root, width=800, height=650, bg = '#afeeee')
self.canvas.create_text(100,10,fill="darkblue",font="Times 20 italic bold",
text="Greensleeves are my...")
So here, change 10 to the line position you want etc. Code copied from Python: how to add text inside a canvas?

Setting entry text box size in Tkinter python

I am attempting to create a form that requires some entry options to be more than one line long. This does not seem possible with the .Entry() function.
This issue is that I dont see anywhere in the documentation how to restrict the size of the text entry box (particularly, number of lines, or height)
To be clear, I am not trying to set the placement height, or text size, I am trying to restrict the number of lines where text can be entered (so the form is a predictable size) this is necessary because for each item I am using .place() to put them at certain pixel locations, and thus .pack() is not a viable solution for my problems either
Some reference material:
Documentation for entry widget
Use the text widget instead. It has a height option
Note: place requires integers that represent pixels, however, within the options of the text box, the width option requires integers that represent number of characters, and the height option requires an integer that represents number of lines. If you attempt to use height=20 expecting 20 pixels, you will be unpleasantly surprised. If you use width = 5, height = 5 it will not be square.
For reference, the default text size and font is an average of 8 pixels wide per character in the text widget, and 6 pixels per character in the default for the entry widget, so if you wanted a form to stretch a certain number of pixels, use width = round(pixel_length /6)
you can combine this with screen width and placement of the corner of forms to fill across the page or to near the end if you like

How do I find out the height of the tab bar in a QTabWidget?

I've created a QTabWidget with images overlaid but the images start at the top of the TabWidget instead of the Tab. I want these images on top of each tab so I need to find out the size of the tab bar to adjust the images down correctly.
Currently on my computer the Tab height is 18 pixels but I need to make sure the adjustment will work across all platforms so is there any way to find out the height of the tabs as a variable?
To clarify - H, the height of the tab bar is what I'm after.
Thanks
I think you should try, this might give your ans
self.widget.geometry().height() - self.any_tab_name.geometry().height()
Second portion will give height of tabs("Demo","tab_name"), subtract that from tabwidget height.
Found the answer without using the difference between 2 heights as follows:
print self.widgetName.tabBar().geometry().height()
returns the height in pixels of the tab bar. This also allows for different tab content sizes
Here is a Pyqt5 example for you reference.
tabs = QTabWidget()
H = tabs.tabBar().size().height()

Why is TKinter OptionMenu changing the size of it's parent?

I have this GUI program, and one of the Frames has an OptionMenu in it. Whenever I select something in the OptionMenu, it resizes the parent frame, even though it has more than enough room to fit in the current panel. It's hard to describe in words, so here's an image of what I mean:
You can see that the purple, blue, and red frames have expanded with the size increase of the OptionMenu.
The window is split into two Frames, the left and right, with a Grid Layout, with a weight of 3 and 2, respectively. Inside of the two frames is a Pack layout of each colored panel, set to fill X. Inside of Purple, I have set this optionMenu, and packed it to the left of the panel.
When it changes its contents, it resizes the entire right frame, ignoring the grid weights and throwing off the balance of the GUI. If I set a fixed width for the OptionMenu, it doesn't resize, but still grows the frame out of alignment with the GridLayout. How can I get the frames to not resize based off of the width of this one element, and just place the element inside of the frame, which has more than enough room to handle it, even at it's widest?
Here's a boiled-down version of the GUI code I'm using, the full code is a bit too long since each panel is broken into classes for future functionality:
root = Tk()
root.geometry('640x480')
#Begin defining left frame areas
leftFrame = Frame(root)
grayPanel = Frame(leftFrame,bg="gray")
whitePanel = Frame(leftFrame,bg="white", height=50)
grayPanel.pack(expand=True,fill=BOTH)
whitePanel.pack(fill=X)
#Begin defining right frame areas
rightFrame = Frame(root)
purplePanel = Frame(rightFrame,bg="purple", height=50)
bluePanel = Frame(rightFrame,bg="blue")
redPanel = Frame(rightFrame,bg="red", height=100)
purplePanel.pack(fill=X)
bluePanel.pack(expand=True,fill=BOTH)
redPanel.pack(fill=X)
#create the options dropdown for purple
currentOption = StringVar(purplePanel)
currentOption.set("None")
optList = ["None","Some incredibly long string that breaks everything"]
options = OptionMenu(purplePanel,currentOption,*optList)
options.pack(side=LEFT)
leftFrame.grid(row=0,column=0,sticky=N+S+E+W)
rightFrame.grid(row=0,column=1,sticky=N+S+E+W)
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=3)
root.grid_columnconfigure(1, weight=2)
root.mainloop()
The root of the problem lies in the fact you don't give any widgets a size, and all the frames are empty. The following description may sound confusing, but it's actually pretty logical, deterministic, and consistent with the documented behavior.
The natural size of the widgets and columns
Because none of the frames have an explicit width, they default to a width of 0 (zero). The optionmenu has the behavior that it adapts its size to be large enough to show the value (except on the Mac, I think). It is the only widget with a non-zero requested (or "natural") size.
The grid is responsible for the left and right sides. The left size is of zero width, and the right side is the width of the option menu. For the sake of this discussion let's say that it is 140 pixels wide just to make math easy; the actual size may be bigger or smaller depending on your fonts, and we're ignoring borders to keep the math simple. Therefore, grid thinks the left wants to be zero, and the right wants to be 140. That is the only information it starts with when it tries to make everything fit.
Dealing with extra space
You force the window to be 640 pixels wide but its children are only require 140 pixels, thus it has 500 extra pixels. You use column weights of three for the left and two for the right, so for every five pixels of extra space, three will go to the left and two will go to the right. That means that the left column gets 300 pixels of the extra space and the right column will get 200 pixels of the extra space. That makes the left column 300 pixels (0+300) and the right 340 (140+200). That's why they appear roughly the same size at startup.
What happens when you change the size of the optionmenu
Now, you change the value of the optionmenu, causing it to grow. Let's say the new width of the optionmenu is 440 pixels. The left side still wants to be zero, but now the right side wants to be 440. That means there are only 200 pixels of extra space: 120 for the left and 80 for the right. Remember, the left column's natural size is zero, so zero plus 120 means the left column will be 120 pixels wide. On the right, the natural size is 440. 440 plus the extra 80 means the right side will now be 520 pixels wide.
That is why, when there's a long option menu, the left side shrinks and the right side has extra space. You requested the left be zero, so it only gets to use the extra space after all of the other widgets with an actual size are laid out. The right side has a non-zero natural size, but the grid algorithm has space left over that it has to allocate to the right.
How to get uniform column widths
You didn't ask, but I'm going to assume a follow-up question will be "how do I make the two columns the same size?" The answer is to use the uniform option with grid. Tell grid you want the two columns to be of a uniform size like this:
root.grid_columnconfigure(0, weight=3, uniform="column")
root.grid_columnconfigure(1, weight=2, uniform="column")
uniform takes any string as an argument. All columns (or rows) with the same value are considered a "uniform group".
Note that with uniform, grid still honors the weight. it's just that with uniform the columns are guaranteed to strictly follow the column weights. In your case that means you have a 3:2 ratio between the left and right wide
Canonical information on grid and pack algorithms
Tkinter is a wrapper around a tcl interpreter with the tk toolkit. While the syntax is different than python, it's easy to translate the tcl documentation into python. Here are links to the descriptions of how grid and pack work:
The Grid Algorithm
The Packer Algorithm

Categories