PyQt5 Revisited…Build a Remarkably Awesome Interactive Calendar

An older image by author

The Learning Never Stops…

A couple of months ago I posted a tutorial about building an interactive calendar using python and PyQt5. The project incorporated two separate GUI’s in two different files. One was for the calendar and appointments display. The other was the input form for adding new appointments. I’d been using this multi-GUI setup on most of my projects for about a year now.

Recently, I was sitting quietly and contemplating the meaning of the universe and PyQt5. I was particularly thinking about classes in PyQt5. A GUI in PyQt5 is a class with a base class name, ie:

class MyClass( base_class_name ):

When looking at examples on the internet, I noticed the base class name used was either QWidget or QMainWindow. I’ve always used QMainWindow.

Anyway, there I was, staring off into space when it hit me. If PyQt5 has a Main Window base class name, doesn’t that connotate that there should be some sort of sub-window?

So, I proceeded to find out. That’s when I came across QDockWidget. According to tutorialspoint.com, a dock widget is a …

“…dockable subwindow that can remain in floating state or can be attached to the main window at a specified position. Main window object of QMainWindow class has an area reserved for dockable windows.”

Interesting.

I started fooling around with the calendar project to see if the appointment GUI could be eliminated and the process moved to a dock widget.

Spoiler Alert!

It could.

So I did.

Since I was rewriting the code anyway, I decided to change the layout a bit from what was originally there. I also have refactored the updated code with black.

The Code…

import sys, os
import pandas as pd
from qtpy_cfg import *
from datetime import datetime
from pymongo import MongoClient
from PyQt5.QtCore import (
Qt,
QDate,
QRect,
QTimer,
pyqtSignal,
)
from PyQt5.QtGui import (
QPen,
QFont,
QColor,
QBrush,
QPainter,
QTextCharFormat,
)
from PyQt5.QtWidgets import (
QLabel,
QFrame,
QTextEdit,
QLineEdit,
QListWidget,
QPushButton,
QDockWidget,
QMainWindow,
QApplication,
QTableWidget,
QCalendarWidget,
QTableWidgetItem,
)
mongo = db(“mydb”, “appointments”)
now = datetime.now()

Starting at the top, qtpy_cfg() is a support file of functions I wrote to support PyQt5. I will explain those functions when they arise. pymongo() is the python interface with MongoDb, my database of choice.

Next, we import the classes and widgets we need from PyQt5.QtCore, PyQt5.QtGui, and PyQt5.QtWidgets.

For mongo = db() , db is a function from qtpy_cfg() that initializes the connection to the database. The code uses *args to allow using more than one collection, or table, from the database. The code for db is:

def db(database_name, collection_name, *args):
client = MongoClient('localhost', 27017)
db = client[f"{database_name}"]
coll = db[f"{collection_name}"]
if not args:
return coll
else:
colls = [db[f"{arg}"] for arg in args]
colls.append(coll)
return colls

To finish this section we define today as now.

Next, we need the code to pull data from the database to populate the appointments display.

class Appointments(MongoClient):

def appointments(self):
appts = []
now = datetime.now()
today = now.strftime("%m-%d-%Y")

data = mongo.find(
{}, {"_id": 0, "date": 1, "time": 1, "place": 1, "note": 1}
).sort("date")
for d in data:
if d["date"] < today: # will not display past appointments
pass
else:
date, time, place, note = (
d["date"][:5],
d["time"],
d["place"],
d["note"],
)
appts.append([date, time, place, note])
return appts

Here we gather the current appointments, sort them in ascending order according to date and format the data for display. For the date, I wanted only the month and day to show, thus the [:5]. Notice its base class name points to MongoClient.

The next code is for the Calendar class.

class Calendar(QCalendarWidget):  # <-- base class assignment

def __init__(self, parent=None):
super(Calendar, self).__init__()

For the GUI class, the base class name is QMainWindow.

class Example(QMainWindow):
"""
GUI that contains the calendar and appointment list.
A popup window (dock widget) is used to add appointments.
"""

def __init__(self, parent=None):
super(Example, self).__init__(parent)


# physical size and location of gui --------
self.left = 1138
self.top = 30
self.width = 302
self.height = 445

# class assignments -------------------------
self.items = QDockWidget("dock", self)
self.appointments = Appointments()
self.cal = QCalendarWidget(self)
format = QTextCharFormat()

# styling functions -------------------------
qblack(self)
qdim = qbutton_calc(self)
canda_10 = QFont("Candalara", 10)
canda_11 = QFont("Candalara", 11)
canda_12 = QFont("Candalara", 12)
segoe_9 = QFont("Segoe UI", 9)
segoe_16 = QFont("Segoe UI", 16)
qgreen = qbutton_green(self)
style1 = "background-color: black"
format = self.cal.weekdayTextFormat(Qt.Saturday)
format.setForeground(QBrush(Qt.darkCyan, Qt.SolidPattern))

