Create undo and redo function - python

In this code, lines created by mouse button in the canvas. With the help of Undo and Redo button remove these lines.
I also want to add UNDO/REDO to the Invoker so that I can go backawards and forwards through time.
from tkinter import *
root = Tk()
my_canvas = Canvas(root, bg = "white", height=500, width=500, cursor="cross")
#Draw Line using mouse
coords = {"x": 0, "y": 0, "x2": 0, "y2": 0}
final = []
lines = []
def click(e):
coords["x"] = e.x
coords["y"] = e.y
lines.append(my_canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"], fill='red', width=2))
def release(l):
lis = []
lis.append(coords["x"]);
lis.append(coords["y"]);
lis.append(coords["x2"]);
lis.append(coords["x2"])
final.append(lis)
def drag(e):
coords["x2"] = e.x
coords["y2"] = e.y
my_canvas.coords(lines[-1], coords["x"], coords["y"], coords["x2"], coords["y2"])
my_canvas.bind("<ButtonPress-1>", click)
my_canvas.bind("<B1-Motion>", drag)
my_canvas.bind('<ButtonRelease-1>', release)
def undo():
return
def redo()
return
undo_btn = Button(root, text="Undo", command=undo)
undo_btn.pack()
redo_btn = Button(root, text="Redo", command=undo)
redo_btn.pack()
my_canvas.pack()
mainloop()

You'll need to store the information of the lines to be able to undo them an play them back. For the former action, you just need the line's ID on the canvas. For the latter, you'll also need its coordinates. You can use a dataclasses.dataclass() to store the line information.
from dataclasses import dataclass
from tkinter import *
root = Tk()
my_canvas = Canvas(root, bg = "white", height=500, width=500, cursor="cross")
#Draw Line using mouse
coords = {"x": 0, "y": 0, "x2": 0, "y2": 0}
final = []
lines = []
deleted_lines = []
#dataclass
class Line:
"""Information about a drawn line."""
id: int
x: int
y: int
x2: int
y2: int
fill: str = 'red'
width: int = 2
def set_coords(self, x: int, y: int, x2: int, y2: int) -> None:
self.x = x
self.y = y
self.x2 = x2
self.y2 = y2
def click(e):
coords["x"] = e.x
coords["y"] = e.y
id_ = my_canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"], fill='red', width=2)
lines.append(Line(id_, coords["x"], coords["y"], coords["x"], coords["y"], fill='red', width=2))
def drag(e):
coords["x2"] = e.x
coords["y2"] = e.y
line = lines[-1]
my_canvas.coords(line.id, coords["x"], coords["y"], coords["x2"], coords["y2"])
line.set_coords(coords["x"], coords["y"], coords["x2"], coords["y2"])
my_canvas.bind("<ButtonPress-1>", click)
my_canvas.bind("<B1-Motion>", drag)
def undo():
try:
line = lines.pop()
except IndexError:
print('No lines to delete.')
return
deleted_lines.append(line)
my_canvas.delete(line.id)
def redo():
try:
line = deleted_lines.pop()
except IndexError:
print('No deleted lines to redo.')
return
id_ = my_canvas.create_line(line.x, line.y, line.x2, line.y2, fill=line.fill, width=line.width)
line = Line(id_, line.x, line.y, line.x2, line.y2, fill=line.fill, width=line.width)
lines.append(line)
undo_btn = Button(root, text="Undo", command=undo)
undo_btn.pack()
redo_btn = Button(root, text="Redo", command=redo)
redo_btn.pack()
my_canvas.pack()
mainloop()

Related

How to select a color in Tkinter?

I have a program written in Python, that makes a window where you can draw, using Tkinter. Every time you left-click your mouse, you make a point in your canvas. When you double-click, a polygon is made, filled with the color you chose. I found a way to change the colors from the boxes, when you right-click a box, but the problem is that the selected color is not saved and i cannot make it replace the previous one. Does anyone know how to solve this problem?
import tkinter as tk
from tkinter import colorchooser
class Point():
def __init__(self, canvas, x, y):
self.x = x
self.y = y
canvas.create_oval(x-2, y-2, x+2, y+2, fill='white')
class Poly():
def __init__(self, canvas, board, p_list=[] ):
self.p_list = p_list
self.canvas = canvas
self.board = board
def draw_poly(self):
points = []
for p in self.p_list:
points.extend([p.x, p.y])
points.extend(points[:2])
self.canvas.create_polygon(points, fill=self.board.current_color, outline=self.board.current_color)
def add_point(self, p):
self.p_list.append(p)
if len(self.p_list)>1:
p1 = self.p_list[-1]
p2 = self.p_list[-2]
self.canvas.create_line(p1.x, p1.y, p2.x, p2.y, fill="white", width=2)
class Palette():
def __init__(self, frame, board, colors):
self.colors = colors
self.board = board
self.allColors = []
for color in self.colors:
f = tk.Frame(frame, bg='lightgrey', bd=3)
f.pack(expand=1, fill='both', side='left')
if self.board.current_color == color: f.config(bg='red')
self.allColors.append(f)
l = tk.Label(f, bg=color)
l.pack(expand=1, fill='both', padx=2, pady=2)
l.bind("<1>", self.set_color)
l.bind("<Button-3>", self.do_popup)
def do_popup(self, event):
clsheet = tk.colorchooser.askcolor()
self.current_color = clsheet[1]
def set_color(self, e):
self.board.current_color = e.widget['bg']
self.selected_color(e.widget.master)
def selected_color(self, colorFrame):
for f in self.allColors: f.config(bg = 'lightgrey')
colorFrame.config(bg="red")
class Board():
def __init__(self, root):
self.colors = ['#B4FE98', '#77E4D4', '#F4EEA9', '#F0BB62', '#FF5F7E', "#9A0680"]
self.root = root
self.current_color = self.colors[0]
self.f1 = tk.Frame(self.root)
self.f1.pack(expand=1, fill='both', padx=5)
self.f2 = tk.Frame(self.root)
self.f2.pack(expand=1, fill='both')
self.canvas = tk.Canvas(self.f2, bg="#000D6B", height=550)
self.canvas.pack(expand=1, fill='both', padx=5, pady=5)
self.pallette = Palette(self.f1, self, self.colors )
self.canvas.bind("<1>", self.draw_point)
self.canvas.bind("<Double-Button-1>", self.draw_poly)
self.poly = None
def draw_point(self, evnt):
if self.poly: self.poly.add_point(Point(self.canvas, evnt.x, evnt.y))
else: self.poly = Poly(self.canvas, self, [Point(self.canvas, evnt.x, evnt.y)])
def draw_poly(self, evnt):
if self.poly and len(self.poly.p_list) > 2:
self.poly.add_point(Point(self.canvas, evnt.x, evnt.y))
self.poly.draw_poly()
self.poly = None
else: self.draw_point(evnt)
#main program
root = tk.Tk()
root.title('my program')
root.geometry("600x700")
root.resizable(0,0)
Board(root)
tk.mainloop()
To fix the part where right-clicking a color was not working i changed two things in your script:
You stored your frame widgets and their sub-widget labels in Palette.allColors. I handed over the index of the selected color to the do_popup event by using partial. Then you can simply iterate over all widgets in Palette.allColors and if the index from the event matches the index in the list, you access the children and further the !label key of those and change the background color to the selected color.
I matched Board.current_color and Palette.current_color
Most changes were made in Palette.do_popup(). Might not be the most elegant solution but it looks like its working like you intend. Full code:
import tkinter as tk
from tkinter import colorchooser
from functools import partial
class Point():
def __init__(self, canvas, x, y):
self.x = x
self.y = y
canvas.create_oval(x - 2, y - 2, x + 2, y + 2, fill='white')
class Poly():
def __init__(self, canvas, board, p_list=[]):
self.p_list = p_list
self.canvas = canvas
self.board = board
def draw_poly(self):
points = []
for p in self.p_list:
points.extend([p.x, p.y])
points.extend(points[:2])
self.canvas.create_polygon(points, fill=self.board.current_color, outline=self.board.current_color)
def add_point(self, p):
self.p_list.append(p)
if len(self.p_list) > 1:
p1 = self.p_list[-1]
p2 = self.p_list[-2]
self.canvas.create_line(p1.x, p1.y, p2.x, p2.y, fill="white", width=2)
class Palette():
def __init__(self, frame, board, colors):
self.colors = colors
self.board = board
self.allColors = []
for idx, color in enumerate(self.colors):
f = tk.Frame(frame, bg='lightgrey', bd=3)
f.pack(expand=1, fill='both', side='left')
if self.board.current_color == color: f.config(bg='red')
self.allColors.append(f)
l = tk.Label(f, bg=color)
l.pack(expand=1, fill='both', padx=2, pady=2)
l.bind("<1>", self.set_color)
l.bind("<Button-3>", partial(self.do_popup, idx))
def do_popup(self, idx, event):
clsheet = tk.colorchooser.askcolor()
self.current_color = clsheet[1].upper()
print(f"You chose: {self.current_color}")
self.board.current_color = self.current_color # required?
self.selected_color(event.widget.master)
for frm_idx, frm in enumerate(self.allColors):
if frm_idx == idx:
frm.children["!label"].config(bg=self.current_color)
def set_color(self, e):
self.board.current_color = e.widget['bg']
self.selected_color(e.widget.master)
def selected_color(self, colorFrame):
for f in self.allColors: f.config(bg='lightgrey')
colorFrame.config(bg="red")
class Board():
def __init__(self, root):
self.colors = ['#B4FE98', '#77E4D4', '#F4EEA9', '#F0BB62', '#FF5F7E', "#9A0680"]
self.root = root
self.current_color = self.colors[0]
self.f1 = tk.Frame(self.root)
self.f1.pack(expand=1, fill='both', padx=5)
self.f2 = tk.Frame(self.root)
self.f2.pack(expand=1, fill='both')
self.canvas = tk.Canvas(self.f2, bg="#000D6B", height=550)
self.canvas.pack(expand=1, fill='both', padx=5, pady=5)
self.pallette = Palette(self.f1, self, self.colors)
self.canvas.bind("<1>", self.draw_point)
self.canvas.bind("<Double-Button-1>", self.draw_poly)
self.poly = None
def draw_point(self, evnt):
if self.poly:
self.poly.add_point(Point(self.canvas, evnt.x, evnt.y))
else:
self.poly = Poly(self.canvas, self, [Point(self.canvas, evnt.x, evnt.y)])
def draw_poly(self, evnt):
if self.poly and len(self.poly.p_list) > 2:
self.poly.add_point(Point(self.canvas, evnt.x, evnt.y))
self.poly.draw_poly()
self.poly = None
else:
self.draw_point(evnt)
# main program
root = tk.Tk()
root.title('my program')
root.geometry("600x700")
root.resizable(0, 0)
Board(root)
tk.mainloop()

Is there any method in python to link two blocks dynamically

I want to draw some blocks and connect them through arrow.
I did it, but if I move the coordinates of blocks I also need to change arrow coordinates.
Is there any method to bind them, like if I move the block position connecting arrow will also adjust automatically?
Can you suggest any other method in python to which will work?
from tkinter import *
top = Tk()
top.geometry("800x600")
#creating a simple canvas
a = Canvas(top,bg = "White",height = "515", width = "1000")
a.create_rectangle(430, 255, 550, 275,fill="White")
a.create_text(490,265,text="School",fill="black",font=('Helvetica 8 bold'))
a.create_rectangle(430, 205, 480, 225,fill="White")
a.create_text(455,215,text="Boys",fill="black",font=('Helvetica 8 bold'))
a.create_rectangle(480, 160, 540, 180,fill="White")
a.create_text(510,170,text="Girls",fill="black",font=('Helvetica 8 bold'))
#School to Boys
a.create_line(450, 225, 450, 255, arrow= BOTH)
#School to COM
a.create_line(510, 180, 510, 255, arrow= BOTH)
a.pack(expand=True)
top.mainloop()
No, there isn't any method given by tkinter to link blocks. But, you can create them yourself. First, create a rectangle and text and assign them the same tag (note: each block must have a unique tag).
Now, if you want to move the blocks using the mouse, you will have to use the canvas.find_withtag("current") to get the currently selected block and canvas.move(tagorid, x, y) to move the block. Once it is moved use widget.generate_event() to generate a custom virtual event.
You will now have to use canvas.tag_bind(unqiue_tagn, "<<customevent>>", update_pos) to call the update_pos function when the event is generated, the update_pos should update the connection position.
Here is a sample code.
import tkinter as tk
class NodeScene(tk.Canvas):
def __init__(self, *args, **kwargs):
super(NodeScene, self).__init__(*args, **kwargs)
self.bind("<Button-1>", self.click_pos)
self.bind("<B1-Motion>", self.selected_item)
self.prev_x, self.prev_y = 0, 0
def click_pos(self, event):
self.prev_x, self.prev_y = event.x, event.y
def selected_item(self, event):
_current = self.find_withtag("current")
assoc_tags = self.itemcget(_current, "tags").split(" ")[0]
if _current and "node" in assoc_tags:
new_pos_x, new_pos_y = event.x - self.prev_x, event.y - self.prev_y
self.move(assoc_tags, new_pos_x, new_pos_y)
self.event_generate("<<moved>>", when="tail")
self.prev_x, self.prev_y = event.x, event.y
class Node:
def __init__(self, canvas: tk.Canvas, text: str, pos=(50, 50)):
self.tag_id = f"node{id(self)}"
self.canvas = canvas
self.c_rect = self.canvas.create_rectangle(0, 0, 0, 0, fill="white", tags=(self.tag_id))
c_text = self.canvas.create_text(*pos, text=text, fill="black", font=("Helvetica 8 bold"), tags=(self.tag_id))
padding = 10
text_rect = list(self.canvas.bbox(c_text))
text_rect[0] -= padding
text_rect[1] -= padding
text_rect[2] += padding
text_rect[3] += padding
self.canvas.coords(self.c_rect, *text_rect)
def bbox(self):
return self.canvas.bbox(self.c_rect)
def top(self):
bbox = self.bbox()
return (bbox[2] + bbox[0]) / 2, bbox[1]
def bottom(self):
bbox = self.bbox()
return (bbox[2] + bbox[0]) / 2, bbox[3]
class Connection:
def __init__(self, canvas, node1: Node, node2: Node, pos_x): # pos_x is the offset from center
self.node1 = node1
self.node2 = node2
self.pos_x_offset = pos_x
self.canvas = canvas
self.connection_id = self.canvas.create_line(0, 0, 0, 0, arrow= tk.BOTH)
self.canvas.tag_bind(self.node1.tag_id, "<<moved>>", self.update_conn_pos, add="+")
self.canvas.tag_bind(self.node2.tag_id, "<<moved>>", self.update_conn_pos, add="+")
self.update_conn_pos()
def update_conn_pos(self, event=None):
""" updates the connection position """
node1_btm = self.node1.bottom()
node2_top = list(self.node2.top())
node2_top[0] += self.pos_x_offset
self.canvas.coords(self.connection_id, *node1_btm, *node2_top)
root = tk.Tk()
canvas = NodeScene(root)
canvas.pack(expand=True, fill="both")
node = Node(canvas, "School", pos=(100, 150))
node1 = Node(canvas, "boys", pos=(80, 80))
node2 = Node(canvas, "girls", pos=(120, 30))
connection = Connection(canvas, node1, node, -20)
connection2 = Connection(canvas, node2, node, 20)
root.mainloop()

Manual Annotation with Tkinter

I am using Tkinter to import images with Openslide. I would like to integrate a manual annotation module into my program like this:
class ResizableCanvas(Canvas):
def __init__(self, parent, **kwargs):
Canvas.__init__(self, parent, **kwargs)
self.bind("<Configure>", self.on_resize)
self.height = self.winfo_reqheight()
self.width = self.winfo_reqwidth()
def on_resize(self, event):
wscale = float(event.width) / self.width
hscale = float(event.height) / self.height
self.width = event.width
self.height = event.height
self.config(width=self.width, height=self.height)
class ViewerTab:
def __init__(self, master, model, dim=800):
self.sideFrame = ttk.Frame(self.master, width=100)
self.coords = {"x":0,"y":0,"x2":0,"y2":0}
self.lines = []
def click(self):
self.coords["x"] = self.x
self.coords["y"] = self.y
self.lines.append(self.canvas.create_line(self.coords["x"],self.coords["y"],self.coords["x"],self.coords["y"]))
def drag(self):
# update the coordinates from the event
self.coords["x2"] = self.x
self.coords["y2"] = self.y
self.canvas.coords(self.lines[-1], self.coords["x"],self.coords["y"],self.coords["x2"],self.coords["y2"])
#self.canvas.bind("<Button-1>", self.dirbutton)
#self.canvas.bind("<B1-Motion>", self.move)
self.canvas.bind("<ButtonRelease-1>", self.nomove)
self.canvas.bind("<Button-2>", self.get_position)
self.canvas.bind("<ButtonPress-1>", self.click)
self.canvas.bind("<B1-Motion>", self.drag)
So if I got it right from the comments, the issue is to be able to both pan the slide and draw on it using binding to mouse clicks and motion. There are several way to do that, for instance:
Use radiobuttons so that the user selects the "mode": either pan or annotate. Here is a small example based on https://stackoverflow.com/a/50129744/6415268 for the drawing part. The click() and drag() functions do different actions depending on the selected mode (stored a the StringVar).
import tkinter as tk
coords = {"x": 0, "y": 0, "x2": 0, "y2": 0}
# keep a reference to all lines by keeping them in a list
lines = []
def click(event):
if mode.get() == "pan":
canvas.scan_mark(event.x, event.y)
else:
# define start point for line
coords["x"] = canvas.canvasx(event.x)
coords["y"] = canvas.canvasy(event.y)
# create a line on this point and store it in the list
lines.append(canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"]))
def drag(event):
if mode.get() == "pan":
canvas.scan_dragto(event.x, event.y, gain=1)
else:
# update the coordinates from the event
coords["x2"] = canvas.canvasx(event.x)
coords["y2"] = canvas.canvasy(event.y)
# Change the coordinates of the last created line to the new coordinates
canvas.coords(lines[-1], coords["x"], coords["y"], coords["x2"], coords["y2"])
root = tk.Tk()
mode = tk.StringVar(root, "pan")
toolbar = tk.Frame(root)
toolbar.pack(fill='x')
tk.Radiobutton(toolbar, text="Pan",
variable=mode, value="pan").pack(side='left')
tk.Radiobutton(toolbar, text="Annotate",
variable=mode, value="annotate").pack(side='left')
canvas = tk.Canvas(root, bg="white")
canvas.create_rectangle(0, 0, 50, 50, fill='red')
canvas.create_rectangle(400, 400, 450, 450, fill='blue')
canvas.pack(fill='both')
canvas.bind("<ButtonPress-1>", click)
canvas.bind("<B1-Motion>", drag)
root.mainloop()
Another possibility is to use different bindings for the two kinds of actions using event modifiers (e.g. pressing the Ctrl, Shift or Alt key). For instance, the panning can be bound to Ctrl + mouse events while the drawing happens on simple mouse clicks and motion.
import tkinter as tk
coords = {"x": 0, "y": 0, "x2": 0, "y2": 0}
# keep a reference to all lines by keeping them in a list
lines = []
def draw_click(event):
# define start point for line
coords["x"] = canvas.canvasx(event.x)
coords["y"] = canvas.canvasy(event.y)
# create a line on this point and store it in the list
lines.append(canvas.create_line(coords["x"], coords["y"], coords["x"], coords["y"]))
def draw_drag(event):
# update the coordinates from the event
coords["x2"] = canvas.canvasx(event.x)
coords["y2"] = canvas.canvasy(event.y)
# Change the coordinates of the last created line to the new coordinates
canvas.coords(lines[-1], coords["x"], coords["y"], coords["x2"], coords["y2"])
root = tk.Tk()
toolbar = tk.Frame(root)
toolbar.pack(fill='x')
canvas = tk.Canvas(root, bg="white")
canvas.create_rectangle(0, 0, 50, 50, fill='red')
canvas.create_rectangle(400, 400, 450, 450, fill='blue')
canvas.pack(fill='both')
canvas.bind("<ButtonPress-1>", draw_click)
canvas.bind("<B1-Motion>", draw_drag)
canvas.bind('<Control-ButtonPress-1>', lambda event: canvas.scan_mark(event.x, event.y))
canvas.bind("<Control-B1-Motion>", lambda event: canvas.scan_dragto(event.x, event.y, gain=1))
root.mainloop()

How to implement a mouse hovering callback on canvas items in tkinter?

I used the code below which I found on internet to implement a mouse hovering action in python:
from tkinter import *
import numpy as np
class rect:
def __init__(self, root):
self.root = root
self.size = IntVar()
self.canvas = Canvas(self.root, width=800, height=300)
self.scale = Scale(self.root, orient=HORIZONTAL, from_=3, to=20, tickinterval=1, variable=self.size)
self.scale.bind('<ButtonRelease>', self.show)
self.canvas.bind('<Motion>', self.motion)
self.board = []
self.array = np.zeros((self.scale.get(),self.scale.get())).tolist()
self.canvas.pack()
self.scale.pack()
def motion(self,event):
if self.canvas.find_withtag(CURRENT):
current_color = self.canvas.itemcget(CURRENT, 'fill')
self.canvas.itemconfig(CURRENT, fill="cyan")
self.canvas.update_idletasks()
self.canvas.after(150)
self.canvas.itemconfig(CURRENT, fill=current_color)
def show(self,event):
self.canvas.delete('all')
x = 50
y = 50
row = []
self.board.clear()
for i in range(self.scale.get()):
row = []
for j in range(self.scale.get()):
rectangle = self.canvas.create_rectangle(x, y, x + 50, y + 50, fill='red')
x += 50
row.append(rectangle)
x -= j*50
y +=50
self.board.append(row)
print(self.board)
root = Tk()
a = rect(root)
root.mainloop()
The problem with the execution is that the color of the item changes to blue only for a limited time.
I need the color of each item in the canvas to be changed whenever I enter its zone and remain blue until the mouse is leaving the item.
You can pass the argument activefill when creating your rectangle.
From effboot.org:
Fill color to use when the mouse pointer is moved over the item, if
different from fill.
To do so, replace:
rectangle = self.canvas.create_rectangle(x, y, x + 50, y + 50, fill='red')
By:
rectangle = self.canvas.create_rectangle(x, y, x + 50, y + 50, fill='red', activefill='cyan')
This removes the need to bind Motion to your canvas, and also makes the code noticebly shorter:
from tkinter import *
import numpy as np
class rect:
def __init__(self, root):
self.root = root
self.size = IntVar()
self.canvas = Canvas(self.root, width=800, height=300)
self.scale = Scale(self.root, orient=HORIZONTAL, from_=3, to=20, tickinterval=1, variable=self.size)
self.scale.bind('<ButtonRelease>', self.show)
self.board = []
self.array = np.zeros((self.scale.get(),self.scale.get())).tolist()
self.canvas.pack()
self.scale.pack()
def show(self,event):
self.canvas.delete('all')
x = 50
y = 50
row = []
self.board.clear()
for i in range(self.scale.get()):
row = []
for j in range(self.scale.get()):
rectangle = self.canvas.create_rectangle(x, y, x + 50, y + 50, fill='red', activefill='cyan')
x += 50
row.append(rectangle)
x -= j*50
y +=50
self.board.append(row)
print(self.board)
root = Tk()
a = rect(root)
root.mainloop()
I changed motion method and added self.last = None to __init__ method:
from tkinter import *
import numpy as np
class rect:
def __init__(self, root):
self.root = root
self.size = IntVar()
self.canvas = Canvas(self.root, width=800, height=300)
self.scale = Scale(self.root, orient=HORIZONTAL, from_=3, to=20, tickinterval=1, variable=self.size)
self.scale.bind('<ButtonRelease>', self.show)
self.canvas.bind('<Motion>', self.motion)
self.board = []
self.array = np.zeros((self.scale.get(),self.scale.get())).tolist()
self.canvas.pack()
self.scale.pack()
self.last = None
def motion(self, event):
temp = self.canvas.find_withtag(CURRENT)
if temp == self.last:
self.canvas.itemconfig(CURRENT, fill="cyan")
self.canvas.update_idletasks()
else:
self.canvas.itemconfig(self.last, fill="red")
self.last = temp
def show(self,event):
self.canvas.delete('all')
x = 50
y = 50
row = []
self.board.clear()
for i in range(self.scale.get()):
row = []
for j in range(self.scale.get()):
rectangle = self.canvas.create_rectangle(x, y, x + 50, y + 50, fill='red')
x += 50
row.append(rectangle)
x -= j*50
y +=50
self.board.append(row)
print(self.board)
root = Tk()
a = rect(root)
root.mainloop()

Global tkinter entry box has no attribute 'get' [duplicate]

This question already has answers here:
Tkinter: AttributeError: NoneType object has no attribute <attribute name>
(4 answers)
Closed 7 years ago.
Let me start by saying I am new to Linux. I am working on modifying some code from github. The original program runs fine(a strip chart). When I try to enter the variables from the entry boxes I receive the following error:
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python3.2/tkinter/__init__.py", line 1426, in __call__
return self.func(*args)
File "/home/pi/SCRAP.py", line 49, in show_entry_fields
frequency = float (e1.get())
AttributeError: 'NoneType' object has no attribute 'get'
The code I have added is between the two editing start and editing end comments. Any help is greatly appreciated. The code I have is as follows:
from tkinter import *
import math, random, threading, time
class StripChart:
def __init__(self, root):
self.gf = self.makeGraph(root)
self.cf = self.makeControls(root)
self.gf.pack()
self.cf.pack()
self.Reset()
def makeGraph(self, frame):
self.sw = 1000
self.h = 200
self.top = 2
gf = Canvas(frame, width=self.sw, height=self.h+10,
bg="#002", bd=0, highlightthickness=0)
gf.p = PhotoImage(width=2*self.sw, height=self.h)
self.item = gf.create_image(0, self.top, image=gf.p, anchor=NW)
return(gf)
def makeControls(self, frame):
cf = Frame(frame, borderwidth=1, relief="raised")
Button(cf, text="Run", command=self.Run).grid(column=2, row=4)
Button(cf, text="Stop", command=self.Stop).grid(column=4, row=4)
Button(cf, text="Reset", command=self.Reset).grid(column=6, row=4)
#editing start
Button(cf, text="Cycle", command=self.show_entry_fields).grid(column=7, row=4)
Label(cf, text="Frequency(Hz)").grid(column=1, row=2)
Label(cf, text="P-P Current(mA)").grid(column=1, row=3)
global e1,e2
e1=Entry(cf).grid(column=2, row=2)
e2=Entry(cf).grid(column=2, row=3)
#editing end
self.fps = Label(cf, text="0 fps")
self.fps.grid(column=2, row=5, columnspan=5)
return(cf)
#editing start
def show_entry_fields(self):
#print("Frequency: %s\nMilliamps: %s\n" % (e1.get(),e2.get()))
frequency = float (e1.get())
currrent = float (e2.get())
#print(check_var.get())
print(frequency+1)
print(current+1)
#editing end
def Run(self):
self.go = 1
for t in threading.enumerate():
if t.name == "_gen_":
print("already running")
return
threading.Thread(target=self.do_start, name="_gen_").start()
def Stop(self):
self.go = 0
for t in threading.enumerate():
if t.name == "_gen_":
t.join()
def Reset(self):
self.Stop()
self.clearstrip(self.gf.p, '#345')
def do_start(self):
t = 0
y2 = 0
tx = time.time()
while self.go:
y1 = 0.2*math.sin(0.02*math.pi*t)
y2 = 0.9*y2 + 0.1*(random.random()-0.5)
self.scrollstrip(self.gf.p,
(0.25+y1, 0.25, 0.7+y2, 0.6, 0.7, 0.8),
( '#ff4', '#f40', '#4af', '#080', '#0f0', '#080'),
"" if t % 65 else "#088")
t += 1
if not t % 100:
tx2 = time.time()
self.fps.config(text='%d fps' % int(100/(tx2 - tx)))
tx = tx2
# time.sleep(0.001)
def clearstrip(self, p, color): # Fill strip with background color
self.bg = color # save background color for scroll
self.data = None # clear previous data
self.x = 0
p.tk.call(p, 'put', color, '-to', 0, 0, p['width'], p['height'])
def scrollstrip(self, p, data, colors, bar=""): # Scroll the strip, add new data
self.x = (self.x + 1) % self.sw # x = double buffer position
bg = bar if bar else self.bg
p.tk.call(p, 'put', bg, '-to', self.x, 0,
self.x+1, self.h)
p.tk.call(p, 'put', bg, '-to', self.x+self.sw, 0,
self.x+self.sw+1, self.h)
self.gf.coords(self.item, -1-self.x, self.top) # scroll to just-written column
if not self.data:
self.data = data
for d in range(len(data)):
y0 = int((self.h-1) * (1.0-self.data[d])) # plot all the data points
y1 = int((self.h-1) * (1.0-data[d]))
ya, yb = sorted((y0, y1))
for y in range(ya, yb+1): # connect the dots
p.put(colors[d], (self.x,y))
p.put(colors[d], (self.x+self.sw,y))
self.data = data # save for next call
def main():
root = Tk()
root.title("StripChart")
app = StripChart(root)
root.mainloop()
main()
You called grid() when you defined e1. Since you did this, e1 is set to what grid() returned, which is None:
>>> e1=Entry(cf).grid(column=2, row=2)
>>> e1
None
Instead, create the widget and then grid it on a separate line:
>>> e1=Entry(cf)
>>> e1.grid(column=2, row=2)
>>> e1
<Tkinter.Entry instance at 0x01D43BC0>

Categories

Resources