파이썬 예제 (그림판)

이번에 만들어 본 예제는 Python + PyQt5 를 이용한 그림판 (Painter) 입니다.



주요 기능

1. 직선, 곡선, 사각형, 원 그리기

2. 선색, 도형 내부 색 변경 가능

3. 지우개 기능

정도 입니다.



주석 포함 300라인이 채 안되는 코드로 최대한 간단히 구현해 보았습니다.

구글을 열심히 검색해봐도 파이썬+PyQt를 이용한 그림판 예제는 거의 없어 직접 만들어 보았습니다.

먼저 이 코드를 이해하기 위해 요구되는 배경 지식입니다.

1. Python 의 기본 자료형 (정수, 문자, 리스트)

2. Python 클래스의 이해 (클래스의 개념,  객체 변수, 함수, 상속)

3. PyQt5, 특히 Qt 클래스에 대한 사용법, 이해 정도 입니다. (가장 중요)

PyQt5 설치 및 소개글 링크


PyQt5는 C++의 크로스 플랫폼 프레임워크인 Qt를 파이썬에 사용가능하도록 만든 파이썬 패키지 입니다.

PyQt는 Riverbankcomputing 이라는 영국회사에서 만들었습니다.

Qt는 C++기반의 멋진 클래스들을 가진 GUI 프레임워크이며, GUI 이외에도 자료형, 네트워크, 3D, 등의 분야에 사용가능한 클래스를 제공해 줍니다.

아래 예제의 Q로 시작하는 모든 타입은 Qt에서 제공되는 클래스입니다.

앞으로 우리는 Qt에서 제공하는 클래스를 가져와 사용하는 방식으로 코드를 만들어 갈 계획입니다.

그럼 본론으로 들어가 보겠습니다.

그림판은 크게 2개의 클래스로 구성되어 있으며, 첫번째는 메인 윈도우 창을 구성하는 CWidget클래스, 두번째는 그림을 표시하는 CView 클래스 입니다.

그리고 클래스를 인스턴스화 시켜 동작하는 메인 함수의 형태로 코드는 구성되어 있습니다.

먼저, 아래는 그림판 전체 코드 입니다.

[전체 코드]


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

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

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_())


[CWidget 클래스]
먼저 기본 윈도우 창 및 컨트롤(라디오버튼, 체크 박스, 콤보박스 등)을 구성하는 CWidget 클래스 설명입니다.

[1~4번 라인]
sys 모듈과 PyQt5의 모듈을 불러오는 부분입니다.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

[6번 라인]
제가 사용하는 모니터가 4K 해상도라 고해상도를 지원하는 설정 코드입니다.
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

[8번 라인]
Qt의 QWidget (윈도우 창)에서 상속받은 나만의 클래스를 만드는 부분입니다.
class CWidget(QWidget):

[10번 라인]
제가 만든 CWidget 클래스의 생성자 함수 입니다.
def __init__(self):

        super().__init__()

먼저, CWidget의 생성자 함수에서 부모 클래스(QWidget)의 생성자를 super()를 통해 호출해 주고 있습니다.

[15~20번 라인]
그 다음은 컨트롤 배치를 위한 박스 레이아웃을 잡습니다.
# 전체 폼 박스
        formbox = QHBoxLayout()
        self.setLayout(formbox)

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


붉은 색 테두리가 가장 바깥쪽 formbox (QHboxLayout 클래스) 변수이며, 좌, 우로 보이는 파란색 테두리 2개가 각각 left(컨트롤 배치용), right(그림그리기용) 로 각각 구성됩니다.

[22~40번 라인]
첫번째 그룹박스인 그리기 종류를 구성하는 부분입니다.
# 그룹박스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


사용되는 그룹박스를 만들고 라디오버튼(QRadioButton 클래스) 4개의 이름을 정한 후 for 반복문을 통해 수직 박스에 추가하는 코드입니다.

