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_()
Related
I'm trying to build an application with PyQT6 that allows users to browse through a list of images with thumbnails and display the selected image in an image viewer. The application can also add and delete images. Adding images seems to work fine, but when I delete an image from the model the row in the QListView suddenly displays the data from the next row in the list. After a random interval of anywhere between half a second and about five seconds the row will actually be removed, and the list will display the proper file ordering. The fact that this behavior occurs makes me think I'm not removing the item from the model properly, and ideally I'd like the deletion of a row to be instantaneous.
Here is my minimum reproducible example:
import PyQt6 as qt
import PyQt6.QtCore as QtCore
from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
import os
import sys
import traceback
class ImageDataModel(QAbstractListModel):
def __init__(self, images=None):
super(ImageDataModel, self).__init__()
if images is None:
self.images = []
else:
self.images = images
self.thumbnails = []
for img_path in self.images:
icon = QPixmap(img_path).scaledToHeight(20)
self.thumbnails.append(icon)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
img_path = self.images[index.row()]
return img_path
if role == Qt.ItemDataRole.DecorationRole:
thumbnail = self.thumbnails[index.row()]
return thumbnail
def rowCount(self, index):
return len(self.images)
def removeRow(self, index):
self.images.pop(index)
self.thumbnails.pop(index)
class myListView(QListView):
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setSelectionMode(QListView.SelectionMode.ExtendedSelection)
def currentChanged(self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex) -> None:
if (current.row() >= 0):
self.parent.get_selection(current) # this method simply displays the selected image
return super().currentChanged(current, previous)
class MyMenu(QMainWindow):
def __init__(self):
super().__init__()
self.layout = QHBoxLayout()
self.list = myListView(self)
try:
image_file_list = [x for x in os.listdir('path/to/image/directory') if x.lower().endswith(".png")]
except:
image_file_list = []
image_file_list.sort()
self.model = ImageDataModel(image_file_list)
self.list.setModel(self.model)
self.list.clicked.connect(self.get_selection) # this method simply displays the selected image
self.list.setCurrentIndex(self.model.index(0,0))
self.layout.addWidget(self.list)
self.widget = QWidget()
self.widget.setLayout(self.layout)
self.setCentralWidget(self.widget)
# Deletes the currently displayed image and annotation from the dataset
def delete_image(self):
# Determine what to set the new index to after deletion
if self.list.currentIndex().row() != 0:
new_index = self.list.currentIndex().row() - 1
else:
new_index = 0
# Attempt to remove the row and delete the file
try:
self.list.model().removeRow(self.list.currentIndex().row())
os.remove(self.img_path)
# Set index row to the image immediately preceding the deleted image
index = self.model.createIndex(new_index, 0)
self.list.setCurrentIndex(index)
except:
traceback.print_exc()
# Replaced display code for brevity
def get_selection(self, item):
print(item.row())
# Handles keypresses
def keyPressEvent(self, e) -> None:
global model_enabled
if (e.key() == Qt.Key.Key_Escape):
app.quit()
if (e.key() == Qt.Key.Key_Delete):
self.delete_image()
def main():
app = QApplication(sys.argv)
window = MyMenu()
window.show()
app.exec()
main()
Any change in the size, order/layout and data of a model should always be done using the proper function calls so that the views linked to the model get proper notifications about those changes.
For size and layout changes, it's important to always call the begin* and end* functions, which allows the view to be notified about the upcoming change, so they can keep a persistent list of the current items (including selection) and restore it when the change is completed.
Row removal is achieved using beginRemoveRows() and endRemoveRows().
In your case:
def removeRow(self, index):
self.beginRemoveRows(QModelIndex(), index, index)
self.images.pop(index)
self.thumbnails.pop(index)
self.endRemoveRows()
return True # <- the function expects a bool in return
Note that the correct way to implement row removal is done by implementing removeRows(), not removeRow() (singular), which internally calls removeRows anyway. So, you can leave the existing removeRow() call, do not override removeRow() and implement removeRows() instead.
def removeRows(self, row, count, parent=QModelIndex()):
if row + count >= len(self.images) or count < 1:
return False
self.beginRemoveRows(parent, row, row + count - 1)
del self.images[row:row+count]
del self.thumbnails[row:row+count]
self.endRemoveRows()
return True
A similar concept should always be done when adding new items after a view is linked to the model; in that case, implement insertRows() and there you'll call beginInsertRows() insert the new data and finally call endInsertRows().
Note that your code will throw an exception if the images is None, as it doesn't create the thumbnails object.
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 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'
I'm rewriting this post to clarify some things and provide a full class definition for the Virtual List I'm having trouble with. The class is defined like so:
from wx import ListCtrl, LC_REPORT, LC_VIRTUAL, LC_HRULES, LC_VRULES, \
EVT_LIST_COL_CLICK, EVT_LIST_CACHE_HINT, EVT_LIST_COL_RIGHT_CLICK, \
ImageList, IMAGE_LIST_SMALL, Menu, MenuItem, NewId, ITEM_CHECK, Frame, \
EVT_MENU
class VirtualList(ListCtrl):
def __init__(self, parent, datasource = None,
style = LC_REPORT | LC_VIRTUAL | LC_HRULES | LC_VRULES):
ListCtrl.__init__(self, parent, style = style)
self.columns = []
self.il = ImageList(16, 16)
self.Bind(EVT_LIST_CACHE_HINT, self.CheckCache)
self.Bind(EVT_LIST_COL_CLICK, self.OnSort)
if datasource is not None:
self.datasource = datasource
self.Bind(EVT_LIST_COL_RIGHT_CLICK, self.ShowAvailableColumns)
self.datasource.list = self
self.Populate()
def SetDatasource(self, datasource):
self.datasource = datasource
def CheckCache(self, event):
self.datasource.UpdateCache(event.GetCacheFrom(), event.GetCacheTo())
def OnGetItemText(self, item, col):
return self.datasource.GetItem(item, self.columns[col])
def OnGetItemImage(self, item):
return self.datasource.GetImg(item)
def OnSort(self, event):
self.datasource.SortByColumn(self.columns[event.Column])
self.Refresh()
def UpdateCount(self):
self.SetItemCount(self.datasource.GetCount())
def Populate(self):
self.UpdateCount()
self.datasource.MakeImgList(self.il)
self.SetImageList(self.il, IMAGE_LIST_SMALL)
self.ShowColumns()
def ShowColumns(self):
for col, (text, visible) in enumerate(self.datasource.GetColumnHeaders()):
if visible:
self.columns.append(text)
self.InsertColumn(col, text, width = -2)
def Filter(self, filter):
self.datasource.Filter(filter)
self.UpdateCount()
self.Refresh()
def ShowAvailableColumns(self, evt):
colMenu = Menu()
self.id2item = {}
for idx, (text, visible) in enumerate(self.datasource.columns):
id = NewId()
self.id2item[id] = (idx, visible, text)
item = MenuItem(colMenu, id, text, kind = ITEM_CHECK)
colMenu.AppendItem(item)
EVT_MENU(colMenu, id, self.ColumnToggle)
item.Check(visible)
Frame(self, -1).PopupMenu(colMenu)
colMenu.Destroy()
def ColumnToggle(self, evt):
toggled = self.id2item[evt.GetId()]
if toggled[1]:
idx = self.columns.index(toggled[2])
self.datasource.columns[toggled[0]] = (self.datasource.columns[toggled[0]][0], False)
self.DeleteColumn(idx)
self.columns.pop(idx)
else:
self.datasource.columns[toggled[0]] = (self.datasource.columns[toggled[0]][0], True)
idx = self.datasource.GetColumnHeaders().index((toggled[2], True))
self.columns.insert(idx, toggled[2])
self.InsertColumn(idx, toggled[2], width = -2)
self.datasource.SaveColumns()
I've added functions that allow for Column Toggling which facilitate my description of the issue I'm encountering. On the 3rd instance of this class in my application the Column at Index 1 will not display String values. Integer values are displayed properly. If I add print statements to my OnGetItemText method the values show up in my console properly. This behavior is not present in the first two instances of this class, and my class does not contain any type checking code with respect to value display.
It was suggested by someone on the wxPython users' group that I create a standalone sample that demonstrates this issue if I can. I'm working on that, but have not yet had time to create a sample that does not rely on database access. Any suggestions or advice would be most appreciated. I'm tearing my hair out on this one.
Are you building on the wxPython demo code for virtual list controls? There are a couple of bookkeeping things you need to do, like set the ItemCount property.
One comment about your OnGetItemText method: Since there's no other return statement, it will return None if data is None, so your test has no effect.
How about return data or "" instead?
There's a problem with the native object in Windows. If GetImg returns None instead of -1 the list has a problem with column 1 for some reason. That from Robin over on the Google Group post for this issue.