Qt 自定义控件实现一些不重要但很膈应的功能

389 阅读6分钟

最近在做需求的时候,总是会发现一些比较尴尬的事情,每个控件的基本功能都有,但就是无论怎么修修补补都不能满足自己想要的效果。有时候强行实现了,却看到了非常多的补丁。就好像新买的裤子,还没穿就已经被打了一个一个的补丁。难看,并且很膈应。特别是在我们都在追求极简以及完美代码的时候。

所以针对这样一些比较尴尬的追求,总结了一些经验。

**需求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 
};

C++继承中子类的构造方法

首先对定义的类中一些东西做一下说明,主要是针对 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();
    }
}