C++ Qt 中的切换开关

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14780517/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 18:41:28  来源:igfitidea点击:

Toggle Switch in Qt

c++qtuser-interface

提问by ElCraneo

I am trying to use an element which is the equivalent of Android Switchesin Qt. I have found a ToggleSwitch in QML, but nothing in the actual C++ Qt libs. Am I just missing something or will I have to reimplement this widget myself?

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

采纳答案by Viv

@piccy's suggestion is what I've done for such a toggle switch previously. With a few tweaks tho.

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

We had to emulate the behaviours similar to the iOS on/off switches. Meaning you needed a gradual movement which you won't have with slider being with a limit of 0-1 without external animations.

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

Hence what I did was set the value range for the slider to be the same as the max width of the slider.

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

Then connect the slider released signal and check if the value is less than half the maximum and if so set slider value to 0 else slider value to max.

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

This will give you a good drag effect and clip to extremes when you release the mouse.

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

If you want the slider to just toggle when clicked on the other side without any drag connect the slider value changed signal and check the new value to be closer to either extreme and set it as the slider value if the slider is not in its down state. Do not change slider value if slider is down since you might then break the previous drag motion.

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

回答by IMAN4K

Here is an example:

下面是一个例子:

switch.h:

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.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:

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();
    }

enter image description here

在此处输入图片说明

Update Aug 20'18

2018 年 8 月 20 日更新

New Material Switch Widget!

新材质切换小工具!

style.h

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

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 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 : public QVariantAnimation {
    Q_OBJECT
    Q_PROPERTY(QObject* targetObject READ targetObject WRITE setTargetObject)

public:
    explicit Animator(QObject* target, QObject* parent = nullptr);
    ~Animator();

    QObject* targetObject() const;
    void setTargetObject(QObject* target);

    bool isRunning() const {
        return state() == Running;
    }

    void setup(int duration, QEasingCurve easing = QEasingCurve::Linear);
    void interpolate(const QVariant& start, const QVariant& end);

protected:
    void updateCurrentValue(const QVariant& value) override;
    void updateState(QAbstractAnimation::State newState, QAbstractAnimation::State oldState) override;

private:
    QPointer<QObject> target;
};

class SelectionControl :public QAbstractButton {
    Q_OBJECT

public:
    explicit SelectionControl(QWidget* parent = nullptr);
    ~SelectionControl();

    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 :public SelectionControl {
    Q_OBJECT

public:
    explicit Switch(QWidget* parent = nullptr);
    explicit Switch(const QString& text, QWidget* parent = nullptr);
    Switch(const QString& text, const QBrush&, QWidget* parent = nullptr);
    ~Switch();

    QSize sizeHint() const override;

protected:
    void paintEvent(QPaintEvent *) override;
    void resizeEvent(QResizeEvent*) override;
    void toggle(Qt::CheckState) override;

    void init();
    QRect indicatorRect();
    QRect textRect();

    static inline QColor colorFromOpacity(const QColor& c, qreal opacity) {
        return QColor(c.red(), c.green(), c.blue(), opacity * 255.0);
    }
    static inline bool ltr(QWidget* w) {
        if (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

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 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"

#define CORNER_RADIUS 8.0
#define THUMB_RADIUS 14.5
#define SHADOW_ELEVATION 2.0

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();
}



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);
        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);

        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) {
        thumbPosAniamtion->interpolate(0, (style.indicatorMargin.left() + style.indicatorMargin.right() + 2) * 2);
        thumbBrushAnimation->interpolate(colorFromOpacity(style.thumbOffBrush, style.thumbOffOpacity), colorFromOpacity(style.thumbOnBrush, style.thumbOnOpacity));
        trackBrushAnimation->interpolate(colorFromOpacity(style.trackOffBrush, style.trackOffOpacity), colorFromOpacity(style.trackOnBrush, style.trackOnOpacity));
    } else { // Qt::Unchecked
        thumbPosAniamtion->interpolate(thumbPosAniamtion->currentValue().toInt(), 0);
        thumbBrushAnimation->interpolate(colorFromOpacity(style.thumbOnBrush, style.thumbOnOpacity), colorFromOpacity(style.thumbOffBrush, style.thumbOffOpacity));
        trackBrushAnimation->interpolate(colorFromOpacity(style.trackOnBrush, style.trackOnOpacity), colorFromOpacity(style.trackOffBrush, style.trackOffOpacity));
    }
}

main.cpp

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();
}

Result:

结果:

enter image description here

在此处输入图片说明

回答by armatita

A few months ago I made an implementation whose visuals needed to be more consistent with common desktop styles (C++ and Python version available; the Python version was the prototype, i.e. they might work differently). Notice that the aesthetics is fully customized using paintEvent. Do not expect different visuals depending on system.

几个月前,我做了一个实现,它的视觉效果需要与常见的桌面风格更加一致(C++ 和 Python 版本可用;Python 版本是原型,即它们的工作方式可能不同)。请注意,美学是使用paintEvent. 不要期望不同的视觉效果取决于系统。

A switch button implementation in Qt (C++;Python)

Qt (C++;Python) 中的开关按钮实现



C++ implementation

C++ 实现

NOTE: don't forget the includes (which are not in my example).

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

Usage:

用法:

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

(...).hpp

(...).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

(...).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 implementation (Prototype; PyQt5)

Python 实现(原型;PyQt5)

Usage:

用法:

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

(...).py

(...).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()

回答by Stefan Scherfke

Here is a Python 3 / PyQt5 implementation of @IMAN4K's answer.

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

Improvements over the original implementation:

对原始实现的改进:

  • Thumb-size can be larger/smaller than the track size
  • Use current app's palette for coloring
  • Emit toggled/clicked signals when clicked
  • Various other fixes
  • 拇指大小可以大于/小于轨道大小
  • 使用当前应用的调色板进行着色
  • 单击时发出切换/单击信号
  • 各种其他修复

enter image description here

在此处输入图片说明

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()

回答by piccy

You could also do this with a QSlider control in a horizontal orientation that has a range of 0 to 1. You'd probably want to set its max width to something like 50 or so, to keep it from stretching across the width of the dialog. You could then tweak it with a style sheet to improve the appearance, or subclass it and draw the controls yourself. It might not take too much code to make it look good.

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

回答by Bart

Davita is right in his answerwhere it concerns checkboxes. If you are looking for something similar to third example however (the on/off switches) you could simply use two QPushButtons for that and set them to be checkable. Make them autoexclusiveat the same time, and you should be good to go.

戴维塔 (Davita)在他关于复选框的回答中是正确的。如果您正在寻找类似于第三个示例的东西(开/关开关),您可以简单地使用两个QPushButton并将它们设置为checkable。同时使它们自动独占,您应该很高兴。

With a bit of visual styling using a stylesheetyou should be able to get close, if not spot on.

通过使用样式表的一些视觉样式,您应该能够接近,如果没有发现。

回答by Davita

Well, you'll have to use QCheckBox. It's not a Toggle Switch, but it does same thing. If you really want different visual, you'll have to create custom widget

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

回答by Zlatomir

Also see QRadioButtonand QPushButtonwith checkableand some style-sheet or custom drawing can be made like "On/off switches toggle"

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

回答by Refugnic Eternium

I know this thread is old, but I struggled quite a bit with this particular issue despite being given a very good hint by Viv.

我知道这个帖子很旧,但尽管 Viv 给了我很好的提示,但我在这个特定问题上挣扎了很多。

Anyway, I figured I'd share the solution I came up with on here, maybe it'll help someone else along the way.

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

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);
}

A little explanation: actionTriggeredwill be called each time the slider is clicked or moved with the keyboard. When it is dragged, it will emit the signal '7'. To avoid immediate snapping, action 7 is blocked.

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

When moving to the right, it emits 3 while clicking and 1 while hitting 'right' (or 'down') on the keyboard, which is why we're snapping to the right when it's not an even number.

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

When moving left, it emits 2 or 4.

向左移动时,它会发出 2 或 4。

sliderReleased()will get called once you let go of the mouse button after dragging, however at this moment, the slider still has its old value (which tripped me up quite a bit). So, in order to get the correct position to snap to I queried sliderPositioninstead of valueand that's that.

sliderReleased()一旦你在拖动后松开鼠标按钮就会被调用,但是此时,滑块仍然具有它的旧值(这让我很不舒服)。因此,为了获得正确的位置以捕捉到我查询sliderPosition而不是仅value此而已。

I hope this helps someone.

我希望这可以帮助别人。