1

I have a QTreeWidget has a top-level "branch" and a seconds-level "sub-branch". Each sub-branch has QTreeWidgetItems that has QLabels with QPixmaps and in order to save memory the QPixmaps are only generated when the user expands a sub-branch item. Each child of this QTtreeWidgetItem gets a QLabel with setItemWidget() and the Pixmaps are generated in another thread. When I double-click on the QLabel, I want to print out the formula.

The problem arises when I want to set each QPixmap to each respective QLabel after the QRunnbale is finished generating them. The first time expanding a sub-branch and it looks like this:

Bad Tree

Notice how the heights of each QTreeWidgetItem is correct (There are 6 formulas, hovering over each item and you can see there exists 6 items, here I am hovering over the fifth item), and double-clicking each item prints out the correct formula (v=s/t on first item, v2-v02=2as on last item even though it has no image). But here the QPixmaps/QLabels aren't aligned with the QTreeWidgetItems. Collapsing and expanding the same item again yields:

Correct Tree

and suddenly each QPixmap is aligned, for some reason. (Also my mouse is the same position in both pictures, hovering over the same QTreeItemWidget supported by the fact that double clicking prints out the same formula)

here is the code:

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys

import matplotlib
import matplotlib.pyplot as mpl
from matplotlib.backends.backend_agg import FigureCanvasAgg

from sympy.printing import latex
from sympy.parsing import parse_expr
from sympy import Eq

matplotlib.rcParams["mathtext.fontset"] = "cm"

data = [
    {
        "velocity": ["V", "Velocity of object"]
    },
    {
        "Fysik": {
            "Kinematics": {
                "v = s/t": {},
                "a = v/t": {},
                "v = v0+a*t": {},
                "s = v0*t+(a*t**2)/2": {},
                "s = (v+v0)/2*t": {},
                "v**2-v0**2=2*a*s": {},
            }
        }
    }
]


def mathTex_to_QPixmap(mathTex, fs, fig):
    """
    Create QPixMap from LaTeX
    https://stackoverflow.com/questions/32035251/displaying-latex-in-pyqt-pyside-qtablewidget

    :param mathTex: str
        LaTeX string
    :param fs: int
        Font size
    :param fig: matplotlib.figure.Figure
        Matplotlib Figure
    :return: QPixmap
        QPixMap contaning LaTeX image
    """
    fig.clf()
    fig.patch.set_facecolor("none")
    fig.set_canvas(FigureCanvasAgg(fig))
    renderer = fig.canvas.get_renderer()

    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)

    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)

    buf, size = fig.canvas.print_to_buffer()
    qimage = QImage.rgbSwapped(QImage(buf, size[0], size[1], QImage.Format_ARGB32))
    qpixmap = QPixmap(qimage)

    return qpixmap


class LaTeXSignals(QObject):
    finished = pyqtSignal()
    current = pyqtSignal(int)
    output = pyqtSignal(list)


class LaTeXWorker(QRunnable):
    def __init__(self, formula_list):
        super(LaTeXWorker, self).__init__()

        self.formula_list = formula_list
        self.fig = mpl.figure()

        self.signals = LaTeXSignals()

    @pyqtSlot()
    def run(self) -> None:
        """
        Create QPixMap from formulas provided
        by self.formula_list
        :return: list
            List of QPixMap
        """
        out = []
        for i, formula in enumerate(self.formula_list):
            expr = formula.split("=")

            left = parse_expr(expr[0], evaluate=False)
            right = parse_expr(expr[1], evaluate=False)

            latex_pixmap = mathTex_to_QPixmap(
                f"${latex(Eq(left, right))}$",
                15,
                fig=self.fig,
            )

            out.append(latex_pixmap)
            self.signals.current.emit(i)

        self.signals.output.emit(out)
        self.signals.finished.emit()


class Testing(QMainWindow):
    def __init__(self):
        super().__init__()

        self.info = data[0]
        self.formulas = data[1]

        self.threadpool = QThreadPool()
        self.threadpool.setMaxThreadCount(1)

        self.init_ui()
        self.add_formulas()
        self.init_bindings()

    def init_ui(self):
        """
        Create UI
        """
        self.FormulaTree = QTreeWidget()
        self.setCentralWidget(self.FormulaTree)

    def add_formulas(self):
        """
        Initialize the Tree
        """
        for branch in self.formulas:
            parent = QTreeWidgetItem(self.FormulaTree)
            parent.setText(0, branch)

            for sub_branch in self.formulas[branch]:
                child = QTreeWidgetItem(parent)
                child.setText(0, sub_branch)

                for formula in self.formulas[branch][sub_branch]:
                    formula_child = QTreeWidgetItem(child)
                    formula_label = QLabel()

                    formula_label.setObjectName(f"{formula}")
                    self.FormulaTree.setItemWidget(formula_child, 0, formula_label)

    def init_bindings(self):
        self.FormulaTree.itemDoubleClicked.connect(self.formula_tree_selected)
        self.FormulaTree.itemExpanded.connect(self.expanded_sub)
        self.FormulaTree.itemCollapsed.connect(self.collapsed_sub)

    def expanded_sub(self, item):
        # Collapse everything else and only expand whatever the user clicked on
        root = self.FormulaTree.invisibleRootItem()
        for i in range(root.childCount()):
            branch = root.child(i)
            for j in range(branch.childCount()):
                sub_branch = branch.child(j)
                if sub_branch != item:
                    self.FormulaTree.collapseItem(sub_branch)

        # Start worker
        if item.parent():
            formulas = [
                self.FormulaTree.itemWidget(item.child(i), 0).objectName()
                for i in range(item.childCount())
            ]
            title = item.text(0)
            total = len(formulas)

            worker = LaTeXWorker(formulas)
            worker.signals.current.connect(lambda current: self.update_current(current, total, title, item))
            worker.signals.output.connect(lambda output: self.set_pixmap(output, item))
            worker.signals.finished.connect(lambda: item.setText(0, title))

            self.threadpool.start(worker)

    def update_current(self, curr, total, title, item):
        """
        Updates sub-branch name
        """
        item.setText(0, f"{title} - Generating LaTeX [{curr}/{total}] {int((curr/total)*100)}%")

    def set_pixmap(self, output, item):
        for i in range(item.childCount()):
            pixmap = output[i]

            qlabel = self.FormulaTree.itemWidget(item.child(i), 0)
            item.child(i).setSizeHint(
                0, QSize(QSizePolicy.Expanding, pixmap.height())
            )
            qlabel.setPixmap(pixmap)

        QPixmapCache.clear()

    def collapsed_sub(self, item):
        """
        In order to save memory, LaTeX QPixmaps are generated when shown
        and cleared once the user clicks on another sub-branch.

        :param item: QTreeWidgetItem
            The item the user clicked at
        :return: None
        """
        if item.parent():
            for i in range(item.childCount()):
                qlabel = self.FormulaTree.itemWidget(item.child(i), 0)
                qlabel.clear()
        QPixmapCache.clear()

    def formula_tree_selected(self):
        """
        Prints formula of selected item
        """""
        selected = self.FormulaTree.selectedItems()
        if selected:
            widget = selected[0]
            qlabel = self.FormulaTree.itemWidget(widget, 0)
            print(qlabel.objectName())

app = QApplication(sys.argv)
_app = Testing()
_app.setFixedSize(QSize(500, 500))
_app.show()
sys.exit(app.exec_())

Since it works on the second try, I tried to set the size hint first then set Pixmap, resulting in the following change:

    def set_pixmap(self, output, item):
        for i in range(item.childCount()):
            pixmap = output[i]

            item.child(i).setSizeHint(
                0, QSize(QSizePolicy.Expanding, pixmap.height())
            )

        for i in range(item.childCount()):
            pixmap = output[i]

            qlabel = self.FormulaTree.itemWidget(item.child(i), 0)
            qlabel.setPixmap(pixmap)

        QPixmapCache.clear()

But the QPixmaps were still getting misaligned.

How can I make the QPixmaps/QLabels align correctly with the QTreeWidgetItems?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Secozzi
  • 252
  • 3
  • 10
  • 2
    I cannot test your code, and I doubt that it's the reason for this behavior, but clearly the constructors you're using for QSize are wrong: the parameters should be width and height, while you're using a QSizePolicy enum (it works just because it's an integer). Did you try to add a call to [`updateGeometries()`](https://doc.qt.io/qt-5/qabstractitemview.html#updateGeometries) after setting the pixmaps? – musicamante Dec 26 '20 at 18:36
  • 1
    What worked? If you are sure that the solution is working fine, then you could add your own answer. – musicamante Dec 26 '20 at 19:52

1 Answers1

2

musicamante's comment answered my question. Here is what I did in order to make it work:

    def set_pixmap(self, qp_list, item):
        """
        Sets pixmaps to respective QLabels

        :param qp_list: list
            List of QPixmaps
        :param item: QTreeWidgetItem
            QTreeWidgetItem being expanded
        """
        for i in range(item.childCount()):
            pixmap = qp_list[i]

            qlabel = self.FormulaTree.itemWidget(item.child(i), 0)
            item.child(i).setSizeHint(
                0, QSize(self.FormulaTree.width(), pixmap.height())
            )
            qlabel.setPixmap(pixmap)

        # Update geometries after setting QPixmaps
        self.FormulaTree.updateGeometries()

        QPixmapCache.clear()
Secozzi
  • 252
  • 3
  • 10