override itemChanged signal so that I can access old and new data values [duplicate] - pyqt5

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()

Related

Remove default numbers from QHeaderView when using a custom label in the header

I am attempting to customize a QHeaderView in order to create custom labels that span two or more columns. In the code below, I have done this by placing a QLabel over the appropriate sections of the overall header. However, the default header numbering of the individual columns still appears as shown below. How can this be removed? I would prefer to do this without adding a background colour to each label.
import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
class CustomHeader(QHeaderView):
def __init__(self, orientation, labels, parent=None):
QHeaderView.__init__(self, orientation, parent=None)
self.sectionResized.connect(self.handleSectionResized)
self.label_text = labels
self.labels = list()
self.setFixedHeight(40)
def handleSectionResized(self, i):
start = self.visualIndex(i)
stop = self.count()
for i in range(start, stop):
self.labels[i].setGeometry(self.sectionViewportPosition(i), 0, self.sectionSize(i) - 5, self.height())
self.labels[i].show()
def showEvent(self, event):
for i in range(len(self.label_text)):
label = QLabel(self.label_text[i], self)
label.setAlignment(Qt.AlignCenter)
widget = QWidget(self)
vbox = QVBoxLayout()
vbox.setSpacing(0)
vbox.setContentsMargins(0,0,0,0)
vbox.addWidget(label)
widget.setLayout(vbox)
widget.show()
self.labels.append(widget)
self.adjustPositions()
return super().showEvent(event)
def adjustPositions(self):
for index, label in enumerate(self.labels):
if index == 1 or index == 3:
size = self.sectionSize(index)
print(size)
geom = QRect(
self.sectionViewportPosition(index),
0,
200,
self.height(),
)
else:
size = self.sectionSize(index)
print(size)
geom = QRect(
self.sectionViewportPosition(index),
0,
self.sectionSize(index),
self.height(),
)
geom.adjust(2, 0, -2, 0)
label.setGeometry(geom)
class MyTable(QTableWidget):
def __init__(self, nrow, ncol, parent=None):
super().__init__(nrow, ncol, parent=parent)
self.verticalHeader().hide()
self.setHorizontalHeaderLabels([])
labels = ['One', 'Two', '', 'Three'] #, 'Four', 'Five']
self.header = CustomHeader(Qt.Horizontal, labels, self)
self.setHorizontalHeader(self.header)
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
data = [
[3, 20, 40, 25, 45],
[5, 22, 42, 27, 47],
]
for i in range(2):
for j in range(5):
item = QTableWidgetItem(str(data[i-2][j]))
self.setItem(i, j, item)
item.setBackground(Qt.white)
if __name__ == '__main__':
app = QApplication(sys.argv)
table = MyTable(7, 5)
table.show()
sys.exit(app.exec())
You could change them the same way you would with the standard header.
In your QTableWidget just call:
self.setHorizontalHeaderLabels([""] * self.columnCount())
For example:
class MyTable(QTableWidget):
def __init__(self, nrow, ncol, parent=None):
super().__init__(nrow, ncol, parent=parent)
self.verticalHeader().hide()
self.setHorizontalHeaderLabels([])
labels = ['One', 'Two', '', 'Three'] #, 'Four', 'Five']
self.header = CustomHeader(Qt.Horizontal, labels, self)
self.setHorizontalHeaderLabels([""] * self.columnCount()) # added this
self.setHorizontalHeader(self.header)
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)

PyQT5 slot parameters not updating after first call

