QtVCP ist eine Infrastruktur zum Erstellen von benutzerdefinierten CNC-Bildschirmen oder Bedienfeldern für LinuxCNC.

Es zeigt eine .ui-Datei an, die mit dem Qt Designer-Bildschirmeditor erstellt wurde, und kombiniert diese mit Python-Programmierung, um einen GUI-Bildschirm für den Betrieb einer CNC-Maschine zu erstellen.

QtVCP ist vollständig anpassbar: Sie können verschiedene Schaltflächen und Status-LEDs usw. hinzufügen oder Python-Code für eine noch feinere Anpassung einfügen.

1. Schaukasten

Einige Beispiele für mit QtVCP erstellte Bildschirme und virtuelle Bedienfelder:

QtDragon-Router
Abbildung 1. QtDragon - 3/4-Achsen-Beispiel
Qtscreen Fräse (engl. mill)
Abbildung 2. QtDefault - 3-Achsen-Beispiel
Qtscreen QtAxis
Abbildung 3. QtAxis - Beispiel für selbsteinstellende Achsen
Qtscreen Blender
Abbildung 4. Blender - 4-Achsen-Beispiel
Qtscreen x1mill
Abbildung 5. X1mill - 4-Achsen-Beispiel
Qtscreen x1mill
Abbildung 6. cam_align – Kameraausrichtung VCP
Test Panel
Abbildung 7. test_panel - Test Panel VCP

2. Übersicht

Zwei Dateien werden, einzeln oder in Kombination, verwendet, um Anpassungen vorzunehmen:

  • Eine UI-Datei, bei der es sich um eine XML-Datei handelt, die mit dem grafischen Editor Qt Designer erstellt wurde.

  • Eine Handler-Datei, die eine Textdatei mit Python-Code ist.

Normalerweise verwendet QtVCP die standardmäßige UI- und Handler-Datei, aber Sie können QtVCP so einstellen, dass es "lokale" UI- und Handler-Dateien verwendet.
Eine lokale Datei ist eine Datei, die sich im Konfigurationsordner befindet, der den Rest der Anforderungen des Rechners definiert.

Man ist nicht darauf beschränkt, ein benutzerdefiniertes Panel auf der rechten Seite oder eine benutzerdefinierte Registerkarte hinzuzufügen, da QtVCP den Qt Designer (den Editor) und PyQt5 (das Widget-Toolkit) nutzt.

QtVCP hat einige spezielle LinuxCNC Widgets und Aktionen hinzugefügt.
Es gibt spezielle Widgets, um Widgets von Drittanbietern mit HAL-Pins zu verbinden.
Es ist möglich, Widget-Antworten zu erstellen, indem man Signale mit Python-Code in der Handler-Datei verbindet.

2.1. QtVCP Widgets

QtVCP nutzt die PyQt5-Toolkits für die Einbeziehung von LinuxCNC.

Widget is the general name for user interface objects such as buttons and labels in PyQt5.

Es steht Ihnen frei, alle verfügbaren Standard-Widgets im Qt Designer-Editor zu verwenden.

Es gibt auch spezielle Widgets für LinuxCNC gemacht, dass die Integration einfacher zu machen.
Diese sind in drei Überschriften auf der linken Seite des Editors aufgeteilt:

  • Eine ist nur für HAL Widgets;

  • Eine ist für CNC-Steuerungs-Widgets;

  • Eine ist für Dialog-Widgets.

Es steht Ihnen frei, sie auf Ihrer Tafel beliebig zu mischen.

Ein sehr wichtiges Widget für die CNC-Steuerung ist das ScreenOptions-Widget: Es fügt dem Bildschirm nichts Visuelles hinzu, sondern ermöglicht die Auswahl wichtiger Details, die dann in der Handler-Datei kodiert werden müssen.

2.2. INI-Einstellungen

Wenn Sie QtVCP zur Erstellung eines CNC-Bewegungssteuerungsbildschirms (und nicht eines HAL-basierten Panels) verwenden, fügen Sie in der INI-Datei im Abschnitt [DISPLAY] eine Zeile mit folgendem Muster ein:

DISPLAY = qtvcp <Optionen> <Bildschirmname>
Anmerkung
Alle "<Optionen>" müssen vor "<Bildschirmname>" stehen.
Optionen
  • -d Debugging an.

  • -i Infoausgabe aktivieren.

  • -v Aktiviert die ausführliche Debug-Ausgabe.

  • -q Aktiviert nur die Fehler-Debug-Ausgabe.

  • -a Fenster immer in Vordergrund (engl. top) setzen.

  • -c NAME Name der HAL-Komponente. Standardmäßig wird der UI-Dateiname verwendet.

  • -g GEOMETRIE`Legt die Geometrie WIDTHxHEIGHT+XOFFSET+YOFFSET fest. Die Werte sind in Pixel-Einheiten, XOFFSET/YOFFSET wird vom linken oberen Bildschirmrand aus referenziert. Verwenden Sie -g WIDTHxHEIGHT, um nur die Größe zu bestimmen, oder -g +XOFFSET+YOFFSET, um nur die Position zu bestimmen. Beispiel: `-g 200x400+0+100

  • `-H DATEI`Führt HAL-Anweisungen aus DATEI mit halcmd aus, nachdem die Komponente eingerichtet und bereit ist.

  • -m Fenster maximieren.

  • -f Vollbild des Fensters.

  • -t THEME Standard ist das Systemdesign

  • -x XID Einbindung in ein X11-Fenster, das nicht die Integration unterstützt.

  • --push_xid Sendet die X11-Fenster-Identifikationsnummer von QtVCP an die Standardausgabe; zum Einbetten.

  • -u USERMOD Dateipfad einer Ersatz-Handler-Datei.

  • -o USEROPTS Übergibt einen String an die Handler-Datei von QtVCP unter der Listenvariablen self.w.USEROPTIONS_. Können mehrere -o sein.

<Bildschirm_name>

<Bildschirmname> ist der Basisname der .ui und _handler.py Dateien. Wenn <Bildschirmname> fehlt, wird der Standardbildschirm geladen.

QtVCP nimmt an, dass die UI-Datei und die Handler-Datei den gleichen Basisnamen verwenden. QtVCP sucht zunächst im LinuxCNC-Konfigurationsverzeichnis, das gestartet wurde, nach den Dateien, dann im System-Skin-Ordner mit den Standardbildschirmen.

Zykluszeiten
[DISPLAY]
CYCLE_TIME = 100
GRAPHICS_CYCLE_TIME = 100
HALPIN_CYCLE = 100

Stellt die Reaktionsgeschwindigkeit der GUI-Aktualisierungen in Millisekunden ein. Standardwert ist 100, nutzbarer Bereich 50 - 200.

Die Widgets, Grafiken und die HAL-Pin-Aktualisierung können separat eingestellt werden.

Wenn die Aktualisierungszeit nicht richtig eingestellt ist, kann der Bildschirm nicht mehr reagieren oder stark ruckeln.

2.3. Qt Designer UI Datei

Eine Qt Designer-Datei ist eine Textdatei, die im XML-Standard organisiert ist und das Layout und die Widgets des Bildschirms beschreibt.

PyQt5 verwendet diese Datei, um die Anzeige zu erstellen und auf diese Widgets zu reagieren.

Der Qt Designer Editor macht es relativ einfach, diese Datei zu erstellen und zu bearbeiten.

2.4. Handler-Dateien

Eine Handler-Datei ist eine Datei, die Python-Code enthält, der zu den QtVCP-Standardroutinen hinzugefügt wird.

Eine Handler-Datei erlaubt es, Voreinstellungen zu ändern oder einem QtVCP-Bildschirm Logik hinzuzufügen, ohne den Kerncode von QtVCP zu verändern. Auf diese Weise können Sie eigene Verhaltensweisen implementieren.

Falls vorhanden, wird eine Handler-Datei geladen. Es ist nur eine Datei erlaubt.

2.5. Bibliotheken Module

QtVCP, so wie es gebaut ist, tut wenig mehr als den Bildschirm anzuzeigen und auf Widgets zu reagieren. Für weitere vorgefertigte Verhaltensweisen gibt es verfügbare Bibliotheken (zu finden in lib/python/qtvcp/lib in RIP LinuxCNC install).

Libraries are prebuilt Python modules that add features to QtVCP. In this way you can select what features you want - yet don’t have to build common ones yourself.

Zu diesen Bibliotheken gehören:

  • audio_player

  • aux_program_loader

  • keybindings

  • message

  • preferences

  • notify

  • virtual_keyboard

  • machine_log

2.6. Themen

Designs sind eine Möglichkeit, das look and feel der Widgets auf dem Bildschirm zu ändern.

Zum Beispiel kann die Farbe oder Größe von Schaltflächen und Schiebereglern mit Hilfe von Themen geändert werden.

Das Windows-Thema ist der Standard für Bildschirme.
Das Systemthema ist der Standard für Bedienfelder.

Um die verfügbaren Themen zu sehen, können Sie sie mit dem folgenden Befehl in einem Terminal laden:

qtvcp -d -t <theme_name>

QtVCP kann auch mit Qt-Stylesheets (QSS) unter Verwendung von CSS angepasst werden.

2.7. Lokale Dateien

Falls vorhanden, werden die lokalen UI/QSS/Python-Dateien im Konfigurationsordner anstelle der Standard-UI-Dateien geladen.

Lokale UI/QSS/Python-Dateien ermöglichen es Ihnen, Ihre eigenen Designs anstelle der Standardbildschirme zu verwenden.

QtVCP sucht nach einem Ordner mit dem Namen <screen_name> (im Ordner für die Startkonfiguration, der die INI-Datei enthält).

In diesem Ordner lädt QtVCP jede der folgenden Dateien:

  • _<screen_name>_.ui,

  • <screen_name>_handler.py, und

  • _<screen_name>_.qss.

2.8. Veränderung mitglieferter Bildschirmmasken

Es gibt drei Möglichkeiten, einen Bildschirm/Panel anzupassen.

2.8.1. Kleinere Stylesheet-Änderungen

Stylesheets können zum Setzen von Qt-Eigenschaften verwendet werden. Wenn ein Widget Eigenschaften verwendet, können diese normalerweise durch Stylesheets verändert werden.

Beispiel für ein Widget mit den zugehörigen Style-Sheet-Einstellungen.
State_LED #name_of_led{
  qproperty-color: red;
  qproperty-diameter: 20;
  qproperty-flashRate: 150;
  }

2.8.2. Handler Patching - Subclassing Builtin Screens

We can have QtVCP load a subclassed version of the standard handler file. In that file we can manipulate the original functions or add new ones.
Subclassing just means our handler file first loads the original handler file and adds our new code on top of it - effectively a patch of changes.
This is useful for changing/adding behaviour while still retaining standard handler updates from LinuxCNC repositories.

You may still need to use the handler copy dialog to copy the original handler file to decide how to patch it. See custom handler file

Es sollte einen Ordner im Konfigurationsordner geben; für Bildschirme: benannt als <KONFIGURATIONSORDNER>/qtvcp/screens/<BILDSCHIRMNAME>/
Fügen Sie dort die Handler-Patch-Datei hinzu, benannt wie folgt <ORIGINAL BILDSCHIRMNAME>_handler.py,
d.h. für QtDragon würde die Datei qtdragon_handler.py genannt werden.

Hier ist ein Beispiel, um X-Achse Jog-Pins zu einem Bildschirm wie QtDragon hinzuzufügen:

import sys
import importlib
from qtvcp.core import Path, Qhal, Action
PATH = Path()
QHAL = Qhal()
ACTION = Action()

# besorge Referenz zu Original Handler Datei, um sie zu spezialisieren (engl. subclass)
sys.path.insert(0, PATH.SCREENDIR)
module = "{}.{}_handler".format(PATH.BASEPATH,PATH.BASEPATH)
mod = importlib.import_module(module, PATH.SCREENDIR)
sys.path.remove(PATH.SCREENDIR)
HandlerClass = mod.HandlerClass

# return our subclassed handler object to QtVCP
def get_handlers(halcomp, widgets, paths):
    return [UserHandlerClass(halcomp, widgets, paths)]

# sub class HandlerClass which was imported above
class UserHandlerClass(HandlerClass):
    # add a terminal message so we know this got loaded
    print('\nCustom subclassed handler patch loaded.\n')

    def init_pins(self):
        # Aufruf der original Handler init_pins Funktionen
        super().init_pins()

        # Hinzufügen Schnellauf pins X axis
        pin = QHAL.newpin("jog.axis.jog-x-plus", QHAL.HAL_BIT, QHAL.HAL_IN)
        pin.value_changed.connect(lambda s: self.kb_jog(s, 0, 1, fast = False, linear = True))

        pin = QHAL.newpin("jog.axis.jog-x-minus", QHAL.HAL_BIT, QHAL.HAL_IN)
        pin.value_changed.connect(lambda s: self.kb_jog(s, 0, -1, fast = False, linear = True))

