Overview
This article details the implementation of a custom date range picker widget using Qt and C++. Unlike standard widgets like QDateEdit, this control allows users to select a specific time interval (start date to end date) through a custom-painted popup interface. The implementation relies heavily on QPainter for rendering, providing high flexibility for custom styling and visual effects.
The primary features of the control include:
- A main button (
DateRangeButton) that triggers the calendar popup. - A popup panel displaying two synchronized calendars for start and end dates.
- Validation ensuring the start date precedes the end date.
- Quick selection buttons (Today, Last Week, Last Month, etc.).
- Visual feedback: highlighted backgrounds for the selected range and circular indicators for specific endpoints.
Architecture
The solution is structured into four main classes to seperate concerns between data management, rendering, and user interaction:
- CalendarGrid: The core widget responsible for calculating dates and rendering the calendar grid via
paintEvent. - CalendarNavigator: A container widget wrapping the
CalendarGridwith month/year navigation controls. - RangeSelectionPanel: The popup frame containing two
CalendarNavigatorwidgets (Start/End), quick selection buttons, and confirmation controls. - DateRangeButton: The main entry point for the application, inheriting from
QPushButtonto manage the popup's visibility.
Implemantation Details
1. Main Entry Point: DateRangeButton
The DateRangeButton class is the public-facing component. It manages the interaction between the user and the selection panel.
class DateRangeButton : public QPushButton
{
Q_OBJECT
public:
explicit DateRangeButton(QWidget *parent = nullptr);
~DateRangeButton();
// Preset modes for quick initialization
enum class PresetMode {
Today,
CurrentWeek,
CurrentMonth,
CurrentYear,
Custom
};
void setPreset(PresetMode mode);
void getRange(unsigned short &startYear, unsigned short &startMonth, unsigned short &startDay,
unsigned short &endYear, unsigned short &endMonth, unsigned short &endDay);
signals:
void selectionConfirmed();
private slots:
void onButtonClicked();
private:
RangeSelectionPanel *m_selectionPanel;
PresetMode m_currentMode;
};
2. The Popup Panel: RangeSelectionPanel
The panel organizes the layout into three vertical sections: quick presets, the dual calendar views, and action buttons. It handles the logic to ensure the start date remains earlier than the end date.
class RangeSelectionPanel : public QFrame
{
Q_OBJECT
public:
// Getters for start/end dates
void getStartDate(unsigned short &year, unsigned short &month, unsigned short &day);
void getEndDate(unsigned short &year, unsigned short &month, unsigned short &day);
void applyPreset(DateRangeButton::PresetMode mode);
signals:
void confirmed();
private:
void setupLayout();
CalendarNavigator *m_startCalendar;
CalendarNavigator *m_endCalendar;
// ... other UI members
};
3. Rendering the Calendar: CalendarGrid
The CalendarGrid class is the most complex component, handling all rendering logic. It draws the week headers, day numbers, and the range selection highlighting.
Painting Workflow
The paintEvent orchestrates the rendering order:
void CalendarGrid::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
// 1. Calculate grid data (dates, positions)
updateGridData();
// 2. Draw the range background first (so text appears on top)
renderRangeBackground(painter);
// 3. Draw week day headers
renderWeekHeaders(painter);
// 4. Draw day numbers and selection indicators
renderDayNumbers(painter);
}
Drawing Week Headers
Headers are drawn based on calculated column positions.
void CalendarGrid::renderWeekHeaders(QPainter &painter)
{
const QString headers[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
painter.save();
QFontMetrics metrics(painter.font());
for (int col = 0; col < 7; ++col) {
QRect rect(getColumnLeft(col), m_topMargin,
getColumnWidth(), metrics.height());
painter.setPen(QColor("#838D9E"));
painter.drawText(rect, Qt::AlignCenter, headers[col]);
}
painter.restore();
}
Drawing the Range Highlight
One of the key visual features is the continuous highlight between the start and end dates. This requires checking each cell to determine if it falls within the selected range and drawing the appropriate shape (rounded rect for endpoints, plain rect for middle segments).
void CalendarGrid::renderRangeBackground(QPainter &painter)
{
painter.save();
QDate currentDate = QDate::currentDate();
for (int row = 0; row < m_rowCount; ++row) {
for (int col = 0; col < 7; ++col) {
int index = row * 7 + col;
QRect cellRect = m_cellRects[index];
// Determine if this cell is within the selection range
SelectionStatus status = getSelectionStatus(index);
if (status == SelectionStatus::None) continue;
// Adjust rect for visual padding
QRect bgRect = cellRect.adjusted(0, 3, 0, -3);
QPainterPath path;
switch (status) {
case SelectionStatus::Start:
// Left side rounded
bgRect.adjust(4, 0, 0, 0);
path.addRoundedRect(bgRect, bgRect.height()/2, bgRect.height()/2);
// Fill the right side to connect with the next cell
path.addRect(bgRect.adjusted(bgRect.width()/2, 0, 0, 0));
break;
case SelectionStatus::End:
// Right side rounded
bgRect.adjust(0, 0, -4, 0);
path.addRoundedRect(bgRect, bgRect.height()/2, bgRect.height()/2);
path.addRect(bgRect.adjusted(0, 0, -bgRect.width()/2, 0));
break;
case SelectionStatus::Middle:
// Flat rectangle connecting start and end
path.addRect(bgRect);
break;
case SelectionStatus::Single:
// Single day selection (circle)
path.addEllipse(bgRect.center(), 12, 12);
break;
}
painter.fillPath(path, QColor("#E3F2FD"));
}
}
painter.restore();
}
Drawing Day Numbers
Finally, the day numbers are rendered. If a specific date matches the start or end point, a blue circle is drawn behind the text.
void CalendarGrid::renderDayNumbers(QPainter &painter)
{
painter.save();
for (int i = 0; i < m_totalCells; ++i) {
if (!m_cellDates[i].isValid()) continue;
QRect rect = m_cellRects[i];
bool isStart = (m_cellDates[i] == m_selectedStart);
bool isEnd = (m_cellDates[i] == m_selectedEnd);
// Draw selection circle for endpoints
if (isStart || isEnd) {
QPainterPath circlePath;
circlePath.addEllipse(rect.center(), 12, 12);
painter.fillPath(circlePath, QColor("#218CF2"));
painter.setPen(Qt::white);
} else {
painter.setPen(Qt::black);
}
painter.drawText(rect, Qt::AlignCenter, QString::number(m_cellDates[i].day()));
}
painter.restore();
}