QTableView not scrolling all the way to the bottom - qt

I'm mocking up a table in Qt using QTableView and QStandardItemModel. The table represents events in a log. It should scroll to the bottom so that it can show the most recent event. To be clear, I am mocking up this display, I am not adding elements to the table dynamically.
When I try using either the scrollToBottom method or the scrollTo method, the view is scrolling about 3/4 of the way down, rather than to the actual bottom of the window.
Here's a simplified version of my code:
logModel = new QStandardItemModel(THREADWATCHER_LOG_TABLE_LENGTH, 2, this);
logTable = new QTableView;
for (int i = 0; i < THREADWATCHER_LOG_TABLE_LENGTH; i += 6) {
QString spawnTimeString = QString("[16:09:10:");
int spawnTimeMs = 186 + i * 8; // a pseudo-random choice for the interval between spawns
spawnTimeString.append(QString::number(spawnTimeMs) + "]");
logMessage[i] = new QStandardItem(spawnTimeString + " Thread \"Image Fetcher 0\" was spawned in the image processor ");
logModel->setItem(i, 0, logMessage[i]);
// do the same thing with [i+1], [i+2]...[i+5]
// so that I get six different messages in each iteration of the loop
}
logTable->setModel(logModel);
QModelIndex lastIndex = logMessage[THREADWATCHER_LOG_TABLE_LENGTH - 1]->index();
logTable->scrollTo(lastIndex);
I get exactly the same thing if I call logTable->scrollToBottom() instead of scrolling to the last index.
Things I've tried already:
using a single-shot timer (to ensure events have completed before scrolling)
creating a public method that calls logTable->scrollToBottom(), and having something else call that method after this class's constructor finishes (the logic above is in the constructor)

Related

QTableView scrolling stopped after dynamic resizing

I have a QTableView with a custom QSortFilterProxyModel for searching and sorting and a QSqlQueryModel for populating the table.
void ProxyModel::searchTable(QString name, QString type, QString date, QString time ){
if(name_ != name)
name_ = name;
if(type_ != type)
type_ = type;
if(date_ != date)
date_ = date;
if(time_ != time)
time_ = time;
invalidateFilter();
}
bool ProxyModel::filterAcceptsRow(int source_row,
const QModelIndex &source_parent) const{
QModelIndex indName = sourceModel()->index(source_row,
0, source_parent);
QModelIndex indType= sourceModel()->index(source_row,
4, source_parent);
QModelIndex indDate = sourceModel()->index(source_row,
2, source_parent);
QModelIndex indTime = sourceModel()->index(source_row,
3, source_parent);
if(
sourceModel()->data(indName).toString().toLower().contains(name_.toLower())
&&sourceModel()->data(indType).toString().toLower().contains(type_.toLower())
&&sourceModel()->data(indDate).toString().toLower().contains(date_.toLower())
&&sourceModel()->data(indTime).toString().toLower().contains(time_.toLower())
)
{
emit adjust();
return true;
}
return false;
}
After a successful search, I emit a signal from my proxy model to a slot where it adjusts the table height to fit the size of the rows.
connect(proxyModel, SIGNAL(adjust()), this, SLOT(dataChanged()));
And when the search button is clicked
connect(ui->searchBtn, &QToolButton::clicked, this, &AllVisitedPlaces::getSearchOptions);
I call the proxy model searchTable method with search parameters
void AllVisitedPlaces::getSearchOptions()
{
proxyModel->searchTable(ui->nameLineEdit->text(),
ui->typeLineEdit->text(),
ui->dateLineEdit->text(),
ui->timeLineEdit->text());
adjustTableSize();
}
void AllVisitedPlaces::dataChanged()
{
adjustTableSize();
this->verticalHeader->setSectionResizeMode(QHeaderView::ResizeToContents);
}
void AllVisitedPlaces::adjustTableSize()
{
QRect rect = ui->table->geometry();
int height = 0;
for (int i =0; i < proxyModel->rowCount() ; i++)
height+= ui->table->rowHeight(i);
rect.setHeight(18 + ui->table->horizontalHeader()->height() + height);
ui->table->setGeometry(rect);
verticalHeader->setSectionResizeMode(QHeaderView::Stretch);
}
The problem is, when the table re-sizes , I lose scrolling.
How can I fix that ?
Before re-sizing :
After re-sizing :
Why would you expect scrolling when you've resized the table to fit its contents?
The likely problem is that your Ui is broken in other ways and the table is obscured: it has been resized, but you can't see that because whatever widget the table view sits in doesn't manage the table widget properly. But we can't tell for sure because you didn't minimize your code to provide a complete compileable example of what you're doing. We're talking about <100 lines of code in all - surely it wouldn't be a big deal to just paste such a main.cpp in your question. See e.g. example 1 or example 2.
This is a bad design anyway since you're presuming that the table will fit on screen. Yet it won't: once the resizing works, you'll end up with a vertically huge window that cannot be used as some of its corners will extend past the screen, with no way to reach them to see the contents or to resize the window.
Finally, once you properly use layouts in your Ui design, the setGeometry() call on any widget below top-level is a no-op: it's the layout that controls the child widget geometry. The solution then is not to set the widget's geometry, but to set its minimum size instead.
You're facing an XY Problem: you're dead set on a solution, without telling us what it is that you're trying to achieve and making sure first that what you're after makes sense (as in: that it will actually lead to a usable Ui!).

QProgressDialog does not display immediately

I have a QProgressDialog that I want to display immediately
QProgressDialog *progress = new QProgressDialog("Downloading files...",
"Cancel", 0, 2*selection.size()+1);
progress->setMinimumDuration(0);
progress->setWindowModality(Qt::WindowModal);
progress->setValue(0);
Then I run a for loop with the task and finally assign the maximum value:
for (int i = 1; i < selection.size()+1; ++i)
{
progress->setValue(2*i-1);
if (progress->wasCanceled())
break;
do_half_task();
progress->setValue(2*i);
if (progress->wasCanceled())
break;
do_second_half();
}
progress->setValue(2*selection.size()+1);
But with this code, the dialog window borders appear, transparents without any widgets inside, and only gets filled with the label and progressbar when a full for loop has completed.
I think this is because only after a full loop has completed is that Qt can compute the duration of each step, and check that it will be >0 which I am setting as minimum duration. However, from the docs I see
minimumDuration : int
If set to 0, the dialog is always shown as soon as any progress is set. The default is 4000 milliseconds.
From where I would've expected the dialog to show up immediately in the first loop pass after setting progress->setValue(1).
How can I get my QProgressDialog to appear immediately?
Not sure if this matches for Qt too
but in C# if you run the execution of your code in the same thread like
ProcessBar p = new ProcessBar();
this.Controls.Add(p);
...
for (int i = 0; i < 100; i++) {
p.Value = i;
Thread.Sleep(1);
}
then you have the problem that your Form does not get to the code where it is redrawn.
Maybe try making your execution loop in a nother thread?
When you make the main thread go into a loop, it can't do any event processing until the loop ends and your method returns.
So it can only process all "paint update" requests once you are done.
You can call QCoreApplication::processEvents() inside the loop to allow it to return to event processing for a while.
To show the dialog immediately then just call QProgressDialog::show().

How to update a TableView with progress data for multiple ProgressBars?

