PDA

View Full Version : [SOLVED] Issues with displaying rich text for a QCheckBox



forgottenduck
3rd October 2014, 23:08
So I've been wrestling with this problem for most of the work day, and at this point I think I need some outside feedback. I've been working with Qt for a couple months now and I'm pretty well versed in C++ so I've got the basics of Qt down and have learned a few of its subtleties, but I can't find an elegant solution to my issue.

I need to have superscript text displayed in the label of a QCheckBox. Now, your initial thought may be to just place and empty check box next to a label which can display rich text. I have seen this suggested many times in various posts about this issue, but I have two problems with this:


Clicking the label does not toggle the check box.
Hovering over the label does not trigger the hoverstate of the check box


The first problem is easy enough to solve. You just need to subclass QLabel and implement mouseClickEvent and emit a clicked signal to connect to the check box's toggle slot. The second problem is somewhat tricky however, and I'm pretty stumped.

My initial instinct was to look for a setState or some such method which would allow me to tell the QCheckBox that it is being hovered over, however no such method exists that I could find. I even tried to use QWidget.setProperty("hover", true but it had no effect. So then I thought that I could place a QCheckBox and a QLabel inside of a custom Widget container which would detect mouseover and if the mouse is over the label I would create a false event containing coordinates of the QCheckBox and pass that to the QCheckBox so it would think it is being hovered over. However you can't explicitly call mouseMoveEvent on a widget. I also tried subclassing QCheckBox and using QTextDocument in the paintEvent to draw rich text next to the checkBox after calling the base class paint event. This worked, but again the checkbox was not counting the drawn text as part of it's contents so hover state was not being triggered.

I feel like I'm close to finding a workable solution here but I just need a push in the right direction. I don't need anyone to write any code for me, but if there's a method I'm missing or something else I could try while subclassing QCheckBox I would be very grateful to have that pointed out.

If all else fails I will just have to stick with a clickable label next to a checkbox, but I would like to avoid that because there are many other checkboxes without the need for rich text in my application and it would be great for them all to have the same behavior.

anda_skoa
4th October 2014, 11:32
So then I thought that I could place a QCheckBox and a QLabel inside of a custom Widget container which would detect mouseover and if the mouse is over the label I would create a false event containing coordinates of the QCheckBox and pass that to the QCheckBox so it would think it is being hovered over. However you can't explicitly call mouseMoveEvent on a widget.

No, but you could call the event() method (which is the entry point for all events a widget processes) or even us QCoreApplication::sendEvent() to push an event through the normal event processing chain.

Cheers,
_

forgottenduck
6th October 2014, 16:46
or even us QCoreApplication::sendEvent() to push an event through the normal event processing chain.

Using send event did allow me to do what I intended but nothing appears to be happening. Here's my class as of right now:



CheckBoxTest::CheckBoxTest(QString text, QWidget *parent) :
QWidget(parent)
{
this->setMouseTracking(true);
label = new QLabel(text);
checkBox = new QCheckBox;
QHBoxLayout* layout = new QHBoxLayout;
layout->addWidget(label);
layout->addWidget(checkBox);
this->setLayout(layout);
}

void CheckBoxTest::mouseMoveEvent(QMouseEvent *event){
if(label->contentsRect().contains(event->pos())){
QMouseEvent* newEvent = new QMouseEvent(QEvent::MouseMove, checkBox->mapFromGlobal(checkBox->pos()), Qt::NoButton, Qt::NoButton, Qt::NoModifier);
QCoreApplication::sendEvent(checkBox, newEvent);
}else{
event->ignore();
}
}



Mouse events are passed on to checkBox when not within the label. When within the label the newEvent is created with a mouse position of checkBox's position and I use sendEvent to send it to checkBox which should trigger a hover state whenever the mouse is in the contents of the label. Any idea why nothing seems to be happening?

anda_skoa
9th October 2014, 17:23
Maybe the trigger is not a mouse event but the enter event.

After all, "hover" only changes on enter/leave

Cheers,
_

forgottenduck
14th October 2014, 23:01
I just figured out an acceptable solution so I will explain what I have done. I made 3 classes total, one being simply a container for the custom qcheckbox and custom qlabel.

I made an extension of QCheckBox called HoverCheckBox which reimplements the paint event and if the private bool isHover is true it will force the QStyle::State_MouseOver onto the style option. Here's the code:

hovercheckbox.h


#ifndef HOVERCHECKBOX_H
#define HOVERCHECKBOX_H

#include <QCheckBox>

class HoverCheckBox : public QCheckBox
{
Q_OBJECT
public:
explicit HoverCheckBox(QWidget *parent = 0);
private:
virtual void paintEvent(QPaintEvent *);
bool isHover;
public slots:
void setHover(bool state);
};

#endif // HOVERCHECKBOX_H

hovercheckbox.cpp


#include "hovercheckbox.h"

#include <QStyleOptionButton>
#include <QStylePainter>

HoverCheckBox::HoverCheckBox(QWidget *parent) :
QCheckBox(parent)
{
isHover = false;
}
void HoverCheckBox::paintEvent(QPaintEvent *){
QStylePainter p(this);
QStyleOptionButton opt;
initStyleOption(&opt);
if(isHover){
opt.state |= QStyle::State_MouseOver;
}
p.drawControl(QStyle::CE_CheckBox, opt);
}
void HoverCheckBox::setHover(bool state){
isHover = state;
repaint();
}



I also made a simple Clickable label class which emits a clicked() signal on left mouse button release.
clickablelabel.h


#ifndef CLICKABLELABEL_H
#define CLICKABLELABEL_H

#include <QLabel>

class ClickableLabel : public QLabel
{
Q_OBJECT
public:
explicit ClickableLabel(QWidget *parent = 0);
void mouseReleaseEvent(QMouseEvent*);

signals:
void clicked();
public slots:

};

#endif // CLICKABLELABEL_H

clickablelabel.cpp


#include "clickablelabel.h"

#include <QMouseEvent>
ClickableLabel::ClickableLabel(QWidget *parent) :
QLabel(parent)
{
}

void ClickableLabel::mouseReleaseEvent(QMouseEvent *event){
if(event->buttons() == Qt::LeftButton){
emit clicked();
}else if(event->button() == Qt::LeftButton){
emit clicked();
}
}



Lastly I made a container class to wrap the two.
richtextbox.h


#ifndef RICHTEXTCHECKBOX_H
#define RICHTEXTCHECKBOX_H

#include <QWidget>

class HoverCheckBox;
class QCheckBox;
class ClickableLabel;

class RichTextCheckBox : public QWidget
{
Q_OBJECT
public:
explicit RichTextCheckBox(QString text, QWidget *parent = 0);

private:
HoverCheckBox* checkBox;
ClickableLabel* label;
virtual void enterEvent(QEvent*);
virtual void leaveEvent(QEvent*);

signals:

public slots:

};

#endif // RICHTEXTCHECKBOX_H

richtextbox.cpp


#include "richtextcheckbox.h"
#include "hovercheckbox.h"
#include "clickablelabel.h"
#include <QMouseEvent>
#include <QLabel>
#include <QLayout>

#include <QDebug>

RichTextCheckBox::RichTextCheckBox(QString text, QWidget *parent) :
QWidget(parent)
{
setMouseTracking(true);
checkBox = new HoverCheckBox;
label = new ClickableLabel;
label->setTextFormat(Qt::RichText);
label->setText(text);
QHBoxLayout* layout = new QHBoxLayout;
layout->setMargin(0);
layout->addWidget(checkBox);
layout->addWidget(label);
setLayout(layout);
connect(label, SIGNAL(clicked()), checkBox, SLOT(toggle()));
}

void RichTextCheckBox::enterEvent(QEvent* /*event*/){
checkBox->setHover(true);
}

void RichTextCheckBox::leaveEvent(QEvent* /*event*/){
checkBox->setHover(false);
}

anda_skoa
15th October 2014, 00:33
Thanks for reporting your solution!

One small suggestion: use update() in setHover() instead of repaint().
This is what setters on widgets usually do.

Cheers,
_

forgottenduck
15th October 2014, 15:03
Thanks for the suggestion, I will modify my solution in my project.

One other addition that this class could use is to apply "QStyle::State_Sunken;" while the mouse is being pressed on the qlabel, then I think it would completely emulate a rich-text check box. For now, what I have is acceptable, but if I end up adding the sunken state I will try to remember to update the solution I've posted.

forgottenduck
15th October 2014, 20:36
I decided to go ahead and add that additional functionality, as well as add a bunch of methods with the same name as the public QCheckBox methods for getting and setting. I also set up some signal forwarding, so you should be able to drop the RichTextCheckBox class in anywhere that QCheckBox is used, and still have everything work even though RichTextCheckBox does not inherit QCheckBox.
Here's my updated code. Hopefully the next time someone needs to display rich text in the label of a checkbox they will find this thread instead of all the other threads which, IMO, offer inadequate solutions.

clickablelabel.h


#ifndef CLICKABLELABEL_H
#define CLICKABLELABEL_H

#include <QLabel>

class ClickableLabel : public QLabel
{
Q_OBJECT
public:
explicit ClickableLabel(QWidget *parent = 0);
void mouseReleaseEvent(QMouseEvent*);
void mousePressEvent(QMouseEvent*);
void mouseMoveEvent(QMouseEvent*);

signals:
void released();
void pressed();
void left();
public slots:

};

#endif // CLICKABLELABEL_H


clickablelabel.cpp


#include "clickablelabel.h"

#include <QMouseEvent>

ClickableLabel::ClickableLabel(QWidget *parent) :
QLabel(parent)
{
}

void ClickableLabel::mouseReleaseEvent(QMouseEvent* event){
if(contentsRect().contains(event->pos())){
if(event->buttons() == Qt::LeftButton){
emit released();
}else if(event->button() == Qt::LeftButton){
emit released();
}
}
}

void ClickableLabel::mousePressEvent(QMouseEvent* event){
if(event->buttons() == Qt::LeftButton){
emit pressed();
}else if(event->button() == Qt::LeftButton){
emit pressed();
}
}

void ClickableLabel::mouseMoveEvent(QMouseEvent* event){
if(contentsRect().contains(event->pos())&&
(event->buttons() == Qt::LeftButton
|| event->button() == Qt::LeftButton)){
emit left();
}
}



hovercheckbox.h


#ifndef HOVERCHECKBOX_H
#define HOVERCHECKBOX_H

#include <QCheckBox>

class HoverCheckBox : public QCheckBox
{
Q_OBJECT
public:
explicit HoverCheckBox(QWidget *parent = 0);
private:
virtual void paintEvent(QPaintEvent *);
bool isHover;
bool isPressed;
public slots:
void setHover();
void setPressed();
void clearPressed();
void clearStates();
};

#endif // HOVERCHECKBOX_H


hovercheckbox.cpp


#include "hovercheckbox.h"

#include <QStyleOptionButton>
#include <QStylePainter>

HoverCheckBox::HoverCheckBox(QWidget *parent) :
QCheckBox(parent)
{
isHover = false;
}
void HoverCheckBox::paintEvent(QPaintEvent *){
QStylePainter p(this);
QStyleOptionButton opt;
initStyleOption(&opt);
if(isHover){
opt.state |= QStyle::State_MouseOver;
}
if(isPressed){
opt.state |= QStyle::State_Sunken;
}
p.drawControl(QStyle::CE_CheckBox, opt);
}
void HoverCheckBox::setHover(){
isHover = true;
update();
}
void HoverCheckBox::setPressed(){
isPressed = true;
update();
}
void HoverCheckBox::clearPressed(){
isPressed = false;
update();
}

void HoverCheckBox::clearStates(){
isHover = false;
isPressed = false;
update();
}



richtextcheckbox.h


#ifndef RICHTEXTCHECKBOX_H
#define RICHTEXTCHECKBOX_H

#include <QWidget>

class HoverCheckBox;
class QCheckBox;
class ClickableLabel;

class RichTextCheckBox : public QWidget
{
Q_OBJECT
public:
explicit RichTextCheckBox(QString text, QWidget *parent = 0);
HoverCheckBox* getCheckBox(){return checkBox;}
QString text() const;
QString plainText() const;
Qt::CheckState checkState() const;
bool isChecked() const;
void clearStates();

private:
HoverCheckBox* checkBox;
ClickableLabel* label;
virtual void enterEvent(QEvent*);
virtual void leaveEvent(QEvent*);

signals:
void toggled(bool);
void clicked();

public slots:
void setChecked(bool);
void setCheckState(Qt::CheckState state);
void emitToggled(bool);
void emitClicked();

};

#endif // RICHTEXTCHECKBOX_H


richtextcheckbox.cpp


#include "richtextcheckbox.h"
#include "hovercheckbox.h"
#include "clickablelabel.h"
#include <QMouseEvent>
#include <QLabel>
#include <QLayout>
#include <QTextDocument>

RichTextCheckBox::RichTextCheckBox(QString text, QWidget *parent) :
QWidget(parent)
{
setMouseTracking(true);
checkBox = new HoverCheckBox;
label = new ClickableLabel;
label->setTextFormat(Qt::RichText);
label->setText(text);
QHBoxLayout* layout = new QHBoxLayout;
layout->setMargin(0);
layout->addWidget(checkBox);
layout->addWidget(label);
layout->setAlignment(Qt::AlignLeft);
setLayout(layout);
connect(label, SIGNAL(released()), checkBox, SLOT(toggle()));
connect(label, SIGNAL(released()), checkBox, SLOT(clearPressed()));
connect(label, SIGNAL(pressed()), checkBox, SLOT(setPressed()));
connect(label, SIGNAL(left()), checkBox, SLOT(clearStates()));
connect(checkBox, SIGNAL(toggled(bool)),this, SLOT(emitToggled(bool)));
connect(checkBox, SIGNAL(clicked()), this, SLOT(emitClicked()));
connect(label, SIGNAL(released()), this, SLOT(emitClicked()));
}

void RichTextCheckBox::emitClicked(){
emit clicked();
}

void RichTextCheckBox::emitToggled(bool b){
emit toggled(b);
}

bool RichTextCheckBox::isChecked() const{
return checkBox->isChecked();
}

Qt::CheckState RichTextCheckBox::checkState() const{
return checkBox->checkState();
}

QString RichTextCheckBox::text() const{
return label->text();
}

QString RichTextCheckBox::plainText() const{
QTextDocument doc;
doc.setHtml(label->text());
return doc.toPlainText();
}

void RichTextCheckBox::clearStates(){
checkBox->clearStates();
}

void RichTextCheckBox::setCheckState(Qt::CheckState state){
checkBox->setCheckState(state);
}

void RichTextCheckBox::setChecked(bool value){
checkBox->setChecked(value);
}

void RichTextCheckBox::enterEvent(QEvent* /*event*/){
checkBox->setHover();
}

void RichTextCheckBox::leaveEvent(QEvent* /*event*/){
checkBox->clearStates();
}

anda_skoa
16th October 2014, 09:59
Another suggestion :)

