1

【VS Code 与 Qt6】QCheckBox的图标为什么不会切换? - 东邪独孤

 1 year ago
source link: https://www.cnblogs.com/tcjiaan/p/17453721.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【VS Code 与 Qt6】QCheckBox的图标为什么不会切换?

本篇专门扯一下有关 QCheckBox 组件的一个问题。老周不水字数,直接上程序,你看了就明白。

#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QCheckBox>
#include <QVBoxLayout>
#include <QIcon>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    // 最平庸的窗口
    QWidget window;
    window.setWindowTitle("看看这是?");
    window.setGeometry(/*坐标*/600, 450, /*大小*/280, 170);
    // 布局
    QVBoxLayout *layout = new QVBoxLayout;
    window.setLayout(layout);
    // 控件列表
    QCheckBox *cb = new QCheckBox(&window);
    cb -> setText("看,左边的图标不会变");
    layout -> addWidget(cb);
    QPushButton *btn = new QPushButton(&window);
    btn->setText("看,左边的图标会变");
    // 让按钮支持check操作
    btn->setCheckable(true);
    layout->addWidget(btn);
    // 图标
    QIcon icon;
    // 第一个图,当checked的时候显示
    icon.addFile("1.png", QSize(), QIcon::Normal, QIcon::On);
    // 第二个图,当unchecked的时候显示
    icon.addFile("2.png", QSize(), QIcon::Normal, QIcon::Off);
    // 应用图标
    cb->setIcon(icon);
    btn->setIcon(icon);
    
    // 显示窗口
    window.show();

    return QApplication::exec();
}

QCheckBox、QRadioButton、QPushButton 都是 QAbstractButton 的子类,所以这几个家伙都归属于按钮组件。在 QAbstractButton 类中已定义有 checkable 属性,表示按钮是否支持 check 操作。这种按钮就类似于现实世界中的自锁按钮——按一下【开】,再按一下【关】。而普通的按钮是无状态记忆的,按下去+弹起来为一个周期,称为 Click。

由于是派生关系,所以 QCheckBox 和 QPushButton 类都会继承 check 功能的支持,只是 QCheckBox 默认是打开这项功能的。QPushButton 类默认不能 check,所以得调用 setCheckable 方法手动打开功能。

   btn->setCheckable(true);

下面重点扯扯 QIcon 这货,看名识义,你能猜到它表示的是图标。QIcon 类可以根据不同状态添加多个图标。老周这个例子是添加了两个 PNG 图标。懒得生成什么鬼 Qrc 资源了,直接把图片文件复制到程序可执行文件所在的目录就完事了。比如,build/Debug,只要和可执行文件在同一个目录就行,方便相对路径引用。

    QIcon icon;
    // 第一个图,当checked的时候显示
    icon.addFile("1.png", QSize(), QIcon::Normal, QIcon::On);
    // 第二个图,当unchecked的时候显示
    icon.addFile("2.png", QSize(), QIcon::Normal, QIcon::Off);

这里涉及到 QIcon 类定义的两枚举:

A、Mode:

1)Normal —— 组件的正常状态;

2)Disbaled —— 被禁用,比如按钮不能点击;

3)Active —— 其实和 Normal 差不多,只是多了一条:正在与用户交互。比如活动窗口,获得焦点的按钮,获得焦点的输入框等;

4)Selected —— 这个有些奇葩,一般在子项对象起作用。比如,ListView 视图的子项被选中时。

B、State:

1)On —— 状态“开”。比如 CheckBox 处于 checked 状态;

2)Off —— 状态“关”。如 CheckBox 的 Unchecked 状态。

回到咱们的代码。这两个图标的区别就在 State 的值不同,当 On 时显示 1.png;当状态为 Off 时显示 2.png。

    icon.addFile("1.png", QSize(), QIcon::Normal, QIcon::On);
    icon.addFile("2.png", QSize(), QIcon::Normal, QIcon::Off);

