pythonpython-3.xpyqt5pyside2qcalendarwidget

How to disable other months days in QCalendarWidget


I am aiming to disable that users can click on days that aren't from the current month in a QCalendarWidget, so I subclassed the widget in order to do it. So far I could make those days don't render any text at all (great). This is the code:

class QCustomCalendar(QCalendarWidget):
    """Create my own Calendar with my own options."""

    def __init__(self, parent=None):
        """Initializing functions"""
        QCalendarWidget.__init__(self, parent)
        self.setEnabled(True)
        self.setGeometry(QRect(0, 0, 320, 250))
        self.setGridVisible(False)
        self.setHorizontalHeaderFormat(QCalendarWidget.SingleLetterDayNames)
        self.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
        self.setNavigationBarVisible(True)
        self.setDateEditEnabled(True)
        self.setObjectName("calendarWidget")

    def paintCell(self, painter, rect, date):
        """Sub-class this and repaint the cells"""
        # Render only this-month days
        month = "{0}-{1}".format(str(self.yearShown()), str(self.monthShown()).zfill(2))
        day = str(date.toPython())
        if not day.startswith(month):
            return
        QCalendarWidget.paintCell(self, painter, rect, date)

However, if I click on a non-rendered day it still counts and triggers the clicked event. Example: I photoshopped a red square where clicking in it, it would select June's 4th (even though we are in May in the screenshot).

Example

How do I disable those days to not being selectables?

I tried setDateRange on currentPageChanged event, but it doesn't works as expected:

def __init__(self, parent=None):
    # some code
    self.currentPageChanged.connect(self.store_current_month)
    self.clicked.connect(self.calendar_itemchosen)

def store_current_month(self):
    self.CURRENT_MONTH = "{0}-{1}".format(str(self.yearShown()), str(self.monthShown()).zfill(2))

def calendar_itemchosen(self):
    day = str(self.selectedDate().toPython())
    print(day)
    if day.startswith(self.CURRENT_MONTH):
        selection = self.selectedDate()
        # some code
        self.close()

The result of clicking on that red square with this code is:

2018-06
2018-06-04

So I guess Qt first triggers the currentPageChanged event when you select a date on another month. setDateRange won't work because if I add it to limit selections to this month only, then the buttons at top of the calendar to "go to next or previous month" won't work, and I need the user to be able to change months. I just don't want the calendar to show days that don't belong to this month page.


Solution

  • One solution is to filter the mousePressEvent event of the QTableView that the QCalendarWidget has Internally. For this we use the event filter:

    from PyQt5 import QtCore, QtWidgets
    
    class CalendarWidget(QtWidgets.QCalendarWidget):
        def __init__(self, parent=None):
            super(CalendarWidget, self).__init__(parent, gridVisible=False,
                horizontalHeaderFormat=QtWidgets.QCalendarWidget.SingleLetterDayNames,
                verticalHeaderFormat=QtWidgets.QCalendarWidget.NoVerticalHeader,
                navigationBarVisible=True,
                dateEditEnabled=True)       
            self.setEnabled(True)
            self.setGeometry(QtCore.QRect(0, 0, 320, 250))
            self.clicked.connect(print)
    
            self.table_view = self.findChild(QtWidgets.QTableView, "qt_calendar_calendarview")
            self.table_view.viewport().installEventFilter(self)
            self.setFirstDayOfWeek(QtCore.Qt.Monday)
    
        def referenceDate(self):
            refDay = 1
            while refDay <= 31:
                refDate = QtCore.QDate(self.yearShown(), self.monthShown(), refDay)
                if refDate.isValid(): return refDate
                refDay += 1
            return QtCore.QDate()
    
        def columnForDayOfWeek(self, day):
            m_firstColumn = 1 if self.verticalHeaderFormat() != QtWidgets.QCalendarWidget.NoVerticalHeader else 0
            if day < 1 or day > 7: return -1
            column = day - int(self.firstDayOfWeek())
            if column < 0:
                column += 7
            return column + m_firstColumn
    
        def columnForFirstOfMonth(self, date):
            return (self.columnForDayOfWeek(date.dayOfWeek()) - (date.day() % 7) + 8) % 7
    
        def dateForCell(self, row, column):
            m_firstRow = 1 if self.horizontalHeaderFormat() != QtWidgets.QCalendarWidget.NoHorizontalHeader else 0
            m_firstColumn = 1 if self.verticalHeaderFormat() != QtWidgets.QCalendarWidget.NoVerticalHeader else 0
            rowCount = 6
            columnCount = 7
            if row < m_firstRow or row > (m_firstRow + rowCount -1) or column < m_firstColumn or column > (m_firstColumn + columnCount -1):
                return QtCore.QDate()
            refDate = self.referenceDate()
            if not refDate.isValid():
                return QtCore.QDate()
            columnForFirstOfShownMonth = self.columnForFirstOfMonth(refDate)
            if (columnForFirstOfShownMonth - m_firstColumn) < 1:
                row -= 1
            requestedDay = 7*(row - m_firstRow) +  column  - columnForFirstOfShownMonth - refDate.day() + 1
            return refDate.addDays(requestedDay)
    
        def eventFilter(self, obj, event):
            if obj is self.table_view.viewport() and event.type() == QtCore.QEvent.MouseButtonPress:    
                ix = self.table_view.indexAt(event.pos())
                date = self.dateForCell(ix.row(), ix.column())
                d_start = QtCore.QDate(self.yearShown(), self.monthShown(), 1)
                d_end = QtCore.QDate(self.yearShown(), self.monthShown(), d_start.daysInMonth())
                if d_start > date or date > d_end:
                    return True
            return super(CalendarWidget, self).eventFilter(obj, event)
    
        def paintCell(self, painter, rect, date):
            d_start = QtCore.QDate(self.yearShown(), self.monthShown(), 1)
            d_end = QtCore.QDate(self.yearShown(), self.monthShown(), d_start.daysInMonth())
            if d_start <= date <= d_end:
                super(CalendarWidget, self).paintCell(painter, rect, date)
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        w = CalendarWidget()
        w.show()
        sys.exit(app.exec_())