How to make tkinter frames in a loop and update object values? - python

I have a class called Bones I have 5 Bones in my skeleton dictionary. However in my actual implementation there are 300+ bones, that's why I am asking this question today on stackoverflow.
Each Bone has:
ID: An int to identify a bone
w: w position (float between -1 and 1)
x: x position (float between -1 and 1)
y: y position (float between -1 and 1)
z: z position (float between -1 and 1)
Bone.py
INCREMENT = 0.01
class Bone:
def __init__(self, boneId, w, x, y, z):
self.id = boneId
self.w = w
self.x = x
self.y = y
self.z = z
def shouldChangePos(self, num):
if (num >= 1 or num <= -1):
return False
return True
def incrW(self):
if(self.shouldChangePos(self.w)):
self.w = self.w + INCREMENT
def decrW(self):
if(self.shouldChangePos(self.w)):
self.w = self.w - INCREMENT
def incrX(self):
if(self.shouldChangePos(self.x)):
self.x = self.x + INCREMENT
def decrX(self):
if(self.shouldChangePos(self.x)):
self.x = self.x - INCREMENT
def incrY(self):
if(self.shouldChangePos(self.y)):
self.y = self.y + INCREMENT
def decrY(self):
if(self.shouldChangePos(self.y)):
self.y = self.y - INCREMENT
def incrZ(self):
if(self.shouldChangePos(self.z)):
self.z = self.z + INCREMENT
def decrZ(self):
if(self.shouldChangePos(self.z)):
self.z = self.z - INCREMENT
Explanation of the problem
I am trying to make a tkinter GUI that looks something like this:
Legend:
Green - represents a Frame (just my annotation to explain)
Red - are attributes of the object (just my annotation to explain)
Black - are methods of the object (just my annotation to explain)
Blue - are text and buttons displayed to me
As you can see, it shows the ID, w, x, y, z. And under it, there is a + button and a - button. Each time these buttons get clicked, I want to decrease the corresponding value in the object and update the tkinter number displayed. I know how to do this manually, but as per my requirement, I have 300+ Bones. I cannot make these frames manually.
How can I create these frames in a loop and update the value displayed on the GUI and object when a + or - button is clicked?
main.py
from tkinter import *
from tkinter import ttk
from Bone import *
skeleton = {
1: Bone(-0.42, 0.1, 0.02, 0.002, 0.234),
4: Bone(4, 0.042, 0.32, 0.23, -0.32),
11: Bone(11, 1, -0.23, -0.42, 0.42),
95: Bone(95, -0.93, 0.32, 0.346, 0.31),
}
root = Tk()
root.geometry('400x600')
boneID = Label(root, text="ID: 1")
boneID.grid(row=1, column=1, sticky=W, padx=(0, 15))
w = Label(root, text="-0.42")
w.grid(row=1, column=2, sticky=W)
x = Label(root, text="0.02")
x.grid(row=1, column=4, sticky=W)
y = Label(root, text="0.002")
y.grid(row=1, column=6, sticky=W)
z = Label(root, text="0.234")
z.grid(row=1, column=8, sticky=W)
wPlusBtn = Button(root, text="+")
wPlusBtn.grid(row=2, column=2)
wMinusBtn = Button(root, text="-")
wMinusBtn.grid(row=2, column=3, padx=(0, 15))
xPlusBtn = Button(root, text="+")
xPlusBtn.grid(row=2, column=4)
xMinusBtn = Button(root, text="-")
xMinusBtn.grid(row=2, column=5, padx=(0, 15))
yPlusBtn = Button(root, text="+")
yPlusBtn.grid(row=2, column=6)
yMinusBtn = Button(root, text="-")
yMinusBtn.grid(row=2, column=7, padx=(0, 15))
zPlusBtn = Button(root, text="+")
zPlusBtn.grid(row=2, column=8)
zMinusBtn = Button(root, text="-")
zMinusBtn.grid(row=2, column=9, padx=(0, 15))
root.mainloop()