2.8.3. Kleinere Änderungen am Python-Code

Eine weitere Python-Datei kann verwendet werden, um Befehle auf dem Bildschirm hinzuzufügen, nachdem die Handler-Datei geparst wurde. Dies kann für kleinere Änderungen nützlich sein, während die Standard-Handler-Updates aus den LinuxCNC-Repositorien weiterhin berücksichtigt werden.

Anmerkung
Handler Patching ist ein besserer Weg, um Änderungen hinzuzufügen - Instanz-Patching ist schwarzer magischer voodoo - dies ist hier nur der Nostalgie halber dokumentiert.

In der INI Datei unter der [DISPLAY] ` Überschrift fügen Sie hinzu *`USER_COMMAND_FILE = _PATH_*
PATH kann jeder gültige Pfad sein. Er kann ~ für das Heimatverzeichnis oder WORKINGDIRECTORY oder CONFIGDIRECTORY verwenden, um QtVCPs Vorstellung von diesen Verzeichnissen zu repräsentieren, z. B.:

[DISPLAY]
USER_COMMAND_FILE = CONFIGFOLDER/<Bildschirm_name_hinzugefuegte_Befehle>

Wenn kein Eintrag in der INI gefunden wird, sucht QtVCP im Standardpfad. Der Standardpfad befindet sich im Konfigurationsverzeichnis als versteckte Datei mit dem Basisnamen des Bildschirms und rc, d.h.: CONFIGURATION DIRECTORY/.<Bildschirmname>rc.

Diese Datei wird als Python-Code im handler-Dateikontext gelesen und ausgeführt.

Only local functions and local attributes can be referenced.
Global libraries defined in the screen’s handler file can be referenced by importing the handler file.
These are usually seen as all capital words with no preceding self.
self references the window class functions
self.w typically references the widgets

Was verwendet werden kann, mag je nach Bildschirm und Entwicklungszyklus variieren.

Ein einfaches Beispiel

Verweis auf das Hauptfenster, um den Titel zu ändern (wird nicht angezeigt, wenn INI-Einträge für die Titeländerung verwendet werden).

self.w.setWindowTitle('Mein Titel-Test')
Ein fortgeschrittenes Beispiel für das Patchen von Instanzen

Dies könnte mit der Handler-Datei des QtDragon-Bildschirms funktionieren.
Hier zeigen wir, wie man neue Funktionen hinzufügt und bestehende überschreibt.

# Benötigt für Instanz-Patch
# Referenz: https://ruivieira.dev/python-monkey-patching-for-readability.html
import types

# importiere das Handlerfile, um einen Verweis auf dessen Bibliotheken zu erhalten.
# benutze <Bildschirmname>_handler
import qtdragon_handler as hdlr

# Dies ist eigentlich eine unbeschränkte Funktion mit 'obj' als Parameter.
# Sie rufen diese Funktion ohne das übliche vorangestellte 'self' auf.
# Das liegt daran, dass sie nicht in die ursprüngliche Instanz der Handler-Klasse eingefügt wird.
# Sie wird nur von Code in dieser Datei aufgerufen.
def test_function(obj):
    print(dir(obj))

# Dies ist eine neue Funktion, die wir der bestehenden Handler-Klasseninstanz hinzufügen werden.
# Beachten Sie, dass sie die unbeschränkte Funktion mit 'self' als Parameter aufruft, 'self' ist der einzige verfügbare globale Referenz.
# Sie verweist auf die Fensterinstanz
def on_keycall_F10(self,event,state,shift,cntrl):
    if state:
        print ('F10')
        test_function(self)

# Dies wird verwendet, um eine bestehende Funktion in der bestehenden Handler-Klasseninstanz außer Kraft zu setzen.
# Beachten Sie, dass wir auch eine Kopie der ursprünglichen Funktion aufrufen.
# Dies zeigt, wie man eine bestehende Funktion um zusätzliche Funktionen erweitert.
def on_keycall_F11(self,event,state,shift,cntrl):
    if state:
        self.on_keycall_F11_super(event,state,shift,cntrl)
        print ('Hallo')

# Wir verweisen auf die KEYBIND-Bibliothek, die in der ursprünglichen Handler-Klasseninstanz instanziiert wurde
# durch Hinzufügen von 'hdlr.' (vom imp).
# Diese Funktion weist KEYBIND an, 'on_keycall_F10' aufzurufen, wenn F10 gedrückt wird
hdlr.KEYBIND.add_call('Key_F10','on_keycall_F10')

# Hier patchen wir die ursprüngliche Handler-Datei, um eine neue Funktion hinzuzufügen,
# die unsere neue Funktion (mit demselben Namen) aufruft, definiert in dieser Datei.
self.on_keycall_F10 = types.MethodType(on_keycall_F10, self)

# Hier definieren wir eine Kopie der ursprünglichen Funktion 'on_keycall_F11'.
# damit wir sie später aufrufen können. Wir können jeden gültigen, unbenutzten Funktionsnamen verwenden.
# Wir müssen dies tun, bevor wir die ursprüngliche Funktion überschreiben.
self.on_keycall_F11_super = self.on_keycall_F11

# Hier patchen wir die ursprüngliche Handler-Datei, um eine bestehende Funktion zu überschreiben,
# und so auf unsere neue Funktion (mit demselben Namen) zu verweisen, definiert in dieser Datei.
self.on_keycall_F11 = types.MethodType(on_keycall_F11, self)


# fügen sie einen neuen Pin dem Bildschirm hinzu:

# pin callback um den Status auszugeben
def new_pin_changed(data):
    print(data)

# Spezielle Funktion, die aufgerufen wird, bevor die HAL-Komponente bereit ist.
# Here we used the function to add a bit input pin with a callback
def after_override__(self):
    try:
        pin = hdlr.QHAL.newpin("new_pin", hdlr.QHAL.HAL_BIT, hdlr.QHAL.HAL_IN)
        pin.value_changed.connect(new_pin_changed)
    except Exception as e:
        print(e)

