Mouse hover on QRect not working as expected
-
Hey,
I have a prolem with mouse hover. I have created a QListView with a custom delegat subclassed from QStyledItemDelegate. I following created:
void ToggleListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QString text = index.data(Qt::DisplayRole).toString(); QRect original = option.rect; QRect button = original.adjusted(original.width() - mButtonSize.width(),0,0,0); // QSize mButtonSize = QSize(40,40); QRect label = original.adjusted(0,0, -mButtonSize.width(),0); painter->drawText(label, Qt::AlignLeft, text); QIcon icon = QIcon(":/icons/trash_black.png"); icon.paint(painter, button); const auto widget = qobject_cast<QWidget*>(option.styleObject); QPoint cursor = QCursor::pos(); QPoint position = widget->mapFromGlobal(cursor); if(button.contains(position)){ bool hover = option.state & QStyle::State_MouseOver; if(hover){ icon = QIcon(":/icons/trash_red.png"); icon.paint(painter, button); } } painter->save(); painter->restore(); }
As you can see in the animated graphic it also works when I hover over the icon it turns red. But only if I move from top to bottom. If I move from the bottom to the top, the icon is not red. Likewise if I move out of the area to the left, the icon does not turn black again.
Does anyone have an idea? Why hover is false if I move from bottom to top or from the icon to the left side?
Thanks
-
A very big thanks goes to @jeremy_k . Your tip and the code example was worth its weight in gold. Thank you!
Now my code looks like this:
Delegate:void ToggleListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QString text = index.data(Qt::DisplayRole).toString(); QRect original = option.rect; QRect button = original.adjusted(original.width() - mButtonSize.width(),0,0,0); QRect label = original.adjusted(0,0, -mButtonSize.width(),0); painter->drawText(label, Qt::AlignLeft, text); QIcon icon = QIcon(":/icons/trash_black.png"); icon.paint(painter, button); const auto widget = qobject_cast<ToggleListView*>(option.styleObject); QPoint cursor = QCursor::pos(); QPoint position = widget->viewport()->mapFromGlobal(cursor); if(button.contains(position)){ const bool hover = option.state & QStyle::State_MouseOver; if(hover){ icon = QIcon(":/icons/trash_red.png"); icon.paint(painter, button); } } painter->save(); painter->restore(); }
mouseMoveEvent:
void ToggleListView::mouseMoveEvent(QMouseEvent *event) { QPoint position = event->pos(); QModelIndex index = indexAt(position); QRect original = visualRect(index); QRect button = original.adjusted(original.width() - mButtonSize.width(),0,0,0); bool currentButtonState = button.contains(position); if(index != mPreviousIndex){ if(mPreviousIndex.isValid()) update(mPreviousIndex); if(index.isValid()) update(index); } else if(currentButtonState != mPreviousButtonState){ update(index); } mPreviousIndex = index; mPreviousButtonState = currentButtonState; }
-
Print out the values of button and position to see what's going on. I would guess paint is not called since there is no need for a repaint.
-
Thank you for your reply. I have taken my screen again. The output is underneath:
--------------------- Button: QRect(280,0 40x28) Position: QPoint(305,2) --------------------- Button: QRect(280,28 40x28) Position: QPoint(306,30) --------------------- Button: QRect(280,56 40x28) Position: QPoint(308,58) --------------------- Button: QRect(280,56 40x28) Position: QPoint(308,57) --------------------- Button: QRect(280,28 40x28) Position: QPoint(305,29) --------------------- Button: QRect(280,0 40x28) Position: QPoint(305,1)
My code part:
if(button.contains(position)){ qDebug() << "---------------------"; qDebug() << "Button: " << button; qDebug() << "Position: " << position; bool hover = option.state & QStyle::State_MouseOver; if(hover){ icon = QIcon(":/icons/trash_red.png"); icon.paint(painter, button); } }
-
@Gabber Print the output before your check to see what value it has when it doesn't work and if there is really a paint event at all in this case.
Maybe override mouseMoveEvent() and call update() in there to force a redraw. -
I put the qDebug() outputs, before the if query. For simplicity, I have only 3 items displayed in the QListView. Below are again the output to the image.
The qDebug() output:
Name: "722" Name: "5456" Name: "6429" --------------------- Button: QRect(294,0 40x28) Position: QPoint(93,-85) --------------------- Button: QRect(294,28 40x28) Position: QPoint(93,-85) --------------------- Button: QRect(294,56 40x28) Position: QPoint(93,-85) --------------------- Button: QRect(294,0 40x28) Position: QPoint(313,2) --------------------- Button: QRect(294,28 40x28) Position: QPoint(313,2) --------------------- Button: QRect(294,56 40x28) Position: QPoint(313,2) --------------------- Button: QRect(294,0 40x28) Position: QPoint(317,30) --------------------- Button: QRect(294,28 40x28) Position: QPoint(317,30) --------------------- Button: QRect(294,28 40x28) Position: QPoint(317,58) --------------------- Button: QRect(294,56 40x28) Position: QPoint(317,58) --------------------- Button: QRect(294,28 40x28) Position: QPoint(317,57) --------------------- Button: QRect(294,56 40x28) Position: QPoint(317,57) --------------------- Button: QRect(294,0 40x28) Position: QPoint(317,29) --------------------- Button: QRect(294,28 40x28) Position: QPoint(317,29) --------------------- Button: QRect(294,0 40x28) Position: QPoint(329,1) --------------------- Button: QRect(294,28 40x28) Position: QPoint(329,1) --------------------- Button: QRect(294,56 40x28) Position: QPoint(329,1) --------------------- Button: QRect(294,0 40x28) Position: QPoint(803,-425) --------------------- Button: QRect(294,28 40x28) Position: QPoint(803,-425) --------------------- Button: QRect(294,56 40x28) Position: QPoint(803,-425)
Can you see why it doesn't work?
I am very grateful for your help!
-
@Gabber said in Mouse hover on QRect not working as expected:
Position: QPoint(803,-425)
This looks strange to me. SInce it operates on a QListWidget please cast your widget to it and work on viewport() when you map the coordinate.
-
I tried this with the following:
const auto widget = qobject_cast<QListWidget*>(option.styleObject); QPoint cursor = QCursor::pos(); QPoint position = widget->viewport()->mapFromGlobal(cursor);
But I get a Segmentation fault at QPoint position.
Edit:
@Christian-Ehrlicher said in Mouse hover on QRect not working as expected:
Position: QPoint(803,-425)
I think this came about when I stopped my screen recording.
-
@Gabber said in Mouse hover on QRect not working as expected:
But I get a Segmentation fault at QPoint position.
const auto widget = qobject_cast<QListWidget*>(option.styleObject);
Did you verify the return result from the
qobject_cast<>()
?? -
@JonB @Christian-Ehrlicher
I got a little further. Top to bottom and bottom to top now works. For this I have changed the following line from:QPoint position = widget->mapFromGlobal(cursor);
to
QPoint position = widget->viewport()->mapFromGlobal(cursor);
But how do I get this to work when I move from the trash icon to the text and back? Any ideas?
-
Hi,
Test whether the point is in the rectangle where the trash icon is drawn.
-
@SGaist said in Mouse hover on QRect not working as expected:
Test whether the point is in the rectangle where the trash icon is drawn.
I have colored the whole thing briefly. When I move the mouse from the red rectangle to the yellow rectangle, I want the trash can icon to change back to black. If I move from yellow to red it should become red again.
The Paint method from the delegate is not called again here, if I move from the red to the yellow rectangle. I don't know exactly how this works with the paint stuff. Do I have to overwrite the paintEvent or work with the mouseMoveEvent or something else?
Thanks for the help!
-
@Christian-Ehrlicher @JonB @SGaist
I have found a way how everything works now. Here is my solution.
My paint function from custom delegate:
void ToggleListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QString text = index.data(Qt::DisplayRole).toString(); QRect original = option.rect; QRect button = original.adjusted(original.width() - mButtonSize.width(),0,0,0); QRect label = original.adjusted(0,0, -mButtonSize.width(),0); painter->drawText(label, Qt::AlignLeft, text); QIcon icon = QIcon(":/icons/trash_black.png"); icon.paint(painter, button); const auto widget = qobject_cast<ToggleListView*>(option.styleObject); QPoint cursor = QCursor::pos(); QPoint position = widget->viewport()->mapFromGlobal(cursor); const bool selected = option.state & QStyle::State_Selected; if(selected){ painter->fillRect(original, option.palette.highlight()); painter->drawText(label, Qt::AlignLeft, text); icon.paint(painter, button); } if(button.contains(position)){ const bool hover = option.state & QStyle::State_MouseOver; if(hover){ icon = QIcon(":/icons/trash_red.png"); icon.paint(painter, button); } } painter->save(); painter->restore(); }
My custom lisstview:
ToggleListView::ToggleListView(QWidget *parent): QListView(parent) { this->setMouseTracking(true); } void ToggleListView::mouseMoveEvent(QMouseEvent *event) { QModelIndex index = indexAt(event->pos()); QPoint position = event->pos(); QRect original = visualRect(index); QRect label = original.adjusted(0,0, -mButtonSize.width(),0); if(label.contains(position)){ this->viewport()->repaint(); } else { this->viewport()->repaint(); } }
Is it overkill to redraw the whole thing every time? Do you have a better idea or a suggestion for improvement?
Thanks for your help!
-
I think the trick for getting repaints for hover events in a view is to turn on the QA_Hover attribute for the viewport.
#include <QApplication> #include <QListView> #include <QStyledItemDelegate> #include <QStringListModel> #include <QDebug> class Delegate: public QStyledItemDelegate { Q_OBJECT public: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { qDebug() << "paint" << index; QStyledItemDelegate::paint(painter, option, index); } }; int main(int argc, char *argv[]) { QApplication a(argc, argv); QListView lv; lv.viewport()->setAttribute(Qt::WA_Hover); lv.setItemDelegate(new Delegate); QStringListModel model({"first", "second", "third"}); lv.setModel(&model); lv.show(); return a.exec(); } #include "main.moc"
-
@jeremy_k said in Mouse hover on QRect not working as expected:
I think the trick for getting repaints for hover events in a view is to turn on the QA_Hover attribute for the viewport.
A more straightforward solution may be to use something like:
view->setMouseTracking(true); QObject::connect(view, &QAbstractItemView::entered, [] (QModelIndex *index) { // Reset the previous index if any, and then do something with this index });
-
Thank you for your reply. I had tried the whole thing with the hover attribute before, but it didn't work. Because as described above I have to change the icon when I go from the red to the yellow area and vice versa. This works with the above solution using mouseMoveEvent.
Thanks for your tip anyway!
-
I missed the problem revision. Thanks for pointing it out.
The mouseMoveEvent() implementation above has a few obvious opportunities for improvement in efficiency. This is going to repaint the entire visible portion of the view on every move, even if the cursor remains in the same portion of the same delegate instance.
if(label.contains(position)){ this->viewport()->repaint(); } else { this->viewport()->repaint(); }
This is a tautology. If nothing else, the code can be simplified by removing the condition. The QRect::contains() is const, so the compiler may already be optimizing the test out.
Repainting can be limited to the impacted indexes by using QAbstractItemView::update(). Cache the previous index and button highlight state to detect when a repaint isn't necessary at all.
if (currentIndex != this->previousIndex) { if (this->previousIndex.isValid()) this->update(this->previousIndex); if (currentIndex.isValid()) this->update(currentIndex); } else if (currentButtonState != this->previousButtonState) { this->update(currentIndex); } this->previousIndex = currentIndex; this->previousButtonState = currentButtonState;
-
@jeremy_k
Thank you for pointing this out. Can you explain your example in more detail? For example, I don't understand how to get the "currentButtonState" or "previousButtonState". I don't have a "real" button, but only two QRects, one representing the "Button" and the other the "Label/Text".How do I implement this in the mouseMoveEvent function of the custom QListView?
-
@Gabber said in Mouse hover on QRect not working as expected:
@jeremy_k
Thank you for pointing this out. Can you explain your example in more detail? For example, I don't understand how to get the "currentButtonState" or "previousButtonState". I don't have a "real" button, but only two QRects, one representing the "Button" and the other the "Label/Text".How do I implement this in the mouseMoveEvent function of the custom QListView?
Use the logic already present in ToggleListDelegate::paint() and ToggleListView::mouseMoveEvent().
eg:bool currentButtonState = button.contains(viewport->mapFromGlobal(QCursor::pos()))
-
A very big thanks goes to @jeremy_k . Your tip and the code example was worth its weight in gold. Thank you!
Now my code looks like this:
Delegate:void ToggleListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QString text = index.data(Qt::DisplayRole).toString(); QRect original = option.rect; QRect button = original.adjusted(original.width() - mButtonSize.width(),0,0,0); QRect label = original.adjusted(0,0, -mButtonSize.width(),0); painter->drawText(label, Qt::AlignLeft, text); QIcon icon = QIcon(":/icons/trash_black.png"); icon.paint(painter, button); const auto widget = qobject_cast<ToggleListView*>(option.styleObject); QPoint cursor = QCursor::pos(); QPoint position = widget->viewport()->mapFromGlobal(cursor); if(button.contains(position)){ const bool hover = option.state & QStyle::State_MouseOver; if(hover){ icon = QIcon(":/icons/trash_red.png"); icon.paint(painter, button); } } painter->save(); painter->restore(); }
mouseMoveEvent:
void ToggleListView::mouseMoveEvent(QMouseEvent *event) { QPoint position = event->pos(); QModelIndex index = indexAt(position); QRect original = visualRect(index); QRect button = original.adjusted(original.width() - mButtonSize.width(),0,0,0); bool currentButtonState = button.contains(position); if(index != mPreviousIndex){ if(mPreviousIndex.isValid()) update(mPreviousIndex); if(index.isValid()) update(index); } else if(currentButtonState != mPreviousButtonState){ update(index); } mPreviousIndex = index; mPreviousButtonState = currentButtonState; }
-
Happy to help.
@Gabber said in Mouse hover on QRect not working as expected:
Now my code looks like this:
Delegate:void ToggleListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { [...] painter->save(); painter->restore(); }
QPainter::save() and restore are only necessary if the painter's settings are altered and need to be restored later. For example:
painter->save(); painter->setBrush(QBrush(QColorConstant::Green); painter->drawRect(0, 0, 10, 10); painter->restore();