2
\$\begingroup\$

After taking advice from Reinderien, I have completely rewritten the code for my Window() subclass.

EDIT *reason for posting was unclear:

I would like for someone to suggest improvements or other features to add to this subclass. Either that, or give a general review.

EDIT *compatibility bug:

In root.config(), I changed root.title to root.title_string because it replaced the title() function, which is used by other tkinter widget functions (eg. tkinter.simpledialog.askstring())

Features:

  • settings-file interaction for persistent application settings

  • fullscreen functionality using F11, can be persistent or defaulted

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

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

You can adjust the default window by calling it using the Window() class, and then you can use two methods 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 list with the settings you want to reference from settings.ini

  • you can supply multiple arguments to both of these methods

One thing that this subclass fixes is the need for multiple root.configure() statements. It uses a new method, root.config(), that loops through each keyword argument supplied and runs the appropriate configurations.

  • this method 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.

Changes:

  • comment convention has been improved

  • Exceptions are printed if present

  • files are handled in a cleaner fashion

  • default settings.ini contents are now a dictionary

  • root.get() no longer requires pointing to a dictionary key, as it now returns a list

  • string-booleans are fixed as a result of the above change

  • less return functions are used

  • new shortHand() function to prevent ridiculous copy/pasting in root.set()

  • args in root.get() are now handled via a dictionary

  • no except: pass

  • new options: instaquit=False and fullscreenable=True

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
from tkinter import messagebox
import os


class Window(tk.Tk):

    def __init__(self, settings_file = './settings.ini'):

        self.window = super(); self.window.__init__()

        self.window.protocol('WM_DELETE_WINDOW', self.close)

        # the settings file is where your window properties are held
        # you can change the file name and type when you create a Window()
        self.file = settings_file


        # create your default settings file here
        # give section markers a value of None
        default_settings = {

            '; ux settings': None,

            'instaquit': False,

            '; display settings': None,

            'fullscreenable': True,
            'fullscreen': False,
            'resolution': '720x480',
            'screenres': f'{self.winfo_screenwidth()}'
                         f'x{self.winfo_screenheight()}',
            'resizex': False,
            'resizey': False,

            '; font settings': None,

            'fontfamily': 'TkDefaultFont',
            'fontsize': 12
        }


        # checking if settings file exists
        with open(self.file, 'a+') as f:

            def writeDefaults():

                # write/format default settings from dictionary
                 for key, value in default_settings.items():

                    # if a value exists, get it as a string
                    if value != None: value = str(value)

                    # if value is None, section off with new lines
                    else: value = '\n'; key = '\n' + key

                    # case a: no value - surround in new lines
                    # case b: value - add equals sign and write pair
                    formatted_content = (
                                         f'''{key}{

                                         '=' if not
                                         value.isspace()
                                         else ''

                                         }{value}'''

                                         + '\n'
                                        )

                    # add formatted settings to self.settings and file
                    self.settings = [].append(formatted_content)
                    f.write(formatted_content)

            # if the file is empty, write the default contents
            if not bool(f.tell()):
                writeDefaults()

            # read file contents
            f.seek(0)
            self.settings = f.readlines()

        #print('settings loaded:\n', ''.join(self.settings))
        self.update()

    # this function refreshes the window and its properties
    def update(self):
        with open(self.file) as f:
            self.settings = f.readlines()

            self.font = (self.get('fontf'),
                         self.get('fonts'))

            self.window.resizable(self.get('rx'),
                                  self.get('ry'))

            self.window.geometry(self.get('res'))
            self.window.overrideredirect(0)

            # check if the window is fullscreen
            if self.get('fs'):
                self.window.geometry(self.get('screenres'))
                self.toggleFullscreen(None)

            # check if the window is allowed to switch modes
            if self.get('fsable'):
                self.window.bind('<F11>', self.toggleFullscreen)
            else:
                self.window.bind('<F11>', None)



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

        # if the user pressed F11
        if event:
            if self.get('fs'):
                # set fullscreen to False
                self.set(fs = False)
                self.window.overrideredirect(0)

            else:
                # set fullscreen to True
                self.set(fs = True)
                self.window.overrideredirect(1)
                self.window.geometry(self.get('screenres') + '+0+0')

        # if the toggle was hard-coded
        else:
            if self.get('fs'):
                self.window.overrideredirect(1)
                self.window.geometry(self.get('screenres') + '+0+0')

            else:
                self.window.overrideredirect(0)

        self.window.update()


    # last-minute cleanup before closing the window
    def close(self, event=None):

        # instaquit not recommended for most programs, but the option is there
        if not self.get('iquit'):
            if messagebox.askokcancel('Exit Program',
                                      'Are you sure you want to exit?'):
                self.window.destroy()
        self.window.destroy()
        #


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

        # shorthands for the set command
        def shortHand(short_name, long_name):
            if short_name in kwargs:
                kwargs[long_name] = kwargs.pop(short_name)

        shortHand('iquit', 'instaquit'); shortHand('fsable', 'fullscreenable')
        shortHand('fs', 'fullscreen'); shortHand('res', 'resolution')
        shortHand('rx', 'resizex'); shortHand('ry', 'resizey')

        for i in kwargs:
            for n, j in enumerate(self.settings):
                if j.lower()[:j.find('=')] == 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 settings and pull chosen values
    def get(self, *args):

        # shorthands for the get command
        shorthands = {
            'iquit': 'instaquit',
            'fsable': 'fullscreenable',
            'fs': 'fullscreen',
            'res': 'resolution',
            'rx': 'resizex',
            'ry': 'resizey',
            'fontf': 'fontfamily',
            'fonts': 'fontsize'
        }
        args = [shorthands.get(x, x) for x in args]
        results = []


        for value in args:
            for i, j in enumerate(self.settings):

                # check for matching text before equals sign
                if j[:j.find('=')] == value:

                    # append value of match
                    results.append(j[j.find('=')+1:].strip())

        if len(results) == 1:
                return (results[0] if results[0]
                not in ['True', 'False']
                else eval(results[0]))
        return results


    def config(self, **kwargs):

        # title the window
        if 'title' in kwargs:
            self.title_string = 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 Exception as e: print(e)

            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()

root.set(fs=False, res='240x160',
         rx=False, ry=False,
         fsable=True, iquit=True,
         fonts=32)

root.config(bg='black',
            bd=12,
            relief=tk.SUNKEN,
            title='Window')


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


root.mainloop()

So many changes were made today that I didn't pick up on much that I seek to immediately improve, but you may find something I've missed, so please let me know!

As I said in my last post, 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.

\$\endgroup\$
2
\$\begingroup\$

Some improvement, but there's still more :)

Don't semicolon-delimit statements

self.window = super(); self.window.__init__()

There's nearly never a good reason to do this. Just use two lines.

.ini support

https://docs.python.org/3/library/configparser.html is what you should be using instead of manual parsing for an actual .ini file.

Inline functions

Your writeDefaults is a closure: it's a function that can access the variables in the scope of its parent function. There are some scenarios where that's called for, but this isn't one of them. You're better off moving it to a write_defaults (Python doesn't recommend camelCase) on the class. If there is any state it requires to function, such as default_settings, put that either in the function itself or as a member of the class.

None comparison

This:

if value != None: value = str(value)

should be

if value is not None:
    value = str(value)

f-strings

...are great, but there's a limit to the amount of stuff that you should actually do in interpolation blocks. This:

                formatted_content = (
                                     f'''{key}{

                                     '=' if not
                                     value.isspace()
                                     else ''

                                     }{value}'''

                                     + '\n'
                                    )

should really just set a variable first, and invert its logic, i.e.

sep = '' if value.isspace() else '='
formatted_content = f'{key}{sep}{value}\n'

...but that all goes away when you use configparser.

List literals

This:

[].append(formatted_content)

should be

[formatted_content]

Casting to bool

In this context there's no need:

if not bool(f.tell()):

simply write

if not f.tell():

Use split

j.lower()[:j.find('=')] == i:

should be

j.lower().split('=', 1)[0] == i:
\$\endgroup\$
  • \$\begingroup\$ Cool, thanks again for your help! I implemented all of the changes you described, and I only left out configparser since I don't know if the end-user will always use INI files. You've been a huge help, thank you! \$\endgroup\$ – hunter.logan Aug 21 at 11:50

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.