Canvas aktualisiert nicht

Fragen zu Tkinter.
Antworten
Kontakt60
User
Beiträge: 2
Registriert: Dienstag 12. Juni 2018, 05:50
Kontaktdaten:

Guten morgen zusammen.

Ich habe in meinen Ausbildungsbetrieb die Aufgabe bekommen, zusammen in einer Gruppe, eine Art Anwesenheitstafel zu bauen.

Um auf der Tafel angezeigt zu werden wird mittels Fingerabdruckleser die Identifikation des Benutzer bestimmt.
Es sollen die Grundrisse der Schreibtische angezeigt werden, und bei Anwesenheits soll der passende Schreibtisch mit einer Farbe gefüllt. (Gruen bspw.)(Später auch ein Foto und weitere Daten)

Uns wird völlig offen gelassen, wie wir die Aufgabe lösen. Wir haben uns dann dazu entschlossen es mit Python auf einen Raspberry Pi zu probieren.

Eine Konsolenvariante, um zu schauen ob die Serielle Verbindung zu den Fingerabdruckscanner (Hängt an einen Arduino) besteht, funktioniert wie erwartet.

Zu meinen Problem:
Ich bekomme es hin, dass die Grundrisse der Schreibtische gezeichnet werden. Das erstmalige lesen eines Fingerabdrucks färbt auch den entsprechende Tisch korrekt um. Allerdings hat danach jeder weiterer Einleseversuch keinen Erfolg mehr.

So sieht es nachdem der erste Abdruck gescannt wurde aus.
https://imgur.com/a/Tsc42gw


Code: Alles auswählen

from Tkinter import *
import MySQLdb
import serial

#db = MySQLdb.connect(host="localhost", user="SUED3", passwd="SUED3", db="anwesenheitstafel")
#cursor = db.cursor()

def drawTables():
	
	#Serielle Daten zur Kommunikation zwischen Arduino und dem Raspberry Pi 3
	ser = serial.Serial(port = '/dev/ttyACM0', baudrate = 9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=None)	

	#Einlesen der Seriellen Daten (Fingerabdruck ID)
	serialData = ser.readline().decode("UTF-8").split()
	fid = int(serialData[0])
	
	##### TESTWEISE OHNE SQL ABFRAGE
	#Tisch einfaerben
	if fid == 19:
		status[15] = 1
	if fid == 25:
		status[12] = 1
	if fid == 20:
		status[2] = 1

	#Banner
	canvas.create_rectangle(100, 50, 980, 250, tags='banner')

	#Ausbildertische
	canvas.create_rectangle(100, 300, 400, 400, fill='yellow', tags='tisch')
	canvas.create_rectangle(680, 300, 980, 400, fill='yellow', tags='tisch')
	
	#Tische richtig anordnen
	for i in range(24):
		
		if status[i] == 0:
			color = 'white'
		else:
			color = 'green'	
			
		if i < 6:
			#Drittes Lehrjahr Tisch 7-12
			canvas.create_rectangle(xAlign, yAlign + (i*h), xAlign+w, yAlign + (i*h) + h, fill=color, tags='tisch')
		elif i< 12:
			#Drittes Lehrjahr Tisch 1-6
			canvas.create_rectangle(xAlign+w, yAlign + ((i-6)*h), xAlign+(2*w), yAlign + ((i-6)*h) +h, fill=color, tags='tisch')
		elif i <18:
			#Zweites Lehrjahr Tisch 7-12
			canvas.create_rectangle((1080-xAlign)-(2*w), yAlign + ((i-12)*h), (1080-xAlign)-w, yAlign + ((i-12)*h) +h, fill=color, tags='tisch')
		else:
			#Zweites Lehrjahr Tisch 1-6
			canvas.create_rectangle((1080-xAlign)-(w), yAlign + ((i-18)*h), (1080-xAlign), yAlign + ((i-18)*h) +h, fill=color, tags='tisch')

# 0:Abwesend, 1:Anwesend
status = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
				
root = Tk()
root.title("Anwesenheitstafel SUED3")

canvas = Canvas(root, width = 1080, heigh = 1920)
canvas.pack()

#Abstand zu den Seiten
xAlign = 150
yAlign = 400

#Masse der Tische
w = 100
h = 220

root.after(100, drawTables())
root.mainloop()
---------------------------

Die funktionierede Konsolenversion

Code: Alles auswählen

import MySQLdb
import serial

from colorama import init, Fore, Style
init()

#Serielle Daten zur Kommunikation zwischen Arduino und dem Raspberry Pi 3
ser = serial.Serial(port = '/dev/ttyACM0', baudrate = 9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=None)

#MySQL
db = MySQLdb.connect(host="localhost", user="SUED3", passwd="SUED3", db="anwesenheitstafel")
cursor = db.cursor()
print ("connected to: " + ser.portstr)

while True:
	
	serialData = ser.readline().decode("UTF-8").split()

	SQL_query = "SELECT * FROM USER WHERE FID='{fingerid}' OR FID2='{fingerid}'"
	SQL_query = SQL_query.format(fingerid=serialData[0])

	cursor.execute(SQL_query)

	for row in cursor.fetchall():
        # row 0: UID, 1: Name, 2: Vorname, 3: Gruppe, 4: Geburtsdatum, 5: Fotopfad, 6: FID1, 7: FID8, 8: Status
		print str(serialData[0] + " " + row[2] + "   " + str(row[8])  + "   " + str(row[3]) + "   "),
		
		userid = int(row[0])
		fingerid = int(row[6])
		if str(serialData[0]) == str(row[6]):
			print "Finger 1",
		else:
			print "Finger 2",
		
		print ("   Confidence: "),
		if int(serialData[1]) <= 150:
			print (Fore.RED),
		elif int(serialData[1]) <= 400:
			print (Fore.YELLOW),
		else:
			print (Fore.GREEN),
		print (serialData[1] + Style.RESET_ALL)


	cursor.execute("INSERT INTO LOGIN (ID,UID,FID,CONFIDENCE,TIME) VALUES (NULL,%s,%s,%s,CURRENT_TIMESTAMP)",(userid,serialData[0],serialData[1],))
	db.commit()

cursor.close()
db.close()
ser.close()
Ich hoffe ihr könnt mir hierbei ein wenig weiterhelfen

Mit freundlichen Grüßen

Jan
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Kontakt60: `drawTables()` wird nur einmal aufgerufen. Und da hast Du gleich zwei Fehler: 1. Rufst Du die Funktion selbst auf und übergibst `after()` dann deren Rückgabewert, wo Du `after()` die Funktion übergeben solltest. 2. Führt `after()` die übergebene Funktion oder Methode nach der angegebenen Zeit genau *einmal* aus. Also selbst wenn Du dort das richtige übergibst, wird `drawTables()` nur einmal ausgeführt. Den `after()`-Aufruf würde ich deshalb als letztes in `drawTables()` machen und dann im Hauptprogramm `drawTables()` einmal aufrufen um das ganze in Gang zu bringen.

Das nächste Problem ist dann das das Lesen von der seriellen Verbindung blockiert bis ein Fingerabdruck gelesen wurde. Solange reagiert dann auch die GUI nicht, denn Rückruffunktionen in GUIs dürfen nur relativ kurz etwas machen und sollten so schnell wie möglich zur GUI-Hauptschleife zurückkehren von der sie aufgerufen wurden. Hier würde man mit einem Thread (`threading`-Modul) und einer Queue (`Queue`-Modul) arbeiten um die serielle Schnittstelle parallel zur GUI-Hauptschleife zu lesen und die Daten in die Queue zu stecken und in der `drawTables()`-Funktion dann alles was gerade in der Queue steckt, zu verarbeiten.

Dann wird in der `drawTables()` immer wieder neu gezeichnet. Das ist falsch weil `Canvas` keine Pixel- sondern Vektorgrafik ist. Es werden also mit jedem Durchgang mehr Rechtecke übereinander gezeichnet die immer alle noch vorhanden sind und jedes mal alle neu gezeichnet werden müssen. Das Programm wird also immer langsamer werden und immer mehr Speicher verbrauchen. Lösung: alles nur *einmal* zeichenen, sich die Rückgabewerte der `create_*`-Aufrufe von den Objekten merken, die man später verändern will (oder `tags`-Werte vergeben), und dann mit `Canvas.itemconfig()` später entsprechen ändern. Zum Beispiel die Füllfarbe der Tische.

Das da Code auf Modulebene steht und die `drawTables()` auf ”magische” Weise auf Werte aus der ”Umgebung” zugreift, und welche davon auch noch verändert, ist nicht gut. Alles was eine Funktion oder Methode verwendet (ausser Konstanten) sollten als Argumente rein kommen. Sonst wird Code sehr schnell sehr unübersichtlich und fehleranfällig, weil man *alles* auf einmal lesen und verstehen muss, und nicht nur den Inhalt einer Funktion oder Methode isoliert betrachten kann. Oder testen kann, denn so ein alles hängt irgndwie mit allem anderen zusammen, ohne klare Schnittstellen, erschwert auch das testen, oder macht es gar unmöglich. Der Code auf Modulebene sollte in einer Funktion verschwinden, die üblicherweise `main()` heisst. Dann kann man auch gar nicht mehr ”wild” auf globale Objekte in anderen Funktionen zugreifen.

Apropos unübersichtlich: Keine Sternchen-Importe. Man verliert dann schnell die Übersicht wo welcher Name eigentlich her kommt, und läuft Gefahr über Namenskollisionen zu stolpern. `Tkinter` kippt Dir da knapp 190 Namen in Dein Modul.

Namen von allem ausser Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase) schreibt man in Python übrigens klein_mit_unterstrichen. Also `draw_tables()` statt `drawTables()` und `X_ALIGN` statt `xAlign`. Konstanten stehen am Anfang des Moduls nach den Importen. Wirf am besten mal einen Blick in den Style Guide for Python Code.

Letztlich wirst Du bei einem GUI-Programm nicht um objektorientierte Programmierung herum kommen. Arbeite Dich da vielleicht unabhängig von der GUI und dem konkreten Programm mal durch dieses Thema durch.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Zusätzlich zu dem, was __blackjack__ schon schrieb:
- Niemals Werte in SQL-Statements hineinformatieren, dafür gibt es Parameter, die Du ja schon beim INSERT benutzt. Das SELECT nach FID erwartet (hoffentlich) nur eine einziges Ergebnis, so dass die for-Schleife nur einmal durchlaufen wird, es daher am besten gar keine Schleife gibt. Um einen Eintrag zu lesen, gibt es fetchone. *-SELECTS vermeiden, weil man dann eine Abhängigkeit hat, in welcher Reihenfolge die Spalten in der Tabelle stehen. Besser Spaltennamen explizit angeben. Dann kannst Du auch per Tuple-Anpacking die Werte gleich sinnvollen Namen zuweisen und mußt nicht mit kryptischen Indizes arbeiten.
Statt Strings mit + zusammenzustückeln benutze die .format-Methode (wie Du sie fälschlicherweise schon beim SQL_query einsetzt).
userid und fingerid sollten schon Zahlen sein, der Aufruf von int ist also überflüssig, serialData ist schon ein String, so dass das erneute umwandeln unnötig ist, obwohl Du hier besser serialData[0] in ein int wandelst, weil fingerid ja wahrscheinlich eine Zahl ist, und sowohl serialData[0] und serialData[1] sollten als Zahl jeweils an eine Variablen (scanned_finger_id, scanned_status) gebunden werden, um die Lesbarkeit zu erhöhen.

cursor sind kurzlebige Objekte und sollten bei jeder Transaktion (Schleifendurchgang / drawTables-Aufruf) neu erzeugt werden.
Kontakt60
User
Beiträge: 2
Registriert: Dienstag 12. Juni 2018, 05:50
Kontaktdaten:

