Autofill to correct size in Qt Tables - c++

I am trying to learn Qt by doing some project, and would like quick pointer on one part of my requirement.
I have database with multi-line passages, that I want to show in Qt using some view.
What I additionally want is that user does not have to re-size the window in order to read, so, if big passage comes in, then size shrinks, and will small passage, the font increase such that it takes the total space to display.
Kindly suggest :
what logic or functionality will suit to shrink and expand size, or is there a widget/view that already do so(by modifying the property) or suggestion on how to achieve it.
Same question again, to show shrink/expanded things, but using only using tree view. Then can I do this in tree view? And how?

Here is a way you could do it with a custom QItemDelegate.
Note that this solution isn't complete, you still need to do some work here.
First, some code to setup a QStandardItemModel and QTreeView.
The View uses a custom Delegate, which is described below.
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QStandardItemModel model(4, 4);
for (int row = 0; row < 4; ++row) {
for (int column = 0; column < 4; ++column) {
QStandardItem *item = new QStandardItem(QString("row %0, column %1").arg(row).arg(column));
model.setItem(row, column, item);
}
}
Delegate delegate;
QTreeView view;
view.setItemDelegate(&delegate);
view.setModel(&model);
view.setWindowTitle("QTreeView with custom delegate");
view.show();
return a.exec();
}
Here comes the code for the Delegate.
It looks how much space is available for the text, and then tries to find a font size that fits.
I'm currently only checking the width and ignoring height.
class Delegate : public QItemDelegate
{
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
};
void Delegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QRect availableSpace = option.rect;
// padding taking place, don't know how to find out how many pixels are padded.
// for me, subtracting 6 looked ok, but that's not a good solution
availableSpace.setWidth(availableSpace.width() - 6);
QString text = index.data().toString();
QStyleOptionViewItem newOption(option);
// initial font size guess
float size = 20;
int width;
// try to make font smaller until the text fits
do
{
newOption.font.setPointSizeF(size);
size -= .1;
width = QFontMetrics(newOption.font).width(text);
}
while (width > availableSpace.width());
newOption.textElideMode = Qt::ElideNone;
// call the parent paint method with the new font size
QItemDelegate::paint(painter, newOption, index);
}
This is what the result looks like:

Related

How to draw different lines inside column of QTableView depending on data in cells in near column?

I want to draw lines inside column that show possible connections between different signals(Further, I also want to make radiobuttons on them to choose what connections are active).
But now I have trouble that delegates allow me to SetItemDelegate only for all column or all row. So I can't just make different blocks of this lines like vertical line, corner lines, horizontal line and then paint them depending on data in cells. I attached an example image. What should I use to draw something like this?
Something like:
Define a new style, override drawPrimitive method and do custom painting?
Could you show me an example, please?
Lines example
What I have for now
My main code for creating rows with signals(I take them from .txt file for simulation for now):
int IPFilesize = IPfilespl.size();
ui->CompTab->setRowCount(IPFilesize);
for (int i = 0; i<IPFilesize; i++)
{
QWidget *ChBx = new QWidget();
QCheckBox *pCheckBox = new QCheckBox();
QHBoxLayout *pLayout = new QHBoxLayout(ChBx);
pLayout->addWidget(pCheckBox);
pLayout->setAlignment(Qt::AlignCenter);
pLayout->setContentsMargins(0,0,0,0);
ChBx->setLayout(pLayout);
ui->CompTab->setCellWidget(i, 0, ChBx);
//connect(ChBx,SIGNAL(clicked()),this,SLOT(checkboxClicked()));
}
for (int ii = 0; ii<IPFilesize; ii++)
{
ui->CompTab->setItem(ii, 2, new QTableWidgetItem(IPfilespl.at(ii)) );
//connect(ChBx,SIGNAL(clicked()),this,SLOT(checkboxClicked()));
}
ui->CompTab->setItemDelegateForColumn(1, new WireDelegateDown());
Header code
class WireDelegate: public QStyledItemDelegate { protected: void paint(QPainter* painter, const QStyleOptionViewItem& opt, const QModelIndex& index) const {
int x = opt.rect.x();
double y = opt.rect.y();
QPoint c = opt.rect.center();
double centerx = c.x();
double centery = c.y();
double r = opt.rect.right();
double width = opt.rect.width();
double height = opt.rect.height();
QPainterPath path;
path.addRect(centerx, centery-height/2, 5.0, height/2);
path.moveTo(0, 0);
path.addRect(centerx, centery, width/2, 5.0);
path = path.simplified();
painter->drawPath(path);
Your item delegate could be a subclass of QAbstractItemDelegate. Then you can set its type with a property like shapeType (or whatever you name it). Based on the shapeType, you can do internal painting stuff from within the reimplemented paint method.
void MyConnectionDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (m_shapeType == ShapeType::horizontalLine) {
//..... Your fancy drawings happens here based on shapetype
} else if (m_shapeType == ShapeType::verticalLine) {
.
.
.
As I see in the picture (your desired result) it's not going to be simple and it can get quite complicated to implement such behavior. You will have to calculate the width, height, position of lines, colors, dots, arrows, nodes, etc for each delegate. When you exactly know which entities should be drawn in each cell, painting them using QPainter is a simple task.
You might consider whether QTableView is getting in the way more than it helps you. The built-in widgets are fantastic, but often, I've found that when I need to venture outside the realm of what they were specifically designed to do, I end up spending more time working around them than I get benefit. I don't know the right solution for what you're doing, but if it were me, I'd explore writing my own view based on QAbstractItemView and then just doing my own custom painting for the whole thing.
The downside of doing that is that QTableView provides a lot of interaction support for you, so if the interaction is a big benefit to you, then you have to write your own as well. So it's a trade-off. It's also a possibility that the built-in interaction for QTableView also gets in the way of what you're trying to do. It can go either way.

How to animate the color of a QTableview cell in time once it's value gets updated?

I want to animate the color (in time) of a QTableview cell once it's value is updated via the connected data model to attract the end-users attention that something has changed.
The idea is that the color changes in gradients of f.i. blue, starts off blue just after the value change and fades to white in about 1 ~ 2 seconds.
I guess one has to use the QStyledItemDelegate here since I use the model-view concept (http://doc.qt.io/qt-5/model-view-programming.html).
One needs a trigger for the cell's value-change for the animation to start, this can be achieved via the paint() method since it is called on a value change. One can figure out the row and column, from the index parameter that is passed to paint(), to get a hold of which cell to animate.
So far so good, you can set the color of that cell to let's say blue.
Here comes the problem; the color shall fade towards white in time steps. So I was thinking about a QTimer in the QStyledItemDelegate class and maintain a kinda bookkeeping for the cell's that are being animated (could be a simple list of countdown values that are used to calculate the blue-gradient color. The lower the value the more the gradient goes towards white, once 0 the result is white which is the default color of the cell. On each QTimer timeout() event all values not equal to 0 are lowered by 1. The QStyledItemDelegate is only connected to the row of the QTableview that I want to color-animate i.e. where the value items are displayed.
Problem I face is that:
paint() is a const method so you cannot change any class
parameters.
how to re-paint the cell color on a QTimer event
(re-paint the whole QTableview is not god-style)
I managed to get it to work but I consider it a dirty solution. What I did is maintain the bookkeeping of the color animation for each cell in the data-model. I consider it a dirty solution because the color-animation is only a visual aspect so imho it should not reside in the data-model. In this way it is also not a portable solution i.e. in another project you have to rework a lot to get it to work.
I stripped down my application to the core problem. Full code can be found here (a working application): https://github.com/fruitCoder123/animated_tableview_cell
void TableViewDelegateValueWritable::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
// Paint background
uint8_t red_gradient = calculate_color_gradient(RGB_RED_MAX, RGB_RED_MIN, red_gradient_step_size, m_step_value);
uint8_t green_gradient = calculate_color_gradient(RGB_GREEN_MAX, RGB_GREEN_MIN, green_gradient_step_size, m_step_value);
painter->fillRect(option.rect, QColor(red_gradient, green_gradient, 255));
// Paint text
QStyledItemDelegate::paint(painter, option, index);
}
uint8_t TableViewDelegateValueWritable::calculate_color_gradient(const uint8_t MAX_COLOR, const uint8_t MIN_COLOR, const uint8_t step_size, uint8_t step) const
{
uint16_t color = (step_size * (1 + MAX_COLOR_GRADIENT_STEP - step)) + MIN_COLOR;
// Handle overflow and rounding errors
if(color > MAX_COLOR || color > (MAX_COLOR-(step_size/2)))
color = MAX_COLOR;
return static_cast<uint8_t>(color);
}
void TableViewDelegateValueWritable::gradient_timer_elapsed()
{
if(m_step_value)
{
m_step_value--;
m_timer->start(GRADIENT_TIMEOUT_VALUE);
//this->paint(m_painter, m_option, m_model_index);
}
}
I spent a horrific amount of hours to find a nice solution. I started with Qt a month ago so maybe I lack knowledge. Hopefully someone can give a hint how to solve it in a nice way - encapsulated in the view and not entangled with the data-model.
For the stated problems :
paint() is declared as const, you can use a mutable variable member and modify it whenever you want. For example :
class TableViewDelegateValueWritable : public QStyledItemDelegate
{
Q_OBJECT
mutable QTableView * m_view;
public:
explicit TableViewDelegateValueWritable(QTableView * view, QObject *parent){
m_view = view;
//...
}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
//...
int nFrame m_view->getFrameOfIndex(index);
drawFrame (painter, nFrame );
m_view->setFrameOfIndex(index, ++nFrame);
//...
}
//class body
}
Repaint the whole QTableView is not a god-style but repaint only the QTableView's viewport is a good option, in the MainWindow constructor :
m_db->insert_record(QString("my_val_1"), "0");
m_db->insert_record(QString("my_val_2"), "0");
m_db->insert_record(QString("my_val_3"), "0");
QTimer * timer = new QTimer( this );
connect( timer, &QTimer::timeout, this, [this](){
ui->tableView->viewport()->repaint();
});
timer->start( TIME_RESOLUTION ); //set to 1000ms
Here is a snippet to animate a cell when it was modified, the color will be changed each 1s, the method used is to subclass QSqlTableModel to track the modified cell (via dataChanged signal) :
enum MyDataRole {
ItemModifiedRole = Qt::UserRole + 1
};
class MySqlTableModel : public QSqlTableModel {
Q_OBJECT
QMap<int, QVariant > mapTimeout;
public:
MySqlTableModel( QObject *parent = Q_NULLPTR, QSqlDatabase db = QSqlDatabase() )
:QSqlTableModel( parent, db ) {
connect( this, &QSqlTableModel::dataChanged,
this, [this]( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles )
{
for(int i = topLeft.row(); i <= bottomRight.row(); i ++ ){
mapTimeout.insert( i , QDateTime::currentDateTime() );
}
} );
}
//this data function will be called in the delegate paint() function.
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE
{
if( role != ItemModifiedRole )
return QSqlTableModel::data( idx, role );
QMap<int, QVariant>::const_iterator it = mapTimeout.find( idx.row() );
return it == mapTimeout.end() ? QVariant() : it.value();
}
void clearEffects() {
mapTimeout.clear();
}
};
And the delegate paint() function:
// background color manipulation
void TableViewDelegateValueWritable::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QSqlTableModel * db_model = (QSqlTableModel *)index.model();
QVariant v = db_model->data( index, ItemModifiedRole );
if( !v.isNull() ){
QDateTime dt = v.toDateTime();
int nTimePassed = dt.secsTo( QDateTime::currentDateTime() );
int step_value = nTimePassed + 2;
uint8_t red_gradient = calculate_color_gradient( RGB_RED_MAX, RGB_RED_MIN, red_gradient_step_size, step_value );
uint8_t green_gradient = calculate_color_gradient( RGB_GREEN_MAX, RGB_GREEN_MIN, red_gradient_step_size, step_value );
painter->fillRect( option.rect, QColor( red_gradient, green_gradient, 255) );
}
// Paint text
QStyledItemDelegate::paint(painter, option, index);
}
You should use QSqlRecord way if possible instead of executing a sql statement. In fact, after each sql statement was executed, you call the select() function, this will reset the whole table model. Here is an example of insert/update record :
void dbase::insert_record(const QString &signal_name, const QString &value)
{
QSqlRecord r = db_model->record();
r.setValue( "signal_name", signal_name );
r.setValue( "signal_value", value );
db_model->insertRecord(-1, r );
}
void dbase::update_record(const QString &signal_name, const QString &new_value)
{
for(int row = 0; row < db_model->rowCount(); row ++ ){
QSqlRecord r = db_model->record( row );
if( r.value("signal_name").toString() == signal_name ){
r.setValue("signal_value", new_value );
db_model->setRecord( row, r );
break;
}
}
}

Is it possible to add a custom widget into a QListView?

I have a large log data (100, 1000, 100000, ... records) and I want to visualize it in the following manner:
Which widget (e.g. QListView, QListWidget) should I use and how, in order to stay away from performance and memory problems?
Is it possible to add a custom widget into a QListView?
Please, read about:
How to display a scrollable list with a substantial amount of widgets as items in a Qt C++ app?
I want to show every log message in the above format
Solution
To achieve the desired result and stay away from performance issues, even with a very long data log, use a QListView with a custom delegate:
Create a subclass of QStyledItemDelegate, say Delegate
Reimplement the QStyledItemDelegate::paint method to do the custom drawing
Reimplement the QStyledItemDelegate::sizeHint to report the correct size of the items in the list
Use the custom delegate in the view by calling QAbstractItemView::setItemDelegate
Example
I have prepared a working example for you in order to demonstrate how the proposed solution could be implemented and used in an application.
The essential part of the example is the way the delegate paints the items in the list view:
void Delegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyleOptionViewItem opt(option);
initStyleOption(&opt, index);
const QPalette &palette(opt.palette);
const QRect &rect(opt.rect);
const QRect &contentRect(rect.adjusted(m_ptr->margins.left(),
m_ptr->margins.top(),
-m_ptr->margins.right(),
-m_ptr->margins.bottom()));
const bool lastIndex = (index.model()->rowCount() - 1) == index.row();
const bool hasIcon = !opt.icon.isNull();
const int bottomEdge = rect.bottom();
QFont f(opt.font);
f.setPointSize(m_ptr->timestampFontPointSize(opt.font));
painter->save();
painter->setClipping(true);
painter->setClipRect(rect);
painter->setFont(opt.font);
// Draw background
painter->fillRect(rect, opt.state & QStyle::State_Selected ?
palette.highlight().color() :
palette.light().color());
// Draw bottom line
painter->setPen(lastIndex ? palette.dark().color()
: palette.mid().color());
painter->drawLine(lastIndex ? rect.left() : m_ptr->margins.left(),
bottomEdge, rect.right(), bottomEdge);
// Draw message icon
if (hasIcon)
painter->drawPixmap(contentRect.left(), contentRect.top(),
opt.icon.pixmap(m_ptr->iconSize));
// Draw timestamp
QRect timeStampRect(m_ptr->timestampBox(opt, index));
timeStampRect.moveTo(m_ptr->margins.left() + m_ptr->iconSize.width()
+ m_ptr->spacingHorizontal, contentRect.top());
painter->setFont(f);
painter->setPen(palette.text().color());
painter->drawText(timeStampRect, Qt::TextSingleLine,
index.data(Qt::UserRole).toString());
// Draw message text
QRect messageRect(m_ptr->messageBox(opt));
messageRect.moveTo(timeStampRect.left(), timeStampRect.bottom()
+ m_ptr->spacingVertical);
painter->setFont(opt.font);
painter->setPen(palette.windowText().color());
painter->drawText(messageRect, Qt::TextSingleLine, opt.text);
painter->restore();
}
The complete code of the example is available on GitHub.
Result
As written, the given example produces the following result:

How to make a fast QTableView with HTML-formatted and clickable cells?

I'm making a dictionary program that displays word definitions in a 3-column QTableView subclass, as user types them, taking data from a QAbstractTableModel subclass. Something like that:
I want to add various formatting to the text, I'm using QAbstractItemView::setIndexWidget to add a QLabel to each cell as data comes in:
WordView.h
#include <QTableView>
class QLabel;
class WordView : public QTableView {
Q_OBJECT
public:
explicit WordView(QWidget *parent = 0);
void rowsInserted(const QModelIndex &parent, int start, int end);
private:
void insertLabels(int row);
void removeLabels(int row);
};
WordView.cpp
#include <QLabel>
#include "WordView.h"
WordView::WordView(QWidget *parent) :
QTableView(parent)
{}
void WordView::rowsInserted(const QModelIndex &parent, int start, int end) {
QTableView::rowsInserted(parent, start, end);
for (int row = start; row <= end; ++row) {
insertLabels(row);
}
}
void WordView::insertLabels(int row) {
for (int i = 0; i < 3; ++i) {
auto label = new QLabel(this);
label->setTextFormat(Qt::RichText);
label->setAutoFillBackground(true);
QModelIndex ix = model()->index(row, i);
label->setText(model()->data(ix, Qt::DisplayRole).toString()); // this has HTML
label->setWordWrap(true);
setIndexWidget(ix, label); // this calls QAbstractItemView::dataChanged
}
}
However, this is very slow - it takes around 1 second to refresh 100 rows (remove all, then add 100 new ones) like that. With original QTableView it worked fast, but I did not have formatting and ability to add links (cross-references in dictionary). How to make this much faster? Or what other widget can I use to display that data?
My requirements are:
Adding/removing around 1000 rows in ~0.2s, where around 30 will be visible at once
Clickable, multiple internal links (<a>?) in each cell (e.g. QLabel has that, QItemDelegate might have been fast, but I don't know how to get info which link I clicked there)
Formatting that allows different font sizes and colors, word wrap, different cell heights
I'm not really dead-set on QTableView, anything that looks like a scrollable table and looks consistent with Qt graphics is okay
Notes:
I tried making a single label with HTML <table> instead, but it wasn't much faster. Seems like QLabel isn't the way to go.
Data in the sample courtesy of the JMdict project.
I solved the problem by putting together few answers and looking at Qt's internals.
A solution which works very fast for static html content with links in QTableView is as folows:
Subclass QTableView and handle mouse events there;
Subclass QStyledItemDelegate and paint the html there (contrary to RazrFalcon's answer, it is very fast, as only a small amount of cells is visible at a time and only those have paint() method called);
In subclassed QStyledItemDelegate create a function that figures out which link was clicked by QAbstractTextDocumentLayout::anchorAt(). You cannot create QAbstractTextDocumentLayout yourself, but you can get it from QTextDocument::documentLayout() and, according to Qt source code, it's guaranteed to be non-null.
In subclassed QTableView modify QCursor pointer shape accordingly to whether it's hovering over a link
Below is a complete, working implementation of QTableView and QStyledItemDelegate subclasses that paint the HTML and send signals on link hover/activation. The delegate and model still have to be set outside, as follows:
wordTable->setModel(&myModel);
auto wordItemDelegate = new WordItemDelegate(this);
wordTable->setItemDelegate(wordItemDelegate); // or just choose specific columns/rows
WordView.h
class WordView : public QTableView {
Q_OBJECT
public:
explicit WordView(QWidget *parent = 0);
signals:
void linkActivated(QString link);
void linkHovered(QString link);
void linkUnhovered();
protected:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
private:
QString anchorAt(const QPoint &pos) const;
private:
QString _mousePressAnchor;
QString _lastHoveredAnchor;
};
WordView.cpp
#include <QApplication>
#include <QCursor>
#include <QMouseEvent>
#include "WordItemDelegate.h"
#include "WordView.h"
WordView::WordView(QWidget *parent) :
QTableView(parent)
{
// needed for the hover functionality
setMouseTracking(true);
}
void WordView::mousePressEvent(QMouseEvent *event) {
QTableView::mousePressEvent(event);
auto anchor = anchorAt(event->pos());
_mousePressAnchor = anchor;
}
void WordView::mouseMoveEvent(QMouseEvent *event) {
auto anchor = anchorAt(event->pos());
if (_mousePressAnchor != anchor) {
_mousePressAnchor.clear();
}
if (_lastHoveredAnchor != anchor) {
_lastHoveredAnchor = anchor;
if (!_lastHoveredAnchor.isEmpty()) {
QApplication::setOverrideCursor(QCursor(Qt::PointingHandCursor));
emit linkHovered(_lastHoveredAnchor);
} else {
QApplication::restoreOverrideCursor();
emit linkUnhovered();
}
}
}
void WordView::mouseReleaseEvent(QMouseEvent *event) {
if (!_mousePressAnchor.isEmpty()) {
auto anchor = anchorAt(event->pos());
if (anchor == _mousePressAnchor) {
emit linkActivated(_mousePressAnchor);
}
_mousePressAnchor.clear();
}
QTableView::mouseReleaseEvent(event);
}
QString WordView::anchorAt(const QPoint &pos) const {
auto index = indexAt(pos);
if (index.isValid()) {
auto delegate = itemDelegate(index);
auto wordDelegate = qobject_cast<WordItemDelegate *>(delegate);
if (wordDelegate != 0) {
auto itemRect = visualRect(index);
auto relativeClickPosition = pos - itemRect.topLeft();
auto html = model()->data(index, Qt::DisplayRole).toString();
return wordDelegate->anchorAt(html, relativeClickPosition);
}
}
return QString();
}
WordItemDelegate.h
#include <QStyledItemDelegate>
class WordItemDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit WordItemDelegate(QObject *parent = 0);
QString anchorAt(QString html, const QPoint &point) const;
protected:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
};
WordItemDelegate.cpp
#include <QPainter>
#include <QTextDocument>
#include <QAbstractTextDocumentLayout>
#include "WordItemDelegate.h"
WordItemDelegate::WordItemDelegate(QObject *parent) :
QStyledItemDelegate(parent)
{}
QString WordItemDelegate::anchorAt(QString html, const QPoint &point) const {
QTextDocument doc;
doc.setHtml(html);
auto textLayout = doc.documentLayout();
Q_ASSERT(textLayout != 0);
return textLayout->anchorAt(point);
}
void WordItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
auto options = option;
initStyleOption(&options, index);
painter->save();
QTextDocument doc;
doc.setHtml(options.text);
options.text = "";
options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &option, painter);
painter->translate(options.rect.left(), options.rect.top());
QRect clip(0, 0, options.rect.width(), options.rect.height());
doc.drawContents(painter, clip);
painter->restore();
}
QSize WordItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
QStyleOptionViewItemV4 options = option;
initStyleOption(&options, index);
QTextDocument doc;
doc.setHtml(options.text);
doc.setTextWidth(options.rect.width());
return QSize(doc.idealWidth(), doc.size().height());
}
Note that this solution is fast only because a small subset of rows is rendered at once, and therefore not many QTextDocuments are rendered at once. Automatic adjusting all row heights or column widths at once will still be slow. If you need that functionality, you can make the delegate inform the view that it painted something and then making the view adjust the height/width if it hasn't before. Combine that with QAbstractItemView::rowsAboutToBeRemoved to remove cached information and you have a working solution. If you're picky about scrollbar size and position, you can compute average height based on a few sample elements in QAbstractItemView::rowsInserted and resize the rest accordingly without sizeHint.
References:
RazrFalcon's answer for pointing me to the right direction
Answer with code sample to render HTML in QTableView: How to make item view render rich (html) text in Qt
Answer with code sample on detecting links in QTreeView: Hyperlinks in QTreeView without QLabel
QLabel's and internal Qt's QWidgetTextControl's source code on how to handle mouse click/move/release for links
Many thanks for these code examples, it helped me implement similar functionalaity in my application. I'm working with Python 3 and QT5 and I would like to share my Python code, is case it may be helpful implementing this in Python.
Note that if you are using QT Designer for the UI design, you can use "promote" to change a regular "QTableView" widget to use your custom widget automatically when converting the XML to Python code with "pyuic5".
Code as follows:
from PyQt5 import QtCore, QtWidgets, QtGui
class CustomTableView(QtWidgets.QTableView):
link_activated = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
self.parent = parent
super().__init__(parent)
self.setMouseTracking(True)
self._mousePressAnchor = ''
self._lastHoveredAnchor = ''
def mousePressEvent(self, event):
anchor = self.anchorAt(event.pos())
self._mousePressAnchor = anchor
def mouseMoveEvent(self, event):
anchor = self.anchorAt(event.pos())
if self._mousePressAnchor != anchor:
self._mousePressAnchor = ''
if self._lastHoveredAnchor != anchor:
self._lastHoveredAnchor = anchor
if self._lastHoveredAnchor:
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
else:
QtWidgets.QApplication.restoreOverrideCursor()
def mouseReleaseEvent(self, event):
if self._mousePressAnchor:
anchor = self.anchorAt(event.pos())
if anchor == self._mousePressAnchor:
self.link_activated.emit(anchor)
self._mousePressAnchor = ''
def anchorAt(self, pos):
index = self.indexAt(pos)
if index.isValid():
delegate = self.itemDelegate(index)
if delegate:
itemRect = self.visualRect(index)
relativeClickPosition = pos - itemRect.topLeft()
html = self.model().data(index, QtCore.Qt.DisplayRole)
return delegate.anchorAt(html, relativeClickPosition)
return ''
class CustomDelegate(QtWidgets.QStyledItemDelegate):
def anchorAt(self, html, point):
doc = QtGui.QTextDocument()
doc.setHtml(html)
textLayout = doc.documentLayout()
return textLayout.anchorAt(point)
def paint(self, painter, option, index):
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
if options.widget:
style = options.widget.style()
else:
style = QtWidgets.QApplication.style()
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
options.text = ''
style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, options)
painter.save()
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
painter.translate(0, 0.5*(options.rect.height() - doc.size().height()))
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(options.rect.width())
return QtCore.QSize(doc.idealWidth(), doc.size().height())
In your case QLabel (re)painting is slow, not QTableView.
On other hand, QTableView does not support formated text at all.
Probably, your only way, is to create your own delegate, QStyledItemDelegate, and make your own painting and click processing in it.
PS: yes, you can use QTextDocument for rendering html inside delegate, but it will be slow too.
I use a slighted improved solution based on Xilexio code. There is 3 fundamental differences:
Vertical alignment so if you put the text in a cell higher than the text it will be center aligned and not top aligned.
The text will be right shifted if the cell contains an icon so the icon will not be displayed above the text.
The widget style to highlighted cells will be followed, so you select this cell, the colors will behave similar to other cells without the delegate.
Here is my code of the paint() function (the rest of the code remains the same).
QStyleOptionViewItemV4 options = option;
initStyleOption(&options, index);
painter->save();
QTextDocument doc;
doc.setHtml(options.text);
options.text = "";
options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter);
QSize iconSize = options.icon.actualSize(options.rect.size);
// right shit the icon
painter->translate(options.rect.left() + iconSize.width(), options.rect.top());
QRect clip(0, 0, options.rect.width() + iconSize.width(), options.rect.height());
painter->setClipRect(clip);
QAbstractTextDocumentLayout::PaintContext ctx;
// Adjust color palette if the cell is selected
if (option.state & QStyle::State_Selected)
ctx.palette.setColor(QPalette::Text, option.palette.color(QPalette::Active, QPalette::HighlightedText));
ctx.clip = clip;
// Vertical Center alignment instead of the default top alignment
painter->translate(0, 0.5*(options.rect.height() - doc.size().height()));
doc.documentLayout()->draw(painter, ctx);
painter->restore();