마지막 39번 라인은 기본 그리기 종류를 line으로 설정하고, 40라인에서 어떤 그리기 종류가 현재 선택되어 있는지 저장할 변수 drawType을 만들어 0(직선)으로 초기화 합니다.

[42~65번 라인]
두번째 그룹박스인 펜설정을 정하는 부분입니다.
# 그룹박스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)


역시 사용되는 그룹박스를 만든 후, 그 이름을 펜설정으로 정하고 left라는 이름의 왼쪽 레이아웃 박스에 추가합니다.

이후, 선 굵기로 표시되는 라벨(QLabel 클래스)을 추가하고, 이에 1~20범위의 값을 가지는 콤보 박스(QComboBox 클래스)를 추가합니다.

58라인부터 선 색상으로 표시되는 라벨을 추가한 후 버튼(QPushButton 클래스) 의 배경색을 변경해 해당 버튼이 눌러지면 색상창을 열어서 색을 정한 후 다시 버튼을 색을 바꾸어 표시되는 방식으로 구현합니다.



64번라인은 선색상 버튼을 눌렀을 때 발생되는 signal을 showColorDlg 함수가 호출 되도록 slot으로 연결시키는 코드입니다.

[68~82번 라인]
세번째 그룹박스인 붓 색상 설정부분입니다.
# 그룹박스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)


위의 펜 설정과 유사한 부분이 많으므로 설명은 생략하겠습니다.

[84~96번 라인]
마지막 그룹박스인 지우개 설정입니다.
# 그룹박스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)


역시 그룹박스를 left 레이아웃에 추가하고 체크박스(QCheckBox 클래스)를 추가하고, 이름은 지우개 동작으로 지었습니다.

체크여부를 판단해 차후 그림을 그릴 때 지우개 용도로 쓸 계획입니다.

96번 라인의 addStretch(1) 함수는 왼쪽 레이아웃 영역이 수직으로 모든 영역을 다 차지하는 것을 방지하기 위해 왼쪽 레이아웃에 여분의 레이아웃을 추가하는 코드입니다.

해당 라인의 맨 앞줄에 주석(#)처리를 해 보면 차이점을 바로 느낄 수 있습니다.

[98~100번 라인]
코드 실행 후 보이는 화면에서 붉은색 테두리로 표시한 흰색 그리기 영역을 잡아주는 부분입니다.
# 우 레이아웃 박스에 그래픽 뷰 추가
        self.view = CView(self)       
        right.addWidget(self.view)


이전까지 코드는 왼쪽에 보이는 영역에 컨트롤을 추가하는 작업이고, 현재 코드는 오른쪽 영역에 QGraphicsView에서 상속받은 CView라는 이름의 클래스를 추가하는 부분입니다.

이 클래스는 뒤에서 자세히 설명하도록 하겠습니다.

[102~109번 라인]
완성된 좌(컨트롤), 우(그림그리는곳) 측 레이아웃을 전체 formbox에 배치하는 코드입니다.
# 전체 폼박스에 좌우 박스 배치
        formbox.addLayout(left)
        formbox.addLayout(right)

        formbox.setStretchFactor(left, 0)
        formbox.setStretchFactor(right, 1)
        
        self.setGeometry(100, 100, 800, 500)

[111~115번 라인]
그룹박스 1번, 그리기 종류를 변경했을때 호출되는 CWidget 클래스의 멤버함수이며, 선택된 라디오 버튼의 번호를 알아내어 drawType 변수에 저장합니다.
    def radioClicked(self):
        for i in range(len(self.radiobtns)):
            if self.radiobtns[i].isChecked():
                self.drawType = i                
                break

drawType은 0번 실선, 1번 곡선, 2번 사각형, 3번 원 으로 지정하였습니다.

[117~118번 라인]
그룹박스 4번, 지우개를 체크했을때, 쓸려고 만들어 둔 함수인데, 그냥 체크 버튼의 상태값을 바로 읽어서 쓰는 형태라 별 필요는 없네요. (삭제해도 무관합니다)
    def checkClicked(self):
        pass

파이썬에서 뭔가 할일이 없을때는 pass를 적어두면 들여쓰기 문법 오류를 방지합니다.

[120~133번 라인]
선 또는 붓 색을 변경할때 호출되는 함수이며, 선색인지 붓색인지 구분하기 위해 시그널을 보낸 sender를 찾아 구분하는 코드로 구성되어 있습니다.
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()))

