简介: 根据QStyle的继承关系和重绘原理;通过实现一个继承QCommonStyle类的实现,实现自己的自定义控件QSlider控件。

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境: deepin 15.11 x64 专业版 Kernel: x86_64 Linux 4.15.0-30deepin-generic

编程软件: Qt Creator 4.8.2 (Enterprise)Qt 5.9.8


系列博文:


运行效果:

先上一张最终的重绘运行效果图

运行代码,在 main()函数里面:

QApplication app(argc, argv);
    qApp->setStyle("chameleon");

    QWidget wTemp;
    wTemp.resize(800, 600);
    
//    NoTicks = 0,
//    TicksAbove = 1,
//    TicksLeft = TicksAbove,
//    TicksBelow = 2,
//    TicksRight = TicksBelow,
//    TicksBothSides = 3

	QHBoxLayout *layout = new QHBoxLayout(&wTemp);
    QSlider *slider1 = new QSlider(&wTemp);               //竖直刻度在右侧的Slider
    slider1->setOrientation(Qt::Vertical);
    slider1->setTickPosition(QSlider::TicksRight);
    slider1->setTickInterval(10);
    slider1->resize(40, 300);

    QSlider *slider2 = new QSlider(&wTemp);              //竖直刻度在左侧的Slider
    slider2->setOrientation(Qt::Vertical);
    slider2->setTickPosition(QSlider::TicksLeft);
    slider2->setTickInterval(10);
    slider2->resize(40, 300);
    slider2->move(150, 0);

    QSlider *slider3 = new QSlider(&wTemp);             //水平刻度在下侧的Slider
    slider3->setOrientation(Qt::Horizontal);
    slider3->setTickPosition(QSlider::TicksBelow);
    slider3->resize(300, 40);
    slider3->setTickInterval(10);
    slider3->move(0, 400);

    QSlider *slider4 = new QSlider(&wTemp);             //水平刻度在上侧的Slider
    slider4->setOrientation(Qt::Horizontal);
    slider4->setTickPosition(QSlider::TicksAbove);
    slider4->resize(300, 40);
    slider4->setTickInterval(10);
    slider4->move(400, 400);

    QSlider *slider5 = new QSlider(&wTemp);             //竖直刻度在两侧的Slider
    slider5->setOrientation(Qt::Vertical);
    slider5->setTickPosition(QSlider::TicksBothSides);
    slider5->resize(40, 300);
    slider5->setTickInterval(10);
    slider5->move(300, 0);

    QSlider *slider6 = new QSlider(&wTemp);            //水平刻度在两侧的Slider
    slider6->setOrientation(Qt::Horizontal);
    slider6->setTickPosition(QSlider::TicksBothSides);
    slider6->resize(300, 40);
    slider6->setTickInterval(10);
    slider6->move(0, 500);

    wTemp.show();
    return app.exec();

QSlider属性:

  • Qt文档:

先看Qt官方文档介绍,我搽,就这么两个属性???然后大致浏览完了整篇。怎么给我一点,怎么只有这么一点讲解的内容啊!!!这可不行,谁受得了啊 ,然后继续看源码,看其继承的基类QAbstractSlider等之后。这还差不多。不然我怎么使用QStyle重绘这个控件

  • tickInterval : int
    此属性保存tickmarks之间的间隔这是一个值间隔,而不是像素间隔。如果为0,滑块将在singleStep和pageStep之间进行选择。
  • tickPosition : TickPosition
    此属性保存此滑块的tickmark位置,有效值由QSlider::TickPosition enum描述。默认值是QSlider:: notice。

刻度条绘画方向:

Constant Description
QSlider::NoTicks 不显示刻度
QSlider::TicksBothSides 两边都显示刻度
QSlider::TicksAbove 刻度显示在上边(水平)
QSlider::TicksBelow 刻度显示在下边(水平)
QSlider::TicksLeft 刻度显示在左边(竖直)
QSlider::TicksRight 刻度显示在右边(竖直)

QSlider的摆放方向:

orientation : Qt::Orientation Description
Qt::Vertical 滑动条竖直绘画
Qt::Horizontal 滑动条水平绘画

其他几个涉及QStyle重绘的重要属性:

