# Adding more functionality to tkinter for projects (Revised)

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

f.seek(0)

self.update()

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

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'):
'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.

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:

• 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! – hunter.logan Aug 21 '19 at 11:50