Skip to content

Instantly share code, notes, and snippets.

@jniemann66
Last active April 22, 2018 22:55
Show Gist options
  • Save jniemann66/40a89ac1e30a1e49f260e02689bf3054 to your computer and use it in GitHub Desktop.
Save jniemann66/40a89ac1e30a1e49f260e02689bf3054 to your computer and use it in GitHub Desktop.
Qt Appointment/Diary Widget
#ifndef APPOINTMENTWIDGET_H
#define APPOINTMENTWIDGET_H
// AppointmentWidget.h : basic appointment (Diary) widget
// (implementation in header)
#include <QWidget>
#include <QStringList>
#include <QTime>
#include <QTimer>
#include <QPainter>
#include <QPaintEvent>
#include <QStyleOption>
#include <QDebug>
#include <QAbstractTextDocumentLayout>
#include <QTextDocument>
#include <cmath>
enum AppointmentWidgetTextFormat {
Plain,
Html
};
struct AppointmentWidgetItem {
QTime startTime;
QTime finishTime;
QString text;
QColor color;
AppointmentWidgetItem() : color{152, 251, 152, 192}{}
};
class AppointmentWidget : public QWidget
{
Q_OBJECT
public:
explicit AppointmentWidget(QWidget *parent = nullptr) : QWidget(parent),
displayHoursStart(0), displayHoursEnd(24), busHoursStart(0.0), busHoursEnd(24.0), warpEffect(true), selectQuantizeTo(0.25),
vMargin(30), hMargin(10), showBusinessHours(true),
defaultDuration(0.5), textFormat(Plain), selectionStart(-1.0)
{
textWidth = 60; // to-do: measure
nMinorLines = (1.0 / selectQuantizeTo) - 1;
displayHoursSpan = displayHoursEnd - displayHoursStart;
autopaintTimer.setInterval(15000);
connect(&autopaintTimer, &QTimer::timeout, this, &AppointmentWidget::onAutopaintTimeout);
}
QList<AppointmentWidgetItem> getAppointments() const
{
return appointments;
}
void setAppointments(const QList<AppointmentWidgetItem>& value)
{
appointments = value;
update();
}
void addAppointment(const AppointmentWidgetItem& appointment)
{
appointments.append(appointment);
update();
}
void clearAppointments()
{
appointments.clear();
update();
}
double convertTimeToYpos(const QTime& t) const
{
double vSpacing = (geometry().height() - 2 * vMargin) / displayHoursSpan;
double h = std::min(std::max(0.0, (t.hour() + t.minute() / 60.0) - displayHoursStart), (double)displayHoursSpan);
return vSpacing * h + vMargin;
}
void setDisplayHoursStart(int value)
{
displayHoursStart = value;
displayHoursSpan = displayHoursEnd - displayHoursStart;
}
void setDisplayHoursEnd(int value)
{
displayHoursEnd = value;
displayHoursSpan = displayHoursEnd - displayHoursStart;
}
void setBusHoursStart(double value)
{
busHoursStart = value;
}
void setBusHoursEnd(double value)
{
busHoursEnd = value;
}
int getVMargin() const
{
return vMargin;
}
void setVMargin(int numPixels)
{
vMargin = numPixels;
}
int getHMargin() const
{
return hMargin;
}
void setHMargin(int numPixels)
{
hMargin = numPixels;
}
void setWarpEffect(bool value)
{
warpEffect = value;
}
void setShowBusinessHours(bool value)
{
showBusinessHours = value;
}
void setDefaultDuration(double numHours)
{
defaultDuration = numHours;
}
QTime getSelectionStart() const
{
int h = std::floor(selectionStart);
int m = (selectionStart - h) * 60.0;
return QTime(h, m);
}
void setTextFormat(const AppointmentWidgetTextFormat& value)
{
textFormat = value;
}
QDate getDateShown() const
{
return dateShown;
}
void setDateShown(const QDate& value)
{
dateShown = value;
autopaintTimer.start();
}
protected:
void paintEvent(QPaintEvent* event) override
{
// apply stylesheet options
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
QTime currentTime(QTime::currentTime());
int currentHour = currentTime.hour();
int currentMinute = currentTime.minute();
int hourLabelToHide = -1;
bool drawNowIndicator = (dateShown == QDate::currentDate());
if(currentMinute < 10) {
hourLabelToHide = currentHour;
} else if (currentMinute > 50) {
hourLabelToHide = currentHour + 1;
}
auto rect = geometry();
if(warpEffect) {
// warping effect (blend between widget height and paint area height)
double h = (3 * rect.height() + event->rect().height() * 2) / 4;
rect.setHeight(h);
}
// set drawing parameters
int hLineStart = hMargin + textWidth + hMargin;
double vSpacing = (rect.height() - 2* vMargin) / displayHoursSpan;
int hLineEnd = rect.width() - hMargin;
int hMinorLineStart = hLineStart + (hMargin >> 1);
int hMinorLineEnd = hLineEnd - (hMargin >> 1);
int vMinorIncrement = selectQuantizeTo * vSpacing;
// set up pens, brushes and painter
QPen gridPen(QColor(128,128,128));
QPen minorGridPen(QColor(230,230,230));
QPen blackPen(QColor(64,64,64,192));
QPen shadePen(QColor(0,0,0,32));
QBrush shadeBrush(QColor(0, 0, 0, 16));
QPainter painter(this);
QFont f("Helvetica", 20);
painter.setFont(f);
// determine text height and offset
double tHeight = QFontMetricsF(f).height();
double tOffset = tHeight / 2;
// draw hour lines and minor gridlines
for(int hour = displayHoursStart; hour < displayHoursEnd; hour++) {
int v = vMargin + vSpacing * (hour - displayHoursStart);
painter.setPen(gridPen);
if(!drawNowIndicator || (hour != hourLabelToHide)) {
painter.drawText(hMargin, v - tOffset, textWidth, tHeight, Qt::AlignVCenter | Qt::AlignRight, hourNames[hour]);
}
painter.drawLine(hLineStart, v, hLineEnd, v);
int vv = v;
painter.setPen(minorGridPen);
for(int m = 0; m < nMinorLines; m++) {
vv += vMinorIncrement;
painter.drawLine(hMinorLineStart, vv, hMinorLineEnd, vv);
}
}
// draw last hour line
int vLastLine = vMargin + vSpacing * displayHoursSpan;
painter.setPen(gridPen);
painter.drawText(hMargin, vLastLine - tOffset, textWidth, tHeight, Qt::AlignVCenter | Qt::AlignRight, hourNames[displayHoursEnd]);
painter.drawLine(hLineStart, vLastLine, hLineEnd, vLastLine);
int appointmentWidth = hLineEnd - hLineStart;
if(showBusinessHours) { // shade after-hours
painter.setPen(shadePen);
painter.setBrush(shadeBrush);
painter.drawRect(0, 0, rect.width(), vMargin + vSpacing * (busHoursStart - displayHoursStart));
painter.drawRect(0, vMargin + vSpacing * (busHoursEnd - displayHoursStart), rect.width(), vMargin + vSpacing * displayHoursSpan);
}
// draw appointments
painter.setPen(blackPen);
for(auto& appointment : appointments) {
painter.setBrush(QColor(appointment.color));
double h0 = (appointment.startTime.hour() + appointment.startTime.minute() / 60.0) - displayHoursStart;
double h1 = (appointment.finishTime.hour() + appointment.finishTime.minute() / 60.0) - displayHoursStart;
if (h1 >= h0) {
int v0 = vMargin + vSpacing * h0;
int v1 = vMargin + vSpacing * h1;
painter.drawRect(hLineStart, v0, appointmentWidth, v1 - v0);
// render text
if(textFormat == Html) {
QTextDocument td;
td.setHtml(appointment.text);
QAbstractTextDocumentLayout::PaintContext paintContext;
painter.save();
painter.translate(hLineStart, v0);
paintContext.clip = QRectF(0, 0, appointmentWidth, v1 - v0);
td.documentLayout()->draw(&painter, paintContext);
painter.restore();
}
else {
painter.drawText(hLineStart, v0, appointmentWidth, v1 - v0, Qt::AlignCenter, appointment.text);
}
}
}
if(selectionStart >= 0) { // draw selection
painter.setBrush(QColor(173,216,230,64));
painter.setPen(QColor(64,64,64,64));
int v0 = vMargin + vSpacing * (selectionStart - displayHoursStart);
int v1 = vMargin + vSpacing * (selectionEnd - displayHoursStart);
painter.drawRect(hLineStart, v0, appointmentWidth, v1 - v0);
}
if(drawNowIndicator) { // draw the 'now' line
if(currentHour >= busHoursStart && currentHour <= busHoursEnd) {
painter.setBrush(QColor(255,0,0,255));
painter.setPen(QColor(255,0,0,255));
int v0 = vMargin + vSpacing * (currentHour + currentMinute / 60.0 - displayHoursStart);
painter.drawLine(hLineStart, v0, hLineStart + appointmentWidth, v0);
painter.drawEllipse(QPoint(hLineStart, v0), 5, 5);
QFont f(this->font());
f.setPointSize(12);
painter.setFont(f);
painter.drawText(hMargin, v0 - tOffset, textWidth, tHeight, Qt::AlignVCenter | Qt::AlignRight, currentTime.toString("hh:mm ap"));
}
}
QWidget::paintEvent(event);
}
void mousePressEvent(QMouseEvent *event) override
{
auto rect = geometry();
double vSpacing = (rect.height() - 2* vMargin) / displayHoursSpan;
double t = displayHoursStart + (event->pos().y()-vMargin) / vSpacing; // convert mouse y to hours
selectionStart = std::min(std::max((double)displayHoursStart, std::floor(t / selectQuantizeTo) * selectQuantizeTo), displayHoursEnd - selectQuantizeTo); // quantize and limit range
selectionEnd = std::min(defaultDuration + selectionStart, (double)displayHoursEnd);
QTime t0(0,0,0);
emit selected(t0.addSecs(selectionStart * 3600), t0.addSecs((selectionStart + defaultDuration) * 3600));
update();
QWidget::mousePressEvent(event);
}
void leaveEvent(QEvent *event) override
{
selectionStart = -1.0;
update();
QWidget::leaveEvent(event);
}
signals:
void selected(QTime startTime, QTime finishTime);
public slots:
private slots:
void onAutopaintTimeout() {
if(dateShown == QDate::currentDate()) {
update();
} else {
autopaintTimer.stop();
}
}
private:
// resources
QTimer autopaintTimer;
// properties
QDate dateShown;
int displayHoursStart; // first hour to be displayed 0..24
int displayHoursEnd; // last hour to be displayed
double busHoursStart; // start of business hours 0.0..23.999
double busHoursEnd; // end of business hours
bool warpEffect;
double selectQuantizeTo; // quantize 'snap' amount (fraction of hour)
int vMargin;
int hMargin;
int textWidth;
bool showBusinessHours;
double defaultDuration;
AppointmentWidgetTextFormat textFormat;
// calculated properties
int displayHoursSpan;
int nMinorLines;
// object state
double selectionStart;
double selectionEnd;
QList<AppointmentWidgetItem> appointments;
// constants
const QStringList hourNames {
"12 am",
"1 am",
"2 am",
"3 am",
"4 am",
"5 am",
"6 am",
"7 am",
"8 am",
"9 am",
"10 am",
"11 am",
"Noon",
"1 pm",
"2 pm",
"3 pm",
"4 pm",
"5 pm",
"6 pm",
"7 pm",
"8 pm",
"9 pm",
"10 pm",
"11 pm",
"12 am"
};
};
#endif // APPOINTMENTWIDGET_H
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment