Tkinter refactoring repeated code

Post here if you need help with creating a Graphical User Interface in Python.

Tkinter refactoring repeated code

Postby KHarvey » Tue Apr 16, 2013 3:00 pm

I'm finally getting around to writing a conversion app that I have been planning on writing for about a year now. The original goal was to have something that would covert to different bases (hex, binary, octal, base 10, etc...), but I have since decided to add a couple of other measurements that I tend to Google a lot (network speed, size, subnet, temperature, volume, weight, checksums).

So I have started to write this app using Tkinter, and I have somethings working. But as I start to add more measurements to my app I am finding it rather monotonous, which makes me feel like I am doing it wrong.
Here is what I have so far:
Code: Select all
from Tkinter import *

class Conversion:
   """
      Main window layout
   """
   def __init__(self, root_window):
      self.button_quit = Button(root_window, text="Quit", fg="red", command=root_window.quit)
      self.button_quit.grid(row=0, column=1, sticky=W)

      self.speed_mbits_label = Label(root_window, text="Mbps")
      self.speed_mbits_label.grid(row=1, column=0, sticky=W)
      self.speed_mbits = Entry(root_window, validate="focusout", validatecommand=self.speed_convert)
      self.speed_mbits.grid(row=1, column=1, sticky=W)
      self.speed_mbits.insert(0, 0)
      
      self.speed_mbytes_label = Label(root_window, text="MBps")
      self.speed_mbytes_label.grid(row=1, column=3, sticky=W)
      self.speed_mbytes = Entry(root_window, validate="focusout", validatecommand=self.speed_convert)
      self.speed_mbytes.grid(row=1, column=4, sticky=W)
      self.speed_mbytes.insert(0, 0)
      
      self.button_convert = Button(root_window, text="Speed Convert", fg="blue", command=self.speed_convert)
      self.button_convert.grid(row=3, column=1, sticky=W)
      
      self.button_clear = Button(root_window, text="Speed Clear", fg="black", command=self.speed_clear)
      self.button_clear.grid(row=3, column=2, sticky=W)
      
      self.size_bits_label = Label(root_window, text="bits")
      self.size_bits_label.grid(row=4, column=1, sticky=W)
      self.size_bits = Entry(root_window, validate="focusout", validatecommand=self.size_convert)
      self.size_bits.grid(row=4, column=2, sticky=W)
      self.size_bits.insert(0, 0)
      
      
      
   def speed_convert(self):
      """
         Based on input into Entry fields calculate either MBps or mbps
      """
      if int(self.speed_mbits.get()) > 0:
         self.speed_mbytes.delete(0, END)
         self.speed_mbytes.insert(0, int(self.speed_mbits.get()) / 8)
      elif int(self.speed_mbytes.get()) > 0:
         self.speed_mbits.delete(0, END)
         self.speed_mbits.insert(0, int(self.speed_mbytes.get()) * 8)
      
   def speed_clear(self):
      """
         Reset Entry fields for speed back to 0
      """
      self.speed_mbytes.delete(0, END)
      self.speed_mbytes.insert(0,0)
      self.speed_mbits.delete(0, END)
      self.speed_mbits.insert(0,0)
      
   def size_convert(self):
      pass
      
root_window = Tk()
app = Conversion(root_window)
root_window.mainloop()


While I still have a problem with the focusout working after I have used it once, and I need to make it a little more user friendly. My problem is I am repeating the same code over and over again (Label, Entry, Grid, Insert).
I feel like I should create a dictionary with a tuple to build the root_window rather than copying and pasting my code over and over. But if I were to create a dictionary with tuples, then I would be better off building another class to handle this.
I'm not really quite sure how I would handle this.
My main hang up is the self.variables. I don't know how I would loop the self.speed_mbits_label.
Or how I would handle the setup of the grid. Maybe I would create a newline variable in a tuple that added 1 to the row, and then have +1 to the column for each item created.

I'm not necessarily stuck, as I can continue on, but I think that my code is bloated and I just want to find a better way of doing it.

