QTimeEdit has no 'popup' widget and it's overall design makes it "not nice to use".
Therefore I coded two custom QWidget classes:
class Clock_hourWidget(QWidget):
class/object returns/emits first valid hour inputclass Clock_minuteWidget(QWidget):
class/object returns/emit first valid minute inputAnd i embedded them into a Custom QTimeEdit:
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QTimeEdit, QApplication
from PyQt5.QtCore import QTime, Qt, QTimer
from PyQt5.QtGui import QIcon
class Clock_hourWidget(QtWidgets.QWidget):
return_hour = 15
first = True
def __init__(self, hours, callback, parent=None):
super(Clock_hourWidget, self).__init__(parent)
self.setWindowFlags(Qt.Popup)
self.callback = callback
self.setFixedSize(150,150)
self.hours = hours
def mousePressEvent(self, event):
if self.rect().contains(event.localPos().toPoint()):
self.first = False
self.return_hour = self.hours
self.close()
@property
def hour(self):
if self.first:
result = -1
else:
result = self.return_hour
return result
def closeEvent(self, event):
self.callback(self.hour)
event.accept()
class Clock_minuteWidget(QtWidgets.QWidget):
return_minutes = 30
def __init__(self, minutes, callback, parent=None):
super(Clock_minuteWidget, self).__init__(parent)
self.setWindowFlags(Qt.Popup)
self.setFixedSize(150,150)
self.callback = callback
self.minutes = minutes
def mousePressEvent(self, event):
if self.rect().contains(event.localPos().toPoint()):
self.return_minutes = self.minutes
self.close()
@property
def minute(self):
return self.return_minutes
def closeEvent(self, event):
self.callback(self.minute)
event.accept()
class CustomTimeEdit(QTimeEdit):
def __init__(self, parent=None):
super(CustomTimeEdit, self).__init__(parent)
self.setDisplayFormat("HH:mm")
self.setCalendarPopup(False)
# calendar_icon = QIcon.fromTheme("calendar")
# self.setButtonSymbols(calendar_icon)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.open_clock_hourwidget()
def return_hour(self, hour):
self.clock_hourwidget = None
if hour != -1:
self.hours = hour
self.minutes = self.time().minute()
self.setTime(QTime(self.hours, self.minutes))
self.clock_minutewidget = Clock_minuteWidget(self.minutes, self.clock_finalizer)
pos = self.mapToGlobal(self.rect().bottomLeft())
pos = self.adjust_Popup_positioning(pos, self.clock_minutewidget)
self.clock_minutewidget.move(pos)
self.clock_minutewidget.show()
def open_clock_hourwidget(self):
self.hours = self.time().hour()
self.clock_hourwidget = Clock_hourWidget(self.hours, self.return_hour)
pos = self.mapToGlobal(self.rect().bottomLeft())
pos = self.adjust_Popup_positioning(pos, self.clock_hourwidget)
self.clock_hourwidget.move(pos)
self.clock_hourwidget.show()
def adjust_Popup_positioning(self, pos, widget):
screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos())
screen_geometry = QApplication.desktop().screenGeometry(screen)
if pos.y() + widget.height() > screen_geometry.height():
pos = self.mapToGlobal(self.rect().topLeft())
pos.setY(pos.y() - widget.height())
if pos.x() < screen_geometry.left():
pos.setX(screen_geometry.left())
elif pos.x() + widget.width() > screen_geometry.right():
pos.setX(screen_geometry.right() - widget.width())
return pos
def clock_finalizer(self, minute):
self.clock_minutewidget = None
self.setTime(QTime(self.hours, minute))
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
time_edit = CustomTimeEdit()
time_edit.show()
sys.exit(app.exec_())
I want the popup to behave like the CalendarPopup of QDateEdit (crossed out functionality already in above code snippet):
setCalendarPopup(True)
): Looks should change depending on used style and platformI can provide full code if needed but for simplicity reasons i tried to cut it down as much as possible.
QTimeEdit and QDateEdit are fundamentally dummy classes: they do not provide any specific difference in behavior than QDateTimeEdit from which they inherit, and, in fact, it's enough to set a proper displayFormat()
on QDateTimeEdit to get the same results.
Now, what QDateTimeEdit does is to partially behave similarly to an editable QComboBox whenever the calendarPopup
property is True
and it's actually showing a date Section
(either one of day, month or year).
This is achieved (other than in mouse handlers) in the paintEvent()
: if the popup shouldn't be shown (calendarPopup
is False
or no date section is present in displayFormat()
), it just calls the basic paintEvent()
of QAbstractSpinBox, which internally creates a QStyleOptionSpinBox instance, calls the virtual initStyleOption()
(which is that of QDateTimeEdit, but in that case would be identical since no date section is shown) and finally uses the widget's style drawComplexControl()
function with it.
If, otherwise, the popup may be shown, it uses QStyle features to show itself like an editable combo box by adding some subControls
specific to it: the combo frame, its edit field and, most importantly, the arrow.
Therefore, in order to force the painting of the arrow, we need to mimic that same behavior. Note that since the arrow may be shown differently depending on the mouse over state, we need to ensure that the option activeSubControls
are also consistent, which is achieved by forcing mouse tracking and checking the position in mouseMoveEvent()
and enter/leave events.
Be aware that the following code also installs an event filter on the inner line edit in order to consistently check if the mouse leaves or enters it: the line edit is a child widget, so entering it doesn't result in a Leave
event for the parent.
class CustomTimeEdit(QTimeEdit):
clock_hourwidget = clock_minutewidget = None
_onArrow = False
def __init__(self, parent=None):
super(CustomTimeEdit, self).__init__(parent)
self.setDisplayFormat("HH:mm")
self.setCalendarPopup(False)
self.setMouseTracking(True)
self.lineEdit().installEventFilter(self)
...
def _setOnArrow(self, onArrow):
if self._onArrow != onArrow:
self._onArrow = onArrow
self.update()
def _checkOnArrow(self, pos):
style = self.style()
opt = QStyleOptionComboBox()
opt.initFrom(self)
arrowRect = style.subControlRect(style.CC_ComboBox, opt,
style.SC_ComboBoxArrow, self)
self._setOnArrow(arrowRect.contains(pos))
def mouseMoveEvent(self, event):
if not event.buttons():
self._checkOnArrow(event.pos())
else:
super().mouseMoveEvent(event)
def enterEvent(self, event):
super().enterEvent(event)
self._checkOnArrow(self.mapFromGlobal(QCursor.pos()))
def leaveEvent(self, event):
super().leaveEvent(event)
self._setOnArrow(False)
def eventFilter(self, obj, event):
if event.type() == event.Enter:
self._setOnArrow(False)
elif event.type() == event.Leave:
self._checkOnArrow(self.mapFromGlobal(QCursor.pos()))
return super().eventFilter(obj, event)
def paintEvent(self, event):
opt = QStyleOptionSpinBox()
QAbstractSpinBox.initStyleOption(self, opt)
style = self.style()
opt.subControls = (
style.SC_ComboBoxFrame | style.SC_ComboBoxEditField
| style.SC_ComboBoxArrow
)
if self.clock_hourwidget or self.clock_minutewidget:
opt.state |= style.State_Sunken
else:
opt.state &= ~style.State_Sunken
optCombo = QStyleOptionComboBox()
optCombo.initFrom(self)
optCombo.editable = True
optCombo.frame = opt.frame
optCombo.subControls = opt.subControls
optCombo.state = opt.state
if self._onArrow:
optCombo.activeSubControls = style.SC_ComboBoxArrow
qp = QPainter(self)
self.style().drawComplexControl(
style.CC_ComboBox, optCombo, qp, self)
Note that QDesktopWidget has been declared obsolete for some time now, and QScreen should always be preferred instead. Also, calling the desktop widget's cursor()
to get its position is pointless (since it's the same as the static QCursor.pos()
), and the geometry computation should be more careful (other than simpler). For instance:
...
def open_clock_hourwidget(self):
self.hours = self.time().hour()
self.clock_hourwidget = Clock_hourWidget(self.hours, self.return_hour)
self.adjustPopup(self.clock_hourwidget)
def adjustPopup(self, widget):
pos = self.mapToGlobal(self.rect().bottomLeft())
geo = widget.geometry()
geo.moveTopLeft(pos)
screen = (QApplication.screenAt(pos) or self.screen()).geometry()
if geo.right() > screen.right():
geo.moveRight(screen.right())
if geo.x() < screen.x():
geo.moveLeft(screen.left())
if geo.bottom() > screen.bottom():
geo.moveBottom(screen.bottom())
if geo.y() < screen.y():
geo.moveTop(screen.y())
widget.setGeometry(geo)
widget.show()
Note that I specifically avoided any elif
above. While we could safely assume that almost nobody would use a screen so small that wouldn't fit the popup, it's still better to formally consider the possibility anyway.
Finally, some further suggestions:
timeChanged
signal of QTimeEdit is emitted;lineEdit()
, meaning that simply overriding mousePressEvent()
based on the button is probably inconsistent, as the user may be clicking on the frame; consider checking the actual press position based on how the _checkOnArrow()
function above was implemented, which is what editable combo boxes actually do;