[CodeReview]Erste Python-Qt App

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

ich bitte euch mal wieder, falls jemand etwas Zeit übrig hat, meinen Code sehr kritisch anzuschauen und jeden möglich (noch so kleinen) Kritikpunkt zu melden.
Ich wollte einem Bekannten einen Gefallen tun und habe das gleich dazu genutzt, endlich mal ein GUI mit Qt zu erstellen. Vor 2(?) Jahren oder so, habe ich das schon mal versucht, das lief aber nur, weil __deets__ und __blackjack__ starke Nerven hatten. (Danke!) Jetzt habe ich mich noch mal, ohne Vorlage, rangesetzt, die GUI mit dem Creator zusammen geklickt und es ist ein Programm rausgekommen, das genau das macht, was ich will. Kann aber auch Zufall sein, ich bin gespannt.
Es geht darum Teile oder Baugruppen die von SolidWorks kommen in STEP-Dateien zu wandeln oder Zeichnungen von SolidWorks in PDF-Dateien zu wandeln. Es gibt viele Programme die das auch können oder man kann es auch für jede Datei manuell exportieren, wie man es von anderen Programmen kennt. Die Zusatzprogramme kosten viel Geld, können viel mehr(was man vielleicht nicht braucht) und die zweite Variante ist bei mehreren Dateien nervig. Ob und was am sinnvollsten ist, lasse ich mal so stehen, auf jeden Fall habe ich gesehen, das SolidWorks ein API hat und habe das mal umgesetzt.

Den Ablauf stelle ich mir so vor:
Der Benutzer darf alle Dateien im Format "sldprt", "sldasm" und "slddrw" auswählen, von denen er entweder STEP-Dateien oder PDF-Dateien benötigt. Um das so anwenderfreundlich wie möglich zu machen, ist es beim Auswahlprozess total egal, ob auch Dateien ausgewählt werden die nicht in STEP exportiert werden können, auch wenn man nur dieses Format will. Wenn man nach dem Auswählen merkt, das eine Datei zuviel drin ist, lässt sich die durch anwählen und "Entfernen"-Button wieder entfernen.
Es gibt zwei weitere Buttons: "PDF" und "STEP".
Wird auf den einen geklickt, werden die Dateien aus der Liste ausgesucht die in das Format gewandelt werden können und beim anderen gleich.
Ich will, dass die Dateien im gleichen Pfad wie die Ursprungsdatei liegt, allerdings in einem Ordner der nach dem Dateienformat benannt ist.

Ich habe mir überlegt, das es sein könnte, dass der Benutzer die Dateien auswählt, aus welchen Gründen auch immer eine der ausgewählten Dateien umbennen/löscht/verschiebt, daher prüfe ich vor dem exportieren noch, ob die Datei existiert und wenn nicht, starte ich den Export nicht, sondern öffne ein Popup-Fenster mit einer entsprechenden Meldung.

Unsicher bin ich mir bei der Klasse `SolidWorks`, weil eigentlich brauche ich die nicht. Ich habe, da ich nicht dauerhaft Zugang zu dem PC mit SolidWorks habe, erst den Code geschrieben, der nur die Dateien exportiert. Der bestand nur aus Funktionen, aber ich fand das schon irgendwie unübersichtlich, auch wenn es nicht so viele Argumente sind, die rumgereicht wurden. Deswegen jetzt die Klasse. Dann bin ich mir nicht sicher, ob ich die Instanz der Klasse vielleicht nur einmal beim öffnen des Programms hätte erstellen sollen, aber ich war mir nicht sicher wie sich das verhält, wenn man das Programm offen lässt und manuel mit SolidWorks arbeitet.

Habe ich irgendwelche Fehlerquellen übersehen?

Der Code:

Code: Alles auswählen

import sys
from contextlib import contextmanager
from functools import partial
from pathlib import Path
from queue import Empty, Queue
from threading import Thread

import pythoncom
import win32com.client
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QWidget

from ui_form import Ui_Widget

SUFFIX_TO_OPEN_METHODE = {".sldprt": 1, ".sldasm": 2, ".slddrw": 3}


