Zimmerautomation mit Tkinter

Fragen zu Tkinter.
Antworten
Obiwaahn
User
Beiträge: 1
Registriert: Samstag 23. Januar 2016, 15:41

Hallo allerseits,
ich versuche mich im Moment an einer Art Hausautomation. Bevor ich jedoch die einzelnen Geräte versuche anzusteuern, möchte ich mir mit Tkinter ein graphisches Interface programmieren. Mein erster Versuch bestand darin die Standard-Bibliothek von Tkinter zu verwenden. Den Code dazu seht ihr hier:

Code: Alles auswählen

#Bibliotheken
from tkinter import *

#Variablen

    #Variable hilft beim einhalten der richtigen Zeilen und Spalten
test = 0
    #Hauptmenue Zeilen
zeilen = 2
    #Hauptmenue Spalten
spalten = 5
    #Buttonbreite
buttonWidth = 10
    #Buttonhoehe
buttonHeight = 2
    #Schriftart
schriftArt = ('Arial', 14)

    #Farbe Button an
colourAn = 'yellow'

    #Farbe Button aus
colourAus = 'grey94'


#Initialisierung des Fensters fuer das Hauptmenue
window = Tk()
window.title('Zimmerautomation')

#Rahmen initialisieren und zentral positionieren
rahmen1 = Frame(window)
rahmen1.pack()

rahmen2 = Frame(window)
rahmen2.pack(side=BOTTOM)


#Definition der Befehle, die die Buttons ausfuehren sollen zum ANSCHALTEN

    #Fenster Button an
def rollladenAn():
    button1.config(bg=colourAn, command=rollladenAus)

    #Fernseher Button an
def fernseherAn():
    button2.config(bg=colourAn, command=fernseherAus)

    #Deckenfluter Button an
def deckenfluterAn():
    button3.config(bg=colourAn, command=deckenfluterAus)

    #Nachttischlampe Button an
def tischlampeAn():
    button4.config(bg=colourAn, command=tischlampeAus)

    #Nachtmodus Button an
def ruhemodusAn():
    button5.config(bg=colourAn, command=ruhemodusAus)

    #Kino Button an
def kinoAn():
    button6.config(bg=colourAn, command=kinoAus)



#Defintionen der Befehle, die die Buttons ausfuehren sollen zum AUSSCHALTEN
    
    #Fenster Button aus
def rollladenAus():
    button1.config(bg=colourAus, command=rollladenAn)

    #Fernseher Button aus
def fernseherAus():
    button2.config(bg=colourAus, command=fernseherAn)

    #Deckenfluter Button aus
def deckenfluterAus():
    button3.config(bg=colourAus, command=deckenfluterAn)

    #Nachttischlampe Button aus
def tischlampeAus():
    button4.config(bg=colourAus, command=tischlampeAn)

    #Nachtmodus Button aus
def ruhemodusAus():
    button5.config(bg=colourAus, command=ruhemodusAn)

    #Kino Button aus
def kinoAus():
    button6.config(bg=colourAus, command=kinoAn)
    


    #Quit Button
def schliessen():
    print('Schliessen')


#Arrays fuer die Texte der einzelnen Buttons im Hauptmenue
buttonText = ['Fenster', 'Fernseher', 'Deckenfluter', 'Tischlampe', 'Ruhemodus', 'Kino']
buttonCommands = [rollladenAn, fernseherAn, deckenfluterAn, tischlampeAn, ruhemodusAn, kinoAn]

#Initialisierung der Buttons im Hauptmenue
button1 = Button(rahmen1, text=buttonText[0], command=buttonCommands[0], width=buttonWidth, height=buttonHeight, font=schriftArt)
button2 = Button(rahmen1, text=buttonText[1], command=buttonCommands[1], width=buttonWidth, height=buttonHeight, font=schriftArt)
button3 = Button(rahmen1, text=buttonText[2], command=buttonCommands[2], width=buttonWidth, height=buttonHeight, font=schriftArt)
button4 = Button(rahmen1, text=buttonText[3], command=buttonCommands[3], width=buttonWidth, height=buttonHeight, font=schriftArt)
button5 = Button(rahmen1, text=buttonText[4], command=buttonCommands[4], width=buttonWidth, height=buttonHeight, font=schriftArt)
button6 = Button(rahmen1, text=buttonText[5], command=buttonCommands[5], width=buttonWidth, height=buttonHeight, font=schriftArt)
#button7 = Button(rahmen1, text=buttonText[6], command=buttonCommands[6], width=buttonWidth, height=buttonHeight, font=schriftArt, visible='No')


buttonQuit = Button(rahmen2, text='QUIT', command=schliessen, width=buttonWidth, height=buttonHeight, font=schriftArt)

#Hauptmenuebuttons werden in Arry eingetragen, jeder Button muss eingetragen werden
hButtons = [button1, button2, button3, button4, button5, button6, buttonQuit]