# Hier patchen wir die ursprüngliche Handler-Datei, um eine neue Funktion hinzuzufügen,
# die unsere neue Funktion (mit demselben Namen) aufruft, definiert in dieser Datei.
self.after_override__ = types.MethodType(after_override__, self)

2.8.4. Volle kreative Kontrolle mit benutzerdefinierten Handler/UI-Dateien

Wenn Sie einen Standardbildschirm mit voller Kontrolle verändern möchten, kopieren Sie dessen UI und Handler-Datei in Ihren Konfigurationsordner.

Es gibt ein QtVCP-Panel, das dabei hilft:

  • Öffnen Sie ein Terminal und führen den folgenden Befehl aus:

    qtvcp copy
  • Wählen Sie den Bildschirm und den Zielordner im Dialog

  • Wenn Sie Ihren Bildschirm anders benennen möchten als den Standardnamen des eingebauten Bildschirms, ändern Sie den Basisnamen im Bearbeitungsfeld.

  • Es sollte einen Ordner im Konfigurationsordner geben; für Bildschirme: mit dem Namen <CONFIG FOLDER>/qtvcp/screens/ für Panels: mit dem Namen <CONFIG FOLDER>/qtvcp/panels/ fügen Sie die Ordner hinzu, wenn sie fehlen, und kopieren Sie Ihre Ordner/Dateien hinein.

  • Bestätigen, um alle Dateien zu kopieren

  • Löschen Sie die Dateien, die Sie nicht ändern möchten, damit die Originaldateien verwendet werden.

3. VCP-Paneele

QtVCP kann verwendet werden, um Bedienfelder zu erstellen, die mit HAL verbunden sind.

3.1. Eingebaute Panels

Es sind mehrere integrierte HAL-Panels verfügbar.

Geben Sie in einem Terminal qtvcp <return> ein, um eine Liste zu sehen:

test_panel

Sammlung nützlicher Widgets zum Testen von HAL-Komponenten, einschließlich der Anzeige des LED-Zustands.

QtVCP HAL Test Integriertes Panel
Abbildung 8. QtVCP HAL Test Integriertes Panel
cam_align

Ein Kameraanzeige-Widget für die Rotationsausrichtung.

Qtscreen x1mill
Abbildung 9. cam_align – Kameraausrichtung VCP
sim_panel

Ein kleines Bedienfeld zur Simulation von MPG-Jogging-Steuerungen usw.
Für simulierte Konfigurationen.

QtVCP Sim Eingebautes Panel
Abbildung 10. QtVCP Sim Eingebautes Panel
vismach_mill_xyz

3D-OpenGL-Ansicht einer 3-Achsen-Fräsmaschine.

QtVismach - 3-Achsen-Fräse Eingebautes Panel
Abbildung 11. QtVismach - 3-Achsen-Fräse Eingebautes Panel

Sie können diese aus dem Terminal oder aus einer HAL-Datei mit diesem einfachen Befehl laden:

loadusr qtvcp test_panel

Aber typischerweise eher so:

loadusr -Wn test_panel qtvcp test_panel

Auf diese Weise wartet HAL bis die HAL-Pins gesetzt sind, bevor es weitergeht.

3.2. Benutzerdefinierte Bedienfelder

Sie können natürlich Ihr eigenes Panel erstellen und laden.

Wenn Sie eine UI-Datei mit dem Namen my_panel.ui und eine HAL-Datei mit dem Namen my_panel.hal erstellt haben, würden Sie diese dann von einem Terminal aus laden mit:

halrun -I -f my_panel.hal
Beispiel einer HAL-Datei, die ein QtVCP-Panel lädt
# Echtzeitkomponenten laden
loadrt threads
loadrt classicladder_rt

# Nicht-Echtzeit-Programme laden
loadusr classicladder
loadusr -Wn my_panel qtvcp my_panel.ui  # <1>

# Komponenten zum Thread hinzufügen
addf classicladder.0.refresh thread1


# Pins verbinden
net bit-input1 test_panel.checkbox_1 classicladder.0.in-00
net bit-hide test_panel.checkbox_4 classicladder.0.hide_gui

net bit-output1    test_panel.led_1             classicladder.0.out-00

net s32-in1        test_panel.doublescale_1-s   classicladder.0.s32in-00

# start thread
start
  1. In diesem Fall laden wir qtvcp mit -Wn, das wartet, bis das Panel das Laden beendet hat, bevor es mit der Ausführung des nächsten HAL-Befehls fortfährt.
    Damit soll gewährleistet werden, dass die vom Panel erstellten HAL-Pins tatsächlich fertig sind, falls sie im Rest der Datei verwendet werden.

4. Erstellen eines einfachen benutzerdefinierten Bildschirms

QtVCP Hässlicher benutzerdefinierter Bildschirm
Abbildung 12. QtVCP Hässlicher benutzerdefinierter Bildschirm

4.1. Übersicht

So erstellen Sie ein Bedienfeld oder einen Bildschirm:

  • Verwenden Sie Qt Designer, um ein Design zu erstellen, das Ihnen gefällt, und speichern Sie es in Ihrem Konfigurationsordner unter einem Namen Ihrer Wahl, der mit .ui endet

  • Ändern Sie die Konfigurations-INI-Datei, um QtVCP mit Ihrer neuen .ui-Datei zu laden.

  • Dann verbinden Sie alle erforderlichen HAL-Kenntnisse in einer HAL-Datei.

4.2. Holen Sie sich Qt Designer, um LinuxCNC-Widgets einzubinden

Qt Designer installieren

Zuerst müssen Sie den Qt Designer installieren.
Die folgenden Befehle sollten ihn zu Ihrem System hinzufügen, oder verwenden Sie Ihren Paketmanager, um dasselbe zu tun:

sudo apt-get install qttools5-dev-tools qttools5-dev libpython3-dev
Hinzufügen des Links qtvcp_plugin.py zum Qt Designer Suchpfad

Dann müssen Sie einen Link zu qtvcp_plugin.py in einem der Ordner hinzufügen, in denen Qt Designer suchen wird.

In einer RIP (engl. Abkürzung von "run in place", d.h. das Programm started dort wo es durch den Quellcode auch kompiliert wurde) Version von LinuxCNC wird qtvcp_plugin.py sein:

'~/LINUXCNC_PROJECT_NAME/lib/python/qtvcp/plugins/qtvcp_plugin.py'

Die installierte Paketversion sollte sein:

