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:
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:
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?