TL;DR - break your one big problem into several smaller problems, and then solve each problem separately.
The main window
Start by looking at the overall design of the UI. You have two sections: a panel holding bones, and a panel holding random text. So the first thing I would do is create these panels as frames:
root = tk.Tk()
bonePanel = tk.Frame(root, background="forestgreen", bd=2, relief="groove")
textPanel = tk.Frame(root, background="forestgreen", bd=2, relief="groove")
Of course, you also need to use pack or grid to lay them out on the window. I recommend pack since there are only two frames and they are side-by-side.
Displaying bones
For the bone panel, you appear to have a single row for each bone. So, I recommend creating a class to represent each row. It can inherit from Frame, and be responsible for everything that goes on inside that row. By inheriting from Frame, you can treat it just like a custom widget with respect to laying it out on the screen.
The goal is for your UI code to look something like this:
bones = (
Bone(boneId=1, w=-0.42, x=0.02, y=0.002, z=0.234),
Bone(boneId=4, w=0.042, x=0.32, y=0.23, z=-0.32),
Bone(boneId=11, w=1, x=-0.23, y=-0.42, z=0.42),
...
)
bonePanel = tk.Frame(root)
for bone in bones:
bf = BoneFrame(bonePanel, bone)
bf.pack(side="top", fill="x", expand=True)
Again, you can use grid if you want, but pack seems like the natural choice since the rows are stacked top-to-bottom.
Displaying a single bone
Now, we need to tackle what each BoneFrame does. It appears to be made up of five sections: a section to display the id, and then four nearly identical sections for the attributes. Since the only difference between these sections is the attribute they represent, it makes sense to represent each section as an instance of a class. Again, if the class inherits from Frame we can treat it like it was a custom widget.
This time, we should pass in the bone, and perhaps a string telling it which id to update.
So, it might start out looking something like this:
class BoneFrame(tk.Frame):
def __init__(self, master, bone):
tk.Frame.__init__(self, master)
self.bone = bone
idlabel = tk.Label(self, text="ID: {}".format(bone.id))
attr_w = BoneAttribute(self, self.bone, "w")
attr_x = BoneAttribute(self, self.bone, "x")
attr_y = BoneAttribute(self, self.bone, "y")
attr_z = BoneAttribute(self, self.bone, "z")
pack is a good choice here since these sections are all lined up left-to-right, but you could use grid if you prefer. The only real difference is that using grid takes a couple more lines of code to configure row and column weights.
Widgets for the attribute buttons and labels
Finally, we have to tackle the BoneAttribute class. This is where we finally add the buttons.
It's pretty straight-forward and follows the same pattern: create the widgets, then lay them out. There's a bit more, though. We need to hook up the buttons to update the bone, and we also need to update the label whenever the bone changes.
I won't go into all of the details. All you need to do is to create a label, a couple of buttons, and functions for the buttons to call. Plus, we want a function to update the label when the value changes.
Let's start with tha function to update the label. Since we know the name of the attribute, we can do a simple lookup to get the current value and change the label:
class BoneAttribute(tk.Frame):
...
def refresh(self):
value = "{0:.4f}".format(getattr(self.bone, self.attr))
self.value.configure(text=value)
With that, we can update the label whenever we want.
Now it's just a matter of defining what the buttons do. There are better ways to do it, but a simple, straight-forward way is to just have some if statements. Here's what the increment function might look like:
...
plus_button = tk.Button(self, text="+", command=self.do_incr)
...
def do_incr(self):
if self.attr == "w":
self.bone.incrW()
elif self.attr == "x":
self.bone.incrX()
elif self.attr == "y":
self.bone.incrY()
elif self.attr == "z":
self.bone.incrZ()
self.refresh()
The do_decr function is identical, except that it calls once of the decrement functions.
And that's about it. The key point here is to break down your larger problem into smaller problems, and then tackle each smaller problem one at a time. Whether you have three bones or 300, the only extra code you have to write is where you initially create the bone objects. The UI code stays exactly the same.

