tkinter textbox resizes instead of wrapping text to fill box - python-3.x

def red():
frame3.output_display.config(fg = 'red', font=root.customFont1)
def blue():
frame3.output_display.config(fg = 'darkblue', font=root.customFont2)
def green():
frame3.output_display.config(fg = 'darkgreen',font=root.customFont3)
def black():
frame3.output_display.config(fg = 'black',font=root.customFont4)
from tkinter import *
from tkinter import ttk
import tkinter.font
from tkinter.scrolledtext import ScrolledText
root = Tk()
root.title("Change Text")
root.geometry('700x500')
# change font size and family: not used currently because of resizing issue
root.customFont1 = tkinter.font.Font(family="Handwriting-Dakota", size=12)
root.customFont2 = tkinter.font.Font(family="Comic sans MS", size=14)
root.customFont3 = tkinter.font.Font(family="Script MT", size=16)
root.customFont4 = tkinter.font.Font(family="Courier", size=10)
# FRAME 3
frame3 = LabelFrame(root, background = '#EBFFFF', borderwidth = 2, text = 'text entry and display frame', fg = 'purple',bd = 2, relief = FLAT, width = 75, height = 40)
frame3.grid(column = 2, row = 0, columnspan = 3, rowspan = 6, sticky = N+S+E+W)
#frame3.grid_rowconfigure(0, weight=0)
#frame3.grid_columnconfigure(0, weight=0)
frame3.grid_propagate(True)
frame3.output_display = ScrolledText(frame3, wrap = WORD)
frame3.output_display.pack( side = TOP, fill = BOTH, expand = True )
frame3.output_display.insert('1.0', 'the text should appear here and should wrap at character forty five', END)
#frame3.output_display.config(state=DISABLED) # could be used to prevent modification to text (but also prevents load new file)
# draws all of the buttons,
ttk.Style().configure("TButton", padding=6, relief="flat",background="#A52A2A", foreground='#660066')
names_colour=(('Red',red),('Blue',blue),('Green',green),('Black',black))
root.button=[]
for i,(name, colour) in enumerate(names_colour):
root.button.append(ttk.Button(root, text=name, command = colour))
row,col=divmod(i,4)
root.button[i].grid(sticky=N+S+E+W, row=6, column=col, padx=1, pady=1)
root.mainloop()
In the GUI when the text font face and font size is changed, the textbox resizes and obscures the buttons. In my naivety I thought that the textbox would remain the same size and the text would simply wrap within the constraints of the textbox. At least taht is what I would like to achieve. Obviously there is some concept in font size or in textbox , tkinter that I do not understand
Thanks

The width of the text widget is defined in units of character widths rather than pixels, and it tries to use its configured width as its minimum width whenever possible. The widget will be wider for wider fonts, and narrower for narrow fonts. Thus, if you give it a wide font it will try to make itself wider to remain X characters wide.
So, how do you solve this?
One solution is to set the width and height to something small. For example, if you set the width and height to 1 (one), the widget will only ever try to force itself to be one character wide and tall. Unless you're using absolutely huge fonts, you'll barely see the widget grow as you enlarge the font.
Then you will need to rely on the pack, grid, or place algorithm to stretch the widget to the desired dimensions. If you're using grid, this usually means you need to make sure that column and row weights are set appropriately, along with setting the sticky attribute.
The downside to this is that you have to make sure your GUI has the right size, rather than depending on it just magically happening based on the preferred size of each widget.
As a quick hack, you can see this in your program by adding these lines after the widgets have been created:
frame3.output_display.configure(width=1, height=1)
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(2, weight=1)
When I run your code with the above additional lines, the text widget remains a fixed size and the text wraps at different places with each font.

Related

Pyside2 - Linenumbers in codeeditor incorrect when changed font family/size