val 含义
minimum 滑动条最小值
maximum 滑动条最大值
singleStep 在min-max之间显示的步长,移动一步的改变数值
pageStep 用于计算刻度的个数(有阈值限制,不完全按这个来,没详细研究,柑橘用来有点迷);
value 当前的显示数值(在min-max之间,也是本信号的槽的参数的数值)
sliderPosition ??
tickInterval 两个刻度之间的间隔数值(重绘使用)
tickPosition 刻度的位置

注意: pageStep 这个用于计算刻度的个数(有阈值限制,不完全按这个来,没详细研究,柑橘用来有点迷); 刻度个数 - 1 = 刻度间隔的个数 = (span - 0) / pageStep; 关于0, span, min, max, val 的关系,参见sliderPositionFromValue()的实现 [此处特指qfusionstyle.cpp 里面的, 不知道qcommstyle.cpp实现原理是否相同?]


理解属性步长sigleStep、pageSteop:

因为QSlider = 滑块(句柄)+ 滑槽 + 刻度(矩形)

创建一个简单的小例子,核心代码如下:

void Widget::on_sliderHor_valueChanged(int value)
{
    int minimum = ui->sliderHor->minimum();                 //滑动条最小值
    int maximum = ui->sliderHor->maximum();                 //滑动条最大值
    int sigleStep = ui->sliderHor->singleStep();            //在min~max之间显示的步长,移动一步的改变数值
    int pageSteop = ui->sliderHor->pageStep();              //用于计算刻度的个数(有阈值限制,不完全按这个来)
    int val = ui->sliderHor->value();                      //当前的显示数值(在min~max之间,也是本信号的槽的参数的数值)
    int sliderPosition = ui->sliderHor->sliderPosition();   //?? 不是很清楚
    int tickinterval = ui->sliderHor->tickInterval();       //两个刻度之间的间隔数值(重绘使用)
    int tickPosition = ui->sliderHor->tickPosition();       //刻度的位置

    QString str = QString("value:%1,  minimum:%2,  maximum:%3,  sigleStep:%4,  pageSteop:%5,  val:%6,  sliderPosition:%7,  tickinterval:%8,  tickPosition:%9").arg(value).arg(minimum).arg(maximum).arg(sigleStep).arg(pageSteop).arg(val).arg(sliderPosition).arg(tickinterval).arg(tickPosition);
                       
    qDebug()<<str;
}

其效果如下:有qDebug可以查看每一个数值的含义,以及变化可以看出其含义(每次按下按下一个方向→或←按键);就会显示一行数据, 重点观察sigleStep、sigleStep、pageSteop的数值;

感觉其上面这些矩形,大多是使用时候,而非更底层的重绘该空间使用的;而sliderPositiontickPosition才是重绘使用得。有了上面的基础之后,下面开始正经的重绘:


重绘QSlider:

重绘思路:

  • 先预先计算好滑块(矩形)+ 滑槽(矩形) + 刻度(矩形)
  • 对该部分矩形进行绘画

计算所需要的三个部分的矩形:

