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

  Actionschnitzel   Lesezeit: 25 Minuten  🗪 3 Kommentare Auf Mastodon ansehen

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

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

Grüße,

ich würde sagen, legen sofort los und beginnen mit einer Bestandsaufnahme.

Die Tabelle zeigt, dass wir 8 von 16 Funktionen schon implementiert haben, wobei es 3 gibt, die noch Nacharbeitung benötigen.

Zu erledigen sind noch Distro Logo, Uptime, Packages, DE, WM, WM-Theme, Icons und Terminal.

Distro Logo Nur Platzhalter
USER & HOST Check Sollte keine Probleme machen
OS Check Sollte keine Probleme machen
Host Check Muss angepasst werden
Kernel Check Sollte keine Probleme machen
Uptime
Packages
Shell Check Muss angepasst werden
Resolution Check Sollte keine Probleme machen
DE
WM
WM-Theme
Icons
Terminal
CPU Check Sollte keine Probleme machen
Memory Check Sollte keine Probleme machen

Rückmeldungen

Mir ist natürlich von Anfang an bewusst gewesen, dass dieses Tutorial nicht alle Linux-Nutzer abholen kann. Das ist dem Grund geschuldet, dass ich hier teils Debian-Befehlen arbeite. Es besteht ja die Möglichkeit eine virtuelle Python Umgebung zu schaffen, die ohne die Bibliotheken einer bestimmten Distro funktioniert. Natürlich will ich niemanden ausschließen.

Die Debian-Basis ist nun mal am weitesten verbreitet und kein Python-Einsteiger-Tutorial der Welt fängt an mit der Erklärung, wie man den Python-Paket-Manager nutzt. Ich werde mir aber in der Sommerpause Gedanken machen, wie/wann ich diesen am besten einführe. 

Uptime

Wir wollen also herausfinden, wie lange unser PC schon an ist. Da hier wieder Mathematik und tiefergehendes Verständnis nötig ist, legen wir ein Test-File an.

Wir importieren:

import psutil
import datetime

Mehr zu datetime

Nur machen wir den Zeitpunkt, an dem der PC gestartet ist. Von psutil nutzen wir die Methode boot_time.

import psutil
import datetime

unix_timestamp = psutil.boot_time()
print(unix_timestamp)

Das Ergebnis sieht bei mir so aus:

1720781358.0

psutil.boot_time() gibt die Zeit als Unix-Zeitstempel zurück, zu der das System gestartet wurde. Um diese Zahl besser verständlich auszudrücken, nutzen wir datetime.

boot_time = datetime.datetime.fromtimestamp(unix_timestamp)

Dieses Modul ist etwas verwirrend. Man nutzt die Methode frometimestamp aus Methode datetime des Moduls datetime und geb unseren unix_timestamp hinein.

Das Resultat 

import psutil
import datetime

unix_timestamp = psutil.boot_time()
boot_time = datetime.datetime.fromtimestamp(unix_timestamp)

print(boot_time)
>>> 
2024-07-12 12:49:18

Jetzt wissen wir genau, wann ich meinen PC angeschaltet habe. Nun müssen wir noch herausfinden welches Datum um wie viel Uhr es gerade in diesem Augenblick sind.

now = datetime.datetime.now()

Jetzt kommt ganz simple Subtraktion zum Einsatz

uptime = now - boot_time

