element.ui-Qt实现之时间控件

时分秒滚动控件

废话少说,直入主题,今天我们来实现一个时分秒滚动控件,类似前端组件
element时间控件

Qt实现的时间控件效果,因为不会传动态效果,所以没有滚动效果。

注意本文只介绍了时分秒滚动区域的实现,只是当前日期组件的一部分,整个日期控件在后面的博客中介绍
element.ui-Qt实现之时间控件

  • QScrollTime有三个listview组成,分别是可滚动的时、分、秒区域
QScrollTime::QScrollTime(QWidget *parent, Qt::WindowFlags enumFlags)
	: QFrame(parent, enumFlags)
{
	ui = new Ui::QScrollTime();
	ui->setupUi(this);
	ui->listViewHour->setModel(new QHourModel(this));
	ui->listViewHour->setItemDelegate(new QScrollTimeDelegate(this));
	ui->listViewHour->viewport()->installEventFilter(this);
	ui->listViewHour->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
	ui->listViewMinute->setModel(new QMinuteModel(this));
	ui->listViewMinute->setItemDelegate(new QScrollTimeDelegate(this));
	ui->listViewMinute->viewport()->installEventFilter(this);
	ui->listViewMinute->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
	ui->listViewSecond->setModel(new QSecondModel(this));
	ui->listViewSecond->setItemDelegate(new QScrollTimeDelegate(this));
	ui->listViewSecond->viewport()->installEventFilter(this);
	ui->listViewSecond->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
	ui->frameScroll->setFixedHeight(ITEM_HEIGHT * 5.5);

	setStyleSheet("\
				#frameScroll,#listViewHour,#listViewMinute,#listViewSecond {background:transparent;} \
				#frame {border-top:1px solid #D1D1D1;} \
				#pBScrollCancel{border:none;} \
				#pBScrollConfirm {border:none;color:#58BBE4;} \
				#pBScrollConfirm:hover {color:#79C8E9;} \
				#pBScrollConfirm:pressed {color:#4695B6;}");

	connect(ui->listViewHour, &QListView::clicked, this, &QScrollTime::slotTimeClicked);
	connect(ui->listViewMinute, &QListView::clicked, this, &QScrollTime::slotTimeClicked);
	connect(ui->listViewSecond, &QListView::clicked, this, &QScrollTime::slotTimeClicked);

	//垂直滚动条Value变化---当前时间被调整
	connect(ui->listViewHour->verticalScrollBar(), &QScrollBar::valueChanged, this, &QScrollTime::slotTimeChangedByScorll);
	connect(ui->listViewMinute->verticalScrollBar(), &QScrollBar::valueChanged, this, &QScrollTime::slotTimeChangedByScorll);
	connect(ui->listViewSecond->verticalScrollBar(), &QScrollBar::valueChanged, this, &QScrollTime::slotTimeChangedByScorll);

	connect(ui->pBScrollCancel, &QPushButton::clicked, this, [&](){
		emit this->singalCancelClicked(m_origTime);
		this->hide();
	});
	connect(ui->pBScrollConfirm, &QPushButton::clicked, this, [&](){
		if (!m_origTime.isValid())
		{
			emit this->timeChanged(m_showTime);
		}
		this->hide();
	});

	setContentsMargins(LEFT_SHAWDE,TOP_SHAWDE,RIGHT_SHAWDE,BOTTOM_SHAWDE);
	m_layerRect = QRect(contentsRect().left(), ITEM_HEIGHT * 2.5 + contentsRect().top(), contentsRect().width(), ITEM_HEIGHT);

	setAttribute(Qt::WA_TranslucentBackground, true);
}
  • 初始化时间
void QScrollTime::initTime(const QTime& time)
{
	if (time.isValid())
	{
		m_origTime = time;
		m_showTime = time;
		QTimer::singleShot(0, this, SLOT(slotDelaySetTime())); //这里不能直接滚动,因为此刻还未显示,放到事件循环尾部
	}
	else
	{
		m_showTime = QTime(0, 0, 0);
	}
}
  • 绘制时间区域底色和:号

  • 绘制背景阴影

  • 绘制区域底色

  • 绘制:号

	QPainter painter(this);
	QPixmap pix(":/images/Calendar/timeBorder.png");
	qDrawBorderPixmap(&painter, rect(), QMargins(0, 0, 0, 0), pix);
	painter.fillRect(m_layerRect, QColor(88, 187, 228));
	QRect colon1Rect(ui->listViewHour->width() + m_layerRect.left() - 2, ITEM_HEIGHT * 2.5 + contentsRect().top(), 4, ITEM_HEIGHT - 2);
	QRect colon2Rect(ui->listViewSecond->x() + m_layerRect.left() - 2, ITEM_HEIGHT * 2.5 + contentsRect().top(), 4, ITEM_HEIGHT - 2);
	QPen pen(Qt::white);
	painter.setPen(pen);
	painter.drawText(colon1Rect, Qt::AlignCenter, ":");
	painter.drawText(colon2Rect, Qt::AlignCenter, ":");
  • 过滤在当前时分秒区域鼠标事件
