When a GUI has a TkInter Scale and they click somewhere on the scale, the default behavior seems to be to slide the slider along the Scale in the direction towards the mouse (and then unexpectedly past their mouse).
What I'd want instead is to have the slider always jump to and stay attached to the user's mouse point while they're clicking anywhere on the slider. If they click to a particular point on the Scale, the slider should jump directly to that point.
I have some code below which attempts to do this but doesn't seem to work and I cannot find the reason for it.
import tkinter as tk
from tkinter import ttk
def show_values():
print('w1 set to',w1.get())
def snapToVal1(val):
scaleVal = float(w1.get())
if int(scaleVal) != scaleVal:
w1.set(round(float(val)))
def scaleFunc1(event):
g = w1.grid_info()
w1.set(round(8 * (event.y - g['pady'])/(w1.winfo_height() - 2*g['pady'] - 2*g['ipady']))-1)
print('w1 set to',w1.get())
#---
root = tk.Tk()
f1 = ttk.Frame(root, relief = tk.GROOVE)
ttk.Label(f1, text='Stellar\nType').grid(row=0,column=0, columnspan=2,padx=2,pady=2)
for i,text in enumerate(['O','B','A','F','G','K','M','L']):
ttk.Label(f1, text = text).grid(row=i+1,column=0,pady=5,padx=(2,0))
w1 = ttk.Scale(f1, to=7, command=snapToVal1, orient=tk.VERTICAL)
w1.grid(row = 1, column = 1, rowspan = 8, pady=5, padx=2, sticky='nsew')
w1.bind('<Button-1>',scaleFunc1)
f1.grid(row = 0, column = 0,padx=(2,1),pady=2,sticky='nsew')
ttk.Button(root, text='Show', command=show_values).grid(row=1,column=0)
root.mainloop()
The pertinent function here is scaleFunc1. The idea is to have this called whenever the user presses their mouse button on the scale. It then tries to calculate, from the event pixel location and the Scale size, the fractional position of the click on the scale, convert this to a scale value, and set it to that value where the user clicked. However, I'm finding that the slider doesn't always jump to the same place, even if it reports it was set to the value I'd expect. What's going on?
I suspect it has something to do with the slide still trying to move for the fraction of a second the user keeps the mouse button pressed down.
That's actually the default right-click behavior. If you want to make the left click do that too, then the easiest thing is to simply detect leftclick and tell tkinter it was a right click instead:
import tkinter as tk
from tkinter import ttk
class Scale(ttk.Scale):
"""a type of Scale where the left click is hijacked to work like a right click"""
def __init__(self, master=None, **kwargs):
ttk.Scale.__init__(self, master, **kwargs)
self.bind('<Button-1>', self.set_value)
def set_value(self, event):
self.event_generate('<Button-3>', x=event.x, y=event.y)
return 'break'
def show_values():
print('w1 set to',w1.get())
root = tk.Tk()
f1 = ttk.Frame(root, relief = tk.GROOVE)
ttk.Label(f1, text='Stellar\nType').grid(row=0,column=0, columnspan=2,padx=2,pady=2)
for i,text in enumerate(['O','B','A','F','G','K','M','L']):
ttk.Label(f1, text = text).grid(row=i+1,column=0,pady=5,padx=(2,0))
w1 = Scale(f1, to=7, orient=tk.VERTICAL)
w1.grid(row = 1, column = 1, rowspan = 8, pady=5, padx=2, sticky='nsew')
f1.grid(row = 0, column = 0,padx=(2,1),pady=2,sticky='nsew')
ttk.Button(root, text='Show', command=show_values).grid(row=1,column=0)
root.mainloop()
from tkinter import *
root = Tk()
root.geometry('500x200')
scale = Scale(to=100, length=300, orient=HORIZONTAL)
scale.pack()
def move_to_click(event):
new_coord = (event.x - 18 + (event.x / 10)) / (300 / 100)
scale.set(new_coord)
scale.bind("<Button-1>", move_to_click)
root.mainloop()
Related
I am trying to understand how to apply a button to a transparent background while keeping its shape. When I generate the code below, there is a gray background around the border that appears, and it also looks like it loses its shape.
Colors Used
Sidebar: #2E3A4B at 53%
Button: #2C2F33 at 100%
from tkinter import *
def btn_clicked():
""" Prints to console a message every time the button is clicked """
print("Button Clicked")
root = Tk()
# Configures the frame, and sets up the canvas
root.geometry("1440x1024")
root.configure(bg="#ffffff")
canvas = Canvas(root, bg="#ffffff", height=1024, width=1440, bd=0, highlightthickness=0, relief="ridge")
canvas.place(x=0, y=0)
background_img = PhotoImage(file=f"background.png")
background = canvas.create_image(719.5, 512.5, image=background_img)
img0 = PhotoImage(file=f"img0.png")
alarm_button = Button(image=img0, borderwidth=0, highlightthickness=0, command=btn_clicked, relief="flat")
alarm_button.place(x=9, y=119, width=90, height=90)
root.resizable(False, False)
root.mainloop()
Required Images
How it looks:
How it should look:
Good news! I was able to get that answer to a related question you found to work. To make it easier to reuse I've converted it into a formal class and added a couple of methods. In addition I made it flash the image off and back on when it's clicked to give the user some visual feedback like "real" tkinter Buttons do.
Note that it responds to mouse button <ButtonRelease-1> events. That's a better choice in most cases than the <Button-1> event because if the user accidentally presses the button, they can move the mouse off the widget image to avoid setting off the event.
Turns out that using the PIL module was unnecessary. Here's the code:
import tkinter as tk # PEP 8 recommends avoiding `import *`.
class CanvasButton:
""" Create leftmost mouse button clickable canvas image object.
The x, y coordinates are relative to the top-left corner of the canvas.
"""
flash_delay = 100 # Milliseconds.
def __init__(self, canvas, x, y, image_path, command, state=tk.NORMAL):
self.canvas = canvas
self.btn_image = tk.PhotoImage(file=image_path)
self.canvas_btn_img_obj = canvas.create_image(x, y, anchor='nw', state=state,
image=self.btn_image)
canvas.tag_bind(self.canvas_btn_img_obj, "<ButtonRelease-1>",
lambda event: (self.flash(), command()))
def flash(self):
self.set_state(tk.HIDDEN)
self.canvas.after(self.flash_delay, self.set_state, tk.NORMAL)
def set_state(self, state):
""" Change canvas button image's state.
Normally, image objects are created in state tk.NORMAL. Use value
tk.DISABLED to make it unresponsive to the mouse, or use tk.HIDDEN to
make it invisible.
"""
self.canvas.itemconfigure(self.canvas_btn_img_obj, state=state)
BGR_IMG_PATH = "sunset_background.png"
BUTTON_IMG_PATH = "alarm_button.png"
def btn_clicked():
""" Prints to console a message every time the button is clicked """
print("Button Clicked")
root = tk.Tk()
background_img = tk.PhotoImage(file=BGR_IMG_PATH)
bgr_width, bgr_height = background_img.width(), background_img.height()
root.geometry(f'{bgr_width}x{bgr_height}')
root.title("TKinter button over transparent background")
root.configure(bg="white")
canvas = tk.Canvas(root, bg="white", height=bgr_height, width=bgr_width, bd=0,
highlightthickness=0, relief="ridge")
canvas.place(x=0, y=0)
background = canvas.create_image(0, 0, anchor='nw', image=background_img)
canvas_btn1 = CanvasButton(canvas, 0, 128, BUTTON_IMG_PATH, btn_clicked)
canvas_btn2 = CanvasButton(canvas, 0, 256, BUTTON_IMG_PATH, btn_clicked)
root.resizable(False, False)
root.mainloop()
Screenshot of the result:
Close up:
This is the code that I have written. As you can see, it has a button which when clicked opens up a Text where one can enter something. It works, the window resizes if you click the button.
But I have noticed that, if I maximize and minimize the window just after starting the program (i.e. the first thing I do is to maximize and minimize the window), then the window doesn't resize to fit the whole frame (on clicking the button) and my widgets get hidden.
from tkinter import *
def Home():
global homeP
homeP = Tk()
homeP.title('Grades')
enterButton = Button(homeP, text='Enter Grades', bg='blue', fg='white', command=enterG)
enterButton.grid(row=1, column=0, padx=(5,5), pady=(5,2), sticky="e")
homeP.mainloop()
curFrame = ''
def enterG():
global homeP
global eGrade
global curFrame
if curFrame == 'e':
return
if 'eGrade' not in globals(): #Prevent frames from stacking up on one another
eGrade = Frame(homeP)
enterGrades = Text(eGrade, width=64, height=10)
enterGrades.grid(row=0, column=0)
eGrade.grid(row=2, column=0, columnspan=2, padx=(10,10), pady=(1,5))
else:
eGrade.grid(row=2, column=0, columnspan=2, padx=(10,10), pady=(1,5))
curFrame = 'e'
# for name, value in globals().copy().items():
# print(name, value)
Home()
Any help as to why this is happening and how I can prevent this?
You will have exactly the same problem if you manually resize the window before clicking the button. The issue isn't tied to maximize/minimize per se, it's tied to the fact that the user has said "I want the window to be this size" and tkinter is trying to honor that by not resizing the window against the user's wishes.
The solution is to reset the size of the window before adding or removing widgets. Add the following line of code after your global statements in enterG:
homeP.geometry("")
I am using Tix to automatically create a scroll bar as the content changes. I want to keep a button or two in the user's view while they scroll through the contents of the application.
I haven't seen this question for Tkinter/Tix yet so I thought I'd ask.
The following code will create a sample of the problem where the button is at a fixed point in the window, and is subject to being scrolled.
from Tkinter import *
import Tix
class some_GUI:
def __init__(self, root):
sw= Tix.ScrolledWindow(root, scrollbar=Tix.AUTO)
sw.pack(fill=Tix.BOTH, expand=1)
frame1 = Frame(sw.window)
frame1.grid(row = 0, column = 1)
frame2 = Frame(sw.window)
frame2.grid(row = 0, column = 2)
def quit():
root.quit()
for i in range(0,300):
label1 = Label(frame1, text = "foo")
label1.grid(row = i, column = 0)
button = Button(frame2, text = "Quit", command = quit)
button.pack()
root = Tix.Tk()
display = some_GUI(root)
root.mainloop()
I want the button(s) to be in "frame2" and centered vertically relative to the application's window. I tried using winfo_height/winfo_width to find the frame's height/ width to work with update, but these values didn't change with the addition of the labels and button.
Attempted/possible solutions:
I put frame2 in sw.subwidgets_all[1] by doing the following:
frame1.pack(side = LEFT)
frame2 = Frame(sw.subwidgets_all()[1])
frame2.pack(side = RIGHT)
button = Button(frame2, text = "Quit", command = quit)
button.pack(side = RIGHT)
This allows the fixed position relative to the application, but the window resizes relative to the button's parent instead of frame1. Another drawback is that the horizontal scrollbar is only relative to frame1.
Find the midpoint of the scrollbar and update the position of the buttons relative to those coordinates using place(maybe?) not sure how to accomplish this, and seeing SO solutions in general I think this might be an inefficient way of doing this.
EDIT: Although this isn't exactly what I had in mind, the following code works as per falsetru's suggestion in the comments:
from Tkinter import *
import Tix
class some_GUI:
def __init__(self, root):
def quit():
root.quit()
frame2 = Frame(root)
frame2.pack(side = RIGHT)
button = Button(frame2, text = "Quit", command = quit)
button.pack()
frame1 = Frame(root)
frame1.pack(side = LEFT)
sw= Tix.ScrolledWindow(frame1, scrollbar=Tix.AUTO)
sw.pack(fill=Tix.BOTH, expand=1)
for widget in sw.subwidgets_all():
print widget
for i in range(0,300):
label1 = Label(sw.window, text = "foo")
label1.grid(row = i, column = i)
print root.winfo_toplevel()
for widget in sw.subwidgets_all():
print widget
root = Tix.Tk()
display = some_GUI(root)
root.mainloop()
You can put the button out of ScrollWindows:
import Tix
from Tkinter import *
def build_ui(root):
sw = Tix.ScrolledWindow(root, scrollbar=Tix.AUTO)
sw.pack(side=LEFT, fill=Tix.BOTH, expand=1)
for i in range(300):
label1 = Label(sw.window, text="foo")
label1.grid(row=i, column=0)
button = Button(root, text="Quit", command=root.quit)
button.pack(side=RIGHT)
root = Tix.Tk()
build_ui(root)
root.mainloop()
The second option you mentioned could be the one that satisfies your situation, however that is computationally expensive as you will need to delete the button(s) and redraw them over and over relatively to the scrollbar up/down motion. Not only this is ugly by design but it can be an obstacle for any further scalability of your application or even lead to unexpected bugs if your application runs some serious operations.
The only realistic solution I see for your problem is to fix the button(s) on (for example the bottom of) the upper canvas (or whatever region you want to set) and outside the scrollable region as #falsetru commented you.
I want to obtain a button out of a Canvas. I've tried to pack the canvas in the button widget, but that didn't work. Googling a bit, I've found (here: How do you create a Button on a tkinter Canvas?) that the Canvas method create_window might help. But there should be something wrong in the way I'm using it.
import Tkinter
DIM = 100
root = Tkinter.Tk()
frame = Tkinter.Frame(root)
button = Tkinter.Button(None, width=DIM, height=DIM, command=root.quit)
circle = Tkinter.Canvas(frame, width=DIM, height=DIM)
circle.create_oval(5, 5, DIM-5, DIM-5, fill="red")
circle.create_window(0, 0, window=button)
frame.grid()
circle.grid(row=1, column=1)
root.mainloop()
If I erase the create_window line, I can se my painting but I can't (obviously) click on it. But in this way, the button widget cover my circle and shows a sad empty button.
Basically, I want to create a button with a red circle painted inside.
Tkinter doesn't allow you to directly draw on widgets other than the canvas, and canvas drawings will always be below embedded widgets.
The simple solution is to create the effect of a button using just the canvas. There's really nothing special about doing this: just create a canvas, then add bindings for ButtonPress and ButtonRelease to simulate a button being pressed.
Here's a rough idea:
class CustomButton(tk.Canvas):
def __init__(self, parent, width, height, color, command=None):
tk.Canvas.__init__(self, parent, borderwidth=1,
relief="raised", highlightthickness=0)
self.command = command
padding = 4
id = self.create_oval((padding,padding,
width+padding, height+padding), outline=color, fill=color)
(x0,y0,x1,y1) = self.bbox("all")
width = (x1-x0) + padding
height = (y1-y0) + padding
self.configure(width=width, height=height)
self.bind("<ButtonPress-1>", self._on_press)
self.bind("<ButtonRelease-1>", self._on_release)
def _on_press(self, event):
self.configure(relief="sunken")
def _on_release(self, event):
self.configure(relief="raised")
if self.command is not None:
self.command()
To complete the illusion you'll want to set a binding on <Enter> and <Leave> (to simulate the active state), and also make sure that the cursor is over the button on a button release -- notice how real buttons don't do anything if you move the mouse away before releasing.
What you can do is bind the canvas to the mouse:
import Tkinter
DIM = 100
root = Tkinter.Tk()
frame = Tkinter.Frame(root)
circle = Tkinter.Canvas(frame)
circle.create_oval(5, 5, DIM-5, DIM-5, fill="red")
frame.grid()
circle.grid(row=1, column=1)
##################################
def click(event):
root.quit()
circle.bind("<Button-1>", click)
##################################
root.mainloop()
Now, if a user clicks inside the canvas, function click will be called (essentially, the canvas has now been made a button).
Notice though that function click will be called if a user clicks anywhere in the canvas. If you want to make it so that click is only called when a user clicks in the circle, you can use event.x and event.y to get a hold of the x and y coordinates of the click. Once you have those, you can run a calculation to determine whether those coordinates are within the circle. Here is a reference on that.
I am newbie in Python using tkinter and I have a problem that I cant solve.Digital time
I want to put digital time in the the upper right corner of my application (Please see the picture). I tried to search on net on how to create a digital time but it is on global root and frame configuration and I cant find a digital clock made for canvas. I also want to put my buttons in middle using grid, but I have no luck finding a solution. Can any one please help me? Ill paste my code here.
from tkinter import *
from tkinter import ttk
from datetime import date
import time
import sys
class main_menu(object):
def __init__(self, root):
self.root = root
self.root.title('System')
self.root.geometry('780x488')
self.background = PhotoImage(file='images/bg.png')
self.canvas = Canvas (root)
self.canvas.grid(sticky=N+S+W+E)
self.canvas.create_image(0,0, image=self.background, anchor="nw")
self.scan_photo = PhotoImage (file='images/scan.png')
self.logs_photo = PhotoImage (file='images/logs.png')
self.settings_photo = PhotoImage (file='images/settings.png')
self.scan_btn = Button (self.canvas, image=self.scan_photo, borderwidth=0, command=self.StartScan)
self.scan_win = self.canvas.create_window(225, 100, anchor="nw", window=self.scan_btn)
self.logs_btn = Button (self.canvas, image=self.logs_photo, borderwidth=0, command=self.Logs)
self.logs_win = self.canvas.create_window(225, 200, anchor="nw", window=self.logs_btn)
self.settings_btn = Button (self.canvas, image=self.settings_photo, borderwidth=0, command=self.Settings)
self.settings_win = self.canvas.create_window(225, 300, anchor="nw", window=self.settings_btn)
self.today = date.today()
self.format = self.today.strftime("%b. %d, %Y")
self.canvas.create_text(730, 30, text=self.format, font=("Helvetica", 10))
self.InstructionsLabel = Label(root, text="""
tadahhhhhh""", fg="black", font=("Calibri", 14))
self.Return_photo = PhotoImage (file='images/back_24x24.png')
self.ReturnMenu_btn = Button (self.canvas, image=self.Return_photo, background='white',activebackground='white', borderwidth=0, command=self.MainMenu)
self.ReturnMenu_win = self.canvas.create_window(0, 0, anchor="nw", window=self.ReturnMenu_btn)
###self.ReturnMenu = Button(root, image=self.back_photo, command=self.MainMenu, )
self.MainMenu()
def MainMenu(self):
self.RemoveAll()
self.ReturnMenu_btn.grid_remove()
self.scan_btn.grid(padx=215)
self.logs_btn.grid(padx=215)
self.settings_btn.grid(padx=215)
def StartScan(self):
self.RemoveAll()
def Logs(self):
self.RemoveAll()
self.ReturnMenu.grid()
def Settings(self):
self.RemoveAll()
self.ReturnMenu.grid()
def RemoveAll(self):
self.scan_btn.grid_remove()
self.logs_btn.grid_remove()
self.settings_btn.grid_remove()
self.InstructionsLabel.grid_remove()
self.ReturnMenu_btn.grid_remove()
if __name__ == '__main__':
root = Tk()
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
main_menu = main_menu(root)
root.mainloop()
To place the time in the upper right corner you need to know the width of the canvas. So use canvas.winfo_width() to get the width of the canvas and subtract some number to place it at the desired position.
If you want the time to stay at the top-right even if the window is resized then bind Configure to a function and move the text using .coords or .moveto.
Sample code(this code will make sure that the time is always at the upper right corner).
from tkinter import font
class MainMenu:
def __init__(self, root):
...
self.time = self.canvas.create_text(0, 0, text=self.format, font=("Helvetica", 10))
self.canvas.bind('<Configure>', self.adjustTimePosition)
...
def adjustTimePosition(self, event):
family, size = self.canvas.itemcget(self.time, 'font').split() # get the font-family and font size
text = self.canvas.itemcget(self.time, 'text')
txt_font = font.Font(family=family, size=size)
width, height = txt_font.measure(text), txt_font.metrics("ascent") # measures the width and height of the text
self.canvas.coords(self.time, self.canvas.winfo_width()-width, height) # moves the text