I'm trying to develop a search bar in Tkinter using the Entry widget. So far, I've managed to insert centred text that moves to the left when the user clicks the Entry by binding the <FocusIn> event. The text then vanishes when the user begins typing and reappears when the Entry widget becomes empty.
The movement of the text from centre to left is done using the justify option. This means that the text sort of jumps from centre to left which isn't what I want. I want to find a way of animating the text so that it slides from the centre of the widget to the left of the widget smoothly.
Any help would be appreciated. I understand that this may be possible using a Tkinter canvas?
Thanks in advance.
Here is my code:
from Tkinter import *
class SearchEntry:
def __init__(self, master):
self.master = master
self.entry = Entry(self.master, foreground = '#666', justify = 'center')
self.entry.insert(0, 'Search')
self.entry.bind('<FocusIn>', self.onfocus)
self.entry.bind('<FocusOut>', self.outfocus)
self.update()
def update(self):
if self.entry.get() == '':
self.entry.insert(0, 'Search')
self.entry.icursor(0)
self.entry.bind('<Key>', self.keypress)
self.master.after(1, self.update)
def onfocus(self, event):
if self.entry.get() == 'Search':
self.entry.configure(justify = 'left')
self.entry.icursor(0)
self.entry.bind('<Key>', self.keypress)
def outfocus(self, event):
if self.entry.get() == 'Search':
self.entry.configure(justify = 'center')
def keypress(self, event):
self.entry.delete(0, END)
self.entry.unbind('<Key>')
There's nothing you can do without resorting to some sort of hack. For example, you could overlay a canvas on top of the entry and do the animation in the canvas. It will be tricky (though probably not impossible) to manage the focus in the case where the user immediately starts typing.
You could also use the xview command to slowly scroll the data, but that would involve inserting enough space characters to make the data scrollable.
Tkinter isn't like css where you can do interesting animations. It's simply not designed to work that way. Tkinter is not a comfortable minivan full of cupholders and dvd players, it's a pickup truck. Though, it's a pickup truck loaded with legos, so you can usually build what you want if you're willing to put in a lot of effort.
Related
python 3.10, 3.11, windows 10
short:
transparency is affecting title bar when it shouldn't, simple code with example below, move the window to the middle of the screen, maximise and restore to reproduce the behaviour
long:
I know it looks so simple, but please bear with me. This thing is driving me nuts. I'm not sure what I could be doing wrong, it looks like a bug, maybe?
Maximising (or restoring to normal state after maximise) breaks the title bar. Title bar is not registering mouse clicks, I can't close the window or resize it because click goes through. It acts as it if it was transparent. In the main app I am using another hidden window with -alpha to grab mouse events on transparent canvas. Both windows are bound together and act as one. Having second window behind this one also doesn't help. Both windows are affected and unclickable (well, -alpha part of the window is clickable, but not the title bar).
I made a very short code to reproduce this behaviour. You can try it with frame instead of a canvas, or a button. Result is the same. Curious thing I have also discovered when using #000000 for colour - it breaks the window in even weirder way. Not sure what to make of it. #000001 works the same as yellow (or blue, red, etc.).
example:
from tkinter import Canvas, Tk
root = Tk()
root.attributes('-transparentcolor', 'yellow', '-topmost', True)
root.geometry("600x600")
x = Canvas(root, bg='yellow')
x.pack(expand=1, fill='both')
root.mainloop()
If I try different width and height values, sometimes I can grab only half of the title bar or close the window, but not minimise. It seems to depend on where the window is on the screen and the size of the object. Feels like the transparent part of the object is extending through the title bar making it unresponsive. I tried separating the title bar from the rest of the window with frames or shapes but it doesn't help (sometimes it works, but is dependent on size and location of the window - you may get lucky and not notice the behaviour)
x = Canvas(root, bg='yellow', height=600, width=600)
x.pack()
The best solution I've come up with so far:
def refresh(self):
self.state('iconic')
if self.state() == 'iconic':
self.state('normal')
self.focus_force() # needed for Entry widget
This function assigned to the button which is minimising the window to the taskbar and then returning it to normal state immediately. Obviously this is far from elegant, because the user have to perform an unnecessary action. I could use overrideredirect and hopefully recreate resize and close functionality of the window but it seems like an overkill for rather simple app.
Not sure what else to say. It's late, bye
edit:
trying this now and it somehow works, but sometimes the window blinks uncomfortably.
from tkinter import Canvas, Tk
class Window(Tk):
def __init__(self):
super().__init__()
self.bind("<Map>", self.refresh)
self.canv = Canvas(self)
self.canv.pack(expand=1, fill='both')
def refresh(self, event):
if self.state() == 'normal':
self.attributes('-transparentcolor', 'yellow', '-topmost', True)
self.canv.configure(bg='yellow')
elif self.state() == 'zoomed':
self.attributes('-transparentcolor', 'blue', '-topmost', True)
self.canv.configure(bg='blue')
if __name__ == '__main__':
w = Window()
w.mainloop()
By default, tkinter's Checkbutton widget responds to clicks anywhere in the widget, rather than just in the check box field.
For example, consider the following (Python 2) code:
import Tkinter as tk
main = tk.Tk()
check = tk.Checkbutton(main, text="Click").pack()
main.mainloop()
Running this code results in a small window, with a single check box, accompanied by the word "Click". If you click on the check box, it is toggled.
However, this also happens if you click on the text of the widget, rather than the check box.
Is this preventable? I could make a separate widget to hold the text, but that still results in a small area around the check box that responds to clicks.
Two solutions come to mind:
Do as you suggest and create a separate widget for the checkbutton and for the label.
replace the bindings on the checkbutton with your own, and examine the x/y coordinates of the click and only accept the click if it happens in a small region of the widget.
This program creates a checkbutton and overrides the default event on it by binding a method to the checkbutton. When the button is clicked, the method checks a defined limit to allow the normal operation or to override. OP wanted to make sure that when text of the checkbutton is clicked, no default action is taken. That is essentially what this does.
import tkinter as tk
import tkinter.ttk as ttk
class App(tk.Frame):
def __init__(self, parent, *args, **kwargs):
self.checkvar = IntVar()
check = tk.Checkbutton(parent, text='Click', variable=self.checkvar)
check.bind('<Button-1>', self.checkCheck)
check.pack()
print(dir(check))
def checkCheck(self, event):
# Set limit based on your font and preference
limit = 18
print(event.x, event.y, self.checkvar.get())
if event.x > limit or event.y > limit:
self.checkvar.set(not self.checkvar.get())
else:
print("Normal behavior")
if __name__ == "__main__":
window = tk.Tk()
app = App(window)
window.mainloop()
This question already has answers here:
tkinter - How to drag and drop widgets?
(3 answers)
Closed 5 years ago.
Currently, I am working with Python 3.5 GUI development using tkinter module. I want to be able to drag an image from one place to another within the application. Does tkinter support drag and drop within an application, and if so, how do you do it?
from tkinter import *
root = Tk()
root.geometry("640x480")
canvas = Canvas(root, height=480, width=640, bg="white")
frame = Frame(root, height=480, width=640, bg="white")
frame.propagate(0)
image = PhotoImage(file="C:/Users/Shivam/Pictures/Paint/Body.png")
label = Label(canvas, image=image)
label.pack()
label_2 = Label(frame, text="Drop Here !")
label_2.pack()
label_2.place(x=200, y=225, anchor=CENTER)
canvas.pack(side=LEFT)
frame.pack()
root.mainloop()
Tkinter doesn't have any direct support for drag and drop within an application. However, drag and drop requires not much more than making suitable bindings for a button click (<ButtonPress-1>), the mouse moving while the button is clicked (<B1-Motion>), and when the button is released (<ButtonRelease-1>).
Here is a very simplestic example which is designed to work with your code.
First, we'll create a class that can manage the dragging and dropping. It's easier to do this as a class rather than a collection of global functions.
class DragManager():
def add_dragable(self, widget):
widget.bind("<ButtonPress-1>", self.on_start)
widget.bind("<B1-Motion>", self.on_drag)
widget.bind("<ButtonRelease-1>", self.on_drop)
widget.configure(cursor="hand1")
def on_start(self, event):
# you could use this method to create a floating window
# that represents what is being dragged.
pass
def on_drag(self, event):
# you could use this method to move a floating window that
# represents what you're dragging
pass
def on_drop(self, event):
# find the widget under the cursor
x,y = event.widget.winfo_pointerxy()
target = event.widget.winfo_containing(x,y)
try:
target.configure(image=event.widget.cget("image"))
except:
pass
To use it, all you need to do is call the add_draggable method, giving it the widget(s) you wish to drag.
For example:
label = Label(canvas, image=image)
...
dnd = DragManager()
dnd.add_dragable(label)
...
root.mainloop()
That's all it takes for the basic framework. It's up to you to create a floating draggable window, and to perhaps highlight the item(s) that can be dropped on.
Other implementations
For another implementation of the same concept, see https://github.com/python/cpython/blob/master/Lib/tkinter/dnd.py
https://github.com/akheron/cpython/blob/master/Lib/tkinter/dnd.py
I tested it and it still seems to work in python 3.6.1, I suggest experimenting with it and making it your own, because it doesn't seem to be officially supported in Tkinter.
I started to learn Python and I got a problem that is not discussed in any of the tutorials I've found.
Basically when the program got more complicated I've lost the ability to control where the new elements are appearing. It's difficult to explain it for me since English is not my native language so i made a mock-up program that shows what is wrong with my main program.
import Tkinter as tk
import ttk as ttk
clutter=['1PLACEHOLDER', '2PLACEHOLDER', '3PLACEHOLDER', 'PICK ME']
class GRAPHIC_INTERFACE(tk.Frame):
def __init__(self,*args):
tk.Frame.__init__(self, *args)
self.grid()
self.first_window()
def first_window(self):
self.button1=tk.Button(self, text="PLACEHOLDER")
self.button1.grid()
self.button2=tk.Button(self, text="CLICK ME", command=self.second_window)
self.button2.grid()
self.button3=tk.Button(self, text="PLACEHOLDER")
self.button3.grid()
#the additional button apears here
def second_window(self):
alpha=tk.Toplevel(self)
self.button4=tk.Button(alpha, text="PLACEHOLDER")
self.button4.grid()
self.button5=tk.Button(alpha, text="CLICK ME", command= self.third_window)
self.button5.grid()
def third_window(self):
beta=tk.Toplevel(self)
self.BOXXY=ttk.Combobox(beta, values= clutter, state='readonly')
self.BOXXY.bind("<<ComboboxSelected>>", self.misplaced_button) #after choosing the third option an aditional button is created
self.BOXXY.current(0)
self.BOXXY.grid()
self.button6=tk.Button(beta, text="PLACEHOLDER")
self.button6.grid()
#the additional button needs to appear here
def misplaced_button(self, *args):
Catie=self.BOXXY.get()
if Catie=='PICK ME':
self.button7=tk.Button(self, text="I am that problematic button")#this button needs to be in the third window
self.button7.grid()
else:
print "that was not the chosen one"
root=tk.Tk()
root.title("Mockup")
root.geometry("180x200")
app=GRAPHIC_INTERFACE(root)
root.mainloop()
At first I was thinking that i can control the placement of the widgets by giving them names (i.e alpha, beta) but apparently I was wrong.
If self.button7 is supposed to be in the third window, all you have to do is use the third window as the parent of the button.
You can accomplish this many ways: you can save the window as an attribute, you can pass the window in when calling the function, or you can compute the window based on which widow caught the event.
Here's a solution that puts the button in the toplevel that got the event:
def misplaced_button(self, event):
...
toplevel = event.widget.winfo_toplevel()
self.button7=tk.Button(toplevel,...)
...
How would that be done?
I have been unable to find it on here or with Google.
#Refrences
from tkinter import *
class Interface:
def __init__(self,stage):
topLeft = Frame(stage,bg='black')
test = Entry(topLeft,bg='#2a2a2a',fg='white',insertontime=0)
test.config(insertbackground='white', exportselection=0)
test.grid()
topLeft.grid(row=0,column=0)
def launch():
window = Tk()
lobby = Interface(window)
window.mainloop()
launch()
I assume you wish for users to not be able to edit the Entry box? If so, you must use the config parameter of (state="disabled"), eg.
test.config(insertbackground='white', exportselection=0, state="disabled")
be wary though, I could not find a way to keep the background of your entry box black. Hope this helps
You can set the text highlight color to match the background of entry widget. Note that the text in the widget can still be selected but the user will not see it visually which will give the illusion that selection is disabled.
test = Entry(topLeft,bg='#2a2a2a',fg='white',insertontime=0)
test.configure(selectbackground=test.cget('bg'), inactiveselectbackground=test.cget('bg'))
When we select a text we tkinter trigger(fire) 3 events, which are ButtonPress, Motion and ButtonRelease all these 3 events call a event handler fucntion.
The function run select_range(start, end) method which highlight selected text.
To disable this we have to hanlde ButtonPress and Motion events together and call select_clear method on the widget.
If you handle the events and call select_clear method it work but not properly, the text will be highlighted and when another event occured highligh color will be cleared.
This happend because of execution order of events. That's mean you have to tell to tk handle your event after default event handler. We can change order of events execution with bindtags and bin_class methods
example:
from tkinter import *
from tkinter import ttk
def on_select(event):
event.widget.select_clear() # clear selected text.
root = Tk()
name_entry = ttk.Entry(root)
name_entry.pack()
# with PostEvent tag, on_select the event hanlde after default event handler
name_entry.bindtags((str(name_entry), "TEntry", "PostEvent", ".", "all"))
name_entry.bind_class("PostEvent", "<ButtonPress-1><Motion>", on_select)
root.mainloop()
A useful solution to Tkinter not having tons of built-in widgets (like, say, JavaFX does), is that you can easily make your own if you don't mind them being not-quite what you wanted :) Here's a rough-around-the-edges example of using a canvas to emulate an entry field that can't be selected. All I've given is the insert functionality (and delete too, I suppose, if you were clever about it), but you may want more. (Another plus is that, because it's a canvas item, you can do nifty formatting with it).
Am I right that by a not-selectable entry widget, you mean a canvas with text written on it? If you want to disable text-highlighting in ALL widgets, including the top-level frame, you could highjack the Button-1 event, deselect everything, and then consume the event whenever text is selected.
from tkinter import *
class NewEntry(Canvas):
def __init__( self, master, width, height ):
Canvas.__init__( self, master, width=width,height=height )
self.width = width
self.height = height
self.text = ''
def insert( self, index, text ):
self.text =''.join([self.text[i] for i in range(index)] + [text] + [self.text[i] for i in range(index,len(self.text))])
self.delete(ALL)
self.create_text(self.width//2,self.height//2,text=self.text)
root = Tk()
entry = NewEntry(root,100,100)
entry.insert(0,'hello world')
entry.insert(5,'world')
entry.pack()
root.mainloop()