Any suggestions?
Last edited by Yoriz on Mon Apr 22, 2013 6:14 pm, edited 1 time in total.
Reason: Changed to a more descriptive title
KHarvey
 
Posts: 34
Joined: Tue Mar 19, 2013 5:13 pm
Location: US

Re: Tkinter Monotony

Postby Yoriz » Tue Apr 16, 2013 5:11 pm

When code becomes monotonous and you could just cut and paste and then just alter a couple of bits, that is where you would create a function or method that does the common parts and pass in the variables for the bits that vary, or break it up into various functions/methods.

I don't use Tkinter so sorry i cant be more specific to your gui library.
New Users, Read This
Join the #python-forum IRC channel on irc.freenode.net!
Image
User avatar
Yoriz
 
Posts: 1020
Joined: Fri Feb 08, 2013 1:35 am
Location: UK

Re: Tkinter Monotony

Postby KHarvey » Tue Apr 16, 2013 6:14 pm

Yoriz, thank you for the reply.

I think I can simplify my conundrum, or at least obfuscate some of the Tkinter from it.

The issue that I am having is that I am creating variable placeholders that I need to change later. It is the creation of these placeholders that has become monotonous as they are all virtually the same, except they have different variable names and locations. I am able to code away the locations by putting them into a loop, but I am unable to think of a way to avoid the placeholders.

For example I have the following objects:
Code: Select all
self.size_bytes
self.size_kilobytes
self.size_megabytes
self.size_gigabytes
self.size_terabytes

Each of those objects are place holders for 4 items (label, label_location, input, input_location). So for each one of those objects I generate virtually identical 4 lines of code:
Code: Select all
self.size_bytes_label = Label(root_window, text="bytes")
self.size_bytes_label.grid(row=2, column=5)
self.size_bytes = Entry(root_window)
self.size_bytes.grid(row=2, column=6, sticky=W)

Since I have many more types and versions of these objects my code is getting extremely long with very little value add.

Now each of those objects have methods that I call later on (based on user input):
Code: Select all
self.size_bytes.get() #gets the text
self.size_bytes.delete(0, END) #clears the variable since there is no update
self.size_bytes.insert(0, "Random stuff") #update the text to my new text


After typing all of this out, I think that I am pretty much stuck with how I am doing it, as I can't think of any way to script those placeholders. This might be a limitation of Tkinter.
Although I can clean up some of my other code and do some more math, rather than a bunch of if elif statements.

Also I will probably loop through the locations (row, column) that way it becomes easier to add and subtract things without having to update dozens or hundreds of coordinates.

I'll think about this some more and see if I can think of another way of handling this.
KHarvey
 
Posts: 34
Joined: Tue Mar 19, 2013 5:13 pm
Location: US

Re: Tkinter Monotony

Postby Yoriz » Tue Apr 16, 2013 7:09 pm

Can you create panels in Tkinter, in wxpython i would create a panel class that contains the repeated controls and any methods that are common so it becomes a big control in its own right.
Where ever i need one of these controls i would create a new instance of it.

This is some pseudo code to try and give you an idea of what im saying.

Code: Select all
class CompositeControl(tkinterpanel?):
    def __init__(self, control_name, any variables to set up ctrl here):
        self.label = Label(root_window, text=control_name)
        self.label.grid(row=2, column=5)
        self.entryctrl = Entry(root_window)
        self.entryctrl.grid(row=2, column=6, sticky=W)
       
    def get(self):
        return entryctrl.get() #guessing at tkinter code or you might access the
                               # entryctrl directly
       
        #rest of code need to make this work
       
class Conversion:
    """
       Main window layout
    """
    def __init__(self, root_window):
        self.size_bytes = CompositeControl('bytes')
       
        self.size_kilobytes = CompositeControl('kilobytes')
        self.size_megabytes = CompositeControl('megabytes')
        # etc
        #code to add above panels to main window
       
        self.size_bytes.entryctrl.get() #gets the text
        #or if you created a method get in CompositeControl
        self.size_bytes.get()