I've started to extend the qGet DownloadManager to emit the progress of a TransferItem, so that i can connect to it. I'm inserting the progress data into a cell of a TableView model for display with an Delegate, finally the delegate paints the progress bar. That works in theory, but i'm running into the following
Problem: when there are multiple downloads in parallel, then i get progress updates from both signals into both cells!
Both progress bars show progress data, but the signal is kind of mixed and not unique to the current index (QModelIndex index / index.row()).
(Please ignore the small transitioning problem between UserRoles (after clicking the download button "ActionCell" is displayed and then "Install", before the "ProgressBar" shows up.). That is not the main problem here. My question is about the index problem.) The text "112" and "113" is the int index.row.
Questions:
How to update a TableView with progress data for multiple ProgressBars?
What must i change to render a progress bar for each download?
Source
Emit progress of a download
I've added the following things to re-emit the signal through the classes, until it bubbles up to the top, where it becomes connectable from the GUI.
a connection from QNetworkReply - downloadProgress(qint64,qint64) to TransferItem - updateDownloadProgress(qint64,qint64)
void TransferItem::startRequest()
{
reply = nam.get(request);
connect(reply, SIGNAL(readyRead()), this, SLOT(readyRead()));
connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
this, SLOT(updateDownloadProgress(qint64,qint64)));
connect(reply, SIGNAL(finished()), this, SLOT(finished()));
timer.start();
}
the SLOT function TransferItem - updateDownloadProgress(qint64,qint64) as receiver calculates the progress and stores it in progress (QMap<QString, QVariant>).
After the calculation the downloadProgress(this) signal is emitted.
// SLOT
void TransferItem::updateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
progress["bytesReceived"] = QString::number(bytesReceived);
progress["bytesTotal"] = QString::number(bytesTotal);
progress["size"] = getSizeHumanReadable(outputFile->size());
progress["speed"] = QString::number((double)outputFile->size()/timer.elapsed(),'f',0).append(" KB/s");
progress["time"] = QString::number((double)timer.elapsed()/1000,'f',2).append("s");
progress["percentage"] = (bytesTotal > 0) ? QString::number(bytesReceived*100/bytesTotal).append("%") : "0 %";
emit downloadProgress(this);
}
QString TransferItem::getSizeHumanReadable(qint64 bytes)
{
float num = bytes; QStringList list;
list << "KB" << "MB" << "GB" << "TB";
QStringListIterator i(list); QString unit("bytes");
while(num >= 1024.0 && i.hasNext()) {
unit = i.next(); num /= 1024.0;
}
return QString::fromLatin1("%1 %2").arg(num, 3, 'f', 1).arg(unit);
}
When a new download is enqueued, i'm connecting the emitted downloadProgress(this) to the Slot DownloadManager - downloadProgress(TransferItem*). (dl is DownloadItem which extends TransferItem).
void DownloadManager::get(const QNetworkRequest &request)
{
DownloadItem *dl = new DownloadItem(request, nam);
transfers.append(dl);
FilesToDownloadCounter = transfers.count();
connect(dl, SIGNAL(downloadProgress(TransferItem*)),
SLOT(downloadProgress(TransferItem*)));
connect(dl, SIGNAL(downloadFinished(TransferItem*)),
SLOT(downloadFinished(TransferItem*)));
}
Finally, i'm re-emitting the download progress one more time:
void DownloadManager::downloadProgress(TransferItem *item)
{
emit signalProgress(item->progress);
}
Now the TableView with Delegate, doDownload(index) and ProgressBarUpdater
QTableView
with added QSortFilterProxyModel (for case-insensitivity)
with added ColumnDelegate, which renders DownloadButton and ProgressBar based on custom UserRoles. The delegate handles the button click: the SIGNAL downloadButtonClicked(index) is emited from the editorEvent(event, model, option, index) method.
actionDelegate = new Updater::ActionColumnItemDelegate;
ui->tableView->setItemDelegateForColumn(Columns::Action, actionDelegate);
connect(actionDelegate, SIGNAL(downloadButtonClicked(QModelIndex)), this, SLOT(doDownload(QModelIndex)));
The doDownload method receives the index and fetches the download URL from the model. Then the URL is added to the DownloadManager
and i'm setting up a ProgressBarUpdater object to set the progress data to the model at the given index. Finally i'm, connecting downloadManager::signalProgress to progressBar::updateProgress and invoke the downloadManager::checkForAllDone to start the download processing.
void UpdaterDialog::doDownload(const QModelIndex &index)
{
QUrl downloadURL = getDownloadUrl(index);
if (!validateURL(downloadURL)) return;
QNetworkRequest request(downloadURL);
downloadManager.get(request); // QueueMode is Parallel by default
ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );
connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));
QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
}
The model update part: the ProgressBarUpdater takes the index and the progress and should update the model at the given index.
ProgressBarUpdater::ProgressBarUpdater(UpdaterDialog *parent, int currentIndexRow) :
QObject(parent), currentIndexRow(currentIndexRow)
{
model = parent->ui->tableView_1->model();
}
void ProgressBarUpdater::updateProgress(QMap<QString, QVariant> progress)
{
QModelIndex actionIndex = model->index(currentIndexRow, UpdaterDialog::Columns::Action);
// set progress to model
model->setData(actionIndex, progress, ActionColumnItemDelegate::DownloadProgressBarRole);
model->dataChanged(actionIndex, actionIndex);
}
The rendering part: i'm rendering the fake ProgressBar from the delegate; fetching the progress data with index.model()->data(index, DownloadProgressBarRole).
void ActionColumnItemDelegate::drawDownloadProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionProgressBarV2 opt;
opt.initFrom(bar);
opt.rect = option.rect;
opt.rect.adjust(3,3,-3,-3);
opt.textVisible = true;
opt.textAlignment = Qt::AlignCenter;
opt.state = QStyle::State_Enabled | QStyle::State_Active;
// get progress from model
QMap<QString, QVariant> progress =
index.model()->data(index, DownloadProgressBarRole).toMap();
QString text = QString::fromLatin1(" %1 %2 %3 %4 %5 ")
.arg(QString::number(index.row()))
.arg(progress["percentage"].toString())
.arg(progress["size"].toString())
.arg(progress["speed"].toString())
.arg(progress["time"].toString());
opt.minimum = 0;
opt.maximum = progress["bytesTotal"].toFloat();
opt.progress = progress["bytesReceived"].toFloat();
opt.text = text;
bar->style()->drawControl(QStyle::CE_ProgressBar,&opt,painter,bar);
}
I've added QString::number(index.row() to the progress bar text, so that each ProgressBar gets its row number rendered. In other words: the rendering is unique to the row, but the incoming progress data is somehow mixed.
I'm stuck on the index problem for a while now. Thank you in advance for your help.
Update: The issue is resolved!
Thank you very much ddriver!! I followed your suggestions and fixed it:
The DownloadManager tracks the progress for all transfers, and you keep each transfer item's data in the respective TransferItem.
The logical thing IMO would be to have a connection from each TransferItem to the corresponding ProgressBarUpdater, and emit from the transfer item.
However, in your case, you are reporting progress not from each individual transfer item, but from the download manager. So each time you are emitting a progress, you are emitting the progress for a particular transfer item to all progress bars.
connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));
So instead of a
TransferItem --progress--> CorrespondingUI
you have a:
TransferItem --transferItem--> DownloadManager --progress--> AllUIs
This leads to having one single and varying progress for all progress bars, which corresponds to the last download that happen to report progress before the UI updated. Which is why you get no more variation after the first download is completed, as the manager only updates the progress for the second.
Finally, i'm re-emitting the download progress one more time:
void DownloadManager::downloadProgress(TransferItem *item)
{
emit signalProgress(item->progress);
}
And who exactly needs an anonymous progress, containing no information whatsoever to which transfer it applies? Aside from the bug of course.
Would you be so nice to explain, how to simplify it?
I was at the end of my mental rope yesterday when I commented, on a clear head it doesn't look all that overdone, but still I'd probably go for something more streamlined, involving 3 key components only:
DownloadsManager -> DownloadController -> UI
-> DownloadController -> UI
It just seems redundant to have a DownloadItem and then also a TransferItem, considering that a download is a transfer.
The model and view are totally unnecessary as well, as is storing the progress in the model rather than just having it as a member of the progress bar. You could go with just a regular widget for each download, and place them in a vertical layout.
Update:
The excessive, unnecessary compartmentalization has led to a level of fragmentation which makes it hard to get to the data, needed to make it work once you put everything together. The main issue is you have no way to tie a transfer item to the right progress bar updater, and since you still haven't posted all of the relevant code, the simplest possible solution I can offer involves the following minor changes:
// in DownloadManager
void signalProgress(QMap<QString, QVariant>); // this signal is unnecessary, remove
void DownloadManager::downloadProgress(TransferItem *item) // change this
{
registry[item->request.url()]->updateProgress(item->progress);
}
QMap<QUrl, ProgressBarUpdater *> registry; // add this
// in UpdaterDialog
void UpdaterDialog::doDownload(const QModelIndex &index)
{
QUrl downloadURL = getDownloadUrl(index);
if (!validateURL(downloadURL)) return;
QNetworkRequest request(downloadURL);
downloadManager.get(request); // QueueMode is Parallel by default
ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );
// remove the connection - source of the bug, instead register the updater
downloadManager.registry[downloadURL] = progressBar;
QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
}
That's pretty much it, the progress updater is associated with the URL, and in DownloadManager::downloadProgress instead of emitting the progress to all progress updaters, you simply lookup the one which actually corresponds to the particular download, and only update its progress. It is somewhat clumsy, but as I said, if your design is proper, it would not be needed and you wouldn't have the problem in the first place.
There are other solutions as well:
change the DownloadManager's signal to void signalProgress(TransferItem *), and the body of downloadProgress toemit signalProgress(item); , change to void ProgressBarUpdater::updateProgress(TransferItem *), and in the body compare the transfer item's request's url to the one in the model at the currentIndexRow, and only model-setData() if it is the same. This solution is not very efficient, since it will emit to all progress updaters just to modify one.
cut out the middleman, what I've been suggesting from the start, make DownloadManager ::get() return a pointer to the DownloadItem/TransferItem created in its body, then in UpdaterDialog::doDownload() you can connect the transfer item directly to the appropriate progress updater, so you will no longer need DownloadManager::downloadProgress() and the signalProgress signal, you only need to change the signature of the signal in TransferItem to void downloadProgress(QMap<QString, QVariant>); and emit the progress rather than the item. This is actually the most efficient solution, as it involves nothing extra, jsut the removal of unnecessary stuff.

