Python Forum

Full Version: Pixel sized button does not work associated method
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello, Python Community

I'm working on a calculator with tkinter. Actually, I'm learning Python to be able to teach to a friend of mine, I'm a (.Net C#) programmer, she's not.
So I created 10 buttons for the digits, and their respective methods which write each digit in the textbox (Entry). Buuuuut ... I had the curious idea of sizing the buttons (in pixels), and searched the web, and found out that I had to add an (invisible 1x1) PhotoImage to the buttons to be able to size them in pixels (those are matters of Python I don't understand/like, that it is too elaborate in some aspects), because, as you may imagine, in the GUI calculator buttons need to be of a customized size. Well, it worked, but on the button I do this, then the associated method does not work any more, and I can't absolutely figure out why. That is, in my code button0 looks like desired, but does not print the 0 character in the textbox.
I will appreciate any help for solving this issue.
I attach the code by now.
Thank you!
Pablo

import tkinter as tk
from tkinter import ttk

class MainWindow(tk.Tk):
    global entry
    def __init__(self):
        global entry
        super().__init__()
        self.geometry("400x600+100+100")
        self.title("Calculadora Científica Python")

        entry = ttk.Entry(width=300, font=('Courier New', 20))
        entry.place(x=10, y=10)

        pixel = tk.PhotoImage(width=1, height=1)
        button0 = tk.Button(text='0', width=60, height=40, image=pixel, compound='c', command=self.button0Press)
        button0.place(x=160, y=540)
        button1 = tk.Button(text='1', command=self.button1Press)
        button1.place(x=80, y=480)
        button2 = tk.Button(text='2', command=self.button2Press)
        button2.place(x=160, y=480)
        button3 = tk.Button(text='3', command=self.button3Press)
        button3.place(x=240, y=480)
        button4 = tk.Button(text='4', command=self.button4Press)
        button4.place(x=80, y=420)
        button5 = tk.Button(text='5', command=self.button5Press)
        button5.place(x=160, y=420)
        button6 = tk.Button(text='6', command=self.button6Press)
        button6.place(x=240, y=420)
        button7 = tk.Button(text='7', command=self.button7Press)
        button7.place(x=80, y=360)
        button8 = tk.Button(text='8', command=self.button8Press)
        button8.place(x=160, y=360)
        button9 = tk.Button(text='9', command=self.button9Press)
        button9.place(x=240, y=360)


    def button0Press(self):
        global entry
        entry.insert(tk.END, '0')

    def button1Press(self):
        global entry
        entry.insert(tk.END, '1') 

    def button2Press(self):
        global entry
        entry.insert(tk.END, '2')

    def button3Press(self):
        global entry
        entry.insert(tk.END, '3')

    def button4Press(self):
        global entry
        entry.insert(tk.END, '4')

    def button5Press(self):
        global entry
        entry.insert(tk.END, '5')

    def button6Press(self):
        global entry
        entry.insert(tk.END, '6')

    def button7Press(self):
        global entry
        entry.insert(tk.END, '7')

    def button8Press(self):
        global entry
        entry.insert(tk.END, '8')

    def button9Press(self):
        global entry
        entry.insert(tk.END, '9')

MainWindow().mainloop()
This works. On a side not it's better practice to not use globals. You created a class so
instead of entry and in the methods/functions global entry you can use self.entry and access the instance variable of self.

import tkinter as tk

root = tk.Tk()
root.geometry('400x200')

pixel = tk.PhotoImage(width=1, height=1)

btn = tk.Button(root, image=pixel, compound='center', text='Submit', width=74, height=10)
btn.pack()

btn2 = tk.Button(root, image=pixel, compound='center', text='Submit', width=124, height=20)
btn2.pack()

btn3 = tk.Button(root, image=pixel, compound='center', text='Submit', width=35, height=100)
btn3.pack()


root.mainloop()
Another working example. Can use mouse or keypad numbers. esc will clear and either return will total

import tkinter as tk
from decimal import Decimal

class Window(tk.Tk):
    ''' Class creates the display window '''
    def __init__(self):
        super().__init__()
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.resizable(False, False)
        self.title('Tkinter Basic Calculator')

        # Create a textvariable for entry manipulation
        self.var = tk.StringVar()
        self.entry = tk.Entry(self, textvariable=self.var)
        self.entry.grid(column=0, row=0, sticky='we', padx=5, pady=3)
        self.entry.insert('end', '0')

        # Sert focus on the entry widget
        self.entry.focus()

        # Create container for buttons
        btn_container = tk.Frame(self)
        btn_container.grid(column=0, row=1, sticky='news')
        for i in range(3):
            btn_container.grid_columnconfigure(i, weight=3, uniform='button')

        # Create the dgits for button text
        digits = [i for i in range(1, 10)]

        # Empty list to hold buttons and some variables for columns and rows
        buttons = []
        i = 0
        col, row = 0, 0

        # 1x1 image
        pixel = tk.PhotoImage(width=1, height=1)

        # Keep a reference of image
        pixel.backup = pixel # Note button will not work without this

        # Create the buttons
        for button in digits:
            buttons.append(tk.Button(btn_container, text=button, width=74, height=15))
            buttons[i]['image'] = pixel
            buttons[i]['compound'] = 'left'
            buttons[i]['cursor'] = 'hand2'
            buttons[i]['command'] = lambda btn=buttons[i]['text']: self.pressed(btn)
            buttons[i].grid(column=col, row=row, padx=2, pady=2)
            
            # Increatment for each loop through      
            i += 1

            # Set columns and rows
            if col >= 2:
                row += 1
                col = 0
            else:
                col += 1
        
        # Create the zeo button
        zero = tk.Button(btn_container, text='0', image=pixel, compound='center')
        zero.grid(column=0, row=3, sticky='we', padx=2, pady=2)
        zero.configure(command=lambda val='0': self.pressed(val))
        
        # Button to clear tthe label text
        clear_btn = tk.Button(btn_container, text='Clear', width=74, height=15, compound='center', command=self.clear)
        clear_btn.grid(column=0, columnspan=4, row=4, sticky='new', padx=2, pady=2)
        clear_btn.configure(cursor='hand2', bg='tomato', activebackground='red', fg='white', activeforeground='white',
        image=pixel)

        # Create the decimal/period button
        self.period = tk.Button(btn_container, text='.', anchor='s', width=74, height=15, image=pixel, compound='center')
        self.period.grid(column=1, row=3, sticky='we', padx=2, pady=2, ipady=1)
        self.period.configure(font=(None, 24), command=lambda val='.': self.pressed(val))

        # Create the equal button
        self.eq_btn = tk.Button(btn_container, text='=', width=74, height=15, image=pixel, compound='center')
        self.eq_btn.grid(column=2, row=3, sticky='we', padx=2, pady=2)
        self.eq_btn.configure(font=(None, 24), command=lambda: self.calculate())

        # Create list of symbols
        symb = ['x','/','+','-']

        # Empty list to hold symbols buttons
        symbols = []
        
        for i in range(len(symb)):
            button = tk.Button(btn_container, text=symb[i])
            symbols.append(button)
            symbols[i]['command'] = lambda symbol=button.cget('text'): self.pressed(symbol)
            symbols[i].grid(column=3, row=i, sticky='we', padx=2, pady=2)
            symbols[i].configure(font=(None, 18), width=74, height=15, image=pixel, compound='center')

        # Bind some buttons
        self.bind('<Return>', self.calculate)
        self.bind('<KP_Enter>', self.calculate)
        self.bind('<Escape>', self.clear)

        # Using trace to manipulate the the entry field
        self.var.trace('w', self.change)

        # Check for valid input
        valid = self.register(self.valid)
        self.entry.configure(validate='key', validatecommand=(valid, '%S'))

    def valid(self, arg):
        ''' Method for validating characters insert. We only want numbers
            or calculation symbols '''
        if arg == 'x':
            return True
        elif arg.isalpha() or arg == '=':
            return False
        else:
            return True

    def change(self, var, mode, index):
        ''' Method will replace * with x for the multiply symbol '''
        var = self.var.get()
        var = var.replace('*', 'x')

        # If there is a leading zero replace with entry
        if self.var.get()[:-1] == '0' and len(var) ==2:
            var = self.var.get()[1:]

        self.entry.delete(0, 'end')
        self.entry.insert('end', var)

    # method for the buttons
    def pressed(self, btn):
        ''' Method places pressed button keys into the entry field '''
        self.entry.insert('end', btn)

    def calculate(self, event=None):
        ''' Method for calaculting results '''
        values = self.entry.get()

        # If x in the calculation replace with *
        if 'x' in values:
            values = values.replace('x', '*')
        result = eval(values)

        # If the results is a float and has only zero on the right side, normalize
        if isinstance(result, float):
            result = Decimal(result).normalize()
        else:
            result = result

        # Clear entry field and insert results
        self.entry.delete(0, 'end')
        self.entry.insert('end', result)
        
    # Clear the entry field
    def clear(self, event=None):
        self.entry.delete(0, 'end')
        self.entry.insert('end', '0')


Window().mainloop()
Hello,
Thank you for your help and code.
With only this
pixel.backup = pixel
my own code works fine.
But I learnt another things from your code.
Now I'm trying to split the Entry.get() string to get both operands and then do the calculation, but the split function is throwing errors that I can´t figure out what they are.
This is my Calculate function:
    def Calculate(self):
        
        expr = self.entry.get()
        array = re.split('+ |- |x |/', expr)
        str1 = array[0]
        str2 = array[1]

        if self.what == '+':
            result = Decimal(str1) + Decimal(str2)
        elif self.what == '-':
            result = Decimal(str1) - Decimal(str2)
        elif self.what == 'x':
            result = Decimal(str1) * Decimal(str2)
        elif self.what == '/':
            result = Decimal(str1) / Decimal(str2)
        self.entry.delete(0, tk.END)
        self.entry.insert(tk.END, str(result))
        self.oper = False
        self.replace = True
This is the error message:
Error:
re.PatternError: nothing to repeat at position 0 (the Entry is justify=RIGHT)
Would you help me, once again?
Thank you in advance
Pablo
You need to read the re documentation.
https://docs.python.org/3/library/re.html

"+" has special meaning in a regular expression. If you want to look for the "+" character, you need to precede with a backslash.

As long as you're doing some reading, you should also read about tkinter variables (StringVar, IntVar, DoubleVar, BoolVar). Using variables is easier than using insert().
https://www.geeksforgeeks.org/python-set...-variable/

And you can also read about python operators.
https://docs.python.org/3/library/operator.html
Hello all who are helping me

What I was looking for before is:

array = re.split('[+ -  x /]', expr)
that gave me the 2 operands for the calculation.

Now I have another issue

        buttonCalculate = self.CreateButton('=', pixel, self.buttonCalculatePress)
        buttonCalculate.place(x=240, y=540)

        radical = tk.PhotoImage(Image.open('radical.png'))
        radical.backup = radical

        buttonSqrt = self.CreateButtonSpec(radical, self.buttonSqRtPress)
        buttonSqrt.place(x=80, y=300)

    def CreateButton(self, what, pixel, cmd) -> tk.Button:
        btn = tk.Button(text=what, font=['Arial', 16], width=60, height=40, image=pixel, compound='left', command=cmd)
        return btn

    def CreateButtonSpec(self, pic, cmd) -> tk.Button:
        btn = tk.Button(width=60, height=40, image=pic, compound='center', command=cmd)
        return btn
Here the function CreateButton() works fine, but the function CreateButtonSpec() doesn't, I get an error on its first line, and (now) the UI doesn't even show. I'm trying to make a Button with an image (the 'radical.png' file is in the same folder than the python file)
Can you help me to fix this?

I appreciate your help
Thanks
Pablo
Where is your error message? If you have an error, you should post the error message and the traceback. You should also post code than can be run by others to reproduce the error. This includes posting the imports. I don't know of you are using PhotoImage from tkinter or Photoimage from PIL.ImageTk.

If you want to use PIL to open an image file and convert to an image you can use in a button, do it like this:
import tkinter as tk
from PIL import Image, ImageTk

root = tk.Tk()
image = ImageTk.PhotoImage(Image.open("true_img.png"))
tk.Button(root, image=image, compound="left").pack()
root.mainloop()
All of your button creates are wrong. The first argument to Button() is the object in which the button will reside, as shown in the example above.
You currently get away with not doing this because all of your widgets reside in the root widget. May as well start doing thing correctly.

I don't understand what self is.
def CreateButton(self, what, pixel, cmd) -> tk.Button:
    btn = tk.Button(text=what, font=['Arial', 16], width=60, height=40, image=pixel, compound='left', command=cmd)
    return btn
Normally "self" is the first argument to an instance method in a class, but you don't define a class. I can tell this by the indentation. A class method cannot start on the left margin (no indent). So what is self?

Setting button sizes and using place() are bad practices. You should use a layout manager like pack() or grid(). Let a layout manager set the button sizes and alignments.
Hello

I solved the image issue with using ImageTk instead of tk.

Now I have another, very strange issue, when I press the ln x button, I see in the debugger the correct result, but it doesn't get written on the Entry widget.

I attach all the code by now, you can find the issue part of the code at the very below.
I suppose it's a silly issue but can't figure out anything.

Thank you very much.
Pablo

import tkinter as tk
from decimal import Decimal
import re
from PIL import ImageTk, Image
import math

class MainWindow(tk.Tk):

    point = False
    oper = False
    entry = None
    what = None
    replace = False

    def __init__(self):

        super().__init__()
        self.geometry("400x600+100+100")
        self.title("Calculadora Científica Python")
        self.resizable(width=False, height=False)

        self.entry = tk.Entry(width=28, font=('Courier New', 16), justify=tk.RIGHT)
        self.entry.place(x=10, y=10)

        pixel = tk.PhotoImage(width=1, height=1)
        pixel.backup = pixel # Note button will not work without this
        button0 = self.CreateButton('0', pixel, self.button0Press)
        button0.place(x=160, y=540)
        button1 = self.CreateButton('1', pixel, self.button1Press)
        button1.place(x=80, y=480)
        button2 = self.CreateButton('2', pixel, self.button2Press)
        button2.place(x=160, y=480)
        button3 = self.CreateButton('3', pixel, self.button3Press)
        button3.place(x=240, y=480)
        button4 = self.CreateButton('4', pixel, self.button4Press)
        button4.place(x=80, y=420)
        button5 = self.CreateButton('5', pixel, self.button5Press)
        button5.place(x=160, y=420)
        button6 = self.CreateButton('6', pixel, self.button6Press)
        button6.place(x=240, y=420)
        button7 = self.CreateButton('7', pixel, self.button7Press)
        button7.place(x=80, y=360)
        button8 = self.CreateButton('8', pixel, self.button8Press)
        button8.place(x=160, y=360)
        button9 = self.CreateButton('9', pixel, self.button9Press)
        button9.place(x=240, y=360)
        buttonPlus = self.CreateButton('+', pixel, self.buttonPlusPress)
        buttonPlus.place(x=320, y=360)
        buttonMinus = self.CreateButton('-', pixel, self.buttonMinusPress)
        buttonMinus.place(x=320, y=420)
        buttonMulBy = self.CreateButton('x', pixel, self.buttonMulByPress)
        buttonMulBy.place(x=320, y=480)
        buttonDivBy = self.CreateButton('/', pixel, self.buttonDivByPress)
        buttonDivBy.place(x=320, y=540)
        buttonPoint = self.CreateButton('.', pixel, self.buttonPointPress)
        buttonPoint.place(x=80, y=540)
        buttonCalculate = self.CreateButton('=', pixel, self.buttonCalculatePress)
        buttonCalculate.place(x=240, y=540)

        radical = ImageTk.PhotoImage(Image.open('radical.png'))
        radical.backup = radical

        buttonSqrt = self.CreateButtonSpec(radical, self.buttonSqRtPress)
        buttonSqrt.place(x=80, y=300)

        buttonLnx = self.CreateButton('ln x', pixel, self.buttonLnxPress)
        buttonLnx.place(x=160, y=300)

        # Check for valid input
        valid = self.register(self.valid)
        self.entry.configure(validate='key', validatecommand=(valid, '%S'))
 
    def valid(self, arg):
        ''' Method for validating characters insert. We only want numbers
            or calculation symbols '''
        if arg.isdigit():
            return True
        elif (arg == '+' or arg == '-' or arg == 'x' or arg == '/') and self.oper == False:
            self.oper = True
            self.point = False
        elif arg == '.' and self.point == False:
            self.point = True
            return True
        else:
            return False

    def CreateButton(self, what, pixel, cmd) -> tk.Button:
        btn = tk.Button(self, text=what, font=['Arial', 16], width=60, height=40, image=pixel, compound='left', command=cmd)
        return btn

    def CreateButtonSpec(self, pic, cmd) -> tk.Button:
        btn = tk.Button(self, width=60, height=40, image=pic, compound='left', command=cmd)
        return btn

    def PressButton(self, what):
        if self.replace == True:
            self.entry.delete(0, tk.END)
            self.replace = False
        self.entry.insert(tk.END, what)

    def button0Press(self):
        self.PressButton('0')

    def button1Press(self):
        self.PressButton('1')

    def button2Press(self):
        self.PressButton('2')

    def button3Press(self):
        self.PressButton('3')

    def button4Press(self):
        self.PressButton('4')

    def button5Press(self):
        self.PressButton('5')

    def button6Press(self):
        self.PressButton('6')

    def button7Press(self):
        self.PressButton('7')

    def button8Press(self):
        self.PressButton('8')

    def button9Press(self):
        self.PressButton('9')

    def buttonPlusPress(self):
        if self.oper == False:
            self.entry.insert(tk.END, '+')
            self.what = '+'
        self.oper = True

    def buttonMinusPress(self):
        if self.oper == False:
            self.entry.insert(tk.END, '-')
            self.what = '-'
        self.oper = True

    def buttonMulByPress(self):
        if self.oper == False:
            self.entry.insert(tk.END, 'x')
            self.what = 'x'
        self.oper = True

    def buttonDivByPress(self):
        if self.oper == False:
            self.entry.insert(tk.END, '/')
            self.what = '/'
        self.oper = True

    def buttonPointPress(self):
        if self.point == False:
            self.entry.insert(tk.END, '.')
        self.point = True

    def buttonCalculatePress(self):
        self.Calculate()

    def Calculate(self):
        
        expr = self.entry.get()
        array = re.split('[+ - x /]', expr)
        if (len(array) == 1):
            self.point = False
            return
        str1 = array[0]
        str2 = array[1]

        if self.what == '+':
            result = Decimal(str1) + Decimal(str2)
        elif self.what == '-':
            result = Decimal(str1) - Decimal(str2)
        elif self.what == 'x':
            result = Decimal(str1) * Decimal(str2)
        elif self.what == '/':
            result = Decimal(str1) / Decimal(str2)
        self.entry.delete(0, tk.END)
        self.entry.insert(tk.END, str(result))
        self.oper = False
        self.replace = True

    def buttonSqRtPress(self):
        self.Calculate()
        arg = Decimal(self.entry.get())
        result = math.sqrt(arg)
        self.entry.delete(0, tk.END)
        self.entry.insert(tk.END, str(result))

    def buttonLnxPress(self):
        self.Calculate()
        arg = Decimal(self.entry.get())
        result = math.log(arg)
        self.entry.delete(0, tk.END)
        self.entry.insert(tk.END, str(result))

MainWindow().mainloop()
Your valid() function is incorrect. Validators are tricky. Comment out line 71 until you learn enough to write one correctly.

I would use a Label instead of an Entry. If you want to support keyboard entry, bind the key down event to a function that calls your button press functions. This eliminates the need for a validator, which, as I said, is tricky to write.

You can replace all your button press functions with one function that takes an argument.
import tkinter as tk
from decimal import Decimal
import re
import math
import operator


class Calculator(tk.Tk):
    """Calculator application."""
    font = ['Arial', 16]
    small_font = ['Arial', 12]
    large_font = ['Arial', 18]
    digits = 16

    def __init__(self):
        """Initialize instance."""

        def button(label, row, column, key=None):
            """Make a button.  Position in row, column."""
            if key is None:
                key = label[0]
            font = self.small_font if len(label) > 1 else self.font
            button = tk.Button(self, text=label, font=font, command=lambda: self._button_press(key))
            button.grid(row=row, column=column, sticky="news")
            self._buttons[key] = button

        super().__init__()
        self._point = False
        self._operator = None
        self._replace = False
        self._operators = {
            '+': operator.add,
            '-': operator.sub,
            'x': operator.mul,
            '/': operator.truediv
        }
        self.bind('<KeyPress>', self._key_press)
        self._display = tk.Label(width=self.digits, anchor="e", font=self.large_font, background="black", foreground="white")
        self._display.grid(row=0, column=0, columnspan=4)
        self._buttons = {}
        button('ln', 1, 0)
        button('sqrt', 1, 1)
        button('1', 2, 0)
        button('2', 2, 1)
        button('3', 2, 2)
        button('+', 2, 3)
        button('4', 3, 0)
        button('5', 3, 1)
        button('6', 3, 2)
        button('-', 3, 3)
        button('7', 4, 0)
        button('8', 4, 1)
        button('9', 4, 2)
        button('x', 4, 3)
        button('0', 5, 0)
        button('.', 5, 1)
        button('=', 5, 2)
        button('/', 5, 3)
        for row in range(1, 6):
            self.grid_rowconfigure(row, weight=1, uniform=True)
        for column in range(4):
            self.grid_columnconfigure(column, weight=1, uniform=True)

    def _key_press(self, event):
        """Called when key is pressed."""
        self._button_press(event.char)

    def _button_press(self, key):
        """Process button press."""
        if key in "0123456789":
            self.display(key)
        elif key == ".":
            if not self._point:
                self._point = True
                self.display(key)
        elif key in self._operators:
            if self._operator is None:
                self._operator = key
                self._point = False
                self.display(key)
        elif key == "=":
            self.calculate()
        elif key == "s":
            self.display(math.sqrt(self.calculate()))
        elif key == "l":
            self.display(math.log(self.calculate()))
 
    def calculate(self):
        array = re.split('[+ - x /]', self._display["text"])
        if (len(array) == 1):
            result = Decimal(array[0])
        else:
            result = self._operators[self._operator](Decimal(array[0]), Decimal(array[1]))
        self.display(result)
        self._point = False
        self._operator = None
        return result

    def display(self, value):
        """Update the display."""
        if isinstance(value, str):
            if self._replace:
                self._display["text"] = value
                self._replace = False
            else:
                self._display["text"] = self._display["text"] + value
        else:
            self._display["text"] = str(value)[:self.digits]
            self._replace = True


Calculator().mainloop()