Hope this helps.
New Users, Read This
Join the #python-forum IRC channel on irc.freenode.net!
Image
User avatar
Yoriz
 
Posts: 1020
Joined: Fri Feb 08, 2013 1:35 am
Location: UK

Re: Tkinter Monotony

Postby wuf » Wed Apr 17, 2013 8:27 am

Hi KHarvey
KHarvey wrote:While I still have a problem with the focusout working after I have used it once,

When you call a function resp. a method with the 'validatecommand' it is expecting a 'False' or 'True' as return value from the called function resp. method. The return default is a 'False'. So the function resp. method is called once only. To solve this problem you have to do the following alterations:
Code: Select all
   def speed_convert(self):
      """
         Based on input into Entry fields calculate either MBps or mbps
      """
      if int(self.speed_mbits.get()) > 0:
         self.speed_mbytes.delete(0, END)
         self.speed_mbytes.insert(0, int(self.speed_mbits.get()) / 8)
      elif int(self.speed_mbytes.get()) > 0:
         self.speed_mbits.delete(0, END)
         self.speed_mbits.insert(0, int(self.speed_mbytes.get()) * 8)
         
      return True
and:
Code: Select all
   def size_convert(self):
      # place your code here
      return True

The standard width for intendation are four spaces for a tab:
So with three spaces (none standard) it looks like:
Code: Select all
   def size_convert(self):
      # place your code here
      return True
and with four spaces (standard) which i would recommend for your code it looks like:
Code: Select all
    def size_convert(self):
        # place your code here
        return True


A question to you. Did you also make use the of 'pack' instead the 'grid' to layouting your application?

I will also have a closer look at your code snippet. May be a can present you an idea how would do it. Of cource there are plenty of ways to write the snippet.

wuf ;)
wuf
 
Posts: 38
Joined: Fri Feb 08, 2013 6:42 am

Re: Tkinter Monotony

Postby KHarvey » Mon Apr 22, 2013 4:16 pm

I apologize, I got buried at work and I have not had a chance to look at this yet.

Yoriz,
Thanks for the pseudo code. I'm not sure if I will be able to do that or not, but I will take a closer look at it and let you know what I find.

wuf,
Thanks for the tip on the response, that would have taken me forever to figure out.
For the indentation, I am unsure what happened. My text editor is set to 4 spaces, and a quick test of it verified that it is 4 spaces. Sometimes for my pseudo or quick code I just type it into the code brackets natively rather than a copy and paste, and I can't use tab while in the post window. That may have been what happened.
I had originally started off using pack, but I was having a hard time getting the layout that I wanted, so that was when I moved to the grid.

I will start playing with it today.
KHarvey
 
Posts: 34
Joined: Tue Mar 19, 2013 5:13 pm
Location: US

Re: Tkinter Monotony

Postby KHarvey » Mon Apr 22, 2013 4:59 pm

Yoriz wrote:Can you create panels in Tkinter, in wxpython i would create a panel class that contains the repeated controls and any methods that are common so it becomes a big control in its own right.
Where ever i need one of these controls i would create a new instance of it.

This is some pseudo code to try and give you an idea of what im saying.

Code: Select all
class CompositeControl(tkinterpanel?):
    def __init__(self, control_name, any variables to set up ctrl here):
        self.label = Label(root_window, text=control_name)
        self.label.grid(row=2, column=5)
        self.entryctrl = Entry(root_window)
        self.entryctrl.grid(row=2, column=6, sticky=W)
       
    def get(self):
        return entryctrl.get() #guessing at tkinter code or you might access the
                               # entryctrl directly
       
        #rest of code need to make this work
       
class Conversion:
    """
       Main window layout
    """
    def __init__(self, root_window):
        self.size_bytes = CompositeControl('bytes')
       
        self.size_kilobytes = CompositeControl('kilobytes')
        self.size_megabytes = CompositeControl('megabytes')
        # etc
        #code to add above panels to main window
       
        self.size_bytes.entryctrl.get() #gets the text
        #or if you created a method get in CompositeControl
        self.size_bytes.get()


