I'm trying to make an Excel-like table in a GUI application with PyQt5. I'm struggling to add a merge/unmerge feature. After adding a few print statements, I noticed that merging a first group of cells will work, but when I try to merge a second group of cells, it will for some reason select only one cell (even though I've selected multiple) and the merge will not be successful. Once I unmerge the first group of cells, I can merge another group again, but thats it, just one group.
Has anyone ever encountered this issue or know a fix? I can't seem to find any helpful posts on stackoverflow or youtube.
I attempted to make it so the entire row would be selected. In that case, I was able to merge multiple different groups of rows. But I want it to be cells, like in Excel. And that doesn't work.
Adding self.tableWidget.clearSelection()
didn't seem to help either.
import sys
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableWidget, QVBoxLayout, QWidget, QPushButton
class ExcelLikeTable(QMainWindow):
def __init__(self):
super().__init__()
self.mergeButton = None
self.unmergeButton = None
self.tableWidget = QTableWidget()
self.initUI()
def initUI(self):
self.setWindowTitle("Excel-like Table with PyQt5")
self.setGeometry(100, 100, 800, 600)
self.tableWidget.setColumnCount(10)
self.tableWidget.setHorizontalHeaderLabels([f'Column {chr(65+i)}' for i in range(10)])
self.tableWidget.setRowCount(1)
self.tableWidget.clearSelection()
self.tableWidget.setSelectionMode(QTableWidget.ContiguousSelection)
self.tableWidget.setSelectionBehavior(QTableWidget.SelectItems)
self.mergeButton = QPushButton("Merge Cells")
self.unmergeButton = QPushButton("Unmerge Cells")
self.mergeButton.clicked.connect(self.mergeCells)
self.unmergeButton.clicked.connect(self.unmergeCells)
layout = QVBoxLayout()
layout.addWidget(self.tableWidget)
layout.addWidget(self.mergeButton)
layout.addWidget(self.unmergeButton)
centralWidget = QWidget()
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
self.tableWidget.installEventFilter(self)
def mergeCells(self):
if not self.tableWidget.selectedRanges():
print("No cells selected for merging.")
return
selectedRange = self.tableWidget.selectedRanges()[0]
topRow, leftColumn = selectedRange.topRow(), selectedRange.leftColumn()
rowCount, columnCount = selectedRange.rowCount(), selectedRange.columnCount()
print(
f"Selected range - Top Row: {topRow}, Left Column: {leftColumn}, Row Count: {rowCount}, Column Count: {columnCount}")
if rowCount == 1 and columnCount == 1:
print("Only a single cell selected, no merge performed.")
return
self.tableWidget.setSpan(topRow, leftColumn, rowCount, columnCount)
print(
f"Merge completed from Cell {chr(65 + leftColumn)}{topRow + 1} to Cell {chr(65 + leftColumn + columnCount - 1)}{topRow + rowCount}")
def unmergeCells(self):
for i in range(self.tableWidget.rowCount()):
for j in range(self.tableWidget.columnCount()):
self.tableWidget.setSpan(i, j, 1, 1)
def addRow(self):
rowCount = self.tableWidget.rowCount()
self.tableWidget.insertRow(rowCount)
def eventFilter(self, source, event):
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Return:
currentRow = self.tableWidget.currentRow()
currentColumn = self.tableWidget.currentColumn()
if currentRow == self.tableWidget.rowCount() - 1:
self.addRow()
self.tableWidget.setCurrentCell(currentRow + 1, currentColumn)
return True
return super(ExcelLikeTable, self).eventFilter(source, event)
def debugPrintCellSpans(self):
print("Debugging cell spans:")
for i in range(self.tableWidget.rowCount()):
for j in range(self.tableWidget.columnCount()):
rowSpan = self.tableWidget.rowSpan(i, j)
colSpan = self.tableWidget.columnSpan(i, j)
if rowSpan > 1 or colSpan > 1:
print(f"Cell at ({i+1}, {chr(65+j)}) has row span: {rowSpan}, column span: {colSpan}")
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ExcelLikeTable()
ex.show()
sys.exit(app.exec_())
As indicated by the name, selectedRanges returns a list of ranges, but the docs do not state under what precise circumstances multiple ranges will occur. It seems that after a merge, every cell in the subsequent selection forms a separate range, but whether this behaviour is as intended, or a bug, is unclear. Whatever the case may be, the behaviour of selectedIndexes is much more intuitive, so it would seem best to use that instead.
I have shown how to fix your example using the selected indexes below. It appears that clearing the spans of the currently selected cells first works best, as otherwise it may result in rather confusing nested spans.
class ExcelLikeTable(QMainWindow):
...
def unmergeCells(self):
self.tableWidget.clearSpans()
def mergeCells(self):
selection = self.tableWidget.selectedIndexes()
if not selection:
print("No cells selected for merging.")
return
if len(selection) == 1:
print("Only a single cell selected, no merge performed.")
return
for index in selection:
row, column = index.row(), index.column()
if (self.tableWidget.rowSpan(row, column) > 1 or
self.tableWidget.columnSpan(row, column) > 1):
self.tableWidget.setSpan(row, column, 1, 1)
# clearing the spans may have changed the selection
selection = sorted(self.tableWidget.selectedIndexes())
topRow, leftColumn = selection[0].row(), selection[0].column()
bottomRow, rightColumn = selection[-1].row(), selection[-1].column()
rowCount = bottomRow - topRow + 1
columnCount = rightColumn - leftColumn + 1
print(
f"Selected range - Top Row: {topRow}, Left Column: {leftColumn}, Row Count: {rowCount}, Column Count: {columnCount}")
self.tableWidget.setSpan(topRow, leftColumn, rowCount, columnCount)
print(
f"Merge completed from Cell {chr(65 + leftColumn)}{topRow + 1} to Cell {chr(65 + leftColumn + columnCount - 1)}{topRow + rowCount}")