[Jetzt] - (MINUS) [Wann wurde der PC angeschaltet

Der ganze Test-Code

import psutil
import datetime

unix_timestamp = psutil.boot_time()
boot_time = datetime.datetime.fromtimestamp(unix_timestamp)

now = datetime.datetime.now()
uptime = now - boot_time

print(f"Startzeit: {boot_time}")
print(f"Jetzt: {now}")
print(f"Uptime: {uptime}")
>>>
Startzeit: 2024-07-12 12:49:18
Jetzt: 2024-07-12 17:49:43.825505
Uptime: 5:00:25.825505

In Main einbinden

Wie auch im Test-Skript importieren wir datetime

import datetime

Da wir ja irgendwann Live-Daten auslesen wollen, habe ich die UPTIME in eine Funktion gepackt und ein paar Änderungen vorgenommen, um die Lesbarkeit zu verbessern.

def get_sys_uptime():

    # System-Startzeit ermitteln
    boot_time_timestamp = psutil.boot_time()
    boot_time = datetime.datetime.fromtimestamp(boot_time_timestamp)

    # Aktuelle Zeit
    now = datetime.datetime.now()

    # Uptime berechnen
    uptime = now - boot_time

    # Uptime in Stunden und Minuten umrechnen
    uptime_hours, remainder = divmod(uptime.total_seconds(), 3600)
    uptime_minutes = remainder // 60

    return f"{int(uptime_hours)} h , {int(uptime_minutes)} m"

Es wird mal wieder etwas komplexer. Der Wert aus uptime wird hier in Stunden und Minuten umgewandelt. Was übrigbleibt, wird durch 60 geteilt. Wird die Funktion abgerufen, soll "X h, X m" ausgegeben werden.

uptime_hours, remainder = divmod(uptime.total_seconds(), 3600)
uptime_minutes = remainder // 60

return f"{int(uptime_hours)} h , {int(uptime_minutes)} m"

Logik

  • Eine Stunde hat 3600 Sekunden.
  • divmod Teil den Wert aus uptime._total_seconds.
  • Was bleibt sind Stunde und ein Rest
  • Der Rest wird durch 60 geteilt
  • Ergibt der Rest eine oder mehrere ganze Minuten, landet in uptime_minutes
  • über return findet die Ausgabe statt
  • Da wir ganze Zahlen wollen, aber eine Kommazahl bekommen(float), nutzen wir int um die Zahlen zu konvertieren
  • Das geht aber nur, weil wir durch divmod ganze Zahlen z. B. 10.0 zurückbekommen

Unter dem kernel_label machen wir etwas Platz frei und schreiben wie gewohnt ein Label, dessen Text den Inhalt der Funktion wiedergibt.

uptime_label = tk.Label(stat_frame,text=f"Uptime: {get_sys_uptime()}")
uptime_label.pack(anchor=tk.NW)

Der ganze Code in Main

## Teil 8 ###

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

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}"

def get_sys_uptime():

    # System-Startzeit ermitteln
    boot_time_timestamp = psutil.boot_time()
    boot_time = datetime.datetime.fromtimestamp(boot_time_timestamp)

    # Aktuelle Zeit
    now = datetime.datetime.now()

    # Uptime berechnen
    uptime = now - boot_time

    # Uptime in Stunden und Minuten umrechnen
    uptime_hours, remainder = divmod(uptime.total_seconds(), 3600)
    uptime_minutes = remainder // 60

    return f"{int(uptime_hours)} h , {int(uptime_minutes)} m"

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)

uptime_label = tk.Label(stat_frame,text=f"Uptime: {get_sys_uptime()}")
uptime_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()

Bye, Bye Platzhalter-Logo

Auch hier gehen wir es langsam an. Wir möchten ja das korrekte Logo unterer Distribution angezeigt bekommen. Wie stellen wir das jetzt an? Um die Basis herauszufinden, können wir wieder ein Modul nutzen.

sudo apt install python3-distro

In manchen Fällen ist distro aber auch schon vorinstalliert. Nutzen können wir es wie folgt.

import distro

print(f"id: {distro.id()}")
print(f"name: {distro.name()}")
print(f"version: {distro.version()}")
print(f"codename: {distro.codename()}")
>>> 
id: ubuntu
name: Ubuntu
version: 24.04
codename: noble

Ich habe bei Distrosea so einige Distributionen durchgesehen und leider stand in den release-Files immer nur die Basis. Würden wir den obigen Code so nutzen könnten wir also nur auslesen, dass wir ein Ubuntu nutzen aber nicht ein Kubuntu oder Xubuntu. Linux Mint bildet übrigens eine Ausnahme. Sucht man die ID steht dort nicht ubuntu, sondern linuxmint.

Natürlich ist es schöner gleich das korrekte Logo zu sehen, um zu lernen, wie wir das überhaupt anstellen, gehen wir erstmal nur mit den Logos der Distro-Basis. 

Ach und Siduction habe ich nicht vergessen! Das ist aber ein Sonderfall. Hier müssen wir den Inhalt einer Datei auslesen. Dazu benötigt man with open. Das muss ich auch erst erklären. (nach der Pause). 

