I am trying to tag x-spans of a data trace and populate a table with tagNames, starting x value, and the ending x value. I am using a dictionary of 'highlight' objects to keep track of the x-spans in case they need to be edited (increased or decreased) later. The dictionary maps the x Start value to the highlight object, as the x start values are expected to be unique (there is no overlap of x-spans for the tagging).
In order to do this, I am emitting a signal when the user beings to edit a cell on the table. The function that the first signal connects to emits another signal (ideally for whether the xStart is changed vs. the xEnd, but I have only implemented the xStart thus far), which actually changes the appearance of the span to match the edit.
I asked a similar question a few weeks ago but wasn't able to get an answer. The old question is here: PyQT5 slot parameters not updating after first call. In response to the tips given there, I wrote the following example:
import sys
from PyQt5.QtWidgets import *
from PyQt5 import QtCore
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.widgets as mwidgets
from functools import partial
class Window(QMainWindow):
def __init__(self, parent = None):
super(Window, self).__init__(parent)
self.resize(1600, 800)
self.MyUI()
def MyUI(self):
canvas = Canvas(self, width=14, height=12, dpi=100)
canvas.move(0,0)
#use this object in the dictionary to hold onto all the spans.
class highlight:
def __init__(self, tag, xStart, xEnd, highlightObj):
self.tag = tag
self.xStart = xStart
self.xEnd = xEnd
self.highlightObj = highlightObj
class Canvas(FigureCanvas):
def __init__(self, parent, width = 14, height = 12, dpi = 100):
Plot = Figure(figsize=(width, height), dpi=dpi)
self.Axes = Plot.add_subplot(111)
self.Axes.set_position([0.05, 0.58, 0.66, 0.55])
self.rowCount = 0
super().__init__(Plot)
self.setParent(parent)
##add all relevant lines to plot
self.Axes.plot([0,1,2,3,4], [3, 4, 5, 6, 7])
self.Axes.set_xlabel('Frame', fontsize = 10)
self.Axes.grid()
self.Axes.set_aspect(1)
Plot.canvas.draw()
self.highlights = {} #empty dictionary to store all the tags.
##define a table to hold the values postselection
self.taggingTable = QTableWidget(self)
self.taggingTable.setColumnCount(3)
self.taggingTable.setRowCount(100)
self.taggingTable.setGeometry(QRect(1005,85, 330, 310))
self.taggingTable.setHorizontalHeaderLabels(['Behavior','Start Frame', 'End Frame'])
Canvas.span = mwidgets.SpanSelector(self.Axes, self.onHighlight, "horizontal",
interactive = True, useblit=True, props=dict(alpha=0.5, facecolor="blue"),)
self.draw_idle()
self.taggingTable.selectionModel().selectionChanged.connect(self.onCellSelect)
self.draw_idle()
##highlighting adds a highlight item to the directory.
def onHighlight(self, xStart, xEnd):
tagName = "No Tag"
self.taggingTable.setItem(self.rowCount, 0, QTableWidgetItem(tagName))
self.taggingTable.setItem(self.rowCount, 1, QTableWidgetItem(str(int(xStart))))
self.taggingTable.setItem(self.rowCount, 2, QTableWidgetItem(str(int(xEnd))))
self.rowCount = self.rowCount + 1
highlightObj = self.Axes.axvspan(xStart, xEnd, color = 'blue', alpha = 0.5)
self.highlights[int(xStart)] = highlight(tagName, xStart, xEnd, highlightObj)
self.draw_idle()
def xStartChanged(self, xStart, rowVal):
if self.inCounter == 0:
print("xStart in slot: ", xStart)
xEnd = self.highlights[xStart].xEnd
xStartNew = int(self.taggingTable.item(rowVal, 1).text())
self.highlights[xStart].highlightObj.remove() #remove old from the plot
del self.highlights[xStart] #remove old from directory
highlightObj = self.Axes.axvspan(xStartNew, xEnd, color = 'blue', alpha = 0.5) #add new to plot
self.highlights[xStartNew] = highlight("No tagName", xStartNew, xEnd, highlightObj) #add new to directory
self.taggingTable.clearSelection() #deselect value from table
self.draw_idle()
self.inCounter = self.inCounter + 1
def onCellSelect(self):
index = self.taggingTable.selectedIndexes()
if len(index) != 0:
rowVal = index[0].row()
if not (self.taggingTable.item(rowVal, 1) is None):
xStart = int(self.taggingTable.item(rowVal, 1).text())
print("--------------")
print("xStart in signal: ", xStart)
self.inCounter = 0
self.taggingTable.itemChanged.connect(lambda: self.xStartChanged(xStart, rowVal))
app = QApplication(sys.argv)
window = Window()
window.show()
app.exec()
A test I run is when I highlight two traces:
and then I successfully change a first trace:
However, when I attempt to edit the second trace, the program crashes:
To debug, I tried to check what the signals were emitting and receiving. it produces the following output:
--------------
xStart in signal: 0
xStart in slot: 0 ##First slot call gets correct signal
--------------
xStart in signal: 3
xStart in slot: 0 ## Second slot gets the first signal instead of the second
Traceback (most recent call last):
File "//Volumes/path/file.py", line 105, in <lambda>
self.taggingTable.itemChanged.connect(lambda: self.xStartChanged(xStart, rowVal))
File "//Volumes/path/file.py", line 86, in xStartChanged
xEnd = self.highlights[xStart].xEnd
KeyError: 0
zsh: abort python Volumes/path file.py
I tried to use information online on unique connections but I am not sure how to implement them. Thank you in advance for any help.
It seems that what you need is table-widget signal that emits an item and its old text value whenever a change is made to a specific column. Unfortunately, the itemChanged signal isn't really suitable, because it doesn't indicate what changed, and it doesn't supply the previous value. So, to work around this limitation, one solution would be to subclass QTableWidget / QTableWidgetItem and emit a custom signal with the required parameters. This will completely side-step the issue with multiple signal-slot connections.
The implementation of the subclasses is quite simple:
class TableWidgetItem(QTableWidgetItem):
def setData(self, role, value):
oldval = self.text()
super().setData(role, value)
if role == Qt.EditRole and self.text() != oldval:
table = self.tableWidget()
if table is not None:
table.itemTextChanged.emit(self, oldval)
class TableWidget(QTableWidget):
itemTextChanged = pyqtSignal(TableWidgetItem, str)
Below is basic a demo based on your example that shows how to use them. (Note that I have made no attempt to handle xEnd as well, as that would go beyond the scope of the immediate issue).
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.widgets as mwidgets
class Window(QMainWindow):
def __init__(self, parent = None):
super(Window, self).__init__(parent)
self.resize(1600, 800)
self.MyUI()
def MyUI(self):
canvas = Canvas(self, width=14, height=12, dpi=100)
canvas.move(0,0)
# CUSTOM SUBCLASSES
class TableWidgetItem(QTableWidgetItem):
def setData(self, role, value):
oldval = self.text()
super().setData(role, value)
if role == Qt.EditRole and self.text() != oldval:
table = self.tableWidget()
if table is not None:
table.itemTextChanged.emit(self, oldval)
class TableWidget(QTableWidget):
itemTextChanged = pyqtSignal(TableWidgetItem, str)
#use this object in the dictionary to hold onto all the spans.
class highlight:
def __init__(self, tag, xStart, xEnd, highlightObj):
self.tag = tag
self.xStart = xStart
self.xEnd = xEnd
self.highlightObj = highlightObj
class Canvas(FigureCanvas):
def __init__(self, parent, width = 14, height = 12, dpi = 100):
Plot = Figure(figsize=(width, height), dpi=dpi)
self.Axes = Plot.add_subplot(111)
self.Axes.set_position([0.05, 0.58, 0.66, 0.55])
self.rowCount = 0
super().__init__(Plot)
self.setParent(parent)
##add all relevant lines to plot
self.Axes.plot([0,1,2,3,4], [3, 4, 5, 6, 7])
self.Axes.set_xlabel('Frame', fontsize = 10)
self.Axes.grid()
self.Axes.set_aspect(1)
Plot.canvas.draw()
self.highlights = {} #empty dictionary to store all the tags.
##define a table to hold the values postselection
# USE CUSTOM TABLE SUBCLASS
self.taggingTable = TableWidget(self)
self.taggingTable.setColumnCount(3)
self.taggingTable.setRowCount(100)
self.taggingTable.setGeometry(QRect(1005,85, 330, 310))
self.taggingTable.setHorizontalHeaderLabels(['Behavior','Start Frame', 'End Frame'])
# CONNECT TO CUSTOM SIGNAL
self.taggingTable.itemTextChanged.connect(self.xStartChanged)
Canvas.span = mwidgets.SpanSelector(self.Axes, self.onHighlight, "horizontal",
interactive = True, useblit=True, props=dict(alpha=0.5, facecolor="blue"),)
self.draw_idle()
##highlighting adds a highlight item to the directory.
def onHighlight(self, xStart, xEnd):
tagName = "No Tag"
self.taggingTable.setItem(self.rowCount, 0, QTableWidgetItem(tagName))
# USE CUSTOM ITEM SUBCLASS
self.taggingTable.setItem(self.rowCount, 1, TableWidgetItem(str(int(xStart))))
self.taggingTable.setItem(self.rowCount, 2, QTableWidgetItem(str(int(xEnd))))
self.rowCount = self.rowCount + 1
highlightObj = self.Axes.axvspan(xStart, xEnd, color = 'blue', alpha = 0.5)
self.highlights[int(xStart)] = highlight(tagName, xStart, xEnd, highlightObj)
self.draw_idle()
def xStartChanged(self, item, oldVal):
try:
# VALIDATE NEW VALUES
xStart = int(oldVal)
xStartNew = int(item.text())
except ValueError:
pass
else:
print("xStart in slot: ", xStart)
xEnd = self.highlights[xStart].xEnd
self.highlights[xStart].highlightObj.remove() #remove old from the plot
del self.highlights[xStart] #remove old from directory
highlightObj = self.Axes.axvspan(xStartNew, xEnd, color = 'blue', alpha = 0.5) #add new to plot
self.highlights[xStartNew] = highlight("No tagName", xStartNew, xEnd, highlightObj) #add new to directory
self.taggingTable.clearSelection() #deselect value from table
self.draw_idle()
app = QApplication(sys.argv)
window = Window()
window.show()
app.exec()