I'm trying to use QTableWidget to display logs. For user, it's a read-only table where only full rows can be selected. Each row is a log entry. New entries are added as rows to the bottom. The table has horizontal header but no vertical header. To limit used memory, I decided to limit number of rows in the table. When new row is added, I check if number of rows exceeds some defined value, and if yes, then I delete the first row. The problem is that after deleting the first row, all other rows are shifted up. This is not convenient for the user. If the user examines some events in the middle of the table and then suddenly rows start to go up and rows that the user is interested in dissapear, it would be very unconvenient.
I found the scrollTo
method but I couldn't find a way to get the first displayed row of the table.
This does nothing (apart from removing the row):
QModelIndex index = ui->tableWidget->indexAt(QPoint(40, 70));
ui->tableWidget->removeRow(0);
ui->tableWidget->scrollTo(index, QAbstractItemView::EnsureVisible);
This at least scrolls the table but it works only when a row is selected and after removing the first row the selected row could be hidden behind the horizontal header:
QModelIndex index = ui->tableWidget->currentIndex();
ui->tableWidget->removeRow(0);
ui->tableWidget->scrollTo(index, QAbstractItemView::EnsureVisible);
Update: I forgot to mention that user has ability to filter log entries, and this is implemented using hideRow
. I found one solution but it doesn't work when table has hidden rows.
The first issue you're probably having is that, in order to ensure that the scroll position remains consistent, you need to check the origin point of the scroll area. Right now you're using an arbitrary position (QPoint(40, 70)
) which is not consistent nor reliable.
If you're assuming you should consider the sizes of the horizontal and vertical headers, that's a wrong assumption, as indexAt()
is always in viewport coordinates, which do not include headers.
If you're trying to get "some item at a reasonable position", you're still wrong, because you may end up with an invalid or inconsistent item.
Also, while handy, scrollTo()
should not be considered as a precise way to scroll to a certain position or item, and, unless you have deep understanding of the behavior and underlying mechanism of that function, it's not appropriate to use it if you want to "keep" a certain item at the previous visual position.
All Qt scroll areas (including Qt item views) inherit from QAbstractScrollArea, and the only appropriate way to scroll to a precise position is by setting the values of its scroll bars, as long as they've not been changed with custom QScrollBar subclasses that behave differently.
Specifically, Qt item views have scroll bars that use value ranges depending on the "scroll mode".
When the mode is ScrollPerPixel
then it works similarly to QScrollArea: the maximum value will be the full extent of the direction subtracted by the viewport dimension (eg: the width of all items minus the viewport width).
Note that the scroll mode defaults to different aspects depending on the view type and contents. For instance, the horizontal mode in a vertically laid out QListView should obviously "scroll per pixel" in case its contents don't fit horizontally.
If the horizontal or vertical scroll policy is set to ScrollPerItem
, then the related scroll bar maximum will be the same as the item count, depending on the orientation, and subtracted by the amount of (theoretically) visible items for that direction. For example, if a model has 10 rows and the table showing it can only vertically display 2 items, the range of the vertical scroll bar will be from 0 to 8.
So, in order to "keep" the viewport scroll position consistent even when removing objects from the top, the proper solution is to get the index that exists at the top left corner of the view (no matter if it's fully visible or not) and then set the value based on the verticalScrollMode
.
If the mode is ScrollPerItem
, then it's relatively simple: get the "row" of the item on the top left, and then use that value for the vertical scroll bar.
Since the view may have hidden rows, you also need to check if rows before that index are also hidden with isRowHidden()
, and then subtract their count before setting the value.
The procedure is:
hiddenSectionCount()
) and eventually subtract the "top row" for as many hidden rows that exist before it; this must be done after the first row has been deleted, as it could be a hidden one;If the mode is ScrollPerPixel
, then we should do what QTableView does, which is relying on its headers.
For vertical scrolling, this means that we need to call the vertical header's sectionPosition()
to get the origin point of the "row". Since we may also want to consider the partial scrolling (if the top item is not fully shown), we also need to check whether the y
of the visualRect()
of the top item is off by some pixels (since it's the "top" item, it can only be off by a negative value).
Also, since changing the model shape causes a delayed lay out of the widgets within the view (including its header geometries and contents), it's important to call the view's updateGeometries()
before getting the section position.
The procedure is basically similar to the ScrollPerItem
case, with the following changes:
visualRect()
of the top item and keep a reference to its y
offset;updateGeometries()
;sectionPosition()
as the possible value for the scroll bar;y
offset (if any) from the value just retrieved (as said, it should be negative, so we're actually increasing the value);I cannot write reliable C++ code, but the following is a Python function that could act as a pseudo code, and which should be easy to read and understand in order to be ported into actual C++ code.
def remove(self):
top = self.table.indexAt(QPoint(0, 0))
if top.row() <= 1 or self.table.isRowHidden(top.row()):
self.table.removeRow(0)
return
value = top.row() - 1
offset = self.table.visualRect(top).y()
self.table.removeRow(0)
if self.table.verticalScrollMode() == self.table.ScrollPerItem:
if self.table.verticalHeader().hiddenSectionCount():
for r in range(top.row()):
if self.table.isRowHidden(r):
value -= 1
self.table.verticalScrollBar().setValue(value)
return
self.table.updateGeometries()
value = self.table.verticalHeader().sectionPosition(value)
if offset:
value -= offset
self.table.verticalScrollBar().setValue(value)
Note: if you're not familiar with Python, that self
mostly refers to this
in the function called for the current instance; in your case, the above self.table
would be ui->tableWidget
.
Note that all of the above is based on the assumption that you'll always remove just one row from the top.
If you want to be able to remove more than one row, or allow the possibility to remove rows in the middle of the model, you will obviously need to consider that, at least for the following aspects:
removeRows()
calls accordingly;