如何在QDateTimeEdit中显示日历弹出窗口,而不在显示的格式字符串中显示日期?

2024-06-15 23:05:05 发布

您现在位置:Python中文网/ 问答频道 /正文

我正在尝试实现一个QDateTimeEdit子类,它将只显示时间部分(显示格式为“HH:mm”)和一个日历图标,单击该图标将触发日历弹出窗口,允许用户设置日期部分。我需要它有一个较短的datetime表示形式,因为很少需要更改日期

在这里添加.setCalendarPopup(True)是不够的,因为只有在格式字符串包含日期部分时才添加triggers,事实并非如此

我尝试过的(按钮嵌入取自this answer):

class ShortDatetimeEdit(QDateTimeEdit):
    def __init__(self, *args, **kwargs):
        super(ShortDatetimeEdit, self).__init__(*args, **kwargs)

        self.setDisplayFormat("HH:mm")
        self.setCurrentSection(QDateTimeEdit.MinuteSection)
        self.setWrapping(True)
        self.setCalendarPopup(True)

        self._setup_date_picker()

    def _setup_date_picker(self):
        self.calendar_trigger = QToolButton(self)
        self.calendar_trigger.setCursor(Qt.PointingHandCursor)
        self.calendar_trigger.setFocusPolicy(Qt.NoFocus)
        self.calendar_trigger.setIcon(QIcon("path/to/icon"))
        self.calendar_trigger.setStyleSheet("background: transparent; border: none;")
        self.calendar_trigger.setToolTip("Show calendar")
        layout = QHBoxLayout(self)
        layout.addWidget(self.calendar_trigger, 0, Qt.AlignRight)
        layout.setSpacing(0)
        self.calendar_trigger.clicked.connect(self._show_calendar)

    def _show_calendar(self, _s):
        self.calendarWidget().show()  # everything breaks here, because calendarWidget() returns None

我的解决方案不起作用,因为self.calendarWidget()返回None(见最后一行)

所以问题是,有没有办法在不使用显示格式字符串中的日期部分的情况下从QDateTimeEdit触发日历弹出窗口(并使用它提供的值)


Tags: selftruedef格式hhshowqtcalendar
1条回答
网友
1楼 · 发布于 2024-06-15 23:05:05

QDateTimeEdit是一种特殊类型的小部件,它也基于其显示格式实现其功能。长话短说,如果您设置的显示格式不显示任何日期值,它的行为将完全类似于QTimeEdit(如果您不显示时间值,它的行为将类似于QDateEdit)

这显然是出于设计原因,我想也是出于优化原因,但这种方法也会带来一些问题,就像您的案例一样

事实上,如果显示格式没有显示任何日期值,您甚至无法设置自己的日历小部件(Qt将对此发出警告)

为了实现您想要的,需要对现有方法进行一些重写。
还可以考虑通过添加子控件来尝试“破解”。不仅您的实现无法按预期工作,而且还可能导致意外的行为和绘制工件,这至少会使小部件难看(如果不是不可用的话)。最好的方法通常是尝试使用小部件已经提供的内容,即使它看起来可能比看起来更复杂。在这种情况下,您可以通过使用用于绘制和单击检测的QStyle函数,手动绘制组合框箭头,该箭头将显示为带有setCalendarPopup(True)的“正常”QDateTimeEdit

最后,由于前面介绍的设计选择,每次更改日期、日期时间或显示格式时,如果显示格式不显示日期值,则日期范围将限制为当前日期,因此每次都需要将范围设置回原来的值,否则,将无法从日历中选择任何其他日期

下面是一个可能的实现:

