python

Bindings QCustomPlot для Python

  • воскресенье, 21 июня 2015 г. в 02:11:42
http://habrahabr.ru/post/260761/

Добрый день, хаброжители!

Введение


В свободное от работы время увлекся написанием приложений на PyQt5. И свой давний проект по ведению домашней бухгалтерии MyWallet решил в конце мая переписать с плюсов на Python, так как в предыдущей версии были допущены ряд архитектурных ошибок, которые на хотелось исправлять. Поэтому собрав PyQt5 из исходников под Fedora 21, где-то за две недели реализовал весь функционал, который был ранее. И теперь встает вопрос в визуализации данных по расходам/доходам помесячно. Так как имел опыт визуализации данных с помощью QCustomPlot , хотел визуализацию сделать с помощью этой либы. Но к огорчению, не нашел биндов.

Сборка


После просмотра исходников PyQt5 было выяснено, что генерация биндов реализована с помощью SIP). SIP принимает на вход что-то вроде урезанного заголовка методов класса (естественно, со своими так называемыми аннотациями), а на выходе генерирует C++ код для создания готового модуля python.

Итак, для сборки модуля QCustomPlot для Python нам понадобится:
  1. Qt 5.x.
  2. SIP наиболее свежей версии.
  3. PyQt 5.x.
  4. Собранная в виде динамически подключаемой библиотеки qcustomplot, собранной под Qt 5.x.
  5. Файл специального вида с описанием интерфейса классов библиотеки.


Покопавшись по github'у в поисках готового файла интерфейса для этой либы, наткнулся на репозиторий qcustomplot-python, владелец которого собрал бинды, правда для PyQt4. Действуя по аналогии, получаем файл интерфейса либы qcustomplot.sip.

В этом же репозитории можно найти и configure.py, который, как известно, необходим для сборки и установки модулей Python. Данный файл пришлось адаптировать к новой версии PyQt.

Ну, а далее стандартно:

$ python3 configure.py build
$ make
$ sudo make install 


Удостоверимся, что у нас все получилось, запуститим IPy:
$ python3
>>> import qcustomplot
>>> dir(qcustomplot)
['QCP', 'QCPAbstractItem', 'QCPAbstractLegendItem', 'QCPAbstractPlottable', 'QCPAxis', 'QCPAxisRect', 'QCPBarData', 'QCPBars', 'QCPBarsGroup', 'QCPColorGradient', 'QCPColorMap', 'QCPColorMapData', 'QCPColorScale', 'QCPColorScaleAxisRectPrivate', 'QCPCurve', 'QCPCurveData', 'QCPData', 'QCPFinancial', 'QCPFinancialData', 'QCPGraph', 'QCPGrid', 'QCPItemAnchor', 'QCPItemBracket', 'QCPItemCurve', 'QCPItemEllipse', 'QCPItemLine', 'QCPItemPixmap', 'QCPItemPosition', 'QCPItemRect', 'QCPItemStraightLine', 'QCPItemText', 'QCPItemTracer', 'QCPLayer', 'QCPLayerable', 'QCPLayout', 'QCPLayoutElement', 'QCPLayoutGrid', 'QCPLayoutInset', 'QCPLegend', 'QCPLineEnding', 'QCPMarginGroup', 'QCPPainter', 'QCPPlotTitle', 'QCPPlottableLegendItem', 'QCPRange', 'QCPScatterStyle', 'QCPStatisticalBox', 'QCustomPlot', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
>>> 


Ну, а чтобы совсем было красиво, привожу код одного из примеров BarsDemo:
Код примера
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QColor, QPen
from qcustomplot import QCustomPlot, QCPBars, QCP

if __name__ == '__main__':

    app = QApplication(sys.argv)

    w = QCustomPlot()
    regen = QCPBars(w.xAxis, w.yAxis)
    nuclear = QCPBars(w.xAxis, w.yAxis)
    fossil = QCPBars(w.xAxis, w.yAxis)

    w.addPlottable(regen)
    w.addPlottable(nuclear)
    w.addPlottable(fossil)

    pen = QPen()
    pen.setWidthF(1.2)
    fossil.setName('Fossil fuels')
    pen.setColor(QColor(255, 131, 0))
    fossil.setPen(pen)
    fossil.setBrush(QColor(255, 131, 0, 50))
    nuclear.setName('Nuclear')
    pen.setColor(QColor(1, 92, 192))
    nuclear.setPen(pen)
    nuclear.setBrush(QColor(1, 92, 191, 50))
    regen.setName('Regenerative')
    pen.setColor(QColor(150, 222, 0))
    regen.setPen(pen)
    regen.setBrush(QColor(150, 222, 0, 70))
    nuclear.moveAbove(fossil)
    regen.moveAbove(nuclear)

    ticks = [1, 2, 3, 4, 5, 6, 7]
    labels = ['USA', 'Japan', 'Germany', 'France', 'UK', 'Italy', 'Canada']
    w.xAxis.setAutoTicks(False)
    w.xAxis.setAutoTickLabels(False)
    w.xAxis.setTickVector(ticks)
    w.xAxis.setTickVectorLabels(labels)
    w.xAxis.setTickLabelRotation(60)
    w.xAxis.setSubTickCount(0)
    w.xAxis.grid().setVisible(True)
    w.xAxis.setRange(0, 8)

    w.yAxis.setRange(0, 12.1)
    w.yAxis.setPadding(5)
    w.yAxis.setLabel('Power Consumption in\nKilowatts per Capita (2007)')
    w.yAxis.grid().setSubGridVisible(True)

    grid_pen = QPen()
    grid_pen.setStyle(Qt.SolidLine)
    grid_pen.setColor(QColor(0, 0, 0, 25))
    w.yAxis.grid().setSubGridPen(grid_pen)

    fossil_data = [0.86 * 10.5, 0.83 * 5.5, 0.84 * 5.5, 0.52 * 5.8, 0.89 * 5.2, 0.90 * 4.2, 0.67 * 11.2]
    nuclear_data = [0.08 * 10.5, 0.12 * 5.5, 0.12 * 5.5, 0.40 * 5.8, 0.09 * 5.2, 0.00 * 4.2, 0.07 * 11.2]
    regen_data = [0.06 * 10.5, 0.05 * 5.5, 0.04 * 5.5, 0.06 * 5.8, 0.02 * 5.2, 0.07 * 4.2, 0.25 * 11.2]
    fossil.setData(ticks, fossil_data)
    nuclear.setData(ticks, nuclear_data)
    regen.setData(ticks, regen_data)

    w.legend.setVisible(True)
    w.axisRect().insetLayout().setInsetAlignment(0, Qt.AlignTop|Qt.AlignHCenter)
    w.legend.setBrush(QColor(255, 255, 255, 200))
    legendPen = QPen()
    legendPen.setColor(QColor(130, 130, 130, 200))
    w.legend.setBorderPen(legendPen)
    w.setInteractions(QCP.iRangeDrag or QCP.iRangeZoom)

    w.show()

    sys.exit(app.exec())


Вот, что получилось:
Результат
image


P.S.


Ссылка на репозиторий с исходниками: QCustomPlot-PyQt5. В репозитории в каталоге RPMS находятся SRPM и RPM для Fedora21 (PyQt5, qcustomplot 1.3.1 и python3-qcustomplot).

Все комментарии и пожелания приветствуются. Надеюсь, этот модуль вам пригодится. Спасибо за внимание!