Customizing scrollbar over QListWidget - qt

I'd like to implement my own semi-transparent scrollbar, that draws on top of the QListWidget instead of taking up permanent space in its viewport. I do not wish to use QML as my QListWidget and its dynamic content is already fully developed over 6 months now.
How can I achieve that. Stylesheets are useless for that purpose as they will not determine the positioning of the scrollbar. I'd like it to be on top of the QListWidget, not on its side, taking up its space.
I'm talking about something in the neighborhood of this:
Any hints as to how to do that will be appreciated.

What you are trying to do is a perfect example of one thing that persistently annoys me about qt - if there is some graphical effect that Qt's designers haven't thought of, creating it on your own is a pain, constant fight against Qt, and usually ends with giving up anyway.
I suspect that you do it with small screens on your mind (cell phones? tablets?), so i guess there is no other way to solve this problem.
What I am trying here is hacky, but otherwise you would probably have to rewrite entire scrollbar yourself just to add those few missing details. My proposition is:
#ifndef MYSCROLLBAR_H
#define MYSCROLLBAR_H
#include <QScrollBar>
class MyScrollBar : public QScrollBar
{
Q_OBJECT
public:
explicit MyScrollBar(QWidget *parent = 0);
protected:
void showEvent ( QShowEvent * event );
signals:
public slots:
void updateMask();
};
#endif // MYSCROLLBAR_H
And in myscrollbar.cpp
#include "myscrollbar.h"
#include <QPaintEvent>
#include <QRegion>
#include <QStyleOptionSlider>
MyScrollBar::MyScrollBar(QWidget *parent) :
QScrollBar(parent)
{
connect(this, SIGNAL(valueChanged(int)), this, SLOT(updateMask()));
}
void MyScrollBar::updateMask(){
QStyleOptionSlider opt;
initStyleOption(&opt);
QRegion r(style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this));
r+= style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarAddLine, this);
r+= style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSubLine, this);
setMask(r);
}
void MyScrollBar::showEvent ( QShowEvent * event ){
QScrollBar::showEvent(event);
updateMask();
}
Such scroller will be transparent (both visually and event-wise) in any of it's non-vital parts. It still creates some artifacts on widgets laying below it - I guess setMask() was never supposed to be used like this. To mitigate it you can connect valueChanged() signal to update() slot of the viewport of your list widget. This worked nice on my toy-example, but if you embed custom widgets in your list it might become unbearable to cope with. It might also run you into performance problems in case of more complex applications - especially if you write for mobile platforms.
Alternatively you can just "fork" entire QScrollBar class and simply modify it's paintEvent to use less subControls than SC_All - with additional setAttribute(Qt::WA_OpaquePaintEvent, false); in constructor it should provide visual transparency. Then you should also forward mouse events (if not hitting anything important) to your viewport of list widget (again, trouble with custom widgets in view).
Now what remains is writing your own layout class (or just manually positioning it) that will put both listview and scrollbar on one another in correct positions - QStackedLayout sounds nice, but it allows only one layer to be visible at any given time - clearly not what we are looking for.
Last step is switching off default scrollbars on view, and connecting signals/slots of the default (invisible) scrollbar to slots/signals of your scrollbar, to achieve effect of actual scrolling.
Shortly this will require a LOT of coding to get done. Are you sure that such a simple effect is worth it?
** EDIT: **
I create a layout class for stacking widgets on top of one another - this question gave me motivation to do it finally ;)
#ifndef STACKLAYOUT_H
#define STACKLAYOUT_H
#include <QLayout>
class StackLayout : public QLayout
{
Q_OBJECT
public:
StackLayout();
explicit StackLayout(QWidget *parent);
~StackLayout();
void addItem ( QLayoutItem * item );
int count () const;
Qt::Orientations expandingDirections () const;
bool hasHeightForWidth () const;
int heightForWidth ( int w ) const;
QLayoutItem * itemAt ( int index ) const;
bool isEmpty () const;
QSize maximumSize () const;
int minimumHeightForWidth ( int w ) const;
QSize minimumSize () const;
void setGeometry ( const QRect & r );
QSize sizeHint () const;
QLayoutItem * takeAt ( int index );
private:
QList<QLayoutItem *> itemList;
};
#endif // STACKLAYOUT_H
And the stacklayout.cpp file:
StackLayout::StackLayout()
:QLayout()
{}
StackLayout::StackLayout(QWidget *parent) :
QLayout(parent)
{
}
StackLayout::~StackLayout(){
QLayoutItem *item;
foreach (item, itemList){
delete item;
}
}
void StackLayout::addItem ( QLayoutItem * item ){
itemList.append(item);
}
int StackLayout::count () const{
return itemList.count();
}
Qt::Orientations StackLayout::expandingDirections () const{
Qt::Orientations result = 0;
QLayoutItem *item;
foreach (item, itemList){
result = result | item->expandingDirections();
}
return result;
}
bool StackLayout::hasHeightForWidth () const{
QLayoutItem *item;
foreach (item, itemList){
if (item->hasHeightForWidth())
return true;
}
return false;
}
int StackLayout::heightForWidth ( int w ) const{
int result = 0;
QLayoutItem *item;
foreach (item, itemList){
if (item->hasHeightForWidth())
result = qMax(result, item->heightForWidth(w));
}
return result;
}
QLayoutItem * StackLayout::itemAt ( int index ) const{
if (index<itemList.count())
return itemList[index];
return 0;
}
bool StackLayout::isEmpty () const{
QLayoutItem *item;
foreach (item, itemList){
if (!item->isEmpty())
return false;
}
return true;
}
QSize StackLayout::maximumSize () const{
QSize result=QLayout::maximumSize();
QLayoutItem *item;
foreach (item, itemList){
result = result.boundedTo(item->maximumSize());
}
return result;
}
int StackLayout::minimumHeightForWidth ( int w ) const{
int result = 0;
QLayoutItem *item;
foreach (item, itemList){
if (item->hasHeightForWidth())
result = qMax(result, item->minimumHeightForWidth(w));
}
return result;
}
QSize StackLayout::minimumSize () const{
QSize result=QLayout::minimumSize();
QLayoutItem *item;
foreach (item, itemList){
result = result.expandedTo(item->minimumSize());
}
return result;
}
void StackLayout::setGeometry ( const QRect & r ){
QLayoutItem *item;
foreach (item, itemList){
item->setGeometry(r);
}
}
QSize StackLayout::sizeHint () const{
QSize result=QSize(0,0);
QLayoutItem *item;
foreach (item, itemList){
result = result.expandedTo(item->sizeHint());
}
return result;
}
QLayoutItem * StackLayout::takeAt ( int index ){
if (index < itemList.count())
return itemList.takeAt(index);
return 0;
}
Assuming you already have some nice transparent scrollbar, to insert it you would do:
QWidget* w = new QWidget();
StackLayout* sl = new StackLayout(w);
QListView* lv = new QListView(w);
sl->addWidget(lv);
QHBoxLayout* hbl = new QHBoxLayout();
sl->addItem(hbl);
TransparentScrollBar* tsc = new TransparentScrollBar(w);
hbl->addWidget(tsc,0);
hbl->insertStretch(0,1);