Vielen dank euch beiden. Ich habe es nun, wie gewollt, zum laufen bekommen.
Ich denke ich konnte alle Anmerkungen von euch umsetzen. Wäre aber trotzdem dankbar wenn über den Code nocheinmal rübergeschaut wird :)

Code: Alles auswählen

from Tkinter import *
import MySQLdb
import serial
import threading
import time

#Abstand zu den Seiten
X_ALIGN = 150
Y_ALIGN = 180

#Masse der Tische,
WIDTH = 100
HEIGHT = 220

#--------------------------------------------------------

def read_serial(ser):
	while True:
		serialData = ser.readline().decode("UTF-8").split()
		convert_serial_data_to_userid(int(serialData[0]))	
#--------------------------------------------------------
	
def convert_serial_data_to_userid(fid):
	
	db = MySQLdb.connect(host="localhost", user="SUED3", passwd="SUED3", db="anwesenheitstafel")
	cursor = db.cursor()

	sql_query = "SELECT * FROM USER WHERE FID='{fingerid}' OR FID2='{fingerid}'"
	sql_query = sql_query.format(fingerid=fid)
	
	cursor.execute(sql_query)
	uid = cursor.fetchone()
	
	print(uid[0])
	
	convert_fid_to_tableid(uid[0])
#--------------------------------------------------------

def convert_fid_to_tableid(uid):
	db = MySQLdb.connect(host="localhost", user="SUED3", passwd="SUED3", db="anwesenheitstafel")
	cursor = db.cursor()
	
	sql_query = "SELECT * FROM SUED WHERE ID='{userid}'"
	sql_query = sql_query.format(userid=uid)
	
	cursor.execute(sql_query)
	table = cursor.fetchone()
	
	#Schreiner
	if table[0] == 1:
		tableid = 1
	#Janssen
	elif table[0] == 2:
		tableid = 0
	#Zweites Lehrjahr
	elif table[1] == 2:
		tableid = table[2] + 15
	#Drittes Lehrjahr
	else:
		tableid = table[2] +1
	
	#print(tableid)
	
	#An/Abwesenheitsumschalter
	if status[tableid] == 0:
		status[tableid] = 1
	else:
		status[tableid]= 0
	
	#Tische updaten		
	update_tables()
#--------------------------------------------------------

def update_tables():
	for i in range(28):
		if status[i] == 0:
			canvas.itemconfig(myTables[i], fill='white')
		else:
			canvas.itemconfig(myTables[i], fill='green')	
#--------------------------------------------------------

def draw_tables():
	#Tische richtig anordnen
	for i in range(28):	
		if i < 1:
			#Schreiner
			myTables.append(canvas.create_rectangle(100, 80, 400, 180, fill='white', tags='tisch'))
		elif i < 2:
			#Janssen
			myTables.append(canvas.create_rectangle(680, 420, 980, 520, fill='white', tags='tisch'))
		elif i < 9:
			#Drittes Lehrjahr Tisch ID 2-7
			myTables.append(canvas.create_rectangle(X_ALIGN+WIDTH, Y_ALIGN + (i-2)*HEIGHT, X_ALIGN+(2*WIDTH), Y_ALIGN + (i-2)*HEIGHT +HEIGHT, fill='white', tags='tisch'))
		elif i< 16:				
			#Drittes Lehrjahr Tisch ID 8-14
			myTables.append(canvas.create_rectangle(X_ALIGN, Y_ALIGN + (15-i)*HEIGHT, X_ALIGN+WIDTH, Y_ALIGN + (15-i)*HEIGHT + HEIGHT, fill='white', tags='tisch'))
		elif i < 22:
			#Zweites Lehrjahr Tisch ID 15-20
			myTables.append(canvas.create_rectangle(1080-X_ALIGN-WIDTH, Y_ALIGN + (i-16)*HEIGHT+340, (1080-X_ALIGN), Y_ALIGN + (i-16)*HEIGHT +HEIGHT +340, fill='white', tags='tisch'))	
		else:
			#Zweites Lehrjahr Tisch ID 21-26
			myTables.append(canvas.create_rectangle((1080-X_ALIGN)-(2*WIDTH), Y_ALIGN + ((27-i)*HEIGHT)+340, (1080-X_ALIGN)-WIDTH, Y_ALIGN + (27-i)*HEIGHT +HEIGHT+340, fill='white', tags='tisch'))
		#print(i)
