I think this will be my final version for now.
I have tried to optimize the code a little.
Everything seems to work. The spells section doesn't use a dict anymore. Just a string.
Anyhow here is the code
import tkinter as tk
from tkinter import ttk, messagebox
import json
from os import sys, path
from itertools import chain
from collections import OrderedDict
from random import randint
class Model:
'''
Model Handles all of the crud methods
'''
def __init__(self, db_file):
'''
Intialize the instance and set the db_file
'''
self.db_file = db_file
def create_database(self):
'''
Check if database exists, if not, create it
'''
if path.exists(self.db_file):
print('database already exist. Continuing')
pass
else:
with open(self.db_file, 'w') as out_file:
database = {}
json.dump(database, out_file, indent=4)
print('database created')
def create_guild(self, arg):
with open(self.db_file, 'r+') as json_file:
data = json.load(json_file)
if arg in data:
print(f'{arg} already exist')
else:
data[arg] = []
json_file.seek(0)
json.dump(data, json_file, indent=4)
print(f'{arg} has been created')
def delete_guild(self, arg):
with open(self.db_file, 'r') as json_file:
data = json.load(json_file)
if arg in data:
data.pop(arg)
with open (self.db_file, 'w') as json_file:
json.dump(data, json_file, indent=4)
def get_guilds(self):
with open(self.db_file, 'r') as json_file:
data = json.load(json_file)
guilds = [guild for guild in data]
return guilds
def create_character(self, arg):
'''
Create the character
'''
with open(self.db_file, 'r+') as json_file:
data = json.load(json_file)
data[arg['guild'].lower()].append(arg)
json_file.seek(0)
json.dump(data, json_file, indent=4)
def delete_character(self, *args):
with open(self.db_file, 'r+') as json_file:
data = OrderedDict(json.load(json_file))
for i, character in enumerate(data[args[1]]):
if args[0].lower() == data[args[1]][i]['name']:
data[args[1]].pop(i)
with open(self.db_file, 'w') as json_file:
json.dump(data, json_file, indent=4)
def edit_character(self, arg):
print('edit character')
def check_character(self, arg):
with open(self.db_file, 'r') as json_file:
data = json.load(json_file)
if arg['name'].lower() in [c.get('name') for c in chain(*data.values())]:
return True
else:
return False
def get_character(self, arg):
with open(self.db_file, 'r') as json_file:
data = json.load(json_file)
for character in chain(*data.values()):
if arg.lower() == character['name']:
return character
def get_guild_characters(self, arg):
with open(self.db_file) as json_file:
data = json.load(json_file)
names = []
if arg in [character.get('guild') for character in chain(*data.values())]:
for character in data[arg]:
if character:
names.append(character['name'])
return names
class MainWindow:
def __init__(self, parent):
# Set and configure the parent container
self.parent = parent
self.parent.columnconfigure(0, weight=1)
self.parent.rowconfigure(0, weight=1)
# Create all containers to hold various widgets
container = tk.Frame(self.parent)
container.grid(column=0, row=0, sticky='news', padx=8, pady=4)
container.grid_columnconfigure(0, weight=3)
header_container = tk.Frame(container)
header_container['highlightbackground'] = 'gray'
header_container['highlightcolor'] = 'gray'
header_container['highlightthickness'] = 1
header_container['borderwidth'] = 1
header_container.grid(column=0, row=0, sticky='new')
header_container.grid_columnconfigure(0, weight=3)
top_container = tk.Frame(container)
top_container.grid(column=0, row=1, sticky='new', pady=(4, 2))
top_container.grid_columnconfigure(0, weight=3)
top_container.grid_columnconfigure(1, weight=3)
mid_container = tk.Frame(container)
mid_container.grid(column=0, row=2, sticky='new', pady=(2, 4))
mid_container.grid_columnconfigure(0, weight=3, uniform='mid')
mid_container.grid_columnconfigure(1, weight=3, uniform='mid')
# Create a frame for the listbox
left_frame = tk.Frame(mid_container)
left_frame['highlightcolor'] = 'gray'
left_frame['highlightbackground'] = 'gray'
left_frame['highlightthickness'] = 1
left_frame['borderwidth'] = 1
left_frame.grid(column=0, row=0, sticky='news', padx=(0, 2))
left_frame.grid_columnconfigure(0, weight=3)
left_frame.grid_rowconfigure(0, weight=3)
# Create a frame as a placeholder for a label
self.right_frame = tk.Frame(mid_container)
self.right_frame['highlightcolor'] = 'gray'
self.right_frame['highlightbackground'] = 'gray'
self.right_frame['highlightthickness'] = 1
self.right_frame['borderwidth'] = 1
self.right_frame.grid(column=1, row=0, sticky='news', padx=(2, 0))
self.right_frame.grid_columnconfigure(0, weight=3)
btm_container = tk.Frame(container)
btm_container['highlightbackground'] = 'gray'
btm_container['highlightcolor'] = 'gray'
btm_container['highlightthickness'] = 1
btm_container['borderwidth'] = 1
btm_container.grid(column=0, row=3, sticky='new', pady=8)
# Create the header
self.text = 'Character Collection'
self.header = tk.Canvas(header_container, height=80, bg='ivory2')
self.shadow_mytext = self.header.create_text(0, 0, text=self.text, fill='gray', angle=4.5, font=('"" 40 bold'), tags=['event'])
self.mytext = self.header.create_text(0, 0, text=self.text, fill='coral', angle=4.5, font=('"" 40 bold'), tags=['event'])
self.header.grid(column=0, row=0, sticky='new')
self.header.bind('<Configure>', self.move_text)
# Create the combobox
_guild = tk.StringVar()
self.combobox = ttk.Combobox(top_container)
self.combobox['textvariable'] = _guild
self.combobox['cursor'] = 'hand2'
self.combobox['state'] = 'readonly'
self.combobox.grid(column=0, row=0, sticky='news', padx=(0, 2))
info = tk.Label(top_container)
info['text'] = 'Double click a name to view character data.'
info['relief'] = 'groove'
info['bg'] = 'lightyellow'
info.grid(column=1, row=0, sticky='new', padx=(2, 0))
# Create a listbox
self.listbox = tk.Listbox(left_frame)
self.listbox['selectmode'] = 'single'
self.listbox.grid(column=0, row=0, sticky='news')
scrollbar = tk.Scrollbar(left_frame)
scrollbar.grid(column=1, row=0, sticky='news')
self.listbox.configure(yscrollcommand = scrollbar.set)
scrollbar.config(command = self.listbox.yview)
# create a list of buttons
self.buttons = [
'Create Character', 'Edit Character', 'Delete Character',
'Create Guild', 'Delete Guild', 'Exit'
]
# List to display different colors for buttons
self.button_colors = ['skyblue', 'orange', 'tomato', 'skyblue', 'tomato', 'red']
# Make sure all buttons have the same width
for i in range(len(self.buttons)):
btm_container.grid_columnconfigure(i, weight=3, uniform='buttons')
# Create the buttons
for i in range(len(self.buttons)):
self.buttons[i] = tk.Button(btm_container, text=self.buttons[i])
self.buttons[i]['bg'] = self.button_colors[i]
self.buttons[i]['cursor'] = 'hand2'
self.buttons[i].grid(column=i, row=0, sticky='new', padx=2, pady=8)
# Method for centering text on the canvas header
def move_text(self, event):
if event.width:
xpos = (event.width/2)-4
self.header.coords(self.mytext, xpos, 40)
self.header.coords(self.shadow_mytext, (xpos+1.5), 41.5)
class AddEditWindow:
def __init__(self, target=None, title=None):
# Set instance variables for the form
self.window = tk.Toplevel(None)
self.window.columnconfigure(0, weight=1)
self.window.rowconfigure(0, weight=1)
self.window['padx'] = 8
self.window['pady'] = 8
self.window.title(title)
self.namevar = tk.StringVar()
self.agevar = tk.StringVar()
self.villagevar = tk.StringVar()
self.weaponvar = tk.StringVar()
self.spellvar = tk.StringVar()
if target == 'edit':
self.form()
if target == 'create':
self.form()
def form(self):
container = tk.Frame(self.window)
container['highlightcolor'] = 'gray'
container['highlightbackground'] = 'gray'
container['highlightthickness'] = 1
container['borderwidth'] = 1
container.grid(column=0, row=0, sticky='new', ipadx=8, ipady=5)
container.grid_columnconfigure(0, weight=1)
container.grid_columnconfigure(1, weight=3)
label = tk.Label(container, text='Name:', anchor='w')
label.grid(column=0, row=1, sticky='new', padx=(0, 1), pady=4)
self.name_entry = tk.Entry(container, textvariable=self.namevar)
self.name_entry.grid(column=1, row=1, sticky='new', padx=(1, 4), pady=4)
label = tk.Label(container, text='Age:', anchor='w')
label.grid(column=0, row=2, sticky='new', padx=(0, 1), pady=4)
self.age_field = tk.Entry(container, textvariable=self.agevar)
self.age_field.grid(column=1, row=2, sticky='new', padx=(1, 4), pady=4)
label = tk.Label(container, text='Guild:', anchor='w')
label.grid(column=0, row=3, sticky='new', padx=(0, 1), pady=4)
self.guild_field = ttk.Combobox(container)
self.guild_field['state'] = 'readonly'
self.guild_field['cursor'] = 'hand2'
self.guild_field.grid(column=1, row=3, sticky='new', padx=(1, 4), pady=4)
label = tk.Label(container, text='Home Village:', anchor='w')
label.grid(column=0, row=4, sticky='new', padx=(0, 1), pady=4)
self.village = tk.Entry(container, textvariable = self.villagevar)
self.village.grid(column=1, row=4, sticky='new', padx=(1, 4), pady=4)
label = tk.Label(container, text='Weapon:', anchor='w')
label.grid(column=0, row=5, sticky='new', padx=(0, 1), pady=4)
self.weapon_field = tk.Entry(container, textvariable=self.weaponvar)
self.weapon_field.grid(column=1, row=5, sticky='new', padx=(1, 4), pady=4)
label = tk.Label(container, text='Spells:', anchor='w')
label.grid(column=0, row=6, sticky='new', padx=(0, 1), pady=4)
self.spell_field = tk.Entry(container, textvariable=self.spellvar)
self.spell_field.grid(column=1, row=6, sticky='new', padx=(1, 4), pady=4)
btnframe = tk.Frame(container)
btnframe.grid(column=0, columnspan=2, row=7, sticky='new', pady=4)
btnframe.grid_columnconfigure(0, weight=3, uniform='button')
btnframe.grid_columnconfigure(1, weight=3, uniform='button')
self.save_btn = tk.Button(btnframe, text='Save')
self.save_btn['bg'] = 'skyblue'
self.save_btn['cursor'] = 'hand2'
self.save_btn.grid(column=0, row=0, sticky='new', padx=(2, 2), pady=4)
self.cancel_btn = tk.Button(btnframe, text='Cancel')
self.cancel_btn['bg'] = 'orangered'
self.cancel_btn['cursor'] = 'hand2'
self.cancel_btn.grid(column=1, row=0, sticky='new', padx=(2, 2), pady=4)
class AddDeleteGuild:
def __init__(self):
self.guildvar = tk.StringVar()
def form(self):
self.title = None
self.window = tk.Toplevel(None)
self.window.title(self.title)
self.window['padx'] = 8
self.window['pady'] = 4
self.container = tk.Frame(self.window)
self.container['borderwidth'] = 1
self.container['highlightcolor'] = 'gray'
self.container['highlightbackground'] = 'gray'
self.container['highlightthickness'] = 1
self.container.grid(column=0, row=0, sticky='new', padx=4, pady=2)
self.container.grid_columnconfigure(0, weight=1)
self.container.grid_columnconfigure(1, weight=3)
btn_container = tk.Frame(self.container)
btn_container.grid(column=0, columnspan=2, row=1, sticky='new', padx=2, pady=8)
btn_container.grid_columnconfigure(0, weight=3, uniform='button')
btn_container.grid_columnconfigure(1, weight=3, uniform='button')
label = tk.Label(self.container, text='Guild Name:')
label.grid(column=0, row=0, sticky='new', padx=(2, 1), pady=4)
self.button = tk.Button(btn_container, text='Save')
self.button['cursor'] = 'hand2'
self.button.grid(column=0, row=0, sticky='new', padx=(2, 1))
cancel_btn = tk.Button(btn_container, text='Cancel')
cancel_btn['bg'] = 'orangered'
cancel_btn['cursor'] = 'hand2'
cancel_btn['command'] = self.window.destroy
cancel_btn.grid(column=1, row=0, sticky='new', padx=(1, 2))
class Controller:
def __init__(self, model, mainwindow,):
'''
Set instance vairables
'''
self.model = model
self.mainwindow = mainwindow
self.guildwindow = AddDeleteGuild()
# Set button commands for mainview buttons
self.mainwindow.buttons[0]['command'] = self.create_character_form
self.mainwindow.buttons[1]['command'] = self.edit_character_form
self.mainwindow.buttons[2]['command'] = self.delete_character
self.mainwindow.buttons[3]['command'] = self.create_guild_form
self.mainwindow.buttons[4]['command'] = self.delete_guild_form
self.mainwindow.buttons[5]['command'] = sys.exit
# Make a frame for character data and populate everything.
self.make_frame()
self.populate()
def create_character_form(self):
'''
Creates the form and sets some variables for the toplevel window
for creating characters
'''
guilds = [guild.title() for guild in self.model.get_guilds()]
self.createwindow = AddEditWindow(target='create', title='Create New Character')
self.createwindow.form()
self.createwindow.guild_field['values'] = guilds
self.createwindow.guild_field.current(0)
self.createwindow.save_btn['command'] = self.save_character
self.createwindow.cancel_btn['command'] = lambda: self.cancel(self.createwindow.window)
self.mainwindow.parent.iconify()
def edit_character_form(self):
'''
Get list of guilds and character data for editing. All but name can be edited.
Data is displayed in the edit form
'''
guilds = [guild.title() for guild in self.model.get_guilds()]
check = True
try:
self.character = self.model.get_character(self.mainwindow.listbox.get(self.mainwindow.listbox.curselection()[0]))
except:
check = False
messagebox.showerror('No Character', 'You have to select a character before, you can edit')
if check:
self.editwindow = AddEditWindow(target='edit', title='Edit Character')
self.editwindow.form()
self.editwindow.name_entry['state'] = 'disabled'
self.editwindow.namevar.set(self.character['name'])
self.editwindow.agevar.set(self.character['age'])
self.editwindow.guild_field['values'] = guilds
guild = self.mainwindow.combobox.current()
self.editwindow.guild_field.current(guild)
self.editwindow.villagevar.set(self.character['home village'])
self.editwindow.weaponvar.set(self.character['weapon'])
self.editwindow.spellvar.set(self.character['spells'])
self.editwindow.save_btn['command'] = self.edit_character
self.editwindow.cancel_btn['command'] = lambda: self.cancel(self.editwindow.window)
self.mainwindow.parent.iconify()
def cancel(self, window):
'''
destroy the window. Create/Edit has been camceled.
'''
window.destroy()
self.mainwindow.parent.deiconify()
def edit_character(self):
'''
Deletes old character and creates new character data
'''
character = {}
character['name'] = self.editwindow.namevar.get()
character['age'] = self.editwindow.agevar.get()
character['guild'] = self.editwindow.guild_field.get().lower()
character['home village'] = self.editwindow.villagevar.get()
character['weapon'] = self.editwindow.weaponvar.get()
character['strength'] = randint(10, 25)
character['agility'] = randint(10, 25)
character['stamina'] = randint(30, 100)
character['spells'] = self.editwindow.spellvar.get()
self.model.delete_character(character['name'], self.character['guild'].lower())
self.model.create_character(character)
self.editwindow.window.destroy()
self.mainwindow.parent.deiconify()
for i, guild in enumerate(self.model.get_guilds()):
if guild.lower() == character['guild']:
index = i
self.populate(index)
list_index = 0
for i, name in enumerate(self.mainwindow.listbox.get(0, tk.END)):
if character['name'].lower() == name.lower():
list_index = i
self.mainwindow.listbox.select_clear(0, tk.END)
self.mainwindow.listbox.select_set(list_index)
self.display_character()
def delete_character(self):
'''
Gets character name and guild for deleting
'''
try:
character = self.mainwindow.listbox.get(self.mainwindow.listbox.curselection()[0]).lower()
guild = self.mainwindow.combobox.get().lower()
verify = messagebox.askyesno('Warning!', f'You are about to delete {character.title()}. Do you wish to continue?')
if verify:
self.model.delete_character(character, guild)
self.populate()
else:
messagebox.showinfo('Canceled', f'Deletion of {character.title()} canceled.')
except:
pass
def save_character(self):
'''
saves new character data
'''
character = {}
character['name'] = self.createwindow.name_entry.get().lower()
character['guild'] = self.createwindow.guild_field.get().lower()
character['age'] = self.createwindow.age_field.get().lower()
character['home village'] = self.createwindow.village.get().lower()
character['weapon'] = self.createwindow.weapon_field.get().lower()
character['strength'] = randint(10, 25)
character['agility'] = randint(10, 25)
character['stamina'] = randint(30, 50)
character['spells'] = self.createwindow.spell_field.get().lower()
fields = []
error = 0
for key, value in character.items():
if not character['weapon']:
character['weapon'] = 'None'
if not character['spells']:
character['spells'] = 'None'
if not value:
error += 1
fields.append(key)
if error > 0:
check = messagebox.showerror('Error!', f'These fields can not be empty.\n{", ".join(fields)}')
else:
verify = True
if_name_exist = self.model.check_character(character)
if if_name_exist:
messagebox.showerror('Name Error!', f'{character["name"].title()} already exists. Please choose another name.')
self.createwindow.deiconify()
verify = False
if not character['age'].isnumeric():
messagebox.showerror('Age Error!', 'Age must be a whole number.')
verify = False
if verify:
messagebox.showinfo('Character Created', f'The character {character["name"].title()} has been created.')
self.createwindow.window.destroy()
self.mainwindow.parent.deiconify()
self.model.create_character(character)
for i, guild in enumerate(self.model.get_guilds()):
if guild.lower() == character['guild']:
index = i
self.populate(index)
list_index = 0
for i, name in enumerate(self.mainwindow.listbox.get(0, tk.END)):
if character['name'].lower() == name.lower():
list_index = i
self.mainwindow.listbox.select_clear(0, tk.END)
self.mainwindow.listbox.select_set(list_index)
self.display_character()
def create_guild_form(self):
'''
Form for creating new guilds
'''
self.guildwindow = AddDeleteGuild()
self.guildwindow.title='Delete Guild'
self.guildwindow.form()
self.guildwindow.entry = tk.Entry(self.guildwindow.container)
self.guildwindow.entry.grid(column=1, row=0, sticky='new', padx=(1, 0), pady=4)
self.guildwindow.button['text'] = 'Save'
self.guildwindow.button['bg'] = 'skyblue'
self.guildwindow.button['command'] = self.create_guild
def create_guild(self):
'''
Creates new guild for form data
'''
if self.guildwindow.entry.get():
guild = self.guildwindow.entry.get().lower().strip()
self.model.create_guild(guild)
self.guildwindow.window.destroy()
self.populate()
else:
messagebox.showerror('Error!', 'You must enter a guild name to create it.')
def delete_guild_form(self):
'''
A form for chosen guild
'''
self.guildwindow.title = 'Delete Guild'
self.guildwindow.form()
self.guildwindow.menu = ttk.Combobox(self.guildwindow.container)
self.guildwindow.menu.grid(column=1, row=0, sticky='new', padx=(1, 2), pady=4)
self.guildwindow.menu['values'] = self.model.get_guilds()
self.guildwindow.menu.current(0)
self.guildwindow.button['text'] = 'Delete'
self.guildwindow.button['bg'] = 'skyblue'
self.guildwindow.button['command'] = self.delete_guild
def delete_guild(self):
'''
Deletes chosen guild and all of it's characters
'''
guild = self.guildwindow.menu.get()
ok = messagebox.askyesno('Warning!', f'You are abount to delete the guild {guild} and all of it\'s characters.\n Do you wish to continue? ')
if ok:
self.model.delete_guild(guild)
self.populate()
self.guildwindow.window.destroy()
else:
messagebox.showinfo('Deletion Aborted', f'The deletion of the {guild} guild was canceled.')
def get_guild_characters(self):
'''
Method for populating the listbox in mainwindow and setting first item
self.frame.destroy is used to remove any data in the character display.
Remake the data frame with self.make_frame
Re-populate the listbox and then display character data with self.display_character
'''
self.frame.destroy()
self.make_frame()
guild = self.mainwindow.combobox.get().lower()
characters = self.model.get_guild_characters(guild)
self.mainwindow.listbox.delete(0, tk.END)
for i, name in enumerate(characters):
self.mainwindow.listbox.insert(i, name.title())
self.mainwindow.listbox.select_set(0)
self.mainwindow.listbox.bind('<Double-Button-1>', lambda x: self.display_character())
self.display_character()
def make_frame(self):
'''
Method to build a container frame for the character.
Can be destroyed to remove the data and rebuilt when needed
'''
self.frame = tk.Frame(self.mainwindow.right_frame)
self.frame.grid(column=0, row=0, sticky='new')
self.frame.grid_columnconfigure(0, weight=0)
self.frame.grid_columnconfigure(1, weight=3)
def display_character(self):
'''
Method for displaying the selected character data
'''
error = False
try:
character = self.mainwindow.listbox.get(self.mainwindow.listbox.curselection()[0])
except IndexError:
error = True
if error:
pass
else:
data = self.model.get_character(character.lower())
i = 0
for key, value in data.items():
key_label = tk.Label(self.frame, text=key.title(), anchor='w', padx=4)
key_label['relief'] = 'groove'
key_label.grid(column=0, row=i, sticky='new', ipadx=8)
value = value.title() if isinstance(value, str) else value
value_label = tk.Label(self.frame, text=value, anchor='w', padx=4)
value_label['relief'] = 'groove'
value_label.grid(column=1, row=i, sticky='new')
i += 1
def populate(self, index=0):
'''
Get all guild and populate the combobox. Set the first entry
Call self.get_guild_characters to populate the listbox
'''
guilds = self.model.get_guilds()
guilds = [guild.title() for guild in guilds]
self.mainwindow.combobox['values'] = guilds
self.mainwindow.combobox.current(index)
self.mainwindow.combobox.bind('<<ComboboxSelected>>', lambda x: self.get_guild_characters())
self.get_guild_characters()
self.display_character()
if __name__ == '__main__':
app = tk.Tk()
controller = Controller(Model(db_file='card2.json'), MainWindow(app))
app.mainloop()