#!/usr/bin/env python3 """ project: GarageCalc1 file: main.py summary: Calculates available size of a garage space license: GPL author: Paul Salajean (p.salajean[at]gmx.de) """ # Standard library imports import sys import os import csv # Third party imports from PySide2.QtWidgets import QApplication, QMainWindow, QTableWidgetItem, QStatusBar, QAction, QFileDialog, \ QAbstractItemView, QMenu, QMessageBox from PySide2.QtGui import QIcon from PySide2.QtCore import QFile, QSize, Qt from PySide2.QtUiTools import QUiLoader # Local imports from utils import show_about, resource_path # Local globals APP_VERSION = "v0.3.1" APP_NAME = "Garagenraum-Rechner" APP_DISPNAME = "GarageCalc" APP_AUTHOR = "Paul Salajean" APP_DESCR = "Berechnet zur Verfügung stehenden Garagenraum" APP_COPYRIGHT = "(c) Paul Salajean 2021" APP_WEBSITE = "https://gitlab.com/ProfP303" APP_DESKTOPFILENAME = "GarageCalc" APP_ICON = "./img/icons8-garage-32.ico" ICON_NEW = "./img/icons8-new-file-32.png" ICON_OPEN = "./img/icons8-opened-folder-32.png" ICON_SAVE = "./img/icons8-save-32.png" ICON_EXPORT = "./img/icons8-export-xls-32.png" ICON_ABOUT = "./img/icons8-information-32.png" ICON_QUIT = "./img/system-shutdown.png" UI_MAIN = "./ui/main.ui" COL_STUFF = 0 COL_LENGTH = 1 COL_WIDTH = 2 COL_HEIGHT = 3 COL_WEIGHT = 4 DEFAULT_GARAGE_LENGTH = "6" DEFAULT_GARAGE_WIDTH = "2.5" DEFAULT_GARAGE_HEIGHT = "2.5" TBL_STUFF_ROW_COUNT = 50 class MyMainWindow(QMainWindow): def __init__(self): super().__init__() self.load_ui() self.init_ui() self.set_defaults() self.connect_signals() self.create_actions() self.create_toolbar() self.create_statusbar() self.statusBar.showMessage(f"{APP_NAME} {APP_VERSION} by {APP_AUTHOR}", 5000) self.calc_voluminae() self.ui.efWeight.setText(str("0")) self.is_modified = False self.opened_file = None self.remembered_row = None def load_ui(self): loader = QUiLoader() path = os.path.join(os.path.dirname(__file__), resource_path(UI_MAIN)) ui_file = QFile(path) ui_file.open(QFile.ReadOnly) self.ui = loader.load(ui_file, self) ui_file.close() # self.headers = self.ui.tableStuff.horizontalHeader() self.headers = self.ui.tableStuff.verticalHeader() self.headers.setContextMenuPolicy(Qt.CustomContextMenu) self.headers.customContextMenuRequested.connect(self.show_rowheader_context_menu) self.headers.setSelectionMode(QAbstractItemView.SingleSelection) def create_actions(self): self.actionNew = QAction() self.actionNew.setIcon(QIcon(resource_path(ICON_NEW))) self.actionNew.triggered.connect(self.file_new) self.actionNew.setShortcut("Ctrl+N") self.actionNew.setToolTip("Neu (Strg+N)") self.actionOpen = QAction() self.actionOpen.setIcon(QIcon(resource_path(ICON_OPEN))) self.actionOpen.triggered.connect(self.file_open) self.actionOpen.setShortcut("Ctrl+O") self.actionOpen.setToolTip("Öffnen... (Strg+O)") self.actionSave = QAction() self.actionSave.setIcon(QIcon(resource_path(ICON_SAVE))) self.actionSave.triggered.connect(self.file_save) self.actionSave.setShortcut("Ctrl+S") self.actionSave.setToolTip("Speichern (Strg+S)") self.actionExport = QAction() self.actionExport.setIcon(QIcon(resource_path(ICON_EXPORT))) self.actionExport.triggered.connect(self.file_export) self.actionExport.setToolTip("Export nach EXCEL...") self.actionAbout = QAction() self.actionAbout.setIcon(QIcon(resource_path(ICON_ABOUT))) self.actionAbout.triggered.connect(show_about) self.actionAbout.setToolTip("Informationen über das Programm") self.actionQuit = QAction() self.actionQuit.setIcon(QIcon(resource_path(ICON_QUIT))) self.actionQuit.triggered.connect(self.app_quit) self.actionQuit.setShortcut("Ctrl+Q") self.actionQuit.setToolTip("Programm beenden (Strg+Q)") def create_toolbar(self): # Main Toolbar (for all pages/views) self.toolbar = self.addToolBar('Main Toolbar') self.toolbar.setIconSize(QSize(32, 32)) self.toolbar.addAction(self.actionNew) self.toolbar.addAction(self.actionOpen) self.toolbar.addAction(self.actionSave) self.toolbar.addSeparator() self.toolbar.addAction(self.actionExport) self.toolbar.addSeparator() self.toolbar.addAction(self.actionAbout) self.toolbar.addSeparator() self.toolbar.addAction(self.actionQuit) def set_defaults(self): tblGarage = self.ui.tableGarage tblStuff = self.ui.tableStuff tblGarage.setItem(0, 0, QTableWidgetItem(DEFAULT_GARAGE_LENGTH)) tblGarage.setItem(0, 1, QTableWidgetItem(DEFAULT_GARAGE_WIDTH)) tblGarage.setItem(0, 2, QTableWidgetItem(DEFAULT_GARAGE_HEIGHT)) tblStuff.setRowCount(TBL_STUFF_ROW_COUNT) self.is_modified = False def create_statusbar(self): self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) def connect_signals(self): tblGarage = self.ui.tableGarage tblStuff = self.ui.tableStuff tblGarage.itemChanged.connect(self.on_garage_changed) tblStuff.itemChanged.connect(self.on_stuff_changed) def keyPressEvent(self, event): tblStuff = self.ui.tableStuff modifiers = QApplication.keyboardModifiers() key = event.key() if modifiers == Qt.ControlModifier: # get selected row sel_rows_idx = tblStuff.selectionModel().selectedRows() row = sel_rows_idx[0].row() # there is onle on row because of singleton selection mode if key == Qt.Key_C: self.remembered_row = row elif key == Qt.Key_V: self.row_insert(tblStuff, self.remembered_row, row) if key == Qt.Key_Delete: self.row_remove(tblStuff) def row_remove(self, table): # get selected row sel_rows_idx = table.selectionModel().selectedRows() row = sel_rows_idx[0].row() # there is onle on row because of singleton selection mode stuff = None item_stuff = table.item(row, COL_STUFF) if item_stuff: stuff = item_stuff.text() if stuff: msg = f"Zeile Nr. {row+1} '{stuff}' entfernen?" else: msg = f"Zeile Nr. {row+1} entfernen?" reply = QMessageBox.question(self, "Zeile entfernen", msg, \ QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.Yes: table.removeRow(row) self.calc_voluminae() def row_insert(self, table, source_row, target_row): # check if empty item_stuff = table.item(target_row, COL_STUFF) item_length = table.item(target_row, COL_LENGTH) item_width = table.item(target_row, COL_WIDTH) item_height = table.item(target_row, COL_HEIGHT) item_weight = table.item(target_row, COL_WEIGHT) if item_stuff or item_length or item_width or item_height or item_weight: msg = "Es sind bereits Werte in dieser Zeile vorhanden. Trotzdem fortfahren?" reply = QMessageBox.question(self, "Zeile einfügen", msg, \ QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.No: return False item_stuff = table.item(source_row, COL_STUFF) item_length = table.item(source_row, COL_LENGTH) item_width = table.item(source_row, COL_WIDTH) item_height = table.item(source_row, COL_HEIGHT) item_weight = table.item(source_row, COL_WEIGHT) if item_stuff: table.setItem(target_row, COL_STUFF, QTableWidgetItem(item_stuff.text())) if item_length: table.setItem(target_row, COL_LENGTH, QTableWidgetItem(item_length.text())) if item_width: table.setItem(target_row, COL_WIDTH, QTableWidgetItem(item_width.text())) if item_height: table.setItem(target_row, COL_HEIGHT, QTableWidgetItem(item_height.text())) if item_weight: table.setItem(target_row, COL_WEIGHT, QTableWidgetItem(item_weight.text())) def show_rowheader_context_menu(self, position): tblStuff = self.ui.tableStuff row = self.headers.logicalIndexAt(position) menu = QMenu() row_add = menu.addAction("Zeile hinzufügen") row_remove = menu.addAction("Zeile entfernen (Entf.-Taste)") menu.addSeparator() row_copy = menu.addAction("Zeile kopieren (Strg+C)") row_insert = menu.addAction("Zeile einfügen (Strg+V)") ac = menu.exec_(tblStuff.mapToGlobal(position)) if ac == row_remove: # tblStuff.removeRow(row) # self.calc_voluminae() self.row_remove(tblStuff) elif ac == row_add: tblStuff.insertRow(row) elif ac == row_copy: self.remembered_row = row elif ac == row_insert: self.row_insert(tblStuff, self.remembered_row, row) def init_ui(self): tblGarage = self.ui.tableGarage tblStuff = self.ui.tableStuff # clear garage # tblGarage.clear() tblGarage.setRowCount(0) tblGarage.setRowCount(1) tblGarage.setVerticalHeaderLabels(["Garage"]) # clear stuff # tblStuff.clear() tblStuff.setRowCount(0) tblStuff.setRowCount(TBL_STUFF_ROW_COUNT) # clear results self.ui.efVol_Garage.clear() self.ui.efVol_Stuff.clear() self.ui.efVol_Free.clear() self.ui.efWeight.clear() self.opened_file = None if self.opened_file: self.setWindowTitle(self.opened_file) else: self.setWindowTitle(f"{APP_NAME} {APP_VERSION} by {APP_AUTHOR}") self.ui.efVol_Free.setStyleSheet("") self.is_modified = False def app_quit(self): if self.is_modified: msg = "Es existieen ungespeicherte Einträge. Ohne Speichern sind alle Änderungen verloren. Trotzdem fortfahren?" reply = QMessageBox.question(self, "Beenden", msg, \ QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.No: return False QApplication.quit() def file_new(self): if self.is_modified: msg = "Es wurden bereits Einträge manuell geändert. Ohne Speichern sind alle Änderungen verloren. Trotzdem fortfahren?" reply = QMessageBox.question(self, "Neu", msg, \ QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.No: return False self.init_ui() def file_save(self): tblGarage = self.ui.tableGarage tblStuff = self.ui.tableStuff is_file_saved = False fileName = self.opened_file if not self.opened_file: # if not file already open options = QFileDialog.Options() fileName, _ = QFileDialog.getSaveFileName(None, "Speichern", None, "CSV-Datei (*.csv);;Alle Dateien (*)", options=options) if fileName: # if not file already open with open(fileName, mode='w', newline='') as garagecalc_file: writer = csv.writer(garagecalc_file, delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL) garage_length = 0 garage_width = 0 garage_height = 0 item_length = tblGarage.item(0, 0) item_width = tblGarage.item(0, 1) item_height = tblGarage.item(0, 2) # loop over table Garage for row in range(tblGarage.rowCount()): # get garage length if item_length: garage_length = item_length.text() try: garage_length = float(garage_length) except ValueError: garage_length = 0.0 # get garage width if item_width: garage_width = item_width.text() try: garage_width = float(garage_width) except ValueError: garage_width = 0.0 # get garage height if item_height: garage_height = item_height.text() try: garage_height = float(garage_height) except ValueError: garage_height = 0.0 if garage_length or garage_width or garage_height: writer.writerow(["Garage", garage_length, garage_width, garage_height]) # loop over table Stuff for row in range(tblStuff.rowCount()): stuff_text = None length = None width = None height = None weight = None item_stuff = tblStuff.item(row, COL_STUFF) item_length = tblStuff.item(row, COL_LENGTH) item_width = tblStuff.item(row, COL_WIDTH) item_height = tblStuff.item(row, COL_HEIGHT) item_weight = tblStuff.item(row, COL_WEIGHT) if item_stuff: stuff_text = item_stuff.text() if item_length: try: length = float(item_length.text()) except ValueError: length = None if item_width: try: width = float(item_width.text()) except ValueError: width = None if item_height: try: height = float(item_height.text()) except ValueError: height = None if item_weight: try: weight = float(item_weight.text()) except ValueError: weight = None if item_stuff or item_length or item_width or item_height or item_weight: writer.writerow([stuff_text, length, width, height, weight]) is_file_saved = True self.opened_file = os.path.basename(fileName) self.setWindowTitle(self.opened_file) self.is_modified = False if is_file_saved: self.statusBar.showMessage(f"Datei {fileName} gespeichert.", 2000) def file_open(self): if self.is_modified: msg = "Es wurden bereits Einträge manuell geändert. Ohne Speichern sind alle Änderungen verloren. Trotzdem fortfahren?" reply = QMessageBox.question(self, "Fortfahren?", msg, \ QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.No: return False tblGarage = self.ui.tableGarage tblStuff = self.ui.tableStuff options = QFileDialog.Options() sTxtFilesAll = "Alle Dateien" sTxtFiles = "CSV-Datei" fileName, _ = QFileDialog.getOpenFileName(self, "Öffnen", None, sTxtFiles + " (*.csv);;" + sTxtFilesAll + " (*)", options=options) if fileName: self.init_ui() file = open(fileName, "r", newline='') reader = csv.reader(file, delimiter=';') row_idx = 0 for row in reader: if row_idx == 0: # first row (index=0) ist always garage dimension try: garage_length = row[1] garage_width = row[2] garage_height = row[3] tblGarage.setItem(0, COL_LENGTH-1, QTableWidgetItem(str(garage_length))) tblGarage.setItem(0, COL_WIDTH-1, QTableWidgetItem(str(garage_width))) tblGarage.setItem(0, COL_HEIGHT-1, QTableWidgetItem(str(garage_height))) except IndexError as ex: pass if row_idx > 0: try: stuff = row[0] stuff_length = row[1] stuff_width = row[2] stuff_height = row[3] stuff_weight = row[4] tblStuff.setItem(row_idx - 1, COL_STUFF, QTableWidgetItem(stuff)) tblStuff.setItem(row_idx - 1, COL_LENGTH, QTableWidgetItem(str(stuff_length))) tblStuff.setItem(row_idx - 1, COL_WIDTH, QTableWidgetItem(str(stuff_width))) tblStuff.setItem(row_idx - 1, COL_HEIGHT, QTableWidgetItem(str(stuff_height))) tblStuff.setItem(row_idx - 1, COL_WEIGHT, QTableWidgetItem(str(stuff_weight))) except IndexError as ex: pass row_idx += 1 tblStuff.setRowCount(TBL_STUFF_ROW_COUNT) self.is_modified = False self.opened_file = os.path.basename(fileName) if fileName: self.setWindowTitle(self.opened_file) else: self.setWindowTitle(f"{APP_NAME} {APP_VERSION} by {APP_AUTHOR}") def file_export(self): tblGarage = self.ui.tableGarage tblStuff = self.ui.tableStuff options = QFileDialog.Options() file_name, _ = QFileDialog.getSaveFileName(None, "Exportieren", None, "Excel-Datei (*.xlsx);;Alle Dateien (*)", options=options) if file_name: try: import xlsxwriter except ModuleNotFoundError: print(f"[{__file__}] Module 'xlsxwriter' not found!") else: print(f"Exporting into file -> {file_name}") workbook = xlsxwriter.Workbook(file_name) worksheet = workbook.add_worksheet() # write col header start_row = 0 worksheet.write(start_row, 0, "Dimension der Garage") start_row = 1 worksheet.write(start_row, COL_LENGTH, "Länge [m]") worksheet.write(start_row, COL_WIDTH, "Breite [m]") worksheet.write(start_row, COL_HEIGHT, "Höhe [m]") worksheet.set_column(0, 0, 25) worksheet.set_column(1, 3, 10) start_row = 2 # loop over table Garage for row in range(tblGarage.rowCount()): # get garage length garage_length = tblGarage.item(0, 0).text() try: garage_length = float(garage_length) except ValueError: garage_length = 0.0 # get garage width garage_width = tblGarage.item(0, 1).text() try: garage_width = float(garage_width) except ValueError: garage_width = 0.0 # get garage height garage_height = tblGarage.item(0, 2).text() try: garage_height = float(garage_height) except ValueError: garage_height = 0.0 worksheet.write(start_row + row, COL_LENGTH, garage_length) worksheet.write(start_row + row, COL_WIDTH, garage_width) worksheet.write(start_row + row, COL_HEIGHT, garage_height) start_row = 4 worksheet.write(start_row, 0, "Dimensionen der zu verstauenden Gegenstände") start_row = 5 worksheet.write(start_row, COL_LENGTH, "Länge [m]") worksheet.write(start_row, COL_WIDTH, "Breite [m]") worksheet.write(start_row, COL_HEIGHT, "Höhe [m]") worksheet.write(start_row, COL_WEIGHT, "Gewicht [kg]") start_row = 6 # loop over table Stuff row_idx = start_row for row in range(tblStuff.rowCount()): item_stuff = tblStuff.item(row, COL_STUFF) item_length = tblStuff.item(row, COL_LENGTH) item_width = tblStuff.item(row, COL_WIDTH) item_height = tblStuff.item(row, COL_HEIGHT) item_weight = tblStuff.item(row, COL_WEIGHT) if item_stuff: stuff_text = item_stuff.text() if len(stuff_text)>0: worksheet.write(start_row + row, COL_STUFF, stuff_text) if item_length: try: length = float(item_length.text()) if length: worksheet.write(start_row + row, COL_LENGTH, length) except ValueError: pass if item_width: try: width = float(item_width.text()) if width: worksheet.write(start_row + row, COL_WIDTH, width) except ValueError: pass if item_height: try: height = float(item_height.text()) if height: worksheet.write(start_row + row, COL_HEIGHT, height) except ValueError: pass if item_weight: try: weight = float(item_weight.text()) if weight: worksheet.write(start_row + row, COL_WEIGHT, weight) except ValueError: pass if item_stuff or item_length or item_width or item_height or item_weight: row_idx += 1 row_idx += 1 # loop over Results worksheet.write(row_idx, 0, "Ergebnis") row_idx += 1 worksheet.write(row_idx, 0, "Volumen der Garage:") worksheet.write(row_idx, 1, float(self.ui.efVol_Garage.text())) row_idx += 1 worksheet.write(row_idx, 0, "Volumen der Gegenstände:") worksheet.write(row_idx, 1, float(self.ui.efVol_Stuff.text())) row_idx += 1 worksheet.write(row_idx, 0, "Freier Raum") worksheet.write(row_idx, 1, float(self.ui.efVol_Free.text())) row_idx += 1 worksheet.write(row_idx, 0, "Gesamtgewicht") worksheet.write(row_idx, 1, float(self.ui.efWeight.text())) workbook.close() self.statusBar.showMessage(f"Erfolgreich nach EXCEL exportiert.", 5000) def on_garage_changed(self): self.is_modified = True self.calc_voluminae() def on_stuff_changed(self): self.is_modified = True self.calc_voluminae() self.calc_weight() def get_garage_vol(self): tblGarage = self.ui.tableGarage is_error = False garage_length = 0 garage_width = 0 garage_height = 0 # get garage length if tblGarage.item(0, 0): garage_length = tblGarage.item(0, 0).text() if garage_length: try: garage_length = float(garage_length) except ValueError: garage_length = 0.0 is_error = True # get garage width if tblGarage.item(0, 1): garage_width = tblGarage.item(0, 1).text() if garage_width: try: garage_width = float(garage_width) except ValueError: garage_width = 0.0 is_error = True # get garage height if tblGarage.item(0, 2): garage_height = tblGarage.item(0, 2).text() if garage_height: try: garage_height = float(garage_height) except ValueError: garage_height = 0.0 is_error = True if not is_error: garage_vol = round(float(garage_length) * float(garage_width) * float(garage_height), 2) else: garage_vol = 0.0 self.statusBar.showMessage("Fehler in der Garagen-Dimension. :-(", 5000) return garage_vol def get_stuff_vol(self): tblStuff = self.ui.tableStuff stuff_vol = 0 length = 0 width = 0 height = 0 # get number of rows row_count = tblStuff.rowCount() is_error = False # get stuff voluminae for row in range(row_count): vol = 0 length = 0 width = 0 height = 0 item_length = tblStuff.item(row, COL_LENGTH) item_width = tblStuff.item(row, COL_WIDTH) item_height = tblStuff.item(row, COL_HEIGHT) if item_length: try: length = float(item_length.text()) except ValueError: length = 0 is_error = True if item_width: try: width = float(item_width.text()) except ValueError: width = 0 is_error = True if item_height: try: height = float(item_height.text()) except ValueError: height = 0 is_error = True vol = round(length * width * height, 2) stuff_vol = stuff_vol + vol if is_error: stuff_vol = 0.0 self.statusBar.showMessage("Fehler in den Dimensionen der zu verstauenden Gegenstände :-(", 5000) return stuff_vol def calc_voluminae(self): tblGarage = self.ui.tableGarage # get garage vol garage_vol = self.get_garage_vol() print("garage_vol", garage_vol) # get stuff vol stuff_vol = self.get_stuff_vol() print("stuff_vol", stuff_vol) # display results self.ui.efVol_Garage.setText(f"{garage_vol:2.2f}") self.ui.efVol_Stuff.setText(f"{stuff_vol:2.2f}") self.ui.efVol_Free.setText(f"{garage_vol - stuff_vol:2.2f}") if ( garage_vol - stuff_vol ) < 0: self.ui.efVol_Free.setStyleSheet("border: 1px solid red;") else: self.ui.efVol_Free.setStyleSheet("") def calc_weight(self): tblStuff = self.ui.tableStuff weight_sum = 0 # get number of rows row_count = tblStuff.rowCount() for row in range(row_count): weight = 0.0 item = tblStuff.item(row, COL_WEIGHT) if item: try: weight = float(item.text()) except ValueError: weight = 0.0 weight_sum = round(weight_sum + weight, 2) self.ui.efWeight.setText(f"{weight_sum:2.2f}") if __name__ == "__main__": qApp = QApplication([]) qApp.setApplicationName(APP_NAME) qApp.setApplicationDisplayName(APP_DISPNAME) qApp.setApplicationVersion(APP_VERSION) qApp.description = APP_DESCR qApp.copyright = APP_COPYRIGHT qApp.website = APP_WEBSITE qApp.setWindowIcon(QIcon(APP_ICON)) qApp.setDesktopFileName(APP_DESKTOPFILENAME) winMain = MyMainWindow() winMain.setFixedWidth(600) winMain.setFixedHeight(620) winMain.show() sys.exit(qApp.exec_())