class ShortDatetimeEdit(QDateTimeEdit):
    def __init__(self, *args, **kwargs):
        super(ShortDatetimeEdit, self).__init__(*args, **kwargs)

        self.setCurrentSection(QDateTimeEdit.MinuteSection)
        self.setWrapping(True)

        self.setDisplayFormat("HH:mm")

        self._calendarPopup = QCalendarWidget()
        self._calendarPopup.setWindowFlags(Qt.Popup)
        self._calendarPopup.setFocus()
        self._calendarPopup.activated.connect(self.setDateFromPopup)
        self._calendarPopup.clicked.connect(self.setDateFromPopup)

    def getControlAtPos(self, pos):
        opt = QStyleOptionComboBox()
        opt.initFrom(self)
        opt.editable = True
        opt.subControls = QStyle.SC_All
        return self.style().hitTestComplexControl(
            QStyle.CC_ComboBox, opt, pos, self)

    def showPopup(self):
        self._calendarPopup.setDateRange(self.minimumDate(), self.maximumDate())

        # the following lines are required to ensure that the popup will always 
        # be visible within the current screen geometry boundaries
        rect = self.rect()
        isRightToLeft = self.layoutDirection() == Qt.RightToLeft
        pos = rect.bottomRight() if isRightToLeft else rect.bottomLeft()
        pos2 = rect.topRight() if isRightToLeft else rect.topLeft()
        pos = self.mapToGlobal(pos)
        pos2 = self.mapToGlobal(pos2)
        size = self._calendarPopup.sizeHint()
        for screen in QApplication.screens():
            if pos in screen.geometry():
                geo = screen.availableGeometry()
                break
        else:
            geo = QApplication.primaryScreen().availableGeometry()

        if isRightToLeft:
            pos.setX(pos.x() - size.width())
            pos2.setX(pos2.x() - size.width())
            if pos.x() < geo.left():
                pos.setX(max(pos.x(), geo.left()))
            elif pos.x() + size.width() > screen.right():
                pos.setX(max(pos.x() - size.width(), geo.right() - size.width()))
        else:
            if pos.x() + size.width() > geo.right():
                pos.setX(geo.right() - size.width())
        if pos.y() + size.height() > geo.bottom():
            pos.setY(pos2.y() - size.height())
        elif pos.y() < geo.top():
            pos.setY(geo.top())
        if pos.y() < geo.top():
            pos.setY(geo.top())
        if pos.y() + size.height() > geo.bottom():
            pos.setY(geo.bottom() - size.height())

        self._calendarPopup.move(pos)
        self._calendarPopup.show()

    def setDateFromPopup(self, date):
        self.setDate(date)
        self._calendarPopup.close()
        self.setFocus()

    def setDate(self, date):
        if self.date() == date:
            return
        dateRange = self.minimumDate(), self.maximumDate()
        time = self.time()
        # when the format doesn't display the date, QDateTimeEdit tries to reset
        # the date range and emits an incorrect dateTimeChanged signal, so we 
        # need to block signals and emit the correct date change afterwards
        self.blockSignals(True)
        super().setDateTime(QDateTime(date, time))
        self.setDateRange(*dateRange)
        self.blockSignals(False)
        self.dateTimeChanged.emit(self.dateTime())

    def setDisplayFormat(self, fmt):
        dateRange = self.minimumDate(), self.maximumDate()
        super().setDisplayFormat(fmt)
        self.setDateRange(*dateRange)

    def mousePressEvent(self, event):
        if self.getControlAtPos(event.pos()) == QStyle.SC_ComboBoxArrow:
            self.showPopup()

    def paintEvent(self, event):
        # the "combobox arrow" is not displayed, so we need to draw it manually
        opt = QStyleOptionSpinBox()
        self.initStyleOption(opt)
        optCombo = QStyleOptionComboBox()
        optCombo.initFrom(self)
        optCombo.editable = True
        optCombo.frame = opt.frame
        optCombo.subControls = opt.subControls
        if self.hasFocus():
            optCombo.activeSubControls = self.getControlAtPos(
                self.mapFromGlobal(QCursor.pos()))
        optCombo.state = opt.state
        qp = QPainter(self)
        self.style().drawComplexControl(QStyle.CC_ComboBox, optCombo, qp, self)

一个重要的音符。即使在使用setWrapping(True)时,QDateTimeEdit(及其子类)也不会遵循时间逻辑:当在分钟部分从01:00向下滚动时,结果将是01:59,日期也是如此。如果您希望能够实际执行步进其他单元,那么还需要重写stepBy()方法;这样,当您从分钟部分的01:00向下滚动时,结果将是00:59,如果您从00:00向下滚动,它将转到前一天的23:59

    def stepBy(self, step):
        if self.currentSection() == self.MinuteSection:
            newDateTime = self.dateTime().addSecs(60 * step)
        elif self.currentSection() == self.HourSection:
            newDateTime = self.dateTime().addSecs(3600 * step)
        else:
            super().stepBy(step)
            return
        if newDateTime.date() == self.date():
            self.setTime(newDateTime.time())
        else:
            self.setDateTime(newDateTime)

显然,这是一种简化:对于完整显示的日期/时间显示格式,您应该添加相关的实现

    def stepBy(self, step):
        section = self.currentSection()
        if section == self.MSecSection:
            newDateTime = self.dateTime.addMSecs(step)
        elif section == self.SecondSection:
            newDateTime = self.dateTime.addSecs(step)
        elif self.currentSection() == self.MinuteSection:
            newDateTime = self.dateTime().addSecs(60 * step)
        elif self.currentSection() == self.HourSection:
            newDateTime = self.dateTime().addSecs(3600 * step)
        elif section == self.DaySection:
            newDateTime = self.dateTime.addDays(step)
        elif section == self.MonthSection:
            newDateTime = self.dateTime.addMonths(step)
        elif section == self.YearSection:
            newDateTime = self.dateTime.addYears(step)
        else:
            super().stepBy(step)
            return
        if newDateTime.date() == self.date():
            self.setTime(newDateTime.time())
        else:
            self.setDateTime(newDateTime)

最后(再次!),如果要确保鼠标滚轮始终响应鼠标光标下的部分,则需要在调用默认^{之前将文本光标放置到适当的位置

    def wheelEvent(self, event):
        cursorPosition = self.lineEdit().cursorPositionAt(event.pos())
        if cursorPosition < len(self.lineEdit().text()):
            letterAt = self.lineEdit().text()[cursorPosition - 1]
            letterWidth = self.fontMetrics().width(letterAt) // 2
            opt = QStyleOptionFrame()
            opt.initFrom(self)
            frameWidth = self.style().pixelMetric(
                QStyle.PM_DefaultFrameWidth, opt, self)
            letterWidth += frameWidth
            pos = event.pos() - QPoint(letterWidth, 0)
            otherCursorPosition = max(0, self.lineEdit().cursorPositionAt(pos))
            if otherCursorPosition != cursorPosition:
                cursorPosition = otherCursorPosition
        self.lineEdit().setCursorPosition(max(0, cursorPosition))
        super().wheelEvent(event)

相关问题 更多 >