Issue
I am attempting to use a QTimer
to append buffered lines to my subclass of QStandatdItemModel
.
Note that in my code below I have also tried using QTimer.singleShot(self.refresh, self.__dump_buffer)
instead of instantiating QTimer
, and neither style seems to work, static member or instantiated class methods. I also tried moving the QTimer
to the main thread, this did not help. No matter what I try, the slot connected to QTimer.timeout
never seems to be fired, no matter what I try. I have verified this by the fact that Dump buffer
is never printed to the console.
class ProgressLogItemModel(QStandardItemModel):
def __init__(self, parent=None, limit=1000, refresh=20):
super().__init__(parent)
self.limit = limit
self.timer = QTimer()
self.buffer = list()
# self.timer.moveToThread(QApplication.instance().thread())
self.timer.setSingleShot(True)
self.timer.setInterval(refresh) # The default is 20ms, being 50Hz
# Every time the single-shot timer runs out after 'refresh' milliseconds, dump the buffer
self.timer.timeout.connect(self.__dump_buffer)
@helpers.make_slot()
def __dump_buffer(self):
print("Dump buffer")
self.insertRows(self.rowCount(), len(self.buffer))
for offset, item in enumerate(self.buffer):
self.setData(self.index(self.rowCount() - len(self.buffer) + offset, 0), item)
self.buffer.clear() # Reset the buffer
# Not sure if the 'stop()' this is necessary but it is here
# to ensure that 'isActive()' returns 'False'
self.timer.stop()
def __apply_limit(self):
if self.rowCount() > self.limit:
# Remove rows from the beginning, count being the number of rows over the limit
self.removeRows(0, self.rowCount() - self.limit)
def insertRows(self, row, count, _=None):
super().insertRows(row, count)
self.__apply_limit()
def appendRow(self, item):
# Append the QStandardItem to the internal list to be popped into the model on the next timeout
self.buffer.append(item)
# If the timer is not currently running (this method has not been called
# before less than 'refresh' milliseconds ago), start the next single-shot to dump the buffer
if not self.timer.isActive():
print("Timer started")
self.timer.start() # Start the next single-shot
class ExampleProgressLogDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.progress_log_list_view = QListView(self)
self.progress_log_item_model = ProgressLogItemModel(self.progress_log_list_view)
self.progress_log_list_view.setModel(self.progress_log_item_model)
self.setLayout(QVBoxLayout(self))
self.layout().addWidget(self.progress_log_list_view)
def log(self, text):
self.progress_log_item_model.appendRow(QStandardItem(text))
if __name__ == "__main__":
import sys
from threading import Thread
from qtpy.QtWidgets import QApplication
app = QApplication()
dialog = ExampleProgressLogDialog()
def __add_log_lines():
for line in range(10000):
dialog.log(f"Log line: {line}")
add_log_lines_thread = Thread(target=__add_log_lines(), daemon=True)
add_log_lines_thread.start()
dialog.show()
sys.exit(app.exec_())
In reality the subclass'ed QDialog
would be instantiated from an instance of QMainWindow
when feedback should be given to the user. The QTimer
would be started from any potential thread that wants to call the log
method.
Solution
I don't know what helpers.make_slot
does, so I just commented it, and the __dump_buffer
is correctly called.
There is one big problem, though: with QStandardItemModel it's not enough to insert rows, you also have to set QStandardItems for those indexes using setItem()
(and not setData()
).
Change that cycle to this:
for offset, item in enumerate(self.buffer):
self.setItem(self.rowCount() - len(self.buffer) + offset, 0, item)
Note that access to Qt objects directly from a python thread is discouraged. While, in this case, it's not a big issue (since QStandardItems are not QObjects, but QStandardItemModel is), it's usually better to create QThread subclasses and use signals/slots to interact with Qt objects that are in the main thread.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.