Why doesn't QTableView row count update?

I created a QAbstractTableModel called PresetTableModel, and connected it to a QTableView. I implemented the rowCount, columnCount, and data functions. Everything works if I have rowCount return a fixed number, but as soon as I get it to return a variable value, the list view doesn't show any rows. The debug statement in the code below shows the size value starting at 0, but changing to 9 once the list gets populated.
int PresetTableModel::rowCount(const QModelIndex & /*parent*/) const
{
qDebug() << preset_list.count();
return preset_list.size();
}
Is there something else I need to do to force it to update the number of rows?
When modifying the underlying data, you must use the model's notification mechanism to notify the views. E.g, when appending data:
beginInsertRows(QModelIndex(), preset_list.size(), preset_list.size()+1); //notify that two rows will be appended (rows size() and size() + 1)
preset_list.append(something);
preset_list.append(somethingelse);
endInsertRows(); //notify views that you're done with modifying the underlying data
Accordingly, you have to call beginRemoveRows() and endRemoveRows() when removing rows, and emit dataChanged() when existing entries are updated.
On a side note, your rowCount() method should read
if (!parent.isValid())
return preset_list.size(); //top-level: return list size
else
return 0; //list item: no further children (flat list)
to limit the depth. Otherwise each item in the list has again preset_list.size() entries.
i use:
void refresh() {
emit dataChanged(index(0, 0),
index(rowCount(), columnCount())); // update whole view
emit layoutChanged();
}