'usr/lib/python2.7/qtvcp/plugins/qtvcp_plugin.py' or
'usr/lib/python2.7/dist-packages/qtvcp/plugins/qtvcp_plugin.py'

Legen Sie einen symbolischen Link auf die obige Datei an und verschieben Sie sie an einen der Orte, an denen Qt Designer sucht.

Qt Designer sucht an diesen beiden Stellen nach Links (wählen Sie einen aus):

'/usr/lib/x86_64-linux-gnu/qt5/plugins/designer/python' or
'~/.designer/plugins/python'

Möglicherweise müssen Sie den Ordner plugins/python erstellen.

Starten Sie Qt Designer:
  • Für eine RIP-Installation:
    Öffnen Sie ein Terminal, setzen Sie die Umgebungsvariablen für LinuxCNC <1>, dann laden Sie Qt Designer <2> mit :

    . scripts/rip-environment   <1>
    designer -qt=5              <2>
  • Für eine Paketinstallation:
    Öffnen Sie ein Terminal und geben Sie ein:

    designer -qt=5

Wenn alles gut geht, wird Qt Designer gestartet und Sie werden die auswählbaren LinuxCNC Widgets auf der linken Seite sehen.

4.3. Erstellen Sie die .ui-Datei des Bildschirms

Erstellen des MainWindow Widgets

Wenn Qt Designer zum ersten Mal gestartet wird, erscheint ein 'New Form' Dialog.
Wählen Sie 'Main Window' und drücken Sie die Schaltfläche 'Create'.
Ein MainWindow-Widget wird angezeigt.

Wir werden diesem Fenster eine bestimmte, nicht veränderbare Größe geben:

Minimale und maximale Größe des Hauptfensters festlegen
  • Fassen Sie die Ecke des Fensters an und ändern Sie die Größe auf eine geeignete Größe, z. B. 1000x600.

  • Klicken Sie mit der rechten Maustaste auf das Fenster und klicken Sie auf Mindestgröße einstellen.

  • Wiederholen Sie dies und stellen Sie maximale Größe ein.

Unser Beispiel-Widget ist nun nicht mehr größenveränderbar.

Hinzufügen des Widgets ScreenOptions

Ziehen Sie das ScreenOptions-Widget per Drag-and-Drop an eine beliebige Stelle im Hauptfenster.

Dieses Widget fügt visuell nichts hinzu, richtet aber einige allgemeine Optionen ein.

Es wird empfohlen, dieses Widget immer vor allen anderen hinzuzufügen.

Klicken Sie mit der rechten Maustaste auf das Hauptfenster, nicht auf das "ScreenOptions"-Widget, und stellen Sie "Layout" auf "Vertikal", um "ScreenOptions" in voller Größe anzuzeigen.

Panel-Inhalt hinzufügen

Auf der rechten Seite befindet sich ein Panel mit Registerkarten für einen Eigenschaftseditor und einen Objektinspektor.

Klicken Sie im Objektinspektor auf ScreenOptions.
Wechseln Sie dann zum Eigenschaftseditor (engl. property editor) und schalten Sie unter der Überschrift ScreenOptions die filedialog_option um.

Ziehen Sie ein GCodeGraphics widget und ein GcodeEditor widget per Drag and Drop.
Platzieren Sie sie und ändern Sie die Größe, wie Sie es für richtig halten, und lassen Sie etwas Platz für Schaltflächen.

Action Buttons hinzufügen

Fügen Sie dem Hauptfenster 7 Aktionsschaltflächen hinzu.

Wenn Sie auf die Schaltfläche (engl. button) doppelklicken, können Sie Text hinzufügen.
Bearbeiten Sie die Schaltflächenbeschriftungen für "Notaus" (engl. "E-stop"), "Maschine ein", Referenzpunkt (engl. "Home"), "Laden" (engl. "Load"), "Ausführen" (engl. "Run"), "Pause" und "Stopp".

Aktionsschaltflächen sind standardmäßig auf keine Aktion eingestellt, daher müssen wir die Eigenschaften für definierte Funktionen ändern. Sie können die Eigenschaften bearbeiten:

  • direkt im Eigenschaften-Editor auf der rechten Seite des Qt-Designers, oder

  • praktischerweise lassen sich durch einen Doppelklick mit der linken Maustaste auf die Schaltfläche ein Dialogfeld "Eigenschaften" aufrufen, was die Auswahl von Aktionen ermöglicht, wobei nur die für die Aktion relevanten Daten angezeigt werden.

Wir werden zunächst den bequemen Weg beschreiben:

  • Klicken Sie mit der rechten Maustaste auf die Schaltfläche Maschine ein (engl.machine on) und wählen Sie Aktionen festlegen (engl. set actions).

  • Wenn das Dialogfeld angezeigt wird, verwenden Sie die Combobox, um zu MASCHINENSTEUERUNGEN - Maschine ein(engl. MACHINE CONTROLS - Machine On) zu navigieren.

  • In diesem Fall gibt es keine Option für diese Aktion, also wählen Sie "OK".

Jetzt schaltet die Taste das Gerät ein, wenn sie gedrückt wird.

Und nun der direkte Weg mit dem Eigenschaftseditor von Qt Designer:

  • Wählen Sie die Schaltfläche "Maschine ein".

  • Gehen Sie zum Eigenschaftseditor auf der rechten Seite von Qt Designer.

  • Blättern Sie nach unten, bis Sie die Überschrift ActionButton finden.

  • Klicken Sie auf das Kontrollkästchen für die Aktion "Machine_on", das Sie in der Liste der Eigenschaften und Werte sehen.

Die Taste steuert nun das Ein- und Ausschalten der Maschine.

Machen Sie das Gleiche für alle anderen Schaltflächen und fügen Sie noch einen hinzu:

  • Bei der Schaltfläche "Home" müssen wir auch die Eigenschaft joint_number auf 1 ändern.
    Dadurch wird der Controller angewiesen, alle Achsen und nicht nur eine bestimmte Achse zu referenzieren.

  • Mit dem Button "Pause":

    • Unter der Überschrift Indicated_PushButton überprüfen Sie die Indicator_option.

    • Unter der Überschrift QAbstactButton markieren Sie checkable.

Qt Designer: Auswahl der Eigenschaften des Pause-Buttons (Schaltfläche)
Abbildung 13. Qt Designer: Auswahl der Eigenschaften des Pause-Buttons (Schaltfläche)
.ui-Datei speichern

