class MainWindow(QtWidgets.QMainWindow): MaxRecentFiles = 5 def __init__(self, app_title="CSV Viewer"): super().__init__() self.progress = QProgressBar() self.threadpool = QThreadPool() self.app_title = app_title self.setMinimumSize(600, 300) self.df = None self.round_num = 2 self.recentFileActs = [] # settings self.settings = QtCore.QSettings('CSV_Viewer', 'CSV_Viewer') self.round_num = self.settings.value('round_numbers', self.round_num, int) # toolbar self.toolbar = QToolBar("MainToolbar") self.toolbar.setIconSize(QSize(16, 16)) self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.addToolBar(self.toolbar) # open action style = self.toolbar.style() icon = style.standardIcon(QStyle.SP_FileDialogStart) self.button_open = QAction(icon, "Open", self) self.button_open.setShortcut('Ctrl+O') self.button_open.setStatusTip("Open CSV file...") self.button_open.triggered.connect(self.onToolbarOpenButtonClick) self.toolbar.addAction(self.button_open) self.button_open.setEnabled(True) # summary action style_summary = self.toolbar.style() icon = style_summary.standardIcon(QStyle.SP_FileDialogListView) self.button_summary = QAction(icon, "Summary", self) self.button_summary.setShortcut('Ctrl+S') self.button_summary.setStatusTip("Show summary for the current file") self.button_summary.triggered.connect(self.onToolbarSummaryButtonClick) self.toolbar.addAction(self.button_summary) # info action style_info = self.toolbar.style() icon = style_info.standardIcon(QStyle.SP_FileIcon) self.button_info = QAction(icon, "Info", self) self.button_info.setShortcut('Ctrl+I') self.button_info.setStatusTip("Show summary for the current file") self.button_info.triggered.connect(self.onToolbarInfoButtonClick) self.toolbar.addAction(self.button_info) # resize action style_resize = self.toolbar.style() icon = style_resize.standardIcon(QStyle.SP_BrowserReload) self.button_resize = QAction(icon, "Resize columns", self) self.button_resize.setShortcut('Ctrl+R') self.button_resize.setStatusTip("Resize columns width to content") self.button_resize.triggered.connect(self.onResizeColumns) self.toolbar.addAction(self.button_resize) # export to xlsx action style_xlsx = self.toolbar.style() icon = style_xlsx.standardIcon(QStyle.SP_FileLinkIcon) self.button_xlsx = QAction(icon, "Xlsx", self) self.button_xlsx.setStatusTip("Export data to xlsx file") self.button_xlsx.triggered.connect(self.onExportXlsx) self.toolbar.addAction(self.button_xlsx) # export to sqlite action style_sqlite = self.toolbar.style() icon = style_sqlite.standardIcon(QStyle.SP_FileLinkIcon) self.button_sqlite = QAction(icon, "SQLite", self) self.button_sqlite.setStatusTip("Export data to SQLite database") self.button_sqlite.triggered.connect(self.onExportSQLite) self.toolbar.addAction(self.button_sqlite) # export to html action style_html = self.toolbar.style() icon = style_html.standardIcon(QStyle.SP_FileLinkIcon) self.button_html = QAction(icon, "HTML", self) self.button_html.setStatusTip("Export data to HTML file") self.button_html.triggered.connect(self.onExportHTML) self.toolbar.addAction(self.button_html) # export to CSV action style_csv = self.toolbar.style() icon = style_csv.standardIcon(QStyle.SP_FileLinkIcon) self.button_csv = QAction(icon, "CSV", self) self.button_csv.setStatusTip("Export data to CSV file") self.button_csv.triggered.connect(self.onExportCSV) self.toolbar.addAction(self.button_csv) # export to Markdown action style_mark = self.toolbar.style() icon = style_mark.standardIcon(QStyle.SP_FileLinkIcon) self.button_mark = QAction(icon, "Markdown", self) self.button_mark.setStatusTip("Export data to Markdown file") self.button_mark.triggered.connect(self.onExportMarkdown) self.toolbar.addAction(self.button_mark) # import data from world bank climate api style_api = self.toolbar.style() icon = style_api.standardIcon(QStyle.SP_DialogSaveButton) self.button_api = QAction(icon, "API", self) self.button_api.setStatusTip("Import data from World Bank Climate API") self.button_api.triggered.connect(self.onImportFromAPI) self.toolbar.addAction(self.button_api) self.button_api.setEnabled(True) # close action style_close = self.toolbar.style() icon = style_close.standardIcon(QStyle.SP_DialogCloseButton) self.button_close = QAction(icon, "Close", self) self.button_close.setShortcut('Ctrl+X') self.button_close.setStatusTip("Close CSV file...") self.button_close.triggered.connect(self.onToolbarCloseButtonClick) self.toolbar.addAction(self.button_close) # quit action self.button_quit = QAction("Quit", self) self.button_quit.setShortcut('Ctrl+Q') self.button_quit.setStatusTip("Quit application") self.button_quit.triggered.connect(self.close) # toolbar show/hide action self.button_tool = QAction("Show/Hide toolbar", self) self.button_tool.setShortcut('Ctrl+T') self.button_tool.setStatusTip("Show or hide toolbar") self.button_tool.triggered.connect(self.showToolbar) # remove NaN self.button_nan = QAction("Remove NaN", self) self.button_nan.setShortcut('Ctrl+R') self.button_nan.setStatusTip("Remove rows with missing values") self.button_nan.triggered.connect(self.onRemoveNaN) # settings action style_settings = self.toolbar.style() icon = style_settings.standardIcon(QStyle.SP_ComputerIcon) self.button_settings = QAction(icon, "Settings", self) self.button_settings.setStatusTip("Application settings") self.button_settings.triggered.connect(self.onSettings) self.toolbar.addAction(self.button_settings) self.button_settings.setEnabled(True) # about action style_about = self.toolbar.style() icon = style_about.standardIcon(QStyle.SP_FileDialogInfoView) self.button_about = QAction(icon, "About", self) self.button_about.setStatusTip("About application") self.button_about.triggered.connect(self.about) self.toolbar.addAction(self.button_about) self.button_about.setEnabled(True) self.setButtons(False) # recent menu action for i in range(MainWindow.MaxRecentFiles): self.recentFileActs.append( QAction(self, visible=False, triggered=self.openRecentFile) ) # menu bar menu = self.menuBar() file_menu = menu.addMenu("&File") file_menu.addAction(self.button_open) file_menu.addAction(self.button_close) file_menu.addSeparator() file_menu.addAction(self.button_nan) file_menu.addSeparator() file_menu.addAction(self.button_settings) self.separatorAct = file_menu.addSeparator() for i in range(MainWindow.MaxRecentFiles): file_menu.addAction(self.recentFileActs[i]) file_menu.addSeparator() file_menu.addAction(self.button_quit) view_menu = menu.addMenu("Vie&w") view_menu.addAction(self.button_summary) view_menu.addAction(self.button_info) view_menu.addSeparator() view_menu.addAction(self.button_resize) view_menu.addSeparator() view_menu.addAction(self.button_tool) export_menu = menu.addMenu("&Export") export_menu.addAction(self.button_xlsx) export_menu.addAction(self.button_sqlite) export_menu.addAction(self.button_html) export_menu.addAction(self.button_csv) export_menu.addAction(self.button_mark) import_menu = menu.addMenu("&Import") import_menu.addAction(self.button_api) help_menu = menu.addMenu("&Help") help_menu.addAction(self.button_about) self.updateRecentFileActions() # status bar self.my_status = QStatusBar(self) self.my_status.addPermanentWidget(self.progress) self.progress.hide() self.labelStatus = QLabel("Rows: 0 Cols: 0") self.my_status.addPermanentWidget(self.labelStatus) self.setStatusBar(self.my_status) # set TableView self.table = QtWidgets.QTableView() self.table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) self.table.setSelectionMode(QtWidgets.QTableView.SingleSelection) self.setCentralWidget(self.table) self.setWindowTitle(self.app_title) self.setMinimumSize(400, 250) self.setGeometry(200, 100, 1000, 600) def onToolbarOpenButtonClick(self) -> None: """ Show open dialog """ dlg = ParameterDialog() dlg.setWindowTitle("Open") if dlg.exec_(): file_name = dlg.filename.text() separator = dlg.separator decimal = dlg.decimal header = dlg.header index = dlg.index else: file_name = None if file_name: self.saveRecent(file_name) self.open_csv_file(file_name, separator, decimal, header, index) def onOpenRecentFile(self, file_name: str, sep=',', decimal='.') -> None: """ Open file from recent list, show open dialog """ dlg = ParameterDialog(file_name, sep, decimal) dlg.setWindowTitle("Open") if dlg.exec_(): file_name = dlg.filename.text() separator = dlg.separator decimal = dlg.decimal header = dlg.header index = dlg.index else: file_name = None if file_name: self.saveRecent(file_name) self.open_csv_file(file_name, separator, decimal, header, index) def open_csv_file(self, file_name: str, sep=',', decimal=".", header=True, index=True) -> None: """ Open csv file, load to tableview, set statusbar, enable close icon""" try: if header: my_header = 'infer' else: my_header = None if index: my_index = 0 else: my_index = False print(my_header, my_index) data = pd.read_csv(file_name, sep=sep, decimal=decimal, header=my_header, index_col=my_index) self.df = data self.model = TableModel(self.df, self.round_num) self.labelStatus.setText(f"Rows: {self.df.shape[0]} Cols: {self.df.shape[1]}") self.table.setModel(self.model) if data.shape[0] > 0: self.table.selectRow(0) self.setButtons(True) self.setWindowTitle(self.app_title + ": " + file_name) except Exception as e: QMessageBox.warning(self, 'Error', f"Error loading the file:\n {file_name}") def setButtons(self, state: bool) -> None: """ Set state of buttons/actions """ self.button_close.setEnabled(state) self.button_summary.setEnabled(state) self.button_info.setEnabled(state) self.button_resize.setEnabled(state) self.button_xlsx.setEnabled(state) self.button_sqlite.setEnabled(state) self.button_html.setEnabled(state) self.button_csv.setEnabled(state) self.button_nan.setEnabled(state) self.button_mark.setEnabled(state) def onResizeColumns(self) -> None: """Resize columns action, run from menu View->Resize columns""" self.table.resizeColumnsToContents() def onToolbarCloseButtonClick(self) -> None: """Clear tableview, set statusbar and disable toolbar close, summary and info icons""" self.table.setModel(None) self.df = None self.setButtons(False) self.setWindowTitle(self.app_title) self.labelStatus.setText("Rows: 0 Cols: 0") def onToolbarSummaryButtonClick(self) -> None: """Show Summary dialog""" dlg = SummaryDialog(self.df.describe()) dlg.setWindowTitle("Summary") dlg.exec_() def onToolbarInfoButtonClick(self) -> None: """Show Info dialog""" # buf = io.StringIO() # self.df.info(buf=buf) # tmp = buf.getvalue() dlg = InfoDialog(self.df) dlg.setWindowTitle("Info") dlg.exec_() def onExportXlsx(self) -> None: """ Export data to xlsx file """ file_name, _ = QFileDialog.getSaveFileName(self, 'Export to xlsx', '', ".xlsx(*.xlsx)") if file_name: self.df.to_excel(file_name, engine='xlsxwriter') def onExportSQLite(self) -> None: file_name, _ = QFileDialog.getSaveFileName(self, 'Export to sqlite db', '', ".sqlite(*.sqlite)") if file_name: engine = create_engine(f'sqlite:///{file_name}', echo=False) self.df.to_sql('csv_data', con=engine) def onExportHTML(self) -> None: """ Export data to HTML file """ file_name, _ = QFileDialog.getSaveFileName(self, 'Export to HTML file', '', ".html(*.html)") if file_name: self.df.to_html(file_name) def onExportCSV(self) -> None: """ Export data to new CSV file """ file_name, _ = QFileDialog.getSaveFileName(self, 'Export to CSV file', '', ".csv(*.csv)") if file_name: self.df.to_csv(file_name, sep=',', decimal='.') def onImportFromAPI(self) -> None: dlg = ApiDialog() dlg.setWindowTitle("Import a Data CSV via API") if dlg.exec_(): file_name = dlg.filename.text() address = dlg.address.text() else: file_name = None if file_name and address: res, text = dataload.import_data_by_api(address) if res: with open(file_name, "w") as f: f.write(text) self.onOpenRecentFile(file_name) else: QMessageBox.warning(self, "Error", text) def closeEvent(self, event) -> None: """ Quit application, ask user before """ if not app_test: result = QMessageBox.question( self, self.app_title, "Are you sure you want to quit?", QMessageBox.Yes | QMessageBox.No, ) if app_test or result == QMessageBox.Yes: event.accept() else: event.ignore() def about(self) -> None: """ Show About dialog (info about application)""" dlg = AboutDialog() dlg.exec_() def openRecentFile(self) -> None: """ Open file from recent list action """ action = self.sender() if action: self.onOpenRecentFile(action.data()) def saveRecent(self, file_name) -> None: """ Save information about currently opened file, update recent list""" settings = QSettings('CSV_Viewer', 'CSV_Viewer') files = settings.value('recentFileList', []) try: files.remove(file_name) except ValueError: pass files.insert(0, file_name) del files[MainWindow.MaxRecentFiles:] settings.setValue('recentFileList', files) for widget in QtWidgets.QApplication.topLevelWidgets(): if isinstance(widget, MainWindow): widget.updateRecentFileActions() def updateRecentFileActions(self) -> None: """ Update recent file list """ settings = QSettings('CSV_Viewer', 'CSV_Viewer') files = settings.value('recentFileList', []) numRecentFiles = min(len(files), MainWindow.MaxRecentFiles) for i in range(numRecentFiles): text = "&%d %s" % (i + 1, self.strippedName(files[i])) self.recentFileActs[i].setText(text) self.recentFileActs[i].setData(files[i]) self.recentFileActs[i].setVisible(True) for j in range(numRecentFiles, MainWindow.MaxRecentFiles): self.recentFileActs[j].setVisible(False) self.separatorAct.setVisible((numRecentFiles > 0)) def strippedName(self, fullFileName: str) -> str: """ Return only file name, without path""" return QFileInfo(fullFileName).fileName() def showToolbar(self): """ show / hide main toolbar""" if self.toolbar.isHidden(): self.toolbar.show() else: self.toolbar.hide() def onSettings(self): """ Round numbers to """ n, result = QInputDialog.getInt(self, "Settings", "Round numbers to:", self.round_num, 0, 10, 1) if result and n != self.round_num: self.round_num = n self.settings.setValue('round_numbers', self.round_num) if self.df.shape[0] > 0: index = self.table.currentIndex() self.model = TableModel(self.df, self.round_num) self.table.setModel(self.model) self.table.selectRow(index.row()) def onRemoveNaN(self): """ Remove rows with missing values """ if self.df.shape[0] > 0: button = QMessageBox.question(self, "Remove NaN", "Remove rows with missing values?") if button == QMessageBox.Yes: self.df.dropna(axis=0, how='any', inplace=True) self.model = TableModel(self.df, self.round_num) self.table.setModel(self.model) self.table.selectRow(0) self.labelStatus.setText(f"Rows: {self.df.shape[0]} Cols: {self.df.shape[1]}") def onExportMarkdown(self): """ export to markdown table in thread """ file_name, _ = QFileDialog.getSaveFileName(self, 'Export to Markdown file', '', ".md(*.md)") if file_name: worker = MarkdownWorker(file_name, self.df) worker.signals.progress.connect(self.update_progress) worker.signals.status.connect(self.update_status) self.threadpool.start(worker) def update_progress(self, progress): self.progress.setValue(progress) def update_status(self, status): if status: self.progress.show() else: self.progress.hide()