How to use a QList of UserDefined class, as the Storage Model for QAbstractItemModel to be used by QTreeWidget
-
hi guys, I'm puzzled about which would be the more elegant way to use a " QList<MyTypeClass> " as model for a QTreeWidget with 4 columns.
the final result shoul look like the following:
** attention this is only a graphic design in figma - there is not a concrete implementation yet.description
- QTreeWidget with 4 column : "file type", "full_path", "State" and "Openable"
-> file type [bool] : display folder icon when the value is 0, else display File icon.
-> full path [QString] : a path to a folder or a file
-> State [bool] : 0 if the path doesn't exist anymore at that location display red circle and a text, else a green circle and a text
-> Openable[bool] : this option will let the user decide whether to open the file/folder when a PushButton is pressed.
my UserDefinedClass, called PathInfo will hold all the 4 values quoted above
// WARNING: this is not a real code implementation. // is only a trace for listing the features I'll need class PathInfo { // the constructor will inspect the path and fill // "isValid", "isFile" and "isOpenable" to the respective member variables PathInfo( QString& path ) ; public: static bool pathIsValid( const QString& path ) ; static bool isFile( const QString& path ) ; // getter bool openable() const ; bool fileType() const; bool validityState() const ; QString path() const ; // setter void changePath( QString& path ) ; void updateValidityState() ; void toggleOpenable() ; private: QString path; bool isValid; bool isFile; bool isOpenable { 1 }; };
Questions:
- How can I subClass QAbstractItem model and take the QList<PathInfo> as the model structure that will display the PathInfo.isFile , PathInfo.path(), PathInfo.isValid and PathInfo.isOpenable on the TreeWidget ?
- Should I implement both an abstractItemModel and also a custom TreeWidget ?
GOALS:
my goals are to update the model and the view as a consequence of adding a new Instance of PathInfo (my user defined class) to the QList<PathInfo> and
implement a QStyledItemDelegate for the fourth colum. For the first and third column, QDisplayRole and QDecorationRole, maybe, will be enough ? - QTreeWidget with 4 column : "file type", "full_path", "State" and "Openable"
-
@JonB you were right and indeed makes sense not calling "painter.end()" because this will create problem to the painting of the second element and so on... using Painter::save() at the beginning and Painter::restore() at the end of the designated class to paint the obj is enoug like StarDelegate example provided by Qt's documentation Illustrates.
SOLUTION:
I found a easier a fastest way to change the state of a "togglable item" (like checkbox, toggle sw, ecc..) inside a Custom Delegate.
here the breakdown of the steps to follow:-
Is always better and simpler to draw the "static version" of the delegate inside the QStyledItemDelegate() instead of creating a custom class and pass the delegate's painter to a custom method of a class that have the painting implementation details about how to drow the item.
-
You don't always follow the starDelegate example provided by Qt because is not suitable for every purpose as I just learned.
-
if you want to directly toggle an item without passing through the process "click the cell you want to toggle" >> "onother click or double-click to trigger the editor creation" >> "onother click to change the state inside the editor just created" , you can directly toggle the state with only one click and using one methods like so:
#ifndef TOGGLESWITCHDELEGATE_H #define TOGGLESWITCHDELEGATE_H #include <QStyledItemDelegate> #include "viewtogglesw.h" #include <QPaintEvent> #include <QMouseEvent> class ToggleSwitchDelegate: public QStyledItemDelegate { Q_OBJECT public: ToggleSwitchDelegate(QObject* parent); ~ToggleSwitchDelegate(); QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; void paint(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; //* THIS IS THE MAGIC METHOD YOU WANT bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; private: mutable ViewToggleSw* toggle_sw; }; #endif // TOGGLESWITCHDELEGATE_H
#include "toggleswitchdelegate.h" ToggleSwitchDelegate::ToggleSwitchDelegate(QObject* parent ) : QStyledItemDelegate(parent) , toggle_sw( new ViewToggleSw( qobject_cast<QWidget*>(parent)) ) {} ToggleSwitchDelegate::~ToggleSwitchDelegate() { delete toggle_sw; } void ToggleSwitchDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const { if ( index.data().canConvert<bool>() ) { auto is_checked = qvariant_cast<bool>( index.data() ); // *** DRAW DIRECTLY - NO WIDGET INSTANCE *** painter->setRenderHint(QPainter::Antialiasing, true); int handle_radius = std::round(0.24 * option.rect.height()); int w_fx_h = std::round((option.rect.height() * 11.0) / 13.0); // Use 11.0 and 13.0 for floating-point division QRectF frame_slider(option.rect.x(), option.rect.y(), w_fx_h - handle_radius, option.rect.height() * 0.40); frame_slider.moveCenter(option.rect.center()); int border_radius = frame_slider.height() / 2; int handle_path_len = w_fx_h - handle_radius; int x_position = frame_slider.x() + (handle_path_len * (is_checked ? 1.0 : 0.0)); // Draw Frame (using colors from ViewToggleSw or define them here) QPen framePen(is_checked ? QColor(152, 73, 249) : QColor(113, 113, 113), 3); QBrush frameBrush(is_checked ? QColor(255, 255, 255) : QColor(207, 207, 207)); painter->setPen(framePen); painter->setBrush(frameBrush); painter->drawRoundedRect(frame_slider, border_radius, border_radius); // Draw Handle QPen handlePen(QColor(81, 80, 102), 3); QBrush handleBrush(is_checked ? QColor(161, 86, 253) : QColor(132, 131, 151)); painter->setPen(handlePen); painter->setBrush(handleBrush); painter->drawEllipse(QPointF(x_position, option.rect.center().y()), handle_radius, handle_radius); } } // MAGIC METHOD IMPLEMENTATION bool ToggleSwitchDelegate::editorEvent( QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index ) { // checks if index is valid, colum is the fourth (in my case, the column where i will install the delegate on the view // remember that first column as index.column() = 0 not 1, so Column 3 is the fourth // in my case I chose to toggle the checkedState when the event is of type mouseReleaseEvent bool idx_isValid = index.isValid(); bool isCol3_isTypeBool = ( index.column() == 3 && index.data().canConvert<bool>() ); bool isMouseReleaseEvent = ( event->type() == QEvent::MouseButtonRelease ); if ( idx_isValid && isCol3_isTypeBool && isMouseReleaseEvent ) { QMouseEvent* mouseEvent = static_cast<QMouseEvent*>( event ); // if the mouse Click is inside the cell toggle the model value if ( option.rect.contains(mouseEvent->pos()) ) { // if the current value is true, set it to false and viceversa bool is_checked = model->data( index, Qt::DisplayRole ).toBool(); model->setData( index, !is_checked, Qt::EditRole ); return true; // needed to consume the event } return true; } return false; } QSize ToggleSwitchDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const { Q_UNUSED(option) if ( index.data().canConvert<bool>() ) { return toggle_sw->sizeHint(); } return QSize(); }
Important: see the comments inside the above code snippet for further explanations
here the code on the constructor of QWidget (could be of type QMainWindow also)
Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); PathInfo info(QString("C:\\Users\\some\\path\\to\\Desktop\\file.png")); PathInfo info2(QString("C:\\Users\\123\\test\\documents")); PathInfo info3(QString("C:\\Users\\test\\Tor Browser\\Browser\\distribution\\extensions")); // only for test, default is set to true info3.setOpenable(false); QList<PathInfo> group; group.append( info ); group.append( info2 ); group.append( info3 ); paths_model = new PathsInfoTableModel( group, this ); // hide the row headers and add wrap word if path is to long ui->t_view->setModel( paths_model ); ui->t_view->verticalHeader()->hide(); ui->t_view->setWordWrap(true); ui->t_view->verticalHeader()->setDefaultSectionSize(54); // setting toggle switch delegate for col 3 auto* toggle_sw_delegate = new ToggleSwitchDelegate( ui->t_view ); ui->t_view->setItemDelegateForColumn( 3, toggle_sw_delegate ); // there is no needs of editTriggers because our magic method calls automatically setModel without creating an Editor ui->t_view->setEditTriggers(QAbstractItemView::NoEditTriggers); // this allow the user to double-click on the path cell and select a new one // with the QFileDialog::getOpenFileName and update the model.. connect( ui->t_view, &QTableView::doubleClicked, this, [this] ( const QModelIndex& index ) { if ( !index.isValid() ) return; if ( index.column() == 1 ) { auto* model = qobject_cast<PathsInfoTableModel*>( ui->t_view->model() ); if ( !model ) return; QString current_path = model->data( index, Qt::DisplayRole ).toString(); // if the parent of current path exist, use // it as default location else, Home PathInfo path_info( current_path ); auto default_path = (QFileInfo::exists(path_info.absPath())) ? path_info.absPath() : QDir::homePath(); QString newPath = QFileDialog::getOpenFileName(this, "Select new Path", default_path ); if ( !newPath.isEmpty() && newPath != current_path ) { QVariant newPathVariant = QVariant( newPath ); model->setData( index, newPathVariant, Qt::EditRole ); } } } ); }
-
-
@Andrea_Venturelli
Normally to subclassQAbstractItemModel
you need to supply rows and columns from your data. Do read https://doc.qt.io/qt-6/qabstractitemmodel.html#subclassing for what you need to implement for subclassing. YourrowCount()
will be the the length of theQList
.columnCount()
would return4
. You implementdata()
(andsetData()
if editable) to map between the fields/members in your class and corresponding column numbers.You may choose to implement some of the columns via different
data()
roles instead of columns if it seems more suitable, e.g. the file type icon. Whatever you can resolve how it is displayed in your view/QStyledItemDelegate
.At the moment what you show does not look like a tree view/widget. You would presumably want to do something with Full Path for that. Look into
parent
to create the tree hierarchy. -
hi @JonB , i'm using a QTreeWidget only because QList has a "single column only" and the QTableWidget has QCornerButton and rows indicator that I don't need in the view.
So I opted to use QTreeWidget with only QModelIndex() as parent and I'm not planning to use children elements in it.
My idea is to use it as a "multi-column" QListView with Headers. This is the reason I made when I choose It.for the Structure I Thought about the possible implementation and come to the same conclusion you suggested that is:
-> rowCounts() will be QList<FileInfo>.size()
-> columnCounts() will be fixed to 4.- What I'm struggling with, is how in the setData() I'll fill the model starting only from an indexModel ?
- Is setData being called for every cell like (row 0, col 0) ecc.. or by rows ( row 0: col 0, col 1, col 2, col 3) ?
is this how setData passes the IndexModel to fill the data?
custom behaviors:
-
the rows CAN be deleted with a dedicated QPushButton on the currentItemSelected ( so i need to implement the model's RemoveRows() virtual method)
-
only the last colum is editable by the user via a QStyledItemDelegate ( when pressed inside the option.rect() toggle the bool value ) ... is like a checkbox with a custom paintEvent
-
double-clicking on one row, will call "QFileSystemExplorer" to choose a new path to substitute in place of the current one (this will trigger a check on the other 3 columns because the all depend on the path
Can you please write a boilerPlate of a generic implementation of "setData()" in order to understand visually ?
-
@Andrea_Venturelli
If you're not going to create a hierarchical model with parentage you don't want aQTree....
anything, you want to stick to aQTable...
.
You can suppress display of corner buttons or rows indicators in aQTableWidget
.
QTreeWidget
is aQTreeView
with its own internal model and a way of working, and the same forQTableWidget
versusQTableView
.
If you wish to use your own model (e.g. yourQList<MyTypeClass>
) you will want to use one of theQ...View
classes not theQ...Widget
ones. If you use the latter you will have to copy your data in & out of its internal model.
QTableView
is more suitable for multi-column table presentation thanQListView
. But you may have your reasons for using the latter with aQStyledItemDelgate
andpaintEvent()
s etc.
With aQ{Table,Tree}View
it retrieves data from your model viadata()
and theQModelIndex
es like you show in our picture.setData()
works just the same if you want to store data to your model. Once you have writtendata()
correctly it is self-evident how to writesetData()
. Do the former the way you want first, then similar code insetData()
. -
Hi everyone, I implemented the PathInfo class and defined a basic implementation of PathsInfoTableModel which inherit from QAbstractTableModel
PathsInfoTableModel.cpp
PathsInfoTableModel::PathsInfoTableModel(QList<PathInfo> group, QObject *parent) : QAbstractTableModel{parent}, m_model_data{ group } {} int PathsInfoTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_model_data.size(); } int PathsInfoTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 4; } QVariant PathsInfoTableModel::data(const QModelIndex &index, int role) const { Q_UNUSED(role) qDebug() << QString("-").repeated(30); qDebug() << "\nindex" << index << "[row] :" << index.row() <<" [col]: " << index.column(); return QVariant(); }
and PathInfo.h
class PathInfo: QObject { Q_OBJECT public: Q_SIGNAL void pathChanged( QString, QString); // constructors PathInfo( QString path ); PathInfo( char* path ); PathInfo(const PathInfo& other ); PathInfo operator=(const PathInfo& other ); // destructor ~PathInfo() {} ; // getters bool openable() const { return m_isOpenable; }; bool fileType() const { return m_isFile; }; bool pathValidity() const { return m_isValid; }; //setters void changePath ( QString new_path ); void setOpenable( bool state ) { m_isOpenable = state; }; void setFileType( bool type ) { m_isFile = type; }; void setPathValidity( bool state ) { m_isValid = state; }; // file info QList<QString> absolute_fullPath_fName(); QString absPath() const; QString fileName() const; QString filePath() const; // static convinience methods static bool pathIsValid( const QString path ); static int isFile( const QString path ); void printInfo(); private: QString m_path; bool m_isValid{ 0 }; bool m_isFile{ 1 }; bool m_isOpenable{ 1 }; QFileInfo m_file_info; void updatePathInfo(); };
Question:
How can I access the member variables (m_path, m_isValid, m_isFile, m_isOpenable) with the array access notation like: my_obj[0], my_obj[1] ... ?
The problem:
QAbstractTableModel.data( params ) will provide a QIndexModel(row, col) for each cell in the table times the numbers of Qt::Roles
My model is QList<PathInfo> so the QModelIndex.row() indicates the element in the QList and this is the easy part. What If I want to access the 4 members inside PathInfo like so:
PathInfo[ QModelIndex.column() ]
and this will give me something like:
PathInfo[ 0 ] = PathInfo.m_isFile,
PathInfo[ 1 ] = PathInfo.m_path,
PathInfo[ 2 ] = PathInfo.m_isValid,
PathInfo[ 3 ] = PathInfo.m_isOpenableI thought about implementing FileInfo::operator[] with a bunch of switchCase conditions or via a random access iterator pointing to each member_var address...
My main concern is that, if a throw a bunch of "if-else" statements inside QAbstractTableModel.data() this will slow down significantly if a user decide to add a good amount of rows
-
@Andrea_Venturelli
Yes you need to map between your class with individual C++ member variables andQModelIndex
integer column indexes. In both directions. Yes you should use aswitch
statement indata()
/setData()
methods. Yes there is a marginal overhead in aswitch
(orif
s,switch
might be faster here), no it won't be significant. There are only 4 cases anyway. Yes this is standard. -
I did some work on the model and the basics works as expected; this is the result I have:
problem with column 4:
When I try to set the custom delegate on the column 4, I get some Console's messages saying the following:
QPainter::save: Painter not active QPainter::setRenderHint: Painter must be active to set rendering hints QPainter::setPen: Painter not active QPainter::setBrush: Painter not active QPainter::drawRects: Painter not active QPainter::restore: Unbalanced save/restore QPainter::save: Painter not active
and if three items is given to the model, the view draws only the first and the others will not been drawn.
toggleswitchdelegate.h
#ifndef TOGGLESWITCHDELEGATE_H #define TOGGLESWITCHDELEGATE_H #include <QStyledItemDelegate> #include "toggleswitcheditor.h" // implementation info // every time the editing is finished // you must emit QStyledItemDelegate::commitData( pointer to Editor) // and QStyledItemDelegate::closeEditor( pointer to Editor) class ToggleSwitchDelegate: public QStyledItemDelegate { Q_OBJECT public: ToggleSwitchDelegate(QObject* parent); //~ToggleSwitchDelegate(); QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; // editing releated methods QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; void paint(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: //mutable ViewToggleSw* toggle_sw; private slots: void commitAndCloseEditor(); };
toggleswitchdelegate.cpp
#include "toggleswitchdelegate.h" #include "toggleswitcheditor.h" ToggleSwitchDelegate::ToggleSwitchDelegate(QObject* parent ) : QStyledItemDelegate(parent) //, toggle_sw( new ViewToggleSw( qobject_cast<QWidget*>(parent)) ) {} /* ToggleSwitchDelegate::~ToggleSwitchDelegate() { delete toggle_sw; }*/ void ToggleSwitchDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const { if ( index.data().canConvert<bool>() ) { auto sw_state = qvariant_cast<bool>( index.data() ); ViewToggleSw toggle_sw; toggle_sw.setChecked( sw_state ); //toggle_sw->setChecked( sw_state ); // highlighted bg when selected if ( option.state & QStyle::State_Selected ) painter->fillRect( option.rect, option.palette.highlight() ); // static paint of the widget toggle_sw.paint( painter, option.rect, option.palette ); //toggle_sw->paint( painter, option.rect, option.palette ); } } QSize ToggleSwitchDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const { Q_UNUSED(option) if ( index.data().canConvert<bool>() ) { ViewToggleSw toggle_sw; return toggle_sw.sizeHint(); } return QSize(); } QWidget* ToggleSwitchDelegate::createEditor( QWidget* parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const { Q_UNUSED(option) if ( index.data().canConvert<bool>() ) { ToggleSwitchEditor* editor = new ToggleSwitchEditor( parent ); connect( editor, &ToggleSwitchEditor::editingFinished, this, &ToggleSwitchDelegate::commitAndCloseEditor ); return editor; } return nullptr; } void ToggleSwitchDelegate::setEditorData( QWidget* editor, const QModelIndex &index ) const { if ( index.data().canConvert<bool>() ) { auto sw_state = qvariant_cast<bool>( index.data() ); ViewToggleSw toggle_sw; toggle_sw.setChecked( sw_state ); auto* my_editor = qobject_cast<ToggleSwitchEditor*>( editor ); my_editor->setToggleSwitch( toggle_sw ); } } void ToggleSwitchDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const { if ( index.data().canConvert<bool>() ) { auto* my_editor = qobject_cast<ToggleSwitchEditor*>( editor ); model->setData( index, QVariant::fromValue(my_editor->viewToggleSw().isChecked()) ); } } /*************************** * * SLOTS * ***************************/ void ToggleSwitchDelegate::commitAndCloseEditor() { auto* editor = qobject_cast<ToggleSwitchEditor*>( sender() ); // this two signal come from QStyledItemDelegate emit this->commitData( editor ); emit this->closeEditor( editor ); }
-
the ViewToggleSw class look like that:
viewtogglesw.h
class ViewToggleSw : public QCheckBox { Q_OBJECT Q_PROPERTY(qreal m_handle_pos READ handlePosition WRITE setHandlePosition NOTIFY handlePosChanged FINAL) using PenBrushStyle = std::pair<QPen, QBrush>; int PEN_WIDTH { 3 }; // unchecked state QBrush BG_COL_UNCHECK { QColor(207, 207, 207) }; QBrush BORDER_COL_UNCHECK { QColor(113, 113, 113) }; // checked state QBrush BG_COL_CHECK { QColor(255, 255, 255) }; QBrush BORDER_COL_CHECK { QColor(152, 73, 249) }; // handle unchecked QBrush HANDLE_COL_UNCHECK { QColor(132, 131, 151) }; QBrush BORDER_HANDLE_COL { QColor(81, 80, 102) }; // handle checked QBrush HANDLE_COL_CHECK { QColor(161, 86, 253) }; public: explicit ViewToggleSw(QWidget *parent = nullptr); ~ViewToggleSw() override = default; // copy constructor ViewToggleSw( const ViewToggleSw& other); // copy assignment ViewToggleSw& operator= (const ViewToggleSw& other); QSize sizeHint() const override; void paint(QPainter *painter, const QRect &rect, const QPalette &palette) const; protected: bool hitButton( const QPoint& pos ) const override; // CheckBox method re-implementation private: qreal m_handle_pos{}; QPropertyAnimation* m_handle_animation; PenBrushStyle m_handle_style; PenBrushStyle m_frame_style; void setHandlePosition( qreal pos ) { m_handle_pos = pos; } qreal handlePosition() const { return m_handle_pos; } void setPainterBrushPen(QPainter* p, QPen& pen, QBrush& brush) const; private slots: void setAnimDirection( bool checked ); void onAnimationFinished(); signals: void handlePosChanged( int pos ); void animationFinished(); };
viewtogglesw.cpp
ViewToggleSw::ViewToggleSw(QWidget *parent) : QCheckBox{parent} { // setup checkbox and size this->setCheckable(true); setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); setMouseTracking(true); setContentsMargins(8, 0, 8, 0); // padding left-right for the handle // setup animation m_handle_animation = new QPropertyAnimation(this, "m_handle_pos", parent); m_handle_animation->setStartValue(0.0); m_handle_animation->setDuration(150); // connect animation::finished // to a slot that will notify // the editor connect( m_handle_animation, &QPropertyAnimation::finished, this, &ViewToggleSw::onAnimationFinished ); // change the Animation->endValue() // The ViewToggleSw.setChecked(bool) will // be called from the editor connect( this, &QCheckBox::toggled, this, &ViewToggleSw::setAnimDirection ); m_handle_style = std::make_pair( QPen(BORDER_HANDLE_COL, PEN_WIDTH), HANDLE_COL_UNCHECK ); m_frame_style = std::make_pair( QPen(BG_COL_UNCHECK, PEN_WIDTH), BORDER_COL_UNCHECK ); } //-------------------- copy constructor ----------------------------- ViewToggleSw::ViewToggleSw( const ViewToggleSw& other) : QCheckBox{ qobject_cast<QWidget*>( other.parent() ) } { // setup checkbox and size this->setCheckable(true); this->setChecked( other.isChecked() ); setMouseTracking(true); setContentsMargins(8, 0, 8, 0); // padding left-right for the handle // setup animation m_handle_animation = other.m_handle_animation; // setup toggle switch color m_handle_style = other.m_handle_style; m_frame_style = other.m_frame_style; // connect animation::finished // to a slot that will notify // the editor connect( m_handle_animation, &QPropertyAnimation::finished, this, &ViewToggleSw::onAnimationFinished ); // change the Animation->endValue() // The ViewToggleSw.setChecked(bool) will // be called from the editor connect( this, &QCheckBox::toggled, this, &ViewToggleSw::setAnimDirection ); } //------------------ copy assignment --------------------------------- ViewToggleSw& ViewToggleSw::operator= (const ViewToggleSw& other) { if ( this == &other ) return *this; this->setChecked( other.isChecked() ); // copy animation this->m_handle_animation = other.m_handle_animation; // copy toggle switch colors this->m_handle_style = other.m_handle_style; this->m_frame_style = other.m_frame_style; return *this; } bool ViewToggleSw::hitButton( const QPoint& pos ) const { return this->contentsRect().contains(pos); } void ViewToggleSw::setPainterBrushPen(QPainter* p, QPen& pen, QBrush& brush) const { p->setPen( pen ); p->setBrush( brush ); } /********************************* * * SLOTS * * *******************************/ void ViewToggleSw::setAnimDirection( bool checked ) { QPen handle_pen( BORDER_HANDLE_COL, PEN_WIDTH ); if ( checked ) { // handle pen and brush for paintEvent m_handle_style = std::make_pair( handle_pen, HANDLE_COL_CHECK ); // frame pen and brush m_frame_style = std::make_pair( QPen(BORDER_COL_CHECK, PEN_WIDTH), BG_COL_CHECK ); } else { // handle pen and brush for paintEvent m_handle_style = std::make_pair( handle_pen, HANDLE_COL_UNCHECK ); // frame pen and brush m_frame_style = std::make_pair( QPen(BORDER_COL_UNCHECK, PEN_WIDTH), BG_COL_UNCHECK ); } // sets animation direction m_handle_animation->stop(); m_handle_animation->setEndValue( checked ? 1.0 : 0.0 ); m_handle_animation->start(); } void ViewToggleSw::onAnimationFinished() { qDebug() << "\nanimation completed..."; emit animationFinished(); } /********************************* * * PUBLIC METHODS * * *******************************/ QSize ViewToggleSw::sizeHint() const { return QSize(30, 60); } void ViewToggleSw::paint( QPainter *painter, const QRect &rect, const QPalette &palette ) const { Q_UNUSED(palette) // this method will be called within // the Editor::paintEvent or // QStyledItemDelegate::paint() painter->setRenderHint(QPainter::Antialiasing, true); auto handle_radius = std::round( 0.24 * rect.height() ); // width will vary depending on Height.. 11/13 * h auto w_fx_h = std::round( (rect.height() * 11) / 13 ); auto frame_slider = QRectF( rect.x(), rect.y(), // x, y w_fx_h - handle_radius, // width = total_width minus radius of handle rect.height() * 0.40 // height = 40% of the frame_rect (analogus to 30% top & bottom padding) ); frame_slider.moveCenter( QPointF(rect.center()) ); auto border_radius = frame_slider.height() / 2; // the lenght that the handle will follow auto handle_path_len = w_fx_h - handle_radius; // x offset where the origin of the handle // will be drawn auto x_position = frame_slider.x() + // x constant offset ( handle_path_len * m_handle_pos ); // % of path lenght run //! drawing // // frame_rect's colors auto [pen, brush] = m_frame_style; this->setPainterBrushPen( painter, pen, brush ); painter->drawRoundedRect( frame_slider, border_radius, border_radius ); // handle's colors auto [pen2, brush2] = m_handle_style; this->setPainterBrushPen( painter, pen2, brush2 ); painter->drawEllipse( QPointF( x_position, rect.center().y() ), // ellipse center point handle_radius, handle_radius ); painter->end(); }
I asked google's AI (Gemini) to troubleshoot the possible reason and it said:
The errors "QPainter::save: Painter not active", "QPainter::setRenderHint: Painter must be active to set rendering hints", etc., within your ToggleSwitchDelegate::paint() method indicate that you're attempting to use a QPainter outside of a valid paint event context. Specifically, you are creating a local ViewToggleSw object and calling its paint method, but that paint method expects to be called by a QPainter that is already active (i.e., inside a widget's paintEvent).
I don't really get it.. Gemini suggested to directly paint it inside the paint event, but i'm using the ViewToggleSw also for painting the Editor that is an external class with a member variable ViewToggleSw toggle_sw and inside his paintEvent calls
QPainter painter(this);
toggle_sw.paint( painter, rect(), options); -
@Andrea_Venturelli said in How to use a QList of UserDefined class, as the Storage Model for QAbstractItemModel to be used by QTreeWidget:
toggle_sw.paint( painter, option.rect, option.palette );
I hope I'm not wrong, and I have never done anything like you do, but I don't think this is right: you never explicitly call
paint()
on something else. [EDIT per later posts this is incorrect.] Qt infrastructure calls it when something needs painting. I think this will give rise to the errors you see. If you comment that out do the 7 errors go away?Don't ask me what you're supposed to do, I'm not sure.
[This reply written before you typed your latest post, and refers to the one before.]
-
now i will try to better explain what should happen, simplifing the example a little bit.
instead of a toggle switch, take as assumption a QCheckBox instead:
-
the delegate must draw a checked checkbox if the model value is true, otherwise must draw only the empty checkbox frame
-
when the user click inside the cell, the editor should be called and an animation will start as soon as the user release the mouse's left button. when the animation::finished() signal is emitted, the editor catch it and emit "closeEditor()" for notifing the Delegate to setModelData and destroy the editor.
When I copied this code (suggested by Gemini), and commented out the line you suggested @JonB, the view correctly displayed the widget:
void ToggleSwitchDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const { if ( index.data().canConvert<bool>() ) { auto sw_state = qvariant_cast<bool>( index.data() ); ViewToggleSw toggle_sw; toggle_sw.setChecked( sw_state ); //toggle_sw->setChecked( sw_state ); // highlighted bg when selected if ( option.state & QStyle::State_Selected ) painter->fillRect( option.rect, option.palette.highlight() ); // static paint of the widget //toggle_sw.paint( painter, option.rect, option.palette ); //toggle_sw->paint( painter, option.rect, option.palette ); // *** DRAW DIRECTLY - NO WIDGET INSTANCE *** painter->setRenderHint(QPainter::Antialiasing, true); int handle_radius = std::round(0.24 * option.rect.height()); int w_fx_h = std::round((option.rect.height() * 11.0) / 13.0); // Use 11.0 and 13.0 for floating-point division QRectF frame_slider(option.rect.x(), option.rect.y(), w_fx_h - handle_radius, option.rect.height() * 0.40); frame_slider.moveCenter(option.rect.center()); int border_radius = frame_slider.height() / 2; int handle_path_len = w_fx_h - handle_radius; int x_position = frame_slider.x() + (handle_path_len * (sw_state ? 1.0 : 0.0)); // Draw Frame (using colors from ViewToggleSw or define them here) QPen framePen(sw_state ? QColor(152, 73, 249) : QColor(113, 113, 113), 3); QBrush frameBrush(sw_state ? QColor(255, 255, 255) : QColor(207, 207, 207)); painter->setPen(framePen); painter->setBrush(frameBrush); painter->drawRoundedRect(frame_slider, border_radius, border_radius); // Draw Handle QPen handlePen(QColor(81, 80, 102), 3); QBrush handleBrush(sw_state ? QColor(161, 86, 253) : QColor(132, 131, 151)); painter->setPen(handlePen); painter->setBrush(handleBrush); painter->drawEllipse(QPointF(x_position, option.rect.center().y()), handle_radius, handle_radius); } }
resulting in that:
and it's okay but the animation does not work but i think it finish to fast and the editor is closed but the Delegate:paint() provided by Gemini doesn't take in account the inactive state.. I will try implementing that and see if it works.. but i rather prefer to calling ViewToggleSw::paint() instead in a similar way as described by the Qt's StarDelegate Example
-
-
@Andrea_Venturelli said in How to use a QList of UserDefined class, as the Storage Model for QAbstractItemModel to be used by QTreeWidget:
but i rather prefer to calling ViewToggleSw::paint() instead in a similar way as described by the Qt's StarDelegate Example
Ah, I now see that has
starRating.paint(painter, option.rect, option.palette, StarRating::EditMode::ReadOnly);
(called from a
paint()
event) so apparently that is OK, so I withdraw my earlier comment.Going back then to your code. Warnings went away when you did not call
ViewToggleSw::paint()
. So look at that. Yours has noQPainter::begin()
but hasQPainter::end()
. Hence warning messages for mismatch? Star Delegate'sStarRating::paint()
is enclosed withQPainter::save()
/restore()
instead. I think you should do that. -
@JonB you were right and indeed makes sense not calling "painter.end()" because this will create problem to the painting of the second element and so on... using Painter::save() at the beginning and Painter::restore() at the end of the designated class to paint the obj is enoug like StarDelegate example provided by Qt's documentation Illustrates.
SOLUTION:
I found a easier a fastest way to change the state of a "togglable item" (like checkbox, toggle sw, ecc..) inside a Custom Delegate.
here the breakdown of the steps to follow:-
Is always better and simpler to draw the "static version" of the delegate inside the QStyledItemDelegate() instead of creating a custom class and pass the delegate's painter to a custom method of a class that have the painting implementation details about how to drow the item.
-
You don't always follow the starDelegate example provided by Qt because is not suitable for every purpose as I just learned.
-
if you want to directly toggle an item without passing through the process "click the cell you want to toggle" >> "onother click or double-click to trigger the editor creation" >> "onother click to change the state inside the editor just created" , you can directly toggle the state with only one click and using one methods like so:
#ifndef TOGGLESWITCHDELEGATE_H #define TOGGLESWITCHDELEGATE_H #include <QStyledItemDelegate> #include "viewtogglesw.h" #include <QPaintEvent> #include <QMouseEvent> class ToggleSwitchDelegate: public QStyledItemDelegate { Q_OBJECT public: ToggleSwitchDelegate(QObject* parent); ~ToggleSwitchDelegate(); QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; void paint(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; //* THIS IS THE MAGIC METHOD YOU WANT bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; private: mutable ViewToggleSw* toggle_sw; }; #endif // TOGGLESWITCHDELEGATE_H
#include "toggleswitchdelegate.h" ToggleSwitchDelegate::ToggleSwitchDelegate(QObject* parent ) : QStyledItemDelegate(parent) , toggle_sw( new ViewToggleSw( qobject_cast<QWidget*>(parent)) ) {} ToggleSwitchDelegate::~ToggleSwitchDelegate() { delete toggle_sw; } void ToggleSwitchDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const { if ( index.data().canConvert<bool>() ) { auto is_checked = qvariant_cast<bool>( index.data() ); // *** DRAW DIRECTLY - NO WIDGET INSTANCE *** painter->setRenderHint(QPainter::Antialiasing, true); int handle_radius = std::round(0.24 * option.rect.height()); int w_fx_h = std::round((option.rect.height() * 11.0) / 13.0); // Use 11.0 and 13.0 for floating-point division QRectF frame_slider(option.rect.x(), option.rect.y(), w_fx_h - handle_radius, option.rect.height() * 0.40); frame_slider.moveCenter(option.rect.center()); int border_radius = frame_slider.height() / 2; int handle_path_len = w_fx_h - handle_radius; int x_position = frame_slider.x() + (handle_path_len * (is_checked ? 1.0 : 0.0)); // Draw Frame (using colors from ViewToggleSw or define them here) QPen framePen(is_checked ? QColor(152, 73, 249) : QColor(113, 113, 113), 3); QBrush frameBrush(is_checked ? QColor(255, 255, 255) : QColor(207, 207, 207)); painter->setPen(framePen); painter->setBrush(frameBrush); painter->drawRoundedRect(frame_slider, border_radius, border_radius); // Draw Handle QPen handlePen(QColor(81, 80, 102), 3); QBrush handleBrush(is_checked ? QColor(161, 86, 253) : QColor(132, 131, 151)); painter->setPen(handlePen); painter->setBrush(handleBrush); painter->drawEllipse(QPointF(x_position, option.rect.center().y()), handle_radius, handle_radius); } } // MAGIC METHOD IMPLEMENTATION bool ToggleSwitchDelegate::editorEvent( QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index ) { // checks if index is valid, colum is the fourth (in my case, the column where i will install the delegate on the view // remember that first column as index.column() = 0 not 1, so Column 3 is the fourth // in my case I chose to toggle the checkedState when the event is of type mouseReleaseEvent bool idx_isValid = index.isValid(); bool isCol3_isTypeBool = ( index.column() == 3 && index.data().canConvert<bool>() ); bool isMouseReleaseEvent = ( event->type() == QEvent::MouseButtonRelease ); if ( idx_isValid && isCol3_isTypeBool && isMouseReleaseEvent ) { QMouseEvent* mouseEvent = static_cast<QMouseEvent*>( event ); // if the mouse Click is inside the cell toggle the model value if ( option.rect.contains(mouseEvent->pos()) ) { // if the current value is true, set it to false and viceversa bool is_checked = model->data( index, Qt::DisplayRole ).toBool(); model->setData( index, !is_checked, Qt::EditRole ); return true; // needed to consume the event } return true; } return false; } QSize ToggleSwitchDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const { Q_UNUSED(option) if ( index.data().canConvert<bool>() ) { return toggle_sw->sizeHint(); } return QSize(); }
Important: see the comments inside the above code snippet for further explanations
here the code on the constructor of QWidget (could be of type QMainWindow also)
Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); PathInfo info(QString("C:\\Users\\some\\path\\to\\Desktop\\file.png")); PathInfo info2(QString("C:\\Users\\123\\test\\documents")); PathInfo info3(QString("C:\\Users\\test\\Tor Browser\\Browser\\distribution\\extensions")); // only for test, default is set to true info3.setOpenable(false); QList<PathInfo> group; group.append( info ); group.append( info2 ); group.append( info3 ); paths_model = new PathsInfoTableModel( group, this ); // hide the row headers and add wrap word if path is to long ui->t_view->setModel( paths_model ); ui->t_view->verticalHeader()->hide(); ui->t_view->setWordWrap(true); ui->t_view->verticalHeader()->setDefaultSectionSize(54); // setting toggle switch delegate for col 3 auto* toggle_sw_delegate = new ToggleSwitchDelegate( ui->t_view ); ui->t_view->setItemDelegateForColumn( 3, toggle_sw_delegate ); // there is no needs of editTriggers because our magic method calls automatically setModel without creating an Editor ui->t_view->setEditTriggers(QAbstractItemView::NoEditTriggers); // this allow the user to double-click on the path cell and select a new one // with the QFileDialog::getOpenFileName and update the model.. connect( ui->t_view, &QTableView::doubleClicked, this, [this] ( const QModelIndex& index ) { if ( !index.isValid() ) return; if ( index.column() == 1 ) { auto* model = qobject_cast<PathsInfoTableModel*>( ui->t_view->model() ); if ( !model ) return; QString current_path = model->data( index, Qt::DisplayRole ).toString(); // if the parent of current path exist, use // it as default location else, Home PathInfo path_info( current_path ); auto default_path = (QFileInfo::exists(path_info.absPath())) ? path_info.absPath() : QDir::homePath(); QString newPath = QFileDialog::getOpenFileName(this, "Select new Path", default_path ); if ( !newPath.isEmpty() && newPath != current_path ) { QVariant newPathVariant = QVariant( newPath ); model->setData( index, newPathVariant, Qt::EditRole ); } } } ); }
-
-
Thanks to @JonB for always take part on my complicated, unusual problems and questions and for helping me.
I really appreciate it!this is how the application look like now
soon I'll post the full code on my github at https://github.com/aVenturelli-qt so anyone interested in it can take a look.
-