Danke für die Antworten und entschuldigt den fehlenden Code, war spät gestern

Erläuterung: das ist das Modul, das das ganze GUI Zeugs enthält und das Steuerungsmodul, welches das Programm startet und auch das GUI Zeug erstmalig aufruft. Mein Demeter Problem (danke, Deets, SO hieß das…) befindet sich an folgenden Stellen:
Button-Klasse UpdateJob. Hier erfolgt der böse Zugriff
Klasse TextTableMediator – wie der Name schon sagt, die Vermmittlerklasse. Sie enthält eine Referenz auf:
Klasse Textitem – die Klasse mit der Methode, die ich in UpdateJob aufrufen will.
Ich weiß inzwischen schon gar nicht mehr, wie oft ich dieses Modul schon umgebaut habe, weil das Design nachweislich oder meiner unqualifizierten Meinung nach unschön war… Vielleicht finde ich ja irgendwann mal eine Version, die ich stehen lassen kann. Das ganze Programm würde ich dann mal separat zur Begutachtung posten, wenn es einigermaßen fertig ist.
GUI-Modul:
Code: Alles auswählen
import tkinter as tk
import sys
import datetime
import AV3_Dialogs as dial
import AV3_DB
class MainMenu():
def __init__(self, root, controller, db_path):
self.controller = controller
self.root = root
self.db = AV3_DB.Database(db_path)
self.joblist = self.db.jobs_select_all()
self.current_date = datetime.date.today()
self.date_displayed = tk.StringVar()
self.container = None
self.date_label = None
self.timetable = None
self.set_date(self.current_date)
self.set_frame()
def set_frame(self):
''' Set a frame (with the current date displayed on top) for all buttons to be packed in.'''
self.container = tk.Frame(
self.root,
bd=2,
relief=tk.GROOVE)
self.container.grid(
row = 0,
column = 0,
padx = int(self.root.screen_width*0.03),
pady = int(self.root.screen_height*0.04),
sticky = tk.NW)
self.date_label = tk.Label(
self.container,
textvariable=self.date_displayed,
font=('Segoe UI', 14, 'bold'))
self.date_label.pack()
# Initiate the timetable grid for today
self.timetable = TextTable_Mediator(self.root, self.current_date, self.joblist)
self.site_buttons()
def set_date(self, date):
''' Take any date(time) object and convert it to a certain format, which will be displayed at the head of the main menu. '''
formatted_date = datetime.datetime.strftime(date, '%A, %d. %B %Y')
self.date_displayed.set(formatted_date)
def site_buttons(self):
''' Create object of the button classes. Place these buttons inside the menu frame. '''
# Exit button is some kind of exception: the callback method is stored inside the controller attribute. It's not really worth creating a separate class.
exit_program = tk.Button(
self.container,
text="Beenden",
command=self.controller.close_app)
exit_program.pack(
padx=int(self.root.screen_width*0.02),
pady=int(self.root.screen_height*0.03))
# PDF Creator
print_pdf = PrintPdfButton(self.container)
print_pdf.pack(
padx=int(self.root.screen_width*0.02),
pady=int(self.root.screen_height*0.03))
# Change the current processing date (this will load previously saved jobs to the timetable grid).
load_date = LoadDate(self.container, self.root, self)
load_date.pack(
padx=int(self.root.screen_width*0.02),
pady=int(self.root.screen_height*0.03))
# Add a job to d&d menu
add_job = AddJob(self.container, self.root, self.db, self.timetable)
add_job.pack(
padx=int(self.root.screen_width*0.02),
pady=int(self.root.screen_height*0.03))
# Save currently deployed jobs
save_jobs = SaveJobs(self.container, self.root, self.db, self.timetable)
save_jobs.pack(
padx=int(self.root.screen_width*0.02),
pady=int(self.root.screen_height*0.03))
# Remove a job
delete_job = RemoveJob(self.container, self.root, self.db, self.timetable)
delete_job.pack(
padx=int(self.root.screen_width*0.02),
pady=int(self.root.screen_height*0.03))
# Modify a job's name
update_job = UpdateJob(self.container, self.root, self.db, self.timetable)
update_job.pack(
padx=int(self.root.screen_width*0.02),
pady=int(self.root.screen_height*0.03))
class PrintPdfButton(tk.Button):
def __init__(self, container):
tk.Button.__init__(
self,
master=container,
text="PDF erzeugen",
command=self.execute)
def execute(self):
''' Dummy class yet - intended to create some "PDF_Printer" object later. '''
print('1000x geklickt, 1000x ist nix passiert.')
class LoadDate(tk.Button):
def __init__(self, container, root, menu):
self.root = root
self.menu = menu
tk.Button.__init__(
self,
master=container,
text="Datum laden",
command=self.execute)
def execute(self):
''' Loads a user provided date for display and further processing. '''
new_date = dial.ChangeDate(self.root)
# Now set the new date at two places:
# On the class attribute (as datetime object) for further processing, and:
# On the instance attribute of MainMenu (as string/stringvar) for display only.
self.menu.current_date = new_date.date
self.menu.set_date(new_date.date)
# Replace the timetable object since we have a new day
self.menu.timetable = TextTable_Mediator(self.root, new_date.date, self.menu.joblist)
class AddJob(tk.Button):
def __init__(self, container, root, db, timetable):
tk.Button.__init__(self,
master=container,
text="Job hinzufügen",
command=self.execute)
self.root = root
self.db = db
self.timetable = timetable
self.job = None
def execute(self):
''' Requests a jobname. '''
jobname = dial.GetJobName(self.root)
self.job = jobname.job
insert_flag = self.db.insert_job(self.job)
# The function will return either the ID or None if the given job already exists
if insert_flag:
print('Job ', self.job, ' has been successfully inserted at index', insert_flag)
self.timetable.list_job(self.job)
else:
tk.messagebox.showerror("Fehler beim Erstellen des Jobs", "Es existiert bereits ein Job unter dem angegebenen Namen.\nBitte wählen Sie einen anderen!")
class RemoveJob(tk.Button):
def __init__(self, container, root, db, timetable):
tk.Button.__init__(self,
master=container,
text="Job löschen",
command=self.execute)
self.root = root
self.db = db
self.timetable = timetable
def execute(self):
''' Remove a certain job from the joblist, the timetable (if present) and the database. '''
dialog = dial.DeleteJob(self.root, self.db)
self.timetable.canvas.delete(dialog.delJob)
class SaveJobs(tk.Button):
def __init__(self, container, root, db, timetable):
tk.Button.__init__(self,
master=container,
text="Jobs speichern",
command=self.execute)
self.root = root
self.db = db
self.timetable = timetable
def execute(self):
''' Call a method in canvas class that returns info about all dropped jobs. Then pass them to the db object for saving. '''
print(self.timetable.job_info)
class UpdateJob(tk.Button):
def __init__(self, container, root, db, timetable):
'''This class lets the user change a job's name without having to delete it - and with that all the dates this particular job was performed. '''
tk.Button.__init__(self,
master=container,
text="Jobname anpassen",
command=self.execute)
self.root = root
self.db = db
self.timetable = timetable
def execute(self):
''' Call dialog: select a job, then update the stuff on the canvas if possible. If not, delete and rebuild the canvas. Ugly - avoid that!'''
dialog = dial.RenameJob(self.root, self.db)
self.timetable.active_textitem.change_text(dialog.old_name, dialog.new_name) # Indirekter Zugriff - ist das scheiße???
class Textitem():
def __init__(self, canvas, coords, tags, **kwargs):
self.canvas = canvas
self.coords = coords
self.tags = tags
self.textitem = None
self.highlighted = None
# The first of these methods is mandatory and executed in any case:
# Place the item; then edit its appearance (optional, if necessary).
self.place_item()
self.edit_item(**kwargs)
def place_item(self):
assert type(self.tags) is tuple, 'tags need to be of type tuple. Use one-tuples for single tags: (tag1,)'
self.textitem = self.canvas.create_text(self.coords[0],
self.coords[1],
text=self.tags[0],
tags=self.tags)
def edit_item(self, **kwargs):
try:
for arg, value in kwargs.items():
self.canvas.itemconfig(self.textitem, **kwargs)
except tk.TclError as e:
print(e, ' - argument is not valid for function tk.Canvas.itemconfig()! See tkinter docu for allowed options.')
def change_text(self, name, value):
itemID = self.canvas.find_withtag(name)
self.canvas.itemconfig(itemID, text=value)
def fill(self, event=None):
# Size for background is the text's bbox and an additional padding of 10.
size = self.canvas.bbox(tk.CURRENT)
size = (size[0] - 10, size[1] - 10, size[2] + 10, size[3] + 10)
self.canvas.itemconfig(tk.CURRENT, activefill='white')
rect = self.canvas.create_rectangle(size, fill='blue', width=0)
self.canvas.tag_lower(rect, 'list')
self.highlighted = rect
def delete_backgr(self, event=None):
self.canvas.delete(self.highlighted)
class Timetable():
def __init__(self, canvas, date):
self.canvas = canvas
self.date = date
self.increment = datetime.timedelta(seconds=900) # 15 minutes
self.width = 0
self.height = 0
self.coords = []
self.info = []
self.set_size()
def set_size(self):
self.canvas.update() # Necessary because winfo_width() will otherwise always return 1 - no idea why...
self.width = self.canvas.winfo_width() / 6 # 2/3 are preserved for the rectagnles, the rest remains free for the jobtiles
self.height = self.canvas.winfo_height() / 16.5 # 15 Rows, a label and some padding
self.draw_grid()
def draw_grid(self):
''' Generate a 4x14 table, where each field represents 15 minutes of time. The respective timespan is saved together with the ID of the connected field. '''
start_time = datetime.time(hour=7)
start_time = datetime.datetime.combine(self.date, start_time)
y1 = self.height # Initial value is one tile's distance from top border (left clear for label)
y2 = y1 + self.height
# outer loop: build rows
for y in range(14):
x1 = self.width * 1.5 # Initial value is one tile's distance from left border (left clear for label)
x2 = x1 + self.width
#inner loop: add four columns per row
for x in range(4):
rect = self.canvas.create_rectangle(x1,y1,x2,y2)
# increment the counters
x1 += self.width
x2 += self.width
end_time = start_time + self.increment
#Create a text item displaying the end time in the background of the current rectangle
rect_bbox = self.canvas.bbox(rect)
x_coord = rect_bbox[2] - (rect_bbox[2] - rect_bbox[0]) / 2
y_coord = rect_bbox[3] - (rect_bbox[3] - rect_bbox[1]) / 2
text = 'bis ' + str(end_time.time())
Textitem(self.canvas, (x_coord, y_coord), (text, 'time'), fill='#AFABAB')
# save the id, start and end times and increment the time counter
self.info.append((rect, start_time, end_time))
start_time = end_time
y1 += self.height
y2 += self.height
def check_overlap(self, text_coords):
for rect in self.info:
rect_coords = self.canvas.coords(rect[0])
if rect_coords[0] <= text_coords[0] <= rect_coords[2] and \
rect_coords[1] <= text_coords[1] <= rect_coords[3]:
return rect[0]
class TextTable_Mediator():
def __init__(self, root, date, jobs):
self.date = date
self.jobs = jobs
self.active_textitem = None
self.canvas = tk.Canvas(root,
width=int(root.screen_width*0.7),
height=int(root.screen_height*0.85),
bd=3,
relief=tk.GROOVE)
self.canvas.grid(
row = 0,
column = 1,
padx = int(root.screen_width*0.03),
pady = int(root.screen_height*0.04))
self.timetable = Timetable(self.canvas, self.date)
for x in self.jobs.values():
self.list_job(x)
self.event_bindings()
def list_job(self, job):
''' Adds a textitem to the joblist at the left of the timetable. The item is placed on the first piece of free space available, meaning that free gaps will be filled before a job is attached to the end of the list. '''
x_coord = self.canvas.winfo_width() * 0.08
y_coord = self.canvas.winfo_height() * 0.05
while self.canvas.find_overlapping(
x_coord,
y_coord,
x_coord+1,
y_coord+1):
y_coord += self.canvas.winfo_height() * 0.04
item = Textitem(self.canvas, (x_coord, y_coord), (job, 'list'))
self.active_textitem = item
def event_bindings(self):
''' Event bindings for Drag and Drop, rightclicking and maybe other events are defined here. '''
self.canvas.tag_bind('list', '<ButtonPress-1>', self.on_B1_press)
self.canvas.tag_bind('list', '<B1-Motion>', self.on_csr_move)
self.canvas.tag_bind('list', '<ButtonRelease-1>', self.on_B1_rel)
self.canvas.tag_bind('list','<Enter>', self.active_textitem.fill)
self.canvas.tag_bind('list','<Leave>', self.active_textitem.delete_backgr)
def on_B1_press(self, event=None):
''' Called when user clicks on a job to d&d it. Gets required info and creates a new Texitem object, which should should have been done directly in the event binding optimally. '''
coords = (event.x, event.y)
name = self.canvas.itemcget(tk.CURRENT, 'text')
self.active_textitem = Textitem(self.canvas, coords, (name, 'isDouble'))
def on_csr_move(self, event):
''' Move the dragged textitem around - if it is hovered over a rectangle, highlight it. '''
# Move it
double = self.canvas.find_withtag('isDouble')
self.canvas.coords(double, event.x, event.y)
# Clear last matched rectangle, if exisiting
last_hovered = self.canvas.find_withtag('last_hovered')
if last_hovered:
self.canvas.itemconfig(last_hovered, outline='black', width=1)
self.canvas.dtag(last_hovered, 'last_hovered')
# Find a matching rectangle and highlight it, set a 'current' flag for this rect
match = self.timetable.check_overlap(self.canvas.bbox(double))
if match:
self.canvas.itemconfig(match, outline='#215EBB', width=3)
self.canvas.addtag_withtag('last_hovered', match)
def on_B1_rel(self, event=None):
''' Called when user drops the job somewhere, that is on release of mouse button 1. '''
match = self.timetable.check_overlap((event.x, event.y))
if match: # If Job is dropped outside grid, check_overlap returns none
# prepare coords and name so we can display the job in the background
match_coords = self.canvas.coords(match)
text_coords = (int(match_coords[0]+20), int(match_coords[1]+10))
name = self.canvas.gettags(self.active_textitem.textitem)[0] # get the double item's name (=first tag)
name = (name,)
self.active_textitem = Textitem(self.canvas,
text_coords,
name)
self.active_textitem.edit_item(
width=int(self.timetable.width-5),
font=('Segoe UI', 10),
fill='white')
self.canvas.itemconfig(match, fill='#008000')
# Delete the background time, this got somewhat ugly
for items in self.canvas.find_withtag('time'):
if self.timetable.check_overlap(self.canvas.coords(items)) == match:
self.canvas.delete(items)
double = self.canvas.find_withtag('isDouble')
self.canvas.delete(double)
Steuerungsmodul:
[Codebox=python file=Unbenannt.py]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import AV3_CONF
import AV3_GUI
import AV3_Dialogs as Dial
import platform, locale
import tkinter as tk
import datetime
class Launcher():
def __init__(self):
''' This class makes sure alle prerequisites, mainly loading or requesting cfg data, are met for the GUI to start correctly. '''
self.config = AV3_CONF.Config()
self.config_values = {}
self.db_path = None
self.root = None
self.setup()
def setup(self):
''' This controls the correct setup of the main window and its most central module, the main menu. '''
self.set_locale()
self.root = MainWindow()
self.root.protocol('WM_DELETE_WINDOW', self.close_app)
# Get the cfg stuff
if self.config.configfile_exists():
# read_cfg returns 1 if all is OK - we must not proceed unless some checks are OK.
if not self.read_cfg():
return
else:
self.get_cfg_data()
self.root.deiconify()
AV3_GUI.MainMenu(self.root, self, self.config_values['db'])
self.root.mainloop()
def read_cfg(self):
''' Read values from cfg file and check the password '''
try:
self.config_values = self.config.read_confvalue()
except KeyError as e:
tk.messagebox.showerror("Fehler in settings.ini", "Die Konfigurationsdatei 'settings.ini' wurde unzulässig modifiziert.\n Korrigieren Sie den Fehler oder löschen Sie settings.ini - alle Einstellungen gehen dabei aber verloren!\n\nSiehe Konsolen-Log für Details.")
print('The following value in the cfg file was corrupt: ', e)
return
pw_win = Dial.CheckPassword(self.root, self.config_values['pw'])
try:
pw_win.password
print('Setup complete, building GUI...')
except AttributeError: # If the user skips the password check, there will be no such attribute. This must be handled and is used here to exit the program.
self.root.destroy()
print('Password entry aborted, exiting program.')
return
# If nothing went wrong:
return 1
def get_cfg_data(self):
''' As we have no cfg file, request the necessary data from the user via dialogs. '''
self.db_path = Dial.ask_db_path()
self.db_path += '/data.db'
pw_win = Dial.RequestNewPassword(self.root)
self.config_values = {'db':self.db_path, 'pw':pw_win.password} # Dict is used instead of passing the values directly as parms because of better expandability later.
self.config.set_confvalues(init_values=self.config_values)
def set_locale(self):
''' Set locale depending on OS. Tkinter Widgets tend to be mixed English and German on Mac OS X systems (in my experience) '''
if platform.system() == 'Windows':
locale.setlocale(locale.LC_ALL, 'deu_deu')
else:
locale.setlocale(locale.LC_ALL, 'de_DE')
def close_app(self):
''' User must confirm before closing the application. All changes to config file are saved here; therefore any changes will be lost if the app crashes or is closed via terminal kill. '''
yesno = tk.messagebox.askyesno("Beenden", "Programm wirklich beenden?")
if yesno:
self.config.set_confvalues(add_value = str(datetime.datetime.now()))
self.config.write_settings()
self.root.destroy()
class MainWindow(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.screen_width = self.winfo_screenwidth()
self.screen_height = self.winfo_screenheight()
self.setup_window()
def setup_window(self):
self.title("Arbeits-Management BETA")
self.geometry('%dx%d+0+0' %(self.screen_width, self.screen_height))
self.withdraw() # Root window should not be visible until starting routines like PW check etc. are complete.
if __name__ == '__main__':
x = Launcher()