Ping in tkinter

Fragen zu Tkinter.
Antworten
Unknown123
User
Beiträge: 4
Registriert: Freitag 29. Mai 2020, 09:57

Hallo liebes Forum,

ich möchte gerne aus tkinter heraus pingen. In tkinter habe ich ein Fenster erstellt, in welchem man das Ziel angeben kann. Meine Idee war, das dann im Hintergrund in der cmd das Ziel gepingt wird und in tkinter angezeigt wird ob das Ziel online ist.

Dazu sieht mein Code folgendermaßen aus:

import os
from tkinter import *

fenster = Tk()
fenster.title('Ping in tkinter')
fenster.geometry('500x500')

eingabefeld = Entry(fenster, bd=5, width=40)
entry_text = eingabefeld.get()

def button_action():
entry_text = eingabefeld.get()
if (entry_text == ""):
welcome_label.config(text="Gib zuerst einen Namen ein.")
else:
entry_text = "Ping läuft @ " + entry_text
welcome_label.config(text=entry_text)

hostname = eingabefeld.get()

my_label = Label(fenster, text='Ziel: ')
hostname_isup = Label(fenster, text=hostname + ' is up!')
hostname_isdown = Label(fenster, text=hostname + ' is up!')

welcome_label = Label(fenster)

eingabefeld = Entry(fenster, bd=5, width=40)

welcom_button = Button(fenster, text="Start", command=button_action)
exit_button = Button(fenster, text="Exit", command=fenster.quit)

my_label.grid(row = 0, column = 0)
eingabefeld.grid(row = 0, column = 1)
welcom_button.grid(row = 1, column = 0)
if eingabefeld == True:
response = os.system("ping " + hostname)
if response == 0:
hostname_isup.grid(row = 0, column = 0)
else:
hostname_isdown.grid(r0w = 0, column = 0)

exit_button.grid(row = 1, column = 1)
welcome_label.grid(row = 2, column = 0, columnspan = 2)

mainloop()

Leider wird der Hostname den ich angebe nicht gepingt. Hat jemand eine Lösung?

Vielen Dank!
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Keine * Importe verwenden.
Importierw tkinter mit

Code: Alles auswählen

import tkinter as tk
und greife mittels tk. auf die Funktionen und Klassen zu.

Setze deinen Code-Tags. Die erscheinen automatisch, wenn du den </> Button im "Vollständigen Editor & Vorschau" klickst. Dein Code gehört zwischen die Tags.
So kann man nicht erkennen, welche Einrückungsebene die Codezeilen haben.

Oh, und os.system verwendet man nicht mehr. Schau mal in die Donumentation. Dort steht, dass man das subprocess Modul verwendet
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

Auf oberster Ebene gehört kein ausführbarer Code, nur (Funktions-)Definitionen. Zu dem Zeitpunkt wo `hostname` abgefragt wird, hatte der Nutzer noch gar keine Gelegenheit, etwas einzugeben, weil das passiert erst im `mainloop`. So linear funktioniert GUI-Programmierung nicht. Da ist auch einiges doppelt. `eingabefeld` kann niemals `True` sein.
`os.system` benutzt man nicht mehr, und vor allem überträgt man unkontrolliert irgendwelche Nutzereingaben an die aufgerufene Shell.
Die Einrückungen sind kaputt, und `r0w` ist nicht `row`.
Schau Dir eines der Duzenden Beispiele hier im Forum an, wie das richtig gemacht wird.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Unknown123: Der Code kommt wegen einem Einrückungsfehler nicht einmal am Compiler vorbei.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblichweise in einer Funktion die `main()` heisst.

Funktionen und Methoden sollten alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. Dazu benötigt man bei GUI-Programmierung Closures und/oder Klassen. Um Klassen kommt man bei jeder nicht-trivialen GUI nicht herum.

Sternchen-Importe sind Böse™. Gerade bei `tkinter` holt man sich so hunderte von Namen ins Modul, von denen nur ein ganz kleiner Bruchteil tatsächlich verwendet wird. Es besteht auch die Gefahr von Namenskollisionen und man bekommt nicht nur Namen die im `tkinter`-Modul definiert sind, sondern auch alles was das `tkinter`-Modul seinerseits von irgendwo her importiert hat.