class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)
        self.chosen_files = set()
        self.export_progress = Queue()
        self.ui.choose_file.clicked.connect(self.open_file_name_dialog)
        self.ui.remove_file.clicked.connect(self.remove_file_from_view)
        self.ui.create_pdf.clicked.connect(
            partial(self.export_files, [".slddrw"], ".pdf")
        )
        self.ui.create_step.clicked.connect(
            partial(self.export_files, [".sldprt", ".sldasm"], ".step")
        )

    def on_process(self, percentage, file=Path.home()):
        self.export_progress.put((percentage, file))

    def show_process_update(self):
        try:
            percent, progressing_file = self.export_progress.get()
        except Empty:
            self.ui.progress.setText("Starte SolidWorks")
        else:
            self.ui.progress_bar.setValue(percent)
            self.ui.progress.setText(f"Exporting: {progressing_file.name}")
            try:
                self.chosen_files.remove(progressing_file)
                self.update_file_view()
            except KeyError:
                pass
            if percent == 100:
                self.change_button_state(True)
                self.ui.progress.setText("Bereit")
                return
        QTimer.singleShot(10, self.show_process_update)

    def open_file_name_dialog(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_names, _ = QFileDialog.getOpenFileNames(
            self,
            "Datei(en) auswählen",
            str(Path.home()),
            "SolidWorks (*.slddrw *.sldprt *.sldasm)",
            options=options,
        )
        self.chosen_files.update(list(map(Path, file_names)))
        self.update_file_view()

    def update_file_view(self):
        self.ui.user_choice.clear()
        self.ui.user_choice.addItems([file.name for file in self.chosen_files])

    def remove_file_from_view(self):
        to_remove = self.ui.user_choice.currentItem()
        if to_remove is None:
            return
        for file in self.chosen_files:
            if to_remove.text() == file.name:
                self.chosen_files.remove(file)
                break
        self.update_file_view()

    def change_button_state(self, state):
        self.ui.create_step.setEnabled(state)
        self.ui.create_pdf.setEnabled(state)
        self.ui.remove_file.setEnabled(state)
        self.ui.choose_file.setEnabled(state)

    def search_missing_file(self):
        for file in self.chosen_files:
            if not file.exists():
                return file
        return

    def export_files(self, valid_file_formats, type_of_export):
        if not self.chosen_files:
            return
        missing_file = self.search_missing_file()
        if missing_file:
            self.show_warning(missing_file.name)
            return
        self.change_button_state(False)
        Thread(
            target=control_export_process,
            args=[
                self.chosen_files,
                valid_file_formats,
                type_of_export,
                self.on_process,
            ],
        ).start()
        self.show_process_update()

    @staticmethod
    def show_warning(file):
        pop_up = QMessageBox()
        pop_up.setIcon(QMessageBox.Warning)
        pop_up.setText(f'"{file}" wurde nicht gefunden')
        pop_up.setWindowTitle("Datei nicht gefunden")
        pop_up.setStandardButtons(QMessageBox.Ok)
        pop_up.exec()


def control_export_process(
    chosen_files, valid_file_formats, type_of_export, on_progress
):
    on_progress(0)
    solidworks = SoldiWorks()
    files_to_export = [
        file for file in chosen_files if file.suffix.lower() in valid_file_formats
    ]
    for number, file in enumerate(files_to_export, 1):
        on_progress(number // len(files_to_export) * 100, file)
        solidworks.file_path = file
        destination_path = file.parent / type_of_export[1:].upper()
        destination_path.mkdir(exist_ok=True)
        solidworks.destination_path = destination_path
        solidworks.export_file(type_of_export)


class SoldiWorks:
    def __init__(self):
        # 2023 is the SolidWorks version.
        self.solid_works = win32com.client.Dispatch(
            f"SldWorks.Application.{2023 - 2012 + 20}"
        )
        self.destination_path = None
        self.file_path = None

    def _activate_doc(self):
        return self.solid_works.ActivateDoc3(
            self.file_path.name,
            False,
            2,
            win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0),
        )

    @contextmanager
    def open_model(self):
        open_methode = SUFFIX_TO_OPEN_METHODE[str(self.file_path.suffix).lower()]
        try:
            yield self.solid_works.OpenDoc6(
                win32com.client.VARIANT(pythoncom.VT_BSTR, self.file_path),
                win32com.client.VARIANT(pythoncom.VT_I4, open_methode),
                win32com.client.VARIANT(pythoncom.VT_I4, 1),
                "",
                win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 2),
                win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 128),
            )
        finally:
            self._close_model()

    def _close_model(self):
        return self.solid_works.CloseDoc(str(self.file_path))

    @staticmethod
    def _save_as(model, export_file_path):
        return model.Extension.SaveAs2(
            str(export_file_path),
            0,
            1,
            win32com.client.VARIANT(pythoncom.VT_DISPATCH, None),
            "",
            win32com.client.VARIANT(pythoncom.VT_BOOL, 0),
            win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0),
            win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0),
        )

    def export_file(self, export_format):
        with self.open_model():
            model = self._activate_doc()
            export_file_path = (
                self.destination_path / f"{self.file_path.stem}{export_format}"
            )
            return self._save_as(model, export_file_path)


def main():
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
und die generierte ui_forms.py:

Code: Alles auswählen

# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'form.ui'
##
## Created by: Qt User Interface Compiler version 6.6.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
    QMetaObject, QObject, QPoint, QRect,
    QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
    QFont, QFontDatabase, QGradient, QIcon,
    QImage, QKeySequence, QLinearGradient, QPainter,
    QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidget, QListWidgetItem,
    QProgressBar, QPushButton, QSizePolicy, QWidget)

class Ui_Widget(object):
    def setupUi(self, Widget):
        if not Widget.objectName():
            Widget.setObjectName(u"Widget")
        Widget.resize(800, 600)
        self.create_step = QPushButton(Widget)
        self.create_step.setObjectName(u"create_step")
        self.create_step.setGeometry(QRect(450, 390, 331, 191))
        self.create_pdf = QPushButton(Widget)
        self.create_pdf.setObjectName(u"create_pdf")
        self.create_pdf.setGeometry(QRect(20, 390, 331, 191))
        self.choose_file = QPushButton(Widget)
        self.choose_file.setObjectName(u"choose_file")
        self.choose_file.setGeometry(QRect(540, 290, 211, 51))
        self.user_choice = QListWidget(Widget)
        self.user_choice.setObjectName(u"user_choice")
        self.user_choice.setGeometry(QRect(40, 20, 256, 192))
        self.remove_file = QPushButton(Widget)
        self.remove_file.setObjectName(u"remove_file")
        self.remove_file.setGeometry(QRect(330, 140, 81, 51))
        self.progress_bar = QProgressBar(Widget)
        self.progress_bar.setObjectName(u"progress_bar")
        self.progress_bar.setGeometry(QRect(40, 300, 341, 71))
        self.progress_bar.setValue(0)
        self.progress = QLabel(Widget)
        self.progress.setObjectName(u"progress")
        self.progress.setGeometry(QRect(40, 250, 341, 41))

        self.retranslateUi(Widget)

        QMetaObject.connectSlotsByName(Widget)
    # setupUi

    def retranslateUi(self, Widget):
        Widget.setWindowTitle(QCoreApplication.translate("Widget", u"SW-Helper", None))
        self.create_step.setText(QCoreApplication.translate("Widget", u"STEP", None))
        self.create_pdf.setText(QCoreApplication.translate("Widget", u"PDF", None))
        self.choose_file.setText(QCoreApplication.translate("Widget", u"Datei(en) ausw\u00e4hlen", None))
        self.remove_file.setText(QCoreApplication.translate("Widget", u"Entfernen", None))
        self.progress.setText(QCoreApplication.translate("Widget", u"Bereit", None))
    # retranslateUi

Falls es jemanden interessiert, die API:
https://help.solidworks.com/2023/Englis ... elcome.htm

Das Ansprechen von SolidWorks habe ich mir hier etwas abgeschaut, weil ich das auch noch nicht ganz verstanden habe:
https://github.com/ThomasNeve/pySldWrap/tree/main

Vielen Dank vorab!

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

Ohne in den Code zu schauen: .ui Dateien kann man direkt laden. Die muss man nicht vorher als .py-Dateien speichern. Das hat den Vorteil, dass man sie direkt wieder in den Designer laden kann und vor allen kommt man dann gar nicht erst in die Versuchung, da drin rumeditieren zu wollen.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Vielen Dank, das schaue ich mir an.
Die Struktur hat der Qt-Creator so erstellt, da habe ich das gar nicht in Frage gestellt. Es gab auch noch einen automatisch generierten Kommentar in dem stand mit welchen Befehlen man die ui-Datei manuell in eine .py-Datei wandelt. Ich habe das aber den Qt-Creator machen lassen und den Kommentar gelöscht.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

so das GUI soll auch etwas schön aussehen, nach dem ich Bild eingefügt habe und alles schön platziert habe, habe ich natürlich noch ein mal von vorne angefangen, weil ich das Layout vergessen hatte. Das habe ich jetzt nachgeholt und wenn ich die Fenstergröße ändere, dann ändern sich die Elemente in ihrer Größe und Position auch. Juhuu.

Ich würde gern die Spacer die ich drin habe gerne noch etwas ändern, aber das ist jetzt alles gesperrt. Ich muss Layout für Layout wieder "breaken" um da dran zu kommen. Ist das normal? Bis ich alles im endgültigen Layout habe, bin ich mir über die Größe und Anordnung noch gar nie sicher.

So sieht das jetzt aus und jetzt würde ich zum Beispiel die Buttons um die Datei zu öffnen und Dateien zu entfernen etwas kleiner machen und die zwei Export-Buttons etwas größer:
https://www.dropbox.com/scl/fi/8o826eyk ... b4xl0&dl=0