bool QScrollTime::eventFilter(QObject *obj, QEvent *event)
{
	QEvent::Type t = event->type();
	if ((obj == ui->listViewHour->viewport() || obj == ui->listViewMinute->viewport() || obj == ui->listViewSecond->viewport()) \
		&& (t == QEvent::MouseMove || t == QEvent::MouseButtonPress || t == QEvent::MouseButtonRelease || t == QEvent::MouseButtonDblClick))
	{
		QMouseEvent* pMouseEvent = dynamic_cast<QMouseEvent*>(event);
		QPoint pos = pMouseEvent->pos();
		if (m_layerRect.contains(pos))
		{
			return true;
		}	
	}
	return false;
}
  • 时分秒Item被点击后移到中间
void QScrollTime::slotTimeClicked(const QModelIndex &index)
{
	if (QListView* view = qobject_cast<QListView*>(sender()))
	{
		setCenterCoveredRow(view, index.row());
	}	
}
  • 当滚动条变化后,发射时间变化信号
  • 这里有针对滚动超出范围后,微调整滚动条
void QScrollTime::slotTimeChangedByScorll()
{
	bool bTimeChanged = false;
	if (ui->listViewHour->verticalScrollBar() == sender())
	{
		const QModelIndex index = ui->listViewHour->indexAt(ui->listViewHour->viewport()->rect().center());
		if (index.isValid())
		{
			//调整index位置
			QRect indexRect = ui->listViewHour->visualRect(index);
			indexRect.adjust(contentsRect().left(), contentsRect().top(), contentsRect().left(), contentsRect().top());
			if (indexRect != m_layerRect.intersected(indexRect))
			{
				setCenterCoveredRow(ui->listViewHour, index.row());
				return;
			}
			QString value = index.data(Qt::DisplayRole).toString();
			if (!value.isEmpty())
			{
				int hour = value.toInt();
				if (hour != m_showTime.hour())
				{
					bTimeChanged = true;
					m_showTime.setHMS(hour, m_showTime.minute(), m_showTime.second());
				}
			}
		}
	}
	else if (ui->listViewMinute->verticalScrollBar() == sender())
	{
		const QModelIndex index = ui->listViewMinute->indexAt(ui->listViewMinute->viewport()->rect().center());
		if (index.isValid())
		{
			//调整index位置
			QRect indexRect = ui->listViewMinute->visualRect(index);
			indexRect.adjust(contentsRect().left(), contentsRect().top(), contentsRect().left(), contentsRect().top());
			if (indexRect != m_layerRect.intersected(indexRect))
			{
				setCenterCoveredRow(ui->listViewMinute, index.row());
				return;
			}
			QString value = index.data(Qt::DisplayRole).toString();
			if (!value.isEmpty())
			{
				int minu = value.toInt();
				if (minu != m_showTime.minute())
				{
					bTimeChanged = true;
					m_showTime.setHMS(m_showTime.hour(), minu, m_showTime.second());
				}
			}
		}
	}
	else if (ui->listViewSecond->verticalScrollBar() == sender())
	{
		const QModelIndex index = ui->listViewSecond->indexAt(ui->listViewSecond->viewport()->rect().center());
		if (index.isValid())
		{
			//调整index位置
			QRect indexRect = ui->listViewSecond->visualRect(index);
			indexRect.adjust(contentsRect().left(), contentsRect().top(), contentsRect().left(), contentsRect().top());
			if (indexRect != m_layerRect.intersected(indexRect))
			{
				setCenterCoveredRow(ui->listViewSecond, index.row());
				return;
			}
			QString value = index.data(Qt::DisplayRole).toString();
			if (!value.isEmpty())
			{
				int secs = value.toInt();
				if (secs != m_showTime.second())
				{
					bTimeChanged = true;
					m_showTime.setHMS(m_showTime.hour(), m_showTime.minute(), secs);
				}
			}
		}
	}

	if (bTimeChanged)
	{
		emit timeChanged(m_showTime);
	}
}
  • 设置时分秒时,将滚动条值设置
void QScrollTime::slotDelaySetTime()
{
	setCenterCoveredRow(ui->listViewHour, m_origTime.hour() + TOP_SPACEITEM_NUM);
	setCenterCoveredRow(ui->listViewMinute, m_origTime.minute() + TOP_SPACEITEM_NUM);
	setCenterCoveredRow(ui->listViewSecond, m_origTime.second() + TOP_SPACEITEM_NUM);
}

void QScrollTime::setCenterCoveredRow(QListView* view, int row)
{
	if (view == ui->listViewHour)
	{
		ui->listViewHour->verticalScrollBar()->setValue((row - TOP_SPACEITEM_NUM) * ITEM_HEIGHT);
	}
	else if (view == ui->listViewMinute)
	{
		ui->listViewMinute->verticalScrollBar()->setValue((row - TOP_SPACEITEM_NUM) * ITEM_HEIGHT);
	}
	else if (view == ui->listViewSecond)
	{
		ui->listViewSecond->verticalScrollBar()->setValue((row - TOP_SPACEITEM_NUM) * ITEM_HEIGHT);
	}
}
  • 时分秒的Model
enum TimePartValue
{
	HoursOfDay = 24,
	MinutesOfHour = 60,
	SecondsOfMin = 60,
};

class QTimeModel : public QAbstractListModel
{
	Q_OBJECT
protected:
	QTimeModel(TimePartValue v, QObject *parent) : QAbstractListModel(parent), value(v){}
	~QTimeModel() {}
public:
	int rowCount(const QModelIndex &parent = QModelIndex()) const;
	QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
	Qt::ItemFlags flags(const QModelIndex &index) const;
private:
	int value;
};

class QHourModel : public QTimeModel
{
	Q_OBJECT

public:
	QHourModel(QObject *parent) : QTimeModel(HoursOfDay, parent){}
	~QHourModel(){}
};

class QMinuteModel : public QTimeModel
{
	Q_OBJECT

public:
	QMinuteModel(QObject *parent) : QTimeModel(MinutesOfHour, parent){}
	~QMinuteModel(){}
};

class QSecondModel : public QTimeModel
{
	Q_OBJECT

public:
	QSecondModel(QObject *parent) : QTimeModel(SecondsOfMin, parent){}
	~QSecondModel(){}
};

.cpp,注意上下有两个空Item,为了给滚动条留空间

int QTimeModel::rowCount(const QModelIndex &parent) const
{ 
	if (parent.isValid())
	{
		return 0;
	}
	return value + TOP_SPACEITEM_NUM + BUTTOM_SPACEITEM_NUM; //上下2空Item
}

QVariant QTimeModel::data(const QModelIndex &index, int role) const
{
	int row = index.row();
	if (Qt::TextAlignmentRole == role)
	{
		return Qt::AlignCenter;
	}
	if (Qt::DisplayRole == role)
	{
		if (row >= TOP_SPACEITEM_NUM && row < value + TOP_SPACEITEM_NUM)
		{
			QString strTime;
			strTime.sprintf("%02d", row - TOP_SPACEITEM_NUM);
			return strTime;
		}
	}
	return QVariant();
}

Qt::ItemFlags QTimeModel::flags(const QModelIndex &index) const
{
	int row = index.row();
	if ((row >= 0 && row < TOP_SPACEITEM_NUM) || row >= value + TOP_SPACEITEM_NUM)
	{
		return 0;
	}

	return QAbstractListModel::flags(index);
}

  • view的delegate,控制View的绘制

  • 注意第二个Item的高度是别的Item一半高度,为了滚动区域顶部显示一半的效果,为什么不用第一个Item呢? 因为滚动条的singlestep是第一个Item高度,这从QlistView源码得知

QScrollTimeDelegate::QScrollTimeDelegate(QObject *parent)
	: QItemDelegate(parent)
{

}

QScrollTimeDelegate::~QScrollTimeDelegate()
{

}

void QScrollTimeDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
	QString text = index.data(Qt::DisplayRole).toString();
	if (text.isEmpty())
	{
		return;
	}
	painter->save();
	int height = option.widget->height() / 2;
	if (option.rect.y() <= height && option.rect.y() + option.rect.width() >= height)
	{
		QPen pen(Qt::white);
		painter->setPen(pen);
		painter->drawText(option.rect,Qt::AlignCenter,text);
	}
	else
	{
		if (option.state & QStyle::State_MouseOver)
		{
			painter->fillRect(option.rect, QColor(214, 238, 248));
		}
		painter->drawText(option.rect, Qt::AlignCenter, text);
	}
	painter->restore();
}

QSize QScrollTimeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
	if (SCROLLBAR_STEP_BY_INDEX == index.row())
	{
		return QSize(0, ITEM_HEIGHT >> 1);
	}
	return QSize(0, ITEM_HEIGHT);
}
  • 以上用的的宏定义如下
#define CALENDAR_DEFINE_H
#define ITEM_HEIGHT 35
#define TOP_SPACEITEM_NUM 3
#define BUTTOM_SPACEITEM_NUM 2
#define SCROLLBAR_STEP_BY_INDEX 1
#define TOP_SHAWDE 6
#define BOTTOM_SHAWDE 18
#define LEFT_SHAWDE 12
#define RIGHT_SHAWDE 12