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

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)

Related

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

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

How to override mpl_toolkits.mplot3d.Axes3D.draw() method?

I'm doing a small project which requires to resolve a bug in matplotlib in order to fix zorders of some ax.patches and ax.collections. More exactly, ax.patches are symbols rotatable in space and ax.collections are sides of ax.voxels (so text must be placed on them). I know so far, that a bug is hidden in draw method of mpl_toolkits.mplot3d.Axes3D: zorder are recalculated each time I move my diagram in an undesired way. So I decided to change definition of draw method in these lines:
for i, col in enumerate(
sorted(self.collections,
key=lambda col: col.do_3d_projection(renderer),
reverse=True)):
#col.zorder = zorder_offset + i #comment this line
col.zorder = col.stable_zorder + i #add this extra line
for i, patch in enumerate(
sorted(self.patches,
key=lambda patch: patch.do_3d_projection(renderer),
reverse=True)):
#patch.zorder = zorder_offset + i #comment this line
patch.zorder = patch.stable_zorder + i #add this extra line
It's assumed that every object of ax.collection and ax.patch has a stable_attribute which is assigned manually in my project. So every time I run my project, I must be sure that mpl_toolkits.mplot3d.Axes3D.draw method is changed manually (outside my project). How to avoid this change and override this method in any way inside my project?
This is MWE of my project:
import matplotlib.pyplot as plt
import numpy as np
#from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d.art3d as art3d
from matplotlib.text import TextPath
from matplotlib.transforms import Affine2D
from matplotlib.patches import PathPatch
class VisualArray:
def __init__(self, arr, fig=None, ax=None):
if len(arr.shape) == 1:
arr = arr[None,None,:]
elif len(arr.shape) == 2:
arr = arr[None,:,:]
elif len(arr.shape) > 3:
raise NotImplementedError('More than 3 dimensions is not supported')
self.arr = arr
if fig is None:
self.fig = plt.figure()
else:
self.fig = fig
if ax is None:
self.ax = self.fig.gca(projection='3d')
else:
self.ax = ax
self.ax.azim, self.ax.elev = -120, 30
self.colors = None
def text3d(self, xyz, s, zdir="z", zorder=1, size=None, angle=0, usetex=False, **kwargs):
d = {'-x': np.array([[-1.0, 0.0, 0], [0.0, 1.0, 0.0], [0, 0.0, -1]]),
'-y': np.array([[0.0, 1.0, 0], [-1.0, 0.0, 0.0], [0, 0.0, 1]]),
'-z': np.array([[1.0, 0.0, 0], [0.0, -1.0, 0.0], [0, 0.0, -1]])}
x, y, z = xyz
if "y" in zdir:
x, y, z = x, z, y
elif "x" in zdir:
x, y, z = y, z, x
elif "z" in zdir:
x, y, z = x, y, z
text_path = TextPath((-0.5, -0.5), s, size=size, usetex=usetex)
aff = Affine2D()
trans = aff.rotate(angle)
# apply additional rotation of text_paths if side is dark
if '-' in zdir:
trans._mtx = np.dot(d[zdir], trans._mtx)
trans = trans.translate(x, y)
p = PathPatch(trans.transform_path(text_path), **kwargs)
self.ax.add_patch(p)
art3d.pathpatch_2d_to_3d(p, z=z, zdir=zdir)
p.stable_zorder = zorder
return p
def on_rotation(self, event):
vrot_idx = [self.ax.elev > 0, True].index(True)
v_zorders = 10000 * np.array([(1, -1), (-1, 1)])[vrot_idx]
for side, zorder in zip((self.side1, self.side4), v_zorders):
for patch in side:
patch.stable_zorder = zorder
hrot_idx = [self.ax.azim < -90, self.ax.azim < 0, self.ax.azim < 90, True].index(True)
h_zorders = 10000 * np.array([(1, 1, -1, -1), (-1, 1, 1, -1),
(-1, -1, 1, 1), (1, -1, -1, 1)])[hrot_idx]
sides = (self.side3, self.side2, self.side6, self.side5)
for side, zorder in zip(sides, h_zorders):
for patch in side:
patch.stable_zorder = zorder
def voxelize(self):
shape = self.arr.shape[::-1]
x, y, z = np.indices(shape)
arr = (x < shape[0]) & (y < shape[1]) & (z < shape[2])
self.ax.voxels(arr, facecolors=self.colors, edgecolor='k')
for col in self.ax.collections:
col.stable_zorder = col.zorder
def labelize(self):
self.fig.canvas.mpl_connect('motion_notify_event', self.on_rotation)
s = self.arr.shape
self.side1, self.side2, self.side3, self.side4, self.side5, self.side6 = [], [], [], [], [], []
# labelling surfaces of side1 and side4
surf = np.indices((s[2], s[1])).T[::-1].reshape(-1, 2) + 0.5
surf_pos1 = np.insert(surf, 2, self.arr.shape[0], axis=1)
surf_pos2 = np.insert(surf, 2, 0, axis=1)
labels1 = (self.arr[0]).flatten()
labels2 = (self.arr[-1]).flatten()
for xyz, label in zip(surf_pos1, [f'${n}$' for n in labels1]):
t = self.text3d(xyz, label, zdir="z", zorder=10000, size=1, usetex=True, ec="none", fc="k")
self.side1.append(t)
for xyz, label in zip(surf_pos2, [f'${n}$' for n in labels2]):
t = self.text3d(xyz, label, zdir="-z", zorder=-10000, size=1, usetex=True, ec="none", fc="k")
self.side4.append(t)
# labelling surfaces of side2 and side5
surf = np.indices((s[2], s[0])).T[::-1].reshape(-1, 2) + 0.5
surf_pos1 = np.insert(surf, 1, 0, axis=1)
surf = np.indices((s[0], s[2])).T[::-1].reshape(-1, 2) + 0.5
surf_pos2 = np.insert(surf, 1, self.arr.shape[1], axis=1)
labels1 = (self.arr[:, -1]).flatten()
labels2 = (self.arr[::-1, 0].T[::-1]).flatten()
for xyz, label in zip(surf_pos1, [f'${n}$' for n in labels1]):
t = self.text3d(xyz, label, zdir="y", zorder=10000, size=1, usetex=True, ec="none", fc="k")
self.side2.append(t)
for xyz, label in zip(surf_pos2, [f'${n}$' for n in labels2]):
t = self.text3d(xyz, label, zdir="-y", zorder=-10000, size=1, usetex=True, ec="none", fc="k")
self.side5.append(t)
# labelling surfaces of side3 and side6
surf = np.indices((s[1], s[0])).T[::-1].reshape(-1, 2) + 0.5
surf_pos1 = np.insert(surf, 0, self.arr.shape[2], axis=1)
surf_pos2 = np.insert(surf, 0, 0, axis=1)
labels1 = (self.arr[:, ::-1, -1]).flatten()
labels2 = (self.arr[:, ::-1, 0]).flatten()
for xyz, label in zip(surf_pos1, [f'${n}$' for n in labels1]):
t = self.text3d(xyz, label, zdir="x", zorder=-10000, size=1, usetex=True, ec="none", fc="k")
self.side6.append(t)
for xyz, label in zip(surf_pos2, [f'${n}$' for n in labels2]):
t = self.text3d(xyz, label, zdir="-x", zorder=10000, size=1, usetex=True, ec="none", fc="k")
self.side3.append(t)
def vizualize(self):
self.voxelize()
self.labelize()
plt.axis('off')
arr = np.arange(60).reshape((2,6,5))
va = VisualArray(arr)
va.vizualize()
plt.show()
This is an output I get after external change of ...\mpl_toolkits\mplot3d\axes3d.py file:
This is an output (an unwanted one) I get if no change is done:
What you want to achieve is called Monkey Patching.
It has its downsides and has to be used with some care (there is plenty of information available under this keyword). But one option could look something like this:
from matplotlib import artist
from mpl_toolkits.mplot3d import Axes3D
# Create a new draw function
#artist.allow_rasterization
def draw(self, renderer):
# Your version
# ...
# Add Axes3D explicitly to super() calls
super(Axes3D, self).draw(renderer)
# Overwrite the old draw function
Axes3D.draw = draw
# The rest of your code
# ...
Caveats here are to import artist for the decorator and the explicit call super(Axes3D, self).method() instead of just using super().method().
Depending on your use case and to stay compatible with the rest of your code you could also save the original draw function and use the custom only temporarily:
def draw_custom():
...
draw_org = Axes3D.draw
Axes3D.draw = draw_custom
# Do custom stuff
Axes3D.draw = draw_org
# Do normal stuff

Interference between the Matplotlib Graphs in Tkinter GUI

I learned how to embed a matploblib graph into a Tkinter GUI via this post https://matplotlib.org/examples/user_interfaces/embedding_in_tk.html. It seems working fine.
However, I had a problem with getting multiple matplotlib graphs to work correctly simultaneously, let me explain a bit more here. The code below generates two buttons, each links to a new window with two new buttons (load data and plot data). You may generate some dummy two column data, say data1.txt, data2.txt, data3.txt, data4.txt, for plotting. What I observed is (1) If I invoke the window separately, I have no problem loading and plotting data1.txt, data2.txt, data3.txt, data4.txt. However, if I open the two windows at the same time, I can only plot freely and correctly in the second window, while the first window plots nothing. It seems as if the first window were suppressed by the existence of the second.
Can anyone help me understand what is happening here. My matplotlib version is 2.0.2. Tkinter version is $Revision:81008$. Python version 2.7.15. Thank you!
from Tkinter import *
import Tkinter as tk
import ttk
import tkFileDialog
import numpy
##loading matplotlib modules
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.gridspec as gridspec
class Overall_Look:
def __init__(self, master):
self.master = master
self.top_frame = ttk.Frame(self.master, padding = (10, 10))
self.top_frame.pack()
##here are the layout for step 1, load structure files
ttk.Button(self.top_frame, text = "Button_1", command = self.plot_one,
style = "TButton").grid(row = 1, column = 0, columnspan = 2, padx = 5, sticky = "sw")
ttk.Button(self.top_frame, text = "Button_2",command = self.plot_two,
style = "TButton").grid(row = 1, column = 2, columnspan = 2, padx = 5, sticky = "sw")
def plot_one(self):
self.plot_one = tk.Toplevel(self.master)
self.GUI = Plot_One(self.plot_one)
def plot_two(self):
self.plot_two = tk.Toplevel(self.master)
self.GUI = Plot_Two(self.plot_two)
class Plot_One():
def __init__(self, master):
self.master = master
self.top_frame = ttk.Frame(self.master, padding = (10, 10))
self.top_frame.pack()
##here are the layout for step 1, load structure files
ttk.Button(self.top_frame, text = "Load Data 1", command = self.load_data_1,
style = "TButton").grid(row = 1, column = 0, columnspan = 2, padx = 5, sticky = "sw")
ttk.Button(self.top_frame, text = "Plot Data 1",command = self.start_plot_one,
style = "TButton").grid(row = 1, column = 2, columnspan = 2, padx = 5)
self.bottom_frame = ttk.Frame(self.master, padding = (10, 10))
self.bottom_frame.pack()
self.fig_1 = plt.figure(figsize=(5, 5), dpi=100) ##create a figure; modify the size here
self.fig_1.add_subplot(111)
self.fig_1.tight_layout()
self.canvas = FigureCanvasTkAgg(self.fig_1, master = self.bottom_frame)
self.canvas.show()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar = NavigationToolbar2TkAgg(self.canvas, self.bottom_frame)
self.toolbar.update()
self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def load_data_1(self):
self.data_1 = tkFileDialog.askopenfilename(defaultextension = ".txt",
filetypes = [("Text Documents", "*.txt")])
def start_plot_one(self):
data = numpy.loadtxt(self.data_1).transpose()
x = data[0]
y = data[1]
self.fig_1.clf()
self.fig_1.add_subplot(111)
plt.plot(x, y, 'b-', lw=2)
self.fig_1.tight_layout()
self.canvas.draw()
class Plot_Two():
def __init__(self, master):
self.master = master
self.top_frame = ttk.Frame(self.master, padding = (10, 10))
self.top_frame.pack()
##here are the layout for step 1, load structure files
ttk.Button(self.top_frame, text = "Load Data 2", command = self.load_data_2,
style = "TButton").grid(row = 1, column = 0, columnspan = 2, padx = 5, sticky = "sw")
ttk.Button(self.top_frame, text = "Plot Data 2",command = self.start_plot_two,
style = "TButton").grid(row = 1, column = 2, columnspan = 2, padx = 5)
self.bottom_frame = ttk.Frame(self.master, padding = (10, 10))
self.bottom_frame.pack()
self.fig_2 = plt.figure(figsize=(5, 5), dpi=100) ##create a figure; modify the size here
self.fig_2.add_subplot(111)
self.fig_2.tight_layout()
self.canvas = FigureCanvasTkAgg(self.fig_2, master = self.bottom_frame)
self.canvas.show()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar = NavigationToolbar2TkAgg(self.canvas, self.bottom_frame)
self.toolbar.update()
self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def load_data_2(self):
self.data_2 = tkFileDialog.askopenfilename(defaultextension = ".txt",
filetypes = [("Text Documents", "*.txt")])
def start_plot_two(self):
data = numpy.loadtxt(self.data_2).transpose()
x = data[0]
y = data[1]
self.fig_2.clf()
self.fig_2.add_subplot(111)
plt.plot(x, y, 'b-', lw=2)
self.fig_2.tight_layout()
self.canvas.draw()
def main():
root = Tk()
GUI = Overall_Look(root)
root.mainloop()
if __name__ == "__main__": main()

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 access from parent method to child object in PyQt5?

I have a button in widget widget which is a central widget of MyApp class, which inherits from QMainWindow. I have connected widget's button to MyApp's method called logic. This is fine, because if I write a method in my main class MyApp(QMainWindow):
def logic(self):
sender = self.sender()
print(sender)
... and click the button I get a message:
PyQt5.QtWidgets.QPushButton object at 0x7ff92e19eaf8
My question is how can I from MyApp's method called logic access to its child object like MyApp.widget.saltLine which is a QLineEdit object? I need to read that line.
class MyApp(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.initui()
def initui(self):
self.setMinimumSize(500, 150)
# create central widget
widget = QWidget()
# lines for entering data
widget.saltLabel = QLabel("Salt:")
widget.hashSunkenLabel = QLabel()
widget.passwordLine = QLineEdit()
widget.resultButton = QPushButton("&Calculate", self)
# set layout
grid = QGridLayout()
grid.addWidget(widget.saltLabel, 0, 0)
grid.addWidget(widget.passwordLine, 1, 0)
grid.addWidget(widget.hashSunkenLabel, 2, 0)
grid.addWidget(widget.resultButton, 2, 1)
# set widget a grid layout and set widget
# as central widget of QMainWindows
widget.setLayout(grid)
self.setCentralWidget(widget)
# don't know how should this look like
widget.resultButton.clicked.connect(self.logic)
def logic(self):
salt = self.saltLine.text()
password = self.passwordLine.text()
resulting_hash = crypt.crypt(password, salt)
self.hashSunkenLabel.setText(resulting_hash)
Or am I definig centralwidget widget wrong? If I do it without a central widget it works fine:
#!/usr/bin/env python3
import sys
import crypt
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QMainWindow, QLabel, QLineEdit, QPushButton,
QWidget, QApplication, QSystemTrayIcon, QFrame, QGridLayout)
class MyApp(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.saltLabel = QLabel("Salt:")
self.saltLine = QLineEdit()
self.saltLine.setPlaceholderText("e.g. $6$xxxxxxxx")
self.passwordLabel = QLabel("Password:")
self.passwordLine = QLineEdit()
self.hashLabel = QLabel("Hash:")
self.hashSunkenLabel = QLabel()
# widget.hashSunkenLabel.setFrameStyle(QFrame.Box | QFrame.Sunken)
self.hashSunkenLabel.setFrameShadow(QFrame.Sunken)
self.resultButton = QPushButton("&Calculate", self)
self.resultButton.setMaximumSize(100, 50)
self.initui()
def initui(self):
# main window size, title and icon
self.setGeometry(300, 300, 500, 150)
self.setWindowTitle("Password hash calculator | Linux")
# set layout
grid = QGridLayout()
grid.addWidget(self.passwordLabel, 0, 0)
grid.addWidget(self.passwordLine, 0, 1)
grid.addWidget(self.saltLabel, 1, 0)
grid.addWidget(self.saltLine, 1, 1)
grid.addWidget(self.resultButton, 2, 1)
grid.addWidget(self.hashLabel, 3, 0)
grid.addWidget(self.hashSunkenLabel, 3, 1)
self.setLayout(grid)
self.resultButton.clicked.connect(self.logic)
def logic(self):
"""
Calculates hash from salt and password
"""
salt = self.saltLine.text()
password = self.passwordLine.text()
resulting_hash = crypt.crypt(password, salt)
self.hashSunkenLabel.setText(resulting_hash)
# sender = self.sender()
# print(sender)
# print(dir(MyApp))
def main():
app = QApplication(sys.argv)
instance = MyApp()
instance.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()