QRect CustomStyle::subControlRect(QStyle::ComplexControl cc, const QStyleOptionComplex *opt,
                                     QStyle::SubControl sc, const QWidget *w) const
{
    switch (cc) {
    case CC_Slider: {
        if (const QStyleOptionSlider *option = qstyleoption_cast<const QStyleOptionSlider *>(opt)) {
            QRectF rect = option->rect;                                                    //Slider控件总的大小矩形
            int slider_size = proxy()->pixelMetric(PM_SliderControlThickness, opt, w);     //滑块的高度
//            int tick_size = proxy()->pixelMetric(PM_SliderTickmarkOffset, opt, w);         //刻度的高度
            QRectF slider_handle_rect = rect;                                              //滑块和滑漕的的最小公共矩形 (后面被用作临时且被改变的)

            if (option->orientation == Qt::Horizontal) {
                slider_handle_rect.setHeight(slider_size);
                if (option->tickPosition == QSlider::TicksAbove) slider_handle_rect.moveBottom(rect.bottom());
                if (option->tickPosition == QSlider::TicksBelow) slider_handle_rect.moveTop(rect.top());
                if (option->tickPosition == QSlider::TicksBothSides) slider_handle_rect.moveCenter(rect.center());
            } else {
                slider_handle_rect.setWidth(slider_size);
                if (option->tickPosition == QSlider::TicksRight)  slider_handle_rect.moveLeft(rect.left());
                if (option->tickPosition == QSlider::TicksLeft)   slider_handle_rect.moveRight(rect.right());
                if (option->tickPosition == QSlider::TicksBothSides) slider_handle_rect.moveCenter(rect.center());
            }

            QRectF rectStatic =  slider_handle_rect;   //rectStatic作为 滑块和滑漕的的最小公共矩形(不改变)

            switch (sc) {
            case SC_SliderGroove: {  //滑漕
                qreal groove_size = slider_size / 4.0;
                QRectF groove_rect;

                if (option->orientation == Qt::Horizontal) {
                    groove_rect.setWidth(slider_handle_rect.width());
                    groove_rect.setHeight(groove_size);
                } else {
                    groove_rect.setWidth(groove_size);
                    groove_rect.setHeight(slider_handle_rect.height());
                }

                groove_rect.moveCenter(slider_handle_rect.center());
                return groove_rect.toRect();
            }
            case SC_SliderHandle: {  //滑块
                int sliderPos = 0;
                int len = proxy()->pixelMetric(PM_SliderLength, option, w);
                bool horizontal = option->orientation == Qt::Horizontal;
                sliderPos = sliderPositionFromValue(option->minimum, option->maximum, option->sliderPosition,
                                                    (horizontal ? slider_handle_rect.width() : slider_handle_rect.height()) - len, option->upsideDown);
                if (horizontal) {
                    slider_handle_rect.moveLeft(slider_handle_rect.left() + sliderPos);
                    slider_handle_rect.setWidth(len);
                    slider_handle_rect.moveTop(rectStatic.top());
                } else {
                    slider_handle_rect.moveTop(slider_handle_rect.top() + sliderPos);
                    slider_handle_rect.setHeight(len);
                    slider_handle_rect.moveLeft(rectStatic.left());
                }

                return slider_handle_rect.toRect();
            }
            case SC_SliderTickmarks: {  //刻度的矩形
                QRectF tick_rect = rect;

                if (option->orientation == Qt::Horizontal) {
                    tick_rect.setHeight(rect.height() - slider_handle_rect.height());

                    if (option->tickPosition == QSlider::TicksAbove) {
                        tick_rect.moveTop(rect.top());
                    } else if (option->tickPosition == QSlider::TicksBelow) {
                        tick_rect.moveBottom(rect.bottom());
                    }
                } else {
                    tick_rect.setWidth(rect.width() - slider_handle_rect.width());

                    if (option->tickPosition == QSlider::TicksLeft) {
                        tick_rect.moveLeft(rect.left());
                    } else if (option->tickPosition == QSlider::TicksRight) {
                        tick_rect.moveRight(rect.right());
                    }
                }

                return tick_rect.toRect();
            }
            default:
                break;
            }
        }
        break;
    }
    default:
        break;
    }
    
    return QCommonStyle::subControlRect(cc, opt, sc, w);
}

再对其中每一个矩形(一共3个)进行重绘:

