I'm setting up a custom PlotDataItem to recieve mouseDragEvents. I've adjusted this answer to my needs. For now I've just added a simple setData to the event to check if it's working. The custom PlotDataItem is as:
class CustomPlotItem(pg.PlotDataItem):
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def setParentItem(self, parent):
super().setParentItem(parent)
self.parentBox = self.parentItem().parentItem()
def mouseDragEvent(self, ev):
if ev.button() != QtCore.Qt.LeftButton:
ev.ignore()
return
if ev.isStart():
if self.parentBox.curveDragged != None or not self.mouseShape().contains(ev.pos()):
ev.ignore()
return
self.parentBox.curveDragged = self
elif ev.isFinish():
self.parentBox.curveDragged = None
return
elif self.parentBox.curveDragged != self:
ev.ignore()
return
self.setData([40,50,60,200],[20,50,80,500])
ev.accept()
The PlotDataItem is added to a custom ViewBox this implements curveDragged, so I know which curve is being dragged, if any. I've also disabled the ViewBox's mouseDragEvents for debugging purposes.
However when try to drag the line in the ViewBox, nothing happens. Also if I add an exception at the top of the mouseDragEvent nothing happens. This leads me to believe mouseDragEvent is not being called at all.
Im using Python 3.3 (Anaconda Distribution) and the develop version (0.9.9) of pyqtgraph.
I hope someone can help me with this :). Thanks in advance.
PlotDataItem is a wrapper around a PlotCurveItem and a ScatterPlotItem. As such, it does not actually have any graphics or a clickable shape of its own. I would try making a subclass of PlotCurveItem instead. If you really need to use PlotDataItem, then it is possible to modify it such that it inherits its shape from the wrapped curve:
class CustomPlotItem(pg.PlotDataItem):
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
# Need to switch off the "has no contents" flag
self.setFlags(self.flags() & ~self.ItemHasNoContents)
def mouseDragEvent(self, ev):
print("drag")
if ev.button() != QtCore.Qt.LeftButton:
ev.ignore()
return
if ev.isStart():
print("start")
elif ev.isFinish():
print("finish")
def shape(self):
# Inherit shape from the curve item
return self.curve.shape()
def boundingRect(self):
# All graphics items require this method (unless they have no contents)
return self.shape().boundingRect()
def paint(self, p, *args):
# All graphics items require this method (unless they have no contents)
return
def hoverEvent(self, ev):
# This is recommended to ensure that the item plays nicely with
# other draggable items
print("hover")
ev.acceptDrags(QtCore.Qt.LeftButton)
Related
I have 2 QTreeViews both with a QFileSystemModel attached. One is a local directory and one is pointing at a server.
I am looking to change the colors of text on files based on its relation to the opposite location. For example:
If both server and local have the same modified time - set to green
If the server is more up to date - set local to red, server to orange. Etc etc.
I have had multiple working examples of this but I keep running into the same roadblock, sluggy UI response. I am using the data() function in both QFileSystemModels to achieve this, I tried threading the operation outside of data() to return the color back but it just makes all my files turn in to a light show. Files will randomly go green/red/orange based on me moving the mouse around.
I currently have the date modified column visible as a workaround but it doesn't help the user know if they have a file from the server locally already.
Has anyone got anything like this working without having the UI chug? Or even found a method to return the result from a thread and have it work for the correct entry?
I appreciate this may look a little messy but here is where I init the roots (some of these globals are used elsewhere)
def initRoots(self, loc):
'''
This function sets the starting directories for both server and local
Does not handle anything except top level
'''
global localMask
global serverMask
global serverRoot
global userRoot
global GserverModel
global GlocalModel
if loc == "Server":
self.serverRoot = self.cb_serverWorkspace.currentText()
serverRoot = self.serverRoot
self.serverModel = ServerFileSystemModel()
GserverModel = self.serverModel
self.tv_serverFiles.setModel(self.serverModel)
#self.serverModel.setRootPath(QtCore.QDir.rootPath())
self.serverModel.setRootPath("")
self.tv_serverFiles.setRootIndex(self.serverModel.index(self.serverRoot))
self.tv_serverFiles.setUniformRowHeights(True)
self.tv_serverFiles.setExpandsOnDoubleClick(True)
self.tv_serverFiles.hideColumn(1)
self.tv_serverFiles.hideColumn(2)
self.tv_serverFiles.setColumnWidth(0,400)
if loc == "Local":
self.userRoot = self.cb_localWorkspace.currentText()
userRoot = self.userRoot
self.localModel = LocalFileSystemModel()
GlocalModel = self.localModel
self.tv_localFiles.setModel(self.localModel)
#self.localModel.setRootPath(QtCore.QDir.rootPath())
self.localModel.setRootPath("")
self.tv_localFiles.setRootIndex(self.localModel.index(self.userRoot))
self.tv_serverFiles.setUniformRowHeights(True)
self.tv_localFiles.setExpandsOnDoubleClick(True)
self.tv_localFiles.hideColumn(1)
self.tv_localFiles.hideColumn(2)
self.tv_localFiles.setColumnWidth(0,400)
Then my two QFileSystemModels are pretty much this (This is my non-threaded example)
class ServerFileSystemModel(QtWidgets.QFileSystemModel):
def __init__(self, *args, **kwargs):
super(ServerFileSystemModel, self).__init__(*args, **kwargs)
def data(self, index, role=QtCore.Qt.DisplayRole):
try:
global GlocalModel
global GserverModel
serverPath = self.filePath(index)
localMask = userRoot + "/"
serverMask = serverRoot + "/"
newPath = serverPath.replace(serverMask, localMask)
serverIndex = GserverModel.index(serverPath)
localIndex = GlocalModel.index(newPath)
if role == QtCore.Qt.TextColorRole and serverPath.endswith(".fbx") and os.path.exists(newPath) and downloading == False:
if GlocalModel.lastModified(localIndex) == GserverModel.lastModified(serverIndex):
return QtGui.QColor("#58cd1c")
if GlocalModel.lastModified(localIndex) < GserverModel.lastModified(serverIndex):
return QtGui.QColor("#ed7011")
if GlocalModel.lastModified(localIndex) > GserverModel.lastModified(serverIndex):
return QtGui.QColor("#ed1111")
return super(ServerFileSystemModel, self).data(index, role)
except:
return super(ServerFileSystemModel, self).data(index, role)
Anything else just let me know.
EDIT******
I was asked to put minimal replicable code so here we go
MainWindow
class WorkspacerUI(QMainWindow, Ui_MainWindow):
def __init__(self, parent = None):
super(WorkspacerUI, self).__init__(parent)
self.setupUi(self)
self.userRoot = "D:/OCooke_Workspace"
self.serverRoot = "D:/OCooke_Server"
self.initRoots("Local")
self.initRoots("Server")
def initRoots(self, loc):
'''
This function sets the starting directories for both server and local
Does not handle anything except top level
'''
if loc == "Server":
self.serverModel = ServerFileSystemModel()
self.tv_serverFiles.setModel(self.serverModel)
#self.serverModel.setRootPath(QtCore.QDir.rootPath())
self.serverModel.setRootPath("")
self.tv_serverFiles.setRootIndex(self.serverModel.index(self.serverRoot))
self.tv_serverFiles.setUniformRowHeights(True)
self.tv_serverFiles.setExpandsOnDoubleClick(True)
self.tv_serverFiles.hideColumn(1)
self.tv_serverFiles.hideColumn(2)
self.tv_serverFiles.setColumnWidth(0,400)
if loc == "Local":
self.localModel = LocalFileSystemModel()
self.tv_localFiles.setModel(self.localModel)
#self.localModel.setRootPath(QtCore.QDir.rootPath())
self.localModel.setRootPath("")
self.tv_localFiles.setRootIndex(self.localModel.index(self.userRoot))
self.tv_serverFiles.setUniformRowHeights(True)
self.tv_localFiles.setExpandsOnDoubleClick(True)
self.tv_localFiles.hideColumn(1)
self.tv_localFiles.hideColumn(2)
self.tv_localFiles.setColumnWidth(0,400)
QRunnable with Signals
class WorkerSignals(QObject):
finished = Signal()
result = Signal(object)
progress = Signal(int)
class ColourWorker(QRunnable):
'''
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(ColourWorker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress
self.kwargs['indexy']
#Slot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
Finally the 2 QSystemFileModels
class ServerFileSystemModel(QFileSystemModel):
def __init__(self, *args, **kwargs):
super(ServerFileSystemModel, self).__init__(*args, **kwargs)
self.threadpool = QThreadPool()
self.colour = None
def data(self, index, role=Qt.DisplayRole):
if role == Qt.TextColorRole:
worker = ColourWorker(self.execute_this_fn, indexy = index) # Any other args, kwargs are passed to the run function
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
# Execute
self.threadpool.start(worker)
return self.colour
return super(ServerFileSystemModel, self).data(index, role)
#
def execute_this_fn(self, progress_callback, indexy):
serverPath = self.filePath(indexy)
localMask = "D:/OCooke_Workspace/"
serverMask = "D:/OCooke_Server/"
#create an expected path to the local using the server address
# D:/OCooke_Server/2020/file1 turns into D:/OCooke_Workspace/2020/file1
newPath = serverPath.replace(serverMask, localMask)
#Check the stat time between the two locations
if os.stat(newPath).st_mtime == os.stat(serverPath).st_mtime:
return QColor("#58cd1c")
if os.stat(newPath).st_mtime < os.stat(serverPath).st_mtime:
return QColor("#ed7011")
if os.stat(newPath).st_mtime > os.stat(serverPath).st_mtime:
return QColor("#ed1111")
else:
return None
def print_output(self, s):
#return the value of color
self.colour = s
def thread_complete(self):
#ive tried passing the color once the thread completes, no joy
pass
class LocalFileSystemModel(QFileSystemModel):
def __init__(self, *args, **kwargs):
super(LocalFileSystemModel, self).__init__(*args, **kwargs)
def data(self, index, role=Qt.DisplayRole):
return super(LocalFileSystemModel, self).data(index, role)
MAIN
def main():
app = QApplication(sys.argv)
MainWindow = WorkspacerUI()
MainWindow.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
In my application I build a vtkAssembly Object and I can interact with it by using a custom InteractorStyle that expands the vtkInteractorStyleTrackballActor. I add the Interactor to the render window and set my custom style. I can rotate, scale and move the object, everything works as intended.
Because this is the only object in the render window, I'd like to allow the user to control the object from anywhere. More specifically: The user doesn't have to place the mouse exactly over the object in order to rotate, scale, etc.
I already have an Observer that triggers when the user clicks on the render window and not only the object itself, but how can I trigger the corresponding rotation of the object?
This is my custom vtkInteractorStyle class:
class SynchronousInteractorStyle(vtk.vtkInteractorStyleTrackballActor):
button_active = False
def __init__(self, process, parent=None):
self.process = process
self.AddObserver("LeftButtonPressEvent", self.left_button_press_event)
self.AddObserver("LeftButtonReleaseEvent", self.left_button_release_event)
self.AddObserver("MiddleButtonPressEvent", self.middle_button_press_event)
self.AddObserver("MiddleButtonReleaseEvent", self.middle_button_release_event)
self.AddObserver("RightButtonPressEvent", self.right_button_press_event)
self.AddObserver("RightButtonReleaseEvent", self.right_button_release_event)
def left_button_press_event(self, obj, event):
print("I'm triggered!")
self.button_active = True
self.OnLeftButtonDown()
return
def left_button_release_event(self, obj, event):
self.button_active = False
self.OnLeftButtonUp()
return
def middle_button_press_event(self, obj, event):
self.button_active = True
self.OnMiddleButtonDown()
return
def middle_button_release_event(self, obj, event):
self.button_active = False
self.OnMiddleButtonUp()
return
def right_button_press_event(self, obj, event):
self.button_active = True
self.OnRightButtonDown()
return
def right_button_release_event(self, obj, event):
self.button_active = False
self.OnRightButtonUp()
return
Thank you in advance.
UPDATE:
I think I found a possible way to get what I want to achieve.
There is the possibility to override the OnLeftButtonDown(interactor) method, but I can't figure out what functions I have to call in order to start, for example, a rotation.
This is a example of my override attempt, but interactor delivers a vtkInteractorStyleTrackballActor object and calling StartRotation() doesn't do anything.
def OnLeftButtonDown(interactor):
print(interactor)
interactor.StartRotate()
return
I am using PYQT5. I have a QtableView widget which I use for quick data entry similar to a spreadsheet. Some columns are editable while others (labels etc) are not. I have achieved this quite simply by subclassing QItemDelegate.
What I would like to do is when a user tabs from an editable cell, it will skip any non-editable cell and go to the next editable cell. I think I need to examine a keypress event after editing somewhere and determine which column is next. Alternatively, when I land in a non-editable cell, I should move immediately to the next editable cell. My code is:
class Rates_sheet(QDialog, Ui_rates_sheet):
"""Displays rates for the next x days for quick changes"""
#----------------------------------------------------------------------
def __init__(self):
"""Constructor"""
QDialog.__init__(self)
self.ui = Ui_rates_sheet()
self.ui.setupUi(self)
self.ui.num_days.setValue(45)
self.ui.get_avail.clicked.connect(self.make_rate_sheet)
self.ui.publish_avail.clicked.connect(self.publish_rates)
self.populate()
def populate(self):
self.rates_model = QSqlTableModel()
self.rates_model.setTable("rates_sheet")
self.rates_model.select()
self.rates_view = self.ui.rates_grid
self.rates_view.setModel(self.rates_model)
self.rates_view.resizeColumnsToContents()
self.rates_view.setSortingEnabled(True)
self.rates_view.setColumnHidden(0, True)
for x in range(7,12):
self.rates_view.setColumnHidden(x, True)
self.rates_view.horizontalHeader().moveSection(3,10)
self.rates_view.horizontalHeader().moveSection(3,12)
self.rates_view.horizontalHeader().moveSection(13,2)
self.rates_view.setItemDelegate(Tmodel(self))
self.rates_model.setHeaderData(1, Qt.Horizontal, "Date")
self.rates_model.sort(1, Qt.AscendingOrder)
def make_rate_sheet(self):
pass
def publish_rates(self):
pass
class Tmodel(QItemDelegate):
"""Remplement rates_sheet table"""
def __init__(self, parent=None):
QItemDelegate.__init__(self)
def createEditor(self, parent, option, index):
if index.column() == 4:
spinbox = QSpinBox(parent)
spinbox.setRange(1,4)
return spinbox
elif index.column() in [5,6,11,12]:
spinbox = QSpinBox(parent)
spinbox.setRange(49,499)
return spinbox
EDIT:
I have tried to reimplement QSqlTableModel to change the value in flags but end up with a runtime error:
class MySqlTableModel(QSqlTableModel):
def __init__(self):
QSqlTableModel.__init__(self)
def flags(self, index):
if index.isValid():
if index.column() == 4:
flags ^= 1
return flags
The error I get is:
File "f:\Dropbox\pms\main-5.py", line 2658, in <module>
sys.exit(app.exec_())
builtins.TypeError: invalid result from MySqlTableModel.flags(), NoneType cannot be converted to PyQt5.QtCore.ItemFlags in this context
Now I am even more confused.
After sweating over this for days, the answer is that you can successfully reimplement QsqlTableModel. Here is how I arrived at the answer.
First I saw recommendations on the net mostly in the QT forums for C++ that you should reimplement QsqlQueryModel instead of QsqlTableModel. But then you have to create the methods for setdata, data, rowcount, columncount, insertrows, deleterows not to mention flags yourself. This seemed like a lot of work and prone to error. I could not get it to work.
Thinking that all of the above was pointless and a lot of work, I found a code snippet from stackoverflow where someone was trying to use the dataChanged signal from QsqlQueryModel and they were recommended to overide QsqlTableModel. Finally I saw an example of how to do precisely what I was attempting. Therefore I updated my code as follows:
class Rates_sheet(QDialog, Ui_rates_sheet):
"""Displays rates for the next x days for quick changes"""
def __init__(self):
"""Constructor"""
QDialog.__init__(self)
self.ui = Ui_rates_sheet()
self.ui.setupUi(self)
self.ui.num_days.setValue(45)
self.ui.get_avail.clicked.connect(self.make_rate_sheet)
self.ui.publish_avail.clicked.connect(self.publish_rates)
self.populate()
def populate(self):
self.rates_model = MySqlTableModel()
self.rates_model.setTable("rates_sheet")
self.rates_model.select()
self.rates_view = self.ui.rates_grid
self.rates_view.setModel(self.rates_model)
self.rates_view.resizeColumnsToContents()
self.rates_view.setSortingEnabled(True)
self.rates_view.setColumnHidden(0, True)
self.rates_view.setItemDelegate(Tmodel(self))
self.rates_model.setHeaderData(1, Qt.Horizontal, "Date")
self.rates_model.sort(1, Qt.AscendingOrder)
def make_rate_sheet(self):
pass
def publish_rates(self):
pass
class MySqlTableModel(QSqlTableModel):
"""Overides QSqlTableModel to make columns not selectable"""
def __init__(self):
QSqlTableModel.__init__(self)
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
value = value.strip() if type(value) == str else value
return super(MySqlTableModel, self).setData(index, value, role)
def flags(self, index):
flags = super(MySqlTableModel, self).flags(index)
if index.column() in (4, 5, 6, 11):
flags |= Qt.ItemIsEditable
else:
flags &= Qt.ItemIsSelectable
return flags
This was the solution. I am still testing but I haven't found any problems yet.
This is my code:
def configure_event(self, widget):
if self.is_hiding:
self.window.present()
else:
self.window.iconify()
def delete_event(self, widget, data=None):
gtk.main_quit()
return True
def popup_menu(self):
self.menu = gtk.Menu()
self.about = gtk.MenuItem("about")
if self.is_hiding:
self.expand = gtk.MenuItem("show")
else:
self.expand = gtk.MenuItem("hide")
self.quit = gtk.MenuItem("quit")
self.about.connect("activate", self.about_monitor)
self.expand.connect("activate", self.configure_event)
self.quit.connect("activate", self.delete_event)
self.menu.popup(None, None, gtk.status_icon_position_menu, event_button, event_time, self.tray_icon)
self.menu.append(self.about)
self.menu.append(self.expand)
self.menu.append(self.monitor)
self.menu.append(self.quit)
self.menu.show_all()
delete_event works, but configure_event does not. Why?
look at the signatures of both of them:
def configure_event(self, widget):
def delete_event(self, widget, data=None):
delete_event has a third argument data (that has a default of None) but configure_event only has two.
although i don't know what the exception was i bet that the exception was:
TypeError: configure_event() takes exactly 2 arguments (3 given)
if so changing configure_event's signature to:
def configure_event(self, widget, data):
would fix it. note that i think the default value of None is unneeded as gtk will pass something in always.
I'm wanting to use an NSOpenPanel for an application I'm designing. Here's what I have so far:
#objc.IBAction
def ShowOpenPanel_(self, sender):
self.panel = NSOpenPanel.openPanel()
self.panel.setCanChooseFiles_(False)
self.panel.setCanChooseDirectories_(True)
NSLog(u'Starting OpenPanel')
self.panel.beginForDirectory_file_types_modelessDelegate_didEndSelector_contextInfo_(
self.defaults.objectForKey_(u'projpath'),
objc.nil,
objc.nil,
self,
objc.selector(self.OpenPanelDidEnd_returnCode_contextInfo_,
signature='v:#ii'),
objc.nil)
NSLog(u'OpenPanel was started.')
def OpenPanelDidEnd_returnCode_contextInfo_(self, panel, returnCode, context):
NSLog('Panel ended.')
if (returnCode == NSOKButton):
NSLog(u'User selected OK')
path = self.panel.filenames()[0]
self.defaults.setObject_forKey_(path, u'projpath')
del self.panel
The main two lines I'm concerned about are:
objc.selector(self.OpenPanelDidEnd_returnCode_contextInfo_,
signature='v:#ii'),
objc.nil) #this is the argument that gets passed as the void pointer
The third argument is supposed to be a void pointer. Since I don't intend to use that data, I'd rather just leave it empty. I've tried making the signature 'v:#iv' and tried using objc.NULL and python's None, and just about every combination of all these things. What is the best way to handle this?
I think you don't need to use objc.selector at all; try this instead:
#objc.IBAction
def ShowOpenPanel_(self, sender):
self.panel = NSOpenPanel.openPanel()
self.panel.setCanChooseFiles_(False)
self.panel.setCanChooseDirectories_(True)
NSLog(u'Starting OpenPanel')
self.panel.beginForDirectory_file_types_modelessDelegate_didEndSelector_contextInfo_(
self.defaults.objectForKey_(u'projpath'),
objc.nil,
objc.nil,
self,
self.OpenPanelDidEnd_returnCode_contextInfo_,
objc.nil)
NSLog(u'OpenPanel was started.')
I've also found that I need to decorate the end-of-panel function with PyObjCTools.AppHelper.endSheetMethod:
#PyObjCTools.AppHelper.endSheetMethod
def OpenPanelDidEnd_returnCode_contextInfo_(self, panel, returnCode, context):
NSLog('Panel ended.')
if (returnCode == NSOKButton):
NSLog(u'User selected OK')
path = self.panel.filenames()[0]
self.defaults.setObject_forKey_(path, u'projpath')
del self.panel
Here's how I would write what you have:
#objc.IBAction
def showOpenPanel_(self, sender):
panel = NSOpenPanel.openPanel()
panel.setCanChooseFiles_(False)
panel.setCanChooseDirectories_(True)
NSLog(u'Starting openPanel')
panel.beginForDirectory_file_types_modelessDelegate_didEndSelector_contextInfo_(
self.defaults.objectForKey_(u'projpath'), #forDirectory
None, #file
None, #types
self, #modelessDelegate
self.openPanelDidEnd_returnCode_contextInfo_, #didEndSelector
None) #contextInfo
NSLog(u'openPanel started')
#PyObjCTools.AppHelper.endSheetMethod
def openPanelDidEnd_returnCode_contextInfo_(self, panel, returnCode, context):
NSLog(u'Panel ended')
if returnCode != NSOKButton:
return
NSLog(u'User selected OK')
path = panel.filenames()[0]
self.defaults.setObject_forKey_(path, u'projpath')
Explanation of changes: I always use None rather than objc.nil and it hasn't messed me up yet; I don't think your panel needs to be a property of self since you get it in your return function; objc convention is to have the first letter of your function in lower case.
The right way to open the panel is:
#objc.IBAction
def showOpenPanel_(self, sender):
panel = NSOpenPanel.openPanel()
panel.setCanChooseFiles_(False)
panel.setCanChooseDirectories_(True)
NSLog(u'Starting openPanel')
panel.beginForDirectory_file_types_modelessDelegate_didEndSelector_contextInfo_(
self.defaults.objectForKey_(u'projpath'), #forDirectory
None, #file
None, #types
self, #modelessDelegate
'openPanelDidEnd:returnCode:contextInfo:', #didEndSelector
None) #contextInfo
NSLog(u'openPanel started')
Dan's code works as well, but IMHO my variant is slighly clearer: you don't pass the actual method but the name of the method that should be called.