Die Fenstergrösse sollte man hier nicht selber vorgeben. Da ist viel zu viel leerer Platz. `grid()` sorgt doch schon dafür, dass das Fenster so gross ist, dass alles sichbar ist was man in dem Fenster angeordnet hat.

Im Hauprogramm wird der Inhalt vom `eingabefeld` abgefragt und an den Namen `entry_text` gebunden, aber der Name `entry_text` wird danach nirgends mehr verwendet.

Gleich nach dem erzeugen des Eingabefeldes wird der Inhalt abgefragt und an den Namen `hostname` gebunden. Das macht wenig Sinn, denn zu dem Zeitpunkt wo das ausgeführt wird, ist das Eingabefeld garantiert *leer*, denn da hatte der Benutzer noch gar keine Gelegenheit dort etwas einzutragen. Die bekommt er erst wenn die GUI-Hauptschleife läuft, also wenn `mainloop()` aufgerufen wurde.

Folglich kann man vor dem Aufruf von `mainloop()` auch nichts mit dem noch nicht eingegebenen Hostnamen anstellen. Also weder den Namen in Labeln anzeigen noch ohne einen Hostnamen ein Gerät anpingen. Beides wird aber unsinnigerweise versucht.

Dann erzeugst Du noch ein `Entry` und bindest das an den gleichen Namen: `eingabefeld`. Und dieses neue `Entry` wird dann angezeigt. Das alte `Entry` nicht, es wird also effektiv für nichts verwendet. Ich vermute mal Du hast das übersehen weil das Hauprogramm auf Modulebene steht und von einer Funktionsdefinition unterbrochen wird. Das ist halt auch sehr unübersichtlich und damit fehleranfällig.

An dem Code so wie die Zeilen angeordnet sind, kann man nur sehr mühsam ablesen wie die Anzeigeelemente am Ende angeordnet sind. Es ist sinnvoller die Zeilen so anzuordnen, dass man das besser erkennen kann was in welcher Reihenfolge und mit welchem Inhalt in der GUI landet.

`my_label` ist kein guter Name. Der `my_`-Präfix hat keinerlei Informationsgehalt. Das könnte auch einfach `label` heissen. Aber da das nicht wirklich noch mal gebraucht wird, kann man sich den Namen auch ganz sparen.

Und was soll der `welcom_`/`welcome_`-Präfix bedeuten? Der macht keinen Sinn. Beim `Button` macht `start_button` analog zum `exit_button` mehr Sinn und beim `Label` beispielsweise `ausgabe_label` als Gegenstück zum `eingabefeld`.

Der Name `button_action()` ist ziemlich generisch. `start_ping()` würde dem Leser deutlich mehr über den Inhalt/die Tätigkeit der Funktion verraten.

Da vor dem Aufruf der GUI-Hauptschleife noch gar kein Hostname eingegeben wurde, kann man den auch nicht in neu erstellten `Label`-Objekten verwennden. Man würde auch nicht pro möglichem Ausgang des Pings ein eigenes Label erstellen, sondern genau wie beim `welcome_label` *ein* `Label` erstellen und bei dem den Text ändern, nach dem man das Ergebnis vom Ping hat. Zudem würdest Du das in das Grid über das viel kleinere Label mit dem Text "Ziel: " setzen. Das gehört da wohl eher nicht in. Ich würde da einfach das `ausgabe_label` wiederverwenden.

Du würdest, wenn das so funktionieren würde, in beiden Fällen "… is up!" ausgeben.

Das ``ping`` wird aber auch mit einem leeren Hostnamen hier nie ausgeführt, weil da sehr komische, und sehr offensichtlich falsche Dinge getan werden.

Der Code der Pingt wird nur ausgeführt wenn ``eingabefeld == True`` gilt, was *nie* der Fall ist, denn `eingabefeld` ist ein `Entry`-Objekt und das kann niemals den Wert `True` haben. Was genau sollte dieses ``if`` denn bewirken?

`os.system()` sollte man nicht verwenden. Zum starten von externen Programmen gibt es das `subprocess`-Modul und darauf wird in der Dokumentation von `os.system()` auch hingewiesen. Das ist hier auch nicht ungefährlich weil der Benutzer da auch einen ”Hostnamen” eingeben kann der dann von der Shell des Betriebssystems als Code ausgeführt werden kann. Also beispielsweise könnte der Benutzer des Programms "localhost; rm -rf $HOME" eingeben, was dann alles im Heimatverzeichnis des gerade angemeldeten Systembenutzer löscht. Also zumindest unter Linux und MacOS. Aber auch unter Windows könnte man entsprechende Befehle von der Betriebssystemsshell ausführen lassen. Ausserdem kann man bei für die Shell syntaktisch falschen Benutzereingaben nicht unterscheiden ob einem das ``ping``-Programm einen Rückgabewert ≠0 liefert, oder die Shell. Alles Probleme die `subprocess.run()` (ohne ``shell=True``) nicht hat.