void ChameleonStyle::drawComplexControl(QStyle::ComplexControl cc, const QStyleOptionComplex *opt,
                                        QPainter *p, const QWidget *w) const
{
    switch (cc) {
    case CC_Slider : {
        if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider *>(opt)) {
            //各个使用的矩形大小和位置
            QRectF rect = opt->rect;                                                                            //Slider控件最大的矩形(包含如下三个)
            QRectF rectHandle = proxy()->subControlRect(CC_Slider, opt, SC_SliderHandle, w);                    //滑块矩形
            QRectF rectSliderTickmarks = proxy()->subControlRect(CC_Slider, opt, SC_SliderTickmarks, w);        //刻度的矩形
            QRect rectGroove = proxy()->subControlRect(CC_Slider, opt, SC_SliderGroove, w);                     //滑槽的矩形

//            qDebug()<<"____04_____Slider控件最大的矩形(包含如下三个):"<<rect<<"  滑块矩形:"<<rectHandle<<"  刻度的矩形:"<<rectSliderTickmarks<<"   滑槽的矩形:"<<rectGroove<<endl;

//            //测试(保留不删)
            p->fillRect(rect, Qt::gray);
            p->fillRect(rectSliderTickmarks, Qt::blue);
            p->fillRect(rectGroove, Qt::red);
            p->fillRect(rectHandle, Qt::green);
            qDebug()<<"---rect:"<<rect<<"  rectHandle:"<<rectHandle<<"   rectSliderTickmarks:"<<rectSliderTickmarks<<"   rectGroove:"<<rectGroove;

            QPen pen;
            //绘画 滑槽(线)
            if (opt->subControls & SC_SliderGroove) {
                pen.setStyle(Qt::CustomDashLine);
                QVector<qreal> dashes;
                qreal space = 1.3;
                dashes << 0.1 << space;
                pen.setDashPattern(dashes);
                pen.setWidthF(3);
                pen.setColor(getColor(opt, QPalette::Highlight));
                p->setPen(pen);
                p->setRenderHint(QPainter::Antialiasing);

                if (slider->orientation == Qt::Horizontal) {
                    p->drawLine(QPointF(rectGroove.left(), rectHandle.center().y()), QPointF(rectHandle.left(), rectHandle.center().y()));
                    pen.setColor(getColor(opt, QPalette::Foreground));
                    p->setPen(pen);
                    p->drawLine(QPointF(rectGroove.right(), rectHandle.center().y()), QPointF(rectHandle.right(), rectHandle.center().y()));
                } else {
                    p->drawLine(QPointF(rectGroove.center().x(), rectGroove.bottom()), QPointF(rectGroove.center().x(),  rectHandle.bottom()));
                    pen.setColor(getColor(opt, QPalette::Foreground));
                    p->setPen(pen);
                    p->drawLine(QPointF(rectGroove.center().x(),  rectGroove.top()), QPointF(rectGroove.center().x(),  rectHandle.top()));
                }
            }

            //绘画 滑块
            if (opt->subControls & SC_SliderHandle) {
                pen.setStyle(Qt::SolidLine);
                p->setPen(Qt::NoPen);
                p->setBrush(getColor(opt, QPalette::Highlight));
                p->drawRoundedRect(rectHandle, DStyle::pixelMetric(DStyle::PM_FrameRadius), DStyle::pixelMetric(DStyle::PM_FrameRadius));
            }

            //绘画 刻度,绘画方式了参考qfusionstyle.cpp
            if ((opt->subControls & SC_SliderTickmarks) && slider->tickInterval) {                                   //需要绘画刻度
                p->setPen(opt->palette.foreground().color());
                int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, w);  //可用空间
                int interval = slider->tickInterval;                                       //标记间隔
//                int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, opt, w);      //标记偏移
//                int ticks = slider->tickPosition;                                          //标记位置

                int v = slider->minimum;
                int len = proxy()->pixelMetric(PM_SliderLength, slider, w);
                while (v <= slider->maximum + 1) {                                          //此处不添加+1的话, 会少绘画一根线
                    const int v_ = qMin(v, slider->maximum);
                    int pos = sliderPositionFromValue(slider->minimum, slider->maximum, v_, available) + len / 2;

                    if (slider->orientation == Qt::Horizontal) {
                        if (slider->tickPosition == QSlider::TicksBothSides) {              //两侧都会绘画, 总的矩形-中心滑槽滑块最小公共矩形
                            p->drawLine(pos, rect.top(), pos, rectHandle.top());
                            p->drawLine(pos, rect.bottom(), pos, rectHandle.bottom());
                        } else {
                            p->drawLine(pos, rectSliderTickmarks.top(), pos, rectSliderTickmarks.bottom());
                        }
                    } else {
                        if (slider->tickPosition == QSlider::TicksBothSides) {
                            p->drawLine(rect.left(), pos, rectHandle.left(), pos);
                            p->drawLine(rect.right(), pos, rectHandle.right(), pos);
                        } else {
                            p->drawLine(rectSliderTickmarks.left(), pos, rectSliderTickmarks.right(), pos);
                        }
                    }
                    // in the case where maximum is max int
                    int nextInterval = v + interval;
                    if (nextInterval < v)
                        break;
                    v = nextInterval;
                }
            }

        }
        break;
    }
    default:
        break;
    }

    DStyle::drawComplexControl(cc, opt, p, w);
}

若刻度异常情况(非bug):

注意: 下图中是使用pen.setStyle(Qt::DotLine);来绘画的,上面的代码修改为了pen.setStyle(Qt::CustomDashLine);来绘画,所以会(看到的滑槽)略有不一样;

当显示区域比较小的时候,而刻度条的个数又比较多的时候(密密麻麻的那种),当超过某一阈值时候,系统会自动压缩显示,一个变成**”胖瘦相间隔”**,此时如果将该粗窗口放大,则会被重绘画显示正常,刻度条均匀相间隔。

原图效果:

本来还以为是自己重新绘画的效果造成的,后面确定是过密形成的显示异常情况.就想着有没有能够的解决方法,能够重新绘画显示.想着参考QFusion 风格的实现,于是翻看源码,只看绘画刻度部分的Qt源码:

if (option->subControls & SC_SliderTickmarks) {
                painter->setPen(outline);
                int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget);
                int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget);
                int interval = slider->tickInterval;
                if (interval <= 0) {
                    interval = slider->singleStep;
                    if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval,
                                                        available)
                            - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum,
                                                              0, available) < 3)
                        interval = slider->pageStep;
                }
                if (interval <= 0)
                    interval = 1;

                int v = slider->minimum;
                int len = proxy()->pixelMetric(PM_SliderLength, slider, widget);
                while (v <= slider->maximum + 1) {
                    if (v == slider->maximum + 1 && interval == 1)
                        break;
                    const int v_ = qMin(v, slider->maximum);
                    int pos = sliderPositionFromValue(slider->minimum, slider->maximum,
                                                      v_, (horizontal
                                                           ? slider->rect.width()
                                                           : slider->rect.height()) - len,
                                                      slider->upsideDown) + len / 2;
                    int extra = 2 - ((v_ == slider->minimum || v_ == slider->maximum) ? 1 : 0);

                    if (horizontal) {
                        if (ticksAbove) {
                            painter->drawLine(pos, slider->rect.top() + extra,
                                              pos, slider->rect.top() + tickSize);
                        }
                        if (ticksBelow) {
                            painter->drawLine(pos, slider->rect.bottom() - extra,
                                              pos, slider->rect.bottom() - tickSize);
                        }
                    } else {
                        if (ticksAbove) {
                            painter->drawLine(slider->rect.left() + extra, pos,
                                              slider->rect.left() + tickSize, pos);
                        }
                        if (ticksBelow) {
                            painter->drawLine(slider->rect.right() - extra, pos,
                                              slider->rect.right() - tickSize, pos);
                        }
                    }
                    // in the case where maximum is max int
                    int nextInterval = v + interval;
                    if (nextInterval < v)
                        break;
                    v = nextInterval;
                }
            }

等等, 这根本没有考虑这个问题,好不好。而且感觉绘画刻度的方法,是对一个矩形矩形多次精确的计算,eeemmmmmmmmmm,这样子是不是有点复杂了了。感觉没有我的将这一个超大矩形,分割成为三个小矩形这一思路简单,然后在对每一块小矩形进行相应的绘画。


解决方法:

决定尝试一下其原生**QFusion风格()是怎么解决这个效果,当设置刻度间隔比较小的时候,显示宽度比较窄时候,这个东西出现了”胖瘦相互间隔”,然后利用上面的一开始创建的小例子,改变步长和刻度间个和max值,看看效果**

甚至再次改变步长和刻度间个和max值,看看效果。过于密集,成了线

握草,握了个草,我握了个大草.hhhhhhhhhhhhhhhhhh,原来老哥你也没有设置这个问题啊,果断的出结论,这不是bug.果断不再继续修改了重绘画了.坏坏的笑了几下之后。

于是将一开始出现问题的地方,将该窗口最大化,然后局部拉大,看到这个效果(果然得到了验证):

最后总结:这个不是bug,或者显示异常。冷静下来是思考:如此窄的矩形里面,显示如此多根刻度线,由于像素限制,只能后绘画的比较密集,当这个数值更大的时候,会发现,这个会变成一条直线(放大拉开显示,才会显示其实是均匀相间隔的).


思考总结:

  • 首先检查代码,是否是相关部分的代码逻辑有问题
  • 仍然觉得不应该之后,试一下Qt自带的是否会重现,排除是自己还是非自己原因
  • 改变相关的值,写小例子验证,查看效果
  • 发现经验:+1; 完美结束

更新:更加精准的绘画滑槽

更新于2019-09-09


思考分享:

因为有着许许多多的热心网友的无私分享,从他们的博客中学习成长,学会很多,故也不辞辛苦也将自己的项目或经验整理成博客的形式,也提供给一起大家学习探讨与交流