Kann man das hier über das Forum irgendwie in Worte fassen, wie ich vorgehen muss?

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich kann’s nicht erklären, wie man da gut vorher. Aber tatsächlich muss man bei Qt andauernd die Layouts aufbrechen. Ist etwas nervig, aber wenn man mal etwas am Stück damit arbeitet, gewöhnt man sich das an.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,


okay, vielen Dank. Habe es jetzt auch so hinbekommen, damit ich zufrieden bin.


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Habe mir gerade überlegt, ich könnte das Programm auf GitHub hochladen, vielleicht sucht jemand mal so was.

Muss ich da irgendwas mit Lizenzen beachten oder kann ich das einfach hochladen und fertig? Ich meine irgendwas, gerade in Verbindung mit Qt, müsste man beachten. Aber ich habe mich noch gar nie damit beschäftigt, wüsste auch gar nicht wo anfangen und falls es unnötig wäre, würde ich die Zeit lieber mit etwas anderem verbringen :D


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

ich nutze dieses Thema noch einmal, da die Frage die ich habe gut zu den Anfängen mit Qt passt.
Die hier geschriebene Anwendung funktioniert und es passt soweit alles. Wer sich an meinen Post von heute morgen erinnert, weis das ich gerade ein anderes GUI geschrieben habe, auch mit Python und Qt und bin da wieder auf Probleme mit der Darstellung gestoßen.
Die grafischen Elemente habe ich mir mit Qt-Creator zusammen geklickt, ich habe vertikale und horizontale Layouts benutzt und wenn ich die Anwendung starte und die Größe des Fensters ändere, dann ändert sich die Elemente darin mit, wie man es von richtigen Anwendungen kennt. Ich denke da habe ich nichts falsch gemacht.
Programmiert habe ich alles auf einem Linux-Laptop, das hat den Hintergrund, das ich da Admin bin und mich "entfalten" kann. Ich habe zwischen die Elemente teilweise Spacer eingebaut, damit die Darstellung so war, wie es mir gefallen hat. Dann habe ich das ganze auf einem Windows PC gestartet und es sah katastrophal aus. Ja anderer Monitor, andere Grafikkarte, aber ich dachte durch die Layouts wird sichergestellt, dass das überall, von mir aus überall mit dem gleichen Betriebssystem (falls das EInfluss darauf hat), gleich aussieht.

Muss ich Anwendungen für Windows unter Windows entwickeln? (Der grafische Teil)
Man kann bei den meisten Elementen sowas wie "Maximum", "Expanding", "Prefered" usw auswählen. Ich hab da viel rum probiert und kann es sein das ich mir damit sowas ähnliches wie `place` in `tkinter` eingebaut habe?

Ich habe zum Beispiel sowas:

Code: Alles auswählen

                ________
Durchmesser    |________| mm    
                ________
Sicherheit     |________|
Der zweite Wert hat keine Einheit, die Eingabefelder sollen aber gleich lang sein und bündig zueinander, dann muss ich da wo `mm` beim zweiten ein Spacer machen und wenn die Labels unterschiedlich lang sind, dann zwischen denen und den EIngabefelder auch wieder ein Spacer. Also so bin ich vorgegangen. Den Spacer habe ich auf `maximum` gestellt und dann die Länge eingetragen, bis es gepasst hat. Die Labels und Eingabefelder habe ich horizontal auch auf `maximum` gestellt. Falls es wichtig ist, das Ganze ist in einem `QStackedWidget`.

Das ist so ähnlich wie die Frage weiter oben, ich weis nicht ob man das in einem Forum beantworten kann, aber ich wollte es mal versuchen. Ich schaue nachher nach ob ich das Projekt hier auch öffnen kann, dann kann ich nur mehr Details liefern, falls benötigt.

Das ist noch so ein wildes probieren, vielleicht kann man es grob erklären?

Schon mal danke, alleine schon falls sich jemand den Text durch gelesen hat.🫣

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Whitie
User
Beiträge: 216
Registriert: Sonntag 4. Juni 2006, 12:39
Wohnort: Schulzendorf

Ich bin jetzt nicht der Qt Profi, aber das gewünschte sollte mit einem Gridlayout ganz ohne Spacer möglich sein.

Viele Grüße
Whitie
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die Antwort.
Ich habe es gerade getestet und es sieht unter Linux und Windows gut aus.

Jetzt finde ich das nicht mehr, weis aber nicht mehr sicher ob ich es gelesen oder in einem Video gesehen habe, aber ich hatte im Hinterkopf, das man eigentlich immer vertikale und horizontale Layouts verwendet und damit Grid überflüssig wird. Aber hier sehe ich jetzt einen Vorteil von Grid, zumindest war das jetzt total easy.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten