0

I have made a custom placeholder entry class that inherits from the ttk.Entry. The problem with that is, it only works with the instantiation once, if I make two instances of the same entry, it will start to show some weird effects.

Class and its usage:

import tkinter as tk
from tkinter import ttk

class PlaceholderEntry(ttk.Entry):
    '''
    Custom modern Placeholder Entry box, takes positional argument master and placeholder\n
    Use acquire() for getting output from entry widget\n
    Use shove() for inserting into entry widget\n
    Use remove() for deleting from entry widget\n
    Use length() for getting the length of text in the widget\n
    BUG 1: Possible bugs with binding to this class\n
    BUG 2: Anomalous behaviour with config or configure method
    '''

    def __init__(self, master, placeholder, **kwargs):
        # style for ttk widget
        self.s = ttk.Style()
        self.s.configure('my.TEntry')

        # init entry box
        ttk.Entry.__init__(self, master, style='my.TEntry', **kwargs)
        self.text = placeholder
        self.__has_placeholder = False  # placeholder flag

        # add placeholder if box empty
        self._add()

        # bindings of the widget
        self.bind('<FocusIn>', self._clear)
        self.bind('<FocusOut>', self._add)
        self.bind_all('<Key>', self._normal)
        self.bind_all('<Button-1>', self._cursor)

    def _clear(self, *args):  # method to remove the placeholder
        if self.get() == self.text and self.__has_placeholder:  # remove placeholder when focus gain
            self.delete(0, tk.END)
            self.s.configure('my.TEntry', foreground='black',
                             font=(0, 0, 'normal'))
            self.__has_placeholder = False  # set flag to false

    def _add(self, *args):  # method to add placeholder
        if self.get() == '' and not self.__has_placeholder:  # if no text add placeholder
            self.s.configure('my.TEntry', foreground='grey',
                             font=(0, 0, 'bold'))
            self.insert(0, self.text)  # insert placeholder
            self.icursor(0)  # move insertion cursor to start of entrybox
            self.__has_placeholder = True  # set flag to true

    def _normal(self, *args):  # method to set the text to normal properties
        self._add()  # if empty add placeholder
        if self.get() == self.text and self.__has_placeholder:  # clear the placeholder if starts typing
            self.bind('<Key>', self._clear)
            self.icursor(-1)  # keep insertion cursor to the end
        else:
            self.s.configure('my.TEntry', foreground='black',
                             font=(0, 0, 'normal'))  # set normal font

    def acquire(self):  # custom method to get the text
        if self.get() == self.text and self.__has_placeholder:
            return 'None'
        else:
            return self.get()

    def shove(self, index, string):  # custom method to insert into entry
        self._clear()
        self.insert(index, string)

    def remove(self, first, last):  # custom method to remove from entry
        if self.get() != self.text:
            self.delete(first, last)
            self._add()
        elif self.acquire() == self.text and not self.__has_placeholder:
            self.delete(first, last)
            self._add()

    def length(self):
        if self.get() == self.text and self.__has_placeholder:
            return 0
        else:
            return len(self.get())

    def _cursor(self, *args):  # method to not allow user to move cursor when placeholder exists
        if self.get() == self.text and self.__has_placeholder:
            self.icursor(0)

# MRE

if __name__ == '__main__':
    root = tk.Tk()
    
    e1 = PlaceholderEntry(root,'First Entry')
    e1.pack()
    e2 = PlaceholderEntry(root,'Second Entry')
    e2.pack()
    
    root.mainloop()

Here on clicking one of the entry and changing focus to the next entry, you can notice the "behavior" that the both the instances are somewhat linked? Although a single instance works perfectly fine. It was my first OOP project, so im not sure what went wrong.

Thanks in advance :D

Delrius Euphoria
  • 14,910
  • 3
  • 15
  • 46
  • You should not use `bind_all()` as it is application-wise. Use `bind()` instead. – acw1668 Oct 30 '20 at 06:23
  • @acw1668 Tried replacing `bind_all()` with `bind()` but it just creates more issues in here and behavior still persists :( Could it be something with the creation of `self.s`? – Delrius Euphoria Oct 30 '20 at 06:24
  • Actually I think the bindings on `` and `` are not necessary. – acw1668 Oct 30 '20 at 06:41
  • actually code appears working fine and the only possible "behavior" is that before entering data when you click on entry text dissapears and clicking on th eother makes text appear but hides the on clicked on – Matiiss Oct 30 '20 at 06:44
  • @acw1668 I tried getting rid of it too, but the effects still remain same – Delrius Euphoria Oct 30 '20 at 06:44
  • @Matiiss That is a huge malfunction as far as entry boxes are concerned, right? – Delrius Euphoria Oct 30 '20 at 06:45
  • but what do you want then. You have set it so that if there is focus dont show text but if one has focus the other does not and so its full. so what do you want exactly?? – Matiiss Oct 30 '20 at 06:51
  • Try removing one instance created, and it will be working fine – Delrius Euphoria Oct 30 '20 at 06:52
  • The only weird thing I can see after removing the bindings of `` and `` is the color of the placeholder becomes black. – acw1668 Oct 30 '20 at 06:57
  • @acw1668 I think its more of a `ttk.Style()` issue, getting rid of that and making all the normal tk widget, seems to work, but i want to use the themed one – Delrius Euphoria Oct 30 '20 at 07:22
  • 1
    I have changed part of your code [here](https://pastebin.com/uLAQErXJ). See whether it is what you want. – acw1668 Oct 30 '20 at 07:28
  • @acw1668 It seems to be fine, but will there be a way to also add the placeholder when the user hits backspace and clears all the text inside the box? – Delrius Euphoria Oct 30 '20 at 07:36

1 Answers1

1

You have two problem, both related to the fact that your class depends on global data: you are using bind_all which effectively creates global bindings, and you are using the same style for both normal and placeholder but the style is global.

The first problem is with your use of bind_all. Each time you create an instance, the bind_all of that instance replaces the bindings from the previous instance. Both instances will inherit a binding that only calls the _normal and _cursor methods of the last instance that was created.

As a general rule of thumb, a class like this should always only create bindings on itself. You need to change self.bind_all to just self.bind.

self.bind('<Key>', self._normal)
self.bind('<Button-1>', self._cursor)

The second problem is your use of styles. You're using a single style for both widgets in both states, but since styles are global, when you change the style in one widget it affects the other.

Instead of having a single style that you reconfigure, you should instead configure two styles and switch between them. For example, you can create a my.TEntry style for the normal case, and a placeholder.TEntry style for when you want to display the placeholder.

Start by configuring the styles in the __init__ method:

def __init__(self, master, placeholder, **kwargs):
    # style for ttk widget
    self.s = ttk.Style()
    self.s.configure('my.TEntry', foreground='black', font=(0, 0, 'normal'))
    self.s.configure('placeholder.TEntry', foreground='grey', font=(0, 0, 'bold'))

Next, instead of reconfiguring the style definition, just swap the style on the widget. For example, _clear would look something like this:

def _clear(self, *args):  # method to remove the placeholder
    if self.get() == self.text and self.__has_placeholder:  
        self.configure(style="my.TEntry")
        ...

Similarly, _add should look something like this:

def _add(self, *args):  # method to add placeholder
    if self.get() == '' and not self.__has_placeholder:  # if no text add placeholder
        self.configure(style="placeholder.TEntry")
        ...

Finally, you have a bug in your logic, perhaps due to a misunderstanding how bindings work. When you bind to <Key>, that binding happens before the default bindings. Thus, your check in self._add where you're checking if the entry is empty happens before the key you just typed is inserted. Therefore, the code thinks the entry widget is empty and switches the style to have a placeholder. So, it adds the placeholder text and then the default handling of the key happens and inserts the key.

There are at least three solutions to that particular problem.

  1. You could bind on <KeyRelease> instead of <Key> since that will fire after the character has been inserted by the default bindings.
  2. You could add a custom bindtag that is after the default binding so that the default happens first and then your binding happens.
  3. You can stick with bind_all. In that case, you only need to do the binding once instead of once per widget, and you need to adjust your functions to use event.widget rather than a self.

This answer and This answer give a more detailed description of how bind tags work.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Oh yessss i see, the style part has been solved, But making `bind_all()` to `bind()` creates problem with the placeholders for me. Could you try it on and see? – Delrius Euphoria Oct 30 '20 at 18:06
  • @CoolCloud What is the problem? When I make these changes the placeholders seem to work for me. – Bryan Oakley Oct 30 '20 at 18:07
  • Changing `bind_all()` to `bind()` and trying on to type just adds to the placeholder, not as a new text, What i used `bind_all()` was for the fact that after the user hits backspace and empties out the entry box the placeholder should be inserted, is there any other way to mock this? – Delrius Euphoria Oct 30 '20 at 18:13
  • Thanks man, the binding to `` did the trick – Delrius Euphoria Oct 30 '20 at 19:04