Hope this helps.


Yoriz, you're brilliant. It took me a couple of minutes to figure it out, but this works perfectly. I never thought of building a separate control to load the different options into.

So here is my test code, which works perfectly:
Code: Select all
from Tkinter import *

class CompositeControl():
    def __init__(self, control_name, grid_row, grid_column):
        self.label = Label(root_window, text=control_name)
        self.label.grid(row=grid_row, column=grid_column)
        self.entryctrl = Entry(root_window)
        self.entryctrl.grid(row=grid_row, column=(grid_column + 1), sticky=W)

class Conversion:
   """
      Main window layout
   """
   def __init__(self, root_window):
      self.button_quit = Button(root_window, text="Quit", fg="red", command=root_window.quit)
      self.button_quit.grid(row=0, column=1, sticky=W)
       
      self.size_kilobytes = CompositeControl('kilobytes', 1, 0)
      self.size_megabytes = CompositeControl('megabytes', 1, 2)
      
      self.button_size_convert = Button(root_window, text="Size Convert", fg="blue", command=self.size_convert)
      self.button_size_convert.grid(row=5, column=1, sticky=W)

   def size_convert(self):
      print self.size_kilobytes.entryctrl.get()
      print self.size_megabytes.entryctrl.get()
       
root_window = Tk()
app = Conversion(root_window)
root_window.mainloop()


I still don't like how I am handling the layout by statically assigning the grid positions, so I will build a function that calculates those for me. Besides the conversion math, I still need to figure out on keyboard change. But with this, I should be able to code most it.

Once it is completed I will post it for review and critiques.

Once again Yoriz, thank you.
KHarvey
 
Posts: 34
Joined: Tue Mar 19, 2013 5:13 pm
Location: US

Re: Tkinter refactoring repeated code

Postby KHarvey » Mon Apr 22, 2013 7:32 pm

Alright, I knew with my last post I would be eating my words. Well, I can code around it, but I would rather do it right.

One thing that I would like to add is the command control to my entryctrl's.
Here is what I was doing before:
Code: Select all
self.size_megabytes = Entry(root_window, validate="focusout", validatecommand=self.speed_convert)
self.size_megabytes.grid(row=1, column=3, sticky=W)


Here is what I am doing now:
Code: Select all
class CompositeControlEntryEntry():
   def __init__(self, control_name, grid_row, grid_column):
      self.label = Label(root_window, text=control_name)
      self.label.grid(row=grid_row, column=grid_column)
      self.entryctrl = Entry(root_window)
      self.entryctrl.grid(row=grid_row, column=(grid_column + 1), sticky=W)
      self.entryctrl.insert(0, 0)
class Conversion:
   """
      Main window layout
   """
   def __init__(self, root_window):
      self.button_quit = Button(
            root_window, text="Quit", fg="red", command=root_window.quit
      )
      self.button_quit.grid(row=0, column=1, sticky=W)
      
      self.speed_start_row = 1
      self.speed_start_column = 0
      self.size_start_row = 0
      self.size_start_column = 5
      
      self.speed_mbits = CompositeControlEntry(
            "Mbps", self.speed_start_row, self.speed_start_column
      )
      self.speed_mbits = CompositeControlEntry(
            "MBps", self.speed_start_row, self.speed_start_column + 2
      )
      self.size_bits = CompositeControlEntry(
            "Bits", self.size_start_row, self.size_start_column
      )
      self.size_bytes = CompositeControlEntry(
            "Bytes", self.size_start_row + 1, self.size_start_column
      )
      self.size_kilobytes = CompositeControlEntry(
            "Kilobytes", self.size_start_row + 2, self.size_start_column
      )
      self.size_megabytes = CompositeControlEntry(
            "Megabytes", self.size_start_row + 3, self.size_start_column
      )


