Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Displaying LaTeX in pyQt/pySide QTableWidget

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?

like image 313
jankos Avatar asked Aug 16 '15 12:08

jankos


1 Answers

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:

enter image description here

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.

like image 101
Jean-Sébastien Avatar answered Nov 15 '22 00:11

Jean-Sébastien