Here is sample code for your questoin.
Not done:
Mouse dragging of scroller
Done:
Support of any mouse hover/leave events
Support of scrolling
Scroll bar is transparent for mouse events
It is good start point for any customization depending on you task. Usage:
GUI::MegaScrollBar *bar = new GUI::MegaScrollBar( ui->listWidget );
bar->resize( 40, 30 ); // First arg - width of scroller
MegaScrollBar.h
#ifndef MEGASCROLLBAR_H
#define MEGASCROLLBAR_H
#include <QWidget>
#include <QPointer>
class QAbstractItemView;
class QResizeEvent;
namespace GUI
{
class MegaScrollBar
: public QWidget
{
Q_OBJECT
public:
MegaScrollBar( QAbstractItemView *parentView );
~MegaScrollBar();
private slots:
void updatePos();
private:
bool eventFilter( QObject *obj, QEvent *event );
void onResize( QResizeEvent *e );
void paintEvent( QPaintEvent * event );
void resizeEvent( QResizeEvent * event );
QPointer< QAbstractItemView > m_view;
QPointer< QWidget > m_scrollBtn;
};
}
#endif // MEGASCROLLBAR_H
MegaScrollBar.cpp
#include "MegaScrollBar.h"
#include <QAbstractItemView>
#include <QEvent>
#include <QResizeEvent>
#include <QScrollBar>
#include <QDebug>
#include <QPainter>
#include "ScrollButton.h"
namespace GUI
{
MegaScrollBar::MegaScrollBar( QAbstractItemView *parentView )
: QWidget( parentView, Qt::FramelessWindowHint )
, m_view( parentView )
{
Q_ASSERT( parentView );
setAttribute( Qt::WA_TranslucentBackground );
setAttribute( Qt::WA_TransparentForMouseEvents );
m_scrollBtn = new ScrollButton( parentView );
m_scrollBtn->setFixedSize( 20, 40 );
m_view->installEventFilter( this );
QScrollBar *sb = m_view->verticalScrollBar();
connect( sb, SIGNAL( valueChanged( int ) ), this, SLOT( updatePos() ) );
}
MegaScrollBar::~MegaScrollBar()
{
removeEventFilter( m_view );
}
bool MegaScrollBar::eventFilter( QObject *obj, QEvent *event )
{
switch ( event->type() )
{
case QEvent::Enter:
m_scrollBtn->show();
break;
case QEvent::Leave:
m_scrollBtn->hide();
break;
case QEvent::Resize:
onResize( static_cast< QResizeEvent * >( event ) );
break;
}
return QWidget::eventFilter( obj, event );
}
void MegaScrollBar::onResize( QResizeEvent *e )
{
const int x = e->size().width() - width();
const int y = 0;
const int w = width();
const int h = e->size().height();
move( x, y );
resize( w, h );
updatePos();
}
void MegaScrollBar::updatePos()
{
QScrollBar *sb = m_view->verticalScrollBar();
const int min = sb->minimum();
const int val = sb->value();
const int max = sb->maximum();
const int x = pos().x() + ( width() - m_scrollBtn->width() ) / 2;
if ( max == 0 )
{
m_scrollBtn->move( x, pos().y() );
return ;
}
const int maxY = height() - m_scrollBtn->height();
const int y = ( maxY * val ) / max;
m_scrollBtn->move( x, y );
}
void MegaScrollBar::paintEvent( QPaintEvent * event )
{
Q_UNUSED( event );
QPainter p( this );
QRect rc( 0, 0, rect().width() - 1, rect().height() - 1 );
// Draw any scroll background
p.fillRect( rc, QColor( 255, 255, 200, 100 ) );
}
void MegaScrollBar::resizeEvent( QResizeEvent * event )
{
Q_UNUSED( event );
updatePos();
}
}
Preview:
It is possible to set up any widget for scroll button: Here is custom one:
ScrollButton.h
#ifndef SCROLLBUTTON_H
#define SCROLLBUTTON_H
#include <QWidget>
namespace GUI
{
class ScrollButton
: public QWidget
{
Q_OBJECT
public:
ScrollButton( QWidget *parent );
~ScrollButton();
private:
void paintEvent( QPaintEvent * event );
};
}
#endif // SCROLLBUTTON_H
ScrollButton.cpp
#include "ScrollButton.h"
#include <QPainter>
#include <QGraphicsOpacityEffect>
#include <QColor>
namespace GUI
{
ScrollButton::ScrollButton( QWidget *parent )
: QWidget( parent )
{
QGraphicsOpacityEffect *op = new QGraphicsOpacityEffect( this );
op->setOpacity( 0.5 );
setGraphicsEffect( op );
}
ScrollButton::~ScrollButton()
{
}
void ScrollButton::paintEvent( QPaintEvent * event )
{
Q_UNUSED( event );
// Draw any scroll button
QPainter p( this );
QRect rc( 5, 5, rect().width() - 6, rect().height() - 6 );
p.fillRect( rc, QColor( 0, 0, 0, 255 ) );
}
}
Please comment, if you can't handle mouse interaction.

