tkinter Python: How to change Button colour after clicking other button? - python

I want the color of neighbouring buttons to change when I click a button. I am making a GUI in tkinter for a 19x19 boardgame. Imagine something like mindsweeper, so the entire board consists of Buttons. I am using python 3.7
Here is my code for a simple 1x2 playing field.
import tkinter as tk
def do_something(p):
p.configure(bg = 'blue')
def personal_do_something(p):
p.configure(bg = 'blue')
p1.configure(bg = 'blue')
window = tk.Tk()
frame = tk.Frame(master = window)
frame.pack()
p1 = tk.Button(master = frame, width = 10, height = 5, bg = 'orange', command = lambda:do_something(p1))
p2 = tk.Button(master = frame, width = 10, height = 5, bg = 'khaki', command = lambda:personal_do_something(p2))
p1.pack(side=tk.LEFT)
p2.pack(side=tk.LEFT)
window.mainloop()
starting situation:
click p1:
click p2:
When p1 is clicked, only p1 turns blue, but not its neighbour p2. when p2 is clicked both p1 and p2 turn blue. This however only works because p2 has a personal_do_something() function. In this function I explicitly tell p1 to turn blue. This might be a viable method for a small 1x2 board, but I can't write personal functions for every button when there are 19x19 buttons. Does anyone have any clue how I could write 1 general function that always turns the direct neighbours blue? I assume I would have to add some kind of attribute to a button, for example a coordinate, and then search all the buttons that match neighbouring coordinates or something like that. But I have no clue how.
Thanks in advance :)

You can store the references of those buttons in a 2D list and pass the row and col of the clicked button to do_something() function. Then you can update the background color of those buttons based on passed row and col.
Below is an example based on your code:
import tkinter as tk
def do_something(row, col):
buttons[row][col].config(bg="blue")
# change direct connected buttons (left, right, up, down)
for dir in (-1, +1):
if 0 <= row+dir < len(buttons):
buttons[row+dir][col].config(bg="blue")
if 0 <= col+dir < len(buttons[row]):
buttons[row][col+dir].config(bg="blue")
window = tk.Tk()
frame = tk.Frame(window)
frame.pack()
ROWS = 10
COLS = 10
buttons = [[None]*COLS for _ in range(ROWS)] # for storing references of buttons
for row in range(ROWS):
for col in range(COLS):
buttons[row][col] = tk.Button(frame, width=10, height=5, bg="orange", command=lambda r=row,c=col: do_something(r,c))
buttons[row][col].grid(row=row, column=col, sticky="nsew")
window.mainloop()

Related

intersect a progress bar in Tkinter

Like the title says, I want to intersect a progress bar in Tkinter at some variable position. See the image for what I want to achieve, those red vertical lines that I edited into the image.
The idea is to put text like "||" inside the bar and color it differently so it should look like what I want to achieve. I have seen a couple of examples with text being put into the progress bar, but it was always right in the middle of the bar which doesn't work for me.
Here is the code that generates progress bars.
import ttk
from Tkinter import *
import random
master = Tk()
stl = ttk.Style()
stl.theme_use("winnative")
stl.configure("colour.Horizontal.TProgressbar", background="lime green")
for i in range(0,4):
prgb = ttk.Progressbar(master, orient = "horizontal", length = 150, mode = "determinate", style = "colour.Horizontal.TProgressbar")
prgb.grid(row=i, column=0, pady=10)
prgb["maximum"] = 1.0
x = random.random()
prgb["value"] = x
master.mainloop()
The ProgressBar widget doesn't support this. However you could create a small colored Frame and use place to put it on top of the progress bar.
Example:
for i in range(0,4):
prgb = ttk.Progressbar(...)
...
x = random.random()
...
marker = Frame(prgb, width=4, background="red")
position = random.uniform(0,x)
marker.place(relx=position, rely=.5, anchor="w", relheight=.5)
I don't have access to a windows machine, but this is what it looks like on my mac:

How to make each button display a different image on click using tkinter (python)