There are two issues here: creating the frames in a loop, and updating the values upon a press on the +/- buttons.
To handle the frame issue, I suggest that you create a BoneFrame class that holds all the widgets (buttons and labels) related to one Bone instance.
There, you can also bind the buttons to the Bone methods so as to act on the values.
Something like that - I'm sure you'll know how to complete this with the other variables and the grid coordinates you want
class BoneFrame(tk.Frame):
def __init__(self, parent, bone):
super().__init__(parent)
# Create your widgets
self.x_label = tk.Label(self, text=bone.x)
self.x_decr_button = tk.Button(self, text="-", action=bone.decr_x)
self.x_incr_button = tk.Button(self, text="+", action=bone.incr_x)
...
# Then grid all the widgets as you want
self.x_label.grid()
...
Then you can easily iterate over your dict of Bones, instantiate BoneFrame every time, and pack or grid that instance to a parent container.
Maybe you'll want to add a bone_id to the parameters of BoneFrame.__init__ and pass it in the loop.
# In your main script
for bone_id, bone in skeleton.items():
frame = BoneFrame(root, bone)
frame.pack()
For now, the values in the label never update.
That's because we just set their text once, and then we never update them.
Rather than binding the buttons directly to methods of Bone, we can define more complex methods in BoneFrame that achieve more logic, including updating the values, and also refreshing the widgets.
Here's one way to do it:
class BoneFrame(tk.Frame):
def __init__(self, parent, bone):
super().__init__(parent)
# Store the bone to update it later on
self.bone = bone
# Instantiate a StringVar in order to be able to update the label's text
self.x_var = tk.StringVar()
self.x_var.set(self.bone.x)
self.x_label = tk.Label(self, textvariable=self.x_var)
self.x_incr_button = tk.Button(self, text="+", action=self.incr_x)
...
def incr_x(self):
self.bone.incr_x()
self.x_var.set(self.bone.x)
So we need a StringVar to update the content of the label.
To sum it up, instead of binding the button to bone.incr_x, we bind it to self.incr_x, which allows us to do whatever we want upon a button press, that is 1. change the value in the Bone instance, and 2. update the value displayed by the label.

A usual way to address this kind of problem is to create functions (or class methods) to perform the repetitious bits of the code (i.e. the DRY principle of software engineering).
Ironically, doing this can itself be a little tedious as I quickly discovered trying to refactor your existing code to be that way — but below is the result which should give you a good idea of how it can be done.
Besides reducing the amount of code you have to write, it also simplifies making changes or adding enhancements because they only have be done in one spot. Often the trickiest thing is determining what arguments to pass the functions so they can do what it needs to be done in a generic way and avoiding hardcoded values.
from tkinter import *
from tkinter import ttk
from Bone import *
skeleton = {
1: Bone(1, -0.42, 0.02, 0.002, 0.234),
4: Bone(4, 0.042, 0.32, 0.23, -0.32),
11: Bone(11, 1, -0.23, -0.42, 0.42),
95: Bone(95, -0.93, 0.32, 0.346, 0.31),
}
def make_widget_group(parent, col, bone, attr_name, variable, incr_cmd, decr_cmd):
label = Label(parent, textvariable=variable)
label.grid(row=1, column=col, sticky=W)
def incr_callback():
incr_cmd()
value = round(getattr(bone, attr_name), 3)
variable.set(value)
plus_btn = Button(parent, text='+', command=incr_callback)
plus_btn.grid(row=2, column=col)
def decr_callback():
decr_cmd()
value = round(getattr(bone, attr_name), 3)
variable.set(value)
minus_btn = Button(parent, text='-', command=decr_callback)
minus_btn.grid(row=2, column=col+1, padx=(0, 15))
def make_frame(parent, bone):
container = Frame(parent)
boneID = Label(container, text='ID: {}'.format(bone.id))
boneID.grid(row=1, column=1, sticky=W, padx=(0, 15))
parent.varW = DoubleVar(value=bone.w)
make_widget_group(container, 2, bone, 'w', parent.varW, bone.incrW, bone.decrW)
parent.varX = DoubleVar(value=bone.x)
make_widget_group(container, 4, bone, 'x', parent.varX, bone.incrX, bone.decrX)
parent.varY = DoubleVar(value=bone.y)
make_widget_group(container, 6, bone, 'y', parent.varY, bone.incrY, bone.decrY)
parent.varZ = DoubleVar(value=bone.z)
make_widget_group(container, 8, bone, 'z', parent.varZ, bone.incrZ, bone.decrZ)
container.pack()
if __name__ == '__main__':
root = Tk()
root.geometry('400x600')
for bone in skeleton.values():
make_frame(root, bone)
root.mainloop()
Screenshot of it running:
BTW, I noticed a lot of repetition in the Bone.py module's code, which could probably be reduced in a similar manner.

Related

How to make array of Tkinter widgets OOP method?