Diesen Entwurf müssen wir dann als tester.ui im Ordner sim/qtvcp speichern.

Wir speichern sie unter dem Namen tester, da dies ein Dateiname ist, den QtVCP erkennt und eine eingebaute Handler-Datei verwendet, um sie anzuzeigen.

4.4. Handler-Datei

Eine Handler-Datei ist erforderlich.

Er ermöglicht das Schreiben von Anpassungen in Python.

Zum Beispiel werden Tastatursteuerungen normalerweise in die Handler-Datei geschrieben.

In diesem Beispiel wird die eingebaute Datei tester_handler.py automatisch verwendet: Sie tut das Minimum, das erforderlich ist, um den in tester.ui definierten Bildschirm darzustellen und einfache Tastatureingaben vorzunehmen.

4.5. INI-Konfiguration

[DISPLAY] Abschnitt

Wenn Sie QtVCP zur Erstellung eines CNC-Steuerungsbildschirms verwenden, setzen Sie unter der Überschrift INI-Datei [DISPLAY]:

DISPLAY = qtvcp <Bildschirmname>

_<Bildschirmname>_ ist der Basisname der Dateien .ui und _handler.py.

In unserem Beispiel gibt es bereits eine Sim-Konfiguration namens tester, die wir zur Anzeige unseres Testbildschirms verwenden werden.

[HAL] Abschnitt

Wenn Ihr Bildschirm Widgets mit HAL-Pins verwendet, dann müssen Sie diese in einer HAL-Datei verbinden.

QtVCP sucht in der INI-Datei unter der Überschrift [HAL] nach den folgenden Einträgen:

POSTGUI_HALFILE=<Dateiname>

Der Konvention nach wäre <Dateiname> als +<Bildschirm_name>_postgui.hal+ genannt, aber es kann jeder legale Dateiname sein.
Sie können mehrere POSTGUI_HALFILE-Zeilen in der INI haben: jede wird nacheinander in der Reihenfolge ausgeführt, in der sie erscheint.
Diese Befehle werden nach der Erstellung des Bildschirms ausgeführt, um sicherzustellen, dass die HAL-Pins des Widgets verfügbar sind.

POSTGUI_HALCMD=<Befehl>

<Befehl> wäre jeder gültige HAL-Befehl.
Sie können mehrere POSTGUI_HALCMD-Zeilen in der INI haben: jede wird nacheinander in der Reihenfolge ausgeführt, in der sie erscheint.
Um zu garantieren, dass die HAL-Pins des Widgets verfügbar sind, werden diese Befehle ausgeführt:

  • nachdem der Bildschirm gebaut ist,

  • nachdem alle POSTGUI_HALFILEs ausgeführt wurden.

In unserem Beispiel gibt es keine HAL-Pins zu verbinden.

5. Handler-Datei im Detail

Handler-Dateien werden zur Erstellung von benutzerdefinierten Steuerelementen mit Python verwendet.

5.1. Übersicht

Hier ist ein Beispiel für eine Handler-Datei.

Es ist in Abschnitte unterteilt, um die Diskussion zu erleichtern.

############################
# **** IMPORT SECTION **** #
############################
import sys
import os
import linuxcnc

from PyQt5 import QtCore, QtWidgets

from qtvcp.widgets.mdi_line import MDILine as MDI_WIDGET
from qtvcp.widgets.gcode_editor import GcodeEditor as GCODE
from qtvcp.lib.keybindings import Keylookup
from qtvcp.core import Status, Action

# Set up logging
from qtvcp import logger
LOG = logger.getLogger(__name__)

# Set the log level for this module
#LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL

###########################################
# **** BIBLIOTHEKEN INSTANZIIEREN **** #
###########################################

KEYBIND = Keylookup()
STATUS = Status()
ACTION = Action()
###################################
# **** HANDLER CLASS SECTION **** #
###################################