Der Code der den Ping ausführt darf erst laufen wenn der Benutzer den Startknopf gedrückt hat, also muss der in die Funktion die diesen Fall behandelt. Dann hatte der Benutzer vorher auch die Gelegenheit den Hostnamen einzugeben.

Um ``if``-Bedingungen gehören keine unnötigen Klammern.

Wenn vor dem Ping ein Text ausgegeben werden soll, der während des Pings angezeigt wird, dann braucht man noch eine Indirektion, denn Änderungen an der GUI die in einer Rückruffunktion getätigt werden, sieht man erst wenn diese Funktion wieder zur GUI-Hauptschleife zurückkehrt.

``ping`` ist leider nicht so richtig plattforunabhängig. Das Programm heisst zwar unter den grossen Betriebssystemen so, und nimmt auch den Hostnamen der angepingt werden soll als Argument, aber das Standardverhalten ist leider nicht überall gleich. Unter Linux pingt das ohne weitere Argumente beispielsweise unendlich lang, unter Windows nur vier mal. Wenn man das also unter Linux laufen lassen will, muss man die Anzahl per Option begrenzen. Die Option dafür funktioniert so allerdings dann wieder nicht unter Windows, weil die dort anders heisst ("/n" statt "-c").

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import tkinter as tk
from functools import partial


def do_ping(ausgabe_label, hostname):
    ping_result = subprocess.run(["ping", "-c", "3", hostname])
    host_state = "up" if ping_result.returncode == 0 else "down"
    ausgabe_label["text"] = f"{hostname} is {host_state}!"


def start_ping(eingabefeld, ausgabe_label):
    hostname = eingabefeld.get().strip()
    if hostname:
        ausgabe_label["text"] = f"Ping @ {hostname} läuft..."
        eingabefeld.after(10, do_ping, ausgabe_label, hostname)
    else:
        ausgabe_label["text"] = "Gib zuerst einen Namen ein."


def main():
    fenster = tk.Tk()
    fenster.title("Ping in tkinter")

    tk.Label(fenster, text="Ziel: ").grid(row=0, column=0)
    eingabefeld = tk.Entry(fenster, bd=5, width=40)
    eingabefeld.grid(row=0, column=1)

    start_button = tk.Button(fenster, text="Start")
    start_button.grid(row=1, column=0)
    tk.Button(fenster, text="Exit", command=fenster.quit).grid(row=1, column=1)

    ausgabe_label = tk.Label(fenster)
    ausgabe_label.grid(row=2, column=0, columnspan=2)

    start_button["command"] = partial(start_ping, eingabefeld, ausgabe_label)
    fenster.mainloop()


if __name__ == "__main__":
    main()
Das hat allerdings immer noch den Schöheitsfehler das die GUI blockiert, solange das ``ping``-Programm läuft. Nicht so schlimm für die ca. 3 Sekunden die das bei drei erfolgreichen Versuchen braucht, aber problematisch wenn die Domainnamensauflösung hängt und/oder das Ziel nicht oder nur sehr verzögert antwortet.

Da möchte man dann eigentlich `subprocess.Popen` verwenden um den Prozess nebenläufig zu starten und mit `after()` und der `poll()`-Methode auf das Ende warten. Und gegebenfalls auch den Prozess abbrechen. Da müsste man dann auch den Startknopf blockieren solange der Prozess läuft, oder zu einem Abbrechen-Knopf umfunktionieren. An der Stelle wäre dann für mich die Grenze erreicht an der man das noch sinnvoll ohne Klasse schreiben könnte.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Unknown123
User
Beiträge: 4
Registriert: Freitag 29. Mai 2020, 09:57

Vielen Dank für die tollen Tipps.
Ihr habt mir wirklich sehr geholfen. Ich programmiere noch nicht sehr lange und muss noch viel lernen.
Also danke jedenfalls.
Antworten