From eae788072fff001028e0c8f04babd61325125281 Mon Sep 17 00:00:00 2001 From: Adrian Herrmann Date: Mon, 11 Jul 2022 11:29:35 +0200 Subject: PySide6-examples: SpreadSheet example Port pre-existing SpreadSheet example to PySide. Pick-to: 6.2 6.3 Change-Id: I32d014fbd5e3e406672b6bec2465c6e41c3a6580 Reviewed-by: Cristian Maureira-Fredes --- .../itemviews/spreadsheet/doc/spreadsheet.png | Bin 0 -> 40187 bytes .../itemviews/spreadsheet/doc/spreadsheet.rst | 10 + examples/widgets/itemviews/spreadsheet/main.py | 19 + .../widgets/itemviews/spreadsheet/spreadsheet.py | 544 +++++++++++++++++++++ .../itemviews/spreadsheet/spreadsheetdelegate.py | 67 +++ .../itemviews/spreadsheet/spreadsheetitem.py | 122 +++++ 6 files changed, 762 insertions(+) create mode 100644 examples/widgets/itemviews/spreadsheet/doc/spreadsheet.png create mode 100644 examples/widgets/itemviews/spreadsheet/doc/spreadsheet.rst create mode 100644 examples/widgets/itemviews/spreadsheet/main.py create mode 100644 examples/widgets/itemviews/spreadsheet/spreadsheet.py create mode 100644 examples/widgets/itemviews/spreadsheet/spreadsheetdelegate.py create mode 100644 examples/widgets/itemviews/spreadsheet/spreadsheetitem.py (limited to 'examples/widgets') diff --git a/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.png b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.png new file mode 100644 index 000000000..ae7dde24b Binary files /dev/null and b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.png differ diff --git a/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.rst b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.rst new file mode 100644 index 000000000..c0839b232 --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/doc/spreadsheet.rst @@ -0,0 +1,10 @@ +Spreadsheet example +=================== + +The Spreadsheet example shows how a table view can be used to create a simple +spreadsheet application. Custom delegates are used to render different types of +data in distinctive colors. + +.. image:: spreadsheet.png + :width: 400 + :alt: Spreadsheet screenshot diff --git a/examples/widgets/itemviews/spreadsheet/main.py b/examples/widgets/itemviews/spreadsheet/main.py new file mode 100644 index 000000000..0ecc5ec23 --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/main.py @@ -0,0 +1,19 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QApplication, QLayout + +from spreadsheet import SpreadSheet + +if __name__ == "__main__": + app = QApplication() + + sheet = SpreadSheet(10, 6) + sheet.setWindowIcon(QPixmap(":/images/interview.png")) + sheet.show() + sheet.layout().setSizeConstraint(QLayout.SetFixedSize) + + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/spreadsheet/spreadsheet.py b/examples/widgets/itemviews/spreadsheet/spreadsheet.py new file mode 100644 index 000000000..2f5fbb64f --- /dev/null +++ b/examples/widgets/itemviews/spreadsheet/spreadsheet.py @@ -0,0 +1,544 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import QPoint, Qt, QCoreApplication, Slot +from PySide6.QtGui import QAction, QBrush, QPixmap, QColor, QPainter +from PySide6.QtWidgets import (QColorDialog, QComboBox, QDialog, QFontDialog, + QGroupBox, QHBoxLayout, QMainWindow, QLabel, + QLineEdit, QMessageBox, QPushButton, QToolBar, + QTableWidgetItem, QTableWidget, QVBoxLayout, QWidget) + +from spreadsheetdelegate import SpreadSheetDelegate +from spreadsheetitem import SpreadSheetItem + +from typing import Optional +from numbers import Number + + +class SpreadSheet(QMainWindow): + def __init__(self, rows: Number, cols: Number, parent: Optional[QWidget] = None) -> None: + super().__init__(parent) + + self._tool_bar = QToolBar(self) + self._color_action = QAction() + self._font_action = QAction() + self._first_separator = QAction() + self._cell_sum_action = QAction() + self._cell_add_action = QAction() + self._cell_sub_action = QAction() + self._cell_mul_action = QAction() + self._cell_div_action = QAction() + self._second_separator = QAction() + self._clear_action = QAction() + self._about_spreadsheet = QAction() + self._exit_action = QAction() + + # self._print_action = QAction() + + self._cell_label = QLabel(self._tool_bar) + self._table = QTableWidget(rows, cols, self) + self._formula_input = QLineEdit(self) + + self.addToolBar(self._tool_bar) + + self._cell_label.setMinimumSize(80, 0) + + self._tool_bar.addWidget(self._cell_label) + self._tool_bar.addWidget(self._formula_input) + + self._table.setSizeAdjustPolicy(QTableWidget.SizeAdjustPolicy.AdjustToContents) + for c in range(cols): + character = chr(ord('A') + c) + self._table.setHorizontalHeaderItem(c, QTableWidgetItem(character)) + + self._table.setItemPrototype(self._table.item(rows - 1, cols - 1)) + self._table.setItemDelegate(SpreadSheetDelegate()) + + self.create_actions() + self.update_color(None) + self.setup_menu_bar() + self.setup_contents() + self.setup_context_menu() + self.setCentralWidget(self._table) + + self.statusBar() + self._table.currentItemChanged.connect(self.update_status) + self._table.currentItemChanged.connect(self.update_color) + self._table.currentItemChanged.connect(self.update_line_edit) + self._table.itemChanged.connect(self.update_status) + self._formula_input.returnPressed.connect(self.return_pressed) + self._table.itemChanged.connect(self.update_line_edit) + + self.setWindowTitle("Spreadsheet") + + def create_actions(self) -> None: + self._cell_sum_action = QAction("Sum", self) + self._cell_sum_action.triggered.connect(self.action_sum) + + self._cell_add_action = QAction("&Add", self) + self._cell_add_action.setShortcut(Qt.CTRL | Qt.Key_Plus) + self._cell_add_action.triggered.connect(self.action_add) + + self._cell_sub_action = QAction("&Subtract", self) + self._cell_sub_action.setShortcut(Qt.CTRL | Qt.Key_Minus) + self._cell_sub_action.triggered.connect(self.action_subtract) + + self._cell_mul_action = QAction("&Multiply", self) + self._cell_mul_action.setShortcut(Qt.CTRL | Qt.Key_multiply) + self._cell_mul_action.triggered.connect(self.action_multiply) + + self._cell_div_action = QAction("&Divide", self) + self._cell_div_action.setShortcut(Qt.CTRL | Qt.Key_division) + self._cell_div_action.triggered.connect(self.action_divide) + + self._font_action = QAction("Font...", self) + self._font_action.setShortcut(Qt.CTRL | Qt.Key_F) + self._font_action.triggered.connect(self.select_font) + + self._color_action = QAction(QPixmap(16, 16), "Background &Color...", self) + self._color_action.triggered.connect(self.select_color) + + self._clear_action = QAction("Clear", self) + self._clear_action.setShortcut(Qt.Key_Delete) + self._clear_action.triggered.connect(self.clear) + + self._about_spreadsheet = QAction("About Spreadsheet", self) + self._about_spreadsheet.triggered.connect(self.show_about) + + self._exit_action = QAction("E&xit", self) + self._exit_action.triggered.connect(QCoreApplication.quit) + + self._first_separator = QAction(self) + self._first_separator.setSeparator(True) + + self._second_separator = QAction(self) + self._second_separator.setSeparator(True) + + def setup_menu_bar(self) -> None: + file_menu = self.menuBar().addMenu("&File") + # file_menu.addAction(self._print_action) + file_menu.addAction(self._exit_action) + + cell_menu = self.menuBar().addMenu("&Cell") + cell_menu.addAction(self._cell_add_action) + cell_menu.addAction(self._cell_sub_action) + cell_menu.addAction(self._cell_mul_action) + cell_menu.addAction(self._cell_div_action) + cell_menu.addAction(self._cell_sum_action) + cell_menu.addSeparator() + cell_menu.addAction(self._color_action) + cell_menu.addAction(self._font_action) + + self.menuBar().addSeparator() + + about_menu = self.menuBar().addMenu("&Help") + about_menu.addAction(self._about_spreadsheet) + + @Slot() + def update_status(self, item: QTableWidgetItem) -> None: + if item and item == self._table.currentItem(): + self.statusBar().showMessage(str(item.data(Qt.StatusTipRole)), 1000) + self._cell_label.setText( + "Cell: ({})".format( + SpreadSheetItem.encode_pos(self._table.row(item), self._table.column(item)) + ) + ) + + @Slot() + def update_color(self, item: QTableWidgetItem) -> None: + pix = QPixmap(16, 16) + col = QColor() + if item: + col = item.background().color() + if not col.isValid(): + col = self.palette().base().color() + + pt = QPainter(pix) + pt.fillRect(0, 0, 16, 16, col) + + lighter = col.lighter() + pt.setPen(lighter) + light_frame = [QPoint(0, 15), QPoint(0, 0), QPoint(15, 0)] + pt.drawPolyline(light_frame) + + pt.setPen(col.darker()) + darkFrame = [QPoint(1, 15), QPoint(15, 15), QPoint(15, 1)] + pt.drawPolyline(darkFrame) + + pt.end() + + self._color_action.setIcon(pix) + + @Slot() + def update_line_edit(self, item: QTableWidgetItem) -> None: + if item != self._table.currentItem(): + return + if item: + self._formula_input.setText(str(item.data(Qt.EditRole))) + else: + self._formula_input.clear() + + @Slot() + def return_pressed(self) -> None: + text = self._formula_input.text() + row = self._table.currentRow() + col = self._table.currentColumn() + item = self._table.item(row, col) + if not item: + self._table.setItem(row, col, SpreadSheetItem(text)) + else: + item.setData(Qt.EditRole, text) + self._table.viewport().update() + + @Slot() + def select_color(self) -> None: + item = self._table.currentItem() + col = item.background().color() if item else self._table.palette().base().color() + col = QColorDialog.getColor(col, self) + if not col.isValid(): + return + + selected = self._table.selectedItems() + if not selected: + return + + for i in selected: + if i: + i.setBackground(col) + + self.update_color(self._table.currentItem()) + + @Slot() + def select_font(self) -> None: + selected = self._table.selectedItems() + if not selected: + return + + ok = False + fnt = QFontDialog.getFont(ok, self.font(), self) + + if not ok: + return + for i in selected: + if i: + i.setFont(fnt) + + def run_input_dialog(self, title: str, c1Text: str, c2Text: str, opText: str, + outText: str, cell1: str, cell2: str, outCell: str) -> bool: + rows, cols = [], [] + for c in range(self._table.columnCount()): + cols.append(chr(ord('A') + c)) + for r in range(self._table.rowCount()): + rows.append(str(1 + r)) + + add_dialog = QDialog(self) + add_dialog.setWindowTitle(title) + + group = QGroupBox(title, add_dialog) + group.setMinimumSize(250, 100) + + cell1_label = QLabel(c1Text, group) + cell1_row_input = QComboBox(group) + c1_row, c1_col = SpreadSheetItem.decode_pos(cell1) + cell1_row_input.addItems(rows) + cell1_row_input.setCurrentIndex(c1_row) + + cell1_col_input = QComboBox(group) + cell1_col_input.addItems(cols) + cell1_col_input.setCurrentIndex(c1_col) + + operator_label = QLabel(opText, group) + operator_label.setAlignment(Qt.AlignHCenter) + + cell2_label = QLabel(c2Text, group) + cell2_row_input = QComboBox(group) + c2_row, c2_col = SpreadSheetItem.decode_pos(cell2) + cell2_row_input.addItems(rows) + cell2_row_input.setCurrentIndex(c2_row) + cell2_col_input = QComboBox(group) + cell2_col_input.addItems(cols) + cell2_col_input.setCurrentIndex(c2_col) + + equals_label = QLabel("=", group) + equals_label.setAlignment(Qt.AlignHCenter) + + out_label = QLabel(outText, group) + out_row_input = QComboBox(group) + out_row, out_col = SpreadSheetItem.decode_pos(outCell) + out_row_input.addItems(rows) + out_row_input.setCurrentIndex(out_row) + out_col_input = QComboBox(group) + out_col_input.addItems(cols) + out_col_input.setCurrentIndex(out_col) + + cancel_button = QPushButton("Cancel", add_dialog) + cancel_button.clicked.connect(add_dialog.reject) + + ok_button = QPushButton("OK", add_dialog) + ok_button.setDefault(True) + ok_button.clicked.connect(add_dialog.accept) + + buttons_layout = QHBoxLayout() + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_button) + buttons_layout.addSpacing(10) + buttons_layout.addWidget(cancel_button) + + dialog_layout = QVBoxLayout(add_dialog) + dialog_layout.addWidget(group) + dialog_layout.addStretch(1) + dialog_layout.addItem(buttons_layout) + + cell1_layout = QHBoxLayout() + cell1_layout.addWidget(cell1_label) + cell1_layout.addSpacing(10) + cell1_layout.addWidget(cell1_col_input) + cell1_layout.addSpacing(10) + cell1_layout.addWidget(cell1_row_input) + + cell2_layout = QHBoxLayout() + cell2_layout.addWidget(cell2_label) + cell2_layout.addSpacing(10) + cell2_layout.addWidget(cell2_col_input) + cell2_layout.addSpacing(10) + cell2_layout.addWidget(cell2_row_input) + + out_layout = QHBoxLayout() + out_layout.addWidget(out_label) + out_layout.addSpacing(10) + out_layout.addWidget(out_col_input) + out_layout.addSpacing(10) + out_layout.addWidget(out_row_input) + + v_layout = QVBoxLayout(group) + v_layout.addItem(cell1_layout) + v_layout.addWidget(operator_label) + v_layout.addItem(cell2_layout) + v_layout.addWidget(equals_label) + v_layout.addStretch(1) + v_layout.addItem(out_layout) + + if add_dialog.exec(): + cell1 = cell1_col_input.currentText() + cell1_row_input.currentText() + cell2 = cell2_col_input.currentText() + cell2_row_input.currentText() + outCell = out_col_input.currentText() + out_row_input.currentText() + return True + + return False + + @Slot() + def action_sum(self) -> None: + row_first = row_last = row_cur = 0 + col_first = col_last = col_cur = 0 + + selected = self._table.selectedItems() + + if selected is not None: + first = selected[0] + last = selected[-1] + row_first = self._table.row(first) + row_last = self._table.row(last) + col_first = self._table.column(first) + col_last = self._table.column(last) + + current = self._table.currentItem() + + if current: + row_cur = self._table.row(current) + col_cur = self._table.column(current) + + cell1 = SpreadSheetItem.encode_pos(row_first, col_first) + cell2 = SpreadSheetItem.encode_pos(row_last, col_last) + out = SpreadSheetItem.encode_pos(row_cur, col_cur) + + if self.run_input_dialog( + "Sum cells", "First cell:", "Last cell:", + f"{(chr(0x03a3))}", "Output to:", + cell1, cell2, out + ): + row, col = SpreadSheetItem.decode_pos(out) + self._table.item(row, col).setText(f"sum {cell1} {cell2}") + + def action_math_helper(self, title: str, op: str) -> None: + cell1 = "C1" + cell2 = "C2" + out = "C3" + + current = self._table.currentItem() + if current: + out = SpreadSheetItem.encode_pos(self._table.currentRow(), self._table.currentColumn()) + + if self.run_input_dialog(title, "Cell 1", "Cell 2", op, "Output to:", cell1, cell2, out): + row, col = SpreadSheetItem.decode_pos(out) + self._table.item(row, col).setText(f"{op} {cell1} {cell2}") + + @Slot() + def action_add(self) -> None: + self.action_math_helper("Addition", "+") + + @Slot() + def action_subtract(self) -> None: + self.action_math_helper("Subtraction", "-") + + @Slot() + def action_multiply(self) -> None: + self.action_math_helper("Multiplication", "*") + + @Slot() + def action_divide(self) -> None: + self.action_math_helper("Division", "/") + + @Slot() + def clear(self) -> None: + selected_items = self._table.selectedItems() + for item in selected_items: + item.setText("") + + def setup_context_menu(self) -> None: + self.addAction(self._cell_add_action) + self.addAction(self._cell_sub_action) + self.addAction(self._cell_mul_action) + self.addAction(self._cell_div_action) + self.addAction(self._cell_sum_action) + self.addAction(self._first_separator) + self.addAction(self._color_action) + self.addAction(self._font_action) + self.addAction(self._second_separator) + self.addAction(self._clear_action) + self.setContextMenuPolicy(Qt.ActionsContextMenu) + + def setup_contents(self) -> None: + title_background = QBrush(Qt.lightGray) + title_font = self._table.font() + title_font.setBold(True) + + # column 0 + self._table.setItem(0, 0, SpreadSheetItem("Item")) + self._table.item(0, 0).setBackground(title_background) + self._table.item(0, 0).setToolTip( + "This column shows the purchased item/service" + ) + self._table.item(0, 0).setFont(title_font) + + self._table.setItem(1, 0, SpreadSheetItem("AirportBus")) + self._table.setItem(2, 0, SpreadSheetItem("Flight (Munich)")) + self._table.setItem(3, 0, SpreadSheetItem("Lunch")) + self._table.setItem(4, 0, SpreadSheetItem("Flight (LA)")) + self._table.setItem(5, 0, SpreadSheetItem("Taxi")) + self._table.setItem(6, 0, SpreadSheetItem("Dinner")) + self._table.setItem(7, 0, SpreadSheetItem("Hotel")) + self._table.setItem(8, 0, SpreadSheetItem("Flight (Oslo)")) + self._table.setItem(9, 0, SpreadSheetItem("Total:")) + + self._table.item(9, 0).setFont(title_font) + self._table.item(9, 0).setBackground(title_background) + + # column 1 + self._table.setItem(0, 1, SpreadSheetItem("Date")) + self._table.item(0, 1).setBackground(title_background) + self._table.item(0, 1).setToolTip( + "This column shows the purchase date, double click to change" + ) + self._table.item(0, 1).setFont(title_font) + + self._table.setItem(1, 1, SpreadSheetItem("15/6/2006")) + self._table.setItem(2, 1, SpreadSheetItem("15/6/2006")) + self._table.setItem(3, 1, SpreadSheetItem("15/6/2006")) + self._table.setItem(4, 1, SpreadSheetItem("21/5/2006")) + self._table.setItem(5, 1, SpreadSheetItem("16/6/2006")) + self._table.setItem(6, 1, SpreadSheetItem("16/6/2006")) + self._table.setItem(7, 1, SpreadSheetItem("16/6/2006")) + self._table.setItem(8, 1, SpreadSheetItem("18/6/2006")) + + self._table.setItem(9, 1, SpreadSheetItem()) + self._table.item(9, 1).setBackground(title_background) + + # column 2 + self._table.setItem(0, 2, SpreadSheetItem("Price")) + self._table.item(0, 2).setBackground(title_background) + self._table.item(0, 2).setToolTip("This column shows the price of the purchase") + self._table.item(0, 2).setFont(title_font) + + self._table.setItem(1, 2, SpreadSheetItem("150")) + self._table.setItem(2, 2, SpreadSheetItem("2350")) + self._table.setItem(3, 2, SpreadSheetItem("-14")) + self._table.setItem(4, 2, SpreadSheetItem("980")) + self._table.setItem(5, 2, SpreadSheetItem("5")) + self._table.setItem(6, 2, SpreadSheetItem("120")) + self._table.setItem(7, 2, SpreadSheetItem("300")) + self._table.setItem(8, 2, SpreadSheetItem("1240")) + + self._table.setItem(9, 2, SpreadSheetItem()) + self._table.item(9, 2).setBackground(Qt.lightGray) + + # column 3 + self._table.setItem(0, 3, SpreadSheetItem("Currency")) + self._table.item(0, 3).setBackground(title_background) + self._table.item(0, 3).setToolTip("This column shows the currency") + self._table.item(0, 3).setFont(title_font) + + self._table.setItem(1, 3, SpreadSheetItem("NOK")) + self._table.setItem(2, 3, SpreadSheetItem("NOK")) + self._table.setItem(3, 3, SpreadSheetItem("EUR")) + self._table.setItem(4, 3, SpreadSheetItem("EUR")) + self._table.setItem(5, 3, SpreadSheetItem("USD")) + self._table.setItem(6, 3, SpreadSheetItem("USD")) + self._table.setItem(7, 3, SpreadSheetItem("USD")) + self._table.setItem(8, 3, SpreadSheetItem("USD")) + + self._table.setItem(9, 3, SpreadSheetItem()) + self._table.item(9, 3).setBackground(Qt.lightGray) + + # column 4 + self._table.setItem(0, 4, SpreadSheetItem("Ex. Rate")) + self._table.item(0, 4).setBackground(title_background) + self._table.item(0, 4).setToolTip("This column shows the exchange rate to NOK") + self._table.item(0, 4).setFont(title_font) + + self._table.setItem(1, 4, SpreadSheetItem("1")) + self._table.setItem(2, 4, SpreadSheetItem("1")) + self._table.setItem(3, 4, SpreadSheetItem("8")) + self._table.setItem(4, 4, SpreadSheetItem("8")) + self._table.setItem(5, 4, SpreadSheetItem("7")) + self._table.setItem(6, 4, SpreadSheetItem("7")) + self._table.setItem(7, 4, SpreadSheetItem("7")) + self._table.setItem(8, 4, SpreadSheetItem("7")) + + self._table.setItem(9, 4, SpreadSheetItem()) + self._table.item(9, 4).setBackground(title_background) + + # column 5 + self._table.setItem(0, 5, SpreadSheetItem("NOK")) + self._table.item(0, 5).setBackground(title_background) + self._table.item(0, 5).setToolTip("This column shows the expenses in NOK") + self._table.item(0, 5).setFont(title_font) + + self._table.setItem(1, 5, SpreadSheetItem("* C2 E2")) + self._table.setItem(2, 5, SpreadSheetItem("* C3 E3")) + self._table.setItem(3, 5, SpreadSheetItem("* C4 E4")) + self._table.setItem(4, 5, SpreadSheetItem("* C5 E5")) + self._table.setItem(5, 5, SpreadSheetItem("* C6 E6")) + self._table.setItem(6, 5, SpreadSheetItem("* C7 E7")) + self._table.setItem(7, 5, SpreadSheetItem("* C8 E8")) + self._table.setItem(8, 5, SpreadSheetItem("* C9 E9")) + + self._table.setItem(9, 5, SpreadSheetItem("sum F2 F9")) + self._table.item(9, 5).setBackground(title_background) + + @Slot() + def show_about(self) -> None: + html_text = ( + "" + "

This demo shows use of QTableWidget with custom handling for" + " individual cells.

" + "

Using a customized table item we make it possible to have dynamic" + " output in different cells. The content that is implemented for this" + " particular demo is:" + "