Related

How to connect a QRadioButton of a QTableWidget cell with another QTableWidget cell using an arrow?

I have a QListWidget and a QGraphicsView both subclassed to overwrite some of their members. I prepared a minimal verifiable example showing the problem I have here
From the QListWidget I can drag and drop specific field (represented by a QTableWidget) and drop them into a QGraphicsView and in order to do that I am using a QGraphicsProxyWidget approach as shown below.
The Problem
Now, how do I connect 2 QRadioButton inside cell of a QTableWidget with another cell of another QTableWidget?
It is important to mention that the green QGraphicsRectItem it is used to move around the QTableWidget as well as adjusting its dimension.
Below the result I was able to arrive so far:
And below the expected result I have been trying to achieve:
Below the most important part of the code:
scene.h
#ifndef SCENE_H
#define SCENE_H
#include <QGraphicsScene>
class Scene : public QGraphicsScene
{
public:
Scene(QObject *parent = nullptr);
protected:
void dragEnterEvent(QGraphicsSceneDragDropEvent *event);
void dragMoveEvent(QGraphicsSceneDragDropEvent *event);
void dropEvent(QGraphicsSceneDragDropEvent *event);
};
#endif // SCENE_H
scene.cpp
#include "arrow.h"
#include <QGraphicsSceneDragDropEvent>
#include <QMimeData>
#include <QTableWidget>
#include <QGraphicsProxyWidget>
#include <QVBoxLayout>
#include <QMetaEnum>
#include <QEvent>
#include <QSizeGrip>
#include <QRadioButton>
Scene::Scene(QObject *parent)
{
setBackgroundBrush(Qt::lightGray);
}
void Scene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) {
if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
void Scene::dragMoveEvent(QGraphicsSceneDragDropEvent *event) {
if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
void Scene::dropEvent(QGraphicsSceneDragDropEvent *event) {
QByteArray encoded =
event->mimeData()->data("application/x-qabstractitemmodeldatalist");
QDataStream stream(&encoded, QIODevice::ReadOnly);
QStringList rosTables;
QString newString;
while (!stream.atEnd()) {
int row, col;
QMap<int, QVariant> roleDataMap;
stream >> row >> col >> roleDataMap;
rosTables << roleDataMap[Qt::DisplayRole].toString();
}
for (const QString &tableType : rosTables) {
if (tableType == "Images") {
QPoint initPos(0, 0);
auto *wgt = new CustomTableWidget;
auto *proxyControl = addRect(0, 0, 0, 0, QPen(Qt::black),
QBrush(Qt::darkGreen));
auto *sizeGrip = new QSizeGrip(wgt);
auto *layout = new QHBoxLayout(wgt);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(sizeGrip, 0, Qt::AlignRight | Qt::AlignBottom);
connect(wgt, &CustomTableWidget::sizeChanged, [wgt, proxyControl](){
proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
});
wgt->setColumnCount(4);
wgt->setRowCount(4);
for (int ridx = 0; ridx < wgt->rowCount(); ridx++) {
for (int cidx = 0; cidx < wgt->columnCount(); cidx++) {
QRadioButton *radio1, *radio2;
auto* item = new QTableWidgetItem();
item->setText(QString("%1").arg(ridx));
wgt->setItem(ridx,cidx,item);
radio1 = new QRadioButton;
radio2 = new QRadioButton;
wgt->setCellWidget(cidx, 0, radio1);
wgt->setCellWidget(cidx, 3, radio2);
Arrow *arrow = new Arrow;
}
}
auto *const proxy = addWidget(wgt);
proxy->setPos(initPos.x(), initPos.y()
+ proxyControl->rect().height());
proxy->setParentItem(proxyControl);
proxyControl->setPos(initPos.x(), initPos.y());
proxyControl->setFlag(QGraphicsItem::ItemIsMovable, true);
proxyControl->setFlag(QGraphicsItem::ItemIsSelectable, true);
proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
}
}
}
diagramitem.h
#ifndef DIAGRAMITEM_H
#define DIAGRAMITEM_H
#include <QGraphicsPolygonItem>
class Arrow;
class DiagramItem : public QGraphicsPolygonItem
{
public:
DiagramItem(QMenu *contextMenu, QGraphicsItem *parent = Q_NULLPTR);
void removeArrow(Arrow *arrow);
void removeArrows();
void addArrow(Arrow *arrow);
QPixmap image() const;
protected:
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
QPolygonF myPolygon;
QList<Arrow*> arrows;
QMenu *myContextMenu;
};
#endif // DIAGRAMITEM_H
diagramitem.cpp
#include "diagramitem.h"
#include "arrow.h"
#include <QPainter>
#include <QGraphicsScene>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>
DiagramItem::DiagramItem(QMenu *contextMenu, QGraphicsItem *parent) : QGraphicsPolygonItem(parent)
{
myContextMenu = contextMenu;
setPolygon(myPolygon);
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsItem::ItemIsSelectable, true);
setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}
void DiagramItem::removeArrow(Arrow *arrow)
{
int index = arrows.indexOf(arrow);
if (index != -1)
arrows.removeAt(index);
}
void DiagramItem::removeArrows()
{
foreach (Arrow *arrow, arrows) {
arrow->startItem()->removeArrow(arrow);
arrow->endItem()->removeArrow(arrow);
scene()->removeItem(arrow);
delete arrow;
}
}
void DiagramItem::addArrow(Arrow *arrow)
{
arrows.append(arrow);
}
void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
scene()->clearSelection();
setSelected(true);
myContextMenu->exec(event->screenPos());
}
QVariant DiagramItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemPositionChange) {
foreach (Arrow *arrow, arrows) {
arrow->updatePosition();
}
}
return value;
}
arrow.h
#ifndef ARROW_H
#define ARROW_H
#include <QGraphicsLineItem>
#include "diagramitem.h"
class Arrow : public QGraphicsLineItem
{
public:
enum { Type = UserType + 4 };
Arrow(DiagramItem *startItem, DiagramItem *endItem,
QGraphicsItem *parent = nullptr);
DiagramItem *startItem() const { return myStartItem; }
DiagramItem *endItem() const { return myEndItem; }
QPainterPath shape() const override;
void setColor(const QColor &color) {
myColor = color;
}
int type() const override { return Type; }
void updatePosition();
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
private:
QColor myColor;
DiagramItem *myStartItem;
DiagramItem *myEndItem;
QPolygonF arrowHead;
};
#endif // ARROW_H
arrow.cpp
#include "arrow.h"
#include <QPen>
#include <QPainter>
#include "qmath.h"
Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent) : QGraphicsLineItem(parent)
{
myStartItem = startItem;
myEndItem = endItem;
myColor = Qt::GlobalColor::black;
setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
setFlag(QGraphicsItem::ItemIsSelectable, true);
}
QPainterPath Arrow::shape() const
{
QPainterPath path = QGraphicsLineItem::shape();
path.addPolygon(arrowHead);
return path;
}
void Arrow::updatePosition()
{
QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0));
setLine(line);
}
void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option)
Q_UNUSED(widget)
if (myStartItem->collidesWithItem(myEndItem))
return;
QPen myPen = pen();
myPen.setColor(myColor);
qreal arrowSize = 20;
painter->setPen(myPen);
painter->setBrush(myColor);
QLineF centerLine(myStartItem->pos(), myEndItem->pos());
QPolygonF endPolygon = myEndItem->polygon();
QPointF p1 = endPolygon.first() + myEndItem->pos();
QPointF p2;
QPointF intersectPoint;
QLineF polyLine;
for (int i = 1; i < endPolygon.count(); ++i) {
p2 = endPolygon.at(i) + myEndItem->pos();
polyLine = QLineF(p1, p2);
QLineF::IntersectType intersectType =
polyLine.intersect(centerLine, &intersectPoint);
if (intersectType == QLineF::BoundedIntersection)
break;
p1 = p2;
}
setLine(QLineF(intersectPoint, myStartItem->pos()));
double angle = std::atan2(-line().dy(), line().dx());
QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize,
cos(angle + M_PI / 3) * arrowSize);
QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
cos(angle + M_PI - M_PI / 3) * arrowSize);
arrowHead.clear();
arrowHead << line().p1() << arrowP1 << arrowP2;
painter->drawLine(line());
painter->drawPolygon(arrowHead);
if (isSelected()) {
painter->setPen(QPen(myColor, 1, Qt::DashLine));
QLineF myLine = line();
myLine.translate(0, 4.0);
painter->drawLine(myLine);
myLine.translate(0,-8.0);
painter->drawLine(myLine);
}
}
What I have done so far to solve the problem:
1) I came across this post which was useful to understand the initial idea on how to do that, but it didn't really provide a way, or an implementation idea on how to best proceed
2) I researched the official documentation and before asking this question I went through the whole Diagram Scene example provided and understood how to create an Arrow object. The documentation about that was very good and made me understand how the graphics line item has to be formed.
However I was not able (coming back to my example) how to make "aware" the QRadioButton that I am trying to use its center as starting point for an arrow ad, therefore, how do I make "aware" the destination QRadioButton in another cell that it has to be connected there?
Below a particular of what I mean:
So basically the start point of the QRadioButton change color (or style) and the arrival point also change color.
3) I thought that the Arrow object has to be created inside the subclassed QGraphicsScene since it already handles the mouse events.
4) Despite what I tried so far I could not find any other useful help. Although I am still investigating how to do that.
If anyone has ever been in the same situation please provide guidance on how to better proceed to solve this problem and find a solution to this issue.
Solution
When a start and end radio buttons are checked, you need to create the arrow with those buttons as start and end nodes, e.g.:
void Backend::onInputRadioButton(bool checked)
{
m_endNode = checked ? static_cast<QRadioButton *>(sender()) : nullptr;
if (m_startNode && m_endNode)
m_scene->addItem(new ArrowItem(m_startNode, m_endNode));
}
Then you need to connect the signal of the top-most graphics items, which hold the tables, with the updatePosition slot of the ArrowItem, e.g.:
connect(m_startItem->property("item").value<MovableItem *>(),
&MovableItem::itemMoved, this, &ArrowItem::updatePosition);
connect(m_endItem->property("item").value<MovableItem *>(),
&MovableItem::itemMoved, this, &ArrowItem::updatePosition);
Note: I am using a property to hold a reference to the container item.
Finally, you need to update the arrow line, e.g.:
void ArrowItem::updatePosition()
{
QPointF offset(7, 15);
QPointF p1 = m_startItem->property("item").value<MovableItem *>()->pos()
+ m_startItem->parentWidget()->mapToParent(m_startItem->pos())
+ offset;
QPointF p2 = m_endItem->property("item").value<MovableItem *>()->pos()
+ m_endItem->parentWidget()->mapToParent(m_endItem->pos())
+ offset;
setLine(QLineF(p1, p2));
}
Example
I have dared to suggest improvements in your code. You can find the complete example I wrote for you on GitHub.
Result
The provided example produces the following result:
Note: The arrow heads are missing. Check once again the Diagram Scene Example to get an idea of how to draw them.

How to Zoom / Fit Image to GraphicsView

I read a lot of posts/threads but I can't get it to work.
I'd like to fit every Image to a GraphicsView regardless if it is smaller or bigger then the view.
What's wrong?
void frmMain::on_btLoadImage_clicked()
{
QGraphicsScene *scene;
QPixmap image;
QString imgPath = "O:/IMG_0001.JPG";
QRectF sceneRect = ui->imgMain->sceneRect();
image.load(imgPath);
image.scaled (sceneRect.width (),sceneRect.height (), Qt::KeepAspectRatio, Qt::SmoothTransformation);
scene = new QGraphicsScene(this);
scene->addPixmap(image);
scene->setSceneRect(sceneRect); //image.rect());
//ui->imgMain->fitInView (scene->itemsBoundingRect(), Qt::KeepAspectRatio); //ui->imgMain->width (), ui->imgMain->height ());
ui->imgMain->setScene(scene);
}
Here is a basic custom QGraphicsView implementation which displays one image and keeps it sized/scaled to fit the available viewport space. Note that the image needs to be rescaled every time the viewport size changes, which is why it is simplest to reimplement the QGraphicsView itself and change the scaling in resizeEvent(). Although it could be done inside a custom QGraphicsScene instead. (Or, really, a number of other ways depending on the exact needs.)
The same technique could be used to keep a QGraphicsWidget as the root item in the scene to always take up the full space. Then a layout could be used in the widget to keep children aligned/resized/positioned/etc.
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsPixmapItem>
class GrpahicsImageView : public QGraphicsView
{
Q_OBJECT
public:
using QGraphicsView::QGraphicsView;
public slots:
void setImage(const QString &imageFile)
{
if (m_imageFile != imageFile) {
m_imageFile = imageFile;
loadImage(viewport()->contentsRect().size());
}
}
void setImageScaleMode(int mode)
{
if (m_scaleMode != Qt::AspectRatioMode(mode)) {
m_scaleMode = Qt::AspectRatioMode(mode);
if (m_item)
loadImage(viewport()->contentsRect().size());
}
}
void loadImage(const QSize &size)
{
if (!scene())
return;
if (m_imageFile.isEmpty()) {
// remove existing image, if any
removeItem();
return;
}
// Load image at original size
QPixmap pm(m_imageFile);
if (pm.isNull()) {
// file not found/other error
removeItem();
return;
}
// Resize the image here.
pm = pm.scaled(size, m_scaleMode, Qt::SmoothTransformation);
if (createItem())
m_item->setPixmap(pm);
}
protected:
void resizeEvent(QResizeEvent *e) override
{
QGraphicsView::resizeEvent(e);
if (!scene())
return;
// Set scene size to fill the available viewport size;
const QRect sceneRect(viewport()->contentsRect());
scene()->setSceneRect(sceneRect);
// Keep the root item sized to fill the viewport and scene;
if (m_item)
loadImage(sceneRect.size());
}
private:
bool createItem() {
if (m_item)
return true;
if (!m_item && scene()) {
m_item = new QGraphicsPixmapItem();
scene()->addItem(m_item);
return true;
}
return false;
}
void removeItem()
{
if (m_item) {
if (scene())
scene()->removeItem(m_item);
delete m_item;
m_item = nullptr;
}
}
Qt::AspectRatioMode m_scaleMode = Qt::KeepAspectRatio;
QString m_imageFile;
QGraphicsPixmapItem *m_item = nullptr;
};
Usage example:
#include <QApplication>
#include <QtWidgets>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QDialog d;
d.setLayout(new QVBoxLayout);
d.resize(350, 350);
GrpahicsImageView *view = new GrpahicsImageView(new QGraphicsScene, &d);
QComboBox *imgCb = new QComboBox(&d);
imgCb->addItems({
"./so-logo.png",
"./se-logo.png",
"./su-logo.png"
});
QComboBox *scaleCb = new QComboBox(&d);
scaleCb->addItems({
"IgnoreAspectRatio",
"KeepAspectRatio",
"KeepAspectRatioByExpanding"
});
QHBoxLayout *cbLayout = new QHBoxLayout;
cbLayout->setSpacing(9);
cbLayout->addWidget(imgCb);
cbLayout->addWidget(scaleCb);
d.layout()->addItem(cbLayout);
d.layout()->addWidget(view);
QObject::connect(imgCb, QOverload<const QString &>::of(&QComboBox::currentIndexChanged), view, &GrpahicsImageView::setImage);
QObject::connect(scaleCb, QOverload<int>::of(&QComboBox::currentIndexChanged), view, &GrpahicsImageView::setImageScaleMode);
view->setImageScaleMode(scaleCb->currentIndex());
view->setImage(imgCb->currentText());
return d.exec();
}
https://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/so-logo.png
https://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/se-logo.png
https://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/su-logo.png

