I need a Spinner widget in which the user can select integer values with a certain step and without lower or upper limits
(I mean, they should be at least in the billion range, so no chance of memorizing the whole sequence).
I saw kivy's Spinner widget but I don't think doing something like Spinner(values=itertool.count()) would work.
Also it is limited to string values.
Is there any simple way of obtaining something similar to QSpinBox of the Qt?
It seems like kivy at the moment does not provide anything similar to a Spinner or SpinBox or however you want to call it. A widget that might be used instead is the Slider but it looks awful and it's not so useful if you want to allow a very big range but with a small step.
Therefore I wrote my own implementation of a SpinBox:
class SpinBox(BoxLayout):
"""A widget to show and take numeric inputs from the user.
:param min_value: Minimum of the range of values.
:type min_value: int, float
:param max_value: Maximum of the range of values.
:type max_value: int, float
:param step: Step of the selection
:type step: int, float
:param value: Initial value selected
:type value: int, float
:param editable: Determine if the SpinBox is editable or not
:type editable: bool
"""
min_value = NumericProperty(float('-inf'))
max_value = NumericProperty(float('+inf'))
step = NumericProperty(1)
value = NumericProperty(0)
range = ReferenceListProperty(min_value, max_value, step)
def __init__(self, btn_size_hint_x=0.2, **kwargs):
super(SpinBox, self).__init__(orientation='horizontal', **kwargs)
self.value_label = Label(text=str(self.value))
self.inc_button = TimedButton(text='+')
self.dec_button = TimedButton(text='-')
self.inc_button.bind(on_press=self.on_increment_value)
self.inc_button.bind(on_time_slice=self.on_increment_value)
self.dec_button.bind(on_press=self.on_decrement_value)
self.dec_button.bind(on_time_slice=self.on_decrement_value)
self.buttons_vbox = BoxLayout(orientation='vertical',
size_hint_x=btn_size_hint_x)
self.buttons_vbox.add_widget(self.inc_button)
self.buttons_vbox.add_widget(self.dec_button)
self.add_widget(self.value_label)
self.add_widget(self.buttons_vbox)
def on_increment_value(self, btn_instance):
if float(self.value) + float(self.step) <= self.max_value:
self.value += self.step
def on_decrement_value(self, btn_instance):
if float(self.value) - float(self.step) >= self.min_value:
self.value -= self.step
def on_value(self, instance, value):
instance.value_label.text = str(value)
Actually the code I use is slightly different because I think it is ugly to subclass a layout to implement a widget and thus I subclassed Widget and added a horizontal BoxLayout as only children of the Widget, then I binded every size and position change to update the size and position of this child(see this question for why I had to do that).
The TimedButton is a subclass of Button that allows long-presses and, when long-pressed, emits a on_time_slice event every a certain amount of millisecond(thus the user will be able to hold the button to do a continuous increment). You can simply use a normal Button if you want, removing the binds to on_time_slice event.
The TimedButton source code is this:
class TimedButton(Button):
"""A simple ``Button`` subclass that produces an event at regular intervals
when pressed.
This class, when long-pressed, emits an ``on_time_slice`` event every
``time_slice`` milliseconds.
:param long_press_interval: Defines the minimum time required to consider
the press a long-press.
:type long_press_interval: int
:param time_slice: The number of milliseconds of each slice.
:type time_slice: int
"""
def __init__(self, long_press_interval=550, time_slice=225, **kwargs):
super(TimedButton, self).__init__(**kwargs)
self.long_press_interval = long_press_interval
self.time_slice = time_slice
self._touch_start = None
self._long_press_callback = None
self._slice_callback = None
self.register_event_type('on_time_slice')
self.register_event_type('on_long_press')
def on_state(self, instance, value):
if value == 'down':
start_time = time.time()
self._touch_start = start_time
def callback(dt):
self._check_long_press(dt)
Clock.schedule_once(callback, self.long_press_interval / 1000.0)
self._long_press_callback = callback
else:
end_time = time.time()
delta = (end_time - (self._touch_start or 0)) * 1000
Clock.unschedule(self._slice_callback)
# Fixes the bug of multiple presses causing fast increase
Clock.unschedule(self._long_press_callback)
if (self._long_press_callback is not None and
delta > self.long_press_interval):
self.dispatch('on_long_press')
self._touch_start = None
self._long_press_callback = self._slice_callback = None
def _check_long_press(self, dt):
delta = dt * 1000
if delta > self.long_press_interval and self.state == 'down':
self.dispatch('on_long_press')
self._long_press_callback = None
def slice_callback(dt):
self.dispatch('on_time_slice')
return self.state == 'down'
Clock.schedule_interval(slice_callback, self.time_slice / 1000.0)
self._slice_callback = slice_callback
def on_long_press(self):
pass
def on_time_slice(self):
pass
Note that I had to bind the state property instead of using on_touch_down and on_touch_up because they give some strange behaviour, and even when "working" there were some strange things happening by no reason(e.g. clicking the decrement button caused on_increment to be called even though the bindings where correct).
Edit: Updated the TimedButton class fixing a little bug(the previous implementation when clicked rapidly multiple times and then holding down the button would yield too many on_time_slice events: I'd forgot to "unschedule" the _long_press_callback when the state goes 'normal'
Related
I have a QTimeEdit in Python with a predefined range less than one hour, let's say from 08:45:00 to 09:15:00. I read about the problematic of entering a new value which gets out these limits when keying (https://doc.qt.io/qt-6/qdatetimeedit.html#keyboard-tracking) and set the keyboardTracking to False. I set the default value to minimum (so 08:45:00), then I can't change it to values above 08:59:59 because the spin arrows are deactivated for hour field, and I can't change 08 to 09 in hour field with the numpad neither.
Do you experience the same limitations for QTimeEdit especially ?
Btw, the wrapping function isn't adapted to times as it loops on the same field without incrementing the next one...
tl;dr
Some solutions already exist for this issue only related to the wheel and arrow buttons, but they don't consider keyboard editing.
In order to achieve that, it's necessary to override the validate() function (inherited from QAbstractSpinBox) and eventually try to fix up its contents:
class FlexibleTimeEdit(QTimeEdit):
def validate(self, input, pos):
valid, newInput, newPos = super().validate(input, pos)
if valid == QValidator.Invalid:
possible = QTime.fromString(newInput)
if possible.isValid():
fixed = max(self.minimumTime(), min(possible, self.maximumTime()))
newInput = fixed.toString(self.displayFormat())
valid = QValidator.Acceptable
return valid, newInput, newPos
A more complete solution
Since these aspects are actually common within the other related classes (QDateTimeEdit and QDateEdit), I propose a more comprehensive fix that could be used as a mixin with all three types, providing keyboard input and arrow/wheel fixes for these aspects.
The fix works by using an "abstract" class that has to be used with multiple inheritance (with it taking precedence over the Qt class), and provides the following:
optionally override the wheel behavior by setting the cursor position based on the mouse position, allowing to update a specific section without using the keyboard or clicking to change it (i.e.: if the current section is the hour one, and the mouse is on the minutes, then the wheel will update the minutes);
updates the arrow buttons (and related stepBy() calls) depending on the available range, without limiting the range to the section: if the current hour is 23 and the current range allows past the midnight, stepping up will update the value accordingly;
the validation allows values within the full current range, without limiting it to the section range;
Note that this is a bit advanced, so I strongly advise to carefully study the following code in order to understand how it works.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class _DateTimeEditFix(object):
_fullRangeStepEnabled = False
_wheelFollowsMouse = True
_deltaFuncs = {
QDateTimeEdit.YearSection: lambda obj, delta: obj.__class__.addYears(obj, delta),
QDateTimeEdit.MonthSection: lambda obj, delta: obj.__class__.addMonths(obj, delta),
QDateTimeEdit.DaySection: lambda obj, delta: obj.__class__.addDays(obj, delta),
QDateTimeEdit.HourSection: lambda obj, delta: obj.__class__.addSecs(obj, delta * 3600),
QDateTimeEdit.MinuteSection: lambda obj, delta: obj.__class__.addSecs(obj, delta * 60),
QDateTimeEdit.SecondSection: lambda obj, delta: obj.__class__.addSecs(obj, delta),
QDateTimeEdit.MSecSection: lambda obj, delta: obj.__class__.addMSecs(obj, delta),
}
_typeRefs = {
QTimeEdit: ('Time', QTime),
QDateEdit: ('Date', QDate),
QDateTimeEdit: ('DateTime', QDateTime)
}
_sectionTypes = {
QDateTimeEdit.YearSection: 'date',
QDateTimeEdit.MonthSection: 'date',
QDateTimeEdit.DaySection: 'date',
QDateTimeEdit.HourSection: 'time',
QDateTimeEdit.MinuteSection: 'time',
QDateTimeEdit.MSecSection: 'time'
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for cls in QTimeEdit, QDateEdit, QDateTimeEdit:
if isinstance(self, cls):
ref, self._baseType = self._typeRefs[cls]
break
else:
raise TypeError('Only QDateTimeEdit subclasses can be used')
self._getter = getattr(self, ref[0].lower() + ref[1:])
self._setter = getattr(self, 'set' + ref)
self._minGetter = getattr(self, 'minimum' + ref)
self._maxGetter = getattr(self, 'maximum' + ref)
#pyqtProperty(bool)
def fullRangeStepEnabled(self):
'''
Enable the arrows if the current value is still within the *full*
range of the widget, even if the current section is at the minimum
or maximum of its value.
If the value is False (the default), using a maximum time of 20:30,
having the current time at 20:29 and the current section at
HourSection, the up arrow will be disabled. If the value is set to
True, the arrow is enabled, and going up (using arrow keys or mouse
wheel) will set the new time to 20:30.
'''
return self._fullRangeStepEnabled
#fullRangeStepEnabled.setter
def fullRangeStepEnabled(self, enabled):
if self._fullRangeStepEnabled != enabled:
self._fullRangeStepEnabled = enabled
self.update()
def setFullRangeStepEnabled(self, enabled):
self.fullRangeStepEnabled = enabled
#pyqtProperty(bool)
def wheelFollowsMouse(self):
'''
By default, QDateTimeEdit "scrolls" with the mouse wheel updating
the section in which the cursor currently is, even if the mouse
pointer hovers another section.
Setting this property to True always tries to update the section
that is *closer* to the mouse cursor.
'''
return self._wheelFollowsMouse
#wheelFollowsMouse.setter
def wheelFollowsMouse(self, follow):
self._wheelFollowsMouse = follow
def wheelEvent(self, event):
if self._wheelFollowsMouse:
edit = self.lineEdit()
edit.setCursorPosition(edit.cursorPositionAt(event.pos() - edit.pos()))
super().wheelEvent(event)
def stepBy(self, steps):
section = self.currentSection()
if section in self._deltaFuncs:
new = self._deltaFuncs[section](self._getter(), steps)
self._setter(
max(self._minGetter(), min(new, self._maxGetter()))
)
self.setSelectedSection(section)
else:
super().stepBy(steps)
def _stepPossible(self, value, target, section):
if self._fullRangeStepEnabled:
return value < target
if value > target:
return False
if section in self._deltaFuncs:
return self._deltaFuncs[section](value, 1) < target
return False
def stepEnabled(self):
enabled = super().stepEnabled()
current = self._getter()
section = self.currentSection()
if (
not enabled & self.StepUpEnabled
and self._stepPossible(current, self._maxGetter(), section)
):
enabled |= self.StepUpEnabled
if (
not enabled & self.StepDownEnabled
and self._stepPossible(self._minGetter(), current, section)
):
enabled |= self.StepDownEnabled
return enabled
def validate(self, input, pos):
valid, newInput, newPos = super().validate(input, pos)
if valid == QValidator.Invalid:
# note: Qt6 deprecated some fromString() forms and QLocale functions
# should be preferred instead; see the documentation
possible = self._baseType.fromString(newInput, self.displayFormat())
if possible.isValid():
m = self._minGetter()
M = self._maxGetter()
fixedUp = max(m, min(possible, M))
if (
self._fullRangeStepEnabled
or m <= fixedUp <= M
):
newInput = fixedUp.toString(self.displayFormat())
valid = QValidator.Acceptable
return valid, newInput, newPos
class BetterDateTimeSpin(_DateTimeEditFix, QDateTimeEdit): pass
class BetterTimeSpin(_DateTimeEditFix, QTimeEdit): pass
class BetterDateSpin(_DateTimeEditFix, QDateEdit): pass
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
test = QWidget()
layout = QVBoxLayout(test)
fullRangeCheck = QCheckBox('Allow full range')
layout.addWidget(fullRangeCheck)
timeSpin = BetterTimeSpin(
displayFormat='hh:mm:ss',
minimumTime=QTime(8, 45, 0),
maximumTime=QTime(9, 15, 50),
)
layout.addWidget(timeSpin)
dateSpin = BetterDateTimeSpin(
displayFormat='dd/MM/yy hh:mm',
minimumDateTime=QDateTime(2022, 9, 15, 19, 25),
maximumDateTime=QDateTime(2023, 2, 12, 4, 58),
)
layout.addWidget(dateSpin)
fullRangeCheck.toggled.connect(lambda full: [
timeSpin.setFullRangeStepEnabled(full),
dateSpin.setFullRangeStepEnabled(full),
])
test.show()
sys.exit(app.exec())
Note: as with the standard QTimeEdit control, it's still not possible to use the time edit with a range having a minimum time greater than the maximum (ie: from 20:00 to 08:00).
I'm trying to implement a QDateTimeEdit subclass that will show only time part (display format "HH:mm") and a calendar icon, clicking on which will trigger calendar popup, allowing user to set date part. I need it to have a shorter representation of datetime, because changing date is needed only rarely.
Adding .setCalendarPopup(True) is not enough here, because it triggers only if the format string includes date part, which is not the case.
What I have tried (button embedding was taken from 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
My solution doesn't work as self.calendarWidget() returns None (see last line).
So the question is, is there any way to trigger calendar popup (and use the value provided by it) from QDateTimeEdit without using date section in display format string?
QDateTimeEdit is a special type of widget that implements its features also based on its display format. Long story short, if you set a display format that doesn't show any date value, it will behave exactly like a QTimeEdit (and if you don't show time values, it will behave like a QDateEdit).
This is clearly by-design and, I suppose, for optimization reasons, but that approach also presents some issues, exactly like in your case.
In fact, you cannot even set your own calendar widget if the display format doesn't show any date value (Qt will warn you about this).
In order to achieve what you want, some overriding of existing method is required.
Also consider that you cannot try to "hack" your way through this by adding a child widget. Not only your implementation will not work as expected, but it will also probably result in unexpected behavior and painting artifacts, which will make the widget at least ugly (if not even unusable). The best approach usually is to try to work with what the widget already provides, even if it might seem more complex than it would appear. In this case, you can manually paint the combobox arrow that would be shown for a "normal" QDateTimeEdit with setCalendarPopup(True) by using QStyle functions both for painting and click detection.
Finally, due to the design choices explained before, each time the date, the datetime or the display format are changed, the date range will be limited to the current date if the display format doesn't show date values, so you need to set the range back each time, otherwise it won't be possible to select any other date from the calendar.
Here's a possible implementation:
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)
An important note. Even when using setWrapping(True), QDateTimeEdit (and its subclasses) don't follow the time logic: when scrolling down from 01:00 in the minute section, the result will be 01:59, and the same happens for dates too. If you want to be able to actually step the other units, you need to override the stepBy() method too; in this way, when you scroll down from 01:00 on the minute section, the result will be 00:59, and if you scroll down from 00:00 it will go to 23:59 of the previous day.
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)
Obviously, this is a simplification: for full shown date/time display format, you should add the related implementations.
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)
Finally (again!), if you want to ensure that the mouse wheel always responds to the section that is under the mouse cursor, you need to place the text cursor to the appropriate position before calling the default wheelEvent().
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)
I have to create multiple buttons (for each element in a given list). For this I'm using for loops. The button that is pressed, must be recorded. However, the value of self.i is not stored at that particular instance, the last value (in this case - 2 is stored, after the loop is completed). How can I solve this error? Is there a way to add conditional statements to check if the button is clicked or not, and if it is, the value of self.i is stored in another variable.
class InfoPage(GridLayout):
def __init__(self, **kwargs):
super(InfoPage, self).__init__(**kwargs)
self.cols = 1
self.peers = ["ishika", "nate", "nf"]
for self.i in range(3):
self.peers[self.i] = Button(text=peers[self.i], id=str(self.i))
self.add_widget(self.peers[self.i])
self.peers[self.i].bind(on_press=self.add)
def add(self, _):
id_of_button_pressed = self.peers[self.i].id
print(id_of_button_pressed)
Your add() method is using self.i, which will be the last value that self.i was set to in the __init__() method (2). So you just need to modify your add() method to something like:
def add(self, button):
id_of_button_pressed = button.id
print(id_of_button_pressed)
So I am building I program that manages a bunch of custom slider widgets. Currently, I have a slider_container(class) that holds a list of slider objects(class). These slider objects are then inserted into the layout in the main window. This has been working well while I was only adding and moving the position of the sliders up and down. But when I try to delete the sliders, everything goes bad. When ever there the list of slider is manipulated (add, move or delete a slider), the clear and rebuild functions are called in the main window as seen below.
def clear_layout(self, layout):
print "Cleared Layout..."
while layout.count() > 0:
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
def rebuild_slider_display(self):
""" Delete the old sliders, then add all the new sliders """
self.clear_layout(self.slider_layout)
print "Rebuild layout..."
print len(self._widgets.widgets)
for i, widget in enumerate(self._widgets.widgets):
print widget.slider_name, " ", i
self.slider_layout.insertWidget(i, widget)
print "Layout widget count: ", self.slider_layout.count()
Currently I am running into this error on this line "self.slider_layout.insertWidget(i, widget)"
RuntimeError: wrapped C/C++ object of type SliderWidget has been deleted
My hunch is that storing the actual widget in the widget container is bad form. I think what is happening when I deleteLater() a widget, is that it isnt just deleting a widget from the list, it actually deletes the widget class that was store in the widget container itself.
Hopefully that is explained clearly, thanks for your help in advance.
Edit:
Here is the widget class:
class SliderWidget(QWidget, ui_slider_widget.Ui_SliderWidget):
""" Create a new slider. """
def __init__(self, name, slider_type, digits, minimum, maximum, value, index, parent=None):
super(SliderWidget, self).__init__(parent)
self.setupUi(self)
self.slider_name = QString(name)
self.expression = None
self.accuracy_type = int(slider_type)
self.accuracy_digits = int(digits)
self.domain_min = minimum
self.domain_max = maximum
self.domain_range = abs(maximum - minimum)
self.numeric_value = value
self.index = index
#self.name.setObjectName(_fromUtf8(slider.name))
self.update_slider_values()
self.h_slider.valueChanged.connect(lambda: self.update_spinbox())
self.spinbox.valueChanged.connect(lambda: self.update_hslider())
self.edit.clicked.connect(lambda: self.edit_slider())
# A unique has for the slider.
def __hash__(self):
return super(Slider, self).__hash__()
# How to compare if this slider is less than another slider.
def __lt__(self, other):
r = QString.localAware.Compare(self.name.toLower(), other.name.toLower())
return True if r < 0 else False
# How to compare if one slider is equal to another slider.
def __eq__(self, other):
return 0 == QString.localAwareCompare(self.name.toLower(), other.name.toLower())
And here is the actually creation of the widget in the widget container:
def add_slider(self, params=None):
if params:
new_widget = SliderWidget(params['name'], params['slider_type'], params['digits'], params['minimum'],
params['maximum'], params['value'], params['index'])
else:
new_widget = SliderWidget('Slider_'+str(self.count()+1), 1, 0, 0, 50, 0, self.count())
#new_widget.h_slider.valueChanged.connect(self.auto_save)
#new_widget.h_slider.sliderReleased.connect(self.slider_released_save)
new_widget.move_up.clicked.connect(lambda: self.move_widget_up(new_widget.index))
new_widget.move_down.clicked.connect(lambda: self.move_widget_down(new_widget.index))
self.widgets.append(new_widget)
Thanks for all the help!
The problem I was having was with the way I cleared the layout. It is important to clear the layout from the bottom to the top as seen below.
for i in reversed(range(layout.count())):
layout.itemAt(i).widget().setParent(None)
I'm trying to find a way to update labels which are within dynamically created widgets (that can be deleted), according to properties dynamically set in the preceding widgets.
Is there a way to automatically and dynamically link a pyqt object to a property of other widgets, so that when I change a value anywhere, that object updates itself?
Example: object 'a' has property start, bars and end; bars is given, start is taken by the previous object (or 1 if None), end is calculated. object 'b' takes its start from a.end and so on.
class myclass(object):
def __init__(self, referrer=None, value=1):
self.referrer=referrer
self.value=value
def _get_start(self):
if not self.referrer:
return 1
else:
return self.referrer.end+1
def _get_end(self):
return self.start+self.value-1
start=property(_get_start)
end=property(_get_end)
def create(value=1):
if not vList:
ref=None
else:
ref=vList[-1]
val=myclass(ref, value)
vList.append(val)
def showList():
for i in vList:
print 'item: %d\tstart: %d\tend: %d' % (vList.index(i),i.start, i.end)
vList=[]
If I call create() 3 times, showList() will show:
item: 0 start: 1 end: 1
item: 1 start: 2 end: 2
item: 2 start: 3 end: 3
if I change vList[0].value to 3:
item: 0 start: 1 end: 3
item: 1 start: 4 end: 4
item: 2 start: 5 end: 5
The problem raises when I need to keep those values updated in the gui (think to it as an interface like this): every horizontal widget has a label showing the property of start, a spinbox for bars, and a label for end, and as soon as any spinbox value changes, every subsequent widget should update its start and end properties according to its previous widget and show them in the relative labels.
Moreover, when any widget is deleted, all the subsequent widget should recompute every property.
Using getter/setter to set the values in the labels of the widgets' next instancies doesn't obviously work as I need, because when I change any x.value, the following instancies' start and end will be actually updated only when recalled, AFAIU.
I could connect every new widget to its previous (with valueChanged()) or create a function which finds the subsequent widget and update their properties, but that's not a good solution.
Since I am almost new to python (most important: I'm not a programmer) I think that I am ignoring something about "connecting" variables in a better and cleanest way (maybe related to signals or threading?).
Consider that those widgets will actually be children widgets of another "main" widget, which will have similar properties: start taken from its previous main widget (if any), bars which is the sum of all bars in every children widget, end which will be again start+bars-a.
Thanks!
(I hope you will understand what I meant, my english is not perfect and, since I'm not a programmer, my terminology is not always correct)
I can't find use case for things from your question, but here is possible solution using Qt Signals-Slots:
# -*- coding: utf-8 -*-
import functools
from PyQt4 import QtGui, QtCore
class ObservableVariable(QtCore.QObject):
""" Represents variable with value, when value changes it emits
signal: changed(new_value)
"""
changed = QtCore.pyqtSignal(object)
def __init__(self, initial_value=0):
super(ObservableVariable, self).__init__()
self._value = initial_value
def get_value(self):
return self._value
def set_value(self, new_val):
self._value = new_val
self.changed.emit(new_val)
value = property(get_value, set_value)
def __str__(self):
return str(self.value)
# it can support more operators if needed
def __iadd__(self, other):
self.value += other
return self
def __isub__(self, other):
self.value -= other
return self
class MyClass(object):
def __init__(self, referrer=None, value=1):
self.referrer = referrer
self.value = ObservableVariable(value)
self._initial_value = value
if referrer:
# propagate referrer changes to subscribers
referrer.value.changed.connect(
lambda x: self.value.changed.emit(self.value.value)
)
#property
def start(self):
if not self.referrer:
return self.value.value
return self.referrer.end + 1
#property
def end(self):
return self.start + self.value.value - 1
class GuiExample(QtGui.QWidget):
def __init__(self):
super(GuiExample, self).__init__()
self.values = []
layout = QtGui.QVBoxLayout(self)
obj = None
for i in range(5):
obj = MyClass(obj, i)
self.values.append(obj)
# create gui elements
hlayout = QtGui.QHBoxLayout()
spinbox = QtGui.QSpinBox()
spinbox.setValue(obj.value.value)
start_label = QtGui.QLabel()
end_label = QtGui.QLabel()
hlayout.addWidget(start_label)
hlayout.addWidget(spinbox)
hlayout.addWidget(end_label)
layout.addLayout(hlayout)
# function called on value change
def update_start_end(instance, start_label, end_label):
start_label.setText(str(instance.start))
end_label.setText(str(instance.end))
action = functools.partial(update_start_end, obj, start_label,
end_label)
action() # set initial start end text to labels
# connect signals to gui elements
obj.value.changed.connect(action)
spinbox.valueChanged.connect(obj.value.set_value)
obj.value.changed.connect(spinbox.setValue)
layout.addWidget(QtGui.QPushButton('test',
clicked=self.test_modification))
def test_modification(self):
self.values[1].value += 1
app = QtGui.QApplication([])
gui = GuiExample()
gui.show()
app.exec_()