最近在做需求的时候,总是会发现一些比较尴尬的事情,每个控件的基本功能都有,但就是无论怎么修修补补都不能满足自己想要的效果。有时候强行实现了,却看到了非常多的补丁。就好像新买的裤子,还没穿就已经被打了一个一个的补丁。难看,并且很膈应。特别是在我们都在追求极简以及完美代码的时候。
所以针对这样一些比较尴尬的追求,总结了一些经验。
**需求1、**使用QButtonGroup
按钮组实现了多级菜单的切换,在菜单切换的时候,需要检查切换前的菜单有没有被更改,如果有,则进行提示。确认之后才能进行切换。
这是什么鬼东西啊。
这个需求在拿到的时候想了很久,也找了很久的资源,无奈,都没有找到。也试了很多方法,虽然很多方法都能实现功能,但存在的问题就是,按钮点击之后,其实对buttongroup来说,被选中的按钮已经被切换了,就算你弹窗提醒了,功能实现了,但样式表的显示会让你有一中喉咙卡了鱼刺的感觉。
面对这样的需求,我们最可能想到的方法就是事件过滤。但如何选择过滤事件,如何确定事件过滤器的位置都是一个比较麻烦的事情。
所以,我去研究了下QPushbutton
的鼠标点击事件。发现,鼠标点击事件click
函数都是在 mouseReleaseEvent
事件里面调用的。这就好像突然间打开了一扇大门。下面是摘录的一些源码:
/*! \reimp */
void QAbstractButton::mousePressEvent(QMouseEvent *e)
{
Q_D(QAbstractButton);
if (e->button() != Qt::LeftButton) {
e->ignore();
return;
}
if (hitButton(e->pos())) {
setDown(true);
d->pressed = true;
repaint();
d->emitPressed();
e->accept();
} else {
e->ignore();
}
}
/*! \reimp */
void QAbstractButton::mouseReleaseEvent(QMouseEvent *e)
{
Q_D(QAbstractButton);
if (e->button() != Qt::LeftButton) {
e->ignore();
return;
}
d->pressed = false;
if (!d->down) {
// refresh is required by QMacStyle to resume the default button animation
d->refresh();
e->ignore();
return;
}
if (hitButton(e->pos())) {
d->repeatTimer.stop();
d->click();
e->accept();
} else {
setDown(false);
e->ignore();
}
}
既然如此,那么我们是不是可以通过继承该类来实现自己的 Button
,然后通过重写 mouseReleaseEvent
函数来达到控制 click 函数的调用时机。
说干就干。
class MenuButton : public QPushButton
{
Q_OBJECT
Q_PROPERTY(int id READ id WRITE setId DESIGNABLE true)
Q_PROPERTY(QVariant data READ data WRITE setData DESIGNABLE true)
public:
using QPushButton::QPushButton;
//C++11 之后引入的关键字,using 可直接使用父类的构造。可查看另一篇文章《C++继承中子类的构造方法》
explicit MenuButton(int id, QWidget *parent = 0);
int id() const
{
return m_nId;
}
void setId(const int& id)
{
m_nId = id;
}
QVariant data() const
{
return m_data;
}
void setData(const QVariant& data)
{
m_data = data;
}
protected:
void mousePressEvent(QMouseEvent* e);
void mouseReleaseEvent(QMouseEvent* e);
signals:
void signal_btnClicked(const QString&, int, bool& change);
private:
void btnClick();
int m_nId{ -1 }; // button id
QVariant m_data{ QVariant() }; // user data
};
首先对定义的类中一些东西做一下说明,主要是针对 Q_PROPERTY
,可能刚接触Qt的会不知道这个是什么。这个其实就是我们模仿了下Qt 对属性的操作。就好像,QLabel
中的 text()
和 setText(const QString& text)
函数一样。
#include "menu_button.h"
MenuButton::MenuButton(int id, QWidget* parent) : QPushButton(parent), m_nId(id)
{
}
void MenuButton::mousePressEvent(QMouseEvent *e)
{
}
void MenuButton::mouseReleaseEvent(QMouseEvent *e)
{
btnClick();
}
void MenuButton::btnClick()
{
click();
}
到这儿的时候,重审需求,菜单切换的时候,需要检查切换前的菜单有没有被修改。那么我们一定需要一个变量来控制 click
的调用。
void MenuButton::btnClick()
{
bool isChanged = true;
if(isChanged)
{
click();
}
}
这个变量怎么来,该怎么获取值,其实是一件比较麻烦的事情。但是如果你熟悉信号槽,并且对Qt 的信号有一些研究的话,你会知道,Qt 的信号还有一个功能,就是可以通过参数的形式返回值。也就是说如下:
void signal_btnClicked(const QString&, int, bool& change);
我们通过定义这个信号,来获取变量change的值。具体的这个变量在什么地方赋值,就需要看你的这个菜单的功能在什么地方实现。也就有了下面的书写方法。
void MenuButton::btnClick()
{
bool isChanged = true;
emit signal_btnClicked(id(), isChanged);
if(isChanged)
{
click();
}
}
通过上面的方式,我们也就实现了在鼠标点击 button
的时候,先做一些自己的事情,然后,通过自己的选择来调用Qt 系统的方法,巧妙的实现一些看着很简单,但如果做的完美就很难的那种需求。
需求 2 : 最近在界面上设置数值的时候,使用了控件 QDoubleSpinBox
,发现他的鼠标滚轮事件真的是很烦,不经意的就会改变数值。当这些 QDoubleSpinBox
在界面的滚动框里面时,会显得更讨厌。因为本来是想滚动整个界面的,谁想到,界面没滚动,数值却变了,而变得让你发现不了。
有了上面的 button
的经验,这个修改就会显得非常简单。其实,我肯定是首先在Qt Assistant 里面找了 如何禁用 QDoubleSpinBox
的滚轮事件的属性,没找到之后,才会用自己的想法来简单粗暴的解决问题,非常简单。
#include <QDoubleSpinBox>
class DoubleSpinBox : public QDoubleSpinBox
{
Q_OBJECT
public:
using QDoubleSpinBox::QDoubleSpinBox;
~DoubleSpinBox();
protected:
void wheelEvent(QWheelEvent *event) override;
};
对,就是这样,定义自己的 DoubleSpinBox
,然后重写 wheelEvent
事件,并且重写函数体是空的。
需求3: 想实现一个如下面的功能,总得复选框可以控制下面子复选框的状态,但是如果下面的子复选框有部分被勾选,部分未勾选时,总复选框显示为半选中的状态,如下图所示。然后总复选框鼠标点击时不会出现半选中的状态,也就是下面的第三张图所示。
同样的,首先都会有一段不死心的状态来尽量的找 QCheckBox
的原始属性来进行控制。 找到了方法 void setTristate(bool y = true)
,设置 QCheckBox
是否为三种选中状态。设为 false 之后,发现,只要你后面通过调用方法 void setCheckState(Qt::CheckState state
) 进行状态的设置之后,前面使用 void setTristate(bool y = true)
设置的属性就会失效。扒了扒源代码发现:
/*!
Sets the checkbox's check state to \a state. If you do not need tristate
support, you can also use \l QAbstractButton::setChecked(), which takes a
boolean.
\sa checkState(), Qt::CheckState
*/
void QCheckBox::setCheckState(Qt::CheckState state)
{
Q_D(QCheckBox);
#ifndef QT_NO_ACCESSIBILITY
bool noChange = d->noChange;
#endif
if (state == Qt::PartiallyChecked) {
d->tristate = true;
d->noChange = true;
} else {
d->noChange = false;
}
d->blockRefresh = true;
setChecked(state != Qt::Unchecked);
d->blockRefresh = false;
d->refresh();
if ((uint)state != d->publishedState) {
d->publishedState = state;
emit stateChanged(state);
}
#ifndef QT_NO_ACCESSIBILITY
if (noChange != d->noChange) {
QAccessible::State s;
s.checkStateMixed = true;
QAccessibleStateChangeEvent event(this, s);
QAccessible::updateAccessibility(&event);
}
#endif
}
会重新设置 对象的 tristate
属性的值。也就导致我们怎么样都不能快速并且简单的实现我们的功能。
扒源码的时候发现, QCheckBox
的鼠标事件和 QPushbutton
的调用是一致的,他们都继承自 QAbstractButton
,那么也就顺理成章的使用了前面简单粗暴的方法。
自定类的定义和前面 button 的实现是一样的,就不再多写,主要看下我们实现功能的原理。
void CheckBox::btnClick()
{
if (checkState() == Qt::Unchecked)
{
setCheckState(Qt::PartiallyChecked);
}
click();
}
上面这个函数的含义是,当鼠标点击的时候,先判断 QCheckBox
的选中状态, 如果是 Qt::Unchecked
(未选中)的时候,首先设置 QCheckBox
的选中状态为 Qt::PartiallyChecked
(半选中)。然后调用 click 函数。
而在 click
函数中,会调用下面的方法设置下一个状态,也就是说会设置为选中状态。也因此,我们通过一些偏方来实现了我们的需求。
void QCheckBox::nextCheckState()
{
Q_D(QCheckBox);
if (d->tristate)
setCheckState((Qt::CheckState)((checkState() + 1) % 3));
else {
QAbstractButton::nextCheckState();
QCheckBox::checkStateSet();
}
}