I have a signal that connects to a slot that I use to change an x-range span on a matplotlib plot. I am trying to update the span when a user changes the starting x value in a table that shows all the spans. However, after the first edit of the span, the proceeding edits all act as though they are editing the same span, rather than the newly selected span. When running the code:
#QtCore.pyqtSlot()
def xStartChanged(xStart, rowVal):
if self.inCounter == 0:
print( "row in start changed: ", rowVal)
print("xStart in start changed: ", xStart)
print(type(xStart))
tagName = self.highlights[xStart].tag
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(tagName, xStartNew, xEnd, highlightObj) #add new to directory
self.taggingTable.clearSelection() #deselect value from table
self.draw_idle()#update plot
self.inCounter = self.inCounter + 1
def onCellSelect():
index = self.taggingTable.selectedIndexes()
if len(index) != 0:
rowVal = index[0].row()
#get the starting adn ending indices so they can be edited.
##deal with empty cell click
if not (self.taggingTable.item(rowVal, 1) is None):
xStart = int(self.taggingTable.item(rowVal, 1).text())
print("row in cell select: ", rowVal)
print("xStart in cell select: ", xStart)
xEnd = int(self.taggingTable.item(rowVal, 2).text())
self.inCounter = 0
#self.taggingTable.itemChanged.connect(lambda state, item = None, xStartVal = xStart, rowValFxn = rowVal: xStartChanged(xStartVal, rowValFxn))
print(type(xStart))
self.taggingTable.itemChanged.connect(lambda item = None, xStart = xStart: xStartChanged(xStart, rowVal))
self.draw_idle()
#self.taggingTable.itemChanged.connect(xEndChanged)
#self.taggingTable.cellDoubleClicked.connect(onCellSelect)
self.taggingTable.selectionModel().selectionChanged.connect(onCellSelect)
self.draw_idle()
I get the following output: (NOTE: I have hidden the "path" because it is work related, sorry.
row in cell select: 0
xStart in cell select: 3517
<class 'int'>
row in start changed: 0
xStart in start changed: 3517
<class 'int'>
row in cell select: 1
xStart in cell select: 2801
<class 'int'>
row in start changed: 0
xStart in start changed: 3517
<class 'int'>
Traceback (most recent call last):
File "path", line 314, in <lambda>
self.taggingTable.itemChanged.connect(lambda item = None, xStart = xStart: xStartChanged(xStart, rowVal))
File "path", line 277, in xStartChanged
tagName = self.highlights[xStart].tag
KeyError: 3517
zsh: abort path
I understand that for some reason, the connection sends the row value and xStart value of 0 and 3517 and after the cell is changed and edited, the first slot function (onCellSelect) is reading the new cell and xStart values, but the nested signal-slot connection (xStartChanged) is still holding onto the older cell and xStart values. I'm assuming that I am calling or initializing it wrong in some way. Is there something i'm not realizing?
EDIT: minimal reproducible 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
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)
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()
#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
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'])
##highlighting adds a highlight item to the directory.
def onHighlight(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()
self.span = mwidgets.SpanSelector(self.Axes, onHighlight, "horizontal",
interactive = True, useblit=True, props=dict(alpha=0.5, facecolor="blue"),)
self.draw_idle()
##TODO: editing spans in table is reflected in plot --- use axvspan to track spans? and spanselector to draw them?
##define workflow for when a cell is selected
def xStartChanged(xStart, rowVal):
if self.inCounter == 0:
print( "row in start changed: ", rowVal)
print("xStart in start changed: ", 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()#update plot
self.inCounter = self.inCounter + 1
def onCellSelect():
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("row in cell select: ", rowVal)
print("xStart in cell select: ", xStart)
self.inCounter = 0
self.taggingTable.itemChanged.connect(lambda: xStartChanged(xStart, rowVal))
self.taggingTable.selectionModel().selectionChanged.connect(onCellSelect)
self.draw_idle()
app = QApplication(sys.argv)
window = Window()
window.show()
app.exec()
I receive the following error:
row in cell select: 0
xStart in cell select: 2
row in start changed: 0
xStart in start changed: 2
row in cell select: 1
xStart in cell select: 1
row in start changed: 0
xStart in start changed: 2
Traceback (most recent call last):
File "path", line 103, in <lambda>
self.taggingTable.itemChanged.connect(lambda: xStartChanged(xStart, rowVal))
File "path", line 84, in xStartChanged
xEnd = self.highlights[xStart].xEnd
KeyError: 2
zsh: abort "path"

Is it possible to recreate a pyqtgraph without data

I have a pyqt5 based application where I open a file and plot 2 different plots based on the data from the file. Now I want to open another similar file, but I want to save the status/view of the 2 plots, so that I could come quickly back to the previous plots/views, without having to plot it again by reading the data. Is it possible at all to save the state/view of the plots to recreate it very quickly?
You can guide from this answer .
Basically, you can use the PlotWidget class from pyqtgraph.
Generate a PlotWidget.
import pyqtgraph as pg
plot_widget = pg.PlotWidget()
Use the plot() method and store the PlotDataItem in a variable. The PlotDataItem will contain the information of that specific plot: x-data, y-data, the color of the line, the line width, ...
plot_item = plot_widget.plot(xData, yData)
With this you can add/remove the item from the plot every time you want with the addItem() and removeItem() methods
plot_widget.removeItem(plot_item)
plot_widget.addItem(plot_item)
EDIT:
To get the view state of the plot, you can use the viewRect() method of the PlotWidget class. It will return a QRectF object which contains the information of the view state like this:
PyQt5.QtCore.QRectF(x_0, y_0, w, h)
Where:
x_0 and y_0 are the coordinates where the view starts.
w and h are the width and height of the view area.
Also, you can restore the view using the setRange() method of the PlotWidget class.
Example:
Here is an example of the implementation of this:
import sys
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
class MyApp(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.central_layout = QtGui.QVBoxLayout()
self.buttons_layout = QtGui.QVBoxLayout()
self.boxes_layout = QtGui.QHBoxLayout()
self.save = QtGui.QPushButton('Save View')
self.set = QtGui.QPushButton('Set View')
self.boxes = [QtGui.QCheckBox(f"Box {i+1}") for i in range(3)]
self.plot_widget = pg.PlotWidget()
self.plot_data = [None for _ in range(3)]
self.state = [False for _ in range(3)]
self.setLayout(self.central_layout)
self.central_layout.addWidget(self.plot_widget)
self.central_layout.addLayout(self.buttons_layout)
self.buttons_layout.addLayout(self.boxes_layout)
self.buttons_layout.addWidget(self.save)
self.buttons_layout.addWidget(self.set)
for i in range(3):
self.boxes_layout.addWidget(self.boxes[i])
self.boxes[i].stateChanged.connect(self.box_changed)
self.create_data()
self.save.clicked.connect(self.save_view)
self.set.clicked.connect(self.set_view)
self.view_state = None
self.save_view()
def create_data(self):
x = np.linspace(0, 3.14, 100)
y = [np.sin(x), np.cos(x), np.sin(x)**2]
for i in range(3):
self.plot_data[i] = pg.PlotDataItem(x, y[i])
def box_changed(self):
for i in range(3):
if self.boxes[i].isChecked() != self.state[i]:
self.state[i] = self.boxes[i].isChecked()
if self.state[i]:
if self.plot_data[i] is not None:
self.plot_widget.addItem(self.plot_data[i])
else:
self.plot_data[i] = self.plot_widget.plot(*self.box_data[i])
else:
self.plot_widget.removeItem(self.plot_data[i])
break
def save_view(self):
self.view_state = self.plot_widget.viewRect()
def set_view(self):
self.plot_widget.setRange(self.view_state)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())

how to show the cursor coordinates in wxPython when using wx.SplitterWindow

I'm trying to do something similar to that shown in this link to draw a cursor and report the data coordinates in the status bar. However, my code is a little bit different since I need to use wx.SplitterWindow to separate buttons and plots. Basically, in the main frame module I create the status bar, but the plot is generated in a separated module. See below my current code:
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar, \
wxc as wxc
import pylab
import wx
from numpy import arange, sin, pi
class data:
def __init__(self):
self.t = []
self.s = []
class Plot_Panel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
# create some sizers
mainSizer = wx.BoxSizer(wx.VERTICAL)
checkSizer = wx.BoxSizer(wx.HORIZONTAL)
# create figrue
self.fig = Figure()
self.canvas = FigCanvas(self, -1, self.fig)
self.axes = self.fig.add_subplot(111)
# create the widgets
self.toggleMarker = wx.CheckBox(self, label="Show Marker")
# layout the widgets
mainSizer.Add(self.canvas, 1, wx.EXPAND)
checkSizer.Add(self.toggleMarker, 0, wx.ALL, 5)
mainSizer.Add(checkSizer)
self.SetSizer(mainSizer)
def draw_plot(self, data):
# Clear the previous figure
self.fig.clear()
# Redraw figure
self.axes = self.fig.add_subplot(111)
# Define data to plot
self.plot_data= self.axes.plot(data.t, data.s, linewidth=3, color='y',)[0]
# Draw Cursor or not
if self.toggleMarker.IsChecked():
# Note that event is a MplEvent
self.canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar)
self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.ChangeCursor)
self.canvas.draw()
def ChangeCursor(self, event):
self.canvas.SetCursor(wxc.StockCursor(wx.CURSOR_BULLSEYE))
def UpdateStatusBar(self, event):
if event.inaxes:
x, y = event.xdata, event.ydata
# self.statusBar.SetStatusText(("x= "+str(Pos.x)+" y="+str(Pos.y)))
class Button_Panel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
# create the widgets
self.toggleStart = wx.Button(self, id=wx.ID_ANY, label="Plot data")
class ProportionalSplitter(wx.SplitterWindow):
def __init__(self,parent, id = -1, proportion=0.66, size = wx.DefaultSize, **kwargs):
wx.SplitterWindow.__init__(self,parent,id,wx.Point(0, 0),size, **kwargs)
self.SetMinimumPaneSize(50) #the minimum size of a pane.
self.proportion = proportion
if not 0 < self.proportion < 1:
raise ValueError, "proportion value for ProportionalSplitter must be between 0 and 1."
self.ResetSash()
self.Bind(wx.EVT_SIZE, self.OnReSize)
self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.OnSashChanged, id=id)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.firstpaint = True
def SplitHorizontally(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitHorizontally(self, win1, win2,
int(round(self.GetParent().GetSize().GetHeight() * self.proportion)))
def SplitVertically(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitVertically(self, win1, win2,
int(round(self.GetParent().GetSize().GetWidth() * self.proportion)))
def GetExpectedSashPosition(self):
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
return int(round(tot * self.proportion))
def ResetSash(self):
self.SetSashPosition(self.GetExpectedSashPosition())
def OnReSize(self, event):
"Window has been resized, so we need to adjust the sash based on self.proportion."
self.ResetSash()
event.Skip()
def OnSashChanged(self, event):
"We'll change self.proportion now based on where user dragged the sash."
pos = float(self.GetSashPosition())
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
self.proportion = pos / tot
event.Skip()
def OnPaint(self, event):
if self.firstpaint:
if self.GetSashPosition() != self.GetExpectedSashPosition():
self.ResetSash()
self.firstpaint = False
event.Skip()
class Main_Window(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title = title)
# Create a StatusBar at the bottom of the window
self.statusBar = wx.StatusBar(self, -1)
self.SetStatusBar(self.statusBar)
# Set plot panel
self.splitter = ProportionalSplitter(self,-1, 0.85)
self.ppanel = Plot_Panel(self.splitter)
self.ppanel.SetBackgroundColour('#ffffff')
# Set button panel
self.bpanel = Button_Panel(self.splitter)
# Set frame
self.splitter.SplitVertically(self.ppanel, self.bpanel)
self.Show(True)
self.Maximize(True)
# bind the widgets
self.ppanel.toggleMarker.Bind(wx.EVT_CHECKBOX, self.onToggleMarker)
self.bpanel.toggleStart.Bind(wx.EVT_BUTTON, self.onToggleStart)
# Set classes
self.data = data()
def onToggleMarker(self, event):
self.ppanel.draw_plot(self.data)
def onToggleStart(self, event):
self.data.t = arange(0.0, 1.0, 0.01)
self.data.s = sin(2*2*pi*self.data.t)
# plot data
self.ppanel.draw_plot(self.data)
def main():
app = wx.App(False)
frame = Main_Window(None, "GUI")
frame.Show()
app.MainLoop()
if __name__ == "__main__" :
main()
The plot is shown when the button "Plot data" is pressed. What I would like to do is to show the x and y position in the status bar when the checkbox "Show Marker" is checked (in a similar way as it is done in the code posted in the link), and stop when it is unchecked. But I'm not sure if it is possible to do it in my code due to having the definitions of the status bar and the plot in different modules. Any hint will be welcome.
What a joy to get a full, working example program with the question.
You just need to pass the base module as a parameter to self.ppanel
i.e.
self.ppanel = Plot_Panel(self.splitter, self)
then refer to that when updating the status bar, see below for references to base
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar, \
wxc as wxc
import pylab
import wx
from numpy import arange, sin, pi
class data:
def __init__(self):
self.t = []
self.s = []
class Plot_Panel(wx.Panel):
def __init__(self, parent, base):
wx.Panel.__init__(self, parent)
self.base = base
# create some sizers
mainSizer = wx.BoxSizer(wx.VERTICAL)
checkSizer = wx.BoxSizer(wx.HORIZONTAL)
# create figrue
self.fig = Figure()
self.canvas = FigCanvas(self, -1, self.fig)
self.axes = self.fig.add_subplot(111)
# create the widgets
self.toggleMarker = wx.CheckBox(self, label="Show Marker")
# layout the widgets
mainSizer.Add(self.canvas, 1, wx.EXPAND)
checkSizer.Add(self.toggleMarker, 0, wx.ALL, 5)
mainSizer.Add(checkSizer)
self.SetSizer(mainSizer)
def draw_plot(self, data):
# Clear the previous figure
self.fig.clear()
# Redraw figure
self.axes = self.fig.add_subplot(111)
# Define data to plot
self.plot_data= self.axes.plot(data.t, data.s, linewidth=3, color='y',)[0]
# Draw Cursor or not
if self.toggleMarker.IsChecked():
# Note that event is a MplEvent
self.canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar)
self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.ChangeCursor)
self.canvas.draw()
def ChangeCursor(self, event):
self.canvas.SetCursor(wxc.StockCursor(wx.CURSOR_BULLSEYE))
def UpdateStatusBar(self, event):
if event.inaxes:
x, y = event.xdata, event.ydata
self.base.statusBar.SetStatusText(("x= "+str(x)+" y="+str(y)))
class Button_Panel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
# create the widgets
self.toggleStart = wx.Button(self, id=wx.ID_ANY, label="Plot data")
class ProportionalSplitter(wx.SplitterWindow):
def __init__(self,parent, id = -1, proportion=0.66, size = wx.DefaultSize, **kwargs):
wx.SplitterWindow.__init__(self,parent,id,wx.Point(0, 0),size, **kwargs)
self.SetMinimumPaneSize(50) #the minimum size of a pane.
self.proportion = proportion
if not 0 < self.proportion < 1:
raise ValueError, "proportion value for ProportionalSplitter must be between 0 and 1."
self.ResetSash()
self.Bind(wx.EVT_SIZE, self.OnReSize)
self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.OnSashChanged, id=id)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.firstpaint = True
def SplitHorizontally(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitHorizontally(self, win1, win2,
int(round(self.GetParent().GetSize().GetHeight() * self.proportion)))
def SplitVertically(self, win1, win2):
if self.GetParent() is None: return False
return wx.SplitterWindow.SplitVertically(self, win1, win2,
int(round(self.GetParent().GetSize().GetWidth() * self.proportion)))
def GetExpectedSashPosition(self):
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
return int(round(tot * self.proportion))
def ResetSash(self):
self.SetSashPosition(self.GetExpectedSashPosition())
def OnReSize(self, event):
"Window has been resized, so we need to adjust the sash based on self.proportion."
self.ResetSash()
event.Skip()
def OnSashChanged(self, event):
"We'll change self.proportion now based on where user dragged the sash."
pos = float(self.GetSashPosition())
if self.GetSplitMode() == wx.SPLIT_HORIZONTAL:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().height)
else:
tot = max(self.GetMinimumPaneSize(),self.GetParent().GetClientSize().width)
self.proportion = pos / tot
event.Skip()
def OnPaint(self, event):
if self.firstpaint:
if self.GetSashPosition() != self.GetExpectedSashPosition():
self.ResetSash()
self.firstpaint = False
event.Skip()
class Main_Window(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title = title)
# Create a StatusBar at the bottom of the window
self.statusBar = wx.StatusBar(self, -1)
self.SetStatusBar(self.statusBar)
# Set plot panel
self.splitter = ProportionalSplitter(self,-1, 0.85)
self.ppanel = Plot_Panel(self.splitter, self)
self.ppanel.SetBackgroundColour('#ffffff')
# Set button panel
self.bpanel = Button_Panel(self.splitter)
# Set frame
self.splitter.SplitVertically(self.ppanel, self.bpanel)
self.Show(True)
self.Maximize(True)
# bind the widgets
self.ppanel.toggleMarker.Bind(wx.EVT_CHECKBOX, self.onToggleMarker)
self.bpanel.toggleStart.Bind(wx.EVT_BUTTON, self.onToggleStart)
# Set classes
self.data = data()
def onToggleMarker(self, event):
self.ppanel.draw_plot(self.data)
def onToggleStart(self, event):
self.data.t = arange(0.0, 1.0, 0.01)
self.data.s = sin(2*2*pi*self.data.t)
# plot data
self.ppanel.draw_plot(self.data)
def main():
app = wx.App(False)
frame = Main_Window(None, "GUI")
frame.Show()
app.MainLoop()
if __name__ == "__main__" :
main()