I am creating a GUI with a ".grid" of buttons. And I want to make each of those buttons display a different image on press. So when I click button 1, it will bring up "image1", on the bottom of the buttons. When I click button 2, it will bring up "image2" and etc.
Through some research, I was able to make the buttons run a function that takes in a parameter through the method below. However, I can't seem to make the button display the image. rather, it just makes an empty white space underneath the buttons, when I press any buttons.
Disclaimer:
- I do not want there to be loads of images, there will only be 1 image, and it will change depending on what button i press.
Here's the code:
from tkinter import *
def funct(numimg):
image = PhotoImage(file="image"+str(numimg)+".png")
label = Label(image=image)
label.grid(row = row_no+1, column = 0, columnspan = num_of_cols)
def make_funct(number):
return (lambda: funct(number))
root= Tk()
row_no = -1
buttons = []
num_of_cols = 3
root.resizable(0, 0)
numfiles = 6
for x in range(0, numfiles):
if(x % num_of_cols is 0):
row_no+=1
buttons.append(Button(root, text = "Button "+str(x), bg = '#4098D3', width = 30,height = 13,command = make_funct(x)))
buttons[x].grid(row = row_no, column = x % num_of_cols)
root.mainloop()
So my question is, how do you make each individual button, display a different image when it is pressed? this program right here just leaves an empty white space in replace of the image, and the image is not shown.
There are two major problems with the code you posted.
The first is basically the same as in this question: Why does Tkinter image not show up if created in a function?. You should keep a reference to the PhotoImage object, else it will be garbage collected and it will not show.
The second is that you create a new Label on every button click. You should only make one Label and change the image with the label.config() method.
I would (without wrapping your GUI in a class, which might be a nicer solution) load all images on initialization, save them in a list as attribute of the label and only change the image upon a button click.
I also removed your make_funct function and replaced it with a lambda, which is the most used way to pass variables to a function on callback.
from tkinter import *
def funct(numimg):
label.config(image=label.images[numimg])
root= Tk()
row_no = -1
buttons = []
num_of_cols = 3
root.resizable(0, 0)
numfiles = 3
for x in range(0, numfiles):
if(x % num_of_cols is 0):
row_no+=1
buttons.append(Button(root, text = "Button "+str(x), bg = '#4098D3', width = 30,height = 13, command = lambda n=x: funct(n)))
buttons[x].grid(row = row_no, column = x % num_of_cols)
label = Label(root)
label.grid(row = row_no+1, column = 0, columnspan = num_of_cols)
label.images=[]
for x in range(0, numfiles):
label.images.append(PhotoImage(file="image"+str(x)+".png"))
root.mainloop()

Adjust scrollbar height in Tkinter?

