2

Good Day everyone. I'm currently trying to implement a popup number pad with entry boxes for use on a pi 4 touchscreen. After searching for a while I found this forum with an effective solution provided by scotty101. Unfortunately, the code is only effective for a single entry widget. After fidgeting around for some time with my abysmal object-orientated knowledge, I cant seem to find a way to implement this code to work with multiple entry widgets.

A reduced working version of my code:

import tkinter as tk
from tkinter import simpledialog
from time import strftime

class Page(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
    def show(self):
        self.lift()  
    
class Page1(Page):                                                       
    def __init__(self, *args, **kwargs):
        Page.__init__(self, *args, **kwargs)
        self.Create_Page1_Widgets()
       
    def Create_Page1_Widgets(self):
        self.BackCan=tk.Canvas(self,width=800,height=440,borderwidth=0,bg="white")
        self.BackCan.place(x=0,y=0) 
        #Entry Boxes#
        self.Page1Var1 = tk.StringVar()
        self.Page1Var2 = tk.StringVar()
        self.Page1e1=tk.Entry(self,width=12,justify="center",textvariable=self.Page1Var1)
        self.Page1e1.bind('<FocusIn>',self.numpadEntry); self.edited = False
        self.Page1e1.place(x=10,y=163,width=102,height=26)  
        self.Page1e2=tk.Entry(self,width=12,justify="center",textvariable=self.Page1Var2)
        self.Page1e2.bind('<FocusIn>',self.numpadEntry);
        self.Page1e2.place(x=129,y=163,width=102,height=26)
    
    def numpadEntry(self, event):
        if self.edited == False:
            self.edited = True
            new = numPad(self,self)
        else:
            self.edited = False

class Page2(Page):                                             
    def __init__(self, *args, **kwargs):
        Page.__init__(self, *args, **kwargs)
        self.Create_Page2_Widgets()
    
    def Create_Page2_Widgets(self):      
        #Validation#
        #Page Backround#
        self.BackCan=tk.Canvas(self,width=800,height=440,borderwidth=0,bg="white")
        self.BackCan.place(x=0,y=0)
        ##Entry Boxes##
        self.Page2Var1 = tk.StringVar()
        self.Page2Var2 = tk.StringVar()
        self.PrefertHRe=tk.Entry(self,width=12,justify="center",textvariable=self.Page2Var1)
        self.PrefertHRe.bind('<FocusIn>',self.numpadEntry); self.edited = False #<-calls numpad
        self.PrefertHRe.place(x=10,y=200,width=102,height=26)
        self.PrefertMINe=tk.Entry(self,width=12,justify="center",textvariable=self.Page2Var2)
        self.PrefertMINe.place(x=129,y=200,width=102,height=26)
    
    def numpadEntry(self, event):
        if self.edited == False:
            self.edited = True
            new = numPad(self,self)
        else:
            self.edited = False

class numPad(simpledialog.Dialog):
    def __init__(self,master=None,parent=None):
        self.parent = parent
        self.top = tk.Toplevel(master=master)
        self.top.protocol("WM_DELETE_WINDOW",self.ok)
        self.createWidgets()
    def createWidgets(self):
        btn_list = ['7',  '8',  '9', '4',  '5',  '6', '1',  '2',  '3', '0',  'Close',  'Del']
        r = 1
        c = 0
        n = 0
        btn = []
        for label in btn_list:
            cmd = lambda x = label: self.click(x)
            cur = tk.Button(self.top, text=label, width=6, height=3, command=cmd)
            btn.append(cur)
            btn[-1].grid(row=r, column=c)
            n += 1
            c += 1
            if c == 3:
                c = 0
                r += 1
    def click(self,label):
        print(label)
        if label == 'Del':
            currentText = self.parent.Page1Var1.get() #<--Page1Var1 need to be dynamic?
            self.parent.Page1Var1.set(currentText[:-1])
        elif label == 'Close':
            self.ok()
        else:
            currentText = self.parent.Page1Var1.get()
            self.parent.Page1Var1.set(currentText+label)
        
    def ok(self):
        self.top.destroy()
        self.top.master.focus()

class MainView(tk.Frame):
    def __init__(self,  *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        super().__init__()
        #Navigation Frame#
        p2 = Page2(self);p1 = Page1(self)
        Navigation_frame = tk.Frame(self, width=800, height=55, background="bisque")
        container = tk.Frame(self)
        Navigation_frame.pack(side="bottom");Navigation_frame.pack_propagate(0)
        container.pack(side="top", fill="both", expand=True)
        NavCan=tk.Canvas(Navigation_frame,width=800,height=55,borderwidth=0,bg="white")
        NavCan.place(x=0,y=0)
        p1.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        p2.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        b1 = tk.Button(Navigation_frame, height=2, width=10, text="1", command=p1.lift)
        b2 = tk.Button(Navigation_frame, height=2, width=10, text="2", command=p2.lift)
        b1.place(x=144, y=6);b2.place(x=253, y=6)       
        #Clock#
        def clock(): 
            string = strftime('%H:%M:%S')
            lbl.config(text = string); lbl.after(1000, clock)
        #Clock Label#
        lbl = tk.Label(Navigation_frame,font=("Arial",20,'bold'),background= 'grey',foreground 
        = 'black'); lbl.place(x=20, y=12)  
        p1.show()
        clock()
   
if __name__ == "__main__":
    root = tk.Tk()
    main = MainView(root)
    main.pack(side="top", fill="both", expand=True)
    root.wm_geometry("800x440")
    root.attributes('-fullscreen', False)
    root.mainloop()

So the Question is, Is there a way to change the numpad focus to that of another entry widget?

Adriaan vS
  • 43
  • 5
  • 1
    Your program is not runnable. I'm getting a `'MainView' object has no attribute 'initUI'` error. Please make it runnable for us to test it – Saad Aug 04 '20 at 11:04
  • @Saad Sorry about that, it was just a call to an unnecessary feature for this question that i removed but never removed the call to – Adriaan vS Aug 04 '20 at 11:18
  • You need to bind `.bind('',self.numpadEntry)` with every Entry widget you want the numpad. Your code can be a lot simpler than it looks. – Saad Aug 04 '20 at 11:20
  • Also you can use `event.widget` to get the entry that fired the event. – j_4321 Aug 04 '20 at 11:24
  • @Saad Bringing up the numbad isnt the problem here. Its setting the Numpad to change the variable of the selected entry widget as its currently only changing the 1st entry widget due to this `currentText = self.parent.Page1Var1.get();self.parent.Page1Var1.set(currentText[:-1])` piece of code – Adriaan vS Aug 04 '20 at 11:33

2 Answers2

2

The idea is to have a current_entry attribute in MainView that contains the currently selected entry. Then you define the numpadEntry() function as a method of MainView to update the value of current_entry:

def numpadEntry(self, event):
    # change current entry
    self.current_entry = event.widget

    if self.numpad is None:  # numpad does not exists
        self.numpad = NumPad(self)
    else:
        self.numpad.lift()   # make numpad visible

In this function I also assumed that MainView has a numpad attribute which is the NumPad window.

Now you can bind <FocusIn> on all your entries to numpadEntry to edit the current one. Then in the numpad, instead of modifying a StringVar, you directly modify the entry content with entry.delete(-1) and entry.insert('end', <char>).

Full code:

import tkinter as tk

class Page(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        self.create_widgets()

    def create_widgets(self):
        pass

    def show(self):
        self.lift()

class Page1(Page):
    def create_widgets(self):
        self.BackCan = tk.Canvas(self, width=800, height=440, borderwidth=0, bg="white")
        self.BackCan.place(x=0, y=0)
        #Entry Boxes#
        self.Page1e1 = tk.Entry(self, width=12, justify="center")
        self.Page1e1.bind('<FocusIn>', self.master.numpadEntry)
        self.edited = False
        self.Page1e1.place(x=10, y=163, width=102, height=26)
        self.Page1e2 = tk.Entry(self, width=12, justify="center")
        self.Page1e2.bind('<FocusIn>', self.master.numpadEntry)
        self.Page1e2.place(x=129, y=163, width=102, height=26)

class Page2(Page):
    def create_widgets(self):
        #Validation#
        #Page Backround#
        self.BackCan = tk.Canvas(self, width=800, height=440, borderwidth=0, bg="white")
        self.BackCan.place(x=0, y=0)
        ##Entry Boxes##
        self.PrefertHRe = tk.Entry(self, width=12, justify="center")
        self.PrefertHRe.bind('<FocusIn>', self.master.numpadEntry)
        self.edited = False #<-calls numpad
        self.PrefertHRe.place(x=10, y=200, width=102, height=26)
        self.PrefertMINe = tk.Entry(self, width=12, justify="center")
        self.PrefertMINe.place(x=129, y=200, width=102, height=26)
        self.PrefertMINe.bind('<FocusIn>', self.master.numpadEntry)

class NumPad(tk.Toplevel):
    def __init__(self, master=None):
        tk.Toplevel.__init__(self, master)
        self.protocol("WM_DELETE_WINDOW", self.ok)
        self.createWidgets()

    def createWidgets(self):
        btn_list = ['7', '8', '9', '4', '5', '6', '1', '2', '3', '0', 'Close', 'Del']
        r = 1
        c = 0
        n = 0
        
        for label in btn_list:
            cur = tk.Button(self, text=label, width=6, height=3,
                            command=lambda x=label: self.click(x))
            
            cur.grid(row=r, column=c)
            n += 1
            c += 1
            if c == 3:
                c = 0
                r += 1

    def click(self, label):
        if label == 'Del':
            self.master.current_entry.delete(-1)
        elif label == 'Close':
            self.ok()
        else:
            self.master.current_entry.insert('end', label)

    def ok(self):
        self.destroy()
        self.master.focus()
        self.master.numpad = None

class MainView(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)

        self.numpad = None  # NumPad
        self.current_entry = None  # currently selected entry

        p2 = Page2(self)
        p1 = Page1(self)
        Navigation_frame = tk.Frame(self, width=800, height=55, background="bisque")
        container = tk.Frame(self)
        Navigation_frame.pack(side="bottom")
        Navigation_frame.pack_propagate(0)
        container.pack(side="top", fill="both", expand=True)
        NavCan = tk.Canvas(Navigation_frame, width=800, height=55, borderwidth=0, bg="white")
        NavCan.place(x=0, y=0)
        p1.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        p2.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        b1 = tk.Button(Navigation_frame, height=2, width=10, text="1", command=p1.show)
        b2 = tk.Button(Navigation_frame, height=2, width=10, text="2", command=p2.show)
        b1.place(x=144, y=6)
        b2.place(x=253, y=6)

        p1.show()

    def numpadEntry(self, event):
        # change current entry
        self.current_entry = event.widget
        # create numpad if does not exist yet
        if self.numpad is None:
            self.numpad = NumPad(self)
        else:
            self.numpad.lift()


if __name__ == "__main__":
    root = tk.Tk()
    main = MainView(root)
    main.pack(side="top", fill="both", expand=True)
    root.wm_geometry("800x440")
    root.attributes('-fullscreen', False)
    root.mainloop()    
j_4321
  • 15,431
  • 3
  • 34
  • 61
  • List `btn` can be removed from `createWidgets` function as it is completely unnecessary. – Saad Aug 04 '20 at 12:29
  • 1
    Should use `insert('insert', ...)` instead of `insert('end', ...)` because the input cursor may not be always at the end. – acw1668 Aug 04 '20 at 12:37
  • @Saad Yes sure, I improved a bit the original code but I let some useless things still. – j_4321 Aug 04 '20 at 13:46
  • @acw1668 Yes, sure I am aware of that. But in the original code the insertion/deletion was always at the end of the entry so I kept the same behavior. Anyway the OP can adapt that if wanted. – j_4321 Aug 04 '20 at 13:51
  • @j_4321 Already upvoted and Accepted, very nice work, but I noticed a strange glitch recently. When the Application runs in full screen `else: self.numpad.lift()` does not seem to work, but in windowed mode it works. Odd... – Adriaan vS Aug 26 '20 at 11:27
  • @YuMChUM777 which os are you using? And do you use `overrideredirect(True)` when app is fullscreen? I have tested my code in linux with `root.attributes('-fullscreen', True)` and the numpad was above the window. – j_4321 Aug 26 '20 at 12:16
  • @j_4321 I'm Making use of the Raspberry Pi OS and not using `overrideredirect(True)` (1st time hearing of it now), only `root.attributes('-fullscreen', True)`. In which case, clicking on a entry box creates the numpad, but if I click on another entry box while its still open, it disappears behind the screen with no way of lifting it again. Its can be circumvented by closing it each time. Thanks for the quick response – Adriaan vS Aug 26 '20 at 13:22
  • 1
    @YuMChUM777 maybe add `self.attributes('-topmost', True)` in `NumPad.__init__()` so that it is always on top – j_4321 Aug 26 '20 at 13:36
  • @j_4321 Oddly enough that was the 1st solution I tried. Results in no errors but does nothing. Maybe there's a way to bind `` to call a Method that closes the Numpad? Testing it now but its glitchy, will let you know if get it to work. – Adriaan vS Aug 26 '20 at 13:55
  • @j_4321 Sorry for the long delay. So some researched shows that tkinter doesn't allow lifting above the page when in full screen, so its not a glitch in the code. But just how tinker works. To circumvent the problem. I added `if self.numpad is not None: self.numpadExit()` in the start of `def numpadEntry` to close the previous one before creating a new one that appears on top. – Adriaan vS Oct 22 '20 at 08:54
  • @YuMChUM777 Glad to hear that you found a workaround. I think this behavior is OS specific since `self.attributes('-topmost', True)` works fine for me with a fullscreen window in linux. – j_4321 Oct 22 '20 at 09:14
0

I changed the events to pass the stringVar object to the number pad. There is some issue with number pad coming up, but that is related to your edit flags.

import tkinter as tk
from tkinter import simpledialog
from time import strftime


class Page(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)

    def show(self):
        self.lift()


class Page1(Page):
    def __init__(self, *args, **kwargs):
        Page.__init__(self, *args, **kwargs)
        self.Create_Page1_Widgets()

    def Create_Page1_Widgets(self):
        self.BackCan = tk.Canvas(self, width=800, height=440, borderwidth=0, bg="white")
        self.BackCan.place(x=0, y=0)
        # Entry Boxes#
        self.Page1Var1 = tk.StringVar()
        self.Page1Var2 = tk.StringVar()
        self.Page1e1 = tk.Entry(self, width=12, justify="center", textvariable=self.Page1Var1)
        self.Page1e1.bind('<FocusIn>', lambda event: self.numpadEntry(event, self.Page1Var1))
        self.edited = False
        self.Page1e1.place(x=10, y=163, width=102, height=26)
        self.Page1e2 = tk.Entry(self, width=12, justify="center", textvariable=self.Page1Var2)
        self.Page1e2.bind('<FocusIn>', lambda event: self.numpadEntry(event, self.Page1Var2))
        self.Page1e2.place(x=129, y=163, width=102, height=26)

    def numpadEntry(self, event, string_var):
        if self.edited == False:
            self.edited = True
            new = numPad(string_var)
        else:
            self.edited = False


class Page2(Page):
    def __init__(self, *args, **kwargs):
        Page.__init__(self, *args, **kwargs)
        self.Create_Page2_Widgets()

    def Create_Page2_Widgets(self):
        # Validation#
        # Page Backround#
        self.BackCan = tk.Canvas(self, width=800, height=440, borderwidth=0, bg="white")
        self.BackCan.place(x=0, y=0)
        ##Entry Boxes##
        self.Page2Var1 = tk.StringVar()
        self.Page2Var2 = tk.StringVar()
        self.PrefertHRe = tk.Entry(self, width=12, justify="center", textvariable=self.Page2Var1)
        self.PrefertHRe.bind('<FocusIn>', lambda event: self.numpadEntry(event, self.Page2Var1))
        self.edited = False  # <-calls numpad
        self.PrefertHRe.place(x=10, y=200, width=102, height=26)
        self.PrefertMINe = tk.Entry(self, width=12, justify="center", textvariable=self.Page2Var2)
        self.PrefertMINe.place(x=129, y=200, width=102, height=26)

    def numpadEntry(self, event, string_var):
        if self.edited == False:
            self.edited = True
            new = numPad(string_var)
        else:
            self.edited = False


class numPad(simpledialog.Dialog):
    def __init__(self, the_entry):
        self.top = tk.Toplevel(master=None)
        self.top.protocol("WM_DELETE_WINDOW", self.ok)
        self.the_entry_widget = the_entry
        self.createWidgets()

    def createWidgets(self):
        btn_list = ['7', '8', '9', '4', '5', '6', '1', '2', '3', '0', 'Close', 'Del']
        r = 1
        c = 0
        n = 0
        btn = []
        for label in btn_list:
            cmd = lambda x=label: self.click(x)
            cur = tk.Button(self.top, text=label, width=6, height=3, command=cmd)
            btn.append(cur)
            btn[-1].grid(row=r, column=c)
            n += 1
            c += 1
            if c == 3:
                c = 0
                r += 1

    def click(self, label):
        print(label)
        if label == 'Del':
            currentText = self.the_entry_widget.get()  # <--Page1Var1 need to be dynamic?
            self.the_entry_widget.set(currentText[:-1])
        elif label == 'Close':
            self.ok()
        else:
            currentText = self.the_entry_widget.get()
            self.the_entry_widget.set(currentText + label)

    def ok(self):
        self.top.destroy()
        self.top.master.focus()


class MainView(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        super().__init__()
        # Navigation Frame#
        p2 = Page2(self)
        p1 = Page1(self)
        Navigation_frame = tk.Frame(self, width=800, height=55, background="bisque")
        container = tk.Frame(self)
        Navigation_frame.pack(side="bottom")
        Navigation_frame.pack_propagate(0)
        container.pack(side="top", fill="both", expand=True)
        NavCan = tk.Canvas(Navigation_frame, width=800, height=55, borderwidth=0, bg="white")
        NavCan.place(x=0, y=0)
        p1.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        p2.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        b1 = tk.Button(Navigation_frame, height=2, width=10, text="1", command=p1.lift)
        b2 = tk.Button(Navigation_frame, height=2, width=10, text="2", command=p2.lift)
        b1.place(x=144, y=6)
        b2.place(x=253, y=6)

        # Clock#
        def clock():
            string = strftime('%H:%M:%S')
            lbl.config(text=string)
            lbl.after(1000, clock)

        # Clock Label#
        lbl = tk.Label(Navigation_frame, font=("Arial", 20, 'bold'), background='grey', foreground
        ='black')
        lbl.place(x=20, y=12)
        p1.show()
        clock()


if __name__ == "__main__":
    root = tk.Tk()
    main = MainView(root)
    main.pack(side="top", fill="both", expand=True)
    root.wm_geometry("800x440")
    root.attributes('-fullscreen', False)
    root.mainloop()
ShayneLoyd
  • 633
  • 1
  • 4
  • 9