how to refresh pyplots in wx.auinotebook

In following code, when the button is clicked, I insert the plots into the tabs of an auinotebook in another frame.
for example, When I have multiple plots in the plt window, I can drag a notebook tab into the bottom (that results in displaying two plots). Later on when I delete the bottom tab, and try to go into other plots, I see a flicker like the closed tab is still there.
I guess the issue is with my on_nb_tab_close. Because, without that I was not able to notice any such problem.
I appreciate help. Code samples will be very useful. (wxpython version 2.812)
import wx
import wx.lib.agw.aui as aui
import matplotlib as mpl
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as mplCanvas
def create_plotter(self):
try:
self.plotter.Show()
except AttributeError:
self.plotter =PlotFrame(self, 500, 500)
self.plotter.Show()
return self.plotter
class PlotFrame(wx.Frame):
def __init__(self, parent, height, width):
wx.Frame.__init__(self, None, size=(height,width), title="plts")
self.parent=parent
self.nb = aui.AuiNotebook(self)
self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.on_nb_tab_close, self.nb)
def AddPlotTab(self,name="plot"):
page = Plot(self.nb)
self.nb.AddPage(page,name)
return page
def on_nb_tab_close(self, evt):
print "tab close fired"
s=self.nb.GetSelection()
v=self.nb.RemovePage(s)
if not self.nb.GetPageCount():
self.on_Close(evt)
evt.Veto()
class Plot(wx.Panel):
def __init__(self, parent, id = -1, dpi = None, **kwargs):
wx.Panel.__init__(self, parent, id=id, **kwargs)
self.figure = mpl.figure.Figure(dpi=dpi)
self.canvas = mplCanvas(self, -1, self.figure) # wxPanel object containing plot
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas,1,wx.EXPAND)
self.SetSizer(sizer)
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, title="Plotting test", size=(300, 300))
self.btn1 = wx.Button(self, -1, "Print 1")
self.Bind(wx.EVT_BUTTON, self.OnBtn1, self.btn1)
def OnBtn1(self, evt):
plotter=create_plotter(self)
page1 = plotter.AddPlotTab("case 1: first_plot")
page1.figure.gca().plot(range(10),range(10),'+')
page1.figure.gca().plot(range(10),range(10),'-',color='red')
page1.figure.canvas.draw()
if __name__ == '__main__':
APP = wx.App(False)
FRAME = MainFrame(None)
FRAME.Show()
APP.MainLoop()