The first section above is fairly straightforward with self.items being the QDockWidget. qblack(self) is from qtpy_cfg() and is used to format the main window for display. The code for qblack is:

def qblack(self):
self.setWindowFlags(Qt.FramelessWindowHint) # no title bar
self.setGeometry(self.left,self.top,self.width,self.height)
self.setAutoFillBackground(True)
p = self.palette()
p.setColor(self.backgroundRole(), Qt.black)
self.setPalette(p)

qbutton_calc() and qbutton_green() are styling functions for QPushButtons used in the window. Their code is:

def qbutton_calc(self):
return "QPushButton {color: rgba(54, 136, 200, 250); background-color: rgba(29, 29, 29, 150); border: black; border-width: 2px;}"

def qbutton_green(self):
return "QPushButton {color: rgba(6, 186, 39, 250); background-color: black;}"

Before we continue, there are some limitations for a dock widget. A big one is it can contain only one widget. That means a single QLineEdit, or QLabel, etc. It also means a single frame (QFrame).

Aha.

We can add functionality to the dock widget by assigning widgets to a frame.

Awesome!

The Changes…

The widgets that were part of the appointment GUI have been added to the calendar GUI. These will be assigned to a QFrame named frame1. The following code builds the frame.

# Frame1 contains the widgets for adding an appointment.
self.frame1 = QFrame(self)
self.frame1.setStyleSheet(style1)
# Sets frame1 as the dock widget -----------------
self.items.setWidget(self.frame1)
# SAVE and EXIT buttons --------------------------
qtbutton(
self.frame1, "save", 15, 350, 270, 30, qdim, canda_10, "SAVE", self.repeat
)
qtbutton(
self.frame1, "exit", 15, 390, 270, 30, qdim, canda_10, "EXIT", self.close
)
# Builds the "date" label that shows the chosen appointment date
self.date_data = QLabel(self.frame1)
self.date_data.setGeometry(0, 35, 301, 40)
self.date_data.setStyleSheet(
"QLabel {color: rgba(125,125,125,255); background-color: black;}"
)
self.date_data.setFont(segoe_16)
self.date_data.setAlignment(Qt.AlignCenter)
# Builds the "time" label that shows the chosen appointment time
self.time_label = QLabel(self.frame1)
self.time_label.setGeometry(15, 105, 80, 40)
self.time_label.setStyleSheet(
"QLabel {color: rgba(125,125,125,255); background-color: black;}"
)
self.time_label.setFont(canda_12)
self.time_label.setText("Time:")
self.time_label.hide()
# Builds the "choice" label to display chosen appointment time
self.choice_lbl = QLabel(self.frame1)
self.choice_lbl.setGeometry(70, 105, 130, 40)
self.choice_lbl.setStyleSheet(
"QLabel {color: rgba(150,150,150,255); background-color: black;}"
)
self.choice_lbl.setFont(canda_12)
self.choice_lbl.hide()
# Builds the "place" label ------------------------
self.place_label = QLabel(self.frame1)
self.place_label.setGeometry(15, 158, 80, 40)
self.place_label.setStyleSheet(
"QLabel {color: rgba(125,125,125,255); background-color: black;}"
)
# Builds the "note" label -------------------------
self.note_label = QLabel(self.frame1)
self.note_label.setGeometry(15, 240, 80, 40)
self.note_label.setStyleSheet(
"QLabel {color: rgba(125,125,125,255); background-color: black;}"
)
self.note_label.setFont(canda_12)
self.note_label.setText("Note:")
self.note_label.hide()
# Builds the "list" widget for housing the "times" list
self.listwidget = QListWidget(self.frame1)
self.listwidget.setGeometry(100, 90, 100, 200)
self.listwidget.resetVerticalScrollMode()
self.listwidget.setFont(canda_10)
self.listwidget.setStyleSheet(
"QListWidget {color: rgba(125,125,125,255); background-color: black;}"
)

Notice a new function, qtbutton(), as one of the qtpy_cfg() functions.

def qtbutton(self, name, x, y, w, h, style, font, title, connection):
self.name = QPushButton(self)
self.name.setStyleSheet(style)
self.name.setFont(font)
self.name.setText(title)
self.name.setGeometry(x, y, w, h)
self.name.clicked.connect(connection)

The last widget above is self.listwidget() (QListWidget). The widget is used to scroll through a list of times for an appointment. I’ve included my list of times. You can make your list with different intervals.

times = [
"All Day",
"5:00 AM",
"5:30 AM",
"6:00 AM",
"6:30 AM",
"7:00 AM",
"7:30 AM",
"8:00 AM",
"8:30 AM",
"9:00 AM",
"9:30 AM",
"10:00 AM",
"10:30 AM",
"11:00 AM",
"11:30 AM",
"Noon",
"12:30 PM",
"1:00 PM",
"1:30 PM",
"2:00 PM",
"2:30 PM",
"3:00 PM",
"3:30 PM",
"4:00 PM",
"4:30 PM",
"5:00 PM",
"5:30 PM",
"6:00 PM",
"6:30 PM",
"7:00 PM",
]

Now that we have a list of times, that list needs to be inserted in the list widget.

    # This section iterates through the "times"
# list and inserts them into a list widget.
# variable "count" is used to prevent Index errors.

count = 0
for time in times:
if count < len(times):
self.listwidget.insertItem(count, time)
count += 1
else:
pass
self.listwidget.clicked.connect(self.choose) # Click on a time, perform the function "choose()".

Now we code the widgets for the main window (QMainWindow).

# Builds the "place" text input box --------
self.place_edit = QTextEdit(self.frame1)
self.place_edit.setGeometry(70, 155, 215, 60)
self.place_edit.setStyleSheet(
"QTextEdit {color: rgba(150,150,150,255); background-color: rgba(29, 29, 29, 150); border: none;}"
)
self.place_edit.setFont(canda_12)
self.place_edit.hide()
# Builds the "note" text input box ---------
self.note_edit = QTextEdit(self.frame1)
self.note_edit.setGeometry(70, 235, 215, 80)
self.note_edit.setStyleSheet(
"QTextEdit {color: rgba(150,150,150,255); background-color: rgba(29, 29, 29, 150); border: none;}"
)
self.note_edit.setFont(canda_12)
self.note_edit.hide()
# Here, "format" is used to change color for the weekend days
self.cal.setWeekdayTextFormat(Qt.Saturday, format)
self.cal.setWeekdayTextFormat(Qt.Sunday, format)
# Initiates and customizes the calendar ---
self.cal.setGridVisible(False)
self.cal.setGeometry(0, 0, 292, 221)
self.cal.setFont(segoe_9)
self.cal.setVerticalHeaderFormat(self.cal.NoVerticalHeader)
self.cal.setStyleSheet(
"QCalendarWidget QAbstractItemView{background-color: black;color: rgba(162,201,229,255);selection-background-color: rgb(30,30,30);selection-color: rgba(180, 180, 180, 250);selection-border: 1px solid black;}"
"QCalendarWidget QWidget{alternate-background-color: rgb(20, 20, 20); color: gray;}"
"QCalendarWidget QToolButton{background-color: black; color: rgb(125,125,125); font-size: 14px; font: bold; width: 70px;border: none;}"
"QCalendarWidget QToolButton#qt_calendar_prevmonth{qproperty-icon: url(gif/left_arrow.png);}"
"QCalendarWidget QToolButton#qt_calendar_nextmonth{qproperty-icon: url(gif/right_arrow.png);}"
)
self.cal.clicked&#91;QDate].connect(self.showDate)
# I use a symbol for EXIT buttons ---------
symbol = u"\u2592"
qtbutton(self, "exit", 280, 420, 20, 20, qdim, canda_11, symbol, self.exit)
# Initiates and customizes the appointment table
self.appt_table = QTableWidget(self)
self.appt_table.setGeometry(QRect(20, 240, 257, 180))
# alternate-background-color alternates the row background colors
self.appt_table.setStyleSheet(
"QTableWidget {color: rgb(56,95,220); background-color: rgb(10,10,10); border: none;"
"alternate-background-color: black; border: none;}"
)
self.appt_table.horizontalHeader().hide()
s self.appt_table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.appt_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.appt_table.setFont(canda_10)
self.appt_table.setAlternatingRowColors(True)
self.appt_table.verticalHeader().setVisible(False)

Under self.cal.setStyleSheet, there are five QCalendarWidget parameters. The first parameter sets the style of the calendar’s background color, current date font color, and the color of the date cell. The second line styles the chosen date background color. The third parameter sets sizing on the calendar heading bar. The last two parameters change the next and back month arrows.

The last of the window styling section of the code input the appointment list into a Pandas dataframe. It then sets up the timer to refresh the appointments display.

# Appointment list to pandas df
df = pd.DataFrame(self.appointments.appointments())
self.display_data(df)

# Timer updates appointment table every 2 seconds.
# This way, an added appointment is displayed immediately
self.timer1 = QTimer(self)
self.timer1.timeout.connect(self.update_table)
self.update_table()
self.show()

The first function called is named self.display_data().

    # Sets up the appointment list display
def display_data(self, var):

# sets table columns, rows to number of df columns, rows
self.appt_table.setColumnCount(len(var.columns))
self.appt_table.setRowCount(len(var.index))