How to put an image and a QProgressBar inside a QTableView?

I'm developing some kind of download manager and display the file name, it's size and the remaining bytes in a QTableView. Now I want to visualize the progress with a QProgressBar and display an image (to indicate whether it's an down- or upload). How can I add or display a QProgressBar and an image inside the QTableView?
If you are using QTableView, I presume you use a model linked to this view.
One solution would be to use delegates (see QItemDelegate) to paint the progress, In QItemDelegate::paint method you have to define, use QStyle of the widget (widget->style()) to paint the progress (use QStyle::drawControl with QStyle::CE_ProgressBarContents as control identifier).
Check the documentation from the example Star Delegate, to see how to define the delegate for the column you need.
Later edit: Example of defining the delegate paint method (code sketch, not really tested, take it as a principle, not fully working).
void MyDelegate::paint ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
{
QStyleOptionProgressBar progressStyle;
progressStyle.rect = option.rect; // Maybe some other initialization from option would be needed
// For the sake of the example, I assume that the index indicates the progress, and the next two siblings indicate the min and max of the progress.
QModelIndex minIndex = index.sibling( index.row(), index.column() + 1);
QModelIndex maxIndex = index.sibling( index.row(), index.column() + 2);
progressStyle.minimum = qvariant_cast< int>( minIndex.data( Qt::UserRole));
progressStyle.maximum = qvariant_cast< int>( maxIndex.data( Qt::UserRole));
progressStyle.progress = qvariant_cast< int>( index.data( Qt::UserRole));
progressStyle.textVisible = false;
qApp->style()->drawControl( QStyle::CE_ProgressBarContents, progressStyleOption, painter);
}
TrackDelegate::TrackDelegate(QObject *parent)
: QItemDelegate(parent)
--------------------------------------------------------------------------------
void TrackDelegate::paint( QPainter* painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyleOptionViewItem viewOption(option);
QImage image(m_RowBackGroundImagePath);
QPixmap pixmap(m_RowBackGroundImagePath);
qDebug()<<"forward"<<pixmap.width()<<pixmap.height();
pixmap.scaled(option.rect.width(),option.rect.height());
qDebug()<<"back"<<pixmap.width()<<pixmap.height();
qDebug()<<option.rect.width()<<option.rect.height();
QBrush brush(pixmap);
painter->save();
painter->fillRect(option.rect, brush/*QColor(238, 233, 233, 255)*/);
painter->restore();
viewOption.rect = QRect(option.rect.x(), option.rect.y(), option.rect.width(), option.rect.height());
// viewOption.palette.setColor(QPalette::Text, QColor(Qt::red));
// viewOption.palette.setBrush ( QPalette::ButtonText, brush1);
QItemDelegate::paint(painter, viewOption,index);
int progress = index.model()->data(index,Qt::DisplayRole).toInt();
QStyleOptionProgressBar progressBarOption;
progressBarOption.rect = QRect(option.rect.x(), option.rect.y()+(SETHEIGHT - PROGRESSBARHEIGHT)/2, option.rect.width(), /*option.rect.height()*/PROGRESSBARHEIGHT);
//qDebug()<<progressBarOption.rect.x()<<progressBarOption.rect.y()<<progressBarOption.rect.height()<<progressBarOption.rect.width();
//qDebug()<<option.rect.x()<<option.rect.y()<<option.rect.height()<<option.rect.width();
progressBarOption.state |= QStyle::State_Enabled;
progressBarOption.direction = QApplication::layoutDirection();
progressBarOption.fontMetrics = QApplication::fontMetrics();
progressBarOption.minimum = 0;
progressBarOption.maximum = 100;
progressBarOption.textAlignment = Qt::AlignCenter;
progressBarOption.textVisible = true;
progressBarOption.progress = progress < 0 ? 0 : progress;
progressBarOption.text = QString().sprintf("%d%%", progressBarOption.progress);
QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter);
break;
}
You probably want to use QTableWidget for this. It has a method which allows you to add widgets like a QProgressBar. It's the "setCellWidget" method.
There is a slot in QProgressBar called setValue(int),
You can update it sending a signal to this progress bar from Your file manager.
This one should be designed in a way it could check or monitor download state and sends those signals periodically.
Good approach to manage up/down/finish images would be to have additional column in table with the image item.
It would be quite easy to update your image if the socket/connection/file corrupt state would change.
Writing any example would be actually writing you a program, so i suggest to post parts of the problems (if any) with code performed by yourself.
QTableView is not for displaying widgets in a layout. Use QGridLayout or some other suitable layout and put the widgets in that layout.