引言
qt本身是有自带的日历控件QCalendarWidget ,但是实际使用对于特殊的展示方式很难满足,而且调整样式也比较麻烦,索性就自己实现了日历。效果如下:
核心实现
主体控件为QTableWidget,不同的背景颜色、边框以及显示当前日期的圆点等都是通过代理QStyledItemDelegate实现的。翻动日历通过上方的按钮以及滚轮事件实现。
代码
#include <QMainWindow>
#include <QStyledItemDelegate>
#include <QTableWidget>
#include <QDateTime>
#include <QLabel>
#include <QPushButton>
#include <QComboBox>
#include <QDateTimeEdit>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QStackedLayout>
#include <QLineEdit>
#include <QString>
// 提供绘图函数
class DrawBaseDelegage : public QStyledItemDelegate
{
Q_OBJECT
public:
DrawBaseDelegage(QObject* parent);
protected:
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
protected:
void drawBackgroud(QPainter* painter, const QRect& rect, const QColor& color) const;
void drawBorder(QPainter* painter, const QRect& rect, const QColor& color) const;
void drawText(QPainter* painter, const QRect& rect, const QColor& color, const QString text, const Qt::AlignmentFlag align = Qt::AlignCenter) const;
void drawPoint(QPainter* painter, const QRect& rect, const QColor& color) const;
};
class CalendarDelegage : public DrawBaseDelegage
{
Q_OBJECT
public:
CalendarDelegage(QAbstractItemView* parent);
protected:
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
virtual bool eventFilter(QObject* obj, QEvent* ev);
private:
bool colorByRole(const QModelIndex& modelIndex, int role, QColor& tmpColor, double alpha = 0) const;
private:
QAbstractItemView* m_parentView;
QModelIndex m_hoverIndex;
};
class CalendarTable : public QTableWidget
{
Q_OBJECT
public:
CalendarTable(QWidget* parent, int year = QDate::currentDate().year(), int month = QDate::currentDate().month());
enum CalendarRoleType{
CRT_DATE = Qt::UserRole,
CRT_BACKGROUND_COLOR,
CRT_BORDER_COLOR,
CRT_HOVER_BORDER_COLOR,
CRT_FONT_COLOR,
CRT_POINT_COLOR,
CRT_ALPHA,// 整体不透明度
};
enum CalendarSelectionMode{
CSM_NO_SELECTION,
CSM_SINGLE_SELECTION,
CSM_MULTI_SELECTION,
};
public:
bool setYearMonth(int year, int month);
void setFristDayOnWeek(int fristDayOnWeek);
void setSelMode(CalendarSelectionMode selectionMode);
void addSelectedDate(QDate date);
void clearSelectedDate();
void setMaxDate(QDate date);
void setMinDate(QDate date);
int year(){return m_year;}
int month(){return m_month;}
int fristDayOnWeek(){return m_fristDayOnWeek;}
CalendarSelectionMode selectionMode(){return m_selMode;}
QList<QDate> selectedDate(){return m_selDate;}
QDate maxDate(){return m_maxDate;}
QDate minDate(){return m_minDate;}
protected:
virtual bool eventFilter(QObject* obj, QEvent* ev);
signals:
void sig_refresh(int year, int month);
void sig_selectionChanged();
void sig_selectionAdded(const QDate &date);
void sig_maxDateChanged(const QDate &date);
void sig_minDateChanged(const QDate &date);
private slots:
void slot_itemClicked(QTableWidgetItem* item);
private:
void refreshCalendar();
void refreshCalendarHeader();
void refreshSelection();
bool appendSelection(QDate date);
bool removeSelection(QDate date);
private:
int m_year;
int m_month;
int m_fristDayOnWeek;
QDate m_maxDate;
QDate m_minDate;
CalendarDelegage* m_delegage;
QList<QDate> m_selDate;
CalendarSelectionMode m_selMode;
};
class CalendarWidget : public QWidget
{
Q_OBJECT
public:
CalendarWidget(QWidget* parent, int year = QDate::currentDate().year(), int month = QDate::currentDate().month());
public:
void addWidgetBottom(QWidget* widget){m_mainLayout->addWidget(widget);}
void addLayoutBottom(QLayout* layout){m_mainLayout->addLayout(layout);}
void addSelectedDate(QDate date){m_calendar->addSelectedDate(date);}
void clearSelectedDate(){m_calendar->clearSelectedDate();}
bool setYearMonth(int year, int month){return m_calendar->setYearMonth(year, month);}
void setSelMode(CalendarTable::CalendarSelectionMode selectionMode){m_calendar->setSelMode(selectionMode);}
void setMaxDate(QDate date){m_calendar->setMaxDate(date);}
void setMinDate(QDate date){m_calendar->setMinDate(date);}
void setTitle(QString title);
QDate maxDate(){return m_calendar->maxDate();}
QDate minDate(){return m_calendar->minDate();}
QList<QDate> selectedDate(){return m_calendar->selectedDate();}
int year(){return m_calendar->year();}
int month(){return m_calendar->month();}
private slots:
void slot_calendarRefresh(int year, int month);
void slot_calendarMaxDateChanged(const QDate &date);
void slot_calendarMinDateChanged(const QDate &date);
void slot_preMonth();
void slot_nextMonth();
void slot_preYear();
void slot_nextYear();
signals:
void sig_calendarSelectionChanged();
void sig_calendarSelectionAdded(const QDate &date);
void sig_calendarRefresh(int year, int month);
private:
void refreshNextEnable();
void refreshPreEnable();
private:
QLabel* m_title;
QLabel* m_displayLabel;
QPushButton* m_preMonthBtn;
QPushButton* m_nextMonthBtn;
QPushButton* m_preYearBtn;
QPushButton* m_nextYearBtn;
QString m_dateStr;
CalendarTable* m_calendar;
QVBoxLayout* m_mainLayout;
};
DrawBaseDelegage作为代理的基类提供了绘制函数,绘制背景、边框、文字等。
CalendarDelegage为实际的代理,根据角色值进行相关绘制。
CalendarTable为日历主体,没有按钮等其他控件,是继承QTableWidget,实现了日历展示以及多选功能,对外提供设置当前展示月接口。refreshCalendar函数是展示内容的实现。
CalendarWidget为组合控件,融合了按钮、标题、日历,作为一个最常用的日历单元。
#include <QPainter>
#include <QHeaderView>
#include <QWheelEvent>
#include <QMoveEvent>
DrawBaseDelegage::DrawBaseDelegage(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void DrawBaseDelegage::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyledItemDelegate::paint(painter, option, index);
}
void DrawBaseDelegage::drawBackgroud(QPainter *painter, const QRect &rect, const QColor &color) const
{
painter->fillRect(rect, color);
}
void DrawBaseDelegage::drawBorder(QPainter *painter, const QRect &rect, const QColor &color) const
{
painter->save();
painter->setPen(color);
painter->drawRect(rect.x(), rect.y(), rect.width()-1, rect.height()-1);
painter->restore();
}
void DrawBaseDelegage::drawText(QPainter *painter, const QRect &rect, const QColor &color, const QString text, const Qt::AlignmentFlag align) const
{
painter->save();
painter->setPen(color);
painter->drawText(rect, align, text);
painter->restore();
}
void DrawBaseDelegage::drawPoint(QPainter *painter, const QRect &rect, const QColor &color) const
{
painter->save();
painter->setPen(color);
painter->setBrush(color);
painter->setRenderHint(QPainter::Antialiasing, true);// 反锯齿
painter->drawEllipse(rect);
painter->restore();
}
CalendarDelegage::CalendarDelegage(QAbstractItemView *parent)
: DrawBaseDelegage(parent)
, m_parentView(parent)
{
m_parentView->viewport()->installEventFilter(this);
m_parentView->viewport()->setAttribute(Qt::WA_Hover, true);
}
void CalendarDelegage::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
double colorAlpha = index.data(CalendarTable::CRT_ALPHA).toDouble() ? index.data(CalendarTable::CRT_ALPHA).toDouble() : 1;
if (!(QStyle::State_Enabled & option.state)) {
colorAlpha *= 0.5;
}
QColor tmpColor;
// 绘制背景
if (colorByRole(index, CalendarTable::CRT_BACKGROUND_COLOR, tmpColor, colorAlpha)) {
drawBackgroud(painter, option.rect, tmpColor);
}
// 绘制边框
if (colorByRole(index, CalendarTable::CRT_BORDER_COLOR, tmpColor, colorAlpha)) {
drawBorder(painter, option.rect, tmpColor);
}
// 绘制文字
if (colorByRole(index, CalendarTable::CRT_FONT_COLOR, tmpColor, colorAlpha)) {
drawText(painter, option.rect, tmpColor, index.data().toString());
}
// 绘制文字下方点
if (colorByRole(index, CalendarTable::CRT_POINT_COLOR, tmpColor, colorAlpha)) {
int radius = 1;
QRect tmpRect(option.rect.x() + option.rect.width() / 2 - radius, option.rect.y() + option.rect.height() * 6 / 7 - radius, 2 * radius, 2 * radius);
drawPoint(painter, tmpRect, tmpColor);
}
// 绘制鼠标悬浮边框
if (colorByRole(index, CalendarTable::CRT_HOVER_BORDER_COLOR, tmpColor, colorAlpha)) {
if (index == m_hoverIndex && (QStyle::State_Enabled & option.state))
drawBorder(painter, option.rect, tmpColor);
}
//QStyledItemDelegate::paint(painter, option, index);
}
bool CalendarDelegage::colorByRole(const QModelIndex& modelIndex, int role, QColor& tmpColor, double alpha) const
{
QVariant var = modelIndex.data(role);
if (var.canConvert<QColor>()) {
tmpColor = var.value<QColor>();
if (alpha)
tmpColor.setAlphaF(alpha);
return true;
}
return false;
}
bool CalendarDelegage::eventFilter(QObject *obj, QEvent *ev)
{
if(obj == m_parentView->viewport()){
if(QEvent::HoverMove == ev->type()){
QPoint pos = m_parentView->viewport()->mapFromGlobal(QCursor::pos());
m_hoverIndex = m_parentView->indexAt(pos);
}else if(QEvent::Leave == ev->type()){
m_hoverIndex = QModelIndex();
}
}
return DrawBaseDelegage::eventFilter(obj, ev);
}
CalendarTable::CalendarTable(QWidget *parent, int year, int month)
: QTableWidget(parent)
, m_year(year)
, m_month(month)
, m_fristDayOnWeek(7)
, m_maxDate(QDate(year+6,1,1).addDays(-1))
, m_minDate(QDate(year-6,1,1))
, m_selMode(CSM_SINGLE_SELECTION)
{
QString tableStyle = "QTableWidget#calendar_table { gridline-color: rgb(250,250,250); border:0px; background-color: transparent; }";
QString headerStyle = "QHeaderView#calendar_tableHeader { font: 14px; background-color: transparent; } QHeaderView#calendar_tableHeader::section { background: transparent;border-right: 1px solid transparent;}";
this->setObjectName("calendar_table");
this->horizontalHeader()->setObjectName("calendar_tableHeader");
this->setStyleSheet(tableStyle);
this->horizontalHeader()->setStyleSheet(headerStyle);
this->setRowCount(6);
this->setColumnCount(7);
this->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
this->verticalHeader()->setSectionResizeMode(QHeaderView::Stretch);
this->verticalHeader()->hide();
this->setSelectionMode(QAbstractItemView::NoSelection);
this->setEditTriggers(QAbstractItemView::NoEditTriggers);
this->installEventFilter(this);
m_delegage = new CalendarDelegage(this);
this->setItemDelegate(m_delegage);
refreshCalendar();
refreshCalendarHeader();
connect(this, &QTableWidget::itemClicked, this, &CalendarTable::slot_itemClicked);
}
bool CalendarTable::setYearMonth(int year, int month)
{
year += (month - 1 + 12)/12 - 1;
month = (month - 1 + 12)%12 + 1;
if(year > m_maxDate.year() || year < m_minDate.year()){
return false;
}
else if(QDate(year, month, 1) > QDate(m_maxDate.year(),m_maxDate.month(), 1)){
month = m_maxDate.month();
}
else if(QDate(year, month, 1) < QDate(m_minDate.year(),m_minDate.month(), 1)){
month = m_minDate.month();
}
if(m_year == year && m_month == month)
return false;
m_year = year;
m_month = month;
refreshCalendar();
return true;
}
void CalendarTable::setFristDayOnWeek(int fristDayOnWeek)
{
m_fristDayOnWeek = fristDayOnWeek;
refreshCalendarHeader();
}
void CalendarTable::setSelMode(CalendarTable::CalendarSelectionMode selectionMode)
{
m_selMode = selectionMode;
clearSelectedDate();
}
void CalendarTable::addSelectedDate(QDate date)
{
if(appendSelection(date))
refreshSelection();
}
void CalendarTable::clearSelectedDate()
{
m_selDate.clear();
emit sig_selectionChanged();
refreshSelection();
}
void CalendarTable::setMaxDate(QDate date)
{
if(m_maxDate == date)
return;
m_maxDate = date;
if(QDate(m_year, m_month, m_maxDate.day()) > m_maxDate){
setYearMonth(m_maxDate.year(), m_maxDate.month());
}
emit sig_maxDateChanged(date);
}
void CalendarTable::setMinDate(QDate date)
{
if(m_minDate == date)
return;
m_minDate = date;
if(QDate(m_year, m_month, m_minDate.day()) < m_minDate){
setYearMonth(m_minDate.year(), m_minDate.month());
}
emit sig_minDateChanged(date);
}
bool CalendarTable::eventFilter(QObject *obj, QEvent *ev)
{
if(obj == this){
if (ev->type() == QEvent::Wheel && isEnabled()) {
auto wheelEv = static_cast<QWheelEvent*>(ev);
if(wheelEv->delta() > 0){// 当滚轮远离使用者时
setYearMonth(m_year, m_month-1);
}else{
setYearMonth(m_year, m_month+1);
}
ev->accept();
return true;
}
}
return QWidget::eventFilter(obj, ev);
}
void CalendarTable::slot_itemClicked(QTableWidgetItem *item)
{
if(!item->data(CRT_DATE).canConvert<QDate>())
return;
QDate tmpDate = item->data(CRT_DATE).value<QDate>();
if (m_selDate.contains(tmpDate)) {
if (!removeSelection(tmpDate))
return;
}
else {
if (!appendSelection(tmpDate))
return;
}
if(tmpDate.month() != m_month){
setYearMonth(tmpDate.year(), tmpDate.month());
}else{
refreshSelection();
}
}
void CalendarTable::refreshCalendar()
{
// 计算日历第一天
QDate curDate;
curDate.setDate(m_year, m_month, 1);
int fillDays = curDate.dayOfWeek()-m_fristDayOnWeek;
if(fillDays>0){
curDate = curDate.addDays(-fillDays); //开始日期
}else{
curDate = curDate.addDays(-fillDays-7);
}
// 日历填充
int maxItemCount = this->columnCount()*this->rowCount();
for (int var = 0; var < maxItemCount; ++var){
QTableWidgetItem *item = new QTableWidgetItem();
item->setData(Qt::DisplayRole, curDate.day());
item->setData(CRT_DATE, curDate);
item->setTextAlignment(Qt::AlignCenter);
item->setData(CRT_FONT_COLOR, QColor(93,93,93));
item->setData(CRT_HOVER_BORDER_COLOR, QColor(0,186,218));
// 非本月字体显示灰色
if(curDate.month() != m_month){
item->setData(CRT_ALPHA, 0.2);
}
this->setItem(var/this->columnCount(),var%this->columnCount(), item);
curDate = curDate.addDays(1);
}
refreshSelection();
emit sig_refresh(m_year, m_month);
}
void CalendarTable::refreshCalendarHeader()
{
QStringList strList = QString("Mon,Tue,Wed,Thu,Fri,Sat,Sun").split(",");
QStringList tmpList;
for(int i=0; i<this->columnCount(); i++){
tmpList << strList.at((i+m_fristDayOnWeek-1)%7);
}
this->setHorizontalHeaderLabels(tmpList);
}
void CalendarTable::refreshSelection()
{
for(int row=0; row<this->rowCount(); row++){
for(int col=0; col<this->columnCount(); col++){
auto item = this->item(row, col);
if(m_selDate.contains(this->item(row, col)->data(CRT_DATE).value<QDate>())){
// 已选中背景显示蓝色
this->item(row, col)->setData(CRT_BACKGROUND_COLOR, QColor(0,186,218));
}else{
item->setData(CRT_BACKGROUND_COLOR, QVariant());
}
if(item->data(CRT_DATE).value<QDate>() == QDate::currentDate()){
if(item->data(CRT_BACKGROUND_COLOR).canConvert<QColor>()){
item->setData(CRT_POINT_COLOR, QColor(Qt::white));
}else{
item->setData(CRT_POINT_COLOR, QColor(0,186,218));
}
}
}
}
update();
}
bool CalendarTable::appendSelection(QDate date)
{
if (m_selDate.contains(date))
return false;
if(date > m_maxDate || date < m_minDate)
return false;
switch(m_selMode){
case CSM_SINGLE_SELECTION:
m_selDate.clear();
m_selDate.append(date);
break;
case CSM_MULTI_SELECTION:
m_selDate.append(date);
break;
default:
return false;
}
emit sig_selectionChanged();
emit sig_selectionAdded(date);
return true;
}
bool CalendarTable::removeSelection(QDate date)
{
if (!m_selDate.contains(date))
return false;
// 单选则保证一定有一个选中
if (m_selMode == CSM_SINGLE_SELECTION && m_selDate.count() <= 1) {
return false;
}
m_selDate.removeAll(date);
return true;
}
CalendarWidget::CalendarWidget(QWidget *parent, int year, int month)
: QWidget(parent)
, m_dateStr("%1 year %2 month")
{
// 构造
m_calendar = new CalendarTable(this, year, month);
m_title = new QLabel("title", this);
m_displayLabel = new QLabel(m_dateStr.arg(m_calendar->year()).arg(m_calendar->month()));
m_displayLabel->setAlignment(Qt::AlignCenter);
m_preYearBtn = new QPushButton("<<", this);
m_preMonthBtn = new QPushButton("<", this);
m_nextMonthBtn = new QPushButton(">", this);
m_nextYearBtn = new QPushButton(">>", this);
m_preYearBtn->adjustSize();
m_preMonthBtn->adjustSize();
m_nextMonthBtn->adjustSize();
m_nextYearBtn->adjustSize();
refreshNextEnable();
refreshPreEnable();
// 信号槽关联
connect(m_calendar, &CalendarTable::sig_refresh, this, &CalendarWidget::slot_calendarRefresh);
connect(m_calendar, &CalendarTable::sig_refresh, this, &CalendarWidget::sig_calendarRefresh);
connect(m_calendar, &CalendarTable::sig_maxDateChanged, this, &CalendarWidget::slot_calendarMaxDateChanged);
connect(m_calendar, &CalendarTable::sig_minDateChanged, this, &CalendarWidget::slot_calendarMinDateChanged);
connect(m_calendar, &CalendarTable::sig_selectionChanged, this, &CalendarWidget::sig_calendarSelectionChanged);
connect(m_calendar, &CalendarTable::sig_selectionAdded, this, &CalendarWidget::sig_calendarSelectionAdded);
connect(m_preMonthBtn, &QPushButton::clicked, this, &CalendarWidget::slot_preMonth);
connect(m_nextMonthBtn, &QPushButton::clicked, this, &CalendarWidget::slot_nextMonth);
connect(m_preYearBtn, &QPushButton::clicked, this, &CalendarWidget::slot_preYear);
connect(m_nextYearBtn, &QPushButton::clicked, this, &CalendarWidget::slot_nextYear);
// 布局
auto titleLayout = new QHBoxLayout;
titleLayout->addWidget(m_title);
titleLayout->addStretch();
auto btnLayout = new QHBoxLayout;
btnLayout->addWidget(m_preYearBtn);
btnLayout->addWidget(m_preMonthBtn);
btnLayout->addWidget(m_displayLabel);
btnLayout->addWidget(m_nextMonthBtn);
btnLayout->addWidget(m_nextYearBtn);
auto backgroundWidget = new QFrame(this);
backgroundWidget->setObjectName("calendar_background");
QString backgrounStyle = "QFrame#calendar_background{border: 1px solid rgb(0,171,201);background: rgb(250,250,250);}";
backgroundWidget->setStyleSheet(backgrounStyle);
m_mainLayout = new QVBoxLayout(backgroundWidget);
m_mainLayout->addLayout(titleLayout);
m_mainLayout->addLayout(btnLayout);
m_mainLayout->addWidget(m_calendar);
auto mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(backgroundWidget);
}
void CalendarWidget::setTitle(QString title)
{
if (m_title->isHidden())
m_title->show();
m_title->setText(title);
}
void CalendarWidget::slot_calendarRefresh(int year, int month)
{
m_displayLabel->setText(m_dateStr.arg(year).arg(month));
refreshNextEnable();
refreshPreEnable();
}
void CalendarWidget::slot_calendarMaxDateChanged(const QDate &date)
{
Q_UNUSED(date);
refreshNextEnable();
}
void CalendarWidget::slot_calendarMinDateChanged(const QDate &date)
{
Q_UNUSED(date);
refreshPreEnable();
}
void CalendarWidget::slot_preMonth()
{
m_calendar->setYearMonth(m_calendar->year(), m_calendar->month()-1);
}
void CalendarWidget::slot_nextMonth()
{
m_calendar->setYearMonth(m_calendar->year(), m_calendar->month()+1);
}
void CalendarWidget::slot_preYear()
{
m_calendar->setYearMonth(m_calendar->year()-1, m_calendar->month());
}
void CalendarWidget::slot_nextYear()
{
m_calendar->setYearMonth(m_calendar->year()+1, m_calendar->month());
}
void CalendarWidget::refreshNextEnable()
{
m_nextYearBtn->setEnabled(m_calendar->year() < m_calendar->maxDate().year());
m_nextMonthBtn->setEnabled(QDate(m_calendar->year(), m_calendar->month(), 1) < QDate(m_calendar->maxDate().year(), m_calendar->maxDate().month(), 1));
}
void CalendarWidget::refreshPreEnable()
{
m_preYearBtn->setEnabled(m_calendar->year() > m_calendar->minDate().year());
m_preMonthBtn->setEnabled(QDate(m_calendar->year(), m_calendar->month(), m_calendar->minDate().day()) > QDate(m_calendar->minDate().year(), m_calendar->minDate().month(), 1));
}
cpp的内容具体就看代码吧,不详细说了
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
auto calendar = new CalendarWidget(this);
calendar->setSelMode(CalendarTable::CSM_MULTI_SELECTION);
this->setCentralWidget(calendar);
}
MainWindow::~MainWindow()
{
delete ui;
}
MainWindow里面设置了将CalendarWidget设为多选,也就是上面gif里面展示的效果