Search code examples
pythonpyside2qcalendarwidget

Populating a QCalendarWidget


I created a GUI to show some running workouts via PyQt QCalendarWidget. The following code was based on these posts:

I created a subclass in order to overwrite the paint method. This works fine when I have a date hardcoded within the paintCell method. But ideally I would like to run a function first which will return a set of dates/running distances as a dataframe. This dataframe would be then used to "populate" the QCalendarWidget (by adding the running distance as text for the corresponding date for instance).

class MyQtApp(trigui.Ui_MainWindow, QtWidgets.QMainWindow):
    def __init__(self):
        super(MyQtApp, self).__init__()
        self.setupUi(self)
        self.Calendar()
        self.df = pd.DataFrame()
        self.qPlan_Create_btn.clicked.connect(self.draw_running_plan)

    def Calendar(self):
        self.cal = CalendarWidget(self.qPlan_Widget)
        self.cal.resize(1700, 800)

    def draw_running_plan(self):
        self.df = pd.DataFrame([[25-05-2021, 10], [27-05-2021, 12]], columns=['Date', 'Distance'])
       #########
       # how can I pass this dataframe to the paintCell

class CalendarWidget(QtWidgets.QCalendarWidget):
    def paintCell(self, painter, rect, date):
        painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
        if date == QtCore.QDate(2021, 5, 15):
            painter.save()
            painter.drawRect(rect)
            painter.setPen(QtGui.QColor(168, 34, 3))
            painter.drawText(rect, QtCore.Qt.AlignHCenter, 'Hello\nWorld')
            painter.drawText(QtCore.QRectF(rect), 
             QtCore.Qt.TextSingleLine|QtCore.Qt.AlignCenter, str(date.day()))

            painter.restore()
        else:
            QtWidgets.QCalendarWidget.paintCell(self, painter, rect, date)

def main():
    import sys

    app = QtWidgets.QApplication(sys.argv)
    qt_app = MyQtApp()
    qt_app.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

Somehow I need to call the method Calendar from my __init__. I tried to set self.cal = CalendarWidget (self.qPlan_Widget) within my function draw_running_plan but then the Calendar is not displayed. For info, self.qPlan_Widget is a simple Widget/container that I created via QtDesigner and I initialize it to be a CalendarWidget via the Calendar method. So long story short: after initializing the CalendarWidget, how do I update it with the result of an intermediary function?

Edit: my mistake about the tag it is PySide2 not PyQt


Solution

  • A possible solution is to create a property that the update() method of the internal QTableView viewport is used in the setter, which will call the paintEvent method, which in its logic invokes the paintCell() method:

    On the other hand for filtering it is better to convert the date string column (at least that seems what the OP provides) to datetime. And then make a filter based on the smallest date of one day and the one of the next day.

    import pandas as pd
    from PySide2 import QtCore, QtGui, QtWidgets
    import datetime
    
    
    class CalendarWidget(QtWidgets.QCalendarWidget):
        _dataframe = pd.DataFrame()
    
        @property
        def dataframe(self):
            return self._dataframe
    
        @dataframe.setter
        def dataframe(self, df):
            self._dataframe = df.copy()
            view = self.findChild(QtWidgets.QTableView, "qt_calendar_calendarview")
            if view is not None:
                view.viewport().update()
    
        def paintCell(self, painter, rect, date):
            painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
            if self._dataframe.empty:
                QtWidgets.QCalendarWidget.paintCell(self, painter, rect, date)
                return
    
            if hasattr(date, "toPyDate"):
                dt_start = datetime.datetime.combine(
                    date.toPyDate(), datetime.datetime.min.time()
                )
            else:
                dt_start = datetime.datetime.strptime(
                    date.toString("yyyy-MM-dd"), "%Y-%m-%d"
                )
    
            dt_end = dt_start + datetime.timedelta(days=1)
    
            mask = (dt_start <= self.dataframe["Date"]) & (self.dataframe["Date"] < dt_end)
            res = self.dataframe.loc[mask]
            if res.empty:
                QtWidgets.QCalendarWidget.paintCell(self, painter, rect, date)
            else:
                value = res.iloc[0, res.columns.get_loc("Distance")]
    
                painter.save()
                painter.drawRect(rect)
                painter.setPen(QtGui.QColor(168, 34, 3))
                painter.drawText(
                    QtCore.QRectF(rect),
                    QtCore.Qt.TextSingleLine | QtCore.Qt.AlignCenter,
                    str(value),
                )
                painter.restore()
    
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super().__init__(parent)
    
            button = QtWidgets.QPushButton("Change")
            self.calendar_widget = CalendarWidget()
    
            central_widget = QtWidgets.QWidget()
            self.setCentralWidget(central_widget)
    
            lay = QtWidgets.QVBoxLayout(central_widget)
            lay.addWidget(button)
            lay.addWidget(self.calendar_widget)
    
            button.clicked.connect(self.handle_clicked)
    
        def handle_clicked(self):
            import random
    
            df = pd.DataFrame(
                [
                    ["25-05-2021", random.randint(0, 100)],
                    ["27-05-2021", random.randint(0, 100)],
                ],
                columns=["Date", "Distance"],
            )
            df["Date"] = pd.to_datetime(df["Date"])
            self.calendar_widget.dataframe = df
    
    
    def main():
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
    
        widget = MainWindow()
        widget.show()
    
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()