I have some noisy data which I'm trying to fit with a gaussian. The problem is that I have to do it manually. By that, I mean I have to move the point on the curve (see figure below). When I move the point I have to update the curve so the curve it self can move.
For example on this curve if I move the upper point it changes the mu of my gaussian and if I move the point in the middle it update the sigma parameter. On this example, I've plotted the two curve in a FigureCanvas of matplotlib that I've embedded in a QMainWindow.
I've seached and found no way to do that in a matplotlib figure embedded in a PyQt widget. So, I've changed and tried to use PyQtGraph with the ROI tools but it didn't work very well.
Do you have any idea how i can achieve this? Is there a simple python library to do that? Thanks
EDIT :
Here is the code I've used to produce the image :
from PySide2 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import numpy as np
class PainterCanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
self._instructions = []
self.axes = self.figure.add_subplot(111)
def paintEvent(self, event):
super().paintEvent(event)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
width, height = self.get_width_height()
for x, y, rx, ry, br_color in self._instructions:
x_pixel, y_pixel_m = self.axes.transData.transform((x, y))
# In matplotlib, 0,0 is the lower left corner,
# whereas it's usually the upper right
# for most image software, so we'll flip the y-coor
y_pixel = height - y_pixel_m
painter.setBrush(QtGui.QColor(br_color))
painter.drawEllipse( QtCore.QPoint(x_pixel, y_pixel), rx, ry)
def create_oval(self, x, y, radius_x=2, radius_y=2, brush_color="red"):
self._instructions.append([x, y, radius_x, radius_y, brush_color])
self.update()
class MyPaintWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.canvas = PainterCanvas()
self.canvas.mpl_connect("button_press_event", self._on_left_click)
x = np.arange(0, 10, 0.1)
rand = [np.random.uniform(-0.1, 0.2) for _ in x]
y0 = np.exp(- (x - 5) ** 2 / 2) + rand
y1 = np.exp(- (x - 3) ** 2 / 0.5)
self.canvas.axes.plot(x, y0)
self.canvas.axes.plot(x, y1)
layout_canvas = QtWidgets.QVBoxLayout(self)
layout_canvas.addWidget(self.canvas)
self.canvasMenu = QtWidgets.QMenu(self)
self.canvasMenu.addAction("test")
self.canvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.canvas.customContextMenuRequested.connect(self._on_left_click)
def _on_left_click(self, event):
self.canvas.create_oval(event.xdata, event.ydata, brush_color="green")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MyPaintWidget()
w.show()
sys.exit(app.exec_())
To Add point I've added them by clicking on the curve. I know that code won't work to do what I've asked but it was just to produce an image to explain my idea.
As suggested by mkrieger1, I will briefly describe my solution :
First I've used PyQtGraph to plot my data to draw updates more efficiently than when I was using Matplotlib.
To control curves I've used QSliders (modified to support double) to control the parameters of each curve. When I move one slider it emits an event and in the function handling this event, I update my pyqtgraph plotwidget with the function setData.
I get my inspiration from to do my double cursor : Use float for QSlider
and I've used the pyqtgraph documentation : http://www.pyqtgraph.org/documentation/graphicsItems/plotdataitem.html
There is too much code to put it there but the solution is on my GitHub in the spectrofit project: https://github.com/fulmen27/SpectroFit (It's UI to process and study stellar spectrums). The solution is in the spectrofit.tools.Interactive_Fit.py file.
Hope this can help!
Related
I am trying to remove an image from a figure and release the memory. when colorbar is not added for the image, memory can be released successfully, however, if colorbar is added, it fails. In the demo-code bellow:
click push button Add ColorBar will add a color bar for one image in the figure.
click push button remove will remove one image(and the related colorbar) from the figure.
each time i remove the image, the colorbar related is also removed, so i don't know why the memory recycle fails, I guess there must be some extra reference to the image when add a colorbar to it, which fails the memory recycle.
import numpy as np
from PyQt5 import QtWidgets
from memory_profiler import profile
import matplotlib
from matplotlib.figure import Figure
import matplotlib.cm as cm
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.axes._axes import Axes
matplotlib.use("Qt5Agg")
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
self.axe = self.fig.add_subplot(1, 1, 1, label='good')
super().__init__(self.fig)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout()
self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
self.axe = self.canvas.axe
layout.addWidget(self.canvas)
self.pushButton_addColorBar = QtWidgets.QPushButton('Add ColorBar')
layout.addWidget(self.pushButton_addColorBar)
self.pushButton_remove = QtWidgets.QPushButton('remove')
layout.addWidget(self.pushButton_remove)
widget = QtWidgets.QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.pushButton_remove.clicked.connect(self.removeImage)
self.pushButton_addColorBar.clicked.connect(self.createColorBar)
self.pcolormesh_test()
def pcolormesh_test(self):
"""add two images"""
delta = 0.01
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X ** 2 - Y ** 2)
Z2 = np.exp(-(X - 1) ** 2 - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2
im = self.axe.pcolormesh(X, Y, Z, cmap=cm.viridis, shading='auto')
im.set_clim(vmax=np.amax(Z), vmin=np.amin(Z))
Zx = (Z1 + Z2) * 2
imx = self.axe.pcolormesh(X, Y, Zx, cmap=cm.Blues, shading='auto')
imx.set_clim(vmax=np.amax(Zx), vmin=np.amin(Zx))
def createColorBar(self):
""" to create a color bar for an image. """
axe = self.axe
fig = axe.get_figure()
images = self.getImages(axe)
for image in images:
if not image.colorbar: # color bar doesn't exist
inset_axe = axe.inset_axes([1.0, 0, 0.05, 1], transform=axe.transAxes)
fig.colorbar(image, ax=axe, cax=inset_axe)
break # each trigger create one colorbar for one image
self.reDraw()
#profile
def removeImage(self, checked):
"""
Usage:
* each trigger remove one image
"""
images = self.getImages(self.axe)
# print(f'images={images}')
if images:
image = images[-1]
color_bar = image.colorbar
if color_bar:
color_bar.remove()
del color_bar
# remove image
image.remove()
del image
self.reDraw()
def getImages(self, axe: Axes):
"""to obtain the image list in the axe"""
images = []
images.extend(axe.images)
images.extend(axe.collections)
return images
def reDraw(self):
self.canvas.draw_idle()
self.canvas.flush_events()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
I have found the solution, and post an answer to help.
we need to add gc.collect() at the end of removeImage() method. then the memory can be reclaimed when the image is removed.
I would like to show a context menu on the position of mouse click and then create a new line on that position in the graph.
For that I need both the PyQt position and the graph data position. I thought that I could use the matplotlib transformation functions, but somehow when clicking the lower left and upper right corners of the graph I get in the print values [-0.34, 30.73], [3.02, -1.49] instead of ~[-0.3, -0.9], ~[4.3, 42].
Can anyone fix the mistake I make in the code?
P.S. I know I can connect a matplotlib signal and get the correct data positions. But I would then need to transform those positions to PyQt positions in order to place the widget correctly, resulting in the same issue.
Follows a simplified code:
import sys
import matplotlib
matplotlib.use('Qt5Agg')
from PyQt5 import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
self._menuPoint = None
self.canvasMenu = QtWidgets.QMenu(self)
ca = QtWidgets.QAction('Add line', self)
ca.triggered.connect(self.onAddLineClicked)
self.canvasMenu.addAction(ca)
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self._menuPoint = event.pos()
print(self.axes.transData.inverted().transform((self._menuPoint.x(), self._menuPoint.y())))
if event.button() == QtCore.Qt.RightButton:
self.canvasMenu.exec_(self.mapToGlobal(self._menuPoint))
def onAddLineClicked(self):
pass
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
sc = MplCanvas(self)
sc.axes.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])
self.setCentralWidget(sc)
self.show()
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()
Thanks.
The coordinates returned by QMouseEvent.pos() are comprised between [0 – widget width] and [0 - widget height], while the figure coordinates are between [0 – 1]. You therefore need to divide the mouse pos() by the widget width and height. There is also the subtlety that the Qt coordinates are from the upper left corner, while the matplotlib coordinates are from the lower left corner.
Once you have your position in figure coordinates, it is relatively straightforward to convert them in data coordinates. You could also convert them to Axes coordinates to test whether the click was inside the axes or not.
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self._menuPoint = event.pos()
w, h = self.get_width_height()
xfig = event.x()/w
yfig = 1-(event.y()/h) # necessary because Qt coordinates are from upper left, while matplotlib's are from
# lower left
x, y = self.axes.transData.inverted().transform(self.fig.transFigure.transform([xfig, yfig]))
print(event.pos(), x, y)
if event.button() == QtCore.Qt.RightButton:
self.canvasMenu.exec_(self.mapToGlobal(self._menuPoint))
So I'd like to integrate a matplotlib canvas in qt5 with manual blit.
I've found this thread:
Fast Live Plotting in Matplotlib / PyPlot
and the voted answer seems pretty nice however I need it in a qt5 window...
So I have tried to mash the code above together with the matplotlib qt5 tutorial into one script. https://matplotlib.org/gallery/user_interfaces/embedding_in_qt5_sgskip.html
It kinda works, however the animation only works when using the pan/zoom and the background is black :D and if blit is set to false it doesnt even draw...
If somebody could help me that would be amazing :) Its hilariously broken
from __future__ import unicode_literals
import random
import time
import matplotlib
from PyQt5.QtWidgets import QSizePolicy, QApplication, QWidget, QVBoxLayout
from matplotlib import pyplot as plt
import sys
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import numpy as np
class MyMplCanvas(FigureCanvas):
# Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = plt.figure()
FigureCanvas.__init__(self, self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.x = np.linspace(0, 50., num=100)
self.X, self.Y = np.meshgrid(self.x, self.x)
# self.fig = plt.figure()
self.ax1 = self.fig.add_subplot(2, 1, 1)
self.ax2 = self.fig.add_subplot(2, 1, 2)
self.img = self.ax1.imshow(self.X, vmin=-1, vmax=1, interpolation="None", cmap="RdBu")
self.line, = self.ax2.plot([], lw=3)
self.text = self.ax2.text(0.8, 0.5, "")
self.ax2.set_xlim(self.x.min(), self.x.max())
self.ax2.set_ylim([-1.1, 1.1])
self.t_start = time.time()
self.k = 0.
#self.fig.canvas.draw() # note that the first draw comes before setting data
#self.update(blit=False)
anim = FuncAnimation(self.fig, self.update, interval=20)
def update(self, blit=True):
if blit:
# cache the background
self.axbackground = self.fig.canvas.copy_from_bbox(self.ax1.bbox)
self.ax2background = self.fig.canvas.copy_from_bbox(self.ax2.bbox)
self.img.set_data(np.sin(self.X / 3. + self.k) * np.cos(self.Y / 3. + self.k))
self.line.set_data(self.x, np.sin(self.x / 3. + self.k))
self.k += 0.11
if blit:
# restore background
self.fig.canvas.restore_region(self.axbackground)
self.fig.canvas.restore_region(self.ax2background)
# redraw just the points
self.ax1.draw_artist(self.img)
self.ax2.draw_artist(self.line)
self.ax2.draw_artist(self.text)
# fill in the axes rectangle
self.fig.canvas.blit(self.ax1.bbox)
self.fig.canvas.blit(self.ax2.bbox)
# in this post http://bastibe.de/2013-05-30-speeding-up-matplotlib.html
# it is mentionned that blit causes strong memory leakage.
# however, I did not observe that.
else:
# redraw everything
self.fig.canvas.draw()
# self.fig.canvas.flush_events()
# alternatively you could use
# plt.pause(0.000000000001)
# however plt.pause calls canvas.draw(), as can be read here:
# http://bastibe.de/2013-05-30-speeding-up-matplotlib.html
class PlotDialog(QWidget):
def __init__(self):
QWidget.__init__(self)
self.plot_layout = QVBoxLayout(self)
self.plot_canvas = MyMplCanvas(self, width=5, height=4, dpi=100)
self.navi_toolbar = NavigationToolbar(self.plot_canvas, self)
self.plot_layout.addWidget(self.plot_canvas)
self.plot_layout.addWidget(self.navi_toolbar)
if __name__ == "__main__":
app = QApplication(sys.argv)
dialog0 = PlotDialog()
dialog0.show()
sys.exit(app.exec_())
I'm following the script given at the official mayaVI site (Multiple mlab scene models example), and would like to use the sync_camera command to sync the two figures together within a qt GUI (just as shown), such that any rotation/zoom, etc in one figure automatically rotates/zooms, etc the other in the exact same manner, at the same time.
The sync_camera command is written about briefly on another official mayaVI page Figure handling functions, but I haven't been able to find much on its proper use to utilize successfully within the class hierarchy.
Does anyone have any experience with this procedure or advice?
import numpy as np
from traits.api import HasTraits, Instance, Button, \
on_trait_change
from traitsui.api import View, Item, HSplit, Group
from mayavi import mlab
from mayavi.core.ui.api import MlabSceneModel, SceneEditor
class MyDialog(HasTraits):
scene1 = Instance(MlabSceneModel, ())
scene2 = Instance(MlabSceneModel, ())
button1 = Button('Redraw')
button2 = Button('Redraw')
#on_trait_change('button1')
def redraw_scene1(self):
self.redraw_scene(self.scene1)
#on_trait_change('button2')
def redraw_scene2(self):
self.redraw_scene(self.scene2)
def redraw_scene(self, scene):
# Notice how each mlab call points explicitely to the figure it
# applies to.
mlab.clf(figure=scene.mayavi_scene)
x, y, z, s = np.random.random((4, 100))
mlab.points3d(x, y, z, s, figure=scene.mayavi_scene)
# The layout of the dialog created
view = View(HSplit(
Group(
Item('scene1',
editor=SceneEditor(), height=250,
width=300),
'button1',
show_labels=False,
),
Group(
Item('scene2',
editor=SceneEditor(), height=250,
width=300, show_label=False),
'button2',
show_labels=False,
),
),
resizable=True,
)
m = MyDialog()
m.configure_traits()
The solution is to not use the 2-figure-in-1 method (as originally posted), but to create 2 separate figures. For my needs, I've rewritten the initial code such that each figure is in it's own class, and then simply placed them in a new frame side by side. I don't think using the sync_camera function is possible without such a separation, since it requires two separate figures as inputs. The result is basically identical. I successfully implemented the sync_camera function as follows:
import sys, os, time
import numpy as np
os.environ['ETS_TOOLKIT'] = 'qt4'
from pyface.qt import QtGui, QtCore
from traits.api import HasTraits, Instance, on_trait_change, Str, Float, Range
from traitsui.api import View, Item, HSplit, Group
from mayavi import mlab
from mayavi.core.api import PipelineBase, Engine
from mayavi.core.ui.api import MayaviScene, MlabSceneModel, SceneEditor
class Mayavi1(HasTraits):
scene = Instance(MlabSceneModel, ())
#on_trait_change('scene.activated')
def update_plot(self):
Mayavi1.fig1 = mlab.figure(1)
self.scene.mlab.clf(figure=Mayavi1.fig1)
x, y, z, s = np.random.random((4, 100))
splot = self.scene.mlab.points3d(x, y, z, s, figure=Mayavi1.fig1)
#splot.actor.actor.scale = np.array([25,25,25]) #if plot-types different
view = View(Item('scene', editor=SceneEditor(scene_class=MayaviScene),
height=300, width=300, show_label=False),
resizable=True
)
class Mayavi2(HasTraits):
scene = Instance(MlabSceneModel, ())
#on_trait_change('scene.activated')
def update_plot(self):
Mayavi2.fig2 = mlab.figure(2)
self.scene.mlab.clf(figure=Mayavi2.fig2)
x, y, z, s = np.random.random((4, 100))
cplot = self.scene.mlab.points3d(x, y, z, s, figure=Mayavi2.fig2)
#cplot.actor.actor.position = np.array([1,1,1]) #if plot-types different
view = View(Item('scene', editor=SceneEditor(scene_class=MayaviScene),
height=300, width=300, show_label=False),
resizable=True
)
class P1(QtGui.QWidget):
def __init__(self, parent=None):
super(P1, self).__init__(parent)
layout = QtGui.QGridLayout(self)
layout.setContentsMargins(20,20,20,20) #W,N,E,S
layout.setSpacing(10)
self.visualization1 = Mayavi1()
self.ui1 = self.visualization1.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui1, 0, 0, 1, 1)
self.ui1.setParent(self)
self.visualization2 = Mayavi2()
self.ui2 = self.visualization2.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui2, 0, 2, 1, 1)
self.ui2.setParent(self)
mlab.sync_camera(self.visualization1,self.visualization2)
mlab.sync_camera(self.visualization2,self.visualization1)
#self.visualization1.scene.mlab.view(0,0,10,[1,1,1])
class Hierarchy(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Hierarchy, self).__init__(parent)
self.setGeometry(50, 50, 400, 400) #(int x, int y, int w, int h)
self.gotoP1()
def gotoP1(self):
self.P1f = P1(self)
self.setWindowTitle("Page1")
self.setCentralWidget(self.P1f)
self.show()
if __name__ == '__main__':
app = QtGui.QApplication.instance()
#app = QtGui.QApplication(sys.argv)
w = Hierarchy()
sys.exit(app.exec_())
However, in my own version, I'm using two different data sources within each plot (one a scatter plot and the other a contour plot, with the contour plot origin of interest different from the scatter plot), and because of the camera connection, neither one is on screen at the same time as the other (native coordinates distinct in both).
Thus, if you're only seeing one of the 3d objects in frame at a time, adjust the positions within the def update_plot(self) for either figure until they are both viewed on the screen at the same time. This can be done via such commands as:
splot.actor.actor.scale = np.array([25,25,25]) #with splot for fig1
cplot.actor.actor.position = np.array([-64,-64,-64]) #with cplot for fig2
I highly suggest actually going into the mayaVI pipeline (with the red light clicked to see the output code in real-time) to adjust your plots as needed. If anyone needs any further help with this down the road, please let me know.
I would like to add mathematical expressions to the table labels (e.g.: 2^3 should be properly formatted)
Here is a simple example of a table:
http://thomas-cokelaer.info/blog/2012/10/pyqt4-example-of-tablewidget-usage/
setHorizontalHeaderLabels accepts string, only.
I wonder if is it possible to implement somehow this matplotlib approach:
matplotlib - write TeX on Qt form
are there other options?
I've also been trying for some time to display complex labels in the header of a QTableWidget. I was able to do it by reimplementing the paintSection method of a QHeaderView and painting manually the label with a QTextDocument as described in a thread on Qt Centre.
However, this solution was somewhat limited compared to what could be done with LaTex. I thought this could be a good idea to try the approach you suggested in your OP, i.e. using the capability of matplotlib to render LaTex in PySide.
1. Convert matplotlib Figure to QPixmap
First thing that is required in this approach is to be able to convert matplotlib figure in a format that can be easily painted on any QWidget. Below is a function that take a mathTex expression as input and convert it through matplotlib to a QPixmap.
import sys
import matplotlib as mpl
from matplotlib.backends.backend_agg import FigureCanvasAgg
from PySide import QtGui, QtCore
def mathTex_to_QPixmap(mathTex, fs):
#---- set up a mpl figure instance ----
fig = mpl.figure.Figure()
fig.patch.set_facecolor('none')
fig.set_canvas(FigureCanvasAgg(fig))
renderer = fig.canvas.get_renderer()
#---- plot the mathTex expression ----
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
ax.patch.set_facecolor('none')
t = ax.text(0, 0, mathTex, ha='left', va='bottom', fontsize=fs)
#---- fit figure size to text artist ----
fwidth, fheight = fig.get_size_inches()
fig_bbox = fig.get_window_extent(renderer)
text_bbox = t.get_window_extent(renderer)
tight_fwidth = text_bbox.width * fwidth / fig_bbox.width
tight_fheight = text_bbox.height * fheight / fig_bbox.height
fig.set_size_inches(tight_fwidth, tight_fheight)
#---- convert mpl figure to QPixmap ----
buf, size = fig.canvas.print_to_buffer()
qimage = QtGui.QImage.rgbSwapped(QtGui.QImage(buf, size[0], size[1],
QtGui.QImage.Format_ARGB32))
qpixmap = QtGui.QPixmap(qimage)
return qpixmap
2. Paint the QPixmaps to the header of a QTableWidget
The next step is to paint the QPixmap in the header of a QTableWidget. As shown below, I've done it by sub-classing QTableWidget and reimplementing the setHorizontalHeaderLabels method, which is used to convert the mathTex expressions for the labels into QPixmap and to pass it as a list to a subclass of QHeaderView. The QPixmap are then painted within a reimplementation of the paintSection method of QHeaderView and the height of the header is set up to fit the height of the mathTex expression in the reimplementation of the sizeHint methods.
class MyQTableWidget(QtGui.QTableWidget):
def __init__(self, parent=None):
super(MyQTableWidget, self).__init__(parent)
self.setHorizontalHeader(MyHorizHeader(self))
def setHorizontalHeaderLabels(self, headerLabels, fontsize):
qpixmaps = []
indx = 0
for labels in headerLabels:
qpixmaps.append(mathTex_to_QPixmap(labels, fontsize))
self.setColumnWidth(indx, qpixmaps[indx].size().width() + 16)
indx += 1
self.horizontalHeader().qpixmaps = qpixmaps
super(MyQTableWidget, self).setHorizontalHeaderLabels(headerLabels)
class MyHorizHeader(QtGui.QHeaderView):
def __init__(self, parent):
super(MyHorizHeader, self).__init__(QtCore.Qt.Horizontal, parent)
self.setClickable(True)
self.setStretchLastSection(True)
self.qpixmaps = []
def paintSection(self, painter, rect, logicalIndex):
if not rect.isValid():
return
#------------------------------ paint section (without the label) ----
opt = QtGui.QStyleOptionHeader()
self.initStyleOption(opt)
opt.rect = rect
opt.section = logicalIndex
opt.text = ""
#---- mouse over highlight ----
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
if rect.contains(mouse_pos):
opt.state |= QtGui.QStyle.State_MouseOver
#---- paint ----
painter.save()
self.style().drawControl(QtGui.QStyle.CE_Header, opt, painter, self)
painter.restore()
#------------------------------------------- paint mathText label ----
qpixmap = self.qpixmaps[logicalIndex]
#---- centering ----
xpix = (rect.width() - qpixmap.size().width()) / 2. + rect.x()
ypix = (rect.height() - qpixmap.size().height()) / 2.
#---- paint ----
rect = QtCore.QRect(xpix, ypix, qpixmap.size().width(),
qpixmap.size().height())
painter.drawPixmap(rect, qpixmap)
def sizeHint(self):
baseSize = QtGui.QHeaderView.sizeHint(self)
baseHeight = baseSize.height()
if len(self.qpixmaps):
for pixmap in self.qpixmaps:
baseHeight = max(pixmap.height() + 8, baseHeight)
baseSize.setHeight(baseHeight)
self.parentWidget().repaint()
return baseSize
3. Application
Below is an example of a simple application of the above.
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = MyQTableWidget()
w.verticalHeader().hide()
headerLabels = [
'$C_{soil}=(1 - n) C_m + \\theta_w C_w$',
'$k_{soil}=\\frac{\\sum f_j k_j \\theta_j}{\\sum f_j \\theta_j}$',
'$\\lambda_{soil}=k_{soil} / C_{soil}$']
w.setColumnCount(len(headerLabels))
w.setHorizontalHeaderLabels(headerLabels, 25)
w.setRowCount(3)
w.setAlternatingRowColors(True)
k = 1
for j in range(3):
for i in range(3):
w.setItem(i, j, QtGui.QTableWidgetItem('Value %i' % (k)))
k += 1
w.show()
w.resize(700, 200)
sys.exit(app.exec_())
which results in:
The solution is not perfect, but it is a good starting point. I'll update it when I will improve it for my own application.