본문 바로가기

❤25기/25기 세미나

[25기 세미나] PyQt5로 그림판 만들기(2)

CView 클래스

1. CView 클래스

이제 실제 그림이 그려지는 역할을 담당하는 CView 클래스를 작성해볼거에요. 아래 그림의 빨간선으로 박스쳐져 있는 부분에 해당합니다.

Qt의 QGraphicsView 클래스를상속받아 구현되므로 아래 코드와 같이 작성하면 됩니다.

class CView(QGraphicsView):

 

2. QGraphicsView의 생성자 함수

아래 코드는 QGraphicsView의 생성자 함수로 QGraphicsScene클래스에 그려진 그래픽 아이템들(직선, 곡선, 사각형, 원 등)을 화면에 표시하는 역할을 담당합니다.

QGraphicsScene는 실제 눈에 보이지 않지만 실제 그래픽 아이템들(QGraphicsItem)을 포함하고, 관리합니다. 쉽게 설명을 하자면,QGraphicsView는화면에 그림을 그리는 역할이고,QGraphicsScene은그려지는 요소들을 관리하는 역할을 한다고 볼 수 있지요. 한편,QGraphicsItem은하나의 그래픽 요소로 선, 곡선 등이라고 생각하면 됩니다. 

여기서 그래픽 씬(GraphicsScene) 변수를 만들고, 그래픽 씬이 가지는 그래픽 아이템(GraphicsItem)들을 저장할 리스트를 선언합니다. 뿐만 아니라 시작점 좌표와 끝점 좌표를 저장할 변수(QPointF 클래스)도 선언합니다.

이 기능들을 코딩하면 아래와 같습니다.

    def __init__(self, parent):
        super().__init__(parent)
        self.scene = QGraphicsScene()
        self.setScene(self.scene)
    
        self.items = []
    
        self.start = QPointF()
        self.end = QPointF()
    
        self.setRenderHint(QPainter.HighQulityAntialiasing)

 

**잠시  __init__ 함수에 대해 알아볼까요?**

__init__은 인스턴스를 만들 때 항상 실행되는 함수입니다. 즉, 객체가 생성될 때 객체를 기본값으로 초기화하는 특수한 메서드로 객체가 생성될 때 자동으로 호출됩니다.

 

**잠시  super()에 대해 알아볼까요?**

super(). 은 자식 클래스에서 부모 클래스의 내용을 사용하고 싶을경우 사용합니다. 그렇다면 이 클래스의 부모 클래스는 무엇일까요? 네~맞습니다! 앞서 CView 클래스 생성에서 말씀드렸다시피 이 함수는 QGraphicsView를 상속받았기 때문에 부모 클래스가 QGraphicsView라고 볼 수 있습니다.

3. QGraphicsScene 크기 조정

이제 QGraphicsScene 크기 조정을 해야 합니다. 크기 조정을 하지 않는다면 그림이나 도형을 그릴 때 스크린이 계속 움직이는 문제점이 발생하기 때문입니다. 우리는 좌, 우스크롤바가 생기지 않도록 QGraphicsScene의 크기 2픽셀 작게 조정할 예정입니다.

코드 작성 방법은 moveEvent 함수를 만들어서 변수 rect에 QRectF를 할당한 뒤 adjust를 통해 크기를 조정해줍니다.

    def moveEvent(self, e):
        rect = QRectF(self.rect())
        rect.adjust(0, 0, -2, -2)

 

 

마우스이벤트 함수

1. 마우스이벤트 함수(1)

이제 본격적으로 QGraphicsScene에 그림이나 도형이 그려질 수 있도록 하는 함수를 만들거에요. 함수 이름은 mousePressEvent로 마우스 클릭시 호출되는 함수이며, 마우스 좌클릭시 시작점, 끝점 좌표를 저장하는 역할을 수행합니다.

    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:
            self.start = e.pos()
            self.end = e.pos()

2. 마우스이벤트 함수(2)

mouseMoveEvent 함수는 마우스 이동 시 호출되는 함수이며, 실제 마우스 이동시 그림이 그려지므로 그리기에 대한 대부분의 처리가 이곳에 집중되어 있습니다.