확인 후 색을 저장해 두고 버튼의 색을 바꾸는 방식입니다.

Qt의 QColorDialog 라는 공통 대화상자 클래스를 모달 방식(닫기전까지 블럭됨)으로 호출해 사용하는 형태입니다.

여기까지가 CWidget 클래스 코드이며, 요약하면 메인 윈도우를 생성해 Qt컨트롤들을 배치하는 역할을 담당합니다.


[CView 클래스]

실제 그림이 그려지는 역할을 담당하는 클래스이며, 아래 그림의 오른쪽 흰색 부분에 해당합니다.
class CView(QGraphicsView):



Qt의 QGraphicsView 클래스를 상속받아 구현되어 있습니다.


[140~151번 라인]
QGraphicsView의 생성자 함수이며, 이 클래스는 QGraphicsScene클래스에 그려진 그래픽 아이템들(직선, 곡선, 사각형, 원 등)을 화면에 표시하는 역할을 담당합니다.
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)

QGraphicsScene는 실제 눈에 보이지 않지만 실제 그래픽 아이템들(QGraphicsItem)을 포함하고, 관리하는 클래스 입니다.

쉽게 설명하자면, QGraphicsView는 화면에 그림을 그리는 역할, QGraphicsScene은 그려지는 요소들을 관리하는 역할, QGraphicsItem은 하나의 그래픽 요소(선, 곡선 등) 입니다.

여기서 그래픽 씬 변수을 만들고, 그래픽 씬이 가지는 그래픽 아이템들을 저장할 리스트를 선언합니다.

또한, 시작점 좌표와 끝점 좌표를 저장할 변수(QPointF 클래스) 도 선언합니다.

[154~158번 라인]
QGraphicScene의 크기를 좌, 우 스크롤바가 나타나지 않도록 2픽셀 작게 조정하는 부분입니다.
def moveEvent(self, e):
        rect = QRectF(self.rect())
        rect.adjust(0,0,-2,-2)

        self.scene.setSceneRect(rect)

[160~165번 라인]
마우스 클릭시 호출되는 함수이며, 마우스 좌클릭시 시작점, 끝점 좌표를 저장합니다.
def mousePressEvent(self, e):

        if e.button() == Qt.LeftButton:
            # 시작점 저장
            self.start = e.pos()
            self.end = e.pos() 

[167~235번 라인]
마우스 이동 시 호출되는 함수이며, 실제 마우스 이동시 그림이 그려지므로 그리기에 대한 대부분의 처리가 이곳에 집중되어 있습니다.
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))

174~181번 라인은 지우개 체크 버튼이 체크되어 있을때 배경과 같은 흰색 펜을 만들어 곡선을 그려 지우개 처럼 동작하도록 하는 코드입니다.

지우개 모드인 경우 흰선을 그리고 바로 리턴해 아래에 위치한 다른 그리기 코드들이 수행되지 않도록 해야 합니다.

183번 라인은 현재 설정된 두께와 색을 가져와 펜을 만드는 부분입니다.

186~195번 라인은 만약 그리기 설정이 0번, 즉 직선인 경우 마우스가 이동하면, 시작점 좌표는 변하지 않고, 끝점인 현재의 마우스 좌표를 기준으로 선을 그리는 코드입니다.

 다만, 마우스 이동시 마다 기존에 그려진 선(이전 마우스 이동시 그려놓았던)을 지우는 작업을, 현재 선을 그리기 전에 해주어야 합니다.

