【问题标题】:Toggle Switch in QtQt 中的切换开关
【发布时间】:2013-01-24 16:30:45
【问题描述】:

我正在尝试使用与 Qt 中的 Android Switches 等效的元素。我在 QML 中找到了一个 ToggleSwitch,但在实际的 C++ Qt 库中没有。我只是遗漏了什么,还是我必须自己重新实现这个小部件?

【问题讨论】:

    标签: c++ qt user-interface


    【解决方案1】:

    这是一个例子:

    switch.h:

    #pragma once
    #include <QtWidgets>
    
    class Switch : public QAbstractButton {
        Q_OBJECT
        Q_PROPERTY(int offset READ offset WRITE setOffset)
        Q_PROPERTY(QBrush brush READ brush WRITE setBrush)
    
    public:
        Switch(QWidget* parent = nullptr);
        Switch(const QBrush& brush, QWidget* parent = nullptr);
    
        QSize sizeHint() const override;
    
        QBrush brush() const {
            return _brush;
        }
        void setBrush(const QBrush &brsh) {
            _brush = brsh;
        }
    
        int offset() const {
            return _x;
        }
        void setOffset(int o) {
            _x = o;
            update();
        }
    
    protected:
        void paintEvent(QPaintEvent*) override;
        void mouseReleaseEvent(QMouseEvent*) override;
        void enterEvent(QEvent*) override;
    
    private:
        bool _switch;
        qreal _opacity;
        int _x, _y, _height, _margin;
        QBrush _thumb, _track, _brush;
        QPropertyAnimation *_anim = nullptr;
    };
    

    switch.cpp:

    Switch::Switch(QWidget *parent) : QAbstractButton(parent),
    _height(16),
    _opacity(0.000),
    _switch(false),
    _margin(3),
    _thumb("#d5d5d5"),
    _anim(new QPropertyAnimation(this, "offset", this))
    {
        setOffset(_height / 2);
        _y = _height / 2;
        setBrush(QColor("#009688"));
    }
    
    Switch::Switch(const QBrush &brush, QWidget *parent) : QAbstractButton(parent),
    _height(16),
    _switch(false),
    _opacity(0.000),
    _margin(3),
    _thumb("#d5d5d5"),
    _anim(new QPropertyAnimation(this, "offset", this))
    {
        setOffset(_height / 2);
        _y = _height / 2;
        setBrush(brush);
    }
    
    void Switch::paintEvent(QPaintEvent *e) {
        QPainter p(this);
        p.setPen(Qt::NoPen);
        if (isEnabled()) {
            p.setBrush(_switch ? brush() : Qt::black);
            p.setOpacity(_switch ? 0.5 : 0.38);
            p.setRenderHint(QPainter::Antialiasing, true);
            p.drawRoundedRect(QRect(_margin, _margin, width() - 2 * _margin, height() - 2 * _margin), 8.0, 8.0);
            p.setBrush(_thumb);
            p.setOpacity(1.0);
            p.drawEllipse(QRectF(offset() - (_height / 2), _y - (_height / 2), height(), height()));
        } else {
            p.setBrush(Qt::black);
            p.setOpacity(0.12);
            p.drawRoundedRect(QRect(_margin, _margin, width() - 2 * _margin, height() - 2 * _margin), 8.0, 8.0);
            p.setOpacity(1.0);
            p.setBrush(QColor("#BDBDBD"));
            p.drawEllipse(QRectF(offset() - (_height / 2), _y - (_height / 2), height(), height()));
        }
    }
    
    void Switch::mouseReleaseEvent(QMouseEvent *e) {
        if (e->button() & Qt::LeftButton) {
            _switch = _switch ? false : true;
            _thumb = _switch ? _brush : QBrush("#d5d5d5");
            if (_switch) {
                _anim->setStartValue(_height / 2);
                _anim->setEndValue(width() - _height);
                _anim->setDuration(120);
                _anim->start();
            } else {
                _anim->setStartValue(offset());
                _anim->setEndValue(_height / 2);
                _anim->setDuration(120);
                _anim->start();
            }
        }
        QAbstractButton::mouseReleaseEvent(e);
    }
    
    void Switch::enterEvent(QEvent *e) {
        setCursor(Qt::PointingHandCursor);
        QAbstractButton::enterEvent(e);
    }
    
    QSize Switch::sizeHint() const {
        return QSize(2 * (_height + _margin), _height + 2 * _margin);
    }
    

    main.cpp:

    #include "switch.h"
    
        int main(int argc, char *argv[]) {
            QApplication a(argc, argv);
            QWidget *widget = new QWidget;
            widget->setWindowFlags(Qt::FramelessWindowHint);
            QHBoxLayout layout;
            widget->setLayout(&layout);
            Switch *_switch = new Switch;
            Switch *_switch2 = new Switch;
            _switch2->setDisabled(true);
            layout.addWidget(_switch);
            layout.addWidget(_switch2);
            widget->show();
            return a.exec();
        }
    

    2018 年 8 月更新

    新的材质切换小部件!

    style.h

    /*
     * This is nearly complete Material design Switch widget implementation in qtwidgets module.
     * More info: https://material.io/design/components/selection-controls.html#switches
     * Copyright (C) 2018 Iman Ahmadvand
     *
     * This is free software: you can redistribute it and/or modify
     * it under the terms of the GNU General Public License as published by
     * the Free Software Foundation, either version 3 of the License, or
     * (at your option) any later version.
     *
     * It is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     * GNU General Public License for more details.
    */
    
    #ifndef STYLE_H
    #define STYLE_H
    
    #include <QtCore/qeasingcurve.h>
    
    #define cyan500 QColor("#00bcd4")
    #define gray50 QColor("#fafafa")
    #define black QColor("#000000")
    #define gray400 QColor("#bdbdbd")
    
    Q_DECL_IMPORT void qt_blurImage(QPainter *p, QImage &blurImage, qreal radius, bool quality, bool alphaOnly, int transposed = 0); // src/widgets/effects/qpixmapfilter.cpp
    
    namespace Style {
    
        using Type = QEasingCurve::Type;
    
        struct Animation {
            Animation() = default;
            Animation(Type _easing, int _duration) :easing{ _easing }, duration{ _duration } {
    
            }
    
            Type easing;
            int duration;
        };
    
        struct Switch {
            Switch() :
                height{ 36 },
                font{ QFont("Roboto medium", 13) },
                indicatorMargin{ QMargins(8, 8, 8, 8) },
                thumbOnBrush{ cyan500 },
                thumbOnOpacity{ 1 },
                trackOnBrush{ cyan500 },
                trackOnOpacity{ 0.5 },
                thumbOffBrush{ gray50 },
                thumbOffOpacity{ 1 },
                trackOffBrush{ black },
                trackOffOpacity{ 0.38 },
                thumbDisabled{ gray400 },
                thumbDisabledOpacity{ 1 },
                trackDisabled{ black },
                trackDisabledOpacity{ 0.12 },
                textColor{ black },
                disabledTextOpacity{ 0.26 },
                thumbBrushAnimation{ Animation(Type::Linear, 150) },
                trackBrushAnimation{ Animation(Type::Linear, 150) },
                thumbPosAniamtion{ Animation(Type::InOutQuad, 150) } {
    
            }
    
            int height;
            QFont font;
            QMargins indicatorMargin;
            QColor thumbOnBrush;
            double thumbOnOpacity;
            QColor trackOnBrush;
            double trackOnOpacity;
            QColor thumbOffBrush;
            double thumbOffOpacity;
            QColor trackOffBrush;
            double trackOffOpacity;
            QColor thumbDisabled;
            double thumbDisabledOpacity;
            QColor trackDisabled;
            double trackDisabledOpacity;
            QColor textColor;
            double disabledTextOpacity;
            Animation thumbBrushAnimation;
            Animation trackBrushAnimation;
            Animation thumbPosAniamtion;
        };
    
        inline QPixmap drawShadowEllipse(qreal radius, qreal elevation, const QColor& color) {
            auto px = QPixmap(radius * 2, radius * 2);
            px.fill(Qt::transparent);
    
            { // draw ellipes
                QPainter p(&px);
                p.setBrush(color);
                p.setPen(Qt::NoPen);
                p.setRenderHint(QPainter::Antialiasing, true);
                p.drawEllipse(QRectF(0, 0, px.size().width(), px.size().height()).center(), radius - elevation, radius - elevation);
            }
    
            QImage tmp(px.size(), QImage::Format_ARGB32_Premultiplied);
            tmp.setDevicePixelRatio(px.devicePixelRatioF());
            tmp.fill(0);
            QPainter tmpPainter(&tmp);
            tmpPainter.setCompositionMode(QPainter::CompositionMode_Source);
            tmpPainter.drawPixmap(QPointF(), px);
            tmpPainter.end();
    
            // blur the alpha channel
            QImage blurred(tmp.size(), QImage::Format_ARGB32_Premultiplied);
            blurred.setDevicePixelRatio(px.devicePixelRatioF());
            blurred.fill(0);
            {
                QPainter blurPainter(&blurred);
                qt_blurImage(&blurPainter, tmp, elevation * 4., true, false);
            }
    
            tmp = blurred;
    
            return QPixmap::fromImage(tmp);
        }
    
    } // namespace Style
    
    #endif // STYLE_H
    

    switch.h

    /*
     * This is nearly complete Material design Switch widget implementation in qtwidgets module.
     * More info: https://material.io/design/components/selection-controls.html#switches
     * Copyright (C) 2018-2020 Iman Ahmadvand
     *
     * This is free software: you can redistribute it and/or modify
     * it under the terms of the GNU General Public License as published by
     * the Free Software Foundation, either version 3 of the License, or
     * (at your option) any later version.
     *
     * It is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     * GNU General Public License for more details.
    */
    
    #ifndef SWITCH_H
    #define SWITCH_H
    
    #include <QtWidgets>
    #include "style.h"
    
    class Animator final : public QVariantAnimation {
        Q_OBJECT
        Q_PROPERTY(QObject* targetObject READ targetObject WRITE setTargetObject)
    
      public:
        Animator(QObject* target, QObject* parent = nullptr);
        ~Animator() override;
    
        QObject* targetObject() const;
        void setTargetObject(QObject* target);
    
        inline bool isRunning() const {
            return state() == Running;
        }
    
      public slots:
        void setup(int duration, QEasingCurve easing = QEasingCurve::Linear);
        void interpolate(const QVariant& start, const QVariant& end);
        void setCurrentValue(const QVariant&);
    
      protected:
        void updateCurrentValue(const QVariant& value) override final;
        void updateState(QAbstractAnimation::State newState, QAbstractAnimation::State oldState) override final;
    
      private:
        QPointer<QObject> target;
    };
    
    class SelectionControl : public QAbstractButton {
        Q_OBJECT
    
      public:
        explicit SelectionControl(QWidget* parent = nullptr);
        ~SelectionControl() override;
    
        Qt::CheckState checkState() const;
    
      Q_SIGNALS:
        void stateChanged(int);
    
      protected:
        void enterEvent(QEvent*) override;
        void checkStateSet() override;
        void nextCheckState() override;
        virtual void toggle(Qt::CheckState state) = 0;
    };
    
    class Switch final : public SelectionControl {
        Q_OBJECT
    
        static constexpr auto CORNER_RADIUS = 8.0;
        static constexpr auto THUMB_RADIUS = 14.5;
        static constexpr auto SHADOW_ELEVATION = 2.0;
    
      public:
        explicit Switch(QWidget* parent = nullptr);
        Switch(const QString& text, QWidget* parent = nullptr);
        Switch(const QString& text, const QBrush&, QWidget* parent = nullptr);
        ~Switch() override;
    
        QSize sizeHint() const override final;
    
      protected:
        void paintEvent(QPaintEvent*) override final;
        void resizeEvent(QResizeEvent*) override final;
        void toggle(Qt::CheckState) override final;
    
        void init();
        QRect indicatorRect();
        QRect textRect();
    
        static inline QColor colorFromOpacity(const QColor& c, qreal opacity) {
            return QColor(c.red(), c.green(), c.blue(), qRound(opacity * 255.0));
        }
        static inline bool ltr(QWidget* w) {
            if (nullptr != w)
                return w->layoutDirection() == Qt::LeftToRight;
    
            return false;
        }
    
      private:
        Style::Switch style;
        QPixmap shadowPixmap;
        QPointer<Animator> thumbBrushAnimation;
        QPointer<Animator> trackBrushAnimation;
        QPointer<Animator> thumbPosAniamtion;
    };
    
    #endif // SWITCH_H
    

    switch.cpp

    /*
     * This is nearly complete Material design Switch widget implementation in qtwidgets module.
     * More info: https://material.io/design/components/selection-controls.html#switches
     * Copyright (C) 2018-2020 Iman Ahmadvand
     *
     * This is free software: you can redistribute it and/or modify
     * it under the terms of the GNU General Public License as published by
     * the Free Software Foundation, either version 3 of the License, or
     * (at your option) any later version.
     *
     * It is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     * GNU General Public License for more details.
    */
    
    #include "switch.h"
    
    Animator::Animator(QObject* target, QObject* parent) : QVariantAnimation(parent) {
        setTargetObject(target);
    }
    
    Animator::~Animator() {
        stop();
    }
    
    QObject* Animator::targetObject() const {
        return target.data();
    }
    
    void Animator::setTargetObject(QObject* _target) {
        if (target.data() == _target)
            return;
    
        if (isRunning()) {
            qWarning("Animation::setTargetObject: you can't change the target of a running animation");
            return;
        }
    
        target = _target;
    }
    
    void Animator::updateCurrentValue(const QVariant& value) {
        Q_UNUSED(value);
    
        if (!target.isNull()) {
            auto update = QEvent(QEvent::StyleAnimationUpdate);
            update.setAccepted(false);
            QCoreApplication::sendEvent(target.data(), &update);
            if (!update.isAccepted())
                stop();
        }
    }
    
    void Animator::updateState(QAbstractAnimation::State newState, QAbstractAnimation::State oldState) {
        if (target.isNull() && oldState == Stopped) {
            qWarning("Animation::updateState: Changing state of an animation without target");
            return;
        }
    
        QVariantAnimation::updateState(newState, oldState);
    
        if (!endValue().isValid() && direction() == Forward) {
            qWarning("Animation::updateState (%s): starting an animation without end value", targetObject()->metaObject()->className());
        }
    }
    
    void Animator::setup(int duration, QEasingCurve easing) {
        setDuration(duration);
        setEasingCurve(easing);
    }
    
    void Animator::interpolate(const QVariant& _start, const QVariant& end) {
        setStartValue(_start);
        setEndValue(end);
        start();
    }
    
    void Animator::setCurrentValue(const QVariant& value) {
        setStartValue(value);
        setEndValue(value);
        updateCurrentValue(currentValue());
    }
    
    
    
    SelectionControl::SelectionControl(QWidget* parent) : QAbstractButton(parent) {
        setObjectName("SelectionControl");
        setCheckable(true);
    }
    
    SelectionControl::~SelectionControl() {
    
    }
    
    void SelectionControl::enterEvent(QEvent* e) {
        setCursor(Qt::PointingHandCursor);
        QAbstractButton::enterEvent(e);
    }
    
    Qt::CheckState SelectionControl::checkState() const {
        return isChecked() ? Qt::Checked : Qt::Unchecked;
    }
    
    void SelectionControl::checkStateSet() {
        const auto state = checkState();
        emit stateChanged(state);
        toggle(state);
    }
    
    void SelectionControl::nextCheckState() {
        QAbstractButton::nextCheckState();
        SelectionControl::checkStateSet();
    }
    
    
    
    void Switch::init() {
        setFont(style.font);
        setObjectName("Switch");
        /* setup animations */
        thumbBrushAnimation = new Animator{ this, this };
        trackBrushAnimation = new Animator{ this, this };
        thumbPosAniamtion = new Animator{ this, this };
        thumbPosAniamtion->setup(style.thumbPosAniamtion.duration, style.thumbPosAniamtion.easing);
        trackBrushAnimation->setup(style.trackBrushAnimation.duration, style.trackBrushAnimation.easing);
        thumbBrushAnimation->setup(style.thumbBrushAnimation.duration, style.thumbBrushAnimation.easing);
        /* set init values */
        trackBrushAnimation->setStartValue(colorFromOpacity(style.trackOffBrush, style.trackOffOpacity));
        trackBrushAnimation->setEndValue(colorFromOpacity(style.trackOffBrush, style.trackOffOpacity));
        thumbBrushAnimation->setStartValue(colorFromOpacity(style.thumbOffBrush, style.thumbOffOpacity));
        thumbBrushAnimation->setEndValue(colorFromOpacity(style.thumbOffBrush, style.thumbOffOpacity));
        /* set standard palettes */
        auto p = palette();
        p.setColor(QPalette::Active, QPalette::ButtonText, style.textColor);
        p.setColor(QPalette::Disabled, QPalette::ButtonText, style.textColor);
        setPalette(p);
        setSizePolicy(QSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Fixed));
    }
    
    QRect Switch::indicatorRect() {
        const auto w = style.indicatorMargin.left() + style.height + style.indicatorMargin.right();
        return ltr(this) ? QRect(0, 0, w, style.height) : QRect(width() - w, 0, w, style.height);
    }
    
    QRect Switch::textRect() {
        const auto w = style.indicatorMargin.left() + style.height + style.indicatorMargin.right();
        return ltr(this) ? rect().marginsRemoved(QMargins(w, 0, 0, 0)) : rect().marginsRemoved(QMargins(0, 0, w, 0));
    }
    
    Switch::Switch(QWidget* parent) : SelectionControl(parent) {
        init();
    }
    
    Switch::Switch(const QString& text, QWidget* parent) : Switch(parent) {
        setText(text);
    }
    
    Switch::Switch(const QString& text, const QBrush& brush, QWidget* parent) : Switch(text, parent) {
        style.thumbOnBrush = brush.color();
        style.trackOnBrush = brush.color();
    }
    
    Switch::~Switch() {
    
    }
    
    QSize Switch::sizeHint() const {
        auto h = style.height;
        auto w = style.indicatorMargin.left() + style.height + style.indicatorMargin.right() + fontMetrics().width(text());
    
        return QSize(w, h);
    }
    
    void Switch::paintEvent(QPaintEvent*) {
        /* for desktop usage we do not need Radial reaction */
    
        QPainter p(this);
    
        const auto _indicatorRect = indicatorRect();
        const auto _textRect = textRect();
        auto trackMargin = style.indicatorMargin;
        trackMargin.setTop(trackMargin.top() + 2);
        trackMargin.setBottom(trackMargin.bottom() + 2);
        QRectF trackRect = _indicatorRect.marginsRemoved(trackMargin);
    
        if (isEnabled()) {
            p.setOpacity(1.0);
            p.setPen(Qt::NoPen);
            /* draw track */
            p.setBrush(trackBrushAnimation->currentValue().value<QColor>());
            p.setRenderHint(QPainter::Antialiasing, true);
            p.drawRoundedRect(trackRect, CORNER_RADIUS, CORNER_RADIUS);
            p.setRenderHint(QPainter::Antialiasing, false);
            /* draw thumb */
            trackRect.setX(trackRect.x() - trackMargin.left() - trackMargin.right() - 2 + thumbPosAniamtion->currentValue().toInt());
            auto thumbRect = trackRect;
    
            if (!shadowPixmap.isNull())
                p.drawPixmap(thumbRect.center() - QPointF(THUMB_RADIUS, THUMB_RADIUS - 1.0), shadowPixmap);
    
            p.setBrush(thumbBrushAnimation->currentValue().value<QColor>());
            p.setRenderHint(QPainter::Antialiasing, true);
            //        qDebug() << thumbRect << thumbPosAniamtion->currentValue();
            p.drawEllipse(thumbRect.center(), THUMB_RADIUS - SHADOW_ELEVATION - 1.0, THUMB_RADIUS - SHADOW_ELEVATION - 1.0);
            p.setRenderHint(QPainter::Antialiasing, false);
    
            /* draw text */
            if (text().isEmpty())
                return;
    
            p.setOpacity(1.0);
            p.setPen(palette().color(QPalette::Active, QPalette::ButtonText));
            p.setFont(font());
            p.drawText(_textRect, Qt::AlignLeft | Qt::AlignVCenter, text());
        } else {
            p.setOpacity(style.trackDisabledOpacity);
            p.setPen(Qt::NoPen);
            // draw track
            p.setBrush(style.trackDisabled);
            p.setRenderHint(QPainter::Antialiasing, true);
            p.drawRoundedRect(trackRect, CORNER_RADIUS, CORNER_RADIUS);
            p.setRenderHint(QPainter::Antialiasing, false);
            // draw thumb
            p.setOpacity(1.0);
            if (!isChecked())
                trackRect.setX(trackRect.x() - trackMargin.left() - trackMargin.right() - 2);
            else
                trackRect.setX(trackRect.x() + trackMargin.left() + trackMargin.right() + 2);
            auto thumbRect = trackRect;
    
            if (!shadowPixmap.isNull())
                p.drawPixmap(thumbRect.center() - QPointF(THUMB_RADIUS, THUMB_RADIUS - 1.0), shadowPixmap);
    
            p.setOpacity(1.0);
            p.setBrush(style.thumbDisabled);
            p.setRenderHint(QPainter::Antialiasing, true);
            p.drawEllipse(thumbRect.center(), THUMB_RADIUS - SHADOW_ELEVATION - 1.0, THUMB_RADIUS - SHADOW_ELEVATION - 1.0);
    
            /* draw text */
            if (text().isEmpty())
                return;
    
            p.setOpacity(style.disabledTextOpacity);
            p.setPen(palette().color(QPalette::Disabled, QPalette::ButtonText));
            p.setFont(font());
            p.drawText(_textRect, Qt::AlignLeft | Qt::AlignVCenter, text());
        }
    }
    
    void Switch::resizeEvent(QResizeEvent* e) {
        shadowPixmap = Style::drawShadowEllipse(THUMB_RADIUS, SHADOW_ELEVATION, QColor(0, 0, 0, 70));
        SelectionControl::resizeEvent(e);
    }
    
    void Switch::toggle(Qt::CheckState state) {
        if (state == Qt::Checked) {
            const QVariant posEnd = (style.indicatorMargin.left() + style.indicatorMargin.right() + 2) * 2;
            const QVariant thumbEnd = colorFromOpacity(style.thumbOnBrush, style.thumbOnOpacity);
            const QVariant trackEnd = colorFromOpacity(style.trackOnBrush, style.trackOnOpacity);
    
            if (!isVisible()) {
                thumbPosAniamtion->setCurrentValue(posEnd);
                thumbBrushAnimation->setCurrentValue(thumbEnd);
                trackBrushAnimation->setCurrentValue(trackEnd);
            } else {
                thumbPosAniamtion->interpolate(0, posEnd);
                thumbBrushAnimation->interpolate(colorFromOpacity(style.thumbOffBrush, style.thumbOffOpacity), thumbEnd);
                trackBrushAnimation->interpolate(colorFromOpacity(style.trackOffBrush, style.trackOffOpacity), trackEnd);
            }
        } else { // Qt::Unchecked
            const QVariant posEnd = 0;
            const QVariant thumbEnd = colorFromOpacity(style.thumbOffBrush, style.thumbOffOpacity);
            const QVariant trackEnd = colorFromOpacity(style.trackOffBrush, style.trackOffOpacity);
    
            if (!isVisible()) {
                thumbPosAniamtion->setCurrentValue(posEnd);
                thumbBrushAnimation->setCurrentValue(thumbEnd);
                trackBrushAnimation->setCurrentValue(trackEnd);
            } else {
                thumbPosAniamtion->interpolate(thumbPosAniamtion->currentValue().toInt(), posEnd);
                thumbBrushAnimation->interpolate(colorFromOpacity(style.thumbOnBrush, style.thumbOnOpacity), thumbEnd);
                trackBrushAnimation->interpolate(colorFromOpacity(style.trackOnBrush, style.trackOnOpacity), trackEnd);
            }
        }
    }
    

    main.cpp

    #include "switch.h"
    
    int main(int argc, char *argv[]) {
        QApplication application(argc, argv);
        QWidget container;
        QVBoxLayout mainLayout;
        container.setLayout(&mainLayout);
    
        Switch* switch1 = new Switch("SWITCH");
        mainLayout.addWidget(switch1);
        Switch* switch2 = new Switch("SWITCH");
        mainLayout.addWidget(switch2);
        switch2->setDisabled(true);
        Switch* switch3 = new Switch("SWITCH");
        mainLayout.addWidget(switch3);
        switch3->setLayoutDirection(Qt::RightToLeft);
        Switch* switch4 = new Switch("SWITCH");
        mainLayout.addWidget(switch4);
        switch4->setLayoutDirection(Qt::RightToLeft);
        switch4->setChecked(true);
        switch4->setDisabled(true);
    
        QButtonGroup bg;
        Switch* item1 = new Switch("ITEM1");
        Switch* item2 = new Switch("ITEM2");
        bg.addButton(item1);
        bg.addButton(item2);
        mainLayout.addWidget(item1);
        mainLayout.addWidget(item2);
        mainLayout.setMargin(100);
    
        container.show();
        return application.exec();
    }
    

    结果:

    【讨论】:

    • 这个类很棒,但它需要在 void Switch::mouseReleaseEvent(QMouseEvent *e) 末尾调用 QAbstractButton::mouseReleaseEvent(e);。这将有助于按钮在单击时发出所有正确的信号。
    • 是的,调用基类实现是个好习惯。谢谢
    • 这个实现有一些问题,例如缺少对 QAbstractButton::setCheckable(true) 的调用(以获取正确的切换/单击信号),如果边距为 0,则计算将不起作用。但这些相对容易修复。因为我在寻找 PyQt5 解决方案,所以我也发布了我的固定版本作为答案。谢谢 INAN4K 你的例子!
    【解决方案2】:

    几个月前,我做了一个实现,其视觉效果需要与常见的桌面样式更加一致(可用的 C++ 和 Python 版本;Python 版本是原型,即它们可能会以不同的方式工作)。请注意,美学是使用paintEvent 完全定制的。不要期望因系统而异的视觉效果。


    C++ 实现

    注意:不要忘记包含(不在我的示例中)。

    用法:

    SwitchButton* sbtn = new SwitchButton(this); // Default style is Style::ONOFF
    bool current = sbtn->value();
    sbtn->setValue(!current);
    

    (...).hpp

    class SwitchButton : public QWidget
    {
      Q_OBJECT
        Q_DISABLE_COPY(SwitchButton)
    
    public:
      enum Style
      {
        YESNO,
        ONOFF,
        BOOL,
        EMPTY
      };
    
    public:
      explicit SwitchButton(QWidget* parent = nullptr, Style style = Style::ONOFF);
      ~SwitchButton() override;
    
      //-- QWidget methods
      void mousePressEvent(QMouseEvent *) override;
      void paintEvent(QPaintEvent* event) override;
      void setEnabled(bool);
    
      //-- Setters
      void setDuration(int);
      void setValue(bool);
    
      //-- Getters
      bool value() const;
    
    signals:
      void valueChanged(bool newvalue);
    
    private:
      class SwitchCircle;
      class SwitchBackground;
      void _update();
    
    private:
      bool _value;
      int  _duration;
    
      QLinearGradient _lg;
      QLinearGradient _lg2;
      QLinearGradient _lg_disabled;
    
      QColor _pencolor;
      QColor _offcolor;
      QColor _oncolor;
      int    _tol;
      int    _borderradius;
    
      // This order for definition is important (these widgets overlap)
      QLabel*           _labeloff;
      SwitchBackground* _background;
      QLabel*           _labelon;
      SwitchCircle*     _circle;
    
      bool _enabled;
    
      QPropertyAnimation* __btn_move;
      QPropertyAnimation* __back_move;
    };
    
    class SwitchButton::SwitchBackground : public QWidget
    {
      Q_OBJECT
        Q_DISABLE_COPY(SwitchBackground)
    
    public:
      explicit SwitchBackground(QWidget* parent = nullptr, QColor color = QColor(154, 205, 50), bool rect = false);
      ~SwitchBackground() override;
    
      //-- QWidget methods
      void paintEvent(QPaintEvent* event) override;
      void setEnabled(bool);
    
    private:
      bool            _rect;
      int             _borderradius;
      QColor          _color;
      QColor          _pencolor;
      QLinearGradient _lg;
      QLinearGradient _lg_disabled;
    
      bool _enabled;
    };
    
    
    class SwitchButton::SwitchCircle : public QWidget
    {
      Q_OBJECT
        Q_DISABLE_COPY(SwitchCircle)
    
    public:
      explicit SwitchCircle(QWidget* parent = nullptr, QColor color = QColor(255, 255, 255), bool rect = false);
      ~SwitchCircle() override;
    
      //-- QWidget methods
      void paintEvent(QPaintEvent* event) override;
      void setEnabled(bool);
    
    private:
      bool            _rect;
      int             _borderradius;
      QColor          _color;
      QColor          _pencolor;
      QRadialGradient _rg;
      QLinearGradient _lg;
      QLinearGradient _lg_disabled;
    
      bool _enabled;
    };
    

    (...).cpp

    SwitchButton::SwitchButton(QWidget* parent, Style style)
      : QWidget(parent)
      , _value(false)
      , _duration(100)
      , _enabled(true)
    {
      _pencolor = QColor(120, 120, 120);
    
      _lg = QLinearGradient(35, 30, 35, 0);
      _lg.setColorAt(0, QColor(210, 210, 210));
      _lg.setColorAt(0.25, QColor(255, 255, 255));
      _lg.setColorAt(0.82, QColor(255, 255, 255));
      _lg.setColorAt(1, QColor(210, 210, 210));
    
      _lg2 = QLinearGradient(50, 30, 35, 0);
      _lg2.setColorAt(0, QColor(230, 230, 230));
      _lg2.setColorAt(0.25, QColor(255, 255, 255));
      _lg2.setColorAt(0.82, QColor(255, 255, 255));
      _lg2.setColorAt(1, QColor(230, 230, 230));
    
      _lg_disabled = QLinearGradient(50, 30, 35, 0);
      _lg_disabled.setColorAt(0, QColor(200, 200, 200));
      _lg_disabled.setColorAt(0.25, QColor(230, 230, 230));
      _lg_disabled.setColorAt(0.82, QColor(230, 230, 230));
      _lg_disabled.setColorAt(1, QColor(200, 200, 200));
    
      _offcolor = QColor(255, 255, 255);
      _oncolor = QColor(154, 205, 50);
      _tol = 0;
      _borderradius = 12;
      _labeloff = NEW QLabel(this);
      _background = NEW SwitchBackground(this, _oncolor);
      _labelon = NEW QLabel(this);
      _circle = NEW SwitchCircle(this, _offcolor);
      __btn_move = NEW QPropertyAnimation(this);
      __back_move = NEW QPropertyAnimation(this);
    
      __btn_move->setTargetObject(_circle);
      __btn_move->setPropertyName("pos");
      __back_move->setTargetObject(_background);
      __back_move->setPropertyName("size");
    
      setWindowFlags(Qt::FramelessWindowHint);
      setAttribute(Qt::WA_TranslucentBackground);
    
      _labeloff->setText("Off");
      _labelon->setText("On");
      _labeloff->move(31, 5);
      _labelon->move(15, 5);
      setFixedSize(QSize(60, 24));
      if (style == Style::YESNO)
      {
        _labeloff->setText("No");
        _labelon->setText("Yes");
        _labeloff->move(33, 5);
        _labelon->move(12, 5);
        setFixedSize(QSize(60, 24));
      }
      else if (style == Style::BOOL)
      {
        _labeloff->setText("False");
        _labelon->setText("True");
        _labeloff->move(37, 5);
        _labelon->move(12, 5);
        setFixedSize(QSize(75, 24));
      }
      if (style == Style::EMPTY)
      {
        _labeloff->setText("");
        _labelon->setText("");
        _labeloff->move(31, 5);
        _labelon->move(12, 5);
        setFixedSize(QSize(45, 24));
      }
    
      _labeloff->setStyleSheet("color: rgb(120, 120, 120); font-weight: bold;");
      _labelon->setStyleSheet("color: rgb(255, 255, 255); font-weight: bold;");
    
      _background->resize(20, 20);
    
      _background->move(2, 2);
      _circle->move(2, 2);
    }
    
    SwitchButton::~SwitchButton()
    {
      delete _circle;
      delete _background;
      delete _labeloff;
      delete _labelon;
      delete __btn_move;
      delete __back_move;
    }
    
    void SwitchButton::paintEvent(QPaintEvent*)
    {
      QPainter* painter = new QPainter;
      painter->begin(this);
      painter->setRenderHint(QPainter::Antialiasing, true);
    
      QPen pen(Qt::NoPen);
      painter->setPen(pen);
    
      painter->setBrush(_pencolor);
      painter->drawRoundedRect(0, 0
        , width(), height()
        , 12, 12);
    
      painter->setBrush(_lg);
      painter->drawRoundedRect(1, 1
        , width() - 2, height() - 2
        , 10, 10);
    
      painter->setBrush(QColor(210, 210, 210));
      painter->drawRoundedRect(2, 2
        , width() - 4, height() - 4
        , 10, 10);
    
      if (_enabled)
      {
        painter->setBrush(_lg2);
        painter->drawRoundedRect(3, 3
          , width() - 6, height() - 6
          , 7, 7);
      }
      else
      {
        painter->setBrush(_lg_disabled);
        painter->drawRoundedRect(3, 3
          , width() - 6, height() - 6
          , 7, 7);
      }
      painter->end();
    }
    
    void SwitchButton::mousePressEvent(QMouseEvent*)
    {
      if (!_enabled)
        return;
    
      __btn_move->stop();
      __back_move->stop();
    
      __btn_move->setDuration(_duration);
      __back_move->setDuration(_duration);
    
      int hback = 20;
      QSize initial_size(hback, hback);
      QSize final_size(width() - 4, hback);
    
      int xi = 2;
      int y  = 2;
      int xf = width() - 22;
    
      if (_value)
      {
        final_size = QSize(hback, hback);
        initial_size = QSize(width() - 4, hback);
    
        xi = xf;
        xf = 2;
      }
    
      __btn_move->setStartValue(QPoint(xi, y));
      __btn_move->setEndValue(QPoint(xf, y));
    
      __back_move->setStartValue(initial_size);
      __back_move->setEndValue(final_size);
    
      __btn_move->start();
      __back_move->start();
    
      // Assigning new current value
      _value = !_value;
      emit valueChanged(_value);
    }
    
    void SwitchButton::setEnabled(bool flag)
    {
      _enabled = flag;
      _circle->setEnabled(flag);
      _background->setEnabled(flag);
      if (flag)
        _labelon->show();
      else
      {
        if (value())
          _labelon->show();
        else
          _labelon->hide();
      }
      QWidget::setEnabled(flag);
    }
    
    void SwitchButton::setDuration(int time)
    {
      _duration = time;
    }
    
    void SwitchButton::setValue(bool flag)
    {
      if (flag == value())
        return;
      else
      {
        _value = flag;
        _update();
        setEnabled(_enabled);
      }
    }
    
    bool SwitchButton::value() const
    {
      return _value;
    }
    
    void SwitchButton::_update()
    {
      int hback = 20;
      QSize final_size(width() - 4, hback);
    
      int y = 2;
      int xf = width() - 22;
    
      if (_value)
      {
        final_size = QSize(hback, hback);
        xf = 2;
      }
    
      _circle->move(QPoint(xf, y));
      _background->resize(final_size);
    }
    
    SwitchButton::SwitchBackground::SwitchBackground(QWidget* parent, QColor color, bool rect)
      : QWidget(parent)
      , _rect(rect)
      , _borderradius(12)
      , _color(color)
      , _pencolor(QColor(170, 170, 170))
    {
      setFixedHeight(20);
    
      _lg = QLinearGradient(0, 25, 70, 0);
      _lg.setColorAt(0, QColor(154, 194, 50));
      _lg.setColorAt(0.25, QColor(154, 210, 50));
      _lg.setColorAt(0.95, QColor(154, 194, 50));
    
      _lg_disabled = QLinearGradient(0, 25, 70, 0);
      _lg_disabled.setColorAt(0, QColor(190, 190, 190));
      _lg_disabled.setColorAt(0.25, QColor(230, 230, 230));
      _lg_disabled.setColorAt(0.95, QColor(190, 190, 190));
    
      if (_rect)
        _borderradius = 0;
    
      _enabled = true;
    }
    SwitchButton::SwitchBackground::~SwitchBackground()
    {
    }
    void SwitchButton::SwitchBackground::paintEvent(QPaintEvent*)
    {
      QPainter* painter = new QPainter;
      painter->begin(this);
      painter->setRenderHint(QPainter::Antialiasing, true);
    
      QPen pen(Qt::NoPen);
      painter->setPen(pen);
      if (_enabled)
      {
        painter->setBrush(QColor(154, 190, 50));
        painter->drawRoundedRect(0, 0
          , width(), height()
          , 10, 10);
    
        painter->setBrush(_lg);
        painter->drawRoundedRect(1, 1, width()-2, height()-2, 8, 8);
      }
      else
      {
        painter->setBrush(QColor(150, 150, 150));
        painter->drawRoundedRect(0, 0
          , width(), height()
          , 10, 10);
    
        painter->setBrush(_lg_disabled);
        painter->drawRoundedRect(1, 1, width() - 2, height() - 2, 8, 8);
      }
      painter->end();
    }
    void SwitchButton::SwitchBackground::setEnabled(bool flag)
    {
      _enabled = flag;
    }
    
    SwitchButton::SwitchCircle::SwitchCircle(QWidget* parent, QColor color, bool rect)
      : QWidget(parent)
      , _rect(rect)
      , _borderradius(12)
      , _color(color)
      , _pencolor(QColor(120, 120, 120))
    {
      setFixedSize(20, 20);
    
      _rg = QRadialGradient(static_cast<int>(width() / 2), static_cast<int>(height() / 2), 12);
      _rg.setColorAt(0, QColor(255, 255, 255));
      _rg.setColorAt(0.6, QColor(255, 255, 255));
      _rg.setColorAt(1, QColor(205, 205, 205));
    
      _lg = QLinearGradient(3, 18, 20, 4);
      _lg.setColorAt(0, QColor(255, 255, 255));
      _lg.setColorAt(0.55, QColor(230, 230, 230));
      _lg.setColorAt(0.72, QColor(255, 255, 255));
      _lg.setColorAt(1, QColor(255, 255, 255));
    
      _lg_disabled = QLinearGradient(3, 18, 20, 4);
      _lg_disabled.setColorAt(0, QColor(230, 230, 230));
      _lg_disabled.setColorAt(0.55, QColor(210, 210, 210));
      _lg_disabled.setColorAt(0.72, QColor(230, 230, 230));
      _lg_disabled.setColorAt(1, QColor(230, 230, 230));
    
      _enabled = true;
    }
    SwitchButton::SwitchCircle::~SwitchCircle()
    {
    }
    void SwitchButton::SwitchCircle::paintEvent(QPaintEvent*)
    {
      QPainter* painter = new QPainter;
      painter->begin(this);
      painter->setRenderHint(QPainter::Antialiasing, true);
    
      QPen pen(Qt::NoPen);
      painter->setPen(pen);
      painter->setBrush(_pencolor);
    
      painter->drawEllipse(0, 0, 20, 20);
      painter->setBrush(_rg);
      painter->drawEllipse(1, 1, 18, 18);
    
      painter->setBrush(QColor(210, 210, 210));
      painter->drawEllipse(2, 2, 16, 16);
    
      if (_enabled)
      {
        painter->setBrush(_lg);
        painter->drawEllipse(3, 3, 14, 14);
      }
      else
      {
        painter->setBrush(_lg_disabled);
        painter->drawEllipse(3, 3, 14, 14);
      }
    
      painter->end();
    }
    void SwitchButton::SwitchCircle::setEnabled(bool flag)
    {
      _enabled = flag;
    }
    

    Python 实现(原型;PyQt5)

    用法:

    switchbtn = SwitchButton(self, "On", 15, "Off", 31, 60)
    

    (...).py

    from PyQt5 import QtWidgets, QtCore, QtGui
    
    class SwitchButton(QtWidgets.QWidget):
        def __init__(self, parent=None, w1="Yes", l1=12, w2="No", l2=33, width=60):
            super(SwitchButton, self).__init__(parent)
            self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
            self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
            self.__labeloff = QtWidgets.QLabel(self)
            self.__labeloff.setText(w2)
            self.__labeloff.setStyleSheet("""color: rgb(120, 120, 120); font-weight: bold;""")
            self.__background  = Background(self)
            self.__labelon = QtWidgets.QLabel(self)
            self.__labelon.setText(w1)
            self.__labelon.setStyleSheet("""color: rgb(255, 255, 255); font-weight: bold;""")
            self.__circle      = Circle(self)
            self.__circlemove  = None
            self.__ellipsemove = None
            self.__enabled     = True
            self.__duration    = 100
            self.__value       = False
            self.setFixedSize(width, 24)
    
            self.__background.resize(20, 20)
            self.__background.move(2, 2)
            self.__circle.move(2, 2)
            self.__labelon.move(l1, 5)
            self.__labeloff.move(l2, 5)
    
        def setDuration(self, time):
            self.__duration = time
    
        def mousePressEvent(self, event):
            if not self.__enabled:
                return
    
            self.__circlemove = QtCore.QPropertyAnimation(self.__circle, b"pos")
            self.__circlemove.setDuration(self.__duration)
    
            self.__ellipsemove = QtCore.QPropertyAnimation(self.__background, b"size")
            self.__ellipsemove.setDuration(self.__duration)
    
            xs = 2
            y  = 2
            xf = self.width()-22
            hback = 20
            isize = QtCore.QSize(hback, hback)
            bsize = QtCore.QSize(self.width()-4, hback)
            if self.__value:
                xf = 2
                xs = self.width()-22
                bsize = QtCore.QSize(hback, hback)
                isize = QtCore.QSize(self.width()-4, hback)
    
            self.__circlemove.setStartValue(QtCore.QPoint(xs, y))
            self.__circlemove.setEndValue(QtCore.QPoint(xf, y))
    
            self.__ellipsemove.setStartValue(isize)
            self.__ellipsemove.setEndValue(bsize)
    
            self.__circlemove.start()
            self.__ellipsemove.start()
            self.__value = not self.__value
    
        def paintEvent(self, event):
            s = self.size()
            qp = QtGui.QPainter()
            qp.begin(self)
            qp.setRenderHint(QtGui.QPainter.Antialiasing, True)
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            qp.setPen(pen)
            qp.setBrush(QtGui.QColor(120, 120, 120))
            qp.drawRoundedRect(0, 0, s.width(), s.height(), 12, 12)
            lg = QtGui.QLinearGradient(35, 30, 35, 0)
            lg.setColorAt(0, QtGui.QColor(210, 210, 210, 255))
            lg.setColorAt(0.25, QtGui.QColor(255, 255, 255, 255))
            lg.setColorAt(0.82, QtGui.QColor(255, 255, 255, 255))
            lg.setColorAt(1, QtGui.QColor(210, 210, 210, 255))
            qp.setBrush(lg)
            qp.drawRoundedRect(1, 1, s.width()-2, s.height()-2, 10, 10)
    
            qp.setBrush(QtGui.QColor(210, 210, 210))
            qp.drawRoundedRect(2, 2, s.width() - 4, s.height() - 4, 10, 10)
    
            if self.__enabled:
                lg = QtGui.QLinearGradient(50, 30, 35, 0)
                lg.setColorAt(0, QtGui.QColor(230, 230, 230, 255))
                lg.setColorAt(0.25, QtGui.QColor(255, 255, 255, 255))
                lg.setColorAt(0.82, QtGui.QColor(255, 255, 255, 255))
                lg.setColorAt(1, QtGui.QColor(230, 230, 230, 255))
                qp.setBrush(lg)
                qp.drawRoundedRect(3, 3, s.width() - 6, s.height() - 6, 7, 7)
            else:
                lg = QtGui.QLinearGradient(50, 30, 35, 0)
                lg.setColorAt(0, QtGui.QColor(200, 200, 200, 255))
                lg.setColorAt(0.25, QtGui.QColor(230, 230, 230, 255))
                lg.setColorAt(0.82, QtGui.QColor(230, 230, 230, 255))
                lg.setColorAt(1, QtGui.QColor(200, 200, 200, 255))
                qp.setBrush(lg)
                qp.drawRoundedRect(3, 3, s.width() - 6, s.height() - 6, 7, 7)
            qp.end()
    
    class Circle(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Circle, self).__init__(parent)
            self.__enabled = True
            self.setFixedSize(20, 20)
    
        def paintEvent(self, event):
            s = self.size()
            qp = QtGui.QPainter()
            qp.begin(self)
            qp.setRenderHint(QtGui.QPainter.Antialiasing, True)
            qp.setPen(QtCore.Qt.NoPen)
            qp.setBrush(QtGui.QColor(120, 120, 120))
            qp.drawEllipse(0, 0, 20, 20)
            rg = QtGui.QRadialGradient(int(self.width() / 2), int(self.height() / 2), 12)
            rg.setColorAt(0, QtGui.QColor(255, 255, 255))
            rg.setColorAt(0.6, QtGui.QColor(255, 255, 255))
            rg.setColorAt(1, QtGui.QColor(205, 205, 205))
            qp.setBrush(QtGui.QBrush(rg))
            qp.drawEllipse(1,1, 18, 18)
    
            qp.setBrush(QtGui.QColor(210, 210, 210))
            qp.drawEllipse(2, 2, 16, 16)
    
            if self.__enabled:
                lg = QtGui.QLinearGradient(3, 18,20, 4)
                lg.setColorAt(0, QtGui.QColor(255, 255, 255, 255))
                lg.setColorAt(0.55, QtGui.QColor(230, 230, 230, 255))
                lg.setColorAt(0.72, QtGui.QColor(255, 255, 255, 255))
                lg.setColorAt(1, QtGui.QColor(255, 255, 255, 255))
                qp.setBrush(lg)
                qp.drawEllipse(3,3, 14, 14)
            else:
                lg = QtGui.QLinearGradient(3, 18, 20, 4)
                lg.setColorAt(0, QtGui.QColor(230, 230, 230))
                lg.setColorAt(0.55, QtGui.QColor(210, 210, 210))
                lg.setColorAt(0.72, QtGui.QColor(230, 230, 230))
                lg.setColorAt(1, QtGui.QColor(230, 230, 230))
                qp.setBrush(lg)
                qp.drawEllipse(3, 3, 14, 14)
            qp.end()
    
    class Background(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Background, self).__init__(parent)
            self.__enabled = True
            self.setFixedHeight(20)
    
        def paintEvent(self, event):
            s = self.size()
            qp = QtGui.QPainter()
            qp.begin(self)
            qp.setRenderHint(QtGui.QPainter.Antialiasing, True)
            pen = QtGui.QPen(QtCore.Qt.NoPen)
            qp.setPen(pen)
            qp.setBrush(QtGui.QColor(154,205,50))
            if self.__enabled:
                qp.setBrush(QtGui.QColor(154, 190, 50))
                qp.drawRoundedRect(0, 0, s.width(), s.height(), 10, 10)
    
                lg = QtGui.QLinearGradient(0, 25, 70, 0)
                lg.setColorAt(0, QtGui.QColor(154, 184, 50))
                lg.setColorAt(0.35, QtGui.QColor(154, 210, 50))
                lg.setColorAt(0.85, QtGui.QColor(154, 184, 50))
                qp.setBrush(lg)
                qp.drawRoundedRect(1, 1, s.width() - 2, s.height() - 2, 8, 8)
            else:
                qp.setBrush(QtGui.QColor(150, 150, 150))
                qp.drawRoundedRect(0, 0, s.width(), s.height(), 10, 10)
    
                lg = QtGui.QLinearGradient(5, 25, 60, 0)
                lg.setColorAt(0, QtGui.QColor(190, 190, 190))
                lg.setColorAt(0.35, QtGui.QColor(230, 230, 230))
                lg.setColorAt(0.85, QtGui.QColor(190, 190, 190))
                qp.setBrush(lg)
                qp.drawRoundedRect(1, 1, s.width() - 2, s.height() - 2, 8, 8)
            qp.end()
    

    【讨论】:

      【解决方案3】:

      这是@IMAN4K 的answer 的Python 3 / PyQt5 实现。

      对原始实现的改进:

      • Thumb-size 可以大于/小于轨道大小
      • 使用当前应用的调色板进行着色
      • 点击时发出切换/点击信号
      • 其他各种修复

      from PyQt5.QtCore import QPropertyAnimation, QRectF, QSize, Qt, pyqtProperty
      from PyQt5.QtGui import QPainter
      from PyQt5.QtWidgets import (
          QAbstractButton,
          QApplication,
          QHBoxLayout,
          QSizePolicy,
          QWidget,
      )
      
      
      class Switch(QAbstractButton):
          def __init__(self, parent=None, track_radius=10, thumb_radius=8):
              super().__init__(parent=parent)
              self.setCheckable(True)
              self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
      
              self._track_radius = track_radius
              self._thumb_radius = thumb_radius
      
              self._margin = max(0, self._thumb_radius - self._track_radius)
              self._base_offset = max(self._thumb_radius, self._track_radius)
              self._end_offset = {
                  True: lambda: self.width() - self._base_offset,
                  False: lambda: self._base_offset,
              }
              self._offset = self._base_offset
      
              palette = self.palette()
              if self._thumb_radius > self._track_radius:
                  self._track_color = {
                      True: palette.highlight(),
                      False: palette.dark(),
                  }
                  self._thumb_color = {
                      True: palette.highlight(),
                      False: palette.light(),
                  }
                  self._text_color = {
                      True: palette.highlightedText().color(),
                      False: palette.dark().color(),
                  }
                  self._thumb_text = {
                      True: '',
                      False: '',
                  }
                  self._track_opacity = 0.5
              else:
                  self._thumb_color = {
                      True: palette.highlightedText(),
                      False: palette.light(),
                  }
                  self._track_color = {
                      True: palette.highlight(),
                      False: palette.dark(),
                  }
                  self._text_color = {
                      True: palette.highlight().color(),
                      False: palette.dark().color(),
                  }
                  self._thumb_text = {
                      True: '✔',
                      False: '✕',
                  }
                  self._track_opacity = 1
      
          @pyqtProperty(int)
          def offset(self):
              return self._offset
      
          @offset.setter
          def offset(self, value):
              self._offset = value
              self.update()
      
          def sizeHint(self):  # pylint: disable=invalid-name
              return QSize(
                  4 * self._track_radius + 2 * self._margin,
                  2 * self._track_radius + 2 * self._margin,
              )
      
          def setChecked(self, checked):
              super().setChecked(checked)
              self.offset = self._end_offset[checked]()
      
          def resizeEvent(self, event):
              super().resizeEvent(event)
              self.offset = self._end_offset[self.isChecked()]()
      
          def paintEvent(self, event):  # pylint: disable=invalid-name, unused-argument
              p = QPainter(self)
              p.setRenderHint(QPainter.Antialiasing, True)
              p.setPen(Qt.NoPen)
              track_opacity = self._track_opacity
              thumb_opacity = 1.0
              text_opacity = 1.0
              if self.isEnabled():
                  track_brush = self._track_color[self.isChecked()]
                  thumb_brush = self._thumb_color[self.isChecked()]
                  text_color = self._text_color[self.isChecked()]
              else:
                  track_opacity *= 0.8
                  track_brush = self.palette().shadow()
                  thumb_brush = self.palette().mid()
                  text_color = self.palette().shadow().color()
      
              p.setBrush(track_brush)
              p.setOpacity(track_opacity)
              p.drawRoundedRect(
                  self._margin,
                  self._margin,
                  self.width() - 2 * self._margin,
                  self.height() - 2 * self._margin,
                  self._track_radius,
                  self._track_radius,
              )
              p.setBrush(thumb_brush)
              p.setOpacity(thumb_opacity)
              p.drawEllipse(
                  self.offset - self._thumb_radius,
                  self._base_offset - self._thumb_radius,
                  2 * self._thumb_radius,
                  2 * self._thumb_radius,
              )
              p.setPen(text_color)
              p.setOpacity(text_opacity)
              font = p.font()
              font.setPixelSize(1.5 * self._thumb_radius)
              p.setFont(font)
              p.drawText(
                  QRectF(
                      self.offset - self._thumb_radius,
                      self._base_offset - self._thumb_radius,
                      2 * self._thumb_radius,
                      2 * self._thumb_radius,
                  ),
                  Qt.AlignCenter,
                  self._thumb_text[self.isChecked()],
              )
      
          def mouseReleaseEvent(self, event):  # pylint: disable=invalid-name
              super().mouseReleaseEvent(event)
              if event.button() == Qt.LeftButton:
                  anim = QPropertyAnimation(self, b'offset', self)
                  anim.setDuration(120)
                  anim.setStartValue(self.offset)
                  anim.setEndValue(self._end_offset[self.isChecked()]())
                  anim.start()
      
          def enterEvent(self, event):  # pylint: disable=invalid-name
              self.setCursor(Qt.PointingHandCursor)
              super().enterEvent(event)
      
      
      def main():
          app = QApplication([])
      
          # Thumb size < track size (Gitlab style)
          s1 = Switch()
          s1.toggled.connect(lambda c: print('toggled', c))
          s1.clicked.connect(lambda c: print('clicked', c))
          s1.pressed.connect(lambda: print('pressed'))
          s1.released.connect(lambda: print('released'))
          s2 = Switch()
          s2.setEnabled(False)
      
          # Thumb size > track size (Android style)
          s3 = Switch(thumb_radius=11, track_radius=8)
          s4 = Switch(thumb_radius=11, track_radius=8)
          s4.setEnabled(False)
      
          l = QHBoxLayout()
          l.addWidget(s1)
          l.addWidget(s2)
          l.addWidget(s3)
          l.addWidget(s4)
          w = QWidget()
          w.setLayout(l)
          w.show()
      
          app.exec()
      
      
      if __name__ == '__main__':
          main()
      

      【讨论】:

      • 如何适应 PySide2 ?
      • 如何适配C++?
      • 没关系。我用 C++ 重新实现了你的代码。感谢您提供这个不错的解决方案。
      • @stefan 是否有关于如何将此小部件集成到 Qt Designer 中的指南?
      • 不,很遗憾没有。我不使用设计器。
      【解决方案4】:

      @piccy 的建议是我之前为这样的拨动开关所做的。通过一些调整。

      我们必须模拟类似于 iOS 开/关开关的行为。这意味着您需要一个渐进的运动,而滑块的限制为 0-1,没有外部动画。

      因此我所做的是将滑块的值范围设置为与滑块的最大宽度相同。

      然后连接滑块释放信号并检查该值是否小于最大值的一半,如果是,则将滑块值设置为 0,否则将滑块值设置为最大值。

      这将为您提供良好的拖动效果,并在您释放鼠标时剪辑到极端。

      如果您希望滑块在单击另一侧时仅切换而没有任何拖动连接滑块值更改信号并检查新值是否更接近任一极端并将其设置为滑块值如果滑块未处于关闭状态。如果滑块向下,请勿更改滑块值,因为您可能会中断先前的拖动动作。

      【讨论】:

      • 好人!您在此处分享的技巧可以实现漂亮的“快速结束”效果!我已经添加了相同的代码,以便人们可以更好地理解它。
      【解决方案5】:

      您也可以使用 QSlider 控件在水平方向上执行此操作,范围为 0 到 1。您可能希望将其最大宽度设置为 50 左右,以防止它在宽度上拉伸的对话框。然后,您可以使用样式表对其进行调整以改善外观,或者将其子类化并自己绘制控件。让它看起来不错可能不需要太多代码。

      【讨论】:

        【解决方案6】:

        好吧,你必须使用QCheckBox。它不是拨动开关,但它做同样的事情。如果您真的想要不同的视觉效果,则必须创建自定义小部件

        【讨论】:

          【解决方案7】:

          Davita 是正确的in his answer,它涉及复选框。但是,如果您正在寻找与第三个示例类似的东西(开/关开关),您可以简单地使用两个QPushButtons 并将它们设置为checkable。同时让他们autoexclusive,你应该很高兴。

          通过一点视觉造型using a stylesheet,你应该能够接近,如果不是现场的话。

          【讨论】:

          • 你会说并排安排两个按钮并修改样式表会比引入 QML 对象更优雅吗?我正在尝试了解获得与 Toggle Switch 相同外观和感觉的最佳方法。
          • 更优雅,我真的不能告诉你。我对 Qt 的 QML 部分没有太多经验。 QPushbutton 解决方案应该真的很简单,所以这将是我要走的路。但这只是基于我对 Qt(C++ 方面)的经验和对 QML 的缺乏经验。
          • 从这样的开关中,人们可能会期待一个动画转换,而这两个 QPushButtons 不提供。 (取而代之的是他们自带,这在这种情况下会令人困惑)
          • @FrankOsterfeld 想澄清一下?你会期待什么样的“动画过渡”?我是否在某个地方漏掉了一点?
          【解决方案8】:

          这是@Stefan Scherfke 答案的 C++ 实现,它是@IMAN4K 答案的 Python 3 / PyQt5 实现。除了 Stefan Scherfke 添加的改进之外,我还添加了一些改进。

          切换按钮根据给定构造函数的参数有2种不同的设计。

          explicit ToggleButton(int trackRadius, int thumbRadius, QWidget* parent = nullptr);
          

          在第一个设计中(trackRadius>thumbRadius),球(拇指)半径小于滑块(轨道)半径,因此球在滑块内根据开/关状态,包括勾选“✔”和取消勾选“✕”标记。

          在第二种设计中(trackRadius),球(拇指)半径大于滑道(轨道)半径,一个扁平的胖球滑过轨道并根据开/关情况改变颜色。

          我在 QChar 类型中使用了 U+2714 Heavy Check 和 U+2715 unCheck 标记作为 UTF-16,以免在源代码中产生编码问题。

          由于drawText函数提供的抗锯齿问题,我用DrawPath函数绘制了Heavy Check“✔”和Uncheck“✕”标记。

          绘图路径

          绘制文本

          但是由于drawPath函数不能对齐,所以我根据这张图片手动计算了像素偏移量来提供对齐。


          ToggleButton.h

          #ifndef TOGGLEBUTTON_H
          #define TOGGLEBUTTON_H
          
          #include <QtWidgets/QAbstractButton>
          
          
          class QPropertyAnimation;
          
          class ToggleButton :public QAbstractButton
          {
              Q_OBJECT
                  Q_PROPERTY(int mOffset READ offset WRITE setOffset);
          public:
              explicit ToggleButton(int trackRadius, int thumbRadius, QWidget* parent = nullptr);
              ~ToggleButton();
          
              QSize sizeHint() const override;
          
          protected:
              void paintEvent(QPaintEvent *) override;
              void resizeEvent(QResizeEvent*) override;
              void mouseReleaseEvent(QMouseEvent  *) override;
              void enterEvent(QEvent *event) override;
              void setChecked(bool checked);
          
              int offset();
              void setOffset(int value);
          
          private:
              qreal mOffset;
              qreal mBaseOffset;
              qreal mMargin;
              qreal mTrackRadius;
              qreal mThumbRadius;
              qreal mOpacity;
              QPropertyAnimation* mAnimation;
          
              QHash<bool, qreal> mEndOffset;
              QHash<bool, QBrush> mTrackColor;
              QHash<bool, QBrush> mThumbColor;
              QHash<bool, QColor> mTextColor;
              QHash<bool, QString> mThumbText;
          
          };
          
          #endif // TOGGLEBUTTON_H
          

          ToggleButton.cpp

          #include "toggleButton.h"
          #include <QtCore/QEvent>
          #include <QtCore/QCoreApplication>
          #include <QtCore/QPropertyAnimation> 
          #include <QtGui/QPainter> 
          #include <QtGui/QMouseEvent> 
          
          ///<summary>
          /// Toggle button has 2 different design. In the first design, if the ball (thumb) radius is 
          /// larger than the slide (track) radius, a flat ball slides and colors from the slide according to 
          /// the on / off situation. In the second design, if the ball radius is smaller than the slide radius, 
          /// the ball moves according to the on / off status inside the slide and includes the check and uncheck marks.
          ///</summary>
          ToggleButton::ToggleButton(int trackRadius, int thumbRadius, QWidget* parent)
              : QAbstractButton(parent)
          {
              setCheckable(true);
              setSizePolicy(QSizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Fixed));
              mTrackRadius = trackRadius;
              mThumbRadius = thumbRadius;
              mAnimation = new QPropertyAnimation(this);
              mAnimation->setTargetObject(this);
          
              mMargin = 0 > (mThumbRadius - mTrackRadius) ? 0 : (mThumbRadius - mTrackRadius);
              mBaseOffset = mThumbRadius > mTrackRadius ? mThumbRadius : mTrackRadius;
              mEndOffset.insert(true, 4 * mTrackRadius + 2 * mMargin - mBaseOffset); // width - offset
              mEndOffset.insert(false, mBaseOffset);
              mOffset = mBaseOffset;
              QPalette palette = this->palette();
          
              if (mThumbRadius > mTrackRadius)
              {
                  mTrackColor.insert(true, palette.highlight());
                  mTrackColor.insert(false, palette.dark());
                  mThumbColor.insert(true, palette.highlight());
                  mThumbColor.insert(false, palette.light());
                  mTextColor.insert(true, palette.highlightedText().color());
                  mTextColor.insert(false, palette.dark().color());
                  mThumbText.insert(true, "");
                  mThumbText.insert(false, "");
                  mOpacity = 0.5;
              }
              else
              {
                  mTrackColor.insert(true, palette.highlight());
                  mTrackColor.insert(false, palette.dark());
                  mThumbColor.insert(true, palette.highlightedText());
                  mThumbColor.insert(false, palette.light());
                  mTextColor.insert(true, palette.highlight().color());
                  mTextColor.insert(false, palette.dark().color());
                  mThumbText.insert(true, QChar(0x2714)); // check character
                  mThumbText.insert(false, QChar(0x2715)); // uncheck character
                  mOpacity = 1.0;
              }
          }
          
          
          ToggleButton::~ToggleButton()
          {
              delete mAnimation;
          }
          
          QSize ToggleButton::sizeHint() const
          {
              int w = 4 * mTrackRadius + 2 * mMargin;
              int h = 2 * mTrackRadius + 2 * mMargin;
          
              return QSize(w, h);
          }
          
          void ToggleButton::paintEvent(QPaintEvent *)
          {
              QPainter p(this);
              QPainter::RenderHints m_paintFlags = QPainter::RenderHints(QPainter::Antialiasing |
                  QPainter::TextAntialiasing);
              p.setRenderHints(m_paintFlags, true);
              p.setPen(Qt::NoPen);
              bool check = isChecked();
              qreal trackOpacity = mOpacity;
              qreal textOpacity = 1.0;
              qreal thumbOpacity = 1.0;
              QBrush trackBrush;
              QBrush thumbBrush;
              QColor textColor;
          
              if (this->isEnabled())
              {
          
                  trackBrush = mTrackColor[check];
                  thumbBrush = mThumbColor[check];
                  textColor = mTextColor[check];
              }
              else
              {
                  trackOpacity *= 0.8;
                  trackBrush = this->palette().shadow();
                  thumbBrush = this->palette().mid();
                  textColor = this->palette().shadow().color();
              }
          
              p.setBrush(trackBrush);
              p.setOpacity(trackOpacity);
              p.drawRoundedRect(mMargin, mMargin, width() - 2 * mMargin, height() - 2 * mMargin, mTrackRadius, mTrackRadius);
          
              p.setBrush(thumbBrush);
              p.setOpacity(thumbOpacity);
              p.drawEllipse(mOffset - mThumbRadius, mBaseOffset - mThumbRadius, 2 * mThumbRadius, 2 * mThumbRadius);
          
              p.setPen(textColor);
              p.setOpacity(textOpacity);
              QFont font = p.font();
              font.setPixelSize(1.5*mThumbRadius);
              p.setFont(font);
          
          
              // Since the antialiasasing provided by the drawText function is incompetent,
              // DrawPath function preferred. But since the drawPath function is not capable of aligment,
              // Pixel offsets calculated to provide aligment.
              QPainterPath textPath;
              qreal pixelOffset = (qreal)mThumbRadius * (1 - 1 / 1.414);
              textPath.addText(mOffset - mThumbRadius + pixelOffset, mBaseOffset + mThumbRadius - pixelOffset, font, mThumbText.value(check));
              p.drawPath(textPath);
          
          
              /*p.drawText(QRectF(mOffset - mThumbRadius,
                  mBaseOffset - mThumbRadius,
                  2 * mThumbRadius,
                  2 * mThumbRadius),
                  Qt::AlignCenter,
                  mThumbText.value(check));*/
          }
          
          void ToggleButton::resizeEvent(QResizeEvent* e)
          {
              QAbstractButton::resizeEvent(e);
              mOffset = mEndOffset.value(isChecked());
          }
          
          void ToggleButton::mouseReleaseEvent(QMouseEvent  *e)
          {
              QAbstractButton::mouseReleaseEvent(e);
              if (e->button() == Qt::LeftButton)
              {
                  mAnimation->setDuration(120);
                  mAnimation->setPropertyName("mOffset");
                  mAnimation->setStartValue(mOffset);
                  mAnimation->setEndValue(mEndOffset[isChecked()]);
                  mAnimation->start();
              }
          }
          
          void ToggleButton::enterEvent(QEvent * event)
          {
              setCursor(Qt::PointingHandCursor);
              QAbstractButton::enterEvent(event);
          }
          
          void ToggleButton::setChecked(bool checked)
          {
              QAbstractButton::setChecked(checked);
              mOffset = mEndOffset.value(checked);
          }
          
          int ToggleButton::offset()
          {
              return mOffset;
          }
          
          void ToggleButton::setOffset(int value)
          {
              mOffset = value;
              update();
          }
          

          ma​​in.cpp

          #include "toggleButton.h"
          #include <QtWidgets/QApplication>
          #include <QtWidgets/QHBoxLayout>
          
          int main(int argc, char *argv[])
          {
              QApplication a(argc, argv);
              QWidget *widget = new QWidget;
              widget->setWindowFlags(Qt::FramelessWindowHint);
              QHBoxLayout layout;
              widget->setLayout(&layout);
              ToggleButton *toggleButton1 = new ToggleButton(10, 8);
              ToggleButton *toggleButton2 = new ToggleButton(10, 12);
              layout.addWidget(toggleButton1);
              layout.addWidget(toggleButton2);
              widget->show();
              return a.exec();
          }
          

          【讨论】:

          • 你应该改写非虚拟的setChecked nextCheckState
          • 编辑:不,使用nextCheckState() 会杀死动画,idky。
          • 它在这里工作,但从长远来看,覆盖虚函数would be better
          【解决方案9】:

          另请参阅 QRadioButtonQPushButtoncheckable,并且可以制作一些样式表或自定义绘图,例如“开/关开关切换”

          【讨论】:

            【解决方案10】:

            我知道这个帖子很老了,但是尽管 Viv 给出了很好的提示,但我在这个特殊问题上遇到了很多困难。

            无论如何,我想我会在这里分享我想出的解决方案,也许它会帮助其他人。

            void Switch::on_sldSwitch_actionTriggered(int action) {
                if(action != 7) ui->sldSwitch->setValue((action%2) ? 100 : 0);
            }
            
            void Switch::on_sldSwitch_sliderReleased() {
                ui->sldSwitch->setValue((ui->sldSwitch->sliderPosition() >= 50) ? 100 : 0);
            }
            

            一点解释:每次单击或使用键盘移动滑块时,都会调用actionTriggered。当它被拖动时,它会发出信号'7'。为避免立即捕捉,操作 7 被阻止。

            当向右移动时,它在点击键盘时发出 3,在敲击键盘上的“右”(或“下”)时发出 1,这就是为什么当它不是偶数时我们会向右对齐。

            向左移动时,发射 2 或 4。

            sliderReleased() 将在您拖动鼠标后松开鼠标按钮时被调用,但是此时,滑块仍具有其旧值(这让我很不爽)。所以,为了获得正确的捕捉位置,我查询了sliderPosition 而不是value,就是这样。

            我希望这对某人有所帮助。

            【讨论】:

              猜你喜欢
              • 2021-02-02
              • 1970-01-01
              • 2020-10-03
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-02-18
              相关资源
              最近更新 更多