PySide2 如何将分离的图例放置到另一个小部件?

PySide2 How to place a detached legend to another widget?

最近,我发现我的 QChart 图例可以分离并 placed 到另一个独立的小部件中。 Qt 文档说 QLegend class 方法 QLegend::detachFromChart() 可以将图例与图表分开。

不幸的是,当我尝试将图例添加到另一个小部件布局时,出现以下错误:

Traceback (most recent call last):
  File "/home/artem/.local/lib/python3.6/site-packages/shiboken2/files.dir/shibokensupport/signature/loader.py", line 111, in seterror_argument
    return errorhandler.seterror_argument(args, func_name)
  File "/home/artem/.local/lib/python3.6/site-packages/shiboken2/files.dir/shibokensupport/signature/errorhandler.py", line 97, in seterror_argument
    update_mapping()
  File "/home/artem/.local/lib/python3.6/site-packages/shiboken2/files.dir/shibokensupport/signature/mapping.py", line 240, in update
    top = __import__(mod_name)
  File "/home/artem/.local/lib/python3.6/site-packages/numpy/__init__.py", line 142, in <module>
    from . import core
  File "/home/artem/.local/lib/python3.6/site-packages/numpy/core/__init__.py", line 67, in <module>
    raise ImportError(msg.format(path))
ImportError: Something is wrong with the numpy installation. While importing we detected an older version of numpy in ['/home/artem/.local/lib/python3.6/site-packages/numpy']. One method of fixing this is to repeatedly uninstall numpy until none is found, then reinstall this version.
Fatal Python error: seterror_argument did not receive a result

Current thread 0x00007efc67a96740 (most recent call first):
  File "/home/artem/\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b/Projects/QtChartsExamples/test/main.py", line 20 in <module>

这是一个简单的例子:

from PySide2 import QtGui, QtWidgets, QtCore
from PySide2.QtCharts import QtCharts
from psutil import cpu_percent, cpu_count
import sys
import random


class cpu_chart(QtCharts.QChart):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.legend().setAlignment(QtCore.Qt.AlignLeft)
        self.legend().setContentsMargins(0.0, 0.0, 5.0, 0.0)
        self.legend().setMarkerShape(QtCharts.QLegend.MarkerShapeCircle)
        self.legend().detachFromChart()

        self.axisX = QtCharts.QValueAxis()
        self.axisY = QtCharts.QValueAxis()

        self.axisX.setVisible(False)

        self.x = 0
        self.y = 0

        self.percent = cpu_percent(percpu=True)

        for i in range(cpu_count()):
            core_series = QtCharts.QSplineSeries()
            core_series.setName(f"CPU {i+1}: {self.percent[i]: .1f} %")

            colour = [random.randrange(0, 255),
                      random.randrange(0, 255),
                      random.randrange(0, 255)]

            pen = QtGui.QPen(QtGui.QColor(colour[0],
                                          colour[1],
                                          colour[2])
                             )
            pen.setWidth(1)

            core_series.setPen(pen)
            core_series.append(self.x, self.y)

            self.addSeries(core_series)

        self.addAxis(self.axisX, QtCore.Qt.AlignBottom)
        self.addAxis(self.axisY, QtCore.Qt.AlignLeft)

        for i in self.series():
            i.attachAxis(self.axisX)
            i.attachAxis(self.axisY)

        self.axisX.setRange(0, 100)
        self.axisY.setTickCount(5)
        self.axisY.setRange(0, 100)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = QtWidgets.QMainWindow()

    chart = cpu_chart()

    chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)

    chart_view = QtCharts.QChartView(chart)
    chart_view.setRenderHint(QtGui.QPainter.Antialiasing)

    container = QtWidgets.QWidget()
    hbox = QtWidgets.QHBoxLayout()
    hbox.addWidget(chart.legend())
    hbox.addWidget(chart_view)
    container.setLayout(hbox)

    window.setCentralWidget(container)
    window.resize(400, 300)
    window.show()

    sys.exit(app.exec_())

弄清楚这有什么问题会很有趣。我真的可以这样做吗?

我没有收到您指示的消息,但收到以下消息:

Traceback (most recent call last):
  File "main.py", line 70, in <module>
    hbox.addWidget(chart.legend())
TypeError: 'PySide2.QtWidgets.QBoxLayout.addWidget' called with wrong argument types:
  PySide2.QtWidgets.QBoxLayout.addWidget(QLegend)
Supported signatures:
  PySide2.QtWidgets.QBoxLayout.addWidget(PySide2.QtWidgets.QWidget, int = 0, PySide2.QtCore.Qt.Alignment = Default(Qt.Alignment))
  PySide2.QtWidgets.QBoxLayout.addWidget(PySide2.QtWidgets.QWidget)

这更有意义,因为 QLegend 是一个 QGraphicsWidget 而不是 QWidget,所以您不能将它放在布局中。所以一种可能的解决方案是使用 QGraphicsView:

import sys
import random

from PySide2 import QtGui, QtWidgets, QtCore
from PySide2.QtCharts import QtCharts
from psutil import cpu_percent, cpu_count


class cpu_chart(QtCharts.QChart):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.legend().setAlignment(QtCore.Qt.AlignLeft)
        self.legend().setContentsMargins(0.0, 0.0, 5.0, 0.0)
        self.legend().setMarkerShape(QtCharts.QLegend.MarkerShapeCircle)
        self.legend().detachFromChart()

        self.axisX = QtCharts.QValueAxis()
        self.axisY = QtCharts.QValueAxis()

        self.axisX.setVisible(False)

        self.addAxis(self.axisX, QtCore.Qt.AlignBottom)
        self.addAxis(self.axisY, QtCore.Qt.AlignLeft)
        self.axisX.setRange(0, 100)
        self.axisY.setTickCount(5)
        self.axisY.setRange(0, 100)

        self.percent = cpu_percent(percpu=True)

        for i in range(cpu_count()):
            core_series = QtCharts.QSplineSeries()
            core_series.setName(f"CPU {i+1}: {self.percent[i]: .1f} %")

            colour = random.sample(range(255), 3)

            pen = QtGui.QPen(QtGui.QColor(*colour))
            pen.setWidth(1)

            core_series.setPen(pen)
            core_series.attachAxis(self.axisX)
            core_series.attachAxis(self.axisY)
            for i in range(100):
                core_series.append(i, random.uniform(10, 90))

            self.addSeries(core_series)


class LegendWidget(QtWidgets.QGraphicsView):
    def __init__(self, legend, parent=None):
        super().__init__(parent)
        self.m_legend = legend

        scene = QtWidgets.QGraphicsScene(self)
        self.setScene(scene)
        scene.addItem(self.m_legend)

    def resizeEvent(self, event):
        if isinstance(self.m_legend, QtCharts.QLegend):
            self.m_legend.setMinimumSize(self.size())
        super().resizeEvent(event)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = QtWidgets.QMainWindow()

    chart = cpu_chart()

    chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)

    chart_view = QtCharts.QChartView(chart)
    chart_view.setRenderHint(QtGui.QPainter.Antialiasing)

    container = QtWidgets.QWidget()
    hbox = QtWidgets.QHBoxLayout()

    legend_widget = LegendWidget(chart.legend())

    hbox.addWidget(legend_widget)
    hbox.addWidget(chart_view)
    container.setLayout(hbox)

    window.setCentralWidget(container)
    window.resize(400, 300)
    window.show()

    sys.exit(app.exec_())

如您所见,它也不能很好地工作,所以与其移动 QLegend,不如使用带有 QIcon 的 QListWidget:

import sys
import random
from functools import partial

from PySide2 import QtGui, QtWidgets, QtCore
from PySide2.QtCharts import QtCharts
from psutil import cpu_percent, cpu_count


class cpu_chart(QtCharts.QChart):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.legend().setAlignment(QtCore.Qt.AlignLeft)
        self.legend().setContentsMargins(0.0, 0.0, 5.0, 0.0)
        self.legend().setMarkerShape(QtCharts.QLegend.MarkerShapeCircle)
        self.legend().detachFromChart()
        self.legend().hide()

        self.axisX = QtCharts.QValueAxis()
        self.axisY = QtCharts.QValueAxis()

        self.axisX.setVisible(False)

        self.addAxis(self.axisX, QtCore.Qt.AlignBottom)
        self.addAxis(self.axisY, QtCore.Qt.AlignLeft)
        self.axisX.setRange(0, 100)
        self.axisY.setTickCount(5)
        self.axisY.setRange(0, 100)

        self.x = 0

        # percent = cpu_percent(percpu=True)

        for i in range(cpu_count()):
            core_series = QtCharts.QSplineSeries()
            colour = random.sample(range(255), 3)
            pen = QtGui.QPen(QtGui.QColor(*colour))
            pen.setWidth(1)
            core_series.setPen(pen)
            self.addSeries(core_series)
            core_series.attachAxis(self.axisX)
            core_series.attachAxis(self.axisY)

        timer = QtCore.QTimer(self, timeout=self.onTimeout, interval=100)
        timer.start()

    @QtCore.Slot()
    def onTimeout(self):
        percent = cpu_percent(percpu=True)
        for i, (serie, value) in enumerate(zip(self.series(), percent)):
            serie.append(self.x, value)
            serie.setName(f"CPU {i+1}: {value: .1f} %")

        self.axisX.setRange(max(0, self.x - 100), max(100, self.x))
        self.x += 1


def create_icon(pen):
    pixmap = QtGui.QPixmap(512, 512)
    pixmap.fill(QtCore.Qt.transparent)
    painter = QtGui.QPainter(pixmap)
    painter.setBrush(pen.brush())
    painter.drawEllipse(pixmap.rect().adjusted(50, 50, -50, -50))
    painter.end()
    icon = QtGui.QIcon(pixmap)
    return icon


class LegendWidget(QtWidgets.QListWidget):
    def __init__(self, series, parent=None):
        super().__init__(parent)
        self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
        self.setSeries(series)
        self.horizontalScrollBar().hide()

    def setSeries(self, series):
        self.clear()
        for i, serie in enumerate(series):
            it = QtWidgets.QListWidgetItem()
            it.setIcon(create_icon(serie.pen()))
            self.addItem(it)
            wrapper = partial(self.onNameChanged, serie, i)
            serie.nameChanged.connect(wrapper)
            wrapper()

    def onNameChanged(self, serie, i):
        it = self.item(i)
        it.setText(serie.name())
        self.setFixedWidth(self.sizeHintForColumn(0))


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = QtWidgets.QMainWindow()

    chart = cpu_chart()

    chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)

    chart_view = QtCharts.QChartView(chart)
    chart_view.setRenderHint(QtGui.QPainter.Antialiasing)

    container = QtWidgets.QWidget()
    hbox = QtWidgets.QHBoxLayout()

    legend_widget = LegendWidget(chart.series())

    hbox.addWidget(legend_widget)
    hbox.addWidget(chart_view)
    container.setLayout(hbox)

    window.setCentralWidget(container)
    window.resize(1280, 480)
    window.show()

    sys.exit(app.exec_())