self.items[-1] 리스트의 마지막 그려진 선을 찾아와 삭제하는 것이죠.

198~211번 라인은 만약 그리기 설정이 1번, 즉 곡선인 경우 마우스가 이동하면, 현재의 시작점을 기준으로, 끝점인 현재의 마우스 좌표를 기준으로 짧은 직선을 그리는 코드입니다.

사실 곡선은 짧은 직선의 연속이기 때문에, 여기서는 직선처럼 이전 그려진 선을 지울 필요가 없습니다.

다만, 짧은 직선을 그린 후, 다음 마우스 이동시 시작점이 현재의 끝점으로 변경하는 코드가 211번 라인에 구현되어 있습니다.

213~223번 라인은 만약 그리기 설정이 2번, 즉 사각형인 경우 마우스가 이동하면, 현재의 시작점을 사각형의 왼쪽 윗점으로 정하고, 끝점을 사각형의 오른쪽 아래점으로 설정해 사각형을 그리는 코드입니다.

실제 사각형은 왼쪽 윗점의 좌표와 오른쪽 아래점의 좌표 2개만 알아도 그려낼 수 있습니다.

다만, 마우스 이동시 마다 직선 그리기와 마찬가지로 기존에 그려진 마지막 사각형을 찾아 삭제하고, 현재의 사각형을 새로 그리는 방식으로 구성되어 있습니다.

225~235번 라인의 원그리기는 사각형 그리기와 동일합니다.

차이가 있다면, QGraphicsScene 클래스가 제공하는 사각형 그리는 함수는 addRect() 이고, 원은 addEllipse() 라는 부분외에는 동일합니다.

왜냐하면 Qt에서 원을 그리는 방법은 사각형 영역을 설정하고 그 사각형에 내접하는 원을 그려내는 방식으로 구현되어 있기 때문입니다.

[238~268번 라인]
마우스 클릭을 해제했을때 호출되는 함수입니다.
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)

242~243번 라인은 지우개 모드인 경우, 바로 리턴해서 다음 그리기 동작이 일어나지 않도록 해 두었습니다.

마우스 클릭 해지시 하는 행동은 곡선을 제외한 그리기 모드에서 최종적으로 마우스를 클릭 해지시 완성된 그림을 그리는 방식입니다.

곡선의 경우는 마우스를 움직일 때마다 짧은 선을 이어 붙여 곡선을 그려가므로, 굳이 마우스 클릭 해지 시 새로 그릴 필요는 없습니다.

다만, 직선이나, 사각형, 원의 경우는 클릭 해지 시 기존에 그려진 것들을 삭제하고 최종적으로 다시 한번 그려지는 방식으로 구현하였습니다.

즉, 마우스 이동시에는 그리는 과정을 눈으로 보여주기 위한 중간 단계였다면, 마우스 클릭해지는 마지막 단계인 셈이죠.


[메인 함수]
실제 프로그램의 시작점 입니다.
if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    w.show()
    sys.exit(app.exec_())

클래스에 대부분의 코드가 있으므로 메인에는 앱을 만들고 클래스를 생성하는 행동이 전부입니다.

[271~275번 라인]
모듈 형태로 불려져 실행되는 것을 막기 위해 파이썬에서는 __name__이 __main__인지 비교하는 방식을 많이 사용합니다.

QApplication을 현재의 파이썬 코드 파일(sys.argv 사용) 로 설정해 만든 후, CWidget 클래스의 객체(변수)인 w를 생성합니다.

show()함수를 통해 윈도우 창을 띄운 후, 앱을 실행시키는 방식입니다.

마지막으로 pyinstaller 로 제작한 실행파일 링크 입니다.

링크 : 그림판 실행파일