Zunächst brauchen wir überhaupt mal Logos. Ich habe uns welche vorbereitet. Ihr könnt sie einfach auf Github herunterladen. 

PNGs auf Github

Die PNGs sollten wie folgt in die Ordnerstruktur eingegliedert werden.

~/neofetch-tk$ tree
.
├── images
│   ├── arch_logo_350.png
│   ├── debian_logo_350.png
│   ├── fedora_logo_350.png
│   ├── mint_logo_350.png
│   ├── osuse_logo_350.png
│   ├── test.png
│   └── ubuntu_logo_350.png
└── main.py

Wir gehen in unser main.py-File und importieren distro und fügen folgenden Code zu den anderen Variablen hinzu.

distro_id = distro.id()

Nach dem Vorbild unseres Platzhalters erstellen wir nun Variablen, die Logos beinhalten.

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

Neue Logos:

distro_logo = tk.PhotoImage(file="images/test.png")
arch_logo = tk.PhotoImage(file="images/arch_logo_350.png")
debian_logo = tk.PhotoImage(file="images/debian_logo_350.png")
mint_logo = tk.PhotoImage(file="images/mint_logo_350.png")
suse_logo = tk.PhotoImage(file="images/osuse_logo_350.png")
ubuntu_logo = tk.PhotoImage(file="images/ubuntu_logo_350.png")
fedora_logo = tk.PhotoImage(file="images/fedora_logo_350.png")

Der Übersichtlichkeit halber bauen wir für die Nutzung eine Funktion, diese soll am Ende des Scripts ausgeführt werden.

def get_distro_logo():
    if distro_id == "debian":
        distro_icon.configure(image=debian_logo)
    elif distro_id == "arch":
        distro_icon.configure(image=arch_logo)        
    elif distro_id == "linuxmint":
        distro_icon.configure(image=mint_logo)
    elif distro_id == "ubuntu":
        distro_icon.configure(image=ubuntu_logo)
    elif distro_id == "opensuse":
        distro_icon.configure(image=osuse_logo)
    else:
        distro_icon.configure(image=distro_logo)

[...]

get_distro_logo()

# Starte die Hauptschleife
root.mainloop()

Logik

  • Wenn(if) distro_id genau "debian" entspricht, wird mit der Methode configure das image durch debian_logo ersetzt
  • Ist dem nicht so, werden die anderen Fälle(elif) durchgegangen, bis etwas Passendes gefunden wird
  • Ist keine Annahme zutreffen (else:) wird unser Testbild gesetzt.

Der ganze Code(Mit aktualisierten Kommentaren)

## Teil 8 ###

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

# Macht die RAM-Größe lesbar
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

# Findet die Auflösung heraus
def get_screen_size():
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    return f"{screen_width}x{screen_height}"

def get_sys_uptime():

    # System-Startzeit ermitteln
    boot_time_timestamp = psutil.boot_time()
    boot_time = datetime.datetime.fromtimestamp(boot_time_timestamp)

    # Aktuelle Zeit
    now = datetime.datetime.now()

    # Uptime berechnen
    uptime = now - boot_time

    # Uptime in Stunden und Minuten umrechnen
    uptime_hours, remainder = divmod(uptime.total_seconds(), 3600)
    uptime_minutes = remainder // 60

    # Weist an das diese Funktion Stunden und Minuten ausgeben soll
    return f"{int(uptime_hours)} h , {int(uptime_minutes)} m"

# Setzt das korrekte Logo für die Distro
def get_distro_logo():
    if distro_id == "debian":
        distro_icon.configure(image=debian_logo)
    elif distro_id == "arch":
        distro_icon.configure(image=arch_logo)        
    elif distro_id == "linuxmint":
        distro_icon.configure(image=mint_logo)
    elif distro_id == "ubuntu":
        distro_icon.configure(image=ubuntu_logo)
    elif distro_id == "opensuse":
        distro_icon.configure(image=osuse_logo)
    elif distro_id == "fedora":
        distro_icon.configure(image=fedora_logo)
    else:
        distro_icon.configure(image=distro_logo)