QGraphicsPixmapItem, alternative methods of connecting to slots

I'm aware I need to derive from QObject in order to connect to a slot if I am using QGraphicsPixmapItem, but I am struggling to do this. I have tried alternative ways to achieve what I want, I have tried onMousePress and isSelectable i.e.
run->setFlag(QGraphicsPixmapItem::ItemIsSelectable);
if (run->isSelected())
{
qDebug() << "selected";
}
else if (!run->isSelected())
{
qDebug() << "not selected";
}
although run is selectable, the first argument is never true, it is always "not selected"
This is my code, I am working on the slot method;
mainwindow.cpp
int MainWindow::sim()
{
...
QGraphicsPixmapItem* run = new QGraphicsPixmapItem(QPixmap::fromImage(image6));
run->scale(0.3,0.3);
run->setPos(-200,-200);
run->setFlag(QGraphicsPixmapItem::ItemIsSelectable);
run->setCursor(Qt::PointingHandCursor);
connect(run, SIGNAL(selectionChanged()), this, SLOT(runClicked()));
scene->addItem(run);
//pause
QGraphicsPixmapItem* pause = new QGraphicsPixmapItem(QPixmap::fromImage(image7));
pause->scale(0.3,0.3);
pause->setPos(-160,-197);
pause->setFlag(QGraphicsPixmapItem::ItemIsSelectable);
pause->setCursor(Qt::PointingHandCursor);
connect(pause, SIGNAL(selectionChanged()), this, SLOT(pauseClicked()));
scene->addItem(pause);
...
}
void MainWindow::runClicked()
{
qDebug() << "run Clicked";
}
void MainWindow::pauseClicked()
{
qDebug() << "pause Clicked";
}
mainwindow.h
#define MAINWINDOW_H
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
int sim();
...
public slots:
void runClicked();
void pauseClicked();
...
So obviously I get the error when connecting to the slots. Could anyone help please? Thank you.
To find out if your item is selected, do this:
QVariant MyItem::itemChange( GraphicsItemChange change, const QVariant& value )
{
if ( change == QGraphicsItem::ItemSelectedHasChanged ) {
qDebug() << ( isSelected() ? "selected" : "not selected" );
}
return QGraphicsItem::itemChange( change, value );
}
If you want to use signals and slots, you need to subclass both QObject and QGraphicsPixmapItem.
Because QObject doesn't contain clicked() signal, you need to implement that, too, by re-implementing
void mousePressEvent ( QGraphicsSceneMouseEvent *e ) and void mouseReleaseEvent ( QGraphicsSceneMouseEvent *e ).
MyItem:
#pragma once
#include <QGraphicsPixmapItem>
#include <qobject.h>
#include <QMouseEvent>
#include <QGraphicsSceneMouseEvent>
class MyItem: public QObject, public QGraphicsPixmapItem
/* moc.exe requires to derive from QObject first! */
{
Q_OBJECT
public:
MyItem(QGraphicsItem *parent = 0): QObject(), QGraphicsPixmapItem(parent)
{
}
MyItem(const QPixmap & pixmap, QGraphicsItem * parent = 0 ): QObject(),
QGraphicsPixmapItem(pixmap, parent)
{
}
signals:
void clicked();
protected:
// re-implement processing of mouse events
void mouseReleaseEvent ( QGraphicsSceneMouseEvent *e )
{
// check if cursor not moved since click beginning
if ((m_mouseClick) && (e->pos() == m_lastPoint))
{
// do something: for example emit Click signal
emit clicked();
}
}
void mousePressEvent ( QGraphicsSceneMouseEvent *e )
{
// store click position
m_lastPoint = e->pos();
// set the flag meaning "click begin"
m_mouseClick = true;
}
private:
bool m_mouseClick;
QPointF m_lastPoint;
};
And simple example of usage:
#include <qgraphicsview.h>
#include <qgraphicsscene.h>
#include "reader.h"
#include <qdebug.h>
class MainAppClass: public QObject
{
Q_OBJECT
public:
MainAppClass()
{
QGraphicsScene *scene = new QGraphicsScene();;
scene->setSceneRect( -100.0, -100.0, 200.0, 200.0 );
MyItem *item = new MyItem(QPixmap("about.png"));
connect(item, SIGNAL(clicked()), this, SLOT(pixmapClicked()));
scene->addItem(item);
QGraphicsView * view = new QGraphicsView( scene );
view->setRenderHints( QPainter::Antialiasing );
view->show();
}
public slots:
void pixmapClicked()
{
qDebug() << "item clicked!" ;
}
};

