1. Overview
The previous installment covered a fully custom-drawn calendar widget based on a single widget component. This approach offers performance benefits and minimal memory footprint. However, it lacks flexibility in handling user interactions. This article focuses on an alternative implementation: a calendar widget assembled from multiple label-based components.
Unlike the self-drawn approach, this label-based calendar uses individual DayButton widgets as the fundamental building blocks. Each DayButton inherits standard widget behaviors including mouse events and keyboard interactions, which simplifies event handling compared to the previous implementation.
The calendar discussed in this article is the top component in the demonstration—its size remains fixed and does not adapt to parent window resizing.
2. Architecture Overview
The calendar implementation is divided into four distinct components:
- DayButton: Represents a single day on the calendar, handling visual rendering based on internal state
- DateTitleWidget: Displays the weekday headers (Monday through Sunday)
- DateContentWidget: Contains the calendar grid showing days from previous, current, and next months
- CustomDateTime: The complete calendar widget supporting animated month transitions
Components 1, 2, and 3 form a functional calendar unit. Component 4 adds animation capabilities.
3. DayButton Class
The DayButton serves as the smallest unit in the calendar hierarchy. This class extends QLabel to provide day display functionality with hover state tracking and custom painting:
1 void DayButton::paintEvent(QPaintEvent*)
2 {
3 QRect bounds = rect();
4 QPainter painter(this);
5 painter.setRenderHint(QPainter::Antialiasing);
6
7 // Background based on hover state
8 QColor bgColor = mouseEnter ? QColor(240, 245, 255) : QColor(210, 215, 240);
9 painter.fillRect(bounds, bgColor);
10
11 // Draw selection indicator or today marker
12 if (isSelected)
13 {
14 painter.setBrush(QColor(30, 120, 220));
15 painter.drawEllipse(bounds.center() - QPoint(0, indicatorRadius), indicatorRadius, indicatorRadius);
16 }
17 else if (displayedDate == actualDate)
18 {
19 painter.setBrush(QColor(220, 50, 50));
20 painter.drawEllipse(bounds.center() - QPoint(0, indicatorRadius), indicatorRadius, indicatorRadius);
21 }
22
23 // Render date text
24 QFontMetrics metrics(painter.font());
25 if (displayedDate == actualDate)
26 {
27 painter.setPen(QPen(QColor(255, 255, 255), 1));
28 painter.setBrush(Qt::white);
29 QString todayMark = QStringLiteral("T");
30 painter.drawText(bounds.center() - QPoint(metrics.horizontalAdvance(todayMark) / 2, 0), todayMark);
31 }
32 else
33 {
34 QColor textColor = (displayedDate.month() != visibleMonth)
35 ? QColor(140, 140, 140) : QColor(30, 30, 30);
36 painter.setPen(QPen(textColor, 1));
37 painter.drawText(bounds.center() - QPoint(metrics.horizontalAdvance(QString::number(displayedDate.day())) / 2, 0),
38 QString::number(displayedDate.day()));
39 }
40
41 // Draw event indicator dot
42 if (hasEvents)
43 {
44 painter.setBrush(QColor(50, 100, 200));
45 painter.setPen(Qt::NoPen);
46 painter.drawEllipse(bounds.center() + QPoint(2, 8), 2, 2);
47 }
48 }
4. DateTitleWidget Class
The DateTitleWidget displays weekday headers using seven label instacnes. Each label represents a day of the week. The class provides a ResetTitle method allowing configuration of which day appears in the first column. Styling differentiates weekend columns from weekday columns through color variations.
5. DateContentWidget Class
This widget manages the calendar grid display, typically containing either five or six rows of seven days. The implementation maintains three separate views (for one, five, and six rows) stored in a stacked layout to handle row count transitions smoothly.
1 class DateContentWidget : public QWidget
2 {
3 public:
4 DateContentWidget(QWidget* parent = nullptr);
5 ~DateContentWidget();
6
7 public:
8 void SetDisplayMode(ShowType mode);
9 void ResetMonth(unsigned short month);
10 void ResetYear(unsigned short year);
11 void AdjustMonthBy(short offset);
12 void SetCurrentDate(const QDate& date);
13 void SetCurrentDate(unsigned short year, unsigned short month);
14 void SetFirstWeekday(unsigned short day);
15
16 protected:
17 bool eventFilter(QObject*, QEvent*) override;
18
19 private:
20 void InitializeContentViews();
21 void CreateContentView(unsigned short rowCount);
22 void RefreshContent(unsigned short prevDays, unsigned short prevTotal, unsigned short currentTotal);
23 void RefreshSelectionState();
24
25 private:
26 unsigned short viewIndex = 1;
27 ShowType displayType = ShowType::Month;
28 unsigned short weekStartDay = 1;
29 unsigned short daysInMonth;
30 unsigned short firstWeekday;
31 unsigned short visibleMonth;
32 unsigned short visibleYear;
33 QStackedLayout* stackedLayout = nullptr;
34 };
The CreateContentView method generates a grid of DayButton instances:
1 void DateContentWidget::CreateContentView(unsigned short rowCount)
2 {
3 QWidget* container = new QWidget(this);
4 QGridLayout* grid = new QGridLayout;
5 grid->setContentsMargins(0, 0, 0, 0);
6 grid->setSpacing(columnSpacing);
7
8 for (int row = 0; row < rowCount; ++row)
9 {
10 for (int col = 0; col < 7; ++col)
11 {
12 DayButton* dayBtn = new DayButton(container);
13 dayBtn->setFixedSize(QSize(columnWidth, rowHeight / rowCount));
14 dayBtn->installEventFilter(this);
15 grid->addWidget(dayBtn, row, col);
16 }
17 }
18 container->setLayout(grid);
19 container->setContentsMargins(0, 0, 0, 0);
20 stackedLayout->addWidget(container);
21 }
The RefreshSelectionState method ensures only one date is selected at a time:
1 void DateContentWidget::RefreshSelectionState()
2 {
3 if (QWidget* container = stackedLayout->widget(viewIndex))
4 {
5 if (QGridLayout* grid = qobject_cast<QGridLayout*>(container->layout()))
6 {
7 for (int row = 0; row < rowCounts[viewIndex]; ++row)
8 {
9 for (int col = 0; col < 7; ++col)
10 {
11 if (QLayoutItem* item = grid->itemAtPosition(row, col))
12 {
13 if (QPushButton* btn = qobject_cast<QPushButton*>(item->widget()))
14 {
15 btn->setProperty("selected", btn == activeButton);
16 }
17 }
18 }
19 }
20 }
21 }
22 }
6. CustomDateTime Class
The CustomDateTime class wraps the calendar components and provides animated month transitions. It maintains three DateContentWidget instances in a map for smooth sliding animations:
1 class CustomDateTime : public QWidget
2 {
3 Q_OBJECT
4
5 public:
6 explicit CustomDateTime(QWidget* parent = nullptr);
7 ~CustomDateTime();
8
9 void SetFirstWeekday(unsigned short day = 7);
10
11 public slots:
12 void ShowPreviousMonth();
13 void ShowNextMonth();
14 void SetVisibleMonth(unsigned short month);
15 void SetVisibleYear(unsigned short year);
16
17 private:
18 void InitializeComponents();
19
20 private:
21 DateTitleWidget* headerWidget = nullptr;
22 bool animationInProgress = false;
23 unsigned short animationDuration = 150;
24 std::map<unsigned short, DateContentWidget*> monthViews;
25 };
The month transition uses QParallelAnimationGroup for synchronized animasions:
1 void CustomDateTime::ShowPreviousMonth()
2 {
3 if (animationInProgress) return;
4 animationInProgress = true;
5
6 if (visibleMonth == 1)
7 {
8 visibleMonth = 12;
9 --visibleYear;
10 }
11 else
12 {
13 --visibleMonth;
14 }
15
16 QPoint offScreenLeft = monthViews[(currentIndex + 2) % 3]->pos();
17 QPoint centerPos = monthViews[currentIndex]->pos();
18 QPoint offScreenRight = monthViews[(currentIndex + 1) % 3]->pos();
19
20 QPropertyAnimation* exitAnim = new QPropertyAnimation(monthViews[currentIndex], "pos");
21 exitAnim->setDuration(animationDuration);
22 exitAnim->setStartValue(centerPos);
23 exitAnim->setEndValue(offScreenRight);
24
25 QPropertyAnimation* entryAnim = new QPropertyAnimation(monthViews[(currentIndex + 2) % 3], "pos");
26 connect(exitAnim, &QPropertyAnimation::finished, this, [this]{
27 monthViews[(currentIndex + 2) % 3]->AdjustMonthBy(-3);
28 });
29 entryAnim->setDuration(animationDuration);
30 entryAnim->setStartValue(offScreenLeft);
31 entryAnim->setEndValue(centerPos);
32
33 monthViews[(currentIndex + 1) % 3]->move(offScreenLeft);
34
35 QParallelAnimationGroup* group = new QParallelAnimationGroup(this);
36 connect(group, &QParallelAnimationGroup::finished, this, [this]{ animationInProgress = false; });
37 group->addAnimation(exitAnim);
38 group->addAnimation(entryAnim);
39 group->start();
40
41 currentIndex = (currentIndex + 2) % 3;
42 }
43
44 void CustomDateTime::ShowNextMonth()
45 {
46 if (animationInProgress) return;
47 animationInProgress = true;
48
49 if (visibleMonth == 12)
50 {
51 visibleMonth = 1;
52 ++visibleYear;
53 }
54 else
55 {
56 ++visibleMonth;
57 }
58
59 QPoint offScreenLeft = monthViews[(currentIndex + 2) % 3]->pos();
60 QPoint centerPos = monthViews[currentIndex]->pos();
61 QPoint offScreenRight = monthViews[(currentIndex + 1) % 3]->pos();
62
63 QPropertyAnimation* exitAnim = new QPropertyAnimation(monthViews[currentIndex], "pos");
64 exitAnim->setDuration(animationDuration);
65 exitAnim->setStartValue(centerPos);
66 exitAnim->setEndValue(offScreenLeft);
67
68 QPropertyAnimation* entryAnim = new QPropertyAnimation(monthViews[(currentIndex + 1) % 3], "pos");
69 connect(exitAnim, &QPropertyAnimation::finished, this, [this]{
70 monthViews[(currentIndex + 1) % 3]->AdjustMonthBy(3);
71 });
72 entryAnim->setDuration(animationDuration);
73 entryAnim->setStartValue(offScreenRight);
74 entryAnim->setEndValue(centerPos);
75
76 monthViews[(currentIndex + 2) % 3]->move(offScreenRight);
77
78 QParallelAnimationGroup* group = new QParallelAnimationGroup(this);
79 connect(group, &QParallelAnimationGroup::finished, this, [this]{ animationInProgress = false; });
80 group->addAnimation(exitAnim);
81 group->addAnimation(entryAnim);
82 group->start();
83
84 currentIndex = (currentIndex + 1) % 3;
85 }
7. Implementation Comparison
The label-based approach offers straightforward event handling through Qt's built-in widget mechanisms. However, it introduces complexity in managing row transitions between five and six row layouts. The implementation maintains three pre-created views (one, five, and six rows) to handle these transitions smoothly.
In contrast, the self-drawn widget approach from the previous article redraws the entire calendar on each update. While this might seem less efficient, it ensures data consistency and simplifies state management. The self-drawn method is generally more suitable for performance-critical applications.
8. Potential Enhancements
This implementation serves as a foundation. Several improvement could be made:
- Dynamic widget sizing based on available space
- Configurable color schemes and themes
- Support for multiple date selection
- Keyboard navigation support
- Locale-aware date formatting
9. Source Code
Download the complete implementation: Custom Calendar Widget Source