# Vars für die Labels
# Ließt den User aus
user = os.environ["USER"]
# Ließt den Host aus
hostname = socket.gethostname()
# Ließt den Pretty Name  aus
os_release_pretty = distro.name(pretty=True)
# Ließt den Kernel aus
kernel_release = platform.release()
# Basis um den RAM auszulesen
svmem = psutil.virtual_memory()
# Basis um CPU-Werte auszulesen
cpu_freq = psutil.cpu_freq()
# Ließt Anzahl der CPU-Kerne aus
cpu_core_count = psutil.cpu_count(logical=False)
# Gibt die aktuelle Shell aus
active_shell = os.environ["SHELL"]
# Gibt die Distro-ID aus
distro_id = distro.id()

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

# Distro Logos
distro_logo = tk.PhotoImage(file="images/test.png")
arch_logo = tk.PhotoImage(file="images/arch_logo_350.png")
debian_logo = tk.PhotoImage(file="images/debian_logo_350.png")
mint_logo = tk.PhotoImage(file="images/mint_logo_350.png")
suse_logo = tk.PhotoImage(file="images/osuse_logo_350.png")
ubuntu_logo = tk.PhotoImage(file="images/ubuntu_logo_350.png")
fedora_logo = tk.PhotoImage(file="images/fedora_logo_350.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)

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

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

# Label mit Text Uptime:
uptime_label = tk.Label(stat_frame,text=f"Uptime: {get_sys_uptime()}")
uptime_label.pack(anchor=tk.NW)

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

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

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

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

# Führt get_distro_logo aus
get_distro_logo()

# Starte die Hauptschleife
root.mainloop()

Das war's für heute. Nächstes Mal machen wir alles "schön".

Bis nächste Woche.

Tags

Python, Tkinter, GUI, Neofetch

Claudia
Geschrieben von Claudia am 19. Juli 2024 um 15:39

Wäre es nicht besser die ganzen IF-Statements in einem nur zu machen in Deiner Funktion: def get_distro_logo()

def get_distro_logo(distro_name):
    image_path = &quot;images/{0}_logo_350.png&quot;.format(distro_name)
    if os.path.isfile(image_path):
        distro_icon.configure(image=tk.PhotoImage(file=image_path))
    else:
        distro_icon.configure(image=tk.PhotoImage(file=&quot;images/test.png&quot;))

Dann wäre der Funktionaufruf get_distro_logo(distro_id), ansonsten könnte man auch distro_name als Parameter wegwerfen und bei dem .format(distro_name) gleich .format(distro_id) einsetzen, um den Parameter zu sparen, falls man unbedingt ohne Parameter arbeiten möchte.

Achso für die Leute, die die python3 formatierung mehr mögen hier die alternative zur format Funktion f&quot;images/{distro_name}_logo_350.png&quot;

Jedenfalls würde ich dies als einfachen Code ansehen, als deine IF-Kombinationen, da es bestimmt auch sehr viel schreib bzw. Copy-Paste Arbeit war.

Freue mich schon auf Dein nächsten Post zum Thema.

Actionschnitzel
Geschrieben von Actionschnitzel am 19. Juli 2024 um 16:00

Klar, das geht auch. Sieht auch schöner aus. 'isfile' hatten wir nur noch nicht. Der Code ist ja erstmal auch nicht auf Schönheit und Effizienz gemünzt. Es soll ja so einfach wie möglich zeigen, wie man mit "if/elif/else" arbeiten kann. Im späteren Verlauf wird der Code dann sowieso irgendwann komplett über den Haufen geworfen. Ich merke mir deine Herangehensweise und werde das einbauen, wenn wir nochmal die Zügel anziehen.

Claudia
Geschrieben von Claudia am 20. Juli 2024 um 14:52

Hallo ich bin es nochmal. Ich habe mir mal Dein Code geschnappt und auf (Codeberg.Org/claudia_33155/Neofetch)[https://codeberg.org/claudia_33155/Neofetch] gepackt und dort dann meine Python Kenntnisse einfließen lassen. Damit Du mal siehst, wie ich den Code geschrieben hätte. Ich mag sauberen Code mehr irgendwie - Irgendwie habe ich da eine kleine Macke entwickelt. Aber ich möchte es halt Teilen und sehen, ob andere es genauso sehen oder nicht. Gemeinschaftliches Lernen ist schöneres Lernen als alleine. ^^