#Buttons werden erstellt
for i in range(zeilen):
    for j in range(spalten):
        hButtons[test].grid(row=i, column=j, padx=4, pady=4)
        test=test + 1
        if test == len(hButtons):
            break


window.mainloop()
Prinzipiell funktioniert der Code auch sehr gut, ist jedoch sehr unelegant gelöst. Zudem möchte ich später über ein Einstellungsmenü direkt über die visuelle Oberfläche Buttons hinzufügen können. Daher habe ich versucht das Erstellen der Buttons in eine for-Schlefe einzubinden und die einzelnen Buttonnamen, sowie deren Aufschrift in ein Array zu packen, was sich im Nachhinein erweitern ließe. Dadurch konnte ich allerdings keine einzelnen Buttons mehr konfigurieren, da diese nacheinander in diesselbe Variable übergeben wurden, was ich aber benötige, um die Hintergrundfarbe zu ändern. Weil das dynamische Erstellen von Variablen jedoch sehr "riskant" ist, habe ich diese Idee verwerfen können.

Meinen zweiter Versuch startete ich mit der Erweiterung Tix, weil diese die Möglichkeit einer ButtonBox enthält und für das Hinzufügen eines weiteren Buttons keine Variable benötigt und es damit ermöglicht einen Button über das Interface zu erstellen.
Hier mal ein Ansatz:

Code: Alles auswählen

import tkinter.tix as tix


def test(a):
    #erstellt Titel der Buttonbox
    top = tix.Label(a, padx=20, pady=10, bd=1, relief=tix.RAISED, text='Test Box')
    
    #ordnet Titel an
    top.pack(side=tix.TOP, fill=tix.BOTH, expand=1)

	
    #erstellt Buttonbox
    box = tix.ButtonBox(a, orientation=tix.HORIZONTAL)

    #fuegt Buttons hinzu
    box.add('fenster', text='Fenster', underline=0, width=5, command= ausdruck)
    box.add('lampe', text='Lampe', underline=0, width=5, command= back)

    #ordnet Buttons an
    box.pack(side=tix.BOTTOM, fill=tix.X)

    
def ausdruck():
    print ('Fenster')

def back():
    print('Lampe')
	
if __name__ == '__main__':
    fenster = tix.Tk()
    test(fenster)
    fenster.mainloop()
Die elegante Variante wäre es natürlich wieder das Hinzufügen der Buttons in eine for-Schleife zu setzen, was kein Problem darstellt. Nun hänge ich allerdings fest... Ich suche seit zwei Tagen, habe bis jetzt aber noch keine Möglichkeit gefunden, wie ich einen hinzugefügten Button in Tix konfigurieren kann. Die Python-Docs haben mir leider auch nicht weitergeholfen.

Vielleicht habt Ihr eine Idee, wie ich mein Problem lösen kann. Wenn ihr einen Lösungsvorschlag für eine der beiden Varianten habt oder eventuell eine ganz andere Herangehensweise auch mit einer anderen Bibliothek, wäre ich über eine Antwort sehr dankbar. Sollte es noch Fragen geben stehe ich gern zur Verfügung.

Schonmal Vielen Dank im Vorraus!!!

Grüße
Obiwaahn
Zuletzt geändert von Anonymous am Samstag 23. Januar 2016, 17:24, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@Obiwaahn: Ich würde die `Tix.ButtonBox` wohl eher nicht verwenden. Man muss den einzelnen Schalflächen Namen in Form von Zeichenketten geben, die den Regeln von Tk/Tcl-Bezeichnern folgen und kann dann über diesen Namen und die `subwidget()`-Methode auf diese Schalflächen zugreifen um sie zu konfigurieren. Das wäre mir eine Indirektion zu viel.

Zum ersten Ansatz: Kommentare wie ``#Bibliotheken`` oder ``#Variablen`` sind überflüssig. Kommentare sollten dem Leser etwas mitteilen was er nicht aus dem Quelltext schon ganz offensichtlich lesen kann. Und dann in der Regel auch nicht *was* gemacht wird, denn das sieht man ja am Code, sondern warum es so gemacht wird. Und das auch nur wenn es nicht offensichtlich ist. Auch dann sollte man erst einmal schauen, ob man den Code nicht verständlicher schreiben kann, so dass man keinen Kommentar benötigt. Insbesondere wenn man im Kommentar beschreibt was ein Name bedeutet, ist der Name oft nicht gut gewählt. Das betrifft beispielsweise `test` in Deinem Programm, was überhaupt nichts mit einem Test zu tun hat.

Die Schreibweise von Namen entspricht nicht immer dem Style Guide for Python Code. Deutsch und Englisch bei Namen zu mischen ist schon nicht schön, aber innerhalb eines Namens beide Sprachen zu verwenden ist unerwartet und birgt noch mehr Möglichkeiten Verwirrung zu stiften. (Hiess das jetzt `colorAn`, `colorOn`, `farbeAn`, oder `farbeOn`‽)

Auf Modulebene sollten nur Konstanten, Funktionen, und Klassen definiert werden. Das Hauptprogramm steht üblicherweise in einer `main()`-Funktion.

Das bedeutet dann auch, dass die Funktionen wie `fernseher_an()` nicht mehr einfach so ”magisch” auf die Schalflächen zugreifen können. Werte (ausser Konstanten) betreten Funktionen und Methoden als Argumente und verlassen sie gegebenfalls als Rückgabewerte. Alles andere führt zu Programmen wo Funktionen keine in sich geschlossenen Einheiten mehr sind, und man die gesamte Funktion lesen muss um zu sehen was sie alles verwendet, und dann das gesamte Programm lesen muss um zu schauen wo diese Dinge definiert werden und von aus sie noch verändert werden. Das führt schnell zu überschaubarem, chaotischen Code, den man nicht mehr sinnvoll testen, wiederverwendet, erweitern, oder warten kann.

Namen fortlaufende Nummern anzuhängen ist üblicherweise ein „code smell“ und deutet darauf hin, dass man eigentlich eine Datenstruktur verwenden möchte. Oft eine Liste.

Listen sind übrigens keine Arrays. Es gibt auch Arraydatentypen in Python und deshalb sollte man Listen in Kommentaren nicht so nennen.

Sachen die zusammengehören, sollte man auch räumlich nah im Quelltext stehen haben. Also nicht irgendwo am Anfang Namen definieren der erst am Ende dann tatsächlich verwendet werden. Wenn ich in Zeile 120 wissen will wie `test` initialisiert wurde, muss ich fast an den Anfang des Programms zurück obwohl der Name zwischenzeitlich nicht verwendet wird.

Oder auch nicht erst alle Funktionen zum Anschalten für verschiedene Geräte hinschreiben und dann alle zum Abschalten. Denn wenn man ein Gerät ändern will oder ein Neues hinzufügen möchte, muss man diese Änderung über den Code verteilt vornehmen. Mal probeweise den Code für den Deckenfluter auskommentieren ist beispielsweise nicht an einer Stelle getan.

Das gilt auch für Daten: Die Elemente von `buttonText` und `buttonCommands` gehören jeweils paarweise zusammen (eigentlich müssten die Ausschaltfunktionen da auch erfasst werden), also sollten die nicht in parallelen Datenstrukturen stehen.

Weder die Breite noch die Höhe der Schaltflächen sollte man hart vorgeben. Man kann einen (zusätzlichen) Rand für die Höhe und Breite verwenden, ansonsten sollte sich das an dem Inhalt der Schaltfläche orientieren. Also an Text und Schriftart.

Es würde sich hier auch ein `Checkbutton` anbieten, der hat von Haus aus schon zwei Zustände und Farben dafür und man könnte den prima mit einer `IntVar` in einer neuen Klasse verwenden um ein Rückruffunktion als `command()` anzubieten, welche den Zustand als Argument erhält. Dann braucht man für jedes Gerät auch nur noch eine Funktion zu schreiben. Oft wird sich das an und ausschalten ja auch nicht gross unterscheiden. Im einfachsten Fall kann der Wahrheitswert einfach direkt zum Schalten eines Relais verwendet werden.

Womit wir bei Klassen wären: GUI-Programmierung ohne objektorientierte Programmierung (OOP) ist Murks. Das ist bei nicht-trivialen Programmen nicht sauber und nachvollziehbar hinzubekommen.
BlackJack

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import Tkinter as tk
from functools import partial

ON_COLOR, OFF_COLOR = 'yellow', 'gray94'


class SwitchUI(tk.Checkbutton):

    def __init__(
        self,
        parent,
        command=lambda state: None,
        on_color=ON_COLOR,
        off_color=OFF_COLOR,
        **kwargs
    ):
        tk.Checkbutton.__init__(
            self,
            parent,
            indicatoron=False,
            background=off_color,
            command=self.do_command,
            selectcolor=on_color,
            **kwargs
        )
        self.variable = self['variable'] = tk.BooleanVar(self)
        self.command = command

    def do_command(self):
        self.command(self.variable.get())


def switch(what, state):
    print('Switching {0} {1}.'.format(what, 'on' if state else 'off'))


def main():
    root = tk.Tk()
    root.title('Zimmerautomation')
    
    font_size = 14
    pack_options = {'side': tk.TOP, 'fill': tk.X, 'padx': 4, 'pady': 4}
    
    for text in [
        'Fenster', 'Fernseher', 'Deckenfluter',
        'Tischlampe', 'Ruhemodus', 'Kino',
    ]:
        SwitchUI(
            root, text=text, command=partial(switch, text), font=font_size
        ).pack(ipadx=4, ipady=4, **pack_options)
    
    tk.Button(
        root, text='Quit', command=root.quit, font=font_size
    ).pack(**pack_options)
    
    root.mainloop()


if __name__ == '__main__':
    main()
Antworten