for i in range(len(var.index)):
for j in range(len(var.columns)):
self.appt_table.setItem(i, j, QTableWidgetItem(str(var.iat[i, j])))
self.appt_table.resizeColumnsToContents() # resize the columns

The other function called was self.update_table(). The function loops every two seconds and rewrites the data.

    # Update function for appointment table
def update_table(self):
df = pd.DataFrame(self.appointments.appointments())
self.appt_table.update()
self.cal.update()
self.display_data(df)
self.timer1.start(2000)
self.update_table

The main window of the calendar has two separator lines.

    # Draws the line separators
def paintEvent(self, e):
self.painter = QPainter(self)
self.painter.begin(self)
self.painter.setPen(QColor(75, 75, 75))
self.painter.drawLine(50, 230, 250, 230)
self.painter.drawLine(50, 425, 250, 425)
self.painter.end()

The Dock Widget…

Earlier we coded a list widget for the dock widget. Clicking on a time in the list widget activates the self.choose() function. Here is that code.

    # Opens the list widget for choosing appointment time
def choose(self, qmodelindex):
self.listwidget.show()
choice = self.listwidget.currentItem()
self.choice_lbl.setText(choice.text()) # Displays the chosen time
self.place_label.show()
self.choice_lbl.show()
self.time_label.show()
self.place_edit.show()
self.note_label.show()
self.listwidget.hide()
self.note_edit.show()

Remember the qtbuttons? There are three of them. A QPushButton is connected to a function. The three functions are:

    # Saves new appointment to the database via the SAVE button
def repeat(self):
time = self.listwidget.currentItem().text()
place = self.place_edit.toPlainText()
note = self.note_edit.toPlainText()
date = self.date_data.text()[-10:][:5] #date formatted as '00-00-0000'
# closes add appointment window without saving and returns to calendar
def close(self):
self.frame1.hide() # hides window, shows calendar
self.cal.show()

# exits entire app
def exit(self):
sys.exit()

Ok. Suppose we’re sitting here with the app open and we want to add an appointment. What’s the first thing one does in these situations?

One clicks on the date of the appointment.

So, we now have one of the four parts of the appointment we need, the date.

Now, that click can be captured and used as a trigger. Recall coding the set.cal() parameters. The last line assigned a function to that click.

self.cal.clicked[QDate].connect(self.showDate)

showDate() builds the date string for display in the dock widget. The code for showDate():

# Takes selected date and creates the day name. The "if loop"
# determines whether the selected date is a future date
# (no sense in making an appointment for the past)

def showDate(self, date):
info = self.cal.selectedDate()
date = info.toString("MM-dd-yyyy")
self.cal.selectedDate().isNull() # clears variable
days = [
"",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]
dayo = QDate.dayOfWeek(info)
day = str(days[dayo])

if str(date) < now.strftime("%m-%d-%Y"):
pass
else:
self.event_form(date, day)

So far, all we’ve done is get a date (easier for some, not so much for others :). What does the program do once a date is clicked on? Answer: the Dock Widget is displayed.

Click on a time…
…and the add appointment form appears

The last line of code in showDate() calls the function self.event_form().

def event_form(self, date, day): 
self.date_data.setText(day + " " + date) # formats date for display

# dock widget parameters -----------------------
self.items.setFeatures(QDockWidget.DockWidgetClosable)
self.setCentralWidget(self.frame1)
self.addDockWidget(Qt.BottomDockWidgetArea, self.items)
self.items.show()

And, finally….

if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())

This project is on Github at: https://github.com/zazen000/Calender or my blog at: http://braveinternetmarketing.com/python/ (after 8/17/21).

If you enjoy reading stories like these and want to support me as a writer, consider subscribing to Medium for $5 a month. As a member, you have unlimited access to stories on Medium. If you sign up using my link, I’ll earn a small commission.

--

--

--

Retired military, Retired US Postal Service, Defender of the US Constitution from all enemies, foreign and domestic, Self-taught in python

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

My experience with 42Wolfsburg during the remote Piscine — Day 11

GPT-2 Text Generation Deployment Strategy on AWS

Packages, Comments, and Variables

Getting & Setting Cookie in Spring Boot

Embedding a Flutter Web Application inside an iframe with a dynamic height

Paxos from Scratch

Deploying an Apollo GraphQL application as an AWS lambda function through Serverless

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
ZennDogg

ZennDogg

Retired military, Retired US Postal Service, Defender of the US Constitution from all enemies, foreign and domestic, Self-taught in python

More from Medium

Using Strava to Find New Bike Adventures

Python Behave: Top 5 Timesavers

How do Solar Panels Work Anyway??

How to use Custom Theme Colors in German MS Office for Mac 365