instead of


connect(label, SIGNAL(released()), this, SLOT(emitClicked()));
}

void RichTextCheckBox::emitClicked(){
emit clicked();
}

i.e. instead of calling a local slot only to emit a signal, you can also "forward" the signal using a signal/signal connection


connect(label, SIGNAL(released()), this, SIGNAL(clicked()));


Cheers,
_

forgottenduck
16th October 2014, 15:05
Haha, I have no idea how I didn't know you could forward a signal that way. There's a few places in my code I need to clean up now.
Thanks ;)

d_stranz
16th October 2014, 18:53
Thanks for sharing this nice work. This will probably make it possible to label checkboxes with symbols and math formulas in addition to simple sub- and superscripts. If you are willing to post the final code (maybe as a zip file attachment rather than in CODE blocks :)), that would be greatly appreciated and help others avoid copy-and-paste issues.

forgottenduck
16th October 2014, 22:27
I'd be happy to share my finished code. Here's a zip file with all 3 classes. Feel free to PM me or reply here if you have any questions or suggestions.
Everything seems to be working just as intended, I've tested it in a horizontal layout with other standard checkboxes and it appears to be spaced in the layout exactly the same. I have not tested it in a vertical layout so an adjustment may need to be made to richtextcheckbox.cpp for a vertical layout, as I had to add extra spacing on the right to achieve the correct look:


layout->setContentsMargins(0, 0, 5, 0);


If an admin or mod could mark this thread as solved it would be appreciated.