Maintaining checkedstatus inheritance in a QTreeView - c++

I'm trying to do something basic : you have a QTreeView. 1st depth are folders only, 2nd depth are files only. I want to have a check box with the checked status next to each item. Files are either checked or unchecked, folders can also be partiallyChecked depending on their files; all in all quite natural I believe.
The way I though I should go was using a QStandardItemModel and populate it with a custom subclass of QStandardItem : DescriptionFileItem. Maybe that was a bad idea, if there's an easier way please enlight me.
I tried using signals and slots so that my signal CheckStateChanged on a file would be connected to a slot UpdateCheckedStateOnChildStateChanged on its containing folder. This required my DescriptionFileItem to inherit from QObject as well (BTW, I was surprised that QStandardItem did not inherit from QObject). I initially hoped this would work seamlessly with the provided base classes but it did not : emitDataChanged() didn't seem to trigger my model's dataChanged() signal...
Using the model's dataChanged signals directly didn't work either: it's call is protected so you can't use it without subclassing (I think that's my next move unless somebody can help me get it right).
At the moment I have a signal -> slot connection that won't work and I have no idea why; compile and link work ok. Here's the code; perhapps you'll spot my mistakes easily. I'm leaving some commented lines so you can maybe see what I did wrong in a previous attempt. Thanks for your input!
#ifndef DESCRIPTIONFILEITEM_H
#define DESCRIPTIONFILEITEM_H
#include <QStandardItem>
#include <Qt>
class DescriptionFileItem : public QObject, public QStandardItem
{
Q_OBJECT
public:
explicit DescriptionFileItem(const QString & text, bool isFileName=false, QObject* parent = 0);
void setData ( const QVariant & value, int role = Qt::UserRole + 1 );
QVariant data( int role = Qt::UserRole + 1 ) const;
QString text;
Qt::CheckState checkedState;
bool isFileName;
signals:
void CheckStateChanged();
public slots:
void UpdateCheckedStateOnChildStateChanged();
};
#endif // DESCRIPTIONFILEITEM_H
Corresponding .cpp :
#include "DescriptionFileItem.h"
DescriptionFileItem::DescriptionFileItem(const QString & text, bool isFileName, QObject* parent):
QObject(parent),QStandardItem(text)
{
this->isFileName = isFileName;
checkedState = Qt::Checked;
}
void DescriptionFileItem::setData ( const QVariant & value, int role){
if(role == Qt::CheckStateRole){
Qt::CheckState newCheckState = (Qt::CheckState)value.toInt();
checkedState = newCheckState;
if(isFileName){
if(newCheckState == Qt::Unchecked || newCheckState == Qt::Checked){
for(int i = 0; i<rowCount(); i++){
DescriptionFileItem* child = (DescriptionFileItem*)QStandardItem::child(i);
QModelIndex childIndex = child->index();
child->model()->setData(childIndex,newCheckState, Qt::CheckStateRole);
//child->setCheckState(newCheckState);
//child->setData(newCheckState,Qt::CheckStateRole);
}
/*if(rowCount()>1){
emit this->model()->dataChanged(this->child(0)->index(),this->child(rowCount()-1)->index());
}else{
emit this->model()->dataChanged(this->child(0)->index(),this->child(0)->index());
}*/
}
}else{
emit CheckStateChanged();
}
//emit this->model()->dataChanged(this->index(),this->index());
}else{
QStandardItem::setData(value,role);
}
}
QVariant DescriptionFileItem::data( int role ) const{
if (role == Qt::CheckStateRole){
return checkedState;
}
return QStandardItem::data(role);
}
void DescriptionFileItem::UpdateCheckedStateOnChildStateChanged()
{
Qt::CheckState min = Qt::Checked;
Qt::CheckState max = Qt::Unchecked;
Qt::CheckState childState;
for(int i = 0; i<rowCount(); i++){
DescriptionFileItem* child = (DescriptionFileItem*)QStandardItem::child(i);
childState = (Qt::CheckState) child->data(Qt::CheckStateRole).toInt();
min = min>childState ? childState: min;
max = max<childState ? childState: max;
}
if(min >= max)
setData(min, Qt::CheckStateRole);
else
setData(Qt::PartiallyChecked, Qt::CheckStateRole);
}
And the construction of the connection / tree:
DescriptionFileItem* descFileStdItem = new DescriptionFileItem(descriptionFileName, true);
descFileStdItem->setFlags(Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled|Qt::ItemIsTristate);
descriptionFileSIModel.appendRow(descFileStdItem);
typedef pair<string,int> indexType;
foreach(indexType index,dataFile->indexes){
DescriptionFileItem* key_xItem = new DescriptionFileItem(index.first.c_str());
descFileStdItem->appendRow(key_xItem);
key_xItem->setFlags(Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled);
QObject::connect(key_xItem,SIGNAL(CheckStateChanged()),descFileStdItem,SLOT(UpdateCheckedStateOnModelDataChanged()));
}
EDIT: final answer, thanks to stu (see below)
void DataLoadWidget::ModelItemChanged(QStandardItem *item)
{
QStandardItem* parent = item->parent();
if(parent == 0){
//folder state changed--> update children if not partially selected
Qt::CheckState newState = item->checkState();
if(newState != Qt::PartiallyChecked){
for (int i = 0; i < item->rowCount(); i++)
{
item->child(i)->setCheckState(newState);
}
}
}
else{//child item changed--> count parent's children that are checked
int checkCount = 0;
for (int i = 0; i < parent->rowCount(); i++)
{
if (parent->child(i)->checkState() == Qt::Checked)
checkCount++;
}
if(checkCount == 0)
parent->setCheckState(Qt::Unchecked);
else if (checkCount == parent->rowCount())
parent->setCheckState(Qt::Checked);
else
parent->setCheckState(Qt::PartiallyChecked);
}
}

Unless I've misunderstood your question it seems that your solution is massively over-complicated. You should be able to do this trivially with the default QStandardItemModel implementation.
How about something like this (error handling omitted)?
QObject::connect(model, SIGNAL(itemChanged(QStandardItem*)), someObject, SLOT(modelItemChanged(QStandardItem*)));
And then in the signal handler:
void modelItemChanged(QStandardItem* item)
{
QStandardItem* parent = item->parent();
int checkCount = 0;
int rowCount = parent->rowCount();
for (int i = 0; i < rowCount; i++)
{
if (parent->child(i)->checkState() == Qt::Checked)
checkCount++;
}
switch (checkCount)
{
case 0:
parent->setCheckState(Qt::Unchecked);
break;
case rowCount:
parent->setCheckState(Qt::Checked);
break;
default:
parent->setCheckState(Qt::PartiallyChecked);
}
}
This is by no means optimal but it may be good enough for your purposes.

Related

QCompleter and QSortFilterProxyModel - what to do to be able to choose which column to search from

I made a model for data mining in TableView - the code looks something like this and runs in the constructor of the MainWindow class
(...)
leEdtUserSearch = new QLineEdit(this);
modelSqlBorrowingUsers = new SqlQueryModelBorrowingUsers(this, accessDB); //my model inheriting from QSqlQueryModel
tbViewUserList = new QTableView(this); //model table view above
srtFltrMdl = new QSortFilterProxyModel(this);
cmpltrNameAndSurname = new QCompleter(this);
srtFltrMdl->setSourceModel(modelSqlBorrowingUsers);
tbViewUserList->setModel(srtFltrMdl);
tbViewUserList->setColumnHidden(0,true);
cmpltrNameAndSurname->setModel(modelSqlBorrowingUsers);
leEdtUserSearch->setCompleter(cmpltrNameAndSurname);
QObject::connect(bttnGrpUserSearch, &QButtonGroup::idClicked, this, &MainWindow::searchByFirstNameOrLastName); //slot to search from the selected column
//below is theoretically correct search but ... something doesn't work
QObject::connect(leEdtUserSearch, &QLineEdit::textChanged, srtFltrMdl, qOverload<const QString &>(&QSortFilterProxyModel::setFilterRegularExpression));
(...)
slot to search from the selected column
void MainWindow::searchByFirstNameOrLastName(int idSearch)
{
if(idSearch==1){
srtFltrMdl->setFilterKeyColumn(1);
}
else if(idSearch==2){
srtFltrMdl->setFilterKeyColumn(2);
}
}
I probably know what could be wrong, the error may be in the model function - the data seems to be returned correctly but QCompleter and QSortFilterProxyModel see them as empty and I don't know why. Any of you see a bug here?
QVariant SqlQueryModelBorrowingUsers::data(const QModelIndex &index, int role) const
{
if(this->query().isSelect()){
if(this->query().first() && this->query().isValid()){
if(this->query().isActive()){
QVariant myData;
if(role==Qt::EditRole || role==Qt::DisplayRole){
myData = QSqlQueryModel::data(index, role); //here somehow strangely returned data that QCompleter and QSortFilterProxyModel cannot see
}
if(role==Qt::FontRole && index.column()==1){
QFont font;
font.setWeight(QFont::ExtraBold);
myData = font.toString();
}
if(role==Qt::TextAlignmentRole && (index.column()==1 || index.column()==2)){
return Qt::AlignCenter;
}
if(role==Qt::BackgroundRole && index.row() % 2 == 0){
QColor color;
color.setRgb(242,242,242,255);
myData = color;
}
if(role==Qt::BackgroundRole && index.row() % 2 == 1){
QColor color;
color.setRgb(217,217,217,170);
myData = color;
}
return myData;
}
}
}
else if(this->query().isValid()){
return QVariant();
}
return QVariant();
}
where is the error and how to correct it?
EDIT
#Alexey
Please, show us how and where QSqlQuery is set to the model
the code of the entire model
#include "sqlquerymodelborrowingusers.h"
#include <QDebug>
SqlQueryModelBorrowingUsers::SqlQueryModelBorrowingUsers(QObject *parent, QSqlDatabase *dbConnect) : QSqlQueryModel{parent}
{
db = dbConnect;
QString userData = "SELECT pIDuserWyp, pImie, pNazwisko FROM 'tUzytkownicyWypozyczajacy'";
this->setQuery(userData, *dbConnect);
this->query().exec();
}
int SqlQueryModelBorrowingUsers::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
int row=0;
QString rows = "SELECT count(*) FROM 'tUzytkownicyWypozyczajacy'";
QSqlQuery queryRow(*db);
queryRow.exec(rows);
QSqlRecord recRow;
if(queryRow.first()){
recRow = queryRow.record();
row = recRow.value(0).toInt();
return row;
}
else{
return row;
}
return row;
}
int SqlQueryModelBorrowingUsers::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
int column=0;
QString columns = "SELECT count(*) FROM PRAGMA_table_info('tUzytkownicyWypozyczajacy')";
QSqlQuery queryColumn(*db);
queryColumn.exec(columns);
QSqlRecord recColumn;
if(queryColumn.first()){
recColumn = queryColumn.record();
column = recColumn.value(0).toInt();
return column;
}
else{
return column;
}
return column;
}
QVariant SqlQueryModelBorrowingUsers::headerData(int section, Qt::Orientation orientation, int role) const
{
if(role==Qt::DisplayRole && orientation==Qt::Horizontal){
if(section==1){
QVariant header1;
header1="Name";
return header1;
}
if(section==2){
QVariant header2;
header2="Surname";
return header2;
}
}
if(role==Qt::DisplayRole && orientation==Qt::Vertical){
return QString("%1").arg(section+1);
}
return QVariant();
}
QVariant SqlQueryModelBorrowingUsers::data(const QModelIndex &index, int role) const
{
if(this->query().isSelect()){
if(this->query().first() && this->query().isValid()){
if(this->query().isActive()){
QVariant myData;
if(role==Qt::EditRole || role==Qt::DisplayRole){
myData = QSqlQueryModel::data(index, role);
}
if(role==Qt::FontRole && index.column()==1){
QFont font;
font.setWeight(QFont::ExtraBold);
myData = font.toString();
}
if(role==Qt::TextAlignmentRole && (index.column()==1 || index.column()==2)){
return Qt::AlignCenter;
}
if(role==Qt::BackgroundRole && index.row() % 2 == 0){
QColor color;
color.setRgb(242,242,242,255);
myData = color;
}
if(role==Qt::BackgroundRole && index.row() % 2 == 1){
QColor color;
color.setRgb(217,217,217,170);
myData = color;
}
return myData;
}
}
}
else if(this->query().isValid()){
return QVariant();
}
return QVariant();
}
bool SqlQueryModelBorrowingUsers::setData(const QModelIndex &index, const QVariant &value, int role)
{
QModelIndex pID = QSqlQueryModel::index(index.row(),0,QModelIndex());
int _pID = data(pID,Qt::EditRole | Qt::DisplayRole).toInt();
bool isSet=false;
if(!index.isValid()){
return false;
}
else if(index.column()<1){
return false;
}
else{
if(role==Qt::EditRole || role==Qt::DisplayRole){
if(index.column()==1){
isSet = name(_pID, value.toString());
emit dataChanged(index,index,{Qt::EditRole | Qt::DisplayRole});
this->refresh();
return isSet;
}
if(index.column()==2){
isSet = surname(_pID, value.toString());
emit dataChanged(index,index,{Qt::EditRole | Qt::DisplayRole});
this->refresh();
return isSet;
}
}
}
return isSet;
}
Qt::ItemFlags SqlQueryModelBorrowingUsers::flags(const QModelIndex &index) const
{
Qt::ItemFlags flag = QSqlQueryModel::flags(index); //export domyślnie ustawionych flag przez klasę nadrzędną
if(!index.isValid()){
return Qt::NoItemFlags;
}
else if(index.isValid()){
if(index.column()==1 || index.column()==2){
flag |= Qt::ItemIsEditable; //dopisanie nowej flagi do wcześniej wyeksportowanych flag
return flag;
}
}
return flag;
}
bool SqlQueryModelBorrowingUsers::name(int pID, const QString &pName)
{
QSqlQuery queryUpdateName;
QVariant _pID;
_pID = pID;
QString strUpdateName = "UPDATE 'tUzytkownicyWypozyczajacy' SET pImie = ? WHERE pIDuserWyp = ?";
queryUpdateName.prepare(strUpdateName);
queryUpdateName.addBindValue(pName);
queryUpdateName.addBindValue(_pID.toString());
return queryUpdateName.exec();
}
bool SqlQueryModelBorrowingUsers::surname(int pID, const QString &pSurname)
{
QSqlQuery queryUpdateSurname;
QVariant _pID;
_pID = pID;
QString strUpdateSurname = "UPDATE 'tUzytkownicyWypozyczajacy' SET pNazwisko = ? WHERE pIDuserWyp = ?";
queryUpdateSurname.prepare(strUpdateSurname);
queryUpdateSurname.addBindValue(pSurname);
queryUpdateSurname.addBindValue(_pID.toString());
return queryUpdateSurname.exec();
}
void SqlQueryModelBorrowingUsers::myAddRow()
{
QSqlQuery lastID, addRecord;
QString strGetID = "SELECT pIDuserWyp FROM 'tUzytkownicyWypozyczajacy'";
lastID.prepare(strGetID);
if(lastID.exec()){
if((lastID.last() && lastID.isValid()) || !lastID.isValid()){
QString strAddRecord = "INSERT INTO 'tUzytkownicyWypozyczajacy' (pImie, pNazwisko) VALUES (:Imie,:Nazwisko)";
if(this->pName.isEmpty() && !this->pSurname.isEmpty()){
emit this->messageOfNumber(1);
}
else if(this->pSurname.isEmpty() && !this->pName.isEmpty()){
emit this->messageOfNumber(2);
}
else if(this->pName.isEmpty() && this->pSurname.isEmpty()){
emit this->messageOfNumber(3);
}
else{
addRecord.prepare(strAddRecord);
addRecord.bindValue(":Imie",this->pName);
addRecord.bindValue(":Nazwisko",this->pSurname);
addRecord.exec();
addRecord.clear();
pName.clear();
pSurname.clear();
refresh();
emit this->userDataIsSet(); //sygnał służący do wyczyszczenia QLineEditów po dodaniu danych do bazy
emit this->messageOfNumber(0); //syngał służący do uruchomienia odpowiedniego komunikatu o błędzie
}
}
}
else{
emit this->messageOfNumber(-1);
}
}
void SqlQueryModelBorrowingUsers::myRemoveRow()
{
QModelIndex getIDfromTable;
QVariant getIDtoRemove;
QSqlQuery currentID, removeRow;
QString strGetCurrentID = "SELECT pIDuserWyp FROM 'tUzytkownicyWypozyczajacy'";
currentID.prepare(strGetCurrentID);
int i=0;
if(currentID.exec()){
if(currentID.isSelect()){
if(currentID.isActive()){
while (currentID.next()){
i=currentID.at();
if(i==this->row){
getIDfromTable=index(i,0,QModelIndex());
getIDtoRemove=getIDfromTable.data();
}
}
}
}
}
QString strRemoveRow = "DELETE FROM 'tUzytkownicyWypozyczajacy' WHERE pIDuserWyp = (?)";
removeRow.prepare(strRemoveRow);
removeRow.addBindValue(getIDtoRemove.toString(),QSql::In);
removeRow.exec();
refresh();
}
void SqlQueryModelBorrowingUsers::refresh()
{
this->query().clear();
this->setQuery("SELECT pIDuserWyp, pImie, pNazwisko FROM 'tUzytkownicyWypozyczajacy'");
this->query().exec();
}
void SqlQueryModelBorrowingUsers::setName(const QString &pName)
{
this->pName = pName;
}
void SqlQueryModelBorrowingUsers::setSurname(const QString &pSurname)
{
this->pSurname = pSurname;
}
SqlQueryModelBorrowingUsers::~SqlQueryModelBorrowingUsers()
{
this->query().clear();
}
EDIT
#Alexey
What is purpose to pass QSqlDatabase to the model's constructor? Moreover you pass it as pointer. Why, if you can get it anywhere by calling static method QSqlDatabase::database, see docs?
I suggest you to remove QSqlDatabase *dbConnect from constructor arguments.
I pass the connection indicator with the base to the model because I have made such a class and I have one question about it.
See the code and tell me if I did it right
AccessToDB::AccessToDB()
{
db = QSqlDatabase::addDatabase("QSQLITE");
db.setHostName("127.0.0.1");
db.setPort(-1);
db.setDatabaseName("dbSpIT.db");
db.setUserName("");
db.setPassword("");
}
bool AccessToDB::connectDB()
{
if(db.isValid()==false){
errorConnectDB = db.lastError();
qDebug()<< errorConnectDB.text() << "load driver";
for(int i=0; i<db.drivers().count(); ++i){
qDebug()<< db.drivers().at(i);
}
return false;
}
else{
qDebug()<< "the database driver has been properly initialized";
if(db.open()==true){
qDebug()<< "database is open" << db.open() << db.connectionName();
return true;
}
else{
qDebug()<< "database not open";
return false;
}
}
}
AccessToDB::~AccessToDB()
{
db.close();
}
in the MainWindow class, this way I create a connection to the base
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
(...)
accessDB = new AccessToDB();
accessDB->connectDB();
(...)
//at this point I pass the connection from the base to the model
modelSqlBorrowingUsers = new SqlQueryModelBorrowingUsers(this, accessDB);
(...)
}
#Alexey
The same applies to the refresh method.
How can I refresh the view if I have added data to the database?
Ok, let's start with your model class first.
I do not review your model editing, only data reading.
Remove ' (apostrophes) from query strings.
What is purpose to pass QSqlDatabase to the model's constructor? Moreover you pass it as pointer. Why, if you can get it anywhere by calling static method QSqlDatabase::database, see docs?
I suggest you to remove QSqlDatabase *dbConnect from constructor arguments.
Everywhere where you need a db connection, you should call QSqlDatabase::database.
And you should not store it in a model class.
Further
this->setQuery(userData, *dbConnect);
this->query().exec();
According to the docs setQuery method
Executes the query query for the given database connection db
So, remove this->query().exec();. The same applies to the refresh method.
Remove rowCount method, you do not need to override the one implemented in base class, it returns a query size by default.
columnCount should return the count of columns of the query. Your query SELECT pIDuserWyp, pImie, pNazwisko FROM 'tUzytkownicyWypozyczajacy' has 3 columns, so your columnCount reimplementation should return 3.
Now, let's look at reimplemented data method.
Remove all this unnecessary check-outs
if(this->query().isSelect()){
if(this->query().first() && this->query().isValid()){
if(this->query().isActive()){
Your query is a SELECT query, it is valid (it should), it's active, because it is so.
Regarding the use of your model class. Do not share the same instance between different "consumers" (QSortFilterProxyModel, QCompleter, QLineEdit), create the new one for every of them.

Style Sheet ignored on class inherited from QWidget [duplicate]

I have several custom widget in my current project. I wish to apply stylesheets to them and when I do so inside Qt Creator, it appears to work. However, when executing the program, no stylesheet is used. The stylesheets for the Qt widgets are working normally.
Does anyone have any advice?
WidgetUnits.h
#ifndef WIDGETUNITS_H
#define WIDGETUNITS_H
#include <QList>
#include <QWidget>
#include <QPainter>
#include <Widgets/JECButton.h>
#include <Unit.h>
#include <Time.h>
namespace Ui
{
class WidgetUnits;
}
class WidgetUnits : public QWidget
{
Q_OBJECT
public:
explicit WidgetUnits(QWidget *parent = 0);
~WidgetUnits();
void setNumTimes(const int& numTimes);
public slots:
void updatePictures(const Time* time);
protected:
void paintEvent(QPaintEvent *event);
private:
void checkNewQueue(const QList<QList<Unit*>*>* units);
Ui::WidgetUnits *ui;
const int pictureWidth; // The width of the Unit pictures.
const int pictureHeight; // The height of the Unit pictures.
QList<QList<JECButton*>*> buttonPictures; // The Units' pictures. The outer QList stores the QList of pictures for a given tick.
// The inner QList stores the JECButtons for the specific tick.
};
WidgetUnits.cpp
#include "WidgetUnits.h"
#include "ui_WidgetUnits.h"
WidgetUnits::WidgetUnits(QWidget *parent):
QWidget(parent),
ui(new Ui::WidgetUnits),
pictureWidth(36),
pictureHeight(36)
{
ui->setupUi(this);
}
WidgetUnits::~WidgetUnits()
{
delete ui;
}
void WidgetUnits::updatePictures(const Time *time)
{
// Only showing units that started to get built this turn.
checkNewQueue(time->getUnits());
checkNewQueue(time->getBuildings());
checkNewQueue(time->getUpgrades());
// Updating the position of the remaining pictures (after some were removed).
// Checking the maximum number of Units made in one tick.
int maxNewQueue = 0;
for (int a = 0; a < buttonPictures.length(); ++a)
{
if (buttonPictures.at(a)->length() > maxNewQueue)
{
maxNewQueue = buttonPictures.at(a)->length();
}
}
if (buttonPictures.length() > 0)
{
this->setGeometry(0, 0, buttonPictures.length() * 130,
maxNewQueue * (pictureWidth + 10) + 20);
QList<JECButton*>* tickButtons = 0;
for (int a = 0; a < buttonPictures.length(); ++a)
{
tickButtons = buttonPictures.at(a);
for (int b = 0; b < tickButtons->length(); ++b)
{
tickButtons->at(b)->move(a * 130, b * (pictureHeight + 10));
}
}
}
update();
}
void WidgetUnits::checkNewQueue(const QList<QList<Unit *> *> *units)
{
if (units != 0)
{
const Unit* currentUnit = 0;
JECButton* currentButton = 0;
for (int a = 0; a < units->length(); ++a)
{
buttonPictures.append(new QList<JECButton*>());
for (int b = 0; b < units->at(a)->length(); ++b)
{
currentUnit = units->at(a)->at(b);
// Verifying that there is an item in the queue and the queue action was started this turn.
if (currentUnit->getQueue() != 0 && currentUnit->getAction()->getTimeStart() == currentUnit->getAction()->getTimeCurrent()
&& (currentUnit->getAction()->getType() == Action::BUILD || currentUnit->getAction()->getType() == Action::TRAIN ||
currentUnit->getAction()->getType() == Action::UPGRADE))
{
buttonPictures.last()->append(new JECButton(this));
currentButton = buttonPictures.last()->last();
QImage* image = new QImage(currentUnit->getQueue()->getUnitBase()->getImage().scaled(pictureWidth, pictureHeight));
currentButton->setImage(*image);
currentButton->setGeometry(0, 0, currentButton->getImage().width(),
currentButton->getImage().height());
currentButton->setColorHover(QColor(0, 0, 225));
currentButton->setColorPressed(QColor(120, 120, 120));
currentButton->setImageOwner(true);
currentButton->setVisible(true);
}
}
}
}
}
void WidgetUnits::setNumTimes(const int &numTimes)
{
// Appending new button lists for added ticks.
for (int a = buttonPictures.length(); a < numTimes; ++a)
{
buttonPictures.append(new QList<JECButton*>());
}
}
void WidgetUnits::paintEvent(QPaintEvent *event)
{
QWidget::paintEvent(event);
}
The widget is visible- I set a tooltip which it showed me (It's just the same color of the QScrollArea it's sitting in).
I had a similar problem and it was solved using jecjackal's comment. As sjwarner said, it would be much more noticeable in the form of an answer. So I'll provide it. For the benefit of any future viewers. Again, it isn't my answer! Appreciate jecjackal for it!
As it is said in the Qt's stylesheets reference, applying CSS styles to custom widgets inherited from QWidget requires reimplementing paintEvent() in that way:
void CustomWidget::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
Without doing it your custom widgets will support only the background, background-clip and background-origin properties.
You can read about it here: Qt Stylesheets reference in the section "List of Stylable Widgets" -> QWidget.
There is an answer much easier than writing your own paintEvent: subclass QFrame instead of QWidget and it will work right away:
class WidgetUnits : public QFrame
{
Q_OBJECT
....
For completeness, the same problem is present in PyQt. You can apply a stylesheet to a subclassed QWidget by adding similar code:
def paintEvent(self, pe):
opt = QtGui.QStyleOption()
opt.init(self)
p = QtGui.QPainter(self)
s = self.style()
s.drawPrimitive(QtGui.QStyle.PE_Widget, opt, p, self)
I had same problem with pyside. I post my solution just for completeness.
It is almost like in PyQt as Pieter-Jan Busschaert proposed.
only difference is you need to call initFrom instead of init
def paintEvent(self, evt):
super(FreeDockWidget,self).paintEvent(evt)
opt = QtGui.QStyleOption()
opt.initFrom(self)
p = QtGui.QPainter(self)
s = self.style()
s.drawPrimitive(QtGui.QStyle.PE_Widget, opt, p, self)
One other thing you need to make sure is that you define your custom widget in your css file the following way:
FreeDockWidget{...}
and not like often recommended
QDockWidget#FreeDockWidget{...}
Calling setAttribute(Qt::WA_StyledBackground, true) for the custom widget worked for me.
Setting Qt::WA_StyledBackground to true only works if you remember to add Q_OBJECT to your class. With these two changes you don't need to reimplement paintEvent.

QAbstractTableModel and emit dataChanged for a single row

I derived a model from QAbstractTableModel and now I want to notify, that the data of a whole row has been changed. If for example the data of a row with index 5 is changed (4 columns), than using the following code works as expected.
emit dataChanged(index(5,0), index(5, 0));
emit dataChanged(index(5,1), index(5, 1));
emit dataChanged(index(5,2), index(5, 2));
emit dataChanged(index(5,3), index(5, 3));
But if I try to achieve the same with only one emit, ALL columns of ALL rows in the view are updated.
emit dataChanged(index(5, 0), index(5, 3));
What I am doing wrong here?
Minimal example (C++11, QTCreator 4.7.1, Windows 10 (1803), 64 Bit)
demo.h
#pragma once
#include <QAbstractTableModel>
#include <QTime>
#include <QTimer>
class Demo : public QAbstractTableModel
{
Q_OBJECT
QTimer * t;
public:
Demo()
{
t = new QTimer(this);
t->setInterval(1000);
connect(t, SIGNAL(timeout()) , this, SLOT(timerHit()));
t->start();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
int c = index.column();
if (role == Qt::DisplayRole)
{
QString strTime = QTime::currentTime().toString();
if (c == 0) return "A" + strTime;
if (c == 1) return "B" + strTime;
if (c == 2) return "C" + strTime;
if (c == 3) return "D" + strTime;
}
return QVariant();
}
int rowCount(const QModelIndex &) const override { return 10; }
int columnCount(const QModelIndex &) const override { return 4; }
private slots:
void timerHit()
{
//Works
emit dataChanged(index(5,0), index(5, 0));
emit dataChanged(index(5,1), index(5, 1));
emit dataChanged(index(5,2), index(5, 2));
emit dataChanged(index(5,3), index(5, 3));
//emit dataChanged(index(5,0), index(5, 3)); // <-- Doesn't work
}
};
main.cpp
#include "demo.h"
#include <QApplication>
#include <QTreeView>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QTreeView dataView;
Demo dataModel{};
dataView.setModel( &dataModel );
dataView.show();
return a.exec();
}
I think the problem lies with certain assumptions you're making with regard the behaviour of QTreeView when the QAbstractItemModel::dataChanged signal is emitted.
Specifically, you assume that the view will only invoke QAbstractItemModel::data on those indexes that are specified in the signal. That's not necessarily the case.
Looking at the source for QAbstractItemView::dataChanged (Qt 5.11.2) you'll see...
void QAbstractItemView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
{
Q_UNUSED(roles);
// Single item changed
Q_D(QAbstractItemView);
if (topLeft == bottomRight && topLeft.isValid()) {
const QEditorInfo &editorInfo = d->editorForIndex(topLeft);
//we don't update the edit data if it is static
if (!editorInfo.isStatic && editorInfo.widget) {
QAbstractItemDelegate *delegate = d->delegateForIndex(topLeft);
if (delegate) {
delegate->setEditorData(editorInfo.widget.data(), topLeft);
}
}
if (isVisible() && !d->delayedPendingLayout) {
// otherwise the items will be update later anyway
update(topLeft);
}
} else {
d->updateEditorData(topLeft, bottomRight);
if (isVisible() && !d->delayedPendingLayout)
d->viewport->update();
}
#ifndef QT_NO_ACCESSIBILITY
if (QAccessible::isActive()) {
QAccessibleTableModelChangeEvent accessibleEvent(this, QAccessibleTableModelChangeEvent::DataChanged);
accessibleEvent.setFirstRow(topLeft.row());
accessibleEvent.setFirstColumn(topLeft.column());
accessibleEvent.setLastRow(bottomRight.row());
accessibleEvent.setLastColumn(bottomRight.column());
QAccessible::updateAccessibility(&accessibleEvent);
}
#endif
d->updateGeometry();
}
The important point is that this code behaves differently depending on whether or not the signal specifies a single QModelIndex -- e.g. topLeft is the same as bottomRight. If they are the same then the view tries to ensure that only that model index is updated. However, if multiple model indexes are specified then it will invoke...
d->viewport->update();
which will, presumably, result in the data for all visible model indexes being queried.
Since your implementation of Demo::data always returns new data based on the current time you will see the entire visible part of the view update giving the impression that the dataChanged signal was emitted for all rows and columns.
So the fix is really to make your data model more ``stateful'' -- it needs to keep track of values rather than simply generating them on demand.
Not sure whether this is what you're looking for but I'll put it up anyways.
Even using emit dataChanged(...), you would still see that clicks/selection on rows will cause them to self-update (doing this from a Mac, so might be different).
Instead of using the QAbstractItemModel::dataChanged signal, I will be using the QAbstractItemModel::setData() function.
This is my implementation of demo.h
#pragma once
#include <QAbstractTableModel>
#include <QTime>
#include <QTimer>
class Demo : public QAbstractTableModel
{
Q_OBJECT
public:
Demo()
{
int cCount = columnCount(index(0, 0));
int rCount = rowCount(index(0, 0));
// populate model data with *static* values
QString strTime = QTime::currentTime().toString();
QStringList temp;
for (int j = 0; j < cCount; j++)
temp.append(strTime);
for (int i = 0; i < rCount; i++)
demoModelData.append(temp);
// nothing new here
t = new QTimer(this);
t->setInterval(1000);
connect(t, SIGNAL(timeout()) , this, SLOT(timerHit()));
t->start();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
// tells the *view* what to display
// if this was dynamic (e.g. like your original strTime implementation)
// then the view (QTreeView in main.cpp) will constantly update
if (role == Qt::DisplayRole)
return demoModelData.at(index.row()).at(index.column()); // retrieve data from model
return QVariant();
}
// reimplemented from QAbstractTableModel
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) override
{
if (role == Qt::DisplayRole)
{
demoModelData[index.row()][index.column()] = value.toString(); // set the new data
emit dataChanged(index, index); // explicitly emit dataChanged signal, notifies TreeView to update by
// calling this->data(index, Qt::DisplayRole)
}
return true;
}
int rowCount(const QModelIndex &) const override { return 10; }
int columnCount(const QModelIndex &) const override { return 4; }
private slots:
void timerHit()
{
QString strTime = QTime::currentTime().toString();
setData(index(5, 0), QVariant(strTime), Qt::DisplayRole); // only changes index at (row = 5, col = 0)
}
private:
QTimer *t;
QList<QStringList> demoModelData; // stores the table model's data
};
Since the class is a "model", there should be some way of storing/retrieving data for display. Here, I've used a QList<QStringList>, but you can store data in other ways that suit you as well (e.g. a tree, QVector, QMap).
This is a genuine efficiency bug in Qt.
The Qt project is no longer accepting changes to Qt 5, so I made the change and pushed it to my fork on GitHub. You can see a fix for the issue you've encountered here.
If you want to build your own copy of 5.15.2, you may be interested in my other fixes.

How to create a Generic ListModel for QML

My application consists of many Lists that I display in QML-ListViews by using QAbstractListModel derived model-classes. It's always the same, with the difference of the Item-Type. That's why I want to know how to build a class-template for this approach.
I figured out, that it is not possible to use the Q_OBJECT-Macro in a class-template. That's why my GenericListModel consists of two part.
1. GenericListModelData
The first part is the Model itself that derives from QAbstractListModel and implements the basic functions data(), rowCount() and roleNames().
2. GenericListModel
The second part is the class-template that is used as a wrapper to provide functions similar to a QListView.
If you have any suggestions or questions please let me know. It would be really nice to improve this solution.
I uploaded the full sourcecode here:
https://github.com/sebabebibobu/QGenericListModel/
1. GenericListModelData
QVariant GenericListModelData::data(const QModelIndex &index, int role) const
{
QObject *item = m_itemList.at(index.row());
return item->property(item->metaObject()->property(role).name());
}
/*
* Returns the number of items attached to the list.
*/
int GenericListModelData::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_itemList.size();
}
/*
* Generates a hash out of QMetaObject property-index and property-name.
*/
QHash<int, QByteArray> GenericListModelData::roleNames() const
{
QHash<int, QByteArray> roles;
if (!m_itemList.isEmpty()) {
for(int i = 0; i < m_itemList.at(0)->metaObject()->propertyCount(); i++) {
roles[i] = m_itemList.at(0)->metaObject()->property(i).name();
}
}
return roles;
}
/*
* Append Item to List.
*/
void GenericListModelData::appendItem(QObject *item)
{
/* map the notify()-signal-index with the property-index when the first item get's inserted */
if (m_itemList.isEmpty()) {
for(int i = 0; i < item->metaObject()->propertyCount(); i++) {
m_propertySignalIndexHash.insert(item->metaObject()->property(i).notifySignalIndex(), i);
}
}
/* connect each notify()-signals to the onDataChanged()-slot which call's the dataChanged()-signal */
for(int i = 0; i < item->metaObject()->propertyCount(); i++) {
connect(item, "2" + item->metaObject()->property(i).notifySignal().methodSignature(), this, SLOT(onDataChanged()));
}
/* finally append the item the list */
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_itemList.append(item);
endInsertRows();
}
/*
* Helper-Slot that emit's the dataChanged()-signal of QAbstractListModel.
*/
void GenericListModelData::onDataChanged()
{
QModelIndex index = createIndex(m_itemList.indexOf(sender()),0);
QVector<int> roles;
roles.append(m_propertySignalIndexHash.value(senderSignalIndex()));
emit dataChanged(index, index, roles);
}
2. GenericListModel
template <typename T>
class GenericListModel : public GenericListModelData
{
public:
explicit GenericListModel(QObject *parent) : GenericListModelData(parent) {
}
void append(T *item) {
appendItem(item);
}
T *at(int i) {
return qobject_cast<T *>(m_itemList.at(i));
}
};
Update 01.05.2016
GrecKo posted in the comments, that a project like mine already exists. That's why I decided to share the link of this project here too:
http://gitlab.unique-conception.org/qt-qml-tricks/qt-qml-models

How to refresh a QSqlTableModel while preserving the selection?

I am using a QSqlTableModel and QTableView to view an SQLite database table.
I would like to have the table auto refresh every second or so (it's not going to be a very large table - a couple of hundred rows). And i can do this - like so:
QTimer *updateInterval = new QTimer(this);
updateInterval->setInterval(1000);
updateInterval->start();
connect(updateInterval, SIGNAL(timeout()),this, SLOT(update_table()));
...
void MainWindow::update_table()
{
model->select(); //QSqlTableModel*
sqlTable->reset(); //QTableView*
}
But this removes any selection I have, so the selections only last for up to a second. This is annoying, as another pane in the GUI depends on what is selected. If nothing is selected, then it resets to an explanation splash page.
I then tried a somewhat hacky approach, which gets the selected row number, resets the table, and then selects that row. But this doesn't work either, as the selected row can move up or down based on additions to the table.
I know other classes have a dataChanged() signal, which would be ideal.
Do any of you know how I could have the table refresh to reflect changes to the database (from either command line usage, or other instances of the program) AND keep the current selection?
I know I could get data from the current selection, and then after the reset search for the same row and then reselect it, but this seems like a counter productive and bad solution to the problem.
EDIT: Current attempt at solution:
void MainWindow::update_table()
{
QList<QModelIndex> selection = sqlTable->selectionModel()->selection().indexes();
QList<int> selectedIDs;
bool somethingSelected = true;
for(QList<QModelIndex>::iterator i = selection.begin(); i != selection.end(); ++i){
int col = i->column();
QVariant data = i->data(Qt::DisplayRole);
if(col == 0) {
selectedIDs.append(data.toInt());
}
}
if(selectedIDs.empty()) somethingSelected = false;
model->select();
sqlTable->reset();
if(somethingSelected){
QList<int> selectedRows;
int rows = model->rowCount(QModelIndex());
for(int i = 0; i < rows; ++i){
sqlTable->selectRow(i);
if(selectedIDs.contains(sqlTable->selectionModel()->selection().indexes().first().data(Qt::DisplayRole).toInt())) selectedRows.append(i);
}
for(QList<int>::iterator i = selectedRows.begin(); i != selectedRows.end(); ++i){
sqlTable->selectRow(*i);
}
}
}
Okay so this more or less works now...
The real deal is the primary key on the result of your query. Qt's APIs offer a rather circuitous route from QSqlTableModel::primaryKey() to a list of columns. The result of primaryKey() is a QSqlRecord, and you can iterate over its field()s to see what they are. You can also look up all of the fields that comprise the query proper from QSqlTableModel::record(). You find the former in the latter to get a list of model columns that comprise the query.
If your query doesn't contain a primary key, you'll have to design one yourself and offer it using some protocol. For example, you can choose that if primaryKey().isEmpty() is true, the last column returned by the model is to be used as the primary key. It's up to you to figure out how to key the result of an arbitrary query.
The selected rows can then be indexed simply by their primary keys (a list of values of the cells that comprise the key -- a QVariantList). For this, you could use a custom selection model (QItemSelectionModel) if its design wasn't broken. The key methods, such as isRowSelected() aren't virtual, and you can't reimplement them :(.
Instead, you can use a proxy model that mimicks selection by providing a custom Qt::BackgroundRole for the data. Your model sits on top of the table model, and keeps a sorted list of selected keys. Each time the proxy model's data() is called, you get the row's key from underlying query model, then search for it in your sorted list. Finally, you return a custom background role if the item is selected. You'll have to write relevant comparison operator for QVariantList. If QItemSelectionModel was usable for this purpose, you could put this functionality into a reimplementation of isRowSelected().
The model is generic since you subscribe to a certain protocol for extracting the key from a query model: namely, using primaryKey().
Instead of using primary keys explicitly, you can also use persistent indices if the model supports them. Alas, until at lest Qt 5.3.2, QSqlTableModel does not preserve the persistent indices when the query is rerun. Thus, as soon as the view changes the sort order, the persistent indices become invalid.
Below is a fully worked out example of how one might implement such a beast:
#include <QApplication>
#include <QTableView>
#include <QSqlRecord>
#include <QSqlField>
#include <QSqlQuery>
#include <QSqlTableModel>
#include <QIdentityProxyModel>
#include <QSqlDatabase>
#include <QMap>
#include <QVBoxLayout>
#include <QPushButton>
// Lexicographic comparison for a variant list
bool operator<(const QVariantList &a, const QVariantList &b) {
int count = std::max(a.count(), b.count());
// For lexicographic comparison, null comes before all else
Q_ASSERT(QVariant() < QVariant::fromValue(-1));
for (int i = 0; i < count; ++i) {
auto aValue = i < a.count() ? a.value(i) : QVariant();
auto bValue = i < b.count() ? b.value(i) : QVariant();
if (aValue < bValue) return true;
}
return false;
}
class RowSelectionEmulatorProxy : public QIdentityProxyModel {
Q_OBJECT
Q_PROPERTY(QBrush selectedBrush READ selectedBrush WRITE setSelectedBrush)
QMap<QVariantList, QModelIndex> mutable m_selection;
QVector<int> m_roles;
QBrush m_selectedBrush;
bool m_ignoreReset;
class SqlTableModel : public QSqlTableModel {
public:
using QSqlTableModel::primaryValues;
};
SqlTableModel * source() const {
return static_cast<SqlTableModel*>(dynamic_cast<QSqlTableModel*>(sourceModel()));
}
QVariantList primaryValues(int row) const {
auto record = source()->primaryValues(row);
QVariantList values;
for (int i = 0; i < record.count(); ++i) values << record.field(i).value();
return values;
}
void notifyOfChanges(int row) {
emit dataChanged(index(row, 0), index(row, columnCount()-1), m_roles);
}
void notifyOfAllChanges(bool remove = false) {
auto it = m_selection.begin();
while (it != m_selection.end()) {
if (it->isValid()) notifyOfChanges(it->row());
if (remove) it = m_selection.erase(it); else ++it;
}
}
public:
RowSelectionEmulatorProxy(QObject* parent = 0) :
QIdentityProxyModel(parent), m_roles(QVector<int>() << Qt::BackgroundRole),
m_ignoreReset(false) {
connect(this, &QAbstractItemModel::modelReset, [this]{
if (! m_ignoreReset) {
m_selection.clear();
} else {
for (auto it = m_selection.begin(); it != m_selection.end(); ++it) {
*it = QModelIndex(); // invalidate the cached mapping
}
}
});
}
QBrush selectedBrush() const { return m_selectedBrush; }
void setSelectedBrush(const QBrush & brush) {
if (brush == m_selectedBrush) return;
m_selectedBrush = brush;
notifyOfAllChanges();
}
QList<int> selectedRows() const {
QList<int> result;
for (auto it = m_selection.begin(); it != m_selection.end(); ++it) {
if (it->isValid()) result << it->row();
}
return result;
}
bool isRowSelected(const QModelIndex &proxyIndex) const {
if (! source() || proxyIndex.row() >= rowCount()) return false;
auto primaryKey = primaryValues(proxyIndex.row());
return m_selection.contains(primaryKey);
}
Q_SLOT void selectRow(const QModelIndex &proxyIndex, bool selected = true) {
if (! source() || proxyIndex.row() >= rowCount()) return;
auto primaryKey = primaryValues(proxyIndex.row());
if (selected) {
m_selection.insert(primaryKey, proxyIndex);
} else {
m_selection.remove(primaryKey);
}
notifyOfChanges(proxyIndex.row());
}
Q_SLOT void toggleRowSelection(const QModelIndex &proxyIndex) {
selectRow(proxyIndex, !isRowSelected(proxyIndex));
}
Q_SLOT virtual void clearSelection() {
notifyOfAllChanges(true);
}
QVariant data(const QModelIndex &proxyIndex, int role) const Q_DECL_OVERRIDE {
QVariant value = QIdentityProxyModel::data(proxyIndex, role);
if (proxyIndex.row() < rowCount() && source()) {
auto primaryKey = primaryValues(proxyIndex.row());
auto it = m_selection.find(primaryKey);
if (it != m_selection.end()) {
// update the cache
if (! it->isValid()) *it = proxyIndex;
// return the background
if (role == Qt::BackgroundRole) return m_selectedBrush;
}
}
return value;
}
bool setData(const QModelIndex &, const QVariant &, int) Q_DECL_OVERRIDE {
return false;
}
void sort(int column, Qt::SortOrder order) Q_DECL_OVERRIDE {
m_ignoreReset = true;
QIdentityProxyModel::sort(column, order);
m_ignoreReset = false;
}
void setSourceModel(QAbstractItemModel * model) Q_DECL_OVERRIDE {
m_selection.clear();
QIdentityProxyModel::setSourceModel(model);
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget w;
QVBoxLayout layout(&w);
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:");
if (! db.open()) return 255;
QSqlQuery query(db);
query.exec("create table chaps (name, age, constraint pk primary key (name, age));");
query.exec("insert into chaps (name, age) values "
"('Bob', 20), ('Rob', 30), ('Sue', 25), ('Hob', 40);");
QSqlTableModel model(nullptr, db);
model.setTable("chaps");
RowSelectionEmulatorProxy proxy;
proxy.setSourceModel(&model);
proxy.setSelectedBrush(QBrush(Qt::yellow));
QTableView view;
view.setModel(&proxy);
view.setEditTriggers(QAbstractItemView::NoEditTriggers);
view.setSelectionMode(QAbstractItemView::NoSelection);
view.setSortingEnabled(true);
QObject::connect(&view, &QAbstractItemView::clicked, [&proxy](const QModelIndex & index){
proxy.toggleRowSelection(index);
});
QPushButton clearSelection("Clear Selection");
QObject::connect(&clearSelection, &QPushButton::clicked, [&proxy]{ proxy.clearSelection(); });
layout.addWidget(&view);
layout.addWidget(&clearSelection);
w.show();
app.exec();
}
#include "main.moc"