지우개 체크 버튼이 체크되어 있을때 배경과 같은흰색 펜을 만들어 곡선을 그려 지우개처럼 동작하도록 하는 코드입니다. 즉, 9번째 줄에서 변수 pen에 흰색이 나오도록 QColor를 (255, 255, 255)로 두께는 10으로 설정을 합니다. 그리고 지우개 모드인 경우 흰선을 그리고 바로 리턴해서 아래에 위치한 다른 그리기 코드들이 수행되지 않도록 해야 합니다.

17번째 줄의 코드는 현재 설정된 두께와 색을 가져와 펜을 만드는 부분입니다.

이제 진짜로 도형과 그림을 그려지도록 설정할 거에요. 만약 그리기 설정이 0번이라면, 즉직선을 그리고 싶다면 마우스가 이동할 때시작점 좌표는 변하지 않고,끝점(현재 마우스 좌표)을 기준으로 선을 그리면 됩니다.이때! 우리는 직선을 그려야 하기 때문에 마우스가 이동할 때마다 그려진 선(마우스를 움직일 때 직선으로 움직이지 못하고 울퉁불퉁하게 움직여서 만들어지는 선)을 지우는 작업을 해주어야 합니다.물론 이 작업은 직선이 완전히 그려지기 전에 해야 합니다. 25번째 줄에서 볼 수 있듯이 self.items[-1]을 사용하여 리스트의 마지막 그려진 선을 찾아와 삭제합니다.

만약 그리기 설정이 1번이라면, 즉곡선을 그리고 싶다면 마우스가 이동할 때현재의 시작점,끝점(현재 마우스 좌표)을 기준으로짧은 직선을 그리는 코드입니다. 사실 곡선은 짧은 직선의 연속이기 때문에, 이때는 직선을 그릴 때처럼 이전에 그려진 선을 지울 필요가 없습니다.이때! 짧은 직선이 그려진 후에 마우스를 이동하면 현재의 끝점이 다시 시작점으로 변경되는 코드가 필요합니다.이는 40번째 줄에 구현되어 있습니다.

만약 그리기 설정이 2번이라면, 즉사각형을 그리고 싶다면 마우스가 이동할 때현재의 시작점을 사각형의 왼쪽 윗점으로 정하고, 끝점을 사각형의 오른쪽 아래점으로 설정해 사각형을 그리는 코드입니다. 왜냐하면 직사각형의 경우 왼쪽 윗점의 좌표와 오른쪽 아래점의 좌표 2개만 알아도 그려낼 수 있기 때문입니다.이때! 직선 그리기와 마찬가지로 마우스가 이동할 때마다 기존에 그려진 마지막 사각형을 찾아 삭제하고, 현재의 사각형을 새로 그려야 합니다.

마지막으로원그리기는사각형 그리기와 동일합니다. 하지만 차이가 있다면, QGraphicsScene 클래스가 제공하는 사각형 그리는 함수는 addRect()이고,원은 addEllipse()라는 것입니다. 왜냐하면 Qt에서 원을 그리는 방법은사각형 영역을 설정하고 그사각형에 내접하는 원을 그려내는 방식으로 구현되어 있기 때문입니다.

    def mouseMoveEvent(self, e):  
         
        # e.buttons()는 정수형 값을 리턴, e.button()은 move시 Qt.Nobutton 리턴 
        if e.buttons() & Qt.LeftButton:
            self.end = e.pos()

            # 지우개의 checkbox에 check가 되었을 때
            if self.parent().checkbox.isChecked():
                pen = QPen(QColor(255,255,255), 10)
                path = QPainterPath()
                path.moveTo(self.start)
                path.lineTo(self.end)
                self.scene.addPath(path, pen)
                self.start = e.pos()
                return None

            pen = QPen(self.parent().pencolor, self.parent().combo.currentIndex())

            # 직선 그리기
            if self.parent().drawType == 0:
                 
                # 장면에 그려진 이전 선을 제거            
                if len(self.items) > 0:
                    self.scene.removeItem(self.items[-1])
                    del(self.items[-1])                
                                    
                # 현재 선 추가
                line = QLineF(self.start.x(), self.start.y(), self.end.x(), self.end.y()) 
                self.items.append(self.scene.addLine(line, pen))

            # 곡선 그리기
            if self.parent().drawType == 1:

                # Path 이용
                path = QPainterPath()
                path.moveTo(self.start)
                path.lineTo(self.end)
                self.scene.addPath(path, pen)
                # 시작점을 다시 기존 끝점으로
                self.start = e.pos()

            # 사각형 그리기
            if self.parent().drawType == 2:
                brush = QBrush(self.parent().brushcolor)

                if len(self.items) > 0:
                    self.scene.removeItem(self.items[-1])
                    del(self.items[-1])

                rect = QRectF(self.start, self.end)
                self.items.append(self.scene.addRect(rect, pen, brush))
                 
            # 원 그리기
            if self.parent().drawType == 3:
                brush = QBrush(self.parent().brushcolor)

                if len(self.items) > 0:
                    self.scene.removeItem(self.items[-1])
                    del(self.items[-1])

                rect = QRectF(self.start, self.end)
                self.items.append(self.scene.addEllipse(rect, pen, brush))