I looked at this code editor example from the official Qt5 website https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html. It is written in C++ but I implemented it in Python using Pyside2.
The example code works fine as is, however, when I try to change the font family and size of the QPlainTextEdit things start getting messy. I've tried to tweak a lot of different fields like using the fontMetrics to determine to height etc.
Here is a minimal example to reproduce the problem
import sys
import signal
from PySide2.QtCore import Qt, QSize, QRect
from PySide2.QtGui import QPaintEvent, QPainter, QColor, QResizeEvent
from PySide2.QtWidgets import QWidget, QPlainTextEdit, QVBoxLayout
from PySide2 import QtCore
from PySide2.QtWidgets import QApplication
FONT_SIZE = 20
FONT_FAMILY = 'Source Code Pro'
class PlainTextEdit(QPlainTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.init_settings_font()
def init_settings_font(self):
font = self.document().defaultFont()
font.setFamily(FONT_FAMILY)
font.setFixedPitch(True)
font.setPixelSize(FONT_SIZE)
self.document().setDefaultFont(font)
class LineNumberArea(QWidget):
TMP = dict()
def __init__(self, editor):
super().__init__(editor)
self._editor = editor
self._editor.blockCountChanged.connect(lambda new_count: self._update_margin())
self._editor.updateRequest.connect(lambda rect, dy: self._update_request(rect, dy))
self._update_margin()
def width(self) -> int:
# we use 1000 as a default size, so from 0-9999 this length will be applied
_max = max(1000, self._editor.blockCount())
digits = len(f'{_max}')
space = self._editor.fontMetrics().horizontalAdvance('0', -1) * (digits + 1) + 6
return QSize(space, 0).width()
def _update_line_geometry(self):
content_rect = self._editor.contentsRect()
self._update_geometry(content_rect)
def _update_geometry(self, content_rect: QRect):
self.setGeometry(
QRect(content_rect.left(), content_rect.top(), self.width(), content_rect.height())
)
def _update_margin(self):
self._editor.setViewportMargins(self.width(), 0, 0, 0)
def _update_request(self, rect: QRect, dy: int):
self._update(0, rect.y(), self.width(), rect.height(), self._editor.contentsRect())
if rect.contains(self._editor.viewport().rect()):
self._update_margin()
def _update(self, x: int, y: int, w: int, h: int, content_rect: QRect):
self.update(x, y, w, h)
self._update_geometry(content_rect)
# override
def resizeEvent(self, event: QResizeEvent) -> None:
self._update_line_geometry()
# override
def paintEvent(self, event: QPaintEvent):
painter = QPainter(self)
area_color = QColor('darkgrey')
# Clearing rect to update
painter.fillRect(event.rect(), area_color)
visible_block_num = self._editor.firstVisibleBlock().blockNumber()
block = self._editor.document().findBlockByNumber(visible_block_num)
top = self._editor.blockBoundingGeometry(block).translated(self._editor.contentOffset()).top()
bottom = top + self._editor.blockBoundingRect(block).height()
active_line_number = self._editor.textCursor().block().blockNumber() + 1
# font_size = storage.get_setting(Constants.Editor_font_size).value
font = self._editor.font()
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
number_to_draw = visible_block_num + 1
if number_to_draw == active_line_number:
painter.setPen(QColor('black'))
else:
painter.setPen(QColor('white'))
font.setPixelSize(self._editor.document().defaultFont().pixelSize())
painter.setFont(font)
painter.drawText(
-5,
top,
self.width(),
self._editor.fontMetrics().height(),
int(Qt.AlignRight | Qt.AlignHCenter),
str(number_to_draw)
)
block = block.next()
top = bottom
bottom = top + self._editor.blockBoundingGeometry(block).height()
visible_block_num += 1
painter.end()
if __name__ == "__main__":
app = QApplication(sys.argv)
signal.signal(signal.SIGINT, signal.SIG_DFL)
window = QWidget()
layout = QVBoxLayout()
editor = PlainTextEdit()
line_num = LineNumberArea(editor)
layout.addWidget(editor)
window.setLayout(layout)
window.show()
sys.exit(app.exec_())
One of the biggest issues are that there seems to be a top margin offset in the plaintext exit which I'm unable to dynamically get in the linenumber widget.
And when setting the editor font to the painter the numbers will not be drawn the same size!?
Does anyone know how to adjust the line numbers to the same horizontal level as the corresponding text and also get them to be the same size in a dynamic way, meaning that if the font will be set to something else they should all be adjusted automatically.
The problem comes from the fact that you're using two fonts for different purposes: the widget font and the document font.
Each font has different aspects, and its alignment might differ if you consider those fonts as base for drawing coordinates.
Since you're drawing with the document font but using the widget font as reference, the result is that you'll have drawing issues:
even with the same point size, different fonts are drawn at different heights, especially if the alignment of the text rectangle is not correct (also note that you used an inconsistent alignment, as Qt.AlignRight | Qt.AlignHCenter will always consider the right alignment and defaults to top alignment)
you're using the widget font metrics to set the text rectangle height, which differs from the document's metrics, so you'll limit the height of the drawing.
Unlike other widgets, rich text editors in Qt have two font settings:
the widget font;
the (default) document font, which can be overridden by a QTextOption in the document;
The document will always inherit the widget font (or application font) for the default, and this will also happen when setting the font for the widget at runtime, and even for the application (unless a font has been explicitly set for the widget).
Setting the font for the editor is usually fine for simple situations, but you have to remember that fonts propagate, so children widget will inherit the font too.
On the other hand, setting the default font for the document will not propagate to the children, but, as explained above, can be overridden by the application font if that's changed at runtime.
The simplest solution, in your case, would be to set the font for the editor widget instead of the document. In this way you're sure that the LineNumberArea (which is the editor's child) will also inherit the same font. With this approach you don't even need to set the font of the painter, as it will always use the widget font.
In case you want to use a different font and still keep correct alignment, you have to consider the baseline position of the font used for the document, and use that reference for the baseline of the widget font. In order to do that, you have to translate the block position with the difference of the ascent() of the two font metrics.

Tkinter label font size interfere with the frame structure below

I have a window with two main frames. One frame in row=0 called frame-A is used for a Title. The other frame in row=1 called frame-B is structured in several sub-frames with data. This frame-B has a label at the top (row = 0). It contains also several sub-frames in rows 1-3. If I use a font size of 15 for the label on frame-B, there are no problems. If I increase the font size= 20, the sub-frames in the frame-B become separated. I am trying to understand how the font size is creating problems with the frames in rows 1-3.
Here is my code:
import tkinter as tk
window = tk.Tk()
window.geometry("1200x1200")
#1-Main text
fr_A = tk.Frame(window,width=50, height=50, bd= 1,highlightbackground="green", highlightcolor="green", highlightthickness=1)
tk.Label(fr_A,text="My title",font=("Courier", 30,"bold")).grid(row=0)
fr_A.grid(row=0)
#2-initial configuration
fr_B = tk.Frame(window,width=300, height=300, bd= 1,highlightbackground="red",highlightcolor="red", highlightthickness=1)
fr_B.grid(row=1,column=0,ipady=80)
tk.Label(fr_B,text="Init data",font="helvetica 20",height=2).grid(row=0,column=0) #>>>>>>>font size problem
fr_list = []
for cr in ((1,0),(1,1),(1,2),(2,0),(2,1),(2,2),(3,0),(3,1),(3,2)):
frame_in=tk.Frame(fr_B, highlightbackground="black", highlightcolor="black", highlightthickness=1,bd= 1)
frame_in.grid(row=cr[0],column=cr[1])
fr_list.append(frame_in)
cnt = -1
for fr in fr_list:
for cr in ((0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2)):
cnt += 1
tk.Label(fr,text=cnt,width=3,height =1).grid(row=cr[0],column=cr[1],sticky="nsew")
window.mainloop()
You need to add columnspan to the title label.
tk.Label(fr_B, text="Init data",font="helvetica 20",height=2).grid(row=0,column=0, columnspan=3) #>>>>>>>font size problem

How to make text Button and text Label the same height in grid?

I'm making a very simple GUI app using Tkinter for the first time. The problem I've encountered is that when using text Labels and text Buttons side-by-side using grid(), the button's height exceeds that of the label when I want them to be the same.
I've tried modifying the height option when making the Labels and Buttons, but despite the font size being the same for both, and the height for both being determined by "text units", setting height = 1 for both results in two different heights. I've also tried modifying the pady option of the button but that didn't resolve the problem either.
Here is some code that reproduces the problem:
import tkinter as tk
from tkinter import font
root = tk.Tk()
# making font larger for easier viewing
default_font = tk.font.nametofont("TkDefaultFont")
default_font.configure(size = 44)
# make a text Label and text Button, both with height 1... results in 2 different heights
tk.Label(root, text="foo", bg = 'black', fg = 'white', relief = 'raised', height = 1).grid(row=0,column=0)
tk.Button(root, text="bar", bg = 'red', fg = 'black', relief = 'raised', height = 1).grid(row=0,column=1)
root.mainloop()
Any help with this would be much appreciated.
use sticky option in widget.grid( grid_options ) .
sticky
What to do if the cell is larger than widget. By default, with
sticky='', widget is centered in its cell. sticky may be the string
concatenation of zero or more of N, E, S, W, NE, NW, SE, and SW,
compass directions indicating the sides and corners of the cell to
which widget sticks.
tk.Label(root, text="foo", bg = 'black', fg = 'white', relief = 'raised', height = 1).grid(row=0,column=0, sticky= W+E+N+S)
output:

Updating image in tkinter window with newly made images

I have written a program that simulate orbits of planets around a star, I would like a visualiser to display current planet positions around the star. Since all the planets have their x and y co-ords stored, plotting them on an image is straightforward, but I don't know a good way to update the image and re-display after each simulation step.
Currently I draw the image like so (use the planet position to turn a pixel red)
def createwindow():
img = Image.new( 'RGB', (750,730), "black")
pixels = img.load()
for thing in celestials:
pixels[thing.xpos+375+sun.xpos,thing.ypos+375+sun.xpos]=250,20,20
So I have an image which displays the planets quite well, which can be remade after each time the planets move. But how do I display this in a tkinter window?
Is there a way of doing this without using a Photoimage which must be saved and loaded? Or a better way of doing this all together? Maybe assigning each planet a label and drawing them directly on a tkinter black window, updating the position of the label with each simulation step?
All help appreciated
You should probably draw the animation on a canvas, instead of on a static image; You can then move the elements of the animation that are modified at each step of the simulation.
Something along those lines: (press start at the bottom to start)
import tkinter as tk
def animate():
canvas.move(1, 5, 0) # 1 refers to the object to be moved ID, dx=5, dy=0
root.update()
root.after(50, animate)
if __name__ == '__main__':
root = tk.Tk()
frame = tk.Frame(root)
canvas = tk.Canvas(root, width=800, height=400)
canvas.pack()
canvas.create_polygon(10, 10, 50, 10, 50, 50, 10, 50) # canvas object ID created here by tkinter
btn = tk.Button(root, text='start', command=animate)
btn.pack()
root.mainloop()
There're many ways to display dynamic content, like moving planets. It depends on whether you prefer working with widgets, pixels, images or graphical primitives.
Widgets
If you prefer to move widgets, as you said, you can assign each planet to a Label. Then you can place it on a window using Place Geometry Manager:
yourLabel.place(anchor=CENTER, relx=0.5, rely=0.5, x=-100, y=50)
That'd put a center of the label to coords (-100,50) relative to its parent center (relx=0.5, rely=0.5). To change the position call .place() again with different (x,y) coords.
Pixels
To change image pixels at runtime you can use .blank(), .get() and .put() methods of PhotoImage. They're not in TkInter book, but you can see them in help(PhotoImage) in python console. To set a single pixel (x,y) to color #fa1414 use:
yourPhotoImage.put("{#fa1414}", to=(x,y))
The "{#fa1414}" argument here is actually a pixels list. Tk can put a rectangular block of pixels in one call. Colors are specified row-by-row, separated by spaces, each row enclosed in curly braces. Block of NxM colors would look like "{C0,0 C1,0 ... CN-1,0} {C0,1 C1,1 ... CN-1,1} ... {C0,M-1 C1,M-1 ... CN-1,M-1}". For example, to put 2x2 pixels to coords (4,6) use:
yourPhotoImage.put("{red green} {#7f007f blue}", to=(4,6))
Tk recognizes many symbolic color names in addition to #rrggbb syntax.
Note that all the changes are displayed right away. You don't need to manually "re-display" images. E.g. if you have a Label(image=yourPhotoImage) defined somewhere, it'd display the changes after you modify yourPhotoImage.
Images
Also PhotoImage can load images from base64-encoded strings. So, technically, to skip saving/loading a file, you can load your image from a string, containing base64-encoded .gif or .png (formats supported natively in Tk):
newimg = PhotoImage(data="R0lGODlhHgAUAPAAAP//AAAAACH5BAAAAAAALAAAAAAeABQAAAIXhI+py+0Po5y02ouz3rz7D4biSJZmUgAAOw==")
.put() also supports base64-strings. For example, if you have planets as images, loaded into base64-strings, you can put them into your image with:
yourPhotoImage.put(base64_mars_image_string, to=(100,50))
Finally, if you're using PIL anyway, you can create PIL.ImageTk.PhotoImage from your PIL.Image img and use it everywhere Tkinter accepts an image object[ref]
from PIL import ImageTk
photoimg = ImageTk.PhotoImage(img)
Graphical primitives
If you want lines or arcs you can also use Canvas. It provides a way to put together different widgets, images, texts, graphical primitives, and move them around, or configure interaction with them. A canvas-based planetary system could look like this (python2 syntax):
from Tkinter import *
import math, time
color = ["#fffc00", "#a09991", "#b6b697", "#2c81ca", "#a36447", "#b16811", "#e5ca9d", "#bcdaf2", "#7e96bc", "#d3ac8b"]
size = [20, 4, 7, 7, 5, 15, 13, 9, 9, 4]
start_time = time.time()
def update_positions():
dt = (time.time() - start_time)*5
for i in range(len(planets)):
px, py = 350+35*i*math.cos(dt/(i+1)/size[i]+4*i), 350+35*i*math.sin(dt/(i+1)/size[i]+4*i)
canvas.coords(planets[i], (px-size[i],py-size[i],px+size[i],py+size[i]))
root.after(100, update_positions)
root = Tk()
canvas = Canvas(root, width=700, height=700, bg="black")
arcs = [canvas.create_oval(350-35*i, 350-35*i, 350+35*i, 350+35*i, outline=color[i]) for i in range(len(color))]
planets = [canvas.create_oval(350, 350, 350, 350, fill=color[i]) for i in range(len(color))]
update_positions()
canvas.pack()
root.mainloop()
Note that items created on the canvas are kept until you remove them. If you want to change the drawing, you can either use methods like .coords(), .itemconfig(), and .move() to modify the items, or use .delete() to remove them.

