Python-Tk: Ein Neofetch-GUI selbst schreiben Teil 7

  Actionschnitzel   Lesezeit: 12 Minuten  🗪 3 Kommentare Auf Mastodon ansehen

Der 7. Teil einer Einführung in die Python3-Bibliothek Tkinter und der lange Weg zum perfekten GUI.

python-tk: ein neofetch-gui selbst schreiben teil 7

Bald geht es für mich in die Sommerpause, das heißt, es wird 3-4 Wochen keine Artikel von mir geben. Bis es so weit ist, gibt es aber noch zwei Artikel dieser Serie. Mein Ziel ist es also, bis dahin unser Tool so zusammenzuschrauben, dass etwas Ansehnliches dabei herauskommt. Pause kann man machen, wenn die Arbeit getan ist. Wir spucken also nochmal kräftig in die Hände und geben alles.

Es ist übrigens nicht so, dass ich eine fertige Version des Programms auf der Festplatte habe, aus der ich einfach nur Zeilen kopiere und beschreibe. Die Artikel entstehen parallel zur Software-Entwicklung.

Das war's jetzt aber mit der Einleitung. Auf geht's!

So sieht's aus

Wir können uns mal wieder auf die Schulter klopfen, denn wir haben viel geschafft.

Was steht heute an?

  • Shell auslesen
  • Bildschirmauflösung
  • CPU

Shell auslesen

Das können wir schnell abhandeln. Wenn Ihr, wie empfohlen, mal die Methode envron des Moduls os angeguckt habt, sollte euch das Folgende bekannt vorkommen.

os ist ja schon importiert. Dahingehend müssen wir nicht unternehmen, wir brauchen aber eine neue Variable.

Wie auch beim USER holen wir uns das key von SHELL

active_shell = os.environ["SHELL"]

Unter dem Kernel-Label legen wir ein neues Label an:

shell_label = tk.Label(stat_frame,text=f"Shell: {active_shell}")
shell_label.pack(anchor=tk.NW)

Der Output wir höchstwahrscheinlich so aussehen

/bin/bash # Oder zsh oder fish

Eigentlich wollen wir ja nur die Shell, aber hier wird auch der Pfad angezeigt. Das ist nicht weiter problematisch, darum kümmern wir uns im letzten Teil vor der Sommerpause, den Python hat noch so einige Methoden auf Lager um Daten zu manipulieren. Aber alles nach der Reihe.

Die Auflösung

Python hat von Haus aus so einige Features integriert, die wir einfach abrufen können. Um unsere Bildschirmauflösung herauszubekommen, nutzen wir winfo_screenwidth() und winfo_screenheight().

Test-Programm

import tkinter as tk

screen_width = winfo_screenwidth()
screen_height = winfo_screenheight()

root = tk.Tk()
root.title("Test")
root.geometry("300x300")

root.mainloop()

Wenn wir so wie immer an die Sache herangehen, sollte das Programm crashen:


Traceback (most recent call last):
  File "", line 3, in 
NameError: name 'winfo_screenwidth' is not defined

Wir müssen uns also etwas anderes überlegen.
Das Problem ist, dass winfo eine Methode des Fensters an sich ist und wir es nur so anwenden können:

screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()

Wenn wir das nun ändern, bekommen wir diesen Error:


Traceback (most recent call last):
  File "", line 3, in 
NameError: name 'root' is not defined

Das bedeutet nichts anderes, als dass Python root nicht kennt. Wir haben eine Methode abgerufen, bevor wir Python gesagt haben wir, was root eigentlich ist.

Neuer Versuch

Wir platzieren unsere zwei Variablen einfach unter root

import tkinter as tk

root = tk.Tk()
root.title("Test")
root.geometry("300x300")

screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()

print(screen_width)
print(screen_width)
root.mainloop()

Nun ist das Ganze von Erfolg gekrönt. Wir sind aber leider noch nicht fertig, denn irgendwann wollen wir ja auch Live-Daten auslesen und so eine Auflösung könnte sich ja unter Umständen verändern. Wir packen den Code in eine Definition, fügen ihn in unseren Code ein und schrieben ein neues Label:

def get_screen_size():
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    return f"{screen_width}x{screen_height}"

Diese Definition packen wir unter def get_screen_size.

res_label = tk.Label(stat_frame,text=f"Resolution: {get_screen_size()}")
res_label.pack(anchor=tk.NW)

Logik

  • Eine Definition liest den enthaltenen Code ein, speichert ihn, ruft diesen aber nicht sofort aber. Deswegen bekommen wir keine Fehlermeldung, obwohl sie vor rootsteht.
  • Mit return sagen wir aus, dass get_screen_size() bei Abfrage den String f"{screenwidth}x{screenheight}" ausgeben soll.
  • In unserem Label setzen wir mittels f-String get_screen_size() in den Text ein.

CPU

Hier müssen wir wieder nicht importieren, denn wir nutzen das bereits importierte psutil und dessen Methode cpu_freq.

Es kommen zwei neue Variablen hinzu

cpu_freq = psutil.cpu_freq()
cpu_core_count = psutil.cpu_count(logical=False)

cpu_freq beinhaltet mehrere Methoden .z.B: min, current und max.
Für unsere erste Version, die sich an Neofetch orientiert, benötigen wir nur max.
cpu_core_count macht genau das, was der Namen sagt: Es listet die Kerne der CPU auf. Wir benötigen nur die physischen, also die tatsächlichen Kerne, deswegen wird logical auf False gesetzt.

