Wiki

Implementing a Popup Calendar

by Mark Summerfield

We often need to constrain user input for particular widgets. Qt provides many ways of doing this, but for some complex data types we may want to provide our own "value picker" that lets the user pop up a little dialog through which they can choose a valid value. In this article we present a small calendar popup that demonstrates the necessary techniques.

For a popup it is usually most convenient to provide a static getData() function that presents the value picker, and that always returns a valid value.

Popup Calendar

The screenshot shows a Schedule dialog, with a popup calendar for the start date on top. The Schedule dialog might be used for entering events with a name, start date and end date. The dialog has two QDateEdits. If the user presses F2 on either date edit, or presses the "..." toolbar button to the right of one of the date edits, the calendar is popped up and the correct date edit is updated. The relevant parts of the Schedule's definition look like this:

    class Schedule : public <a href="/qdialog.html">QDialog</a>
    {
        ...
    private slots:
        void popupStartCalendar();
        void popupEndCalendar();
        void popupCalendar();
 
    private:
        void popupCalendar(<a href="/qdateedit.html">QDateEdit</a> *dateEdit);
 
        <a href="/qdateedit.html">QDateEdit</a> *startDate;
        <a href="/qdateedit.html">QDateEdit</a> *endDate;
        ...
    };
 

We keep a pointer to each date edit in the form and have one slot for each date edit, plus a slot that is invoked if the user presses a special key sequence, e.g. F2, and a slot that actually invokes the popup.

    Schedule::Schedule(<a href="/qwidget.html">QWidget</a> *parent, const char *name)
        : <a href="/qdialog.html">QDialog</a>(parent, name)
    {
        ...
        <a href="/qaccel.html">QAccel</a> *f2 = new <a href="/qaccel.html">QAccel</a>(this);
        f2->connectItem(f2->insertItem(Key_F2), this, SLOT(popupCalendar()));
        connect(startButton, SIGNAL(clicked()), this, SLOT(popupStartCalendar()));
        connect(endButton, SIGNAL(clicked()), this, SLOT(popupEndCalendar()));
    }
 
    void Schedule::popupStartCalendar()
    {
        popupCalendar(startDate);
    }
 
    void Schedule::popupEndCalendar()
    {
        popupCalendar(endDate);
    }
 
    void Schedule::popupCalendar()
    {
        if (startDate->hasFocus())
            popupCalendar(startDate);
        else if (endDate->hasFocus())
            popupCalendar(endDate);
    }
 

We create the keyboard accelerator in the constructor, and connect it to a generic popupCalendar() slot. This slot pops up the calendar for the date edit that has the focus, or does nothing if no date edit has the focus.

    void Schedule::popupCalendar(<a href="/qdateedit.html">QDateEdit</a> *dateEdit)
    {
        <a href="/qdate.html">QDate</a> date = PopupCalendar::getDate(
                        this, dateEdit->date(),
                        dateEdit->mapToGlobal(<a href="/qpoint.html">QPoint</a>(0, dateEdit->height())));
        dateEdit->setDate(date);
        dateEdit->setFocus();
    }
 

When we invoke the getDate() function we give it a parent, a default date (the date currently being shown in the relevant date edit), and a position. If we call mapToGlobal(QPoint(0, 0)) on a widget, the position returned is the widget's top-left corner (in global coordinates). In our case we want the popup to appear below the date edit widget it is invoked for, so we offset its y coordinate by the date edit's height. We need to use mapToGlobal() rather than pos() because we need global coordinates rather than the coordinates of the date edit in relation to its parent (the Schedule form).

The definition for our popup calendar looks like this:

    class PopupCalendar : public <a href="/qdialog.html">QDialog</a>
    {
        Q_OBJECT
    public:
        PopupCalendar(const <a href="/qdate.html">QDate</a> &day = <a href="/qdate.html">QDate</a>::currentDate(),
                      <a href="/qpoint.html">QPoint</a> pos = <a href="/qpoint.html">QPoint</a>(), <a href="/qwidget.html">QWidget</a> *parent = 0, const char *name = 0);
 
        static <a href="/qdate.html">QDate</a> getDate(<a href="/qwidget.html">QWidget</a> *parent = 0, const <a href="/qdate.html">QDate</a> &day = <a href="/qdate.html">QDate</a>::currentDate(),
                             <a href="/qpoint.html">QPoint</a> pos = <a href="/qpoint.html">QPoint</a>());
 
        const <a href="/qdate.html">QDate</a> &day() const { return today; }
        void setDay(const <a href="/qdate.html">QDate</a> &day);
        <a href="/qsize.html">QSize</a> sizeHint() const;
 
    protected:
        void mousePressEvent(<a href="/qmouseevent.html">QMouseEvent</a> *event);
        void mouseDoubleClickEvent(<a href="/qmouseevent.html">QMouseEvent</a> *) { accept(); }
        void keyPressEvent(<a href="/qkeyevent.html">QKeyEvent</a> *event);
        void paintEvent(<a href="/qpaintevent.html">QPaintEvent</a> *event);
 
    private:
        enum { ROWS = 6, COLS = 7 };
 
        <a href="/qfont.html">QFont</a> smallFont;
        <a href="/qdate.html">QDate</a> today;
        const <a href="/qdate.html">QDate</a> original;
    };
 

