Search code examples
python-3.xpyqt5qdateedit

PyQt5 - How can I disable weekend on a QDateEdit


I'm using a QDateEdit to choose a specific date, but I would like to disbale weekends, I only want to choose week days.

self.date =  QDateEdit(calendarPopup = True)
self.date.setDisplayFormat("dd-MM-yyyy")
self.date.setMinimumDate(QDate(2021,10,1))    
self.date.setDate(QDate(datetime.today()))

Solution

  • QCalendarWidget only allows a single range of accepted dates, and all dates in that range can be selected.

    The only solution I could think of (except for creating your own calendar from scratch) is to subclass QCalendarWidget, access the underlying QTableView (which is what shows the calendar) and do the following:

    • set the selection mode to NoSelection;
    • install event filters on both the view (to filter key presses) and the view's viewport (to filter mouse events);
    • implement a dateForIndex to retrieve the displayed date at a specific index of the table;
    • set the selection mode to SingleSelection whenever the index at the mouse position is in work days, otherwise set it back to NoSelection;
    • implement proper selection to avoid/skip weekends when using keyboard navigation;
    class CalendarWidget(QtWidgets.QCalendarWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setSelectionMode(self.NoSelection)
            self.view = self.findChild(QtWidgets.QAbstractItemView, 'qt_calendar_calendarview')
            self.view.installEventFilter(self)
            self.view.viewport().installEventFilter(self)
    
        def dateForIndex(self, index):
            row = index.row()
            column = index.column()
            if self.horizontalHeaderFormat():
                row -= 1
            if self.verticalHeaderFormat():
                column -= 1
            if not 0 <= row <= 5 or not 0 <= column <= 6:
                return QtCore.QDate()
    
            day = index.data()
            month = self.monthShown()
            year = self.yearShown()
    
            # day numbers bigger than 21 cannot be shown in the first 3 rows
            if row <= 2 and day > 21:
                month -= 1
                if month <= 0:
                    month = 12
                    year -= 1
            # day numbers smaller than 15 cannot be shown in the last 3 rows
            elif row >= 3 and day < 15:
                month += 1
                if month >= 13:
                    month = 1
                    year += 1
    
            date = QtCore.QDate(year, month, day)
            if self.minimumDate() <= date <= self.maximumDate():
               return date
            return QtCore.QDate()
    
        def moveCursor(self, key):
            currentDate = self.dateForIndex(self.view.currentIndex())
            delta = 1
    
            if key == QtCore.Qt.Key_Up:
                newDate = currentDate.addDays(-7)
            elif key == QtCore.Qt.Key_Down:
                newDate = currentDate.addDays(7)
            elif key == QtCore.Qt.Key_Left:
                newDate = currentDate.addDays(-1)
            elif key == QtCore.Qt.Key_Right:
                newDate = currentDate.addDays(1)
            elif key == QtCore.Qt.Key_Home:
                newDate = QtCore.QDate(currentDate.year(), currentDate.month(), 1)
                delta = -1
            elif key == QtCore.Qt.Key_End:
                newDate = QtCore.QDate(currentDate.year(), currentDate.month(), 
                    currentDate.daysInMonth())
                delta = -1
            elif key == QtCore.Qt.Key_PageUp:
                newDate = currentDate.addMonths(-1)
                delta = -1
            elif key == QtCore.Qt.Key_PageDown:
                newDate = currentDate.addMonths(1)
                delta = -1
            else:
                return
    
            newDate = max(self.minimumDate(), min(newDate, self.maximumDate()))
            if currentDate != newDate:
                # if it's a day of the weekend, add the delta until a work day is
                # found; for Home/End/Page keys the delta is inverted, as we need to
                # ensure that we stay in the days of the selected month, and if the
                # function reaches a weekend it could skip a month
                while newDate.dayOfWeek() > 5:
                    if newDate > currentDate:
                        newDate = newDate.addDays(delta)
                    else:
                        newDate = newDate.addDays(-delta)
                if self.minimumDate() <= newDate <= self.maximumDate():
                    return newDate
    
        def eventFilter(self, obj, event):
            if (event.type() in (event.MouseButtonPress, event.MouseButtonRelease, event.MouseButtonDblClick) 
                and event.button() == QtCore.Qt.LeftButton):
                    index = self.view.indexAt(event.pos())
                    if index.isValid():
                        date = self.dateForIndex(index)
                        if date.dayOfWeek() <= 5:
                            self.setSelectionMode(self.SingleSelection)
                        else:
                            self.setSelectionMode(self.NoSelection)
    
            elif event.type() == event.MouseMove and event.buttons() == QtCore.Qt.LeftButton:
                index = self.view.indexAt(event.pos())
                if index.isValid():
                    date = self.dateForIndex(index)
                    if not date.isValid() or date.dayOfWeek() > 5:
                        # ignore mouse move events for weekends
                        return True
    
            elif event.type() == event.KeyPress:
                newDate = self.moveCursor(event.key())
                if newDate:
                    self.setSelectedDate(newDate)
                    return True
            return super().eventFilter(obj, event)
    

    The only issue with this implementation is that if the dateEditEnabled is set (which is the default), there's no way to prevent selecting weekend days, except from connecting to the activated and selectionChanged signals and eventually reset the selected date to a valid day.