Before Update:
After update:
My code for the update:
def plot(data,the_main):
fig = Figure(figsize=(5,5),dpi=100)
plot1 = fig.add_subplot()
plot1.plot(data)
canvas = FigureCanvasTkAgg(fig,master = the_main)
canvas.draw()
# placing the canvas on the Tkinter window
canvas.get_tk_widget().grid(row=5,column=6)
# creating the Matplotlib toolbar
# toolbar = NavigationToolbar2Tk(canvas,the_main)
# toolbar.update()
My code before accessing the update(it's related cause the window):
def buttons_for_month(country):
the_button = tkinter.Tk()
the_button.title(country)
the_button.geometry('967x590')
month_list = [1,2,3,4,5,6,7,8,9,10,11,12]
columns = 0
for i in range(0,len(month_list)):
name = "button",i
name = tkinter.Button(master = the_button,
command = lambda: plot([1,2,3,4,5],the_button),
height = 2,
width=10,
text=month_list[i])
name.grid(row=0,column=columns)
columns += 1
I'm trying to embedding matplotlib into tkinter
It's working , but not as i expected.
I've tried just make a new window , special for the graph ..However , My desktop screen were a messed.
so my question: How to manipulate my code to make matplotlib embedded into tkinter while
keep the button(s) at their places?
I can't run your code, but I'm guessing this will do it:
canvas.get_tk_widget().grid(row=5,column=6, columnspan=7)
The reason this works is because you are putting the plot inside a single column. Every column has a uniform width so it forces column 6 to be as wide as the plot. By having the plot span multiple columns, it prevents the single column from having to be resized to fit the entire plot.
Arguably, an even better solution is to put your buttons in a frame, pack that frame at the top, and then pack the plot below it. grid is very useful when creating a matrix of widgets, but in your case you're not doing that.
Related
See the following simplified code (as much simplified as I could)
Test that when you first click on any of the blue bars, you get the annotation with its value, but the color does not change.
Now click on a second bar. Color is now changed, all works.
Restart the program again and proceed as with 1.
Now instead of clicking on a second bar, let's just resize the window. Color is now changed!
I am unable to find why the first click does not refresh correctly.
Also, not sure if related, but when running the program, the canvas plot is not fitting the window until window is resized (even if we resize by 1 pixel)
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from mplcursors import cursor
import pandas as pd
class TkGraphs(tk.Frame):
'''
:class:`tk.Frame` widget to plot graphs from the provided dataframes
'''
def __init__(self, parent_frame, data: pd.DataFrame=None):
self.parent_frame = parent_frame
super().__init__(self.parent_frame)
self.df = data.dropna() # drop any row containing a nan value.
self.fig = Figure()
self.ax = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.parent_frame)
self.canvas.get_tk_widget().pack(side='top', expand=True, fill='both', padx=20, pady=10)
self.p = self.df.plot(title='Histogram',
x='X',
y='Y',
kind='bar',
width=1.0,
ax=self.ax)
self.c = cursor(self.fig.get_children(), hover=False) # Allows to select data from the plot to print the Y value.
self.prev = dict() # use this to track which bars have been selected and its color changed
self.c.connect("add",
func=lambda sel,
df=self.df: self.on_add(sel, df))
self.canvas.draw()
def on_add(self, sel: cursor, hist_df: pd.DataFrame) -> None:
'''
every time we click on a value, we want to add an annotation and change the color of the bar.
We use self.prev to store the previous bar colour, so we can restore it on the next call to this function
:param sel: selected bar
:param hist_df: plot dataframe values
'''
for index in list(self.prev.keys()):
sel.artist.get_children()[index].set_fc(self.prev.pop(index))
x_val = hist_df.iloc[:, [0]].loc[sel.index].values[0]
x_col = hist_df.columns[0]
y_val = hist_df.iloc[:, [1]].loc[sel.index].values[0]
y_col = hist_df.columns[1]
self.prev[sel.index] = sel.artist.get_children()[sel.index].get_fc()
# Unsure why, but upon the first click, color is not changed unless we resize the window, or click a second bar
selected_bar = sel.artist.get_children()[sel.index]
selected_bar.set(fc='r')
sel.annotation.set(text=f"{x_col}: {x_val}\n{y_col}: {y_val}", position=(0, 20), anncoords="offset points")
#self.parent_frame.after(1000, self.canvas.draw()) # This would change color, but creates a double annotation
if __name__ == "__main__":
root = tk.Tk()
root.geometry('800x600')
df = pd.DataFrame({'X': ['a', 'b', 'c', 'd'], 'Y': [1, 2, 4, 3]})
graph = TkGraphs(root, df)
root.after(1000, graph.event_generate("<Configure>"))
root.mainloop()
I have tried update() methods on the canvas, on root window, on TkGraphs window...
I have tried to call set_fc() after 1000 miliseconds,
I have tried to call event (knowing that resizing makes it to appear): graph.event_generate(""), and also root.event_generate("")
I have tried to draw the canvas again "self.parent_frame.after(1000, self.canvas.draw())" but although that changes the color correctly, creates a "double" annotation one on top of the other.
Also, when debugging, I have checked that when querying self.graphs.c.get_children() after the first click, it shows the selected index is set to red!!, but that is not displaying on the window. So there is certainly a "refreshing" not happening.
I am trying to create a scrollable canvas in tkinter, filled with a grid of smaller square canvases. I have no problem setting this up, but the result is very slow when scrolling. Any advice on how to speed things up? I have considered, but have yet to try to implement:
Using pages of many widgets rather than one scrollable canvas. This would solve the problem, but I'd like to use a scrolling solution if possible.
When scrolling, have the canvas "snap" to the nearest row of squares. The scrolling does not need to be continuous, e.g. we don't need to see half a square at a time.
Change the canvas to only contain the squares that are currently being viewed. All windows that aren't being viewed can be destroyed and created again later. I am unclear if this approach will even help, but a similar approach worked well for a large image on a different project I've worked on.
It could be possible that this lag is just a hardware issue on my end. Unfortunately, I'm not sure how to test this.
Here is the reprex, any help is appreciated! Thank you.
import tkinter as tk
import numpy as np
root = tk.Tk()
outside = tk.Frame(root, bg='green')
outside.pack(expand=True, fill='both')
def get_box(): # box is 16x16 pixels
colors = ['red', 'blue', 'green', 'yellow']
color = np.random.choice(colors, 1)[0]
ret = tk.Canvas(canvas, bg=color, width=16, height=16, highlightthickness=0)
return ret
canvas = tk.Canvas(outside, width=16*30, bg='gray20', highlightthickness=0). # 30 columns
canvas.pack(fill='y', expand=True, side='left')
scroll_y = tk.Scrollbar(outside, orient="vertical", command=canvas.yview) # Create scrollbar
canvas.configure(scrollregion=canvas.bbox('all'), yscrollcommand=scroll_y.set)
scroll_y.pack(fill='y', side='right', expand=True)
for i in range(600): # create many boxes. ideally, ~4000 once things are running smoothly
box = get_box()
canvas.create_window(16 * (i % 30), 16 * (i //30), anchor='nw', window=box)
canvas.update_idletasks()
canvas.configure(scrollregion=canvas.bbox('all'), yscrollcommand=scroll_y.set)
root.mainloop()
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.
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")
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.