So I would like to find someway to add the validate and command to the CompositeControlEntry class.
My first attempt looked like this:
Code: Select all
class CompositeControlButton():
   def __init__(self, control_name, grid_row, grid_column, color, command_name):
      self.button_size_convert = Button(
            root_window, text=control_name, fg=color, command=command_name
      )
      self.button_size_convert.grid(
            row=grid_row, column=grid_column, sticky=W
      )
class Conversion:
   """
      Main window layout
   """
   def __init__(self, root_window):
      self.button_quit = Button(
            root_window, text="Quit", fg="red", command=root_window.quit
      )
      self.button_quit.grid(row=0, column=1, sticky=W)
      
      self.speed_start_row = 1
      self.speed_start_column = 0
      self.size_start_row = 0
      self.size_start_column = 5

      self.button_size_convert = CompositeControlButton(
            "Size Convert", self.size_start_row + 7, column=self.size_start_column, "blue", size_convert
      )
    def size_convert(self):
      if int(self.size_bits.entryctrl.get()) > 0:
         self.size_byte = round(float(self.size_bits.entryctrl.get()) / 8, 2)
      if int(self.size_bytes.entryctrl.get()) > 0:
         self.size_byte = int(self.size_bytes.entryctrl.get())
      if int(self.size_kilobytes.entryctrl.get()) > 0:
         self.size_byte = round(
               float(self.size_kilobytes.entryctrl.get()) * (1024 ** 1), 2
         )
      if int(self.size_megabytes.entryctrl.get()) > 0:
         self.size_byte = round(
               float(self.size_megabytes.entryctrl.get()) * (1024 ** 2), 2
         )
      if int(self.size_gigabytes.entryctrl.get()) > 0:
         self.size_byte = round(
               float(self.size_gigabytes.entryctrl.get()) * (1024 ** 3), 2
         )
      if int(self.size_terabytes.entryctrl.get()) > 0:
         self.size_byte = round(
               float(self.size_terabytes.entryctrl.get()) * (1024 ** 4), 2
         )
      if int(self.size_petabytes.entryctrl.get()) > 0:
         self.size_byte = round(
               float(self.size_petabytes.entryctrl.get()) * (1024 ** 5), 2
         )


But I error out with:
"Size Convert", self.size_start_row + 7, column=self.size_start_column, "blue", size_convert
SyntaxError: non-keyword arg after keyword arg


Which makes sense, as I can't pass a function to another function. Sorry, the CompositeControlButton was the next class that I created and it also uses the command syntax so that is the one that I was testing with.

I might be able to build my own validate class to monitor the different fields for changes / focus changes. Is there a better way, or should I just build my own class to monitor for the changes?
KHarvey
 
Posts: 34
Joined: Tue Mar 19, 2013 5:13 pm
Location: US

Re: Tkinter refactoring repeated code

Postby Yoriz » Mon Apr 22, 2013 8:48 pm

The error your getting is because you have a keyword 'column' in the following code which is proceeded by non keyword arguments.
Code: Select all
self.button_size_convert = CompositeControlButton(
            "Size Convert", self.size_start_row + 7, column=self.size_start_column, "blue", size_convert
      )

if you take that keyword out that should solve that error
Code: Select all
self.button_size_convert = CompositeControlButton(
            "Size Convert", self.size_start_row + 7, self.size_start_column, "blue", size_convert
      )


Also note yes you can pass a function to another function.
New Users, Read This
Join the #python-forum IRC channel on irc.freenode.net!
Image
User avatar
Yoriz
 
Posts: 1020
Joined: Fri Feb 08, 2013 1:35 am
Location: UK

Re: Tkinter refactoring repeated code

Postby KHarvey » Tue Apr 23, 2013 1:16 pm

Dang!

Sorry about that, I completely missed the column= in there. Since I hadn't tried to pass a function to a function I assumed that was my error rather that troubleshooting my code farther. :oops:

But it works now.

Once again thanks for your help.
KHarvey
 
Posts: 34
Joined: Tue Mar 19, 2013 5:13 pm
Location: US


Return to GUI

Who is online

Users browsing this forum: No registered users and 4 guests