궁금한 점은 댓글이나 문의 주시면 좀 더 자세히 설명드리도록 하겠습니다.


  • 개발 환경
  1. 운영체제 : MS Windows 10 Pro
  2. 개발 언어 : Python 3.7 (32bit), PyQt5 (5.11.3)
  3. 개발 도구 : MS Visual Studio 2017 Pro


감사합니다.

댓글

  1. 안녕하세요! 좋은글 감사합니다.
    혹시 그림판에 그린 그림을 jpg로 저장할수있는 기능을 구현할수 있을까요?
    제가 저장 버튼을 만들고, 누르면
    def Save_pixel(self):
    pixmap = QPixmap(self.view.size())
    pixmap.save("test.jpg")
    함수가 호출되게 해놨더니 저장은 되는데 제가 그린 그림이 아니더라구요 ㅠㅠ

    답글삭제
    답글
    1. 거의 다 해결하셨네요. ^^

      img = QPixmap(self.view.grab(self.view.sceneRect().toRect()))
      img.save('D:\\test.png')

      QGraphicsView의 garb() 함수를 사용하면 해당 영역의 QPixmap class를 리턴합니다.
      이를 QPixmap의 생성자로 전달하고, save() 함수로 저장하면 됩니다.

      감사합니다.

      삭제
    2. 안녕하세요, 이 방법으로 사진을 저장하였는데 왼쪽과 위쪽에 검은 실선까지 사진에 포함이 되었더라고요, 혹시 이 부분을 제외하고 저장할 수 있는 방법이 있을까요..? 예를 들어 좌표를 작성한다던지 등이요..

      삭제
    3. 사각형 영역을 지정하는 QRect 클래스의 adjust() 함수를 참조 바랍니다.

      삭제
  2. 그림판에 그린 그림을 버튼하나로 지우는걸 만들고 싶은데 어떻게 해야할지 이거는 감이 안잡히네요 ㅠㅠ 도움 주실수 있을까요???

    답글삭제
    답글
    1. 아래 과정으로 진행하면 됩니다.

      1. 지우기 버튼 추가 (QPushButton)

      2. 버튼 눌렀을때 시그널, 슬롯 처리 (버튼을 클릭했을때)

      3. 해당 슬롯 함수에서 아래 코드 추가

      for i in self.view.scene.items():
      self.view.scene.removeItem(i)

      4. QGraphicsScene 클래스의 Items 들을 순회하며 모든 아이템을 제거하는 코드입니다.

      삭제
  3. 안녕하세요, 좋은글 정말 감사합니다!
    만들고 싶은 프로그램이 있었는데 덕분에 많은 도움이 되었습니다.
    저장하는 기능이 막혀서 질문하나만 답해주실수 있나요?ㅠㅠ

    제가 만들고자 하는 프로그램은 원하는 사진을 load해서 zoomIn/out도 되고 그림판처럼 사용하고 저장하는 프로그램을 만들고자 합니다.
    현재 모두 구현은 했으나 저장만 안되는 상황입니다ㅜㅜ

    class Viewer_test(QGraphicsView):
    self._scene = QGraphicsScene(self)
    self._photo = QGraphicsPixmapItem()
    self._scene.addItem(self._photo)
    -----~중략~---
    self._photo.setPixmap(pixmap)
    이렇게 로드를 한 상태이며, 그림을 그린것은

    self._added = self._scene.addRect(rect,pen)
    이와 같이 표시해 주었습니다.
    하지만, 알려주신 grab이나, render도 사용해 보았는데,
    불러온 사진은 저장이 안되고,

    그림그린것만 저장이 되네요.. 혹시 조언해주실수 있나요?

    답글삭제
    답글
    1. 안녕하세요 익명으로 답글 달았던 글쓴이입니다.
      계속 시도를 해본결과,
      img = QPixmap(self.viewer_edit.grab())
      painter = QPainter(img)
      painter.setRenderHint(QPainter.Antialiasing)
      self.viewer_edit.render(painter)
      painter.end()
      img.save('D:\\test.png')

      위와같이 코드를 짰든데 저장은 되는 결과를 볼 수 있었습니다.

      문제가 사진은 3000x3000인데, 저장은 정말 화면 그대로 1881x768 로 저장했다는점으로 보아, 사진을 추출하지 않고 화면 그대로를 캡쳐한거 같습니다.
      혹시, 이미지를 추출해서 제가 수정한 부분만 overwrite 시키지 못하나요?ㅠㅠ

      읽어주셔서 감사합니다.

      삭제
    2. 왜냐하면 이미지의 사이즈와 QGraphicsView의 크기가 서로 다르기 때문입니다.
      이미지를 불러들여 편집(선, 사각형, 곡선) 등을 그리고 저장하려면 아래 2가지 방식중 하나를 선택해야 합니다.

      1. QGraphicsView의 크기를 모니터가 아닌 사진 크기에 맞추어 크게 잡아야 합니다. 아마도 좌,우 스크롤바가 필요할 것입니다. 여기에 그림을 올리고 편집후 저장하면 이미지의 크기대로(1:1) 저장됩니다. 현재는 QGraphicsView의 영역보다 이미지 사이즈가 크기 때문에 이미지가 작게 리사이징되어 그래픽 뷰에 올려진 상태가 아닐까 합니다.

      2. 1:1 크기로 뷰와 이미지가 대응되는 것이 싫다면(스크롤바 생성), 기존과 같은 방식으로 모니터의 해상도에 맞게 만들어 진행하고 마지막에 저장할때 이미지를 Scale(확대, 축소) 하여 이미지 파일의 크기에 맞추어야 합니다.

      2번 방법은 확대 과정에서 Qt의 Smooth Transformation 을 사용하더라도 확대시 품질이 저하될 수 있습니다.

      마지막으로, 2번 방법으로 화면의 크기에 맞추어 진행하되 포토샵처럼 확대해 편집이 가능하게 하는 방법도 있겠지만 코드 구현이 쉽지 않을것 같습니다.

      삭제
  4. 정말 자세한 설명 너무너무 감사드립니다 큰 도움이 되었습니다!

    답글삭제
  5. 안녕하세요! 혹시 흰색 박스 대신 불러온 사진 위에 그림을 그리고 싶을 경우에는 어떤 방법이 있을까요?

    답글삭제
    답글
    1. 예제에서 설명해둔 QGraphicsView, QGraphicsScene의 관계를 잘 파악하면 쉽게 구현 가능합니다.

      QPixmap Class를 이용 이미지 로드 후 QGraphicsScene의 addPixmap() 함수로 사진을 표시한 후 그림을 그리면 (기존 예제 코드와 같음) 됩니다.

      삭제
    2. 그러면 혹시 사진 위에 그리는 경우에는 지우개 기능을 추가하기는 힘들겠죠..? 아무래도 사진은 흰색이 아니다보니.. 혹시 사진 위에서도 지우개를 구현할 수 있는 방법은 없을까요..?

      삭제
    3. 일단 사진을 불러들여서 진행해 보세요. 스스로 진행해가는 과정에서 답을 찾을 수 있습니다.

      삭제
    4. 제가 142번 줄 아래에
      pixmap = QPixmap()
      pixmap.load("image.png")
      scene = QGraphicsScene()
      scene.addPixmap(pixmap)
      self.setScene(scene)
      이렇게 코드를 작성하였는데 run이 되질 않습니다.. 오류도 뜨지 않고요
      경로는 정확합니다! 혹시 어떤 문제인지 알 수 있을까요..?

      삭제
    5. 경로를 C:/test.png 절대 경로로 설정해 보세요.
      코드는 문제가 없습니다.

      삭제
    6. 성공하였습니다..! 다른 창에 있는 버튼을 클릭하면 그림판이 나오게 하고 싶어서
      self.pushButton.clicked.connect(self.draw)
      def draw(self):
      self.close()
      w = CWidget()
      w.show()
      w.exec()
      이렇게 작성하였는데 그림판이 뜨지 않습니다... 혹시 저 코드에 문제가 있을까요?

      삭제
    7. 게시물과 관련없는 질문은 답변드리지 않음을 양해 바랍니다.

      삭제
  6. 혹시 파일 분할본도 따로 올려주실수 있으신가요??

    답글삭제
    답글
    1. 파일 분할본 이 뭘 말씀하시는 건지 잘 모르겠네요.

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

      이렇게 되어있는 main .py와 메인 코드로 나뉘어진 파일을 말하는 것입니다.
      코드 분할법이 익숙하지 않아서..

      삭제
    3. 코드를 하나의 파일(*.py)에 작성하지 않고 여러 파일에 분산시켜 작성하는 것을 의미한다면 아래의 링크를 참조바랍니다.

      https://docs.python.org/ko/3.10/tutorial/modules.html

      파이썬에서는 이를 모듈, 패키지 라고 표현합니다.

      삭제
  7. 직선그리기의 경우 하나의 직선을 그린후 그다음직선이 전에 그었던 직선과 연결되게 하려면 어떻게 코드를 짜야할까요?

    답글삭제
    답글
    1. 2번째 그린직선의 시작점을 첫번쨰 그린직선의 끝점으로 하고싶습니다!

      삭제
    2. 이미 본문에 직선그리기 원리를 설명해 두었습니다.

      직선은 마우스의 Press, Release이벤트를 이용해 찾아낸 시작, 끝점을 이어서 그리는 방식이므로 마지막으로 그려진 직선의 끝점 x, y 좌표를 찾아와 새로 그려지는 시작점의 x, y 좌표로 설정하면 됩니다.

      삭제
  8. 도형을 그릴때 전 도형에 덮씌우는 방식이 아니라 겹치는 방식으로 바꾸고 싶은대 어떻게 하면 될까요??

    답글삭제
    답글
    1. 추가로 도형을 그릴때 오른쪽 아래만 그려지고 왼쪽 위 부분은 왜 짤리는 현상도 수정해보고 싶어서 시도하고 있지만 잘 안되서 질문드립니다.

      삭제
    2. 포토샵처럼 Multiple Layer 개념을 적용하면 됩니다.

      예제코드 처럼 하나의 그림층이 아닌 여러 장이 겹친 그림층을 구현하고, 각 그림층을 Raster operation으로 픽셀 연산을 수행하면 구현 가능합니다.

      도형이 잘리는 현상은 게시물을 작성한지가 오래되었지만, 그런 버그는 없었던거 같은데 잘 모르겠네요.

      삭제
    3. operation으로 픽셀 연산을 수행하는 방법은 알겠는대 하나의 그림층이 아니라 여러 장이 겹친 그림층을 구현하려면 어떤 방식으로 해야할지 모르겠네요.. view와 관련된 class를 하나 더 선언해야 할까요?

      삭제
    4. QGraphicsItem Class 의 setZValue() 함수가 비슷한 동작을 수행합니다.

      삭제
  9. 실행시키면 창 이름이 python 이라고 되어있는데 이 창 이름을 바꾸려면 어떻게 해야 할까요

    답글삭제
    답글
    1. QWidget 클래스의 setWidnowTitle 메서드를 사용하면 됩니다.

      삭제
  10. 좋은 게시글 잘 보고 배웠습니다. 감사합니다.
    이미지 처리를 위해서 위의 방법으로 그림판에 그림을 불러왔는데
    이미지가 담기기를 양끝에 1픽셀정도씩 남기고 담기더라구요.
    rect가 위치반환을 정확한 위치를 반환하지는 않는다고해서
    위치조절 및 graphicsview의 크기조절 들을 하고 있는데 도저히 graphicview의 위치 및 크기와 scene을 매치시키지를 못하겠습니다.

    혹시 방법이 있을까요?..

    답글삭제
    답글
    1. 혹시 4K 모니터를 쓴다면 그럴수도 있습니다.

      6번 라인의 아래 명령어는,

      QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

      4k 해상도를 HD인 것처럼 재스케일해 보여주는데 이 때 픽셀간 오차가 생길 수 있으므로, 위 코드를 주석처리하고 시도해 보세요.

      삭제
  11. 혹시 도형이나 테두리가 있는 모형에 색을 채울 수 있는 색채우기 코드는 어떻게 구현할 수 있을까요? 그리고 사용자에게 텍스트를 입력받아서 그림판 화면에 나타나게 하는 코드도 구현할 수 있을까요? 좋은 자료 감사합니다!

    답글삭제
  12. Qt에서 폐곡선을 다루는 클래스를 이용해 영역을 생성하고 색을 채우면 됩니다.
    (이름은 잘 기억이 나지 않네요.)

    글자를 적는 부분은 QPainter 의 drawText() 함수를 참조 바랍니다.

    답글삭제
    답글
    1. 혹시 찾기 어려워서 그런데 글자 적는 코드 정확하게 알 수 있을까요? 감사합니다.

      삭제
    2. 깜빡했는데, QPainter를 직접 사용하지 않는,
      그림판에서는 QGraphicsScene의 addText()를 사용해야 합니다.

      아래 Qt의 도움말 참조 바랍니다.
      https://doc.qt.io/qt-5/qgraphicsscene.html

      삭제
  13. 그림까진 잘 그려지는데 저장하는 기능은 어떻게 만드나요?

    답글삭제
    답글
    1. 같은 질문이 댓글에 있으며 해당 답변 참조 바랍니다.

      삭제
  14. 안녕하세여~ 혹시 이 그림판기능을 pyqt5로 만든 사진편집gui에도 그대로 가져다 쓸수 있을까요?
    불러온 사진위에 그림판기능을 사용하고싶은데 어떤식으로 연동하면될지 감이안잡히네요

    답글삭제
    답글
    1. 같은 질문이 댓글에 있으며 해당 답변 참조 바랍니다.

      삭제
    2. 저도 같은 질문입니다 말씀하신 코드를 토대로 만들어보고 했는데
      저는 프레임을 만들어서 그 안에 사진을 불러올수있지만 사진을 불러오면 그리기 종류 위에 그림판 흰 레이아웃이 나오는현상이 생겼어요 그리고 사진이 불러온 그 프레임은 그리기가 안돼요 반대로 그 프레임안에 뷰를 넣으면 사진도 그리기도 다 안되네요 ㅜ 코드를 보여드리고싶은데 ...현재 배운지 일주일됐어요..

      삭제
    3. 다른 댓글에도 있는 코드이지만 아래와 같이 구현하면 됩니다.

      img = QPixmap('a.jpg')
      self.scene.addPixmap(img)

      CView 클래스 생성자함수 끝부분에 추가해 보기 바랍니다.

      삭제
    4. 말씀하신 코드를 입력해보니 a 라는 사진만 출력이 되고 그리기는 안되는것같아요 ㅠ 클래스가 두개이면 이걸 어떤식으로 묶어야하나요? 사진을 사용자가 직접 폴더에 들어가 가져와서 그릴수있게 하고싶은데 사진을 가져오면 그리기는 안되고 그리는곳은 그리기종류위에 따로 레이아웃이 만들어지네요 아주 작게 ㅠ

      삭제
    5. 게시물의 예제 코드에 위 두 줄을 추가하면 이미지가 출력되고 그 위에 그림이 그려지게 됩니다. 그림이 사진위에 그려지지 않는다면 위 게시물의 코드를 복사해 시도해 보기 바랍니다.

      그리고 게시물과 관련없는 질문은 답변드리지 않음을 양해 바랍니다.

      삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)