In unserem neuen CPU-Label sieht das ganze dann so aus:

cpu_label = tk.Label(stat_frame,text=f"CPU: ({cpu_core_count}) @ {cpu_freq.max:.2f} Mhz")
cpu_label.pack(anchor=tk.NW)

Logik

  • cpu_freq.max:.2f} Mhz liest die Maximal-Taktung der des Prozessors aus.
  • :.2f begrenzt die Zahl auf zwei Stellen nach dem Komma.
  • Die Ausgabe ist in Mhz

Der ganze Code

## Teil 7 ###

import tkinter as tk
from PIL import Image, ImageTk
import os
import socket
import distro
import platform
import psutil

def get_size(bytes, suffix="B"):
    """
    Scale bytes to its proper format
    e.g:
        1253656 => '1.20MB'
        1253656678 => '1.17GB'
    """
    factor = 1024
    for unit in ["", "K", "M", "G", "T", "P"]:
        if bytes < factor:
            return f"{bytes:.2f}{unit}{suffix}"
        bytes /= factor

def get_screen_size():
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    return f"{screen_width}x{screen_height}"

user = os.environ["USER"]
hostname = socket.gethostname()
os_release_pretty = distro.name(pretty=True)
kernel_release = platform.release()
svmem = psutil.virtual_memory()
cpu_freq = psutil.cpu_freq()
cpu_core_count = psutil.cpu_count(logical=False)
active_shell = os.environ["SHELL"]

# Erstelle das Hauptfenster
root = tk.Tk()
root.title("Neofetch-Tk")
root.geometry("800x500")

distro_logo = tk.PhotoImage(file="images/test.png")

# Einen Frame Zeichen
logo_frame = tk.Frame(root,background="yellow")
logo_frame.pack(fill="both",expand=False,side='left',padx=10,pady=10)

# Distro-Logo-Label
distro_icon = tk.Label(logo_frame,text="DISTRO LOGO",image=distro_logo,background="yellow")
distro_icon.pack(anchor=tk.NW)

# Einen Frame Zeichen
stat_frame = tk.Frame(root,background="cyan")
stat_frame.pack(fill="both",expand=True,side='left',padx=10,pady=10)

# Label mit Text USER@HOST
user_host_label = tk.Label(stat_frame,text=f"{user}@{hostname}")
user_host_label.pack(anchor=tk.NW)

# Label mit Text OS:
os_label = tk.Label(stat_frame,text=f"OS: {os_release_pretty}")
os_label.pack(anchor=tk.NW)

host_label = tk.Label(stat_frame,text=f"Host: {hostname}")
host_label.pack(anchor=tk.NW)

kernel_label = tk.Label(stat_frame,text=f"Kernel: {kernel_release}")
kernel_label.pack(anchor=tk.NW)

shell_label = tk.Label(stat_frame,text=f"Shell: {active_shell}")
shell_label.pack(anchor=tk.NW)

res_label = tk.Label(stat_frame,text=f"Resolution: {get_screen_size()}")
res_label.pack(anchor=tk.NW)

cpu_label = tk.Label(stat_frame,text=f"CPU: ({cpu_core_count}) @ {cpu_freq.max:.2f} Mhz")
cpu_label.pack(anchor=tk.NW)

mem_label = tk.Label(stat_frame,text=f"Memory: {(get_size(svmem.used))}/{get_size(svmem.total)}")
mem_label.pack(anchor=tk.NW)

# Starte die Hauptschleife
root.mainloop()

Ja, es füllt sich so langsam, wir sind schon bei 90 Zeilen Code und kein Ende in Sicht. Wie immer: Spielt ein bisschen mit den Möglichkeiten herum und nutzt die Suchmaschine eueres Vertrauens, um vielleicht noch Zusatzinfos zu finden.

Der Code auf Github

Bis nächste Woche.

Tags

Python, Tkinter, GUI, Neofetch

Christoph
Geschrieben von Christoph am 14. Juli 2024 um 12:48

Eine extrem coole Serie; vielen Dank!

Klaus
Geschrieben von Klaus am 29. September 2024 um 17:21

Wegen Krankheit hinke ich erheblich hinterher und komme jetzt erst dazu, die Folgen nachzuarbeiten. Glücklicherweise laufen sie ja nicht weg. Bei dieser Folge hat ich das Problem, dass cpu_freq in der cpu_label Zuweisung einen Fehler warf. Auf meinem System wurde dort cpu_freq als Funktionsaufruf interpretiert, was es ja nicht sein soll, sondern eine Variable. Umbenennen von cpu_freq (z.B. in cpu_frequence) war die Lösung.

Klaus
Geschrieben von Klaus am 29. September 2024 um 17:31

Nur zur Info: Ich benutze opensuse Tumbleweed, Python Version ist 3.11 Die zu importierenden Python Module heißen dort etwas anders. Meist genügt es, die explizite Pythonversion anzugeben, z.B. sudo zypper install phtyon311-distro, etc. Einzige Ausnahme bisher war PIL, von dem ich nur den Fork Pillow für Tumbleweed finden konnte. Der Assistant in Thonny erzählt mir aber etwas von dass die Module Image und ImageTk gar nicht verwendet würden. ... Line 7 : Unused Image imported from PIL It looks like the imported module or variable is not used. Line 7 : Unused ImageTk imported from PIL ... Das Programm läuft fehlerfrei ohne den Import der Methoden aus dem PIL Modul - zumindest soweit.