I currently have an array of Tkinter widgets that are not object oriented:
Image_col = [
tk.Button(
frame,
image=root.render1,
relief="raised",
bg="light gray",
# width = ColWidths[j],
command=lambda y=y: change_img(y)
)
for y in range(0, rq)
]
In order to use a special function, in which I pass the widget self.ButtonA as an argument, (as I did in an oversimplified version of my app below) it seems that I will need to now take an object-oriented approach, and define a class that I can repeatedly use.
class MinCodeEx:
global waitingforImage
def __init__(self, master):
self.master = master
self.ButtonA = tk.Button(width=60,height=40,command = lambda: self.UpdateImg())
self.ButtonA.pack()
self.ButtonA.img = None
def UpdateImg(self):
newDialog = snipping_tool.AcquireImage(self.master, self.ButtonA)
How else could I attach a function like UpdateImage (above) to every tk.Button within Image_col? It doesn't seem possible without taking an OOP aproach. My question is this: How should I structure Image_col in an OOP way? This seems close, but it doesn't work for me. Something must be missing:
class ImageCol(tk.Button):
def __init__(self, master, y):
super().__init__(master)
self.master = master
self.y = y
self.btn = tk.Button(
frame,
image=root.render1,
relief="raised",
bg="blue",
#bg="light gray",
# width = ColWidths[j],
command=lambda y=y: self.change_img)
def change_img(self):
snipping_tool.AcquireImage(self.master, self.btn)
j: int = 1
Image_col = [
ImageCol(frame, y)
for y in range(0, rq)
]
Let me know if you need more of my code. It's challenging for me to shorten it into a minimum viable example in order to display it here.
Probably the most useful thing to note is that change_image(y) is supposed to change the image associated with the tkinter button with index = y.
I tested your code, and the function wasn't being runned. I have solved that by changing:
command=lambda y=y: self.change_img to command=self.change_img. You don't have to tell the class which button is it, as you already do it with self.
Just some features I removed in order to test (I don't think it should affect the program, but if it's still failing, check those first): I removed the button image (as I didn't have any), I replaced the change_img() content to a simple print with the buttonID (or y), replaced rq with a random integer, and i added import tkinter as tk; frame = tk.Tk() at the begining of the script and tk.mainloop() at the very end.

How to get closest tkinter canvas element on click?

I'm making a very simple python programming using tkinter. I want to draw some rectangles on a canvas and then when one clicks on a certain rectangle, show the tags of that rectangle. I can't get it to work. The problem seems to be that wherever I click on the canvas, the function get_closest returns 1. Any help is appreciated. This is my first time working with tkinter (and python for that matter), so any remarks about my code that aren't linked to the problems itself, are welcome as well!
import tkinter as tk
myrecs = [[None for j in range(4)] for i in range(4)]
class application:
def __init__(self, parent):
self.parent = parent
self.frame = tk.Frame(self.parent)
self.frame.grid(row=0)
self.quitbutton = tk.Button(self.frame, text = "Quit", command = lambda:quit())
self.quitbutton.grid(row=0, column = 0, sticky=tk.W + tk.E)
self.canvas = tk.Canvas(self.frame, width=200, height=200, bg = "blue")
self.canvas.bind("<ButtonPress-1>", self.buttonclick)
self.canvas.grid(row=1, columnspan = 2)
self.tag = self.canvas.create_text(10, 150, text="", anchor="nw")
self.makebutton = tk.Button(self.frame, text = "Make nice canvas", command = self.makecanvas)
self.makebutton.grid(row=0, column = 1, sticky = tk.W + tk.E)
def makecanvas(self):
for i in range(4):
for j in range(4):
myrecs[i][j] = self.canvas.create_rectangle(20*i, 20*j, 20*(i+1), 20*(j+1), tags=("rectangle", "i"+str(i), "j"+str(j)))
def buttonclick(self, event):
cnv = self.canvas
item = cnv.find_closest(cnv.canvasx(event.x), cnv.canvasy(event.y))[0]
tags = cnv.gettags(item)
cnv.itemconfigure(self.tag, text=tags[0])
if __name__ == "__main__":
root = tk.Tk()
root.title("Test")
app = application(root)
root.mainloop()
find_closest returns 1 means it's finding the very first element you created within the canvas, which in this case is the create_text.
Oddly enough, when you create_text with text="" it seems to be overtaking all your other elements. With a simple fix of text=" " it will now locate the closet rectangles on clicks.
With the other elements, when you assign option="" it actually disables (as far as I know) the option so instead of using its default values, you are actively telling tcl interpreter to not use it. This can be observed in other elements like create_rectangle(..., outline="") in which the default outline="black" will no longer apply, and you won't even get an outline. I have a feeling text="" yield a similar effect and for some reason basically covers the entire canvas area, so it causes find_closest to always return that element. Perhaps if you're lucky #BryanOakley (a tcl expert) can chime in on the backend reasoning.
In fact, if you tried find_above(item) you will notice that the text is consistently below your other elements drawn afterwards.
In short:
# Change this:
self.tag = self.canvas.create_text(10, 150, text="", anchor="nw")
# To this:
self.tag = self.canvas.create_text(10, 150, text=" ", anchor="nw")

Python+Tkinter: Redrawing a rectangle in a canvas based on some % value

I have spent an embarassing amount of hours looking for a way to do this... Its for a project I'm working on that has over one hundred canvas items that need to update from a text file. Here is a simple version of that:
I would like to update a rectangle drawn in a canvas item when I push a button. I found a real bad hack way to do it that involves a crazy amount of code but I know there has to be some better way.
from tkinter import *
class MyGUI:
def __init__(self, root):
frame = Frame(root)
frame.pack()
self.BoxFillPercent = 0 # the canvas items get their % fill from this value
self.changeButton = Button(frame, text='SB', command=self.changeRange)
self.changeButton.grid(row=1, column=1)
self.hAA = Canvas(frame, width=35, height=35, bg='light blue')
self.hAA.grid(row=2, column=2)
self.hAA.create_rectangle(0,0,self.BoxFillPercent*35,35, fill="pink")
self.hAA.create_text(15, 15, anchor='center', text='AA')
def changeRange(self):
self.BoxFillPercent = 0.5
# When I push the button change the fill amount to 0.5
? What do I need to add here to make this work ?
root = Tk()
b = MyGUI(root)
root.mainloop()
I have tried to use update and update_idletasks among a bunch of other things but I must be missing something.
Every item on a canvas has an id. You can use the itemconfig method of the canvas to change the item.
rect = self.hAA.create_rectangle(...)
...
self.hAA.itemconfig(rect, ...)
If you need to apply the same change to multiple objects, you can give those objects a common tag and then use the tag in place of the id:
rect1 = self.hAA.create_rectangle(..., tags=("special",))
rect2 = self.hAA.create_rectangle(..., tags=("special",))
...
self.hAA.itemconfigure("special", ...)

When using a scrolled canvas in Tkinter, columns of contained frame are cut off

I'm trying to make a Tkinter widget that contains a number of tables, which are currently frames with entries filled using the .grid method, which can be switched between by pressing buttons. My current attempt at a solution uses the following code:
from tkinter import *
def dot(root, num):
root.subframe.destroy()
root.subframe = TFrame(root, num)
root = Tk()
vscrollbar = Scrollbar(root,orient='vertical')
vscrollbar.grid(row=1,column=2,sticky=N+E+W+S)
root.defaultframe = MainFrame(root)
root.canvas = Canvas(root, yscrollcommand=vscrollbar.set)
root.subframe = Frame(root.canvas)
vscrollbar.config(command=root.canvas.yview)
root.canvas.grid(row=1,column=0)
root.subframe.grid(row=0,column=0)
where MainFrame has the following structure:
class MainFrame(Frame):
def __init__(self, root):
Frame.__init__(self, root)
self.grid(row=0,column=0)
b1 = Button(self, text='table 1', command=lambda: dot(root, 0))
b2 = Button(self, text='table 2', command=lambda: dot(root, 1))
b1.grid(row=0, column=0, sticky=N+E+W+S)
b2.grid(row=0, column=1, sticky=N+E+W+S)
and TFrame:
class TFrame(Frame):
def __init__(self, foor, num):
Frame.__init__(self, root.canvas)
for i in range(12):
self.grid_columnconfigure(i, minsize=50)
for x in range(12):
for y in range(20):
label = Label(self, text=num)
label.grid(row=y,column=x,sticky=N+E+W+S)
root.canvas.create_window((0,0),window=self,anchor='nw')
root.canvas.configure(scrollregion=root.canvas.bbox('all'))
When I run the code, pressing the buttons loads the tables, which scroll in the vertical as expected. But only the first 8 columns or so are visible, no matter how the window is resized. Changing the width of the MainFrame by adding empty labels and the like does not affect the size of the TFrame created, even if it is several times wider than the 8 columns the TFrame ends up being. While I could have a somewhat tolerable solution by adding a horizontal scroll bar as well as the vertical, my experiences so far with scrolling in tkinter in general have been negative enough that I hope to avoid using it by any possible means.
Okay, found a solution. It turns out there weren't columns being cut off, the whole canvas was being cut off, and all my test cases just happened to have exactly the right number of columns vs column width that it looked like the columns after the first 8 were being cut off.
Changing:
root.canvas.grid(row=1,column=0)
to
root.canvas.grid(row=1,column=0,sticky=N+E+W+S)
fixed the problem.

Tkinter Resizable Objects Python Canvas

I'm trying to have it so that multiple objects on a canvas in Tkinter can be resized/repositioned using a spinbox, with the value in the spinbox being used as a multiplier to the original coordinates. To make matters slightly more complicated, the spinbox is not visible by default, it's in a Toplevel window that can be opened when a button is pressed.
To summarise:
I need to alter the coordinates of objects on a canvas using a spinbox value as a multiplier (or otherwise) which itself is in a Toplevel window, and have these alterations displayed in 'real time' on the canvas.
For context, I've included the key peripheral code responsible for setting up the objects etc.
Essential Parts of UI module:
import Canvas_1 (module for drawing shapes)
root=Tk()
#root geometry, title set up
#UI then commands set up
canvasBlank=Canvas(root, width... etc) #Blank canvas that is drawn at start
canvasBlank.grid(row... etc)
canvasBlank.bind('Button-3', rightclickcanvas) #Right click function that opens a popup for canvas options
#Other misc commands, I'm using a menubar with drop down options over actual Tk.Buttons
#'New' option in menubar has Command to create objects in UI like:
def createObject():
Objects=MyObjects(root, width... etc)
Objects.grid(row... etc) #Same as layout for canvasBlank
Objects.bind('<Button-3>', rightclickcanvas)
Objectslist.append(Objects) #Stop garbage disposal and makes sure the canvas displays
-The MyObjects Class (in seperate module) has a form similar to:
from Coordinate_Generator import * #imports coordinate arrays
class MyObjects(tk.Canvas)
def __init__(self, master, **kw)
tk.Canvas.__init__(self, master, **kw)
self.create_oval(coordinates[0], dimensions[0], fill... etc)
self.create_oval(coordinates[1], dimensions[1], fill... etc)
#A series of bindings relating to moving objects under mouse clicks
The coordinates are determined using 'a', an arbitrary value. I try to multiply:
scaler=[]
a=70*scaler[-1]
This method doesn't seem to work either, and if it did, it also means potentially drawing a very large number of canvases over one another which I would like to avoid. I'm hoping this demonstrates the method I need to try and use more clearly. I have written a bit of code using the advice given, and while it may be useful for another part of the program I'm planning, it doesn't quite achieve what I am after. So I've cobbled together this 'Demonstration'to maybe illustrate what it is I'm trying to do.
Working Code (SOLUTION)
from Tkinter import *
from numpy import *
import Tkinter as tk
scale=1
class Demonstrator:
def __init__(self, master=None):
global full_coordinates, dimensions, scale
self.master=master
self.master.title( "Demonstrator 2")
self.master.grid()
self.master.rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
self.canvas = Canvas(self.master, width=300, height=300, bg='grey')
self.canvas.grid(row=0, rowspan=3, column=0)
self.canvas.create_rectangle(full_coordinates[0],dimensions[0], activefill='blue', fill='red')
self.canvas.create_rectangle(full_coordinates[1],dimensions[1], activefill='blue', fill='red')
self.canvas.create_line(full_coordinates[0],full_coordinates[1], fill='red')
a=9*scale
Originx=10
Originy=35
coordinates1=[]
coordinates2=[]
x,y,i=Originx,Originy,1
x1,y1,i=Originx,Originy,1
while len(coordinates1)<=25:
coordinates1.append((x,y))
coordinates2.append((x1,y1))
i+=1
if i % 2 == 0:
x,y=x+a,y
x1,y1=x1,y1+a
else:
x,y=x,y+a
x1,y1=x1+a,y1
full_coordinates=list(set(coordinates1+coordinates2))
b=array(full_coordinates)
k=b+10
dimensions=k.tolist()
class Settings:
def __init__(self, parent):
top = self.top = tk.Toplevel(parent)
self.top.title('Settings')
self.spinbox_Label= tk.Label(top, text='Change Scale Factor?')
self.spinbox_Label.grid(row=0, column=0, columnspan=2)
self.spinbox_Label= tk.Label(top, width=30, text='Scale factor:')
self.spinbox_Label.grid(row=1, column=0)
self.spinbox= tk.Spinbox(top, from_=1, to=10, increment=0.1, command=self.change)
self.spinbox.grid(row=1, column=1)
def change(self):
global scale
scale=float(self.spinbox.get())
MG=Demonstrator(root) #This just generates a new Demonstrator with original coordinates
def onClick():
inputDialog = Settings(root)
root.wait_window(inputDialog.top)
def onClick2():
print scale
class coords:
global full_coordinates, dimensions, scale
print scale
a=9*scale
Originx=10
Originy=35
coordinates1=[]
coordinates2=[]
x,y,i=Originx,Originy,1
x1,y1,i=Originx,Originy,1
while len(coordinates1)<=25:
coordinates1.append((x,y))
coordinates2.append((x1,y1))
i+=1
if i % 2 == 0:
x,y=x+a,y
x1,y1=x1,y1+a
else:
x,y=x,y+a
x1,y1=x1+a,y1
full_coordinates=list(set(coordinates1+coordinates2))
b=array(full_coordinates)
k=b+10
dimensions=k.tolist()
root=Tk()
root.minsize=(700,700)
root.geometry=('600x600')
MG=Demonstrator(root)
mainButton2 = tk.Button(root, width=20, text='Print "scale"', command=onClick2)
mainButton2.grid(row=1, column=1)
mainButton = tk.Button(root, width=20, text='Settings', command=onClick)
mainButton.grid(row=2, column=1)
root.mainloop()
mainButton2.grid(row=1, column=1)
mainButton = tk.Button(root, width=20, text='Settings', command=onClick)
mainButton.grid(row=2, column=1)
root.mainloop()
The Question:
What is the best way to go about changing the size (by altering the coordinates) of the objects on the canvas using a spinbox?
I hope this is enough to info, of course I can supply more if necessary. I also apologise in advance for the formatting of this question, I'm new to this :)
(Solution added)
Any help would be awesome. Cheers.
Mark
There's nothing special about the solution. You simply need to define a callback for the spinbox that adjusts the coordinates of the canvas items (which can be done with the coords method of the canvas).
First, you might want to create a dict to contain the base width and height of each item. The keys to this dictionary could also be tags associated with canvas items. For example:
self.base_dimensions = {
"obj1": (10,10),
"obj2": (20,20),
...
}
Next, create items on a canvas using those keys as tags. For example:
...
self.canvas.create_rectangle(..., tags=("obj1",))
self.canvas.create_rectangle(..., tags=("obj2",))
...
Finally, you can save the spinbox widgets in a dictionary using the same keys (so you can associate a spinbox with a canvas object), and assign the spinbox a callback to do the resizing. For example:
self.spinbox = {
"obj1": tk.Spinbox(..., command=lambda self.do_resize("obj1")),
"obj2": tk.Spinbox(..., command=lambda self.do_resize("obj2")),
...
}
Given a tag, your callback can use that to get the reference to the spinbox widget and get it's value, and then use the tag to tell the canvas object which item(s) to resize. For example:
def do_scale(self, tag):
factor = int(self.spinbox[tag].get())
(width, height) = self.default[tag]
(x0,y0,x1,y1) = self.canvas.coords(tag)
width = factor * width
height = factor * height
x1 = x0 + width
y1 = y0 + height
self.canvas.coords(tag, x0,y0,x1,y1)
Of course, there are endless ways to organize your data; what I've shown isn't the best way nor the only way. It might not even work for how you have your code organized. Whatever you choose, it boils down to being able to get the value out of the spinbox and using it to adjust the coordinates of the canvas items.

Categories