Reputation: 1644
I'm trying to create a 'table' where the headers and row labels remain visible. Kind of like the Frozen Column Example, but using a QGridLayout.
My approach is to put three QGridLayouts into aligned QScrollAreas, where the main table contains RxC rows and columns and is scrollable in any direction, and the column and row headers are contained in 1xC and Rx1 cells, respectively. Then I can just get the different QScrollAreas to track each other.
I've got this working (though I haven't touched the matched-scrolling bit yet), but I'm having a lot of trouble getting the three different QGridLayouts to have the same size cells (in width or height).
Part of the problem is that the table cells can be variously sized, meaning that each row and column is not the same height/width as all of the others. But I also can't figure out how to get the heights/widths of the rows of the different QGridLayouts to match.
As you can see in the image above, there are three rows and two columns. I want the Row Headers to line up with the three rows, and the Column Headers to line up with the two columns.
Here's a minimal example. There's a lot still for me to figure out and get working, but this first step is an important one.
import PyQt5.QtGui
import PyQt5.QtWidgets
import PyQt5.QtCore
class MainBuyerWindow(PyQt5.QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(PyQt5.QtWidgets.QMainWindow, self).__init__(*args, **kwargs)
(
columns_widget, rows_widget, table_widget,
columns_layout, rows_layout, table_layout,
) = self.setup_table()
self.columns_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.rows_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.table_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.columns_scroll_widget.setWidget(columns_widget)
self.rows_scroll_widget.setWidget(rows_widget)
self.table_scroll_widget.setWidget(table_widget)
central_layout = PyQt5.QtWidgets.QGridLayout()
central_layout.addWidget(self.columns_scroll_widget, 0, 1, 1, 5)
central_layout.addWidget( self.rows_scroll_widget, 1, 0, 5, 1)
central_layout.addWidget( self.table_scroll_widget, 1, 1, 5, 5)
central_widget = PyQt5.QtWidgets.QWidget()
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
self.show()
def setup_table(self):
columns_layout = PyQt5.QtWidgets.QGridLayout()
rows_layout = PyQt5.QtWidgets.QGridLayout()
table_layout = PyQt5.QtWidgets.QGridLayout()
columns_widget = PyQt5.QtWidgets.QWidget()
columns_widget.setLayout(columns_layout)
rows_widget = PyQt5.QtWidgets.QWidget()
rows_widget.setLayout(rows_layout)
table_widget = PyQt5.QtWidgets.QWidget()
table_widget.setLayout(table_layout)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("This cell\nis a\ntall one"), 0, 0)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("This cell is shorter"), 1, 0)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("This cell is of\nmedium height"), 2, 0)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("Also notice that widths of columns are not all the same"), 0, 1)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("Though that doesn't matter"), 1, 1)
table_layout.addWidget(PyQt5.QtWidgets.QLabel("(In this example anyway)"), 2, 1)
columns_layout.addWidget(PyQt5.QtWidgets.QLabel("Column Header 1"), 0, 0)
columns_layout.addWidget(PyQt5.QtWidgets.QLabel("Column Header 2"), 0, 1)
rows_layout.addWidget(PyQt5.QtWidgets.QLabel("Row Header 1"), 0, 0)
rows_layout.addWidget(PyQt5.QtWidgets.QLabel("Row Header 2"), 1, 0)
rows_layout.addWidget(PyQt5.QtWidgets.QLabel("Row Header 3"), 2, 0)
return (
columns_widget, rows_widget, table_widget,
columns_layout, rows_layout, table_layout,
)
######################################################################
if __name__ == "__main__":
app = PyQt5.QtWidgets.QApplication([])
window = MainBuyerWindow()
app.exec_()
I think my main problem is I don't understand Qt well enough, so I'm not sure what to look at. What I tried was calling the show() (or activate()) method to get the geometries all worked out, and then going through the first row and column of the table_layout to get their dimensions using cellRect, and then calling columnMinimumWidth and rowMinimumHeight on the corresponding row/column of rows_layout and columns_layout.
This is the same idea used in this (almost identical) question, but when I implement it here it also doesn't seem to work. (Essentially just adding the following lines)
table_layout.activate()
for i in range(table_layout.columnCount()):
w = table_layout.columnMinimumWidth(i)
columns_layout.setColumnMinimumWidth(i, w)
for i in range(table_layout.rowCount()):
h = table_layout.rowMinimumHeight(i)
rows_layout.setRowMinimumHeight(i, h)
Can someone help me out here? Thank you so much!
Upvotes: 0
Views: 218
Reputation: 6112
I would set a fixed height to each row header label based on the height of that row in the table (and similarly a fixed width to each column header label based on the width of that column in the table), which fixes the layout alignment but also makes it simple to match up the scrolling.
class MainBuyerWindow(PyQt5.QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(PyQt5.QtWidgets.QMainWindow, self).__init__(*args, **kwargs)
(
columns_widget, rows_widget, table_widget,
columns_layout, rows_layout, table_layout,
) = self.setup_table()
self.columns_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.rows_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.table_scroll_widget = PyQt5.QtWidgets.QScrollArea()
self.columns_scroll_widget.setWidget(columns_widget)
self.rows_scroll_widget.setWidget(rows_widget)
self.table_scroll_widget.setWidget(table_widget)
for i in range(rows_layout.count()):
label = rows_layout.itemAt(i).widget()
label.setFixedHeight(table_layout.cellRect(i, 0).height())
for i in range(columns_layout.count()):
label = columns_layout.itemAt(i).widget()
label.setFixedWidth(table_layout.cellRect(0, i).width())
...
Use QScrollArea.setWidgetResizable(True)
so the scroll area can resize the widget as needed, and allow the header labels to take up the required amount of space to be aligned with the table.
...
self.columns_scroll_widget.setWidgetResizable(True)
self.rows_scroll_widget.setWidgetResizable(True)
self.table_scroll_widget.setWidgetResizable(True)
# Size policies to keep the row and column headers minimal/sufficient
# and allow table_scroll_widget to get as much space as possible when resized
self.columns_scroll_widget.setSizePolicy(PyQt5.QtWidgets.QSizePolicy.Preferred, PyQt5.QtWidgets.QSizePolicy.Minimum)
self.rows_scroll_widget.setSizePolicy(PyQt5.QtWidgets.QSizePolicy.Minimum, PyQt5.QtWidgets.QSizePolicy.Preferred)
...
QScrollBar inherits from QAbstractSlider so you can connect its valueChanged
signal to the setValue
slot of another scroll bar. These values will be consistent across the layouts now since their row heights and column widths are equal.
...
# User scrolling in table will cause headers to scroll
self.table_scroll_widget.horizontalScrollBar().valueChanged[int].connect(self.columns_scroll_widget.horizontalScrollBar().setValue)
self.table_scroll_widget.verticalScrollBar().valueChanged[int].connect(self.rows_scroll_widget.verticalScrollBar().setValue)
# User scrolling on headers will cause table to scroll
self.columns_scroll_widget.horizontalScrollBar().valueChanged[int].connect(self.table_scroll_widget.horizontalScrollBar().setValue)
self.rows_scroll_widget.verticalScrollBar().valueChanged[int].connect(self.table_scroll_widget.verticalScrollBar().setValue)
central_layout = PyQt5.QtWidgets.QGridLayout()
central_layout.addWidget(self.columns_scroll_widget, 0, 1, 1, 5)
central_layout.addWidget( self.rows_scroll_widget, 1, 0, 5, 1)
central_layout.addWidget( self.table_scroll_widget, 1, 1, 5, 5)
central_widget = PyQt5.QtWidgets.QWidget()
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
self.show()
Outcome:
Upvotes: 4