addFile 的第一个参数是文件路径,第二个参数指定图标的大小,这里用 QSize 类的默认构造函数,即宽和高都是 -1,这样图标会根据样式获取默认大小。

运行程序,咱们测试下。如下图所示,这是初始状态,上下两个组件都没有 check。

367389-20230603121228018-967988212.png

然后,咱们依次点击它们,让这两个组件都处于 checked 状态。

367389-20230603121343375-1018302337.png

咱们看到:QPushButton 切换状态后图标也跟着变了,但是 QCheckBox 一点动静都没有。这 NM 是怎么回事?

原来,QPushButton 的默认样式中,在获取图标时,会根据 checked 状态来提取 On 或 Off 相关的图标。

 case CE_PushButtonLabel:
        if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton *>(opt)) {
            ……

            if (!button->icon.isNull()) {
                //Center both icon and text
                QIcon::Mode mode = button->state & State_Enabled ? QIcon::Normal : QIcon::Disabled;
                if (mode == QIcon::Normal && button->state & State_HasFocus)
                    mode = QIcon::Active;
                QIcon::State state = QIcon::Off;
                if (button->state & State_On)
                    state = QIcon::On;

                ……
        break;

再看看 QCheckBox 的样式,发现这厮在获取图标时是不考虑 On 或 Off 状态的。

    case CE_RadioButtonLabel:
    case CE_CheckBoxLabel:
        ……
            if (!btn->icon.isNull()) {
                pix = btn->icon.pixmap(btn->iconSize, p->device()->devicePixelRatio(), btn->state & State_Enabled ? QIcon::Normal : QIcon::Disabled);
                ……
            }
        }
        break;

为什么 QCheckBox 的图标不会随着 check 状态改变,这下找到答案了。

不过,人家 Qt 这样设计也没毛病的。毕竟 QCheckBox 前面有个勾勾,已经可以向用户指示状态了,就没必要换图标了。

那么,这个用 QSS (Qt 样式表)能解决吗?Qt 说了,目前 QStyle 和 QSS 是不兼容的,以后的版本可能会实现。估计也是说说而已。

所以,有效的解决方案时自己写个 Style 类,不用覆盖所有组件的样式,只要覆盖 CE_CheckBoxLabel 部分,自己手动绘制图标和文本就行了。如果你想在 QRadioButton 组件中也用上,当然得同时覆盖 CE_RadioButtonLabel 部分。

    case CE_RadioButtonLabel:
    case CE_CheckBoxLabel:
        ……
        break;

XXXLabel 指的就是绘制此组件的标签——即文本(或文本+图标)部分,前文的 CE_PushButtonLabel 常量也是此意,绘制 QPushButton 组件的标签部分。

要部分覆盖样式,应当从 QProxyStyle 类派生。这里只需要重写 drawControl 方法就够了,它的签名为:

virtual void drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w = nullptr) const = 0;

这方法的原始声明是在 QStyle 类中,而且是纯虚函数,派生类必须重写它。QCommonStyle 和 QProxyStyle 类都重写过。如今,咱们要自定义样式,也要重写它。

element 参数表示当前要绘制组件(控件)的哪个部分,例如,CE_PushButtonLabel 表示要画按钮的标签部分,CE_ProgressBarLabel 表示要画进度条的标签部分。

opt 参数是 QStyleOption 的子类,提供绘制样式所需的数据,如按钮上显示啥文本,组件的大小,组件是否处于禁用状态。

p 是 QPainter,用来绘图。

w 是 QWidget 或其子类,就是引用相关的组件对象,许多时候咱们可以不管。有时候可能要从组件上获取额外的数据时用到。

下面代码重写 QCheckBox 的标签绘制过程。

