Is there any way to make a model to update a QTextEdit like the QAbstractItemModel.
The QAbstractItemModel/QAbstractTableModel has a data (index[, role=Qt.DisplayRole]) method which is called whenever the view needs to display something. This allows me to make my own data structure to save data in python quickly. Is there any way to make a QTextEdit, QTextDocument, or QTextDocumentLayout work this way?
Right now I save data to a queue and periodically display the data by running update_display on a timer.
class QuickTextEdit(QtWidgets.QTextEdit):
def update_display(self):
for _ in range(len(self.queue)):
text, fmt = self.queue.popleft()
cursor = QtGui.QTextCursor(self.textCursor())
cursor.beginEditBlock()
# Move and check the position
pos = cursor.position()
cursor.movePosition(QtGui.QTextCursor.End)
is_end = cursor.position() == pos
# Insert the text
cursor.setCharFormat(fmt)
cursor.insertText(text)
cursor.endEditBlock()
# Move the cursor
if is_end:
# Actually move the text cursor
cursor.movePosition(QtGui.QTextCursor.End)
self.setTextCursor(cursor)
I found that this is much slower than the way a QAbstractTableModel/QTableView works.
I would like the widget to request data (instead of inserting) like the QAbstractItemModel. I tried using an html string in a QLabel.paintEvent, but couldn't get that to work properly. I really would just like an HTML Text Viewer for some data structure/model.
class QuickTextEdit(QtWidgets.QTextEdit):
def data(self):
return ''.join(('<font color="{color}">{text}</font>'.format(color=color, text=text)
for color, text in self.queue))
You can implement a simple model view setup yourself.
Here is some pseudo code:
class TextEditView(QtWidgets.QTextEdit):
def __init__(self, model):
connect(model.dataChanged, self.update_display)
#SLOT
def update_display(self):
text = model.data()
self.setText(text)
class TextEditModel(QObject):
SIGNAL dataChanged
def data(self):
new_text = queue.get_nowait()
return new_text
class WorkerThread(QtCore.QThread):
def run(self):
queue.put(new_data)
model.dataChanged.emit(index, index)
Then in your thread you would first put the new data into the queue. And then issue a model.dataChanged signal. Then the view would poll the new data.
Alternatively you can skip the whole Model view separation and create a signal for TextEditView with the datatype you want.
Let me know how it goes.
Related
Question: What is the right way to not block a GTK UI while loading data from a remote HTTP source?
Background:
I'm working on a cryptocurrency price indicator for Ubuntu Unity. I've modified the original project so that it can run multiple tickers at the same time.
Schematically, it works like this:
The main thread finds every ticker to be loaded and spins up a GTK libAppindicator instance (Indicator) for each. Each indicator then loads a class that retrieves ticker data from the remote API and feeds it back to the indicator.
However, I noticed that the UI becomes irresponsive at irregular intervals. I assume that this is because the HTTP requests (with the requests library) are blocking. So I decided to start every Indicator in its own thread, but the same issue still occurs once the tickers are all loaded. (I don't understand why, if something is on its own thread, it should not block the main thread?)
I've then tried to replace requests with grequests, however I can't get this to work as the callback seems to never get called. There also seems to be something like promises, but the documentation for all of this looks outdated and incomplete.
Code:
# necessary imports here
class Coin(Object):
self.start_main()
self.instances = []
def start_main(self):
self.main_item = AppIndicator.Indicator.new(name, icon)
self.main_item.set_menu(self._menu()) # loads menu and related actions (code omitted)
def add_indicator(self, settings=None):
indicator = Indicator(self, len(self.instances), self.config, settings)
self.instances.append(indicator)
nt = threading.Thread(target=indicator.start())
nt.start()
def _add_ticker(self, widget): # this is clickable from the menu (code omitted)
self.add_indicator('DEFAULTS')
class Indicator():
instances = []
def __init__(self, coin, counter, config, settings=None):
Indicator.instances.append(self)
def start(self):
self.indicator = AppIndicator.Indicator.new(name + "_" + str(len(self.instances)), icon)
self.indicator.set_menu(self._menu())
self._start_exchange()
def set_data(self, label, bid, high, low, ask, volume=None):
# sets data in GUI
def _start_exchange(self):
exchange = Kraken(self.config, self)
exchange.check_price()
exchange.start()
class Kraken:
def __init__(self, config, indicator):
self.indicator = indicator
self.timeout_id = 0
def start(self):
self.timeout_id = GLib.timeout_add_seconds(refresh, self.check_price)
def stop(self):
if self.timeout_id:
GLib.source_remove(self.timeout_id)
def check_price(self):
res = requests.get('https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD')
data = res.json()
self._parse_result(data['result'])
def _parse_result(self, data):
# code to parse json result
self.indicator.set_data(label, bid, high, low, ask)
coin = Coin()
Gtk.main()
So this is kind of a python design question + multiple heritance. I'm working on a program of mine and I've ran into an issue I can't figure out a decent way of solving.
To keep it simple. The software scans a log event file generated from another program. Initially it creates and stores each event in a representative event object. But I want to access them quickly and with a more robust language so I'm loading them into a SQL DB after doing a little processing on each event, so theres more data than previous. When I query the DB I'm wanting to recreate an object for each entry representative of the event so its easier to work with.
The problem I'm running into is that I want to avoid a lot of duplicate code and technically I should be able to just reuse some of the code in the original classes for each event. Example:
class AbstractEvent:
__init__(user, time)
getTime()
getUser()
class MessageEvent(AbstractEvent):
__init__(user,time,msg)
getMessage()
class VideoEvent(AbstractEvent):
pass
But, there is extra data after its gone into the DB so there needs to be new subclasses:
class AbstractEventDB(AbstractEvent):
__init__(user, time, time_epoch)
getTimeEpoch()
(static/classmethod) fromRowResult(row)
class MessageEventDB(AbstractEventDB, MessageEvent):
__init__(user, time, msg, time_epoch, tags)
getTags()
(static/classmethod) fromRowResult(row)
class VideoEventDB(AbstractEventDB, VideoEvent):
pass
This is a simpler version than whats happening, but it shows some of what does happen. I change long form time stamps from the log file into epoch timestamps when they go into the DB and various tags are added on message events but other events have nothing extra really beyond the timestamp change.
The above is ideally how I would like to format it, but the problem I've ran into is that the call signatures are completely different on the DB object side compared to the Simple Event side; so when I try to call super() I get an error about expected arguements missing.
I was hoping someone might be able to offer some advice on how to structure it and avoid duplicating code 10-20 times over, particularly in the fromRowResult (a factory method). Help much appreciated.
I thin what you are looking for is a Python implementation for the decorator design pattern.
http://en.wikipedia.org/wiki/Decorator_pattern
The main idea is to replace multiple inheritance with inheritance + composition:
class AbstractEvent(object):
def __init__(self, user, time):
self.user = user
self.time = time
class MessageEvent(AbstractEvent):
def __init__(self, user, time, msg):
super(MessageEvent, self).__init__(user, time)
self.msg = msg
class AbstractEventDBDecorator(object):
def __init__(self, event, time_epoch):
# event is a member of the class. Using dynamic typing, the event member will
# be a AbstractEvent or a MessageEvent at runtime.
self.event = event
self.time_epoch = time_epoch
#classmethod
def fromRowResult(cls, row):
abstract_event = AbstractEvent(row.user, row.time)
abstract_event_db = AbstractEventDBDecorator(abstract_event, row.time_epoch)
return abstract_event_db
class MessageEventDB(AbstractEventDBDecorator):
def __init__(self, message_event, time_epoch, tags):
super(MessageEventDB, self).__init__(message_event, time_epoch)
self.tags = tags
#classmethod
def fromRowResult(cls, row):
message_event = MessageEvent(row.user, row.time, row.msg)
message_event_db = MessageEventDB(message_event, row.time_epoch, row.tags)
return message_event_db
class Row:
def __init__(self, user, time, msg, time_epoch, tags):
self.user = user
self.time = time
self.msg = msg
self.time_epoch = time_epoch
self.tags = tags
if __name__ == "__main__":
me = MessageEvent("user", "time", "msg")
r = Row("user", "time", "Message", "time_epoch", "tags")
med = MessageEventDB.fromRowResult(r)
print med.event.msg
In my PyQt4 application, there is a functionality that allows users to save a avi file.
To this aim, a saveMovie method has been implemented in the main window:
def saveMovie(self):
""" Let the user make a movie out of the current experiment. """
filename = QtGui.QFileDialog.getSaveFileName(self, "Export Movie", "",
'AVI Movie File (*.avi)')
if filename != "":
dialog = QtGui.QProgressDialog('',
QtCore.QString(),
0, 100,
self,
QtCore.Qt.Dialog |
QtCore.Qt.WindowTitleHint)
dialog.setWindowModality(QtCore.Qt.WindowModal)
dialog.setWindowTitle('Exporting Movie')
dialog.setLabelText('Resampling...')
dialog.show()
make_movie(self.appStatus, filename, dialog)
dialog.close()
My idea is to use a QProgressDialog to show how the video encoding work is proceeding.
Nevertheless, after the selection of the filename, the QFileDialog won't disappear and the entire application stays unresponsive until the make_movie function has completed.
What should I do to avoid this?
Lesson learned: if you have some long-running operations to do -- for example, reading or writing a big file, move them to another thread or they will freeze the UI.
Therefore, I created a subclass of QThread, MovieMaker, whose run method encapsulates the functionality previosly implemented by make_movie:
class MovieMaker(QThread):
def __init__(self, uAppStatus, uFilename):
QtCore.QThread.__init__(self, parent=None)
self.appStatus = uAppStatus
self.filename = uFilename
def run(self):
## make the movie and save it on file
Let's move back to the saveMovie method. Here, I replaced the original call to make_movie with the following code:
self.mm = MovieMaker(self.appStatus,
filename)
self.connect(self.mm, QtCore.SIGNAL("Progress(int)"),
self.updateProgressDialog)
self.mm.start()
Note how I defined a new signal, Progress(int).
Such a signal is emitted by the MovieMaker thread to update the QProgressDialog used to show the user how the movie encoding work is progressing.
I have the following code:
class Functions(QObject):
mysig = Signal(filename)
def __init__(self, parent=None):
super(Functions, self).__init__(parent)
self.result = None
def showDialog(self, filename):
self.mysig.emit(filename)
def grabResult(self):
while not self.result:
time.sleep(5)
return result #this is the question
def setResult(self, result):
self.result = result
The other part of the code has this:
class Dialog(QDialog):
anotherSig = Signal(str)
fun = Functions()
def __init__(self, parent=None, filename=filename):
self.filename = filename
#Here it displays a picture based on the filename parameter
def okButtonClicked(self):
text = self.lineedit.text()
fun.setResult(text)
#Tried also this:
self.anotherSig.emit(text)
The Functions() class is called from a worker QThread (not shown here).
I guess my question is this: how do I tell my Functions class that the user has entered the the text and clicked the OK button? I tried connecting that anotherSig Signal, but when I try to do so, Qt complains about QPixmaps not being safe to be set from a different thread, and it doesn't work.
The method that I am using here "works", but I feel it's not very reliable. Plus, it only works when all of the relevant methods in the Functions class are #classmethod - this way, for some reason, it doesn't work. The setResult is called (I added a print statement to make sure), but the grabResult still shows self.result as None.
This code is not working because the call to showDialog is happening on the instantiation of a Functions object that is an attribute of what ever object is off on the other thread. Your fun in Dialog, which you set the result on, is a different instantiation.
To move the results back to the original Functions object I think you need to connect anotherSig of the Dialog object to the setResult function on the Functions object you want to get the results back.
Does something like this work (hard to test this with out a good bit of boiler plate).
class Functions(QObject):
mysig = Signal(filename,Functions)
def __init__(self, parent=None):
super(Functions, self).__init__(parent)
self.result = None
def showDialog(self, filename):
self.mysig.emit(filename,self)
def grabResult(self):
while not self.result:
time.sleep(5)
return result #this is the question
#QtCore.Slot(str)
def setResult(self, result):
self.result = result
def connection_fun(filename,fun):
d = Dialog(filename)
# what ever else you do in here
d.anotherSig.connect(fun.setResult))
Using time.sleep causes your application to freeze. One method for making your class wait is using QEventLoop like this:
loop = QEventLoop()
myDialog.mySignal.connect(loop.quit)
loop.exec_()
I am searching for a simple dialog with a text entry widget asking the user for some input. The dialog should be easy to run (like the gtk.MessageDialog variants) and as flexible.
There are of course some examples but they are either not flexible enough or too complicated to construct for my taste.
I hate re-inventing the wheel... or a dialog.
Based on an example I found (thanks Ardoris!), I came up with a dialog subclass... hope it helps someone out there!
#!/usr/bin/env python
import gtk
class EntryDialog(gtk.MessageDialog):
def __init__(self, *args, **kwargs):
'''
Creates a new EntryDialog. Takes all the arguments of the usual
MessageDialog constructor plus one optional named argument
"default_value" to specify the initial contents of the entry.
'''
if 'default_value' in kwargs:
default_value = kwargs['default_value']
del kwargs['default_value']
else:
default_value = ''
super(EntryDialog, self).__init__(*args, **kwargs)
entry = gtk.Entry()
entry.set_text(str(default_value))
entry.connect("activate",
lambda ent, dlg, resp: dlg.response(resp),
self, gtk.RESPONSE_OK)
self.vbox.pack_end(entry, True, True, 0)
self.vbox.show_all()
self.entry = entry
def set_value(self, text):
self.entry.set_text(text)
def run(self):
result = super(EntryDialog, self).run()
if result == gtk.RESPONSE_OK:
text = self.entry.get_text()
else:
text = None
return text
The run() method returns either the text entered in the entry box if the user presses <Enter> or clicks Ok. If Cancel is clicked or <Esc> pressed, the run() method returns None.
Except for that, the dialog should behave as any other gtk.MessageDialog instance.
Maybe that is not very general as it assumes you will always have Ok as an option, but that is what I need in 99% of my use cases anyway.
There isn't one available in GTK+. You've got two options:
Create a dialog, pack the Entry and any other content you need (probably the best way in my opinion)
Retrieve the content_area of the MessageDialog and append an Entry to it.
Something along the lines of:
#!/usr/bin/env python
import gtk
messagedialog = gtk.MessageDialog(parent=None, flags=0, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_OK, message_format="Hello")
action_area = messagedialog.get_content_area()
entry = gtk.Entry()
action_area.pack_start(entry)
messagedialog.show_all()
messagedialog.run()
messagedialog.destroy()
Though it does probably need more refinement to get the Entry to display nicely.
Here's the function I wrote, based on the previous answers here. It's a function instead of a class, which means you can use it in one line.
def get_text(parent, message, default=''):
"""
Display a dialog with a text entry.
Returns the text, or None if canceled.
"""
d = gtk.MessageDialog(parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_QUESTION,
gtk.BUTTONS_OK_CANCEL,
message)
entry = gtk.Entry()
entry.set_text(default)
entry.show()
d.vbox.pack_end(entry)
entry.connect('activate', lambda _: d.response(gtk.RESPONSE_OK))
d.set_default_response(gtk.RESPONSE_OK)
r = d.run()
text = entry.get_text().decode('utf8')
d.destroy()
if r == gtk.RESPONSE_OK:
return text
else:
return None