만약 곡선을 그릴 때 path를 사용하지 않고 line을 사용하고 싶다면 아래와 같은 코드를 작성하면 됩니다. 둘 중에 하나만 사용하시면 됩니다.

#Line 이용
line = QLineF(self.start.x(), self.start.y(), self.end.x(), self.end.y())
self.scene.addLine(line, pen)

3. 마우스이벤트 함수(3)

이번에는 마우스 클릭을 해제했을때 호출되는 함수입니다.

먼저지우개 모드인 경우, 바로return None을 사용해서 다음 그리기 동작이 일어나지 않도록 합니다. 그리고 곡선 그리기를 제외한 다른 그리기 모드인 경우, 마우스 클릭을 해제했을 때 최종적으로마우스 클릭 해지시 완성된 그림을 그리도록 합니다. 마지막으로 곡선 그리기 모드의 경우, 마우스를 움직일 때마다 짧은 선을 이어 붙여 곡선을 그려가므로 마우스 클릭을 해제했을 때 새로 그릴 필요는 없습니다.

직선이나 사각형, 원의 경우는 클릭이 해제되었을 때 기존에 그려진 것들을 삭제하고 최종적으로 다시 한번 그려지는 방식으로 구현하였습니다. 즉, 마우스 이동시에는 그리는 과정을 눈으로 보여주기 위한 중간 단계였다면, 마우스 클릭해지는 마지막 단계인 셈이죠.

def mouseReleaseEvent(self, e):
        if e.button() == Qt.LeftButton:
            if self.parent().checkbox.isChecked():
                return None

            pen = QPen(self.parent().pencolor, self.parent().combo.currentIndex())

            if self.parent().drawType == 0:
                self.items.clear()
                line = QLineF(self.start.x(), self.start.y(), self.end.x(), self.end.y())
                self.scene.addLine(line, pen)

            if self.parent().drawType == 2:
                brush = QBrush(self.parent().brushcolor)
                self.items.clear()
                rect = QRectF(self.start, self.end)
                self.scene.addRect(rect, pen, brush)

            if self.parent().drawType == 3:
                brush = QBrush(self.parent().brushcolor)
                self.items.clear()
                rect = QRectF(self.start, self.end)
                self.scene.addEllipse(rect, pen, brush)

 

메인 함수

1. 메인함수

지금까지 클래스를 생성하여 우리가 원하는 프로그램이 실행되도록 다양한 함수를 만들어 보았습니다. 정말 수고 많으셨어요:)

이제 마지막으로 메인함수를 만들어서 그림판을 실행시켜볼거에요. 즉, 이 부분이실제 프로그램의 시작점이라고보시면 됩니다.

클래스에 대부분의 코드가 있으므로 메인에는 앱(app)을 만들고, 클래스를 생성하면 이제 우리가 직접 만든 그림판을 볼 수 있습니다. 이때, 우리가 호출할 것들이 모듈 형태로 불려져 실행되는 것을 막기 위해파이썬에서는__name__이 __main__인지 비교하는 방식을 많이 사용합니다. 그래서 먼저 __name__이 __main__인지 확인을 한 뒤, QApplication을 현재의 파이썬 코드 파일(sys.argv 사용)로 설정해 만든 후, CWidget 클래스의 객체(변수)인 w를 생성합니다. 마지막으로 show()함수를 통해 윈도우 창을 띄운 후, 앱을 실행시키는 방식입니다.

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    w.show()
    sys.exit(app.exec_())