We will discuss each function, and the private variables, as we review the implementation. The mouseDoubleClickEvent() causes the popup (which is a dialog) to close; this event always occurs after a mousePressEvent().

    PopupCalendar::PopupCalendar(const <a href="/qdate.html">QDate</a> &day, <a href="/qpoint.html">QPoint</a> pos,
                                 <a href="/qwidget.html">QWidget</a> *parent, const char *name)
        : <a href="/qdialog.html">QDialog</a>(parent, name), today(day), original(day)
    {
        setCaption(tr("Date Picker"));
        if (!pos.isNull())
            move(pos);
        smallFont = font();
        if (smallFont.pointSize() >= 10) {
            smallFont.setPointSize(smallFont.pointSize() - 2);
            smallFont.setBold(true);
        }
        setFixedSize(sizeHint());
        setFocusPolicy(StrongFocus);
    }
 

The constructor sets the private variables day (which holds the calendar's current date) and original (which holds the original date the calendar is given). If a position is given we move the popup to that position; otherwise the popup will appear centered over its parent (if one is specified) since this is Qt's default behavior.

We could have simply used the user's default font, but we've decided that if their font is large enough we will use a two point smaller bold font instead to make the popup a little more compact.

There is no advantage to the user of expanding or contracting the popup because of the way we've chosen to paint it, so we set its size to the sizeHint(), which we'll review next.

    <a href="/qsize.html">QSize</a> PopupCalendar::sizeHint() const
    {
        <a href="/qfontmetrics.html">QFontMetrics</a> fm(smallFont);
        return <a href="/qsize.html">QSize</a>(COLS * fm.width(tr("Wed")), (ROWS + 2) * fm.height() * 1.2);
    }
 

The COLS and ROWS constants are declared in the header to give us a 7 days Ч 6 weeks grid. We calculate the width to be the COLS (days) Ч the width of "Wed" (Wednesday, the widest of the three-letter weekday abbreviations). For the height we multiply the height of the font by 1.2 to get the height of a single row; we calculate for two more than the number of rows to allow for the year/month and days of the week shown at the top of the popup.

    <a href="/qdate.html">QDate</a> PopupCalendar::getDate(<a href="/qwidget.html">QWidget</a> *parent, const <a href="/qdate.html">QDate</a> &day, <a href="/qpoint.html">QPoint</a> pos)
    {
        PopupCalendar *calendar = new PopupCalendar(day, pos, parent);
        calendar->exec();
        <a href="/qdate.html">QDate</a> date = calendar->day();
        delete calendar;
        return date;
    }
 

The static getDate() function creates a new popup calendar and shows it modally with exec(). It returns the value the calendar returned which is either a date picked by the user or the original date if the user canceled.

    void PopupCalendar::setDay(const <a href="/qdate.html">QDate</a> &day)
    {
        today = day;
        update();
    }
 

When a new date is set, we assign it to the private today variable and call update() to schedule a repaint in Qt's event queue.

    void PopupCalendar::mousePressEvent(<a href="/qmouseevent.html">QMouseEvent</a> *event)
    {
        <a href="/qdate.html">QDate</a> day = today;
        int dayh2 = (height() / (ROWS + 2)) * 2;
        if (event->y() < dayh2) {
            if (event->x() < width() / 2)
                day = day.addMonths(-1);
            else
                day = day.addMonths(1);
        } else {
            day = <a href="/qdate.html">QDate</a>(today.year(), today.month(), 1);
            int xday = event->x() / (width() / COLS);
            int yday = (event->y() - dayh2) / ((height() - dayh2) / ROWS);
            day = day.addDays(xday + COLS * yday);
        }
        if (day != today)
            setDay(day);
    }
 

If the user clicks on the top left of the calendar we set the calendar's date back a month, and if they click on the top right we set the calendar's date forward a month. The dayh2 variable holds the height of the top of the calendar (above the rectangles that show the days themselves). If the user clicks on a day, we work out which day they clicked, counting days (by rows and columns from the first day of the current month), and set the calendar to that date.

    void PopupCalendar::keyPressEvent(<a href="/qkeyevent.html">QKeyEvent</a> *event)
    {
        int days = 0;
        switch (event->key()) {
            case Key_Left: days = -1; break;
            case Key_Right: days = 1; break;
            case Key_Up: days = -COLS; break;
            case Key_Down: days = COLS; break;
            case Key_PageUp: days = today.daysTo(today.addMonths(-1)); break;
            case Key_PageDown: days = today.daysTo(today.addMonths(1)); break;
            case Key_Home: days = today.daysTo(today.addYears(-1)); break;
            case Key_End: days = today.daysTo(today.addYears(1)); break;
            case Key_Escape: today = original; accept(); break;
            case Key_Space: // fallthrough
            case Key_Enter: // fallthrough
            case Key_Return: accept(); return;
            default: <a href="/qdialog.html">QDialog</a>::keyPressEvent(event); return;
        }
        <a href="/qdate.html">QDate</a> day = today.addDays(days);
        if (day != today)
            setDay(day);
    }
 

We must also cater for users who prefer to use the keyboard. We've reimplemented keyPressEvent() to intercept the key presses we're interested in. For example, the left and right arrow keys change the date by one day, and the up and down arrow keys change the date by a week. If the user cancels (by pressing Esc), we reset the calendar to the original date and call accept() to close the dialog. We pass any unhandled key press to the base class.

    void PopupCalendar::paintEvent(<a href="/qpaintevent.html">QPaintEvent</a> *event)
    {
        <a href="/qpainter.html">QPainter</a> painter(this);
        painter.setClipRegion(event->region());
        painter.setFont(smallFont);
        int w = width();
        int h = height();
        int dayh = h / (ROWS + 2);
        int dayw = w / COLS;
        <a href="/qrect.html">QRect</a> rect;
        painter.drawText(1, 1, w - 1, dayh - 1, AlignHCenter,
			 today.toString("<<  yyyy MMMM  >>"), -1, &rect);
        int y = dayh;
        <a href="/qdate.html">QDate</a> day(today.year(), today.month(), 1);
        int i;
 
        for (i = 0; i < COLS; ++i) {
            painter.drawText(dayw * i + 1, y, dayw, dayh - 1,
			     AlignHCenter, day.toString("ddd"), -1, &rect);
            day = day.addDays(1);
        }
        day = day.addDays(-COLS);
        y += dayh;
 
        for (int j = 0; j < ROWS; ++j)
            for (i = 0; i < COLS; ++i) {
                <a href="/qcolor.html">QColor</a> color = day == today ? colorGroup().mid() : colorGroup().light();
                painter.fillRect(dayw * i + 1, dayh * j + y + 1, dayw - 1, dayh - 1, color);
                painter.drawText(dayw * i + 2, dayh * j + y + 2, dayw,
				 dayh, AlignTop|AlignAuto, day.toString("d"));
                day = day.addDays(1);
            }
    }
 

The final function that we need to review is paintEvent(). We calculate the width and height of each column dayw and row dayh and then draw the year and month, along with some crude indicators ("<<" and ">>") at the top of the calendar. Next we find the first day of the current date's month and draw the days of the week along the top below the year and month. We then reset our day variable back to the beginning of the month and iterate over each row and column.

We draw a rectangle for each day, using a different color if the day being drawn is the calendar's current day. We draw each rectangle one pixel too small; this leaves a gap between each rectangle that produces the grid effect. All the drawing is done offset in the y-axis to allow for the year and month and day names that are drawn at the top. On top of each rectangle we draw the day of the month. We offset the text so that it isn't drawn right up to the top-left edge of the rectangle.

Conclusion

The popup calendar works quite nicely, but there are four improvements that we could consider making:

Creating small popup "value pickers" isn't difficult, especially if we derive our class from QDialog. The benefits of using QDialog include the exec() function which gives us modality and an event loop, and the accept() and reject() slots to programmatically close the popup when we want to.


Copyright © 2003 Trolltech Trademarks