Home > Software engineering >  PyQt6 Implementing copy-paste functionality across tabs in same window
PyQt6 Implementing copy-paste functionality across tabs in same window

Time:09-08

I am trying to create a tableWidget in PyQt that has copy-paste functionality and the ability to create new tabs. My table is loaded with a initialized sheet but gives the user the ability to create new tabs with new qTableWidgets which I handled using a for loop to create and initialize the widget everytime a new tab is created in my add_sheet() function.

I wanted to also add functionality for copying and pasting inside each tab and across the tabs. Now when I added the key press event function to do this, I kept getting errors when trying to copy and paste in new tabs as out of index. I tried to fix this by keeping a pointer of which tab the selected indexes come from but this only allows me to edit on the first new tab created. The initial spreadsheet crashes when trying to do operations and the other tabs just do not work. It also does not copy and paste universally amongst the tabs.

I feel I have made my handling too complicated and I have a flaw or am missing something in my design pattern.

How can I properly implement my copy-paste function to work amongst all dynamically tabs with their own QTableWidget instances including the initial QTableWidget created both locally and universally?

import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
    QTableWidget, QTableWidgetItem,QVBoxLayout,QTabWidget,QWidget,QToolButton,QToolBar)
from PyQt6.QtCore import Qt



class MainWindow(QMainWindow):
    
    def __init__(self):
        super().__init__()
        self.tab_widget = MyTabWidget(self)
        self.setCentralWidget(self.tab_widget)
        self.initializeUI()

    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(1200, 500)
        self.setWindowTitle("Spreadsheet - QTableWidget Example")

        # Used for copy and paste actions
        self.item_text = None

        self.setUpMainWindow()
        self.show()
        

    def setUpMainWindow(self):
        """Create and arrange widgets in the main window."""

        # Set initial row and column values
        main_spreadsheet_widget.setRowCount(10)
        main_spreadsheet_widget.setColumnCount(10)
 


class MyTabWidget(QWidget):
    def __init__(self, parent):
        super(QWidget, self).__init__(parent)
        self.layout = QVBoxLayout(self)
  
        # Initialize tab screen
        self.sheets = QTabWidget()
        self.main_sheet = QWidget()
        self.sheets.resize(300, 200)
        self.extra_sheets_tracker = list()
        self.tab_index = []
        self.copied_cells_list = []
        self.paste_index = []


        # Add sheets
        self.sheets.addTab(self.main_sheet, "Main Sheet")
        #self.sheets.addTab(self.tab3, "Geeks")
  
        # Create first tab
        self.main_sheet.layout = QVBoxLayout(self)
        self.main_sheet.layout.addWidget(main_spreadsheet_widget)
        self.main_sheet.setLayout(self.main_sheet.layout)
        self.tabButton = QToolButton(self)
        self.tabButton.setText(' ')
        font = self.tabButton.font()
        font.setBold(True)
        self.tabButton.setFont(font)
        self.sheets.setCornerWidget(self.tabButton)
        self.tabButton.clicked.connect(self.add_sheet)

        
        # Add sheets to widget
        self.layout.addWidget(self.sheets)
        self.setLayout(self.layout)

    def add_sheet(self):

        self.sheet = QWidget()
        self.main_tab_sheet_widget = QTableWidget()
        self.extra_sheets_tracker.append(self.main_tab_sheet_widget)
        self.main_tab_sheet_widget.setRowCount(10)
        self.main_tab_sheet_widget.setColumnCount(10)

        
        self.sheet.layout = QVBoxLayout(self)

        self.sheet.layout.addWidget(self.main_tab_sheet_widget)
        self.sheet.setLayout(self.sheet.layout)
        self.sheets.addTab(self.main_tab_sheet_widget, "Sheet"   str(len(self.extra_sheets_tracker)))


    
    def keyPressEvent(self, event):
        super().keyPressEvent(event)
        if event.key() == Qt.Key.Key_C and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
            self.tab_index = []
            for i in self.extra_sheets_tracker:
                if i.selectedIndexes() is not None:
                    self.tab_index.append(self.extra_sheets_tracker.index(i))
                    self.copied_cells = sorted(self.extra_sheets_tracker[self.tab_index[0]].selectedIndexes())
                    self.copied_cells_list.append(self.copied_cells)
                    self.copied_cells = None
        elif event.key() == Qt.Key.Key_V and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
            self.paste_index = []
            for i in self.extra_sheets_tracker:
                self.paste_index.append(self.extra_sheets_tracker.index(i))
            r = self.extra_sheets_tracker[self.paste_index[0]].currentRow() - self.copied_cells_list[0][0].row()
            c = self.extra_sheets_tracker[self.paste_index[0]].currentColumn() - self.copied_cells_list[0][0].column()
            for cell in self.copied_cells_list[0]:
                self.extra_sheets_tracker[self.paste_index[0]].setItem(cell.row()   r, cell.column()   c, QTableWidgetItem(cell.data()))


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main_spreadsheet_widget = QTableWidget()
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

CodePudding user response:

I have managed to do the following:

1- I treated the main sheet as if it was "just another sheet" and made sure it follows the same relative routine. This included removing unnecessary functions like setUpMainWindow and introducing static values like MAX_ROWS and MAX_COLS for maximum rows and columns respectively. This also entailed renaming extra_sheets_tracker to become sheets_tracker since it will hold all of the sheets.

2- I have created two strategies for pasting: 1st-cell for selecting the first cell in a table, or 1-to-1 for selecting the exact count of cells at the pasting side. This meant, also, checking for the dimensions beforehand and raising the exception Unmatching pasting size if the sizes did not match.

3- I removed self.tab_index and self.copied_cells_list since they are not used. copy_list is used globally and it is not a bad idea to stay in self. However paste_list is not used anywhere else than the paste routine and I removed it from self. You need to look into your self variables, you seem to overuse them.

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QVBoxLayout, QTabWidget, QWidget, QToolButton, QToolBar
from PyQt6.QtCore import Qt



class MainWindow(QMainWindow):
  def __init__(self):
    super().__init__()
    self.tab_widget = MyTabWidget(self)
    self.setCentralWidget(self.tab_widget)
    self.initializeUI()

  def initializeUI(self):
    """Set up the application's GUI."""
    self.setMinimumSize(1200, 500)
    self.setWindowTitle("Spreadsheet - QTableWidget Example")

    # Used for copy and paste actions
    self.item_text = None
    self.show() 


class MyTabWidget(QWidget):
  def __init__(self, parent):
    super(QWidget, self).__init__(parent)
    self.layout = QVBoxLayout(self)
 
    # Initialize tab screen
    self.MAX_ROWS = 10
    self.MAX_COLS = 10
    self.STRATEGY = {0:'1st-cell', 1:'1-to-1'}
    self.sheets = QTabWidget()
    self.main_sheet = QWidget()
    self.sheets.resize(300, 200)
    self.sheets_tracker = list()
    self.copy_list = []
    #self.copied_cells_list = []
    #self.paste_list = []


    # Add a sheet and create first tab
    self.main_sheet.layout = QVBoxLayout(self)
    # Setup main window
    self.add_sheet()
    
    tabButton = QToolButton(self)
    tabButton.setText(' ')
    font = tabButton.font()
    font.setBold(True)
    tabButton.setFont(font)
    tabButton.clicked.connect(self.add_sheet)

    self.sheets.setCornerWidget(tabButton)

    # Add sheets to widget
    self.layout.addWidget(self.sheets)
    self.setLayout(self.layout)

  def add_sheet(self):
    sheets_count = len(self.sheets_tracker)
    if sheets_count < 1:
      label = "Main sheet"
    else:
      label = f"Sheet{sheets_count}"

    self.main_tab_sheet_widget = QTableWidget()
    self.main_tab_sheet_widget.setRowCount(self.MAX_ROWS)
    self.main_tab_sheet_widget.setColumnCount(self.MAX_COLS)
    self.sheets_tracker.append(self.main_tab_sheet_widget)

    self.sheet = QWidget()
    self.sheet.layout = QVBoxLayout(self)
    self.sheet.layout.addWidget(self.main_tab_sheet_widget)
    self.sheet.setLayout(self.sheet.layout)
    self.sheets.addTab(self.main_tab_sheet_widget, label)


  def keyPressEvent(self, event):
    super().keyPressEvent(event)
    if event.key() == Qt.Key.Key_C and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
      self.copy_list = sorted([(e.row(), e.column(), e.data()) for e in self.sheets_tracker[self.sheets.currentIndex()].selectedIndexes()])
      if len(self.copy_list) != 0:
        # recalculate the indicies to absolute values
        least_row = self.copy_list[0][0]
        least_col = sorted(self.copy_list, key=lambda tup: tup[1])[0][1]
        self.copy_list = [(e[0]-least_row, e[1]-least_col, e[2]) for e in self.copy_list]
        print(self.copy_list)
    elif event.key() == Qt.Key.Key_V and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
      paste_list = sorted([(element.row(), element.column(), element.data()) for element in self.sheets_tracker[self.sheets.currentIndex()].selectedIndexes()])
      if len(paste_list) != 0:
        if len(paste_list) == 1:
          # The given paste position is the first cell only
          current_r, current_c, _ = paste_list[-1]
          last_r, last_c, _ = self.copy_list[-1]
          if last_r   current_r - 1 < self.MAX_ROWS and last_c   current_c - 1 < self.MAX_COLS:
            strategy = self.STRATEGY[0]
          else:
            raise Exception('Unmatching pasting size')
        elif len(self.copy_list) == len(paste_list):
          # You can paste in one-to-one correspondence
          strategy = self.STRATEGY[1]
        else:
          raise Exception('Unmatching pasting size')
        
        if strategy == self.STRATEGY[0]:
          r, c, _ = paste_list[0]
          for index, e in enumerate(self.copy_list):
            _, _, d = self.copy_list[index]
            d = '' if d is None else d
            print(f"Pasting at index [{self.sheets.currentIndex()}] cell ({e[0] r}, {e[1] c})")
            textCell = QTableWidgetItem(); textCell.setText(f'{d}')
            self.sheets_tracker[self.sheets.currentIndex()].setItem(e[0] r, e[1] c, textCell)
        
        if strategy == self.STRATEGY[1]:
          for index, e in enumerate(paste_list):
            _, _, d = self.copy_list[index]
            d = '' if d is None else d
            print(f"Pasting at index [{self.sheets.currentIndex()}] cell ({e[0]}, {e[1]})")
            textCell = QTableWidgetItem(); textCell.setText(f'{d}')
            self.sheets_tracker[self.sheets.currentIndex()].setItem(e[0], e[1], textCell)


if __name__ == "__main__":
  app = QApplication(sys.argv)
  window = MainWindow()
  window.show()
  sys.exit(app.exec())
  • Related