一直觉得Qt中的时间选择的控件不能够满足一些特定的需求,比如说通过鼠标滚动或者拖动(手机中时间选择器)实现时间的改变,最近研究了下怎么实现这个需求。
首先我们先看下实现效果,毕竟展示效果具有很强的吸引力:
做这个最主要的难点是怎么控制鼠标的偏移量,其他的都比较直接,实现paintEvent函数将信息绘画在界面上就行了。
我们定义了一个类去实现,这样以后就能够直接拿来用了,我们先看下类头文件的定义
包含头文件
#include <QMouseEvent>
#include <QColor>
#include <QPainter>
#include <QTime>
#include<QDate>
class ScrollBar : public QWidget
{
Q_OBJECT
public:
/**定义类型的目的是滚动屏有垂直和水平之分,为了实现两种模式**/
enum ScrollType{
VERTICAL = 1,
HORIZONTAL
};
/**这个只是界面的颜色显示**/
enum ColorType{
BACKHROUND = 1,
LINE,
CURRENTTEXT,
DISABLETEXT
};
public:
explicit ScrollBar(QWidget *parent = 0, ScrollType nType = VERTICAL);
~ScrollBar();
public:
/**
* @brief 设置界面显示的颜色
*
* @param[in] color 颜色
* @param[in] ColorType 颜色的类型,区分颜色的
**/
void setColor(QColor &pColor, ColorType nType = CURRENTTEXT);
/**
* @brief 获取当前值
*
* @return int
* @retval 当前在界面中间的值
**/
inline int getValue() { return m_nCurrentValue;}
/**
* @brief 设置当前值
*
* @param[in] int 当前值
**/
inline void setValue(int nValue) { m_nCurrentValue = nValue;}
/**
* @brief 设置步长
*
* @param[in] int 步长 界面相邻显示的数值间隔 默认为 1
**/
inline void setStep(int nStep) { m_nStep = nStep;}
/**
* @brief 设置界面显示的个数
*
* @param[in] int 页面中显示多少个数字
**/
inline void setDevice(int nDevice) {m_nDevice = nDevice;}
/**
* @brief 设置滚动屏的类型 默认为垂直
*
* @param[in] ScrollType 滚动屏的类型
**/
inline void setScrollType(ScrollType nType) {m_nType = nType;}
/**
* @brief 设置滚动范围
*
* @param[in] int 最小值
* @param[in] int 最大值
**/
inline void setRang(int nMin, int nMax)
{
m_nMin = nMin;
m_nMax = nMax;
m_nCurrentValue = m_nCurrentValue > m_nMax ? m_nMax : m_nCurrentValue;
m_nCurrentValue = m_nCurrentValue < m_nMin ? m_nMin : m_nCurrentValue;
}
/**主要是鼠标事件的重实现,其实我们用的最多的应该是滚轮,因此也实现轮子事件**/
protected:
void wheelEvent(QWheelEvent* event);
void mousePressEvent(QMouseEvent* event);
void mouseMoveEvent(QMouseEvent* event);
void mouseReleaseEvent(QMouseEvent* event);
void paintEvent(QPaintEvent* event);
private:
/**
* @brief 绘制背景
*
* @param[in] QPainter QPainter指针
**/
void paintBackground(QPainter* pPainter);
/**
* @brief 绘制线条
*
* @param[in] QPainter QPainter指针
**/
void paintLine(QPainter* pPainter);
/**
* @brief 绘制字体
*
* @param[in] QPainter QPainter指针
* @param[in] int 需要绘制的数值
* @param[in] int 绘制的偏移量 相对于中心便宜多少量绘制数值
* @param[in] int 字体的大小
**/
void paintText(QPainter* pPainter, int nValue, int nOffSet, int nFontSize);
signals:
/**
* @brief 当前值改变时发给上层的信号
*
* @param[in] int 当前数值
* @param[in] QWidget* 该滚动条的父类Widget,目的是为了能够准确的找到是属于哪个对象
**/
void signal_currentValueChange(int nValue, QWidget* pWidget);
private:
int m_nCurrentValue;
int m_nOffSet; //偏离值
int m_nMax; //滚动的最大值
int m_nMin; //滚动的最小值
int m_nMousePos; //鼠标点击的位置
int m_nDevice; //显示的数量
int m_nStep; //滚动的步长
ScrollType m_nType; //垂直还是水平
QColor m_cBackground; //背景颜色
QColor m_cCurrentText; //当前值颜色
QColor m_cDisableText; //其他字体颜色
QColor m_cLine; //线条颜色
};
下面是该类的构造函数
ScrollBar::ScrollBar(QWidget *parent, ScrollType nType) : QWidget(parent), m_nType(nType)
, m_nCurrentValue(0)
, m_nOffSet(0)
, m_nMax(0)
, m_nMin(0)
, m_nMousePos(0)
, m_nDevice(5)
, m_nStep(1)
{
this->setFixedSize(parent->size());
}
ScrollBar::~ScrollBar()
{
}
设置界面颜色,这个可以由个人喜好决定
void ScrollBar::setColor(QColor &pColor, ColorType nType)
{
switch (nType)
{
case ...:
{
** = pColor;
break;
}
...
default:
break;
}
}
实现界面数值变换的方式有两种,一种是鼠标的滚轮滚动,另一种是鼠标按下并且拖动,两种方式实现的效果是相同的,目的都是为了得到偏移量,但就省力来说,滚动滚轮方便的岂是一星半点,那我们就先看看滚轮的实现。
void ScrollBar::wheelEvent(QWheelEvent *event)
{
/**滚动的角度,*8就是鼠标滚动的距离**/
int nDegrees = event->delta() / 8;
/**滚动的步数,*15就是鼠标滚动的角度**/
int nSteps = nDegrees / 15;
int nTarget = m_nType == ScrollType::VERTICAL ? this->height() : this->width();
m_nOffSet = nTarget / m_nDevice * nSteps;
update();
}
没错,你没有看错,就是这个简单的几行代码就能够得到鼠标的偏移量。m_nOffSet有正有负,正负表示偏移的方向。
鼠标滚轮的实现已经看完了,我们再看看鼠标拖动的方式。
首先鼠标在按下的时候我们要记录点的坐标值,因为相对于垂直滚动来说,鼠标左边的x值无意义,同理,对水平滚动y值也无意义,因此我们通过滚动条的类型选择对我们有用的数值
void ScrollBar::mousePressEvent(QMouseEvent *event)
{
m_nMousePos = m_nType == ScrollType::VERTICAL ? event->pos().y() : event->pos().x();
update();
}
接下来就是鼠标的移动事件,在移动的过程中我们要时刻计算偏移量,以达到界面不间断的目的。
void ScrollBar::mouseMoveEvent(QMouseEvent *event)
{
int nMouserPos = m_nType == ScrollType::VERTICAL ? event->pos().y() : event->pos().x();
/**判断当前值的大小,如果为范围的极限值则返回**/
if(m_nCurrentValue == m_nMin && nMouserPos >= m_nMousePos ||
m_nCurrentValue == m_nMax && nMouserPos <= m_nMousePos)
{
return;
}
int nTarget = m_nType == ScrollType::VERTICAL ? this->height() : this->width();
int nOffSet = nMouserPos - m_nMousePos;
/**判断鼠标移动的距离是否大于最小偏移量 如果大于偏移量 则将偏移量置位最小偏移量 目的是避免界面出现跨越显示**/
if(nOffSet > (nTarget / m_nDevice)) /**(nTarget / m_nDevice) 为一次偏移的最小值 也就是一个字体的显示的大小边界值**/
{
nOffSet = nTarget / m_nDevice;
}
else if(nOffSet < -nTarget / m_nDevice)
{
nOffSet = -nTarget / m_nDevice;
}
/**nOffSet的正负代表便宜的方向**/
m_nOffSet = nOffSet;
update();
}
鼠标被松开之后,会存在几种比较极端的情况,最后一次偏移量和显示界面大小一半的比较(大于、等于、小于)。出现这种情况的只有最后一次偏移,因为在鼠标移动的过程中已经计算了所有的偏移并显示在界面上了。
void ScrollBar::mouseReleaseEvent(QMouseEvent *event)
{
int nTarget = m_nType == ScrollType::VERTICAL ? this->height() : this->width();
int nOffSet = m_nOffSet;
/**计算鼠标的偏移量,根据显示字体的控件大小的一半来确定该偏移到那个值(正负表示偏移的方向)**/
int nJudge = nOffSet < 0 ? -(nTarget / (m_nDevice * 2)) : nTarget / (m_nDevice * 2);
if(nOffSet < 0)
{
if(nOffSet < nJudge)
{
m_nOffSet = 0;
goto UPDATE;
}
m_nOffSet = -nTarget / m_nDevice;
goto UPDATE;
}
if (nOffSet < nJudge)
{
m_nOffSet = 0;
goto UPDATE;
}
m_nOffSet = nTarget / m_nDevice;
UPDATE:
update();
}
现在偏移量的两种计算都已经介绍完了,接下来就是界面的绘画了。毕竟对大多数程序员来说,查找资料的过程中总是会先看看技术实现的效果能否满足自己的需求,再决定代码需不需要研究。
void ScrollBar::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
if(m_nMin == m_nMax)
{
return;
}
/**在绘制界面之前我们需要通过偏移量来计算当前值,毕竟我们的目的是得到值,而不是看着界面做沉思状**/
int nTarget = m_nType == ScrollType::VERTICAL ? this->height() : this->width();
int nOffSet = m_nOffSet;
if(nOffSet >= (nTarget / m_nDevice) && m_nCurrentValue > m_nMin)
{
m_nMousePos += nTarget / m_nDevice;
nOffSet -= nTarget / m_nDevice;
this->setValue(m_nCurrentValue - m_nStep);
goto PAINTE;
}
else if(nOffSet <= -nTarget / m_nDevice && m_nCurrentValue < m_nMax)
{
m_nMousePos -= nTarget / m_nDevice;
nOffSet += nTarget / m_nDevice;
this->setValue(m_nCurrentValue + m_nStep);
}
/**当前值设置完成后,进入界面的绘制**/
PAINTE:
if(getValue() == m_nMax || getValue() == m_nMin)
{
nOffSet = 0;
}
m_nOffSet = nOffSet;
/**首先绘制背景**/
paintBackground(&painter);
/**绘制线条 具体的绘制方法后面再说**/
paintLine(&painter);
int nFontSize = 14; /**绘制的字体大小,后面会介绍自东获取字体大小的方法,我在这边定义是由于现实的界面比较大,通过自动获取的字体大小,界面会比较难看**/
/**绘制当前字体**/
paintText(&painter, m_nCurrentValue, nOffSet, nFontSize);
/**绘制两边的字体**/
for (int nIndex = 1; nIndex <= m_nDevice / 2; ++nIndex)
{
nFontSize -= 2;
if (m_nCurrentValue - m_nStep * nIndex >= m_nMin)
{
/**两边字体的偏移量是通过距离计算的**/
paintText(&painter, m_nCurrentValue - m_nStep * nIndex, nOffSet - nTarget / m_nDevice * nIndex, nFontSize);
}
if (m_nCurrentValue + m_nStep * nIndex <= m_nMax)
{
paintText(&painter, m_nCurrentValue + m_nStep * nIndex, nOffSet + nTarget / m_nDevice * nIndex, nFontSize);
}
}
/**将父窗口发送目的是能够准确的找到是属于哪个父类(假设界面有多个)**/
emit signal_currentValueChange(getValue(), this->parentWidget());
}
背景和线条的绘制比较简单,就不多做解释,值得注意的地方是线条的绘制个数和坐标的变化
void ScrollBar::paintBackground(QPainter *pPainter)
{
pPainter->save();
pPainter->setPen(Qt::NoPen);
pPainter->setBrush(m_cBackground);
pPainter->drawRect(rect());
pPainter->restore();
}
画线条
void ScrollBar::paintLine(QPainter *pPainter)
{
int nWidth = this->width();
int nHeight = this->height();
pPainter->save();
pPainter->setBrush(Qt::NoBrush);
QPen pen = pPainter->pen();
pen.setWidth(1);
pen.setColor(m_cLine);
pen.setCapStyle(Qt::RoundCap);
pPainter->setPen(pen);
/**绘制线条需要指定线条的起始坐标, 对于不同类型的滚动屏,坐标也有不同的数值**/
for(int nIndex = 2; nIndex <= 3; nIndex++)
{
/**对于垂直滚动屏来说,线条的Y值是不变的,同理对于水平的滚动屏来说,线条的X值是不变的**/
int nPosX = m_nType == ScrollType::VERTICAL ? 0 : nWidth / 5 * nIndex;
int nPosY = m_nType == ScrollType::VERTICAL ? nHeight / 5 * nIndex : 0;
int nEndPosX = m_nType == ScrollType::VERTICAL ? nHeight : nPosX;
int nEndPosY = m_nType == ScrollType::VERTICAL ? nPosY : nHeight;
pPainter->drawLine(nPosX, nPosY, nEndPosX, nEndPosY);
}
pPainter->restore();
}
接下来是绘制数值,数值的绘制分为当前值和两边的临界值,有不同的显示规则
void ScrollBar::paintText(QPainter *pPainter, int nValue, int nOffSet, int nFontSize)
{
pPainter->save();
int nWidth = this->width();
int nHeight = this->height();
/**下面注释掉的两行是通过整个界面的长(高)来控制字体的大小**/
// int nTarget = m_nType == ScrollType::VERTICAL ? this->height() : this->width();
// font.setPixelSize((nTarget - qAbs(nOffSet)) / m_nDevice);
QFont font = QFont("Helvetica", 5);
font.setPixelSize(nFontSize);
QColor nColor = nOffSet == 0 ? m_cCurrentText : m_cDisableText;
QPen pen = pPainter->pen();
pen.setColor(nColor);
pPainter->setPen(pen);
pPainter->setBrush(Qt::NoBrush);
pPainter->setFont(font);
if(m_nType == ScrollType::HORIZONTAL)
{
int textWidth = pPainter->fontMetrics().width(nValue);
int initX = nWidth / 2 + nOffSet - textWidth / 2;
pPainter->drawText(QRect(initX, 0, 15, nHeight), Qt::AlignCenter, QString::number(nValue));
//pPainter->drawText(QRect(initX, 0, textWidth, nHeight), Qt::AlignCenter, QString::number(nValue));
/**有个小问题,当这儿使用下面的语句时,左边的0和1就会不显示,暂时未纠结其具体原因**/
goto NEXT;
}
int textHeight = pPainter->fontMetrics().height();
int initY = nHeight / 2 + nOffSet - textHeight / 2;
pPainter->drawText(QRect(0, initY, nWidth, textHeight), Qt::AlignCenter, QString::number(nValue));
NEXT:
pPainter->restore();
}
上面已经将全部的流程介绍完了,接下来我们介绍下测试用例:
我们这边只做一个界面的说明,其他的照搬就行
QColor dColorCurrentText("#43464B");
QColor dColorLine("#0092FF");
QColor dColorDisableText("#999999");
QColor dColorBackground("#FFFFFF");
ScrollBar* pWidgetHour = new ScrollBar(ui->widget);
pWidgetHour->setRang(0, 23);
pWidgetHour->setValue(time.hour());
pWidgetHour->setColor(dColorCurrentText);
pWidgetHour->setColor(dColorLine, ScrollBar::ColorType::LINE);
pWidgetHour->setColor(dColorDisableText, ScrollBar::ColorType::DISABLETEXT);
pWidgetHour->setColor(dColorBackground, ScrollBar::ColorType::BACKHROUND);
connect(pWidgetHour, SIGNAL(signal_currentValueChange(int,QWidget*)), this, SLOT(slot_timeChange(int,QWidget*)));
ScrollBar* pWidgeMin = new ScrollBar(ui->widget_2, ScrollBar::ScrollType::HORIZONTAL);
pWidgeMin->setRang(0, 60);
pWidgeMin->setValue(time.minute());
pWidgeMin->setColor(dColorCurrentText);
pWidgeMin->setColor(dColorLine, ScrollBar::ColorType::LINE);
pWidgeMin->setColor(dColorDisableText, ScrollBar::ColorType::DISABLETEXT);
pWidgeMin->setColor(dColorBackground, ScrollBar::ColorType::BACKHROUND);
connect(pWidgeMin, SIGNAL(signal_currentValueChange(int,QWidget*)), this, SLOT(slot_timeChange(int,QWidget*)));
/**接收信号的槽函数**/
void DateTime:: slot_timeChange(int, QWidget* pWidget)
{
int nHour = pWidget == ui->widget ? nValue : ui->label->text().toInt();
int nMin = pWidget == ui->widget_2 ? nValue : ui->label_2->text().toInt();
ui->label->setText(QString::number(nHour));
ui->label_2->setText(QString::number(nMin));
}