How to set font Bold to a particular row in table widget

i want to set my font as bold in particular row column position of my tablewidget.
I did like this but getting break.
QFont font("Helvetica", 12, QFont::Bold);
overviewTable->item(2,2)->setFont(font);
Please Help
I think everything is ok. Here what docs said:
void QTableWidgetItem::setFont ( const QFont & font )
Sets the font used to display the item's text to the given font.
Maybe your overviewTable const?
ADDED:
This variant works fine for my Qt 4.6:
tableWidget = new QTableWidget(12, 3, this);
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 3; j++) {
QTableWidgetItem *newItem = new QTableWidgetItem(tr("%1").arg(
(i+1)*(j+1)));
tableWidget->setItem(i, j, newItem);
}
}
QFont font;
font.setBold(true);
tableWidget->item(2, 2)->setFont(font);
Maybe you are getting break because you didn't call setItem() to set an item for the cell (2, 2) before you use overviewTable->item(2,2). As the Qt document says,
QTableWidgetItem * QTableWidget::item(int row, int column) const
Returns the item for the given row and column if one has been set;
otherwise returns 0.
That is, your overviewTable->item(2,2) probably returns 0, thus causes a Segmentation fault in the setFont() call.
So your means to setting font is completely right. You just need to call setItem() at first as mosg's answer suggests.
ADDED:
if your overviewTable is a QTableWidget created in Qt Designer, then in the Designer a double-click on a cell (just to enter its editing mode, no need to actually enter anything) will have the effect of calling setItem() for that cell. Later in your code you can directly using the item() function without having to call setItem() first.

Resources