tkinter resize label width (grid_propagate not working)

I'm having problem with adjusting the width of the label to reflect current width of the window. When the window size changes I'd like label to fill the rest of the width that is left after other widgets in row consume width they need.
Putting the label in a Frame and using grid_propagate(False) does not seem to work.
Consider following code:
import tkinter as tk
import tkinter.ttk as ttk
class PixelLabel(ttk.Frame):
def __init__(self,master, w, h=20, *args, **kwargs):
'''
creates label inside frame,
then frame is set NOT to adjust to child(label) size
and the label keeps extending inside frame to fill it all,
whatever long text inside it is
'''
ttk.Frame.__init__(self, master, width=w, height=h,borderwidth=1)
#self.config(highlightbackground="blue")
self.grid_propagate(False) # don't shrink
self.label = ttk.Label(*args, **kwargs)
self.label.grid(sticky='nswe')
def resize(self,parent,*other_lenghts):
'''
resizes label to take rest of the width from parent
that other childs are not using
'''
parent.update()
new_width = parent.winfo_width()
print(new_width)
for lenght in other_lenghts:
new_width -= lenght
print(new_width)
self.configure(width = new_width)
root = tk.Tk()
master = ttk.Frame(root)
master.grid()
label = ttk.Label(master,text='aaa',borderwidth=1, relief='sunken')
label.grid(row=0,column=0)
label1_width = 7
label1 = ttk.Label(master,text='bbbb',borderwidth=1, relief='sunken',width=label1_width)
label1.grid(row=0,column=1)
label2 = ttk.Label(master,text='ccccccccccccccccccccccccccccccccccccc',borderwidth=1, relief='sunken')
label2.grid(row=0,column=2)
label3_width = 9
label2 = ttk.Label(master,text='ddddd',borderwidth=1, relief='sunken',width=label2_width)
label2.grid(row=0,column=3)
label4 = ttk.Label(master,text='ee',borderwidth=1, relief='sunken')
label4.grid(row=1,column=0)
label5 = ttk.Label(master,text='f',borderwidth=1, relief='sunken')
label5.grid(row=1,column=1,sticky='we')
nest_frame = ttk.Frame(master)
nest_frame.grid(row=2,columnspan=4)
label8_width = 9
label8 = ttk.Label(nest_frame,text='xxxxx',borderwidth=1, relief='sunken',width=label8_width)
label8.grid(row=0,column=0)
label9 = PixelLabel(nest_frame, 5, text='should be next to xxxxx but is not?',borderwidth=1, relief='sunken')
label9.grid(row=0,column=1)
label9.resize(root,label2_width)
root.mainloop()
Why label9 does not appear next to label8
How to make label9 resize to meet current window size (this code is just a sample, I would like to be able to resize label9 as the window size changes dynamically when functions are reshaping the window)
It's not clear why you are using a label in a frame. I suspect this is an XY problem. You can get labels to consume extra space without resorting to putting labels inside frames. However, since you posted some very specific code with very specific questions, that's what I'll address.
Why label9 does not appear next to label8
Because you are creating the label as a child of the root window rather than a child of the frame. You need to create the label as a child of self inside PixelLabel:
class PixelLabel(...):
def __init__(...):
...
self.label = ttk.Label(self, ...)
...
How to make label9 resize to meet current window size (this code is just a sample, I would like to be able to resize label9 as the window size changes dynamically when functions are reshaping the window)
There are a couple more problems. First, you need to give column zero of the frame inside PixelFrame a non-zero weight so that it uses all available space (or, switch to pack).
class PixelLabel(...):
def __init__(...):
...
self.grid_columnconfigure(0, weight=1)
...
Second, you need to use the sticky attribute when placing nest_frame in the window so that it expands to fill its space:
nest_frame.grid(..., sticky="ew")

Resources