I have created a scrollbar in Tkinter and it's working fine, but the size of the Scroll Button is not being scaled correctly (normally it's adjusted to the size of the scrollable area).
I'm placing all my widgets with a .pack(), therefore the .grid sticky configuration is something I would like to avoid.
My question is: Which part of the scrollbar configuration is responsible for scaling the height?
The code example:
master = Tk()
FrameBIG = Frame(master)
Main = Canvas(FrameBIG,height = 1200,width =1500,scrollregion=Main.bbox("all"))
scroll = Scrollbar(FrameBIG ,orient="vertical", command=Main.yview)
scroll.pack(side="right", fill="y")
Main.pack(side = BOTTOM, anchor = NW,fill="x")
FrameBIG.pack(anchor = W, fill = "x")
The code
Main = Canvas(FrameBIG,height=1200,width=1500,scrollregion=Main.bbox("all"))
is wrong because Main does not exists yet. It should be
Main = Canvas(FrameBIG,background="blue", height = 500,width =500)
Main.configure(scrollregion=Main.bbox("all"))
But it is meaningless because Main canvas was created right now and is empty (so the bbox method returns None)
When you created the scrollbar with
scroll = Scrollbar(FrameBIG ,orient="vertical", command=Main.yview)
you forgot to complete the two step contract between scroll and Main, so you have to add the line below (just after the creation of scroll)
Main.configure(yscrollcommand=scroll.set)
Now the code is like this
from tkinter import *
master = Tk()
FrameBIG = Frame(master)
Main = Canvas(FrameBIG,background="blue", height = 500,width =500)
Main.configure(scrollregion=Main.bbox("all"))
scroll = Scrollbar(FrameBIG ,orient="vertical", command=Main.yview)
Main.configure(yscrollcommand=scroll.set)
scroll.pack(side="right", fill="y")
Main.pack(side = BOTTOM, anchor = NW,fill="x")
FrameBIG.pack(anchor = W, fill = "x")
master.mainloop()
Now you can notice that the scroll bar does not have the button. Its because the Main canvas is empty. Let's add something to the Main canvas
FrameBIG.pack(anchor = W, fill = "x")
# creates a diagonal from coordinates (0,0) to (500,1000)
Main.create_line(0, 0, 500, 1000)
master.mainloop()
Now the line is there but the scroll button is not there yet, why?
Because you have to update the scrollregion of the Main canvas.
So let's do it with
FrameBIG.pack(anchor = W, fill = "x")
Main.create_line(0, 0, 500, 1000)
Main.configure(scrollregion=Main.bbox("all"))
master.mainloop()
Now it is working properly.
Here the complete code.
from tkinter import *
master = Tk()
FrameBIG = Frame(master)
Main = Canvas(FrameBIG,background="blue", height = 500,width =500)
Main.configure(scrollregion=Main.bbox("all"))
scroll = Scrollbar(FrameBIG ,orient="vertical", command=Main.yview)
Main.configure(yscrollcommand=scroll.set)
scroll.pack(side="right", fill="y")
Main.pack(side = BOTTOM, anchor = NW,fill="x")
FrameBIG.pack(anchor = W, fill = "x")
Main.create_line(0, 0, 500, 1000)
Main.configure(scrollregion=Main.bbox("all"))
master.mainloop()
In the next question, post a question with a complete working code that shows up you problem. You will get faster and better answers, ok?
Have a nice day.

Python (Tkinter) - canvas for-loop color change

I generated a grid using a for-loop in Tkinter, but want to know how I would be able to bind an on-click function to such that when I click on each individual generated rectangle, the rectangle will change color.
from Tkinter import *
master = Tk()
def rowgen(row, col):
for i in range(row):
for j in range(col):
w.create_rectangle(25+50*i, 25+50*j, 50+50*i, 50+50*j, fill="green")
w = Canvas(master, width=225, height=225)
w.pack()
rowgen(4, 4)
master.resizable(0,0)
mainloop()
I'm thinking that I have to first iterate through another for-loop to make an event, where if I click within these coordinates, I'd reconfig the color of one of the rectangles.
By following Curly Joe's hints and making some mistakes, I got the following, which requires only one tag_bind. You might want to try it yourself first.
from tkinter import *
master = Tk()
def box_click(event):
box = event.widget.find_closest(event.x, event.y)
print(box) # remove later
w.itemconfig(box, fill='red')
def rowgen(row, col):
for i in range(row):
for j in range(col):
w.create_rectangle(25+50*i, 25+50*j, 50+50*i, 50+50*j,
fill="green", tag='BOX')
w = Canvas(master, width=225, height=225)
w.pack()
rowgen(4, 4)
w.tag_bind('BOX', '<Button-1>', box_click)
master.resizable(0,0)
mainloop()

How do I create a Tiling layout / Flow layout in TkInter?