#-------------------------------------------------------

def draw_timebanner():
	canvas.delete("timetext")
	#Banner
	canvas.create_rectangle(600, 80, 1020, 180,fill='blue', tags='banner', outline='white')
	t = time.strftime('%H:%M:%S', time.localtime())
	canvas.create_text(810, 135, fill="white", font="Courier 55 bold", text=t, tags='timetext')
	#Uhr ruft sch selber wieder auf
	root.after(1000, draw_timebanner)
#-------------------------------------------------------

#Serielle Daten zur Kommunikation zwischen Arduino und dem Raspberry Pi 3
#Einlesen der Seriellen Daten  (Fingerabdruck ID)
ser = serial.Serial(port = '/dev/ttyACM0', baudrate = 9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=None)	


#Thread um den Seriellen Eingang zu ueberwachen
serialthread = threading.Thread(target=read_serial, args=(ser,))
serialthread.start()


#Tischliste
myTables=[]

#Status 0=Abwesend, 1=Anwesend (wird convert_fid_to_tableid geupdatet)
status = []
for i in range (28):
	status.append(0)
		
root = Tk()
root.title("Anwesenheitstafel SUED3")

#Fullscreen
root.overrideredirect(True)
root.overrideredirect(False)
root.attributes('-fullscreen', True)
root.bind('<Escape>', lambda e: root.destroy())

canvas = Canvas(root, width = 1080, heigh = 1920)
canvas.pack()

#Einmaliges Zeichnen der Tische
draw_tables()

#Erstmaliger Aufruhr der Uhr
draw_timebanner()

root.mainloop()
Apropos unübersichtlich: Keine Sternchen-Importe. Man verliert dann schnell die Übersicht wo welcher Name eigentlich her kommt, und läuft Gefahr über Namenskollisionen zu stolpern. `Tkinter` kippt Dir da knapp 190 Namen in Dein Modul.
Was genau müsste ich für mein Projekt einfügen? :roll:


MfG Jan



EDIT: Mit OOP muss ich mich erstmal schlau lesen. :)
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Die Anmerkung zu SQL-Injections durch das formatieren von SQL statements hast du schonmal NICHT umgesetzt. Damit oeffnest du Manipulation durch Usereingaben Tuer- und Tor.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Kontakt60: es mag zwar so aussehen, als ob es funktioniert, trotzdem sind einige schwerwiegende Fehler im Programm.

- Die GUI nicht aus einem Thread heraus verändern. Die Daten müssen über eine Queue vom Hauptprogramm an den Thread übergeben werden und dort weiterverarbeitet werden.
- Keine Werte in SQL-Statements hineinformatieren, benutze Platzhalter

Code: Alles auswählen

    sql_query = "SELECT * FROM USER WHERE FID=%(fid)s OR FID2=%(fid)s"
    cursor.execute(sql_query, {'fid': fid})
- Keine *-SELECTS, Felder explizit angeben, und statt magischen Indexes das Tuple entpacken.
- Keine *-Importe:

Code: Alles auswählen

import Tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(...)
etc.
- Datenbankzugangsdaten am Anfang des Programms als Konstanten definieren.
- Statt magischer Table-Ids als Index in Listen zu benutzen, direkt Wörterbuch mit entsprechenden Keys, die zu denen in der Datenbank passen.
- bei status True/False statt 1/0 benutzen.
- Eingerückt wird immer mit 4 Leerzeichen pro Ebene, sonst wird es je nach Editoreinstellung für Tabs ziemlich unleserlich.
Antworten