HowTo draw pixmap in QHeaderView section using delegate?

Problem: I need to draw small pixmap to QHeaderView section like presented on this figure (pixmap located on the right corner of the section, marked with red square):
As I understand, there is two ways to do that:
reimplement QHeaderView's paintSection() method.
create an delegate from QStyledItemDelegate class and reimplement paint() method.
If I tried (1) variant with this code below, filter pixmap are not shown at all:
void DecorativeHeaderView::paintSection( QPainter* painter, const QRect& rect, int logicalIndex ) const
{
if( !rect.isValid() )
{
return;
}
// get the state of the section
QStyleOptionHeader option;
initStyleOption( &option );
// setup the style options structure
option.rect = rect;
option.section = logicalIndex;
option.iconAlignment = Qt::AlignVCenter | Qt::AlignHCenter;
QVariant variant = model()->headerData( logicalIndex, orientation(), Qt::DecorationRole );
option.icon = qvariant_cast< QIcon >( variant );
if( option.icon.isNull() )
{
option.icon = qvariant_cast< QPixmap >( variant );
}
// draw the section
if( !option.icon.isNull() )
{
style()->drawControl( QStyle::CE_Header, &option, painter, this );
}
else
{
QHeaderView::paintSection( painter, rect, logicalIndex );
// HERE is where I'm trying to draw my filter picture!!!
if( logicalIndex == filteredLogicalIndex_ )
{
QPixmap pixmap( ":/spreadsheet/images/spreadsheet/filter_icon_table.png" );
int x = rect.right() - pixmap.width();
int y = rect.top() + ( rect.height() - pixmap.height() ) / 2;
painter->drawPixmap( QPoint( x, y ), pixmap );
}
}
}
The (2) variant is this:
class HeaderDelegate : public QStyledItemDelegate
{
Q_OBJECT
Q_DISABLE_COPY( HeaderDelegate )
public:
HeaderDelegate( QObject* parent = 0 ) : QStyledItemDelegate( parent ) {}
virtual ~HeaderDelegate() {}
virtual void paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const;
}; // HeaderDelegate
void HeaderDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
// THIS method never starts!!!
if( index.column() == 2 )
{
QPixmap pixmap( ":/spreadsheet/images/spreadsheet/filter_icon_table.png" );
int x = option.rect.right() - pixmap.width();
int y = option.rect.top() + ( option.rect.height() - pixmap.height() ) / 2;
painter->save();
painter->drawPixmap( QPoint( x, y ), pixmap );
painter->restore();
}
QStyledItemDelegate::paint( painter, option, index );
}
DecorativeHeaderView::DecorativeHeaderView( Qt::Orientation orientation, QWidget* parent /* = 0 */ )
: QHeaderView( orientation, parent )
, filteredLogicalIndex_( -1 )
{
setItemDelegate( new HeaderDelegate( this ) );
}
Delegate created, but function did not start the paint() method!
Any help?
Thanks!
It Seems like it isn't possible at this time.
From Qt documentation:
Note: Each header renders the data for each section itself, and does not rely on a delegate. As a result, calling a header's setItemDelegate() function will have no effect.
How to have dynamic pixmap in a QHeaderView item
But you may override QHeaderView::paintSection method.
subclassing-QHeaderView
class HeaderView : public QHeaderView {
Q_OBJECT
public:
HeaderView(Qt::Orientation orientation, QWidget * parent = 0)
: QHeaderView(orientation, parent), p("333222.jpeg") {
}
int sectionSizeHint ( int /*logicalIndex*/ ) const { return p.width(); }
protected:
void paintSection(QPainter * painter, const QRect & rect, int /*logicalIndex*/) const {
painter->drawPixmap(rect, p);
}
private:
QPixmap p;
};

