pyqtpragh) Item 작동 방식 이해하기

작성자: [관리자] 하얀설표 2024.12.23 작성됨





(2024.12.23) 수정됨.

pyqtgraph에는 다각형(polygon)을 그리는 방법이 없다.

pyqtgraph를 이용해 주가 차트를 그려주는 finplot 모듈에도 방법이 없었는지 drawLines와 drawRects를 이용해 캔들스틱을 그리고 있다.

https://github.com/highfestiva/finplot/blob/master/finplot/__init__.py#L1282

class CandlestickItem(FinPlotItem):
    ...
    def generate_picture(self, boundingRect):
        left,right = boundingRect.left(), boundingRect.right()
        p = self.painter
        df,origlen = self.datasrc.rows(5, left, right, yscale=self.ax.vb.yscale, resamp=self.resamp)
        f = origlen / len(df) if len(df) else 1
        w = self.candle_width * f
        w2 = w * 0.5
        for shadow,frame,body,df_rows in self.colorfunc(self, self.datasrc, df):
            idxs = df_rows.index
            rows = df_rows.values
            if self.x_offset:
                idxs += self.x_offset
            if self.draw_shadow:
                p.setPen(pg.mkPen(shadow, width=self.shadow_width))
                for x,(t,open,close,high,low) in zip(idxs, rows):
                    if high > low:
                        p.drawLine(QtCore.QPointF(x, low), QtCore.QPointF(x, high))
            if self.draw_body:
                p.setPen(pg.mkPen(frame))
                p.setBrush(pg.mkBrush(body))
                for x,(t,open,close,high,low) in zip(idxs, rows):
                    p.drawRect(QtCore.QRectF(x-w2, open, w, close-open))

 

qtgui에 다각형을 그리는 기능이 있기 때문에, 사용자가 직접 다각형을 그리는 class를 만드는 것은 가능할 것 같다.
우선 BarGraphItem이 어떤 식으로 작동하는지 알아보기 위해 BarGraphItem의 코드에 다음과 같이 print문을 삽입했다.

# pyqtgraph/graphicitems/BarGraphItem.py

class BarGraphItem(GraphicsObject):
    def __init__(self, **opts):
        ...
    def setOpts(self, **opts):
        print('setOpts')
        ...

    def _updatePenWidth(self, pen):
        print('_updatePenWidth')
        ...

    def _updateColors(self, opts):
        print('_updateColors')
        ...
    def _getNormalizedCoords(self):
        print('_getNormalizedCoords')
        ...

    def _prepareData(self):
        print('_prepareData')
        ...
    def _render(self, painter):
        print('_render')
        ...

    def drawPicture(self):
        print('drawPicture')
        ...

    def paint(self, p, *args):
        print('paint')
        ...
            
    def shape(self):
        print('shape')
        ...

    def implements(self, interface=None):
        print('implements')
        ...

    def name(self):
        print('name')
        ...

    def getData(self):
        print('getData')
        ...

    def dataBounds(self, ax, frac=1.0, orthoRange=None):
        print('dataBounds')
        ...

    def pixelPadding(self):
        print('pixelPadding')
        ...

    def boundingRect(self):
        print('boundingRect')
        ...

 

그리고 다음과 같은 코드를 작성하고, 실행해보았다.

import pyqtgraph as pg

x = range(10)
y = range(10)
h = range(1, 11)

widget = pg.plot()
bar = pg.BarGraphItem(x=x, y=y, width=0.5, height=h)
print()
print('create bar')
widget.addItem(bar)
print()
print('add bar')

pg.exec()

 

Item을 만들 때 진행되는 작업과, AddItem 이후에 진행되는 작업이 따로 분리되어있음을 알 수 있었다.
boundingRect와 paint가 반복적으로 작동하는 것을 알 수 있었다.

setOpts
_prepareData
_getNormalizedCoords
_updateColors
_updatePenWidth

add bar

boundingRect
dataBounds
dataBounds
pixelPadding
dataBounds
dataBounds
pixelPadding

boundingRect
dataBounds
dataBounds
pixelPadding

boundingRect
dataBounds
dataBounds
pixelPadding

boundingRect
dataBounds
dataBounds
pixelPadding

boundingRect
dataBounds
dataBounds
pixelPadding

paint

boundingRect
dataBounds
dataBounds
pixelPadding
dataBounds
dataBounds
pixelPadding

boundingRect
dataBounds
dataBounds
pixelPadding

boundingRect
dataBounds
dataBounds
pixelPadding

paint

 

paint와 boundingRect method에서 어떤 값들을 사용하는지 알기 위해 다음과 같이 각 변수들을 print하도록 하고 다시 코드를 작동해보았다.

# pyqtgraph/graphicitems/BarGraphItem.py

class BarGraphItem(GraphicsObject):
    ...
    def paint(self, p, *args):
        print('paint')
        ...
    def boundingRect(self):
        print('boundingRect')
        xmn, xmx = self.dataBounds(ax=0)
        print(f'{(xmn, xmx)=}')
        if xmn is None or xmx is None:
            print('xmn is None or xmx is None')
            return QtCore.QRectF()
        ymn, ymx = self.dataBounds(ax=1)
        print(f'{(ymn, ymx)=}')
        if ymn is None or ymx is None:
            print('ymn is None or ymx is None')
            return QtCore.QRectF()

        px = py = 0
        pxPad = self.pixelPadding()
        print(f'{pxPad=}')
        if pxPad > 0:
            # determine length of pixel in local x, y directions
            px, py = self.pixelVectors()
            print(f'{(px, py)=}')
            px = 0 if px is None else px.length()
            py = 0 if py is None else py.length()
            # return bounds expanded by pixel size
            px *= pxPad
            py *= pxPad
        print(f'{(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=}')
        return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)

 

그 결과 다음과 같은 결과를 얻었고, boundingRect는 그리기 객체가 담긴 상자라는 것을 추정할 수 있었다.
xmn과 xmx는 x축 데이터의 최솟값과 최대값을, ymn과 ymx는 y축 데이터의 최솟값과 최대값을 가져오기만 하고 있었다.
px와 py는 줌 인과 줌 아웃을 할 때마다 값이 변했다.

create bar

add bar

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.001658, -0.000000), Point(0.000000, 0.002183))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25082918739635157, -0.5010917030567685, 9.501658374792703, 14.502183406113538)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016706, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25835290383554493, -0.5173468330676884, 9.516705807671089, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016706, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25835290383554493, -0.5173468330676884, 9.516705807671089, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

paint

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

paint

# 마우스 휠업으로 확대

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.016692, -0.000000), Point(0.000000, 0.034694))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.25834593883133156, -0.5173468330676884, 9.516691877662662, 14.534693666135377)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.012402, -0.000000), Point(0.000000, 0.025778))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.2562011554872625, -0.5128889524879443, 9.512402310974524, 14.52577790497589)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.012402, -0.000000), Point(0.000000, 0.025778))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.2562011554872625, -0.5128889524879443, 9.512402310974524, 14.52577790497589)

paint

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.012402, -0.000000), Point(0.000000, 0.025778))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.2562011554872625, -0.5128889524879443, 9.512402310974524, 14.52577790497589)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.012402, -0.000000), Point(0.000000, 0.025778))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.2562011554872625, -0.5128889524879443, 9.512402310974524, 14.52577790497589)

boundingRect
(xmn, xmx)=(-0.25, 9.25)
(ymn, ymx)=(-0.5, 14.0)
pxPad=0.5
(px, py)=(Point(0.012402, -0.000000), Point(0.000000, 0.025778))
(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)=(-0.2562011554872625, -0.5128889524879443, 9.512402310974524, 14.52577790497589)

paint

 

결국 그림을 그리는 것은 paint method이기 때문에, paint 메소드를 적절하게 수정해주면 내가 원하는 그림을 그릴 수 있게될 것이다.
BarGraphItem의 paint method는 다음과 같다.

인수로 받는 p는 QPainter라는 object라는 것을 알 수 있었다.
최종적으로 큐페인터 오브젝트의 drawRects 메소드를 이용해서 그림을 그린다는 것을 알 수 있다.

# pyqtgraph/graphicitems/BarGraphItem.py

class BarGraphItem(GraphicsObject):
    ...
    def paint(self, p, *args):
        if self._singleColor:
            p.setPen(self._sharedPen)
            p.setBrush(self._sharedBrush)
            drawargs = self._rectarray.drawargs()
            p.drawRects(*drawargs)
        else:
            if self.picture is None:
                self.drawPicture()
            self.picture.play(p)
            

 

폴리곤 아이템 작성

BarGraphItem을 참고하여 다음과 같은 코드로 polygon(다각형)을 그리는 Item class를 작성할 수 있다.
테스트용으로 작성한 거라 코드가 많이 조악하다.

import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui


class PolygonItem(pg.GraphicsObject):
    list_polygon = []

    def __init__(self, segment, pen=None, brush=None, *args, **kwargs):
        super().__init__()

        if not pen: pen = pg.mkPen('r')
        if not brush: brush = pg.mkBrush('y')

        x, y = ([], [])
        for s in segment:
            for i in s: (x.append(i[0]), y.append(i[1]))
        self.xmin, self.xmax = (min(x), max(x))
        self.ymin, self.ymax = (min(y), max(y))

        self.segment = segment
        self.pen, self.brush = (pen, brush)

        return

    def set_polygon(self):
        segment = self.segment
        points = []
        for s in segment:
            points.clear()
            for i in s: points.append(QtCore.QPointF(*i))
            polygon = QtGui.QPolygonF(points)
            self.list_polygon.append(polygon)
        return

    def paint(self, p: QtGui.QPainter, *args):
        if not self.list_polygon: self.set_polygon()
        (p.setBrush(self.brush), p.setPen(self.pen))
        for polygon in self.list_polygon: p.drawPolygon(polygon)
        return

    def boundingRect(self):
        xmin, xmax = (self.xmin, self.xmax)
        ymin, ymax = (self.ymin, self.ymax)

        px = py = 0
        pxPad = 0.4
        px, py = self.pixelVectors()
        # print(f'{(px, py)=}')

        px = 0 if px is None else px.length()
        py = 0 if py is None else py.length()
        px *= pxPad
        py *= pxPad

        r = QtCore.QRectF(xmin-px, ymin-py, (2*px)+xmax-xmin, (2*py)+ymax-ymin)
        return r

segment = [
    (
        (5, 5),
        (2, 2),
        (8, 3),
        (1, 3),
        (6.5, 2),
    ),
    (
        (10, 5),
        (11, 2),
        (16, 8),
        (13, 6),
    ),
]
widget = pg.plot(title='draw polygon item')
pi = PolygonItem(segment)
widget.addItem(pi)
pg.exec()

 





추천 (0)


글 목록

댓글을 달 수 없는 게시물입니다.