2. 최종코드

와!! 이제 드디어 파이썬으로 그림판을 완성했습니다~

마지막으로 최종코드를 보시면서 혹시 빠진 부분이 없는지, 잘못 코딩한 부분이 없는지 확인해보세요:)

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

class CWidget(QWidget): 

    def __init__(self):

        super().__init__()

        # 전체 폼 박스
        formbox = QHBoxLayout()
        self.setLayout(formbox)

        # 좌, 우 레이아웃박스
        left = QVBoxLayout()
        right = QVBoxLayout()

        # 그룹박스1 생성 및 좌 레이아웃 배치
        gb = QGroupBox('그리기 종류')        
        left.addWidget(gb)

        # 그룹박스1 에서 사용할 레이아웃
        box = QVBoxLayout()
        gb.setLayout(box)        

        # 그룹박스 1 의 라디오 버튼 배치
        text = ['line', 'Curve', 'Rectange', 'Ellipse']
        self.radiobtns = []

        for i in range(len(text)):
            self.radiobtns.append(QRadioButton(text[i], self))
            self.radiobtns[i].clicked.connect(self.radioClicked)
            box.addWidget(self.radiobtns[i])

        self.radiobtns[0].setChecked(True)
        self.drawType = 0

        # 그룹박스2
        gb = QGroupBox('펜 설정')        
        left.addWidget(gb)        

        grid = QGridLayout()      
        gb.setLayout(grid)        

        label = QLabel('선굵기')
        grid.addWidget(label, 0, 0)

        self.combo = QComboBox()
        grid.addWidget(self.combo, 0, 1)       

        for i in range(1, 21):
            self.combo.addItem(str(i))

        label = QLabel('선색상')
        grid.addWidget(label, 1,0)        

        self.pencolor = QColor(0,0,0)
        self.penbtn = QPushButton()        
        self.penbtn.setStyleSheet('background-color: rgb(0,0,0)')
        self.penbtn.clicked.connect(self.showColorDlg)
        grid.addWidget(self.penbtn,1, 1)

        # 그룹박스3
        gb = QGroupBox('붓 설정')        
        left.addWidget(gb)

        hbox = QHBoxLayout()
        gb.setLayout(hbox)

        label = QLabel('붓색상')
        hbox.addWidget(label)                

        self.brushcolor = QColor(255,255,255)
        self.brushbtn = QPushButton()        
        self.brushbtn.setStyleSheet('background-color: rgb(255,255,255)')
        self.brushbtn.clicked.connect(self.showColorDlg)
        hbox.addWidget(self.brushbtn)

        # 그룹박스4
        gb = QGroupBox('지우개')        
        left.addWidget(gb)

        hbox = QHBoxLayout()
        gb.setLayout(hbox)        

        self.checkbox  =QCheckBox('지우개 동작')
        self.checkbox.stateChanged.connect(self.checkClicked)
        hbox.addWidget(self.checkbox)
        left.addStretch(1)        

        # 우 레이아웃 박스에 그래픽 뷰 추가
        self.view = CView(self)       
        right.addWidget(self.view)        

        # 전체 폼박스에 좌우 박스 배치
        formbox.addLayout(left)
        formbox.addLayout(right)
        formbox.setStretchFactor(left, 0)
        formbox.setStretchFactor(right, 1)
        self.setGeometry(100, 100, 800, 500) 

    def radioClicked(self):
        for i in range(len(self.radiobtns)):
            if self.radiobtns[i].isChecked():
                self.drawType = i                
                break

    def checkClicked(self):
        pass

    def showColorDlg(self):       
        # 색상 대화상자 생성      
        color = QColorDialog.getColor()
        sender = self.sender()

        # 색상이 유효한 값이면 참, QFrame에 색 적용
        if sender == self.penbtn and color.isValid():           
            self.pencolor = color
            self.penbtn.setStyleSheet('background-color: {}'.format( color.name()))
        else:
            self.brushcolor = color
            self.brushbtn.setStyleSheet('background-color: {}'.format( color.name()))