How to select child's items checkBoxs in QTreeView when select their parent's checkbox

I want to select/unselect all child's items QCheckBoxs when I select/unselect their parent's item QCheckBox.
i inherit from QTreeView and detect when the QCheckBox is selected then i call function to do selecting/unselecting process.
here my code:
#ifndef MYQTREEVIEW_H
#define MYQTREEVIEW_H
#include <QTreeView>
#include <QMouseEvent>
#include <QDebug>
#include <QStandardItem>
class MyQTreeView: public QTreeView {
public:
MyQTreeView(QWidget* parent=0): QTreeView(parent){}
virtual ~MyQTreeView() {}
protected:
void resettingCheckBox (QModelIndex& parentIndex) {
if ( ! parentIndex.isValid() )
return;
QString text = parentIndex.data( Qt::DisplayRole ).value<QString>();
qDebug() << "parent is: " << text;
if ( model()->hasChildren(parentIndex) ) {
for( int i = 0; i < model()->rowCount(parentIndex) ; i++ ) {
QModelIndex childIndex = model()->index( i, 0, parentIndex );
if ( model()->hasChildren(childIndex) )
resettingCheckBox(childIndex);
else {
QString text = childIndex.data( Qt::DisplayRole ).value<QString>();
qDebug() << "child is: " << text;
QStandardItem *parentItem = static_cast<QStandardItem*> (parentIndex.internalPointer());
QStandardItem *childItem = static_cast<QStandardItem*> (childIndex.internalPointer());
if ( parentItem->checkState() == Qt::Checked ) {
qDebug() << "child item " << childItem->checkState();
childItem->setCheckState( Qt::Unchecked);
}
else {
qDebug() << "child item " << childItem->checkState();
childItem->setCheckState( Qt::Checked);
}
}
}
}
}
void mousePressEvent (QMouseEvent *event) {
QModelIndex index = indexAt(event->pos());
if(index.isValid()) {
QStyleOptionButton opt;
opt.QStyleOption::operator=(viewOptions());
opt.rect = visualRect(index);
QRect rect = style()->subElementRect(QStyle::SE_ViewItemCheckIndicator, &opt);
if (rect.contains(event->pos())) {
resettingCheckBox(index);
}
QTreeView::mousePressEvent(event);
}
}
};
#endif // MYQTREEVIEW_H
the code is not working probably when i select/unselect parent checkBox (subchilds is not selected/unselected).
Thanks in advance.
I believe the best way to manipulate treeview items is through the model. It looks like you're using QStandardItemModel; so you can override your model's setData method and reset child items values for the item index passed as a parameter to this method. Below is a small example:
class TestModel : public QStandardItemModel
{
public:
TestModel() {}
bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole)
{
QStandardItem *item = itemFromIndex(index);
for( int i = 0; i < item->rowCount() ; i++ )
{
QStandardItem *childItem = item->child(i);
setData(childItem->index(), value, role);
}
return QStandardItemModel::setData(index, value, role);
}
};
here's how this model gets initialized:
QStandardItemModel* tableModel = new TestModel();
QStandardItem* parentItem = tableModel->invisibleRootItem();
for (int i = 0; i < 4; ++i)
{
QStandardItem *item = new QStandardItem(QString("item %0").arg(i));
item->setCheckable(true);
parentItem->appendRow(item);
parentItem = item;
}
treeView->setModel(tableModel);
hope this helps, regards
It seems to me that you should call QTreeView::mousePressEvent(event) before resettingCheckBox(index), to let QTreeView update the checkState.
override mouseReleaseEvent() instead of mousePressEvent()!
because the checkState changes when mouse Release not mouse - press!

Resources