class HandlerClass:

    ########################
    # **** INITIALIZE **** #
    ########################
    # Widgets ermöglicht den Zugriff auf Widgets aus den QtVCP-Dateien
    # An dieser Stelle werden die Widgets und HAL-Pins nicht instanziiert
    def __init__(self, halcomp,widgets,paths):
        self.hal = halcomp
        self.w = widgets
        self.PATHS = paths

    ##########################################
    # SPECIAL FUNCTIONS SECTION              #
    ##########################################

    # at this point:
    # the widgets are instantiated.
    # the HAL pins are built but HAL is not set ready
    # This is where you make HAL pins or initialize state of widgets etc
    def initialized__(self):
        pass

    def processed_key_event__(self,receiver,event,is_pressed,key,code,shift,cntrl):
        # when typing in MDI, we don't want keybinding to call functions
        # so we catch and process the events directly.
        # We do want ESC, F1 and F2 to call keybinding functions though
        if code not in(QtCore.Qt.Key_Escape,QtCore.Qt.Key_F1 ,QtCore.Qt.Key_F2,
                    QtCore.Qt.Key_F3,QtCore.Qt.Key_F5,QtCore.Qt.Key_F5):

            # search for the top widget of whatever widget received the event
            # then check if it is one we want the keypress events to go to
            flag = False
            receiver2 = receiver
            while receiver2 is not None and not flag:
                if isinstance(receiver2, QtWidgets.QDialog):
                    flag = True
                    break
                if isinstance(receiver2, MDI_WIDGET):
                    flag = True
                    break
                if isinstance(receiver2, GCODE):
                    flag = True
                    break
                receiver2 = receiver2.parent()

            if flag:
                if isinstance(receiver2, GCODE):
                    # if in manual do our keybindings - otherwise
                    # send events to G-code widget
                    if STATUS.is_man_mode() == False:
                        if is_pressed:
                            receiver.keyPressEvent(event)
                            event.accept()
                        return True
                elif is_pressed:
                    receiver.keyPressEvent(event)
                    event.accept()
                    return True
                else:
                    event.accept()
                    return True

        if event.isAutoRepeat():return True

        # ok if we got here then try keybindings
        try:
            return KEYBIND.call(self,event,is_pressed,shift,cntrl)
        except NameError as e:
            LOG.debug('Exception in KEYBINDING: {}'.format (e))
        except Exception as e:
            LOG.debug('Exception in KEYBINDING:', exc_info=e)
            print('Error in, or no function for: %s in handler file for-%s'%(KEYBIND.convert(event),key))
            return False

    ########################
    # CALLBACKS FROM STATUS #
    ########################

    #######################
    # CALLBACKS FROM FORM #
    #######################

    #####################
    # GENERAL FUNCTIONS #
    #####################

    # keyboard jogging from key binding calls
    # double the rate if fast is true
    def kb_jog(self, state, joint, direction, fast = False, linear = True):
        if not STATUS.is_man_mode() or not STATUS.machine_is_on():
            return
        if linear:
            distance = STATUS.get_jog_increment()
            rate = STATUS.get_jograte()/60
        else:
            distance = STATUS.get_jog_increment_angular()
            rate = STATUS.get_jograte_angular()/60
        if state:
            if fast:
                rate = rate * 2
            ACTION.JOG(joint, direction, rate, distance)
        else:
            ACTION.JOG(joint, 0, 0, 0)

    #####################
    # KEY BINDING CALLS #
    #####################

    # Machine control
    def on_keycall_ESTOP(self,event,state,shift,cntrl):
        if state:
            ACTION.SET_ESTOP_STATE(STATUS.estop_is_clear())
    def on_keycall_POWER(self,event,state,shift,cntrl):
        if state:
            ACTION.SET_MACHINE_STATE(not STATUS.machine_is_on())
    def on_keycall_HOME(self,event,state,shift,cntrl):
        if state:
            if STATUS.is_all_homed():
                ACTION.SET_MACHINE_UNHOMED(-1)
            else:
                ACTION.SET_MACHINE_HOMING(-1)
    def on_keycall_ABORT(self,event,state,shift,cntrl):
        if state:
            if STATUS.stat.interp_state == linuxcnc.INTERP_IDLE:
                self.w.close()
            else:
                self.cmnd.abort()

    # Linear Jogging
    def on_keycall_XPOS(self,event,state,shift,cntrl):
        self.kb_jog(state, 0, 1, shift)

    def on_keycall_XNEG(self,event,state,shift,cntrl):
        self.kb_jog(state, 0, -1, shift)

    def on_keycall_YPOS(self,event,state,shift,cntrl):
        self.kb_jog(state, 1, 1, shift)

    def on_keycall_YNEG(self,event,state,shift,cntrl):
        self.kb_jog(state, 1, -1, shift)

    def on_keycall_ZPOS(self,event,state,shift,cntrl):
        self.kb_jog(state, 2, 1, shift)

    def on_keycall_ZNEG(self,event,state,shift,cntrl):
        self.kb_jog(state, 2, -1, shift)

    def on_keycall_APOS(self,event,state,shift,cntrl):
        pass
        #self.kb_jog(state, 3, 1, shift, False)

    def on_keycall_ANEG(self,event,state,shift,cntrl):
        pass
        #self.kb_jog(state, 3, -1, shift, linear=False)

    ###########################
    # **** closing event **** #
    ###########################

    ##############################
    # required class boiler code #
    ##############################

    def __getitem__(self, item):
        return getattr(self, item)
    def __setitem__(self, item, value):
        return setattr(self, item, value)

################################
# required handler boiler code #
################################

def get_handlers(halcomp,widgets,paths):
     return [HandlerClass(halcomp,widgets,paths)]

5.2. IMPORT Bereich

Dieser Abschnitt ist für Import der erforderlichen Bibliotheksmodule für Ihren Bildschirm.

Es wäre typisch, die QtVCP-Bibliotheken keybinding, Status und Action zu importieren.

5.3. Abschnitt INSTANTIATE BIBRARIES

Indem wir die Bibliotheken hier instanziieren, erzeugen wir eine globale Referenz.

Sie können dies an den Befehlen erkennen, denen kein "self" vorangestellt ist.

Konventionell werden die Namen von global referenzierten Bibliotheken großgeschrieben.

5.4. HANDLER CLASS-Abschnitt

Der angepasste Code wird in einer Klasse platziert, damit QtVCP ihn verwenden kann.

Dies ist die Definition der Handler-Klasse.

5.5. INITIALIZE Abschnitt

Wie alle Python-Bibliotheken wird die +__init__+-Funktion aufgerufen, wenn die Bibliothek erstmals instanziiert wird.

Hier können Sie Standardwerte, Referenzvariablen und globale Variablen einrichten.

Die Referenzen der Widgets sind zu diesem Zeitpunkt nicht verfügbar.

Die Variablen halcomp, widgets und paths ermöglichen den Zugriff auf QtVCP’s HAL-Komponenten, Widgets bzw. Pfadinformationen.

5.6. Abschnitt zu BESONDEREN FUNKTIONEN

Es gibt mehrere spezielle Funktionen, nach denen QtVCP in der Handler-Datei sucht. Wenn QtVCP diese findet, ruft es sie auf, wenn nicht, ignoriert es sie stillschweigend.

class_patch__(self):

Class patching, auch bekannt als monkey patching, erlaubt es, Funktionsaufrufe in einem importierten Modul zu überschreiben.
Klassen-Patching muss vor der Instanziierung des Moduls durchgeführt werden und verändert alle danach erstellten Instanzen.
Ein Beispiel wäre das Patchen von Schaltflächenaufrufen aus dem G-Code-Editor, um stattdessen Funktionen in der Handler-Datei aufzurufen.

initialized__(self):

Diese Funktion wird aufgerufen, nachdem die Widgets und HAL-Pins erstellt wurden.
Sie können hier die Widgets und HAL-Pins manipulieren oder weitere HAL-Pins hinzufügen.
In der Regel gibt es

  • Einstellungen überprüft und eingestellt,

  • auf Widgets angewendete Stile,

  • Status von LinuxCNC verbunden mit Funktionen.

  • Tastenbelegungen würden hinzugefügt.

before_hal_init__(self):

This function is called before the HAL-ified widgets have their hal_init_ function called.
Some property changes need to be done before HAL_init is called on the widget.

after_override__(self):

Diese Funktion wird aufgerufen, nachdem die optionale Override-Datei geladen ist,
aber bevor die optionale HAL-Datei geladen oder HAL-Komponente bereit ist.

processed_key_event__(self,receiver,event,is_pressed,key,code,shift,cntrl):

Diese Funktion wird aufgerufen, um Tastatur-Jogging usw. zu erleichtern.
Durch die Verwendung der keybinding-Bibliothek kann dies verwendet werden, um einfach Funktionen hinzuzufügen, die an Tastendrücke gebunden sind.

keypress_event__(self,receiver, event):

Diese Funktion liefert rohe Tastendruckereignisse.
Sie hat _Vorrang vor dem verarbeiteten_Tastenereignis.

keyrelease_event__(receiver, event):

Diese Funktion gibt raw key release events aus.
Es hat Vorrang vor dem processed_key_event.

before_loop__(self):

Diese Funktion wird kurz vor dem Eintritt in die Qt-Ereignisschleife aufgerufen. Zu diesem Zeitpunkt sind alle Widgets/Bibliotheken/Initialisierungscodes abgeschlossen und der Bildschirm wird bereits angezeigt.

system_shutdown_request__(self):

Falls vorhanden, überschreibt diese Funktion die normale Funktion, die beim vollständigen Herunterfahren des Systems aufgerufen wird.
Sie kann dazu benutzt werden, vor dem Herunterfahren Hausarbeiten zu erledigen.

  • + Das Linux System wird nicht heruntergefahren, wenn Sie diese Funktion verwenden, Sie müssen das selbst tun.
    QtVCP/LinuxCNC beendet sich ohne eine Eingabeaufforderung, sobald diese Funktion zurückkehrt.

closing_cleanup__(self):

Diese Funktion wird kurz vor dem Schließen des Bildschirms aufgerufen. Sie kann verwendet werden, um vor dem Schließen aufzuräumen.

5.7. STATUS CALLBACKS Abschnitt

Konventionell würden Sie hier Funktionen unterbringen, die Rückrufe von STATUS-Definitionen sind.

5.8. CALLBACKS FROM FORM Abschnitt

Konventionell würden Sie hier Funktionen ablegen, die Rückrufe von den Widgets sind, die mit dem MainWindow im Qt Designer-Editor verbunden sind.

5.9. Abschnitt mit ALLGEMEINEN FUNKTIONEN

Konventionell werden hier die allgemeinen Funktionen untergebracht.

5.10. Abschnitt zur KEY BINDING (engl. für Tastenbelegung)

Wenn Sie die Keybinding-Bibliothek_ verwenden, platzieren Sie hier Ihre benutzerdefinierten Tastenaufrufroutinen.

Die Funktionssignatur ist:

def on_keycall_KEY(self,event,state,shift,cntrl):
    if state:
        self.do_something_function()

KEY ist der Code (aus der Keybindings-Bibliothek) für den gewünschten Schlüssel.

5.11. CLOSING EVENT Sektion

Wenn Sie die Funktion closeEvent hier einfügen, werden Schließungsereignisse abgefangen.

Dies replaces eine vordefinierte 'closeEvent'-Funktion von QtVCP.

def closeEvent(self, event):
    self.do_something()
    event.accept()
Anmerkung
Normalerweise ist es besser, die spezielle Funktion closing_cleanup__ zu verwenden.

6. Verbinden von Widgets mit Python-Code

Es ist möglich, Widgets über Signale und Slots mit Python-Code zu verbinden.

Auf diese Weise können Sie:

  • LinuxCNC-Widgets neue Funktionen geben, oder

  • Standard Qt-Widgets zur Steuerung von LinuxCNC verwenden.

6.1. Übersicht

In the Qt Designer editor:

  • Sie erstellen Benutzerfunktions-Slots

  • Sie verbinden die Slots mit Widgets, indem Sie Signale verwenden.

In the handler file:

  • Sie erstellen die Funktionen des Slots, die im Qt Designer definiert sind.

6.2. Hinzufügen von Slots mit Qt Designer

Wenn Sie Ihren Bildschirm in Qt Designer geladen haben, fügen Sie einen einfachen PushButton zu dem Bildschirm hinzu.
Sie könnten den Namen der Schaltfläche in etwas Interessantes wie "test_button" ändern.

Es gibt zwei Möglichkeiten, Verbindungen zu bearbeiten - Dies ist die grafische Methode.

  • In der oberen Werkzeugleiste von Qt Designer gibt es eine Schaltfläche zum Bearbeiten von Signalen. Wenn Sie auf die Schaltfläche klicken und sie gedrückt halten, wird ein Pfeil angezeigt (sieht aus wie ein Erdungssignal aus einem elektrischen Schaltplan).

  • Schieben Sie diesen Pfeil auf einen Bereich des Hauptfensters, in dem sich keine Widgets befinden.

  • Ein Dialogfeld „Verbindungen konfigurieren“ wird angezeigt.

    • Die Liste auf der linken Seite enthält die verfügbaren Signale des Widgets.

    • Die Liste auf der rechten Seite sind die verfügbaren Slots im Hauptfenster und Sie können sie ergänzen.

  • Wählen Sie das Signal clicked() - dies macht die Slot-Seite verfügbar.

  • Klicken Sie in der Slotliste auf "Bearbeiten" (engl. edit).

  • Ein Dialogfeld "Slots/Signale des Hauptfensters" wird angezeigt.

  • In der Slots-Liste oben befindet sich ein "+"-Symbol - klicken Sie darauf.

  • Sie können nun einen neuen Slotnamen bearbeiten.

  • Löschen Sie den Standardnamen slot() und ändern Sie ihn in test_button().

  • Drücken Sie die Taste OK.

  • Sie gelangen zurück zum Dialog Verbindungen konfigurieren.

  • Nun können Sie Ihren neuen Slot in der Slotliste auswählen.

  • Drücken Sie dann auf "OK" und speichern Sie die Datei.

Qt Designer-Signal/Slot-Auswahl
Abbildung 14. Qt Designer-Signal/Slot-Auswahl

6.3. Änderungen am Python-Handler

Nun müssen Sie die Funktion in die Handler-Datei einfügen.

Die Funktionssignatur lautet def slot_name(self):.

Für unser Beispiel fügen wir etwas Code hinzu, um den Namen des Widgets zu auszugeben:

def test_button(self):
    name = self.w.sender().text()
    print(name)

Fügen Sie diesen Code unter dem Abschnitt namens:

#######################
# Callbacks vom Formular
#######################

Tatsächlich spielt es keine Rolle, wo in der Handler-Klasse Sie die Befehle ablegen, aber per Konvention ist dies der Ort, an dem Sie sie ablegen müssen.

Speichern der Handlerdatei.

Wenn Sie nun Ihren Bildschirm laden und die Schaltfläche drücken, sollte der Name der Schaltfläche im Terminal angezeigt werden.

7. Mehr zum Thema