# QGraphicsView display QGraphicsScene
class CView(QGraphicsView):
    def __init__(self, parent):
        super().__init__(parent)       
        self.scene = QGraphicsScene()        
        self.setScene(self.scene)

        self.items = []

        self.start = QPointF()
        self.end = QPointF()

        self.setRenderHint(QPainter.HighQualityAntialiasing)


    def moveEvent(self, e):
        rect = QRectF(self.rect())
        rect.adjust(0,0,-2,-2)
        self.scene.setSceneRect(rect)

    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:

            # 시작점 저장
            self.start = e.pos()
            self.end = e.pos()        

    def mouseMoveEvent(self, e):  
        # e.buttons()는 정수형 값을 리턴, e.button()은 move시 Qt.Nobutton 리턴 
        if e.buttons() & Qt.LeftButton:           
            self.end = e.pos()

            if self.parent().checkbox.isChecked():
                pen = QPen(QColor(255,255,255), 10)
                path = QPainterPath()
                path.moveTo(self.start)
                path.lineTo(self.end)
                self.scene.addPath(path, pen)
                self.start = e.pos()
                return None

            pen = QPen(self.parent().pencolor, self.parent().combo.currentIndex())

            # 직선 그리기
            if self.parent().drawType == 0:
                # 장면에 그려진 이전 선을 제거            
                if len(self.items) > 0:
                    self.scene.removeItem(self.items[-1])
                    del(self.items[-1])                
                # 현재 선 추가
                line = QLineF(self.start.x(), self.start.y(), self.end.x(), self.end.y())                
                self.items.append(self.scene.addLine(line, pen))

            # 곡선 그리기
            if self.parent().drawType == 1:
                # Path 이용
                path = QPainterPath()
                path.moveTo(self.start)
                path.lineTo(self.end)
                self.scene.addPath(path, pen)

                # Line 이용
                #line = QLineF(self.start.x(), self.start.y(), self.end.x(), self.end.y())
                #self.scene.addLine(line, pen)

                # 시작점을 다시 기존 끝점으로
                self.start = e.pos()

            # 사각형 그리기
            if self.parent().drawType == 2:
                brush = QBrush(self.parent().brushcolor)

                if len(self.items) > 0:
                    self.scene.removeItem(self.items[-1])
                    del(self.items[-1])

                rect = QRectF(self.start, self.end)
                self.items.append(self.scene.addRect(rect, pen, brush))

            # 원 그리기
            if self.parent().drawType == 3:
                brush = QBrush(self.parent().brushcolor)

                if len(self.items) > 0:
                    self.scene.removeItem(self.items[-1])
                    del(self.items[-1])

                rect = QRectF(self.start, self.end)
                self.items.append(self.scene.addEllipse(rect, pen, brush))

    def mouseReleaseEvent(self, e):        
        if e.button() == Qt.LeftButton:
            if self.parent().checkbox.isChecked():
                return None

            pen = QPen(self.parent().pencolor, self.parent().combo.currentIndex())

            if self.parent().drawType == 0:
                self.items.clear()
                line = QLineF(self.start.x(), self.start.y(), self.end.x(), self.end.y())
                self.scene.addLine(line, pen)

            if self.parent().drawType == 2:
                brush = QBrush(self.parent().brushcolor)
                self.items.clear()
                rect = QRectF(self.start, self.end)
                self.scene.addRect(rect, pen, brush)

            if self.parent().drawType == 3:
                brush = QBrush(self.parent().brushcolor)
                self.items.clear()
                rect = QRectF(self.start, self.end)
                self.scene.addEllipse(rect, pen, brush)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    w.show()
    sys.exit(app.exec_())

이제 코드를 실행시키면 아래와 같은 화면(도형 및 글자 제외)이 실행됩니다. 화면이 잘 실행되고 펜이나 도형들이 잘 그려진다면 여러분은 성공하신 겁니다!!(짝짝짝)


본 강의는 오션코딩학원의 파이썬 예제 (그림판)에 설명을 붙인 강의입니다.

원글 주소: oceancoding.blogspot.com/2019/03/blog-post.html