Unable to bind tkinter slider properly to functions - python

I am trying to make a naive city generator. I have been successful at making the blocks of specified heights.
Also, I have been able to write a script that changes the height of a block using the slider.
Problem
I am not able to do both things together. (I think I can solve this because the error is only about referencing before assignment)
Using a horizontal slider, I want to increase the number of buildings (visualize as increasing the number of bars on the x-axis by reducing the width). But this code just does not work, I mean, it is not even raising errors.
Code so far
from tkinter import *
import random
bars = 5
margin = 30
heights= [5, 2, 1, 8, 4]
blocks = []
length = 600
breadth = 360
def draw():
aligner = margin
width = (breadth - margin*2)/bars
for height in heights :
block = {"height" : height }
block['rect'] = canvas.create_rectangle(aligner,550-50*height,aligner+width,550, fill="green")
block['color'] = 'green'
blocks.append(block)
aligner += width
root.update()
def wid(e):
bars = int(e)
blocks = []
heights = []
for i in range(bars):
h = random.random()*10
heights.append(h)
draw()
def h_rand(e):
factor = e
heights = [i * factor for i in heights]
draw()
root = Tk()
width = 60
aligner = 30
slider_y = Scale(root, from_=1 , to=8, bg="blue",command = h_rand)
slider_y.pack(side=LEFT)
canvas = Canvas(root,height=length,width=breadth)
canvas.pack()
for height in heights :
block = {"height" : height }
block['rect'] = canvas.create_rectangle(aligner,550-50*height,aligner+width,550, fill="green")
block['color'] = 'green'
blocks.append(block)
aligner += width
slider_x = Scale(root, from_=50 , to=200, orient = HORIZONTAL, bg="blue",command = wid)
slider_x.pack(side=TOP)
root.mainloop()
I am sorry for the "not so descriptive formatting" of the question (happy to give further details in the comments). It is bothering me for a while now and I am just frustrated.

The problem is that heights in wid is a local variable. You're changing the value inside the function, but the global variable is left unchanged.
You either need to declare heights as global inside the function, or use the global variable without replacing it with a new list.
To declear it as global, add global heights at the top of the function where it's being modified:
def wid(e):
global heights, blocks
...
You can use the clear method of a list to remove the contents without resetting the variable itself. To do that, replace heights = [] with heights.clear() and the same for blocks:
def wid(e):
bars = int(e)
print("bars:", bars)
blocks.clear()
heights.clear()
...
You also have the problem that you're not deleting the old canvas items before creating the new. Your draw function needs to destroy the old elements. You can do that with canvas.delete("all").

Related

How to enlarge and reduce an already drawn lines, using certain buttons?

Suppose I have the following program made with Tkinter in Python, in which you can click to draw any shape.
import tkinter
canvas = tkinter.Canvas(width=500, height=500, bg='white')
canvas.pack()
line = ()
def draw():
canvas.delete('all')
if len(line) >= 4:
canvas.create_line(line, fill='red', width=2)
def newline(coordinates):
global line
line = line + (coordinates.x, coordinates.y)
draw()
canvas.bind('<Button-1>', newline)
I have tried this for smaller image, but it didn't work.
def reduced():
line_reduced = ()
for i in newline:
line_reduced += (i/2,)
canvas.delete('all')
canvas.create_line(line_reduced, fill='red', width=4, tags='foto1')
I would need to add it so that this shape can then be reduced or enlarged using two keys. The image would therefore remain the same (shape), it would only be reduced / enlarged.
I will be grateful for any advice.
If you are trying to scale a bunch of lines on a canvas you could use scale, but then everything new that you add to the canvas would also have to be scaled or it starts getting weird. What you likely really need is math. Specifically this equation N = Scale * (N - Pn) + Pn. Below is a simple drawing interface with the buttons you requested. Primarily it's just a bunch of storing points and creating lines. The part of interest to you is this line:
line[n] = scale * (point - center) + center.
Lines are stored as [x1, y1, x2, y2] in my example code (the beginning and end of a line). line would refer to such a list. line[n] refers to each x and y value in that list as n is incremented. That being said:
line[n] = scale * (point - center) + center is really saying to subtract the center from the point, multiply it by the scale number, and then add the center back in so the scaled point stays it's scaled distance from the center.
Let's run through this one time so you definitely understand.
Chalkboard:
x1 = 100
center = 200
scale = 2
formula: x1 = scale * (x1 - center) + center
1 : x1 = 2 * (100 - 200) + 200
2 : x1 = 2 * (-100) + 200
3 : x1 = -200 + 200
4 : x1 = 0
The old x1 was 100 away from the center
now x1 is 200 away from the center
x1 is scale times further from the center than it was before
Code:
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=800, height=600, bg='white')
canvas.pack()
#for storing line canvas ids and line coordinates
line_ids, lines = [], []
#line properties ~ width and fill
global w, f
w, f = 2, 'red'
#draw a line
def draw(event):
if event.type is tk.EventType.ButtonPress:
#store line start coordinate
lines.append([event.x, event.y])
#create a dummy line
line_ids.append(canvas.create_line(*lines[-1], *lines[-1], fill=f, width=w))
elif event.type is tk.EventType.Motion:
#keep deleting and redrawing the current line til you release the mouse
canvas.delete(line_ids.pop())
line_ids.append(canvas.create_line(*lines[-1], event.x, event.y, fill=f, width=w))
elif event.type is tk.EventType.ButtonRelease:
#append the end coordinate to the last line
lines[-1] += [event.x, event.y]
#add mouse events to canvas for drawing functionality
for event in ['<B1-Motion>', '<Button-1>', '<ButtonRelease-1>']:
canvas.bind(event, draw)
#scale lines
def scale_lines(scale):
#remove all canvas references to lines
canvas.delete("all")
line_ids = []
#traverse every line
for i, line in enumerate(lines):
#traverse every point in THIS line
for n, point in enumerate(line):
#toggle between center x and center y depending if point is an x or y
center = canvas.winfo_width()/2 if not n%2 else canvas.winfo_height()/2
#scale this point
line[n] = scale * (point - center) + center
#create a new line with scaled points
line_ids.append(canvas.create_line(*line, fill=f, width=w))
#increase/decrease buttons
tk.Button(root, command=lambda: scale_lines(.5), text='decrease').pack(side='left')
tk.Button(root, command=lambda: scale_lines(2), text='increase').pack(side='left')
root.mainloop()

tkinter unwanted function overwriting

so basically i have a function with an animation of 1 object and i want to be able to have more independent objects, however when i call the function again it only overwrites the old one and i want to be able to have multiple objects (i want rain)
import random
import tkinter
canvas = tkinter.Canvas(width=1000, height=600, bg="white")
canvas.pack()
x = 0
y = 0
def idk():
idk2()
canvas.after(2000,idk2)
def idk2():
global x
x = random.randint(0,1000)
idk3()
def idk3():
global y
y = y+10
canvas.delete('idk3')
canvas.create_rectangle(x-2,y-10,x+2,y+10,tag="idk3")
if y<600:
canvas.after(50, idk3)
idk()
If you want many falling rain, you need to save the rain drops before they reach the ground.
Below is an example:
raindrops = {}
def raining():
# update existing rain drops
for rd in list(raindrops):
if raindrops[rd] < 600:
# keep rain falling
raindrops[rd] += 10
canvas.move(rd, 0, 10)
else:
# rain drop reaches the ground, remove it
canvas.delete(rd)
del raindrops[rd]
# create new rain drop at random x
x = random.randint(0, 1000)
rd = canvas.create_line(x, 0, x, 20)
raindrops[rd] = 0 # save the y of rain drop
canvas.after(20, raining)
Just delete this line from your code:
canvas.delete('idk3')
And of course call the idk() function once more

Cutting the codes in the program and making it neater

I'm having a hard time cutting the code and making it into a loop so that it would make the code of the program, neater.
Although my code works as it suppose to be, I think there is a right way of creating it, adding a for loop rather than writing all of these codes, I know there is an easy way to do this, I just couldn't figure how to do it properly. I know I'm suppose to create a for loop.
squares
from graphics import *
def main():
win = GraphWin("Squares", 500, 500)
rect = Rectangle(Point(0,500), Point(500,0))
rect.setFill("Red")
rect.draw(win)
rect2 = Rectangle(Point(20,480), Point(480,20))
rect2.setFill("white")
rect2.draw(win)
rect3 = Rectangle(Point(40,460), Point(460,40))
rect3.setFill("red")
rect3.draw(win)
rect4 = Rectangle(Point(60,440), Point(440,60))
rect4.setFill("white")
rect4.draw(win)
rect5 = Rectangle(Point(80,420), Point(420,80))
rect5.setFill("red")
rect5.draw(win)
rect6 = Rectangle(Point(100,400), Point(400,100))
rect6.setFill("white")
rect6.draw(win)
rect7 = Rectangle(Point(120,380), Point(380,120))
rect7.setFill("red")
rect7.draw(win)
rect8 = Rectangle(Point(140,360), Point(360,140))
rect8.setFill("white")
rect8.draw(win)
rect9 = Rectangle(Point(160,340), Point(340,160))
rect9.setFill("red")
rect9.draw(win)
rect10 = Rectangle(Point(180,320), Point(320,180))
rect10.setFill("white")
rect10.draw(win)
rect11 = Rectangle(Point(200,300), Point(300,200))
rect11.setFill("red")
rect11.draw(win)
rect12 = Rectangle(Point(220,280), Point(280,220))
rect12.setFill("white")
rect12.draw(win)
The results shows squares into some sort of a patchwork
Try the following:
from graphics import *
def main():
win = GraphWin("Squares", 500, 500)
# create all rects
rects = [Rectangle(Point(0 + 20*i,500 - 20*i), Point(500 - 20*i, 0 + 20*i)) for i in range(12)]
# draw all rects
for idx, rect in enumerate(rects):
rect.fill("red" if idx % 2 == 0 else "white")
rect.draw(win)
If the patchwork is just a background and you don't plan on modifying it you could use this:
from graphics import *
def main():
win = GraphWin("Squares", 500, 500)
i = 1
for x in range(0, 221, 20):
rect = Rectangle(Point(x, 500 - x), Point(500 - x,x))
rect.setFill("red" if i % 2 else "white")
rect.draw(win)
i += 1
An alternate approach that only needs to draw half as many rectangles due to using the rectangle's outline as the other color:
SQUARE, WIDTH = 500, 20
def main():
win = GraphWin("Squares", SQUARE, SQUARE)
save_config = dict(DEFAULT_CONFIG)
DEFAULT_CONFIG.update(dict(outline='red', fill='white', width=WIDTH))
for xy in range(WIDTH//2, SQUARE//2, WIDTH*2):
Rectangle(Point(xy, SQUARE - xy), Point(SQUARE - xy, xy)).draw(win)
DEFAULT_CONFIG.update(save_config)
It's fully parameterized so you can fit it to a different size square or have different width stripes by adjusting the SQUARE and WIDTH parameters. Rather than draw 12 rectangles in alternating colors, with the parameters as currently set, it draws 6 white rectangles with red outlines:

How to change the coordinates of a shape (i.e. oval) in tkinter?

I am trying to make a program which when you press the S button it moves shape to the square below it on the grid. I have managed to get the shape to move the first time but after that it just keeps getting bigger.
Here is my code:
from tkinter import *
root = Tk()
global y
y = 0
x = 0
def down(event):
global y
global x
y = y+100
x = x+ 100
global pirate
canvas.delete(pirate)
pirate = canvas.create_oval((x,y), (100,100), fill = 'red')
print(y)
canvas = Canvas(root, width = 1000, height = 1000)
canvas.pack()
for a in range (10):
i = a*100
canvas.create_line((i,0), (i,1000))
for a in range (10):
i = a*100
canvas.create_line((0,i), (1000,i))
pirate = canvas.create_oval((x, y),(100, 100), fill = 'red')
root.bind('<Key - S>', down)
root.mainloop()
As ArtOfWarfare mentioned in comments, instead of creating new ovals everytime, create one and move that thing around.
def down(event):
canvas.move(pirate, 0, 100)
Above code is sufficient to move your oval one square down in your code.
Assuming you'll need to move oval other than just down, instead of binding only S to canvas, I think you should get all key events and do stuff depending on pressed char.
def keyPressed(event):
if event.char.lower() == 's': #move it down if it's S or s
canvas.move(pirate, 0, 100)
root.bind('<Key>', keyPressed) #get all key pressed events
You have a problem that in python-tk the oval is NOT specified create_oval(x,y,w,h), but create_oval(x1,y1,x2,y2). Hope that's helpful!

Loop patchwork in python3

I need to create a patchwork in Python 3. All I have left to do is create a loop which makes the design border the graphics window. I know I need a for loop however I am not sure how to do this.
This is what I have so far:
from graphics import *
def main():
height = eval(input("What is the height of the window"))
width = eval(input("What is the width of the window"))
colour = input("enter the colour of the patch")
win = GraphWin("Patch", 100*width, 100*height)
boat_x = 0
boat_y = 0
for x in range (4):
boat(win, boat_x, boat_y, colour)
boat_x = boat_x + 23
for i in range(height * 5):
boat(win, boat_x, boat_y, colour)
boat_x = boat_x + 24
for j in range(height * 5):
boat(win, boat_x, boat_y, colour)
boat_y = boat_y + 100
win.getMouse()
win.close()
def boat(win, x, y, colour):
body1 = Polygon(Point(1+x,95+y), Point(5+x,100+y),
Point(20+x,100+y), Point(24+x,95+y))
body1.draw(win)
line1 = Line(Point(13+x,95+y), Point(13+x,90+y))
line1.draw(win)
sail1 = Polygon(Point(1+x,90+y), Point(24+x,90+y), Point(13+x, 73+y))
sail1.setFill(colour)
sail1.draw(win)
body2 = Polygon(Point(1+x, 63), Point(5+x, 68),
Point(20+x,68), Point(24+x,63))
body2.draw(win)
line2 = Line(Point(13+x,63), Point(13+x,58))
line2.draw(win)
sail2 = Polygon(Point(1+x,58), Point(24+x, 58), Point(13+x,40))
sail2.setFill(colour)
sail2.draw(win)
body3 = Polygon(Point(1+x,28), Point(5+x,33),
Point(20+x,33), Point(24+x, 28))
body3.draw(win)
line3 = Polygon(Point(13+x,28), Point(13+x,23))
line3.draw(win)
sail3 = Polygon(Point(1+x,23), Point(24+x, 23), Point(13+x, 5))
sail3.setFill(colour)
sail3.draw(win)
main()
So far this creates the top border but nothing else.
I am also aware that the boat function isn't the most efficient way of drawing
When you say that you need to "make the design border the graphics window" I assume you want your boat design to be repeated several times along each edge of the window (that is, the top, bottom, left and right).
This should be doable in two loops. One will draw the top and bottom edges, the other two will draw the left and right edges. I'm not too sure how your drawing code works, so I'm guessing at some offsets here:
top = 0
bottom = (height-1) * 100
for x in range(0, width*100, 25):
boat(x, top, colour)
boat(x, bottom, colour)
left = 0
right = width * 100 - 25
for y in range(100, (height-1)*100, 100):
boat(left, y, colour)
boat(right, y, colour)
This should call your boat subroutine every 25 pixels across the top and bottom, and every 100 pixels along the left and right edges. Adjust the top, bottom, left and right values and the parameters in the range calls in the loops to make the spacing suit your needs (I just made it up). This code avoids drawing the corner items twice, though depending on how the drawing routine works that might not be necessary.

Categories