class MyStyle : public QProxyStyle
{
    Q_OBJECT
public:
    void drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget = nullptr) const override
    {
        if (element == CE_CheckBoxLabel)
        {
            const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option);
            QRect rect = btn->rect;
            qDebug() << "可用大小:" << rect;
            // 有图标
            if (btn->icon.isNull() == false)
            {
                // 组件是否在可用状态?
                QIcon::Mode mode = btn->state & State_Enabled ? QIcon::Normal : QIcon::Disabled;
                // 组件是否已经 checked ?
                QIcon::State state = btn->state & State_On ? QIcon::On : QIcon::Off;
                // 显示缩放因数
                qreal pRatio = painter->device()->devicePixelRatio();
                // 获取图标
                QPixmap theIcon = btn->icon.pixmap(
                    btn->iconSize,
                    pRatio,
                    mode,
                    state);
                qDebug() << "图像原始尺寸:" << theIcon.width() << ", " << theIcon.height();
                // 这里得换算一下
                int iconWidth = theIcon.width() / pRatio;
                int iconHeight = theIcon.height() / pRatio;
                qDebug() << "缩放后图标大小:" << iconWidth << ", " << iconHeight;
                // 计算图标的原点
                int iconX = rect.x();
                int iconY = (rect.height() - iconHeight) / 2 - 1;
                // 画图标
                painter->drawPixmap(
                    QRect(iconX, iconY, iconWidth, iconHeight),
                    theIcon);
                // 可用空间减小一下(因为图标占了一些空间)
                rect.adjust(iconWidth + 2, 0, 0, 0);
            }
            // 有没有文本?
            if (!btn->text.isNull())
            {
                // 水平:左对齐;垂直:居中对齐
                auto alig = Qt::AlignLeft | Qt::AlignVCenter;
                // 算算文本所占空间
                QRect txtRect = painter->fontMetrics().boundingRect(rect, alig, btn->text);
                painter->drawText(txtRect, alig, btn->text);
            }
            return;
        }
        // 其他的保持默认,交给基类去干就行了
        QProxyStyle::drawControl(element, option, painter, widget);
    }
};

这里咱们只关心 CE_CheckBoxLabel,其他值都交给基类处理。在计算图标尺寸的时候,涉及到显示缩放比例。

   qreal pRatio = painter->device()->devicePixelRatio();

假设图标的默认尺寸是 16×16,但要是显示比例是 125%,即 16×1.25 = 20 ==> 20×20。这个显示比例可在系统设置里调整。

367389-20230603171830414-1549337696.png

如果你不希望图标的大小受显示比例干扰,那么需要注意这个比例值了。比如,老周这里设置的是 125%,那么,从 QIcon 中获取到的 QPixmap 对象的大小是 20 像素。所以,要让它还原为 16 像素,就就除以显示比例(devicePixelRatio)。

    int iconWidth = theIcon.width() / pRatio;
    int iconHeight = theIcon.height() / pRatio;

当然了,如果你希望图标也跟着缩放,那就不用换算了。

在绘制完图标后,要用 adjust 方法把矩形对象的 X 坐标向右移,因为图标占了亿点空间,文本只能在图标右边绘制。

  rect.adjust(iconWidth + 2, 0, 0, 0);

四个参数分别指定矩形对角线上两个点的坐标变化量。注意是变化量,不是坐标值。比如,adjust( 10, 15, -28, 5 ),意思是:

左上角:X 坐标 +10,Y 坐标 +15;

右下角:X 坐标 -28,Y 坐标 +5。

painter->fontMetrics().boundingRect 方法很好用的,它可以计算出要画的文本所占的空间(QRect 对象,用矩形描述)。
 QRect txtRect = painter->fontMetrics().boundingRect(rect, alig, btn->text);

然后,写个自定义 QWidget 类,作为顶层窗口,试试自定义样式。

class MyWindow : public QWidget
{
    Q_OBJECT

public:
    explicit MyWindow()
        : QWidget(nullptr)
    {
        setWindowTitle("Demo");
        resize(200, 80);
        // 布局
        m_layout = new QVBoxLayout();
        setLayout(m_layout);
        m_ck1 = new QCheckBox("选项一", this);
        m_ck2 = new QCheckBox("选项二", this);
        // 给这俩checkbox弄弄样式
        auto style = new MyStyle;
        // 把样式对象纳入到Qt对象树中
        // 内存清理时会自动打扫,防止泄漏
        style->setParent(this);
        m_ck1->setStyle(style);
        m_ck2->setStyle(style);
        m_layout->addWidget(m_ck1);
        m_layout->addWidget(m_ck2);
        // 设置图标
        QIcon icon;
        icon.addFile("1.png", QSize(), QIcon::Normal, QIcon::On);
        icon.addFile("2.png", QSize(), QIcon::Normal, QIcon::Off);
        m_ck1->setIcon(icon);
        m_ck2->setIcon(icon);
    }

private:
    QVBoxLayout *m_layout;
    QCheckBox *m_ck1, *m_ck2;
};

由于这个自定义样式只是针对 QCheckBox 组件,没有必要应用到 QApplication 上,直接用在 QCheckBox上就好了。哦,不要用在 QWidget 实例上,不起作用的。原因请看:

void QWidget::paintEvent(QPaintEvent *)
{
}

QWidget 类对 paint 事件是没做任何处理的,所以不会触发样式类中的各类绘制方法,除非你重写 paintEvent 方法,手动触发。所以说,咱们直接把样式应用在 QCheckBox 就很合适,QCheckBox 类是处理 paintEvent 的。

void QCheckBox::paintEvent(QPaintEvent *)
{
    QStylePainter p(this);
    QStyleOptionButton opt;
    initStyleOption(&opt);
    p.drawControl(QStyle::CE_CheckBox, opt);
}

// QStyleOptionButton 的数据在这里收集
void QCheckBox::initStyleOption(QStyleOptionButton *option) const
{
    if (!option)
        return;
    Q_D(const QCheckBox);
    // 从当前QCheckBox实例提取基本数据
    option->initFrom(this);
    // 处于被鼠标按下状态
    if (d->down)
        option->state |= QStyle::State_Sunken;
   // 除了 checked 和 unchecked 外,还存在第三状态
   // 一种未明确状态,介于 checked 与 unchecked 之间 
   if (d->tristate && d->noChange)
        option->state |= QStyle::State_NoChange;
    else
        option->state |= d->checked ? QStyle::State_On : QStyle::State_Off;
    if (testAttribute(Qt::WA_Hover) && underMouse()) {
        option->state.setFlag(QStyle::State_MouseOver, d->hovering);
    }
    // 显示的文本
    option->text = d->text;
    // 显示的图标
    option->icon = d->icon;
    // 图标的大小
    option->iconSize = iconSize();
}

在实现自定义样式时,咱们在 drawControl 方法中读到的数据就是这么来的。

写上 main 函数,试一下水。

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    MyWindow wind;
    wind.show();
    return QApplication::exec();
}

运行,验证。

367389-20230603175857954-2068293071.gif

这回有效果了,图标会自动切换了。是咱们想要的。

顺便提一句,如果你自定义的类,如上面的 MyStyle,MyWindow 类,这些 QObject 的子类,或包含 Q_OBJECT 宏的类。你如果没有在头文件中定义,而是像老周刚才那样,直接写在 cpp 文件中,那么,在类定义的代码后面要加一行(假设代码文件是 app.cpp):

class MyStyle : public QProxyStyle
{
    ……
};

class MyWindow : public QWidget
{
    ……
};

#include "app.moc"

int main(int argc, char **argv)
{
    ……
}

这个与 MOC(Meta Object Compiler) 功能是对应的,Qt 定义了许多奇葩语法,这些不是标准 C++ 里面的,在正式编译之前,要把这些奇葩语法还原为标准 C++ 语法,所以才会有生成 xxx.moc 文件。moc 文件其实是C++代码,同时还生成一个 xxx.moc.d 文件,里面是源代码文件引用到的头文件列表。

======================================================

最后,老周分享一个 VS Code 加背景图的技巧。由于 VS Code 的UI是HTML + CSS弄的,所以,通过修改 CSS 文件可以给它加个背景图。你如果嫌麻烦,可以安装 background 插件,直接配置。background - Visual Studio Marketplace

当然,不想用插件,就自己改。网上也有不少教程,不过,老周这里重点说一下文件路径格式,因为网上说的那些方法不适合新版本。

在VS Code 所在目录(不管你是安装版还是便携版)resources \ app \ out \ vs \ workbench 下面,你会看到一个名为 workbench.desktop.main.css 的文件。这个文件里就是 VS Code 主界面的样式表了。如果怕搞坏了,可以备份一个,然后再改。把这个文件直接扔进 VS Code 里面编辑,可以将其格式化一下,这样好看亿点。

自定义背景图改 body 元素的样式就好了,这样图片可以覆盖整窗口。改其他地方效果都不好,而且有些背景改了也没用,VS Code 运行的时候会被主题参数覆盖。因此,改 body 的样式是目前最方便的方法。你可以直接在 CSS 文件末尾直接写上 body 样式。也可以通过查找“body {” 找到默认的样式,直接在后面追加属性。

body {
    ……
    /* 下面为自定义内容 */
    opacity: 0.85;
    background-image: url('vscode-file://vscode-app/C:/Users/DXC/Pictures/bg/v2.png');
    background-repeat: no-repeat;
    background-position: center;
    background-size:cover;
    background-attachment: fixed; 
}

你直接照抄就行,注意 background-image 的地方改成你自己的图片路径。网上的教程用的是 file:///c:/users/xxx/pictures/ 这样的路径,这个新版本不支持的,协议要改为 vscode-file:,然后路径要使用 vscode-app/ 前缀,之后才是图片的路径。

推荐选一张尺寸大一点的图片,这样既可以看得清楚,也可以覆盖整个窗口。opacity 属性设置背景的透明,这个不要弄成 100%,这样全不透明,看不到背景图片了。也不推荐低于 70%,因为太透明了,代码都看不清楚了,写代码容易近视。最好选 85 - 90% 之间,即 0.85 到 1.0。

老周知道,很多大伙伴都喜欢放动画人物,尤其是猛男们都喜欢放一些魔法少女(水手服的就……,小时候觉得不太好看,现在也觉得不太好看,当然,仅代表个人看法)。为了表示对国产动画的支持,尤其是国产魔法少女系列,老周推荐这个:

367389-20230603184655041-1682638118.png

B 站上面不少猛男喜欢美雪,其实老周也喜欢,所有女生该有的优点她都有,学霸小淑女。

奥飞可能玩具卖得不太好,最近 15 周年的活动还搞得挺带劲。上次去电器城看空调,广告大屏幕里还放这个,超市里也放。

国产动画起步晚,老周小时候看的 99.9% 是日本的,国产的只看过《哪吒》。在为数不多的国产动画(特别魔法少女)里面,真的算是起步就是巅峰了。但说句难听的,衰败也很快,也就是真人版 + 前三部还可以,后面真的不行了,花里花哨,主题也太水。大电影嘛,反正老周不喜欢,都是给那些什么鬼女团做广告的,特效也是敷衍。

有人要说了,动画就是子供向的,就那样了,还要求什么。那可不,以前一些给小娃娃看的动画也设计得很好的,主题思想也明确,情节略带些深度。就像《奈克瑟斯奥特曼》一样,那可不一定小朋友就能看懂的。作为文艺作品,动画只是一样表现形式,核心价值观和思想主题也必须体现,不然还有观赏的意义吗?想一想迪迦、麦克斯、盖亚里面,有几集小时候都看不懂的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK