自定义日历(二)
1、回顾
上一节自定义日历(一)中主要讲述了基于单独widget完全绘制的日历窗口,并对本节要讲的日历控件和上一节的日历窗口做出了优缺点对比,忘记了的同学可以去上一节浏览。单独widget绘制出来的日历窗口具有性能上的优势,而且内存开销比较小,但是使用起来的灵活性没有那么的强大,正所谓起点不同,所具有的优势就同,本节我将重点讲述基于label拼凑而成的日历控件,基于label拼凑的日历控件最小的基本单元就是label,它具有一般label所具有的所有特性,包括鼠标的一系列事件和键盘事件等。在对比上一节的日历窗口,这些基本的事件都需要我们自己去处理,然后派发给相应的区域,对应的也就是上一节的tDayFlag结构,更新这个结构中的内存数据。
为了行文方便,本节没有特殊注明的日历窗口都是本节所讲的日历窗口,是基于label拼凑而成。
本节所讲述的是途中上边这个日历窗口,不会随父窗口的大小变化而变化,如果需要对大小进行改变,需要主动调用每一个窗口的大小重置方法。
日历展示
2、结构划分
本节在讲述日历窗口做法时,想先从这个demo的结构上做以划分,然后针对每一个小模块做具体解释。
1、日期类:DayButton,用于绘制指定日期,代表日历上的每一天,主要的功能是在paintEvent方法中,根据内存状态绘制信息
2、月份上周名称:DateTitleWidget,用于绘制周一到周天,支持重置第一列从周几开始
3、月份日期:DateContentWidget,包含上一个月的天数+本月天数+下一个月天数
4、日历控件:CsutomDateTime:封装完成的日历控件,支持月份动态切换
上述1、2和3其实就可以组成一个日历,4只是为了增加动画效果后来补充上去的一个类。接下来我分别介绍这几个类的功能
3、日期类
日期类,是日历控件中最小的单元,是基于label重写的窗口,主要是用于展示日历上的每一天,他重写了鼠标进入和离开事件,支持hover状态记录,并根据内存中的一些状态进行绘制,具体绘制代码如下:
1 void DayButton::paintEvent(QPaintEvent * event) 2 { 3 QRect r = rect(); 4 5 QPainter painter(this); 6 painter.save(); 7 8 int w = r.width(); 9 10 if (mouseEnter) 11 { 12 painter.fillRect(r, QColor(250, 250, 250)); 13 } 14 else 15 { 16 painter.fillRect(r, QColor(200, 200, 250)); 17 } 18 19 if (isCheck)//画蓝色选中圈 20 { 21 painter.setBrush(QColor(0, 0, 255)); 22 painter.drawEllipse(r.center() - QPoint(0, RoundR/2), RoundR, RoundR); 23 } 24 else 25 { 26 if (date == truthDate)//今天底色红色 27 { 28 painter.setBrush(QColor(255, 0, 0)); 29 painter.drawEllipse(r.center() - QPoint(0, RoundR / 2), RoundR, RoundR); 30 } 31 } 32 33 34 QFontMetrics fontMetric(painter.font()); 35 if (date == truthDate)//"今" 36 { 37 painter.setPen(QPen(QColor(255, 255, 255, 255), 1)); 38 painter.setBrush(QColor(255, 255, 255)); 39 painter.drawText(r.center() - QPoint(fontMetric.width(QStringLiteral("今")) / 2, 0) 40 , QStringLiteral("今")); 41 } 42 else 43 { 44 if (date.month() != currentDisplayMonth) 45 { 46 painter.setPen(QPen(QColor(125, 125, 125, 255), 1)); 47 } 48 else 49 { 50 painter.setPen(QPen(QColor(0, 0, 0, 255), 1)); 51 } 52 painter.drawText(r.center() - QPoint(fontMetric.width(QString::number(date.day())) / 2, 0) 53 , QString::number(date.day())); 54 } 55 56 if (hasNews)//画有新闻小蓝色点标示 57 { 58 painter.setBrush(QColor(0, 0, 255)); 59 painter.setPen(QPen(QColor(255, 255, 255, 255), 0)); 60 painter.drawEllipse(r.center() + QPoint(1, 10), 3, 3); 61 } 62 63 painter.restore(); 64 65 // QPushButton::paintEvent(event); 66 }
4、月份上周名称
月份上的周名称,默认是第一列表示周1,同学们可以通过该类提供的ResetTitle接口进行重置第一列从周几开始。月份上的周名称其实就是用7个label组成的,分表表示周一到周天,然后通过property属性来表示哪几列是周末,然后文字颜色展示和其他几列不同。因为都是一些重复性的代码,而且冗长,这几我就不贴代码了,有兴趣的同学可以自行下载demo,并分析demo,如果有任何问题,欢迎联系我。
5、月份日期
理解了月份上周名称,理解月份上日期就比较容易了,月份上日历无非就是六行七列的label控件或者五行七列的label控件拼凑而成的一个大窗口,由于这个类中提供的接口比较多,我就不一一做以介绍了,下面直接上这个类的头文件,大部分的接口都是有注释的。
1 //月份数据 显示具体日期 2 class DateContentWidget : public QWidget 3 { 4 public: 5 DateContentWidget(QWidget * parent = nullptr); 6 ~DateContentWidget(); 7 8 public: 9 void SetDisplayMode(ShowType type);//暂时没用 10 11 void ResetMonth(unsigned short month);//更新月 12 void ResetYear(unsigned short year);//更新年 13 void ResetCurrentMonth(short month);//增、减指定月 14 void ResetCurrentMonth(const QDate & date);//设置当前日期 15 void ResetCurrentMonth(unsigned short year, unsigned short month);//设置日期内部最终调用函数 16 17 void ResetCurrentWeek(); 18 void ResetCurrentDay(); 19 20 void ResetFirstColumn(unsigned short day);//设置当前第一列为一周中哪一天 21 22 protected: 23 virtual bool eventFilter(QObject *, QEvent *) Q_DECL_OVERRIDE; 24 25 private: 26 void InitializeWeekContents();//初始化日期p窗口,内部包含3个小窗口,分布可以表示1 5 6行日期格式 27 void CreateWeekContents(unsigned short rowCount);//根据指定行数创建窗口,并加入到stacked布局中 28 void UpdateContents(unsigned short lastMonthLeftDay 29 , unsigned short daysofPreviousMonth 30 , unsigned short daysofCurrentMonth);//设置当前窗口(不同于“当前显示窗口”)的日期 31 void UpdateStates();//同时只有一个日期被选中 32 unsigned short currIndex = 1;//当前窗口内子窗口显示索引 33 34 private: 35 ShowType type = ShowType::Month; 36 unsigned short firstDayOfWeek = 1;//第一列表示一周中周1 37 unsigned short daysofCurrentMonth;//当前月总天数 38 unsigned short currentFirstWeek;//当前月1号为周几 39 unsigned short currentMonth;//当前月份 40 unsigned short currentYear;//当前年份 41 QStackedLayout * mainStacked = nullptr;//创建一系列包含不同行数的日期窗口 42 };
细心的同学就会发现InitializeWeekContents接口的注释上所说的1、5和6,这是什么意思呢?不卖官司了,主要是因为月份在展示的时候,很有可能会遇到5行的情况,或者6行的情况,1行是业务要求了,可以不做了解,但月份切换时如果窗口行数发送变化,这对于桌面程序来讲是一个比较难得转化,因此我在一个月份展示上存储了3个窗口,分别是1行、5行和6行的窗口,这个3个窗口使用stacklayout存储,在月份切换的时候,程序会提前把合适的窗口显示到布局的第一个。这么想想,一个日期窗口大概就有7+5*7+6*7个小窗口,想想还是挺多的。呵呵。。。当讲到下边实现动画切换的时候,这个控件数目会成倍数的网上增加,所以如果对性能要求高的同学还是看上一篇文章的日历实现方式比较靠谱自定义日历(一)
CreateWeekContents:私有函数是创建指定行数的日期窗口,参数一般传递1、5和6,实现实现如下:
1 void DateContentWidget::CreateWeekContents(unsigned short rowCount) 2 { 3 QWidget * widget = new QWidget(this); 4 QGridLayout * contentLauout = new QGridLayout; 5 contentLauout->setContentsMargins(0, 0, 0, 0); 6 contentLauout->setSpacing(columnSpace);//水平间距 7 8 for (int row = 0; row < rowCount; ++row) 9 { 10 for (int column = 0; column < 7; ++column) 11 { 12 DayButton * pushButton = new DayButton(widget); 13 14 pushButton->setFixedSize(QSize(titleColumnWidth, contentsHight / rowCount)); 15 pushButton->installEventFilter(this); 16 contentLauout->addWidget(pushButton, row, column); 17 } 18 } 19 widget->setLayout(contentLauout); 20 widget->setContentsMargins(0, 0, 0, 0); 21 22 mainStacked->addWidget(widget); 23 }
UpdateStates:更新日期选中状态,重置当前点击的日期
1 void DateContentWidget::UpdateStates() 2 { 3 if (QWidget * widget = mainStacked->widget(currIndex)) 4 { 5 if (QGridLayout * contentLauout = qobject_cast<QGridLayout *>(widget->layout())) 6 { 7 for (int row = 0; row < countbyIndex[currIndex]; ++row) 8 { 9 for (int column = 0; column < 7; ++column) 10 { 11 if (QLayoutItem * itemLauout = contentLauout->itemAtPosition(row, column)) 12 { 13 if (QPushButton * pushButtonItem = qobject_cast<QPushButton *>(itemLauout->widget())) 14 { 15 if (pushButtonItem == currentCheckedButton) 16 { 17 pushButtonItem->setProperty("isCheck", true); 18 } 19 else 20 { 21 pushButtonItem->setProperty("isCheck", false); 22 } 23 } 24 } 25 } 26 } 27 } 28 } 29 }
UpdateContents:用于更新日期的内容,代码我就不贴了,原理都是类似的
6、日历控件
日历控件时我们最终要使用的类,初始化一个类之后,就可以把该类加到布局中用于界面展示。老规矩先上这个类的接口,有注释的我就不解释了。
1 class CustomDateTime : public QWidget 2 { 3 Q_OBJECT 4 5 public: 6 CustomDateTime(QWidget *parent = 0); 7 ~CustomDateTime(); 8 9 public: 10 11 void SetFirstDayOfWeek(unsigned short day = 7);//设置第一列显示为一周中的哪一天 day为1-7 12 13 public slots : 14 void PreviousMonth();//上一月 15 void NextMonth();//下一月 16 void SetDisplayMonth(unsigned short month);//根据月份更新 17 void SetDisplayYear(unsigned short year);//根据年更新 18 19 private: 20 void Initialize(); 21 22 private: 23 DateTitleWidget * titleWidget = nullptr; 24 25 //当前日期 26 bool naimationCarriedout = false; 27 unsigned short naimationDuration = 100; 28 //一月日期显示窗口 29 std::map<unsigned short, DateContentWidget *> contents; 30 31 private: 32 };
同学们仔细看类接口中最后一个变量声明,是一个map,为什么要用map来存储月份上的日期窗口呢,因为我们要实现动画,就如最开始上面所展示的demo,虽然只有一个月份展示,其实是有3个月份存在的,只是其余两个月份作为备用月份存在,当月份切换的时候只需要移动相应月份的位置,位置移动结束后,在把月份上的数据更新。现在在想想,这个日历控件上的窗口有多少个,大约是月份上的日期窗口的3倍之多。
关于这个类,我就只讲述下月份切换时的动画,如下代码所示,QParallelAnimationGroup 类是qt中的并行动画类,他可以通知支持多个动画同时进行QPropertyAnimation是属性动画类,把类属性传递进行,他就可以工作,在设置上属性对应的开始和结束值,并附加上动画时长,一切都是那么的顺利。更多动画讲解请看qt 窗口动画
1 void CustomDateTime::PreviousMonth() 2 { 3 if (naimationCarriedout == true) 4 { 5 return; 6 } 7 naimationCarriedout = true; 8 9 if (currentDisplayMonth == 1) 10 { 11 currentDisplayMonth = 12; 12 currentDisplayYear -= 1; 13 } 14 else 15 { 16 --currentDisplayMonth; 17 } 18 19 //contents[currWidgetIndex]->ResetCurrentMonth(currentDisplayYear, currentDisplayMonth); 20 21 QPoint leftPos = contents[(currWidgetIndex + 2) % 3]->pos(); 22 QPoint centerPos = contents[(currWidgetIndex) % 3]->pos(); 23 QPoint rightPos = contents[(currWidgetIndex + 1) % 3]->pos(); 24 25 QPropertyAnimation * leaveAnimation = new QPropertyAnimation(contents[currWidgetIndex], "pos"); 26 leaveAnimation->setDuration(naimationDuration); 27 leaveAnimation->setStartValue(centerPos); 28 leaveAnimation->setEndValue(rightPos); 29 30 QPropertyAnimation * enterAnimation = new QPropertyAnimation(contents[(currWidgetIndex + 2) % 3], "pos"); 31 connect(leaveAnimation, &QPropertyAnimation::finished, this, [this]{ 32 contents[(currWidgetIndex + 2) % 3]->ResetCurrentMonth(-3); 33 }); 34 enterAnimation->setDuration(naimationDuration); 35 enterAnimation->setStartValue(leftPos); 36 enterAnimation->setEndValue(centerPos); 37 38 contents[(currWidgetIndex + 1) % 3]->move(leftPos); 39 40 QParallelAnimationGroup * parallelAnimation = new QParallelAnimationGroup(); 41 connect(parallelAnimation, &QParallelAnimationGroup::finished, this, [this]{naimationCarriedout = false; }); 42 parallelAnimation->addAnimation(leaveAnimation); 43 parallelAnimation->addAnimation(enterAnimation); 44 parallelAnimation->start(); 45 46 currWidgetIndex = (currWidgetIndex + 2) % 3; 47 } 48 49 void CustomDateTime::NextMonth() 50 { 51 if (naimationCarriedout == true) 52 { 53 return; 54 } 55 naimationCarriedout = true; 56 57 if (currentDisplayMonth == 12) 58 { 59 currentDisplayMonth = 1; 60 currentDisplayYear += 1; 61 } 62 else 63 { 64 ++currentDisplayMonth; 65 } 66 67 //contents[currWidgetIndex]->ResetCurrentMonth(currentDisplayYear, currentDisplayMonth); 68 69 QPoint leftPos = contents[(currWidgetIndex + 2) % 3]->pos(); 70 QPoint centerPos = contents[(currWidgetIndex) % 3]->pos(); 71 QPoint rightPos = contents[(currWidgetIndex + 1) % 3]->pos(); 72 73 QPropertyAnimation * leaveAnimation = new QPropertyAnimation(contents[currWidgetIndex], "pos"); 74 leaveAnimation->setDuration(naimationDuration); 75 leaveAnimation->setStartValue(centerPos); 76 leaveAnimation->setEndValue(leftPos); 77 78 QPropertyAnimation * enterAnimation = new QPropertyAnimation(contents[(currWidgetIndex + 1) % 3], "pos"); 79 connect(leaveAnimation, &QPropertyAnimation::finished, this, [this]{ 80 contents[(currWidgetIndex + 1) % 3]->ResetCurrentMonth(3); 81 }); 82 enterAnimation->setDuration(naimationDuration); 83 enterAnimation->setStartValue(rightPos); 84 enterAnimation->setEndValue(centerPos); 85 86 contents[(currWidgetIndex + 2) % 3]->move(rightPos); 87 88 QParallelAnimationGroup * parallelAnimation = new QParallelAnimationGroup(this); 89 connect(parallelAnimation, &QParallelAnimationGroup::finished, this, [this]{naimationCarriedout = false; }); 90 parallelAnimation->addAnimation(leaveAnimation); 91 parallelAnimation->addAnimation(enterAnimation); 92 parallelAnimation->start(); 93 94 currWidgetIndex = (currWidgetIndex + 1) % 3; 95 }
7、label拼凑日历和widget自绘比较
说道这儿,我又想对于这两种日历实现进行一下小小的感慨。label拼凑的日历控件我也大概的描述完成了,说实在的,label拼凑而成的日历控件并不怎么好,就拿日历5行和6行切换来说,就要搞这么大一个阵仗,当然了其实这里边也是有可以优化的地方,比如6行到5行只需要隐藏最小边一行,总的看来我还是比较倾向于widget自绘,虽然每次都是整个窗口重新绘制,但这也提升了容错能力,每次的数据都是最新的。
8、问题分析
本节我只是大概讲述了一个用label拼凑而成日历怎么形成的,本demo也只能供大家参考下,demo下载下来使用vs编译器+qt5.x应该是可以正常编译并运行。还有下面这些小问题,需要大家自己去完善这个demo,有兴趣或者有问题的可以联系我,这些功能我基本都已经实现,只是不方便放在demo里。
1、作为一个日历,窗口大小其实应该是可以调整的
2、作为一个日历,背景色是可以设置的
3、还包括一些其他的高质量的展示
9、demo下载
demo下载:自绘制日历控件