I want to to fill my window with, say, labels and I want them to wrap once the column would be bigger than the current window (or rather parent frame) size.
I've tried using the grid layout, but then I have to calculate the size of the content of each row myself, to know when to put the next element in the next row.
The reason I ask, is because I want to create some sort of tiled file icons.
Or asked differently, is there something like Swing's FlowLayout for TkInter?
What I do when I want something like this is use the text widget for a container. The text widget can have embedded widgets, and they wrap just like text. As long as your widgets are all the same height the effect is pretty nice.
For example (cut and pasted from the question at the author's request):
textwidget = tk.Text(master)
textwidget.pack(side=tk.LEFT, fill=tk.BOTH)
for f in os.listdir('/tmp'):
textwidget.window_create(tk.INSERT, window=tk.Label(textwidget, text=f))
Here is a way to make flow behavior inside a frame.
I wrote a function that will do this. Basically you pass a frame to the function (not root or top level) and the function will look at all the children of the frame, go through them measure their sizes and place them in the frame.
Here is the placement procedure
Place the first widget, and move x over an amount equal to its width.
Measure the next widget.
If placing the next widget would cause it to goes past the frame width, bump its x value to 0 and bump it down a y value equal to the largest widget in the current row (start a new row).
Reset the value of the largest widget since you are starting a new row.
Keep repeating until all widgets are placed.
Bind that procedure to the resizing of the frame event.
I used 3 functions to make this work:
The function that runs the procedure.
The function that binds the resizing of the frame to the function.
The function that unbinds the resizing of the frame.
Here are the functions:
from tkinter import *
def _reorganizeWidgetsWithPlace(frame):
widgetsFrame = frame
widgetDictionary = widgetsFrame.children
widgetKeys = [] # keys in key value pairs of the childwidgets
for key in widgetDictionary:
widgetKeys.append(key)
# initialization/priming loop
width = 0
i = 0
x = 0
y = 0
height = 0
maxheight = 0
# loop/algorithm for sorting
while i < len(widgetDictionary):
height = widgetDictionary[widgetKeys[i]].winfo_height()
if height > maxheight:
maxheight = height
width = width + widgetDictionary[widgetKeys[i]].winfo_width()
# always place first widget at 0,0
if i == 0:
x = 0
y = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
# if after adding width, this exceeds the frame width, bump
# widget down. Use maximimum height so far to bump down
# set x at 0 and start over with new row, reset maxheight
elif width > widgetsFrame.winfo_width():
y = y + maxheight
x = 0
width = widgetDictionary[widgetKeys[i]].winfo_width()
maxheight = height
# if after adding width, the widget row length does not exceed
# frame with, add the widget at the start of last widget's
# x value
else:
x = width-widgetDictionary[widgetKeys[i]].winfo_width()
# place the widget at the determined x value
widgetDictionary[widgetKeys[i]].place(x=x, y=y)
i += 1
widgetsFrame.update()
def organizeWidgetsWithPlace(frame):
_reorganizeWidgetsWithPlace(frame)
frame.bind("<Configure>", lambda event: _reorganizeWidgetsWithPlace(frame))
_reorganizeWidgetsWithPlace(frame)
def stopOrganizingWidgetsWithPlace(frame):
frame.unbind("<Configure>")
And here is an example of them in use:
def main():
root = Tk()
root.geometry("250x250")
myframe = Frame(root)
# make sure frame expands to fill parent window
myframe.pack(fill="both", expand=1)
buttonOrganize = Button(myframe, text='start organizing',
command=lambda: organizeWidgetsWithPlace(myframe))
buttonOrganize.pack()
buttonStopOrganize = Button(myframe, text='stop organizing',
command=lambda: stopOrganizingWidgetsWithPlace(myframe))
buttonStopOrganize.pack()
##### a bunch of widgets #####
button = Button(myframe, text="---a random Button---")
canvas = Canvas(myframe, width=80, height=20, bg="orange")
checkbutton = Checkbutton(myframe, text="---checkbutton----")
entry = Entry(myframe, text="entry")
label = Label(myframe, text="Label", height=4, width=20)
listbox = Listbox(myframe, height=3, width=20)
message = Message(myframe, text="hello from Message")
radioButton = Radiobutton(myframe, text="radio button")
scale_widget = Scale(myframe, from_=0, to=100, orient=HORIZONTAL)
scrollbar = Scrollbar(myframe)
textbox = Text(myframe, width=3, height=2)
textbox.insert(END, "Text Widget")
spinbox = Spinbox(myframe, from_=0, to=10)
root.mainloop()
main()
Notice:
That you do not need to grid, pack or place them. As long as you specify the frame, that will all be done at once when the function is called. So that is very convenient. And it can be annoying if you grid a widget, then try to pack another, then try to place another and you get that error that you can only use one geometry manager. I believe this will simply overwrite the previous choices and place them. I believe you can just drop this function in and it will take over management. So far that has always worked for me, but I think you should really not try to mix and match geometry managers.
Notice that initially the buttons are packed, but after pressing the button, they are placed.
I have added the "WithPlace" naming to the functions because I have a similar set of functions that do something very similar with the grid manager.

Categories

Resources