Adding placeholders to tkinter Entry widget in a procedural way

Viewed 569

I know this Q has been answered in this site, but im looking for a more simpler answer, and ive seen one before but then the question has been deleted or something, I cant find it. Hopefully someone is having a better and easier way around to it. Something related to class might be better as I could use it easily with more Entry widgets

Here is a snippet:

from tkinter import *

root = Tk()

def remove(event):
    e.delete(0, END)

e = Entry(root)
e.insert(0, 'PLACEHOLDER')
e.pack(padx=100,pady=(30,0))
e.bind('<FocusIn>', remove)

e2 = Entry(root)
e2.pack( pady=(20,100))

root.mainloop()

Yes, this will remove all the other items inside of the box once it looses focus and gains it again, including the text we enter at first. Anyway to get around that and have a perfect placeholder with tkinter, I'm aware there is no inbuilt way.

Thanks in advance :D

4 Answers

I'm not really clear on what you're asking, so I'm guessing you're asking how to know when the entry widget has placeholder text and when it doesn't so that you know when to clear it and when not to clear it.

The easiest solution is to add an attribute to the entry with the replacement text, and then compare it to the contents before deleting.

Using functions

First, let's create a function to initialize the placeholder text for a widget. This function does a few simple things: it adds a placeholder attribute on the widget, and it establishes the bindings. It also inserts the placeholder if the widget is empty:

def init_placeholder(widget, placeholder_text):
    widget.placeholder = placeholder_text
    if widget.get() == "":
        widget.insert("end", placeholder_text)

    # set up a binding to remove placeholder text
    widget.bind("<FocusIn>", remove_placeholder)
    widget.bind("<FocusOut>", add_placeholder)

Now let's tweak your remove function to be a bit more generic. Since it's called via an event, it can use event.widget rather than a hard-coded reference to a specific widget. It also uses the placeholder attribute which we added to the widget. These two techniques lets it be used by more than one widget.

def remove_placeholder(event):
    placeholder_text = getattr(event.widget, "placeholder", "")
    if placeholder_text and event.widget.get() == placeholder_text:
        event.widget.delete(0, "end")

Finally we need to implement the add_placeholder function. This function will add the placeholder when the widget loses focus and the user hasn't typed anything. It needs to check if the entry widget has a placeholder, and if it does and the widget is empty, it adds the placeholder. Like remove_placeholder it uses event.widget and the placeholder attribute:

def add_placeholder(event):
    placeholder_text = getattr(event.widget, "placeholder", "")
    if placeholder_text and event.widget.get() == "":
        event.widget.insert(0, placeholder_text)

I've modified your program to use different placeholder text for each of the two entry widgets to show that the functions are generic and not tied to a specific entry widget.

from tkinter import *

root = Tk()

def remove_placeholder(event):
    """Remove placeholder text, if present"""
    placeholder_text = getattr(event.widget, "placeholder", "")
    if placeholder_text and event.widget.get() == placeholder_text:
        event.widget.delete(0, "end")

def add_placeholder(event):
    """Add placeholder text if the widget is empty"""
    placeholder_text = getattr(event.widget, "placeholder", "")
    if placeholder_text and event.widget.get() == "":
        event.widget.insert(0, placeholder_text)

def init_placeholder(widget, placeholder_text):
    widget.placeholder = placeholder_text
    if widget.get() == "":
        widget.insert("end", placeholder_text)

    # set up a binding to remove placeholder text
    widget.bind("<FocusIn>", remove_placeholder)
    widget.bind("<FocusOut>", add_placeholder)

e = Entry(root)
e.pack(padx=100,pady=(30,0))

e2 = Entry(root)
e2.pack( pady=(20,100))

init_placeholder(e, "First Name")
init_placeholder(e2, "Last Name")

root.mainloop()

Using a custom class

Arguably, a better way to implement this would be to create a custom class. That way everything is encapsulated in one place. Here's an example:

class EntryWithPlaceholder(Entry):
    def __init__(self, *args, **kwargs):
        self.placeholder = kwargs.pop("placeholder", "")
        super().__init__(*args, **kwargs)

        self.insert("end", self.placeholder)
        self.bind("<FocusIn>", self.remove_placeholder)
        self.bind("<FocusOut>", self.add_placeholder)

    def remove_placeholder(self, event):
        """Remove placeholder text, if present"""
        if self.get() == self.placeholder:
            self.delete(0, "end")

    def add_placeholder(self,event):
        """Add placeholder text if the widget is empty"""
        if self.placeholder and self.get() == "":
            self.insert(0, self.placeholder)

You can use this class just like an Entry widget, but you can specify a placeholder:

e3 = EntryWithPlaceholder(root, placeholder="Address")
e3.pack()

Here is a very simple example. In this example we include a couple of features/caveats:

  • ghost text for the placeholder
  • entry.input will return None if it's text is the placeholder or empty
  • entry.input should be used in place of .get() and .insert(). The .input logic is designed to give you the proper results for this type of widget. .get() is not smart enough to return the proper data, and .insert() has been reconfigured as a proxy to .input
  • placeholder is juggled while you type
  • placeholder can be overwritten with .insert() ~ no need to use .delete(). You should still use entry.input instead

#widgets.py

import tkinter as tk

class PlaceholderEntry(tk.Entry):
    '''
        All Of These Properties Are For Convenience
    '''
    @property
    def input(self):
        return self.get() if self.get() not in [self.__ph, ''] else None
        
    @input.setter
    def input(self, value):
        self.delete(0, 'end')
        self.insert(0, value)
        self.configure(fg = self.ghost if value == self.__ph else self.normal)
    
    @property
    def isempty(self) -> bool:
        return self.get() == ''
    
    @property     
    def isholder(self) -> bool:
        return self.get() == self.__ph
        
    def __init__(self, master, placeholder, **kwargs):
        tk.Entry.__init__(self, master, **{'disabledforeground':'#BBBBBB', **kwargs})
        
        self.normal = self['foreground']
        self.ghost  = self['disabledforeground']
        
        self.__ph = placeholder
        self.input = placeholder
        
        vcmd = self.register(self.validate)
        self.configure(validate='all', validatecommand=(vcmd, '%S', '%s', '%d'))
        
        self.bind('<FocusIn>' , self.focusin)
        self.bind('<FocusOut>', self.focusout)
        self.bind('<Key>'     , self.check)
    
    #rewire .insert() to be a proxy of .input
    def validate(self, action_text, orig_text, action):
        if action == '1':
            if orig_text == self.__ph:
                self.input = action_text
            
        return True
    
    #removes placeholder if necessary    
    def focusin(self, event=None):
        if self.isholder:
            self.input = ''
    
    #adds placeholder if necessary    
    def focusout(self, event=None):
        if self.isempty:
            self.input = self.__ph
    
    #juggles the placeholder while you type    
    def check(self, event):
        if event.keysym == 'BackSpace':
            if self.input and len(self.input) == 1:
                self.input = self.__ph
                self.icursor(0)
                return 'break'
        elif self.isholder:
            if event.char:
                self.input = ''
            else:
                return 'break'

usage example:

#__main__.py

import tkinter as tk
import widgets as ctk #custom tk                
                

if __name__ == "__main__":
    root = tk.Tk()
    root.title("Placeholder Entry")
    root.grid_columnconfigure(2, weight=1)

    #init some data
    entries    = [] #for storing entry references
    label_text = ['email', 'name']
    entry_text = ['you@mail.com', 'John Smith']

    #create form
    for n, (label, placeholder) in enumerate(zip(label_text, entry_text)):
        #make label
        tk.Label(root, text=f'{label}: ', width=8, font='consolas 12 bold', anchor='w').grid(row=n, column=0, sticky='w')
        #make entry
        entries.append(ctk.PlaceholderEntry(root, placeholder, width=14, font='consolas 12 bold'))
        entries[-1].grid(row=n, column=1, sticky='w')

    #form submit function
    def submit():
        for l, e in zip(label_text, entries):
            if e.input:
                print(f'{l}: {e.input}')

    #form submit button        
    tk.Button(root, text='submit', command=submit).grid(column=1, sticky='e')

    root.mainloop()
        
    
    

I have tried this:

from tkinter import *

root = Tk()

def remove(event):
    if e.get() == 'PLACEHOLDER': #Check default value
        e.delete(0, END)

def add(event):
    if not e.get(): #Check if left empty
        e.insert(0, 'PLACEHOLDER')     

e = Entry(root)
e.insert(0, 'PLACEHOLDER')
e.pack(padx=100,pady=(30,0))
e.bind('<FocusIn>', remove)
e.bind('<FocusOut>', add)

e2 = Entry(root)
e2.pack( pady=(20,100))

root.mainloop()

By doing this you will be clearing only if the default value is present in the Text and also, if the field is left empty, the placeholder gets back into the Text.

No, this is not directly, possible with tkinter. You might want to use classes and OOP.

Related