2
\$\begingroup\$

This code adds the following functionality to the basic tkinter.Tk() class...

  • settings-file interaction for persistent application settings

  • fullscreen functionality using F11, can remember last configuration or be defaulted

  • removes the need for multiple root.configure() statements

  • removes the need for root.title() and root.row/columnconfigure()

You adjust the default window by calling it using the Window() class, and then you can use two functions to adjust the settings file (which will be named settings.ini by default).

  • root.set() configures all of the software options found in settings.ini in realtime.
  • root.get() returns a dictionary with the settings you want to reference from settings.ini
  • you can supply multiple arguments to both of these functions

One thing that always bugged me about tkinter was the need for multiple root.configure() statements, so I wrote a function called root.config() that loops through all the keyword arguments you supply and runs the appropriate configuration for them.

  • this function also handles the title() and row/columnconfigure() functions in this format:
root = Window()
root.config(bg='black', title='My Window', row=(0, 1), col=(0, 1))
root.mainloop()

This creates a basic window with a black background, a title ("My Window"), and row 0 and column 0 are set to weight = 1.

The code for this Window() class is below, along with an example setup. You can simply copy/paste this into your editor and it'll run as a standalone.

import tkinter as tk
import os


class Window(tk.Tk):

    def __init__(self, settings_file='./settings.ini'):
        self.window = super()
        self.window.__init__()
        self.window.bind('<F11>', self.toggleFullscreen)
        self.window.protocol('WM_DELETE_WINDOW', self.closing)


        ### the settings file is where your window properties are stored ###
        self.file = settings_file

        ### checking if settings file exists ###
        try:
            open(self.file)


        ### if settings file doesn't exist, it gets created ###
        except Exception as e:

            print('settings file not found.\n'
                  'writing initial settings file.')

            with open(self.file, 'w') as f:
                initial_settings = [

                ### Create your default settings file file here ###
                '; display settings\n\n',
                'fullscreen=False\n',
                'resolution=720x480\n',
                'screenres=%dx%d\n' % (self.winfo_screenwidth(),
                                       self.winfo_screenheight()),
                'resizex=True\n',
                'resizey=True\n\n'
                'fontfamily=TkDefaultFont\n',
                'fontsize=12'

                ]

                for i in initial_settings:
                    f.write(i)

                open(self.file, 'w')

        ###

        ### reading the settings file
        with open(self.file) as f:
            settings = f.readlines()

        self.settings = settings
        self.update()

    ### this refreshes the window size and properties ###
    def update(self):

        with open(self.file) as f:
            settings = f.readlines()

        self.settings = settings
        self.font = (self.get('fontf')['fontfamily'],
                     self.get('fonts')['fontsize'])

        self.window.resizable(self.get('rx')['resizex'],
                              self.get('ry')['resizey'])


        self.window.geometry('%s' % self.get('res')['resolution'])
        self.window.overrideredirect(0)
        if self.get('fs')['fullscreen'] == 'True':
            self.window.geometry('%s' % self.get('screenres')['screenres'])
            self.toggleFullscreen(None)

        self.window.update()

    ### this function changes the window to be fullscreen or normal ###
    def toggleFullscreen(self, event=None):

        if self.get('fullscreen')['fullscreen'] == 'True' and event:
            self.set(fs = False)
            self.window.overrideredirect(0)
            return

        if self.get('fullscreen')['fullscreen'] == 'False' and event:
            if event: self.set(fs = True)
            self.window.overrideredirect(1)
            self.window.geometry('%sx%s+0+0'
                                 % (self.winfo_screenwidth(),
                                    self.winfo_screenheight()))
            return

        if not event:
            if self.get('fullscreen')['fullscreen'] == 'True':
                self.window.overrideredirect(1)
                self.window.geometry('%sx%s+0+0'
                                     % (self.winfo_screenwidth(),
                                        self.winfo_screenheight()))
            else:
                self.window.overrideredirect(0)

        self.window.update()


    ### last-minute cleanup before closing the window ###
    def closing(self):

        #self.set(fs=False)
        self.window.destroy()

    ### set the values in settings file to keyword arguments ###
    def set(self, **kwargs):

        ### shorthands for the set command ###
        if 'fs' in kwargs: kwargs['fullscreen'] = kwargs.pop('fs')
        if 'res' in kwargs: kwargs['resolution'] = kwargs.pop('res')
        if 'rx' in kwargs: kwargs['resizex'] = kwargs.pop('rx')
        if 'ry' in kwargs: kwargs['resizey'] = kwargs.pop('ry')

        for i in kwargs:
            for n, j in enumerate(self.settings):
                if j.lower().startswith(i):

                    j = j.replace(
                            j [j.find('=')+1:],
                            str(kwargs[i]) + '\n'
                        )
                    self.settings[n] = j

        with open(self.file, 'w') as f:
            for i in self.settings:
                f.write(i)

        self.update()


    ### search through the settings and pull the values of each argument ###
    def get(self, *args):

        args = ['fullscreen' if i == 'fs' else i for i in args]
        args = ['resolution' if i == 'res' else i for i in args]
        args = ['resizex' if i == 'rx' else i for i in args]
        args = ['resizey' if i == 'ry' else i for i in args]
        args = ['fontfamily' if i == 'fontf' else i for i in args]
        args = ['fontsize' if i == 'fonts' else i for i in args]

        results = {}

        ### get the rest of the text following the = sign in settings file ###
        for value in args:
            for i, j in enumerate(self.settings):
                if j.startswith(value):
                    results[value] = j[j.find('=')+1:].strip()

        return results



    ### run a configure function for each keyword argument ###
    def config(self, **kwargs):

        ### title the window ###
        if 'title' in kwargs:
            self.title = kwargs['title']
            self.window.title(kwargs.pop('title'))

        ### row configure ###
        if 'row' in kwargs:
            self.window.rowconfigure(kwargs['row'][0],
                                     weight = kwargs['row'][1])
            kwargs.pop('row')

        ### column configure ###
        if 'column' in kwargs or 'col' in kwargs:
            try:
                kwargs['column'] = kwargs.pop('col')
            except: pass

            self.window.columnconfigure(kwargs['column'][0],
                                        weight = kwargs['column'][1])
            kwargs.pop('column')

        self.window.configure(**kwargs)


### the following is an example to show how this class is used ###


root = Window()


### creating a small, non-resizable root window that is windowed by default
root.set(fs=False, res='240x160',
         rx=False, ry=False)

### basic styling and configuration with window title
root.config(bg='black',
            bd=12,
            relief=tk.SUNKEN,
            title='Window')

### setting the default font size for the window
root.set(fontsize=32)


label = tk.Label(root, text='test', fg='white',
                 bg=root['bg'], font=root.font)
label.pack(fill = tk.BOTH, expand=True)

### You can toggle fullscreen by pressing F11

root.mainloop()

You can also add and change shorthands for the set() and get() methods using list comprehension, as shown in the source code.

Any feedback on this would be highly appreciated, as I know there are a few things I'd like to improve!

  • The code for implementing fullscreen is not very DRY, and I know there's probably a way around this.

  • To use the get() function, you have to specify the key you're looking to pull.

# example
print(root.get('fs')['fullscreen'])

I know this can probably be fixed by changing the output from a dictionary to a list, so I'll do that soon.

Either way, let me know what you think, or how I can improve this. I wanted to make this as a way of speeding up the rudimentary GUI creation process, and hopefully it can be used to get the ugly stuff out of the way faster.

Thanks in advance!

\$\endgroup\$
0
\$\begingroup\$

Welcome to CodeReview! Good first post.

Comments

Having comments is great. Your convention is a little odd - there's no need for triple hashes. A single hash at the beginning is more common.

File handling

Your intention was good, but the execution could be improved. Firstly, at this level, don't except Exception. It's too broad. Let exceptions be exceptional. You'll want to instead catch something more specific, in this case FileNotFoundError.

Another strategy for simplifying your code is, instead of opening the settings file twice, only open it once. Consider opening the file in a (append) mode, which will automatically create the file if it doesn't exist. Check the stream position, and if it's non-zero, the file has stuff, in which case you can seek to the beginning and try to read it. If the initial position is zero, then you know you have to write out defaults. Rather than writing out the default file contents in a pre-serialized format, write out the contents as serialized from an in-memory dictionary and don't read them back from the file. One 'gotcha' this avoids is that your existing file operations only explicitly close the file in two out of three cases.

Python 3 niceties

Rather than using the % string formatting operator, consider using interpolated, or "f" strings. That said, this:

'%s' % self.get('res')['resolution']

shouldn't have a format call at all. If the resolution is numeric, simply call str.

Stringly-typed options

'True', as a string, should not exist once the settings file has been deserialized. It should be a grown-up boolean variable. Then, for one thing, this:

if self.get('fullscreen')['fullscreen'] == 'True'

will simply be

if self.get('fullscreen')['fullscreen']

Control structures

Somewhat heavy-handed use of return could be obviated:

if event:
   if self.get('fullscreen')['fullscreen']:
      # ...
   else:
      # ...
else:
   # ...

No returns needed.

Get

Your args filtration could be simplified. Maybe use a dict for those mappings.

arg_map = {
   'fs': 'fullscreen',
   'res': 'resolution',
   'resizex': 'rx',
   'resizey': 'ry',
   'fontfamily': 'fontf',
   'fontsize': 'fonts'
}

args = [arg_map.get(k, k) for k in args]

But why is this mapping occurring at all? Why doesn't get just accept the long-form arguments? Beyond that, why are you doing a prefix match on the configuration keys instead of the entire key?

except: pass

Never ever. Ever. For one thing, this renders Ctrl+C breaking impossible. Also, it defeats the entire purpose of the exception-handling system. If you know of a specific exception you're trying to address, catch that.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your input! I use the triple-hashes as a way of making the comments pop, which is a rather odd personal choice, I'll admit! I had no idea that append mode would check for the file's existence, which is nice (I'll also catch those exceptions as well). I always forget about f strings, thanks for reminding me! With the way I wrote it, I wasn't able to do booleans for the 'True' and 'False,' and I'm looking for a means to fix that as well. Thanks for the tips on control structure as well! The shorthands are for once people are used to the framework. I'll also cut out passing, thank you! \$\endgroup\$ – hunter.logan Aug 20 at 13:59
  • \$\begingroup\$ The usual way to "make the comments pop" is to use a good editor or IDE with highlighting. Comments will be in a different colour. \$\endgroup\$ – Reinderien Aug 20 at 14:02
  • \$\begingroup\$ Sure thing, I'll try messing around with it :) \$\endgroup\$ – hunter.logan Aug 20 at 14:10
  • \$\begingroup\$ By the way, what do you mean by writing the default contents from an in-memory dictionary? I haven't heard of serialized versus deserialized before today. \$\endgroup\$ – hunter.logan Aug 20 at 14:34
  • \$\begingroup\$ # Create your default settings file is followed by a serialized representation of your configuration file. Serialized data is one big long string or byte array representing a more complex data structure. Your configuration, fundamentally, isn't one string: it's a dictionary of key-value pairs. For most of the lifetime of the application, you'll want to hold onto the dictionary in memory and not the serialized representation, which will stay on disk. \$\endgroup\$ – Reinderien Aug 20 at 15:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.