PyQt 예제 3.2.2

이번 글에서는 독(Dock) 위젯을 추가해보고, 이미지를 다루는 방법을 알아보겠다.


독 위젯은 중심 위젯 상하좌우에 배치되거나 창 위에 뜨도록 할 수 있는 위젯을 말한다. 위 스크린샷에서는 오른쪽과 아래에 독 위젯이 배치되어 있다. 이것을 고정시킬 수도 있고, 사용자 마음대로 위치를 옮길 수도 있다. 또는 아예 창 위에 뜨도록 할 수 있다.

여기서는 딱히 넣을 만한 위젯이 없어서 임의로 그림을 나타낼 수 있는 위젯과 리스트 위젯을 독 위젯으로 배치해보았다.

먼저, 독 위젯을 추가해보겠다.
### Dock Widget ###
        # List Dock Widget
        listDockWidget = QDockWidget("리스트 Dock", self)
        listDockWidget.setObjectName("ListDockWidget")
        self.listWidget = QListWidget()
        listDockWidget.setWidget(self.listWidget)
        self.addDockWidget(Qt.RightDockWidgetArea, listDockWidget)

        self.listWidget.addItems(["리스트 항목 {}".format(k) 
                                    for k in range(1, 5)])

        # Image Label Dock Widget
        imageLabelDock = QDockWidget("이미지 Dock", self)
        imageLabelDock.setObjectName("TextBrowserDockWidget")
        self.imageLabel = QLabel()
        imageLabelDock.setWidget(self.imageLabel)
        self.addDockWidget(Qt.BottomDockWidgetArea, imageLabelDock)

        self.imageLabel.setMinimumSize(200, 200)
        self.imageLabel.setAlignment(Qt.AlignCenter)
        self.imageLabel.setFrameShape(QFrame.StyledPanel)

        self.image = QImage(":kubuntuLogoIcon.png")
        self.imageLabel.setPixmap(QPixmap.fromImage(self.image))
QDockWidget을 통해서 독 위젯을 추가해준다. 이때, 인자로는 독에 표시될 이름을 준다. 그리고 setObjectName으로 지난 번과 같이 객체에 이름을 배정해준다.

다음으로 독 위젯에 배치할 실제 위젯을 추가해준다. listDockWidget에는 리스트 위젯을 추가할 것이므로 QListWidget을 사용했고, imageLabelDock 에는 이미지를 표시할 것이므로 QLabel을 사용했다.
(참고로 QLabel은 이미지도 표시해준다.)

그 다음, 독 위젯에서 setWidget을 통해서 배치될 위젯을 설정해주고, 메인 윈도우에 독 위젯을 추가해주기 위해 self.addDockWidget을 통해서 위젯을 추가해준다.

이때, 독 위젯을 어디에 배치해줄지 정하기 위해서 Qt.RightDockWidgetArea와 같은 상수를 사용해서 배치 위치를 알려준다.

그 이후로는 실제 위젯들이 작동하는 코드로, listWidget에는 항목들을 추가해주었고, imageLabel에는 이미지의 최소 사이즈 지정과, 이미지의 정렬, 레이블 테두리 모양 지정을 해주었다.

그리고나서 레이블에 이미지를 배치하기 위해서는 QPixmap을 사용해야 하는데, QPixmap의 static 메소드인 fromImage를 통해서 이미지를 pixmap으로 변경시켜주면 된다. 이렇게 하면 레이블에 이미지가 표시되게 된다.
(참고로, QPixmap은 화면에 이미지들을 최적화 해서 표시해주기 때문에서 속도가 빠르다. 그리고 QImage는 이미지를 수정하는 측면에서 최적화 되어 속도가 빠르다. 따라서 이미지 데이터(self.image)와 이미지를 표시해주는 부분이 따로 구성되어 있다.)

그리고 위에서는 사용하지 않았지만, 독 위젯에 setAllowedAreas를 통해서 독 위젯이 배치될 수 있는 위치를 지정해줄 수 있다. 이때 필요한 인자는 위에서 사용한 Qt.RightDockWidgetArea와 같은 상수들의 비트 합(|)을 통해서 구성된다.
(예를 들어, 왼쪽, 오른쪽 배치를 위해서는 Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea를 인자로 넘기면 된다.)

이렇게 하면, 화면에 독 위젯들이 나타난다. 그리고 위젯들을 클릭한 상태로 움직이면 독 위젯의 배치를 변경할 수 있다. 이때, 창의 크기가 충분히 크지 않으면 위젯의 배치가 되지 않을 수 있다.

그리고 독 위젯에서 x를 눌러서 닫게 되면, 윈도우의 툴바나 독 위젯 위치에서 우클릭을 하면 다시 표시할 수 있다.
(처음에는 독 위젯이 없어져서 어떻게 복구해야 하는지 몰라서 놀랐었다...)


이제 독 위젯을 추가해보았으니, 이미지를 다루는 부분에 대해서도 알아보겠다. 여기서는 간단하게, 다른 이미지를 가져오고, 확대/축소를 해보는 것만 해보겠다.

먼저, 액션을 간단하게 살펴보겠다.
# Open Image Action
        openImageAction = QAction("이미지 열기", self)
        openImageAction.setShortcut("Ctrl+I")
        openImageHelp = "이미지를 나타냅니다"
        openImageAction.setToolTip(openImageHelp)
        openImageAction.setStatusTip(openImageHelp)
        self.connect(openImageAction, SIGNAL("triggered()"), self.OpenImage)

        # Image Zoom Action
        imageZoomAction = QAction("이미지 확대/축소", self)
        imageZoomHelp = "이미지를 확대하거나 축소합니다."
        imageZoomAction.setToolTip(imageZoomHelp)
        imageZoomAction.setStatusTip(imageZoomHelp)
        self.connect(imageZoomAction, SIGNAL("triggered()"), self.ImageZoom)
이제는 많이 보았고 다루어 봤으니, 따로 설명은 하지 않겠다.

그리고 이미지에서 우클릭을 했을 때, 팝업 메뉴를 표시하도록 했다.
# Image Label in Dock
        self.imageLabel.setContextMenuPolicy(Qt.ActionsContextMenu)
        self.imageLabel.addAction(openImageAction)
        self.imageLabel.addAction(imageZoomAction)

그러면 이제, 위의 액션에서 연결한 슬롯들을 살펴보겠다. 먼저, 이미지를 여는 슬롯이다.
#
    def OpenImage(self):
        imageFormats = ["{0} 파일 (*.{0})".format(ext.data().decode()) for ext in
                        QImageReader.supportedImageFormats()]
        imageFormats.append("모든 파일 (*.*)")
        fileDialog = QFileDialog(self, "이미지 열기", ".")
        fileDialog.setFilters(imageFormats)
        fileDialog.setAcceptMode(QFileDialog.AcceptOpen)
        if fileDialog.exec_():
            imageLink = fileDialog.selectedFiles()[0]
            self.image = QImage(imageLink)
            self.imageLabel.setPixmap(QPixmap.fromImage(self.image))
먼저, 이 슬롯에서는 이미지 파일을 찾기 위해서 파일 대화상자를 열어서 그 이미지의 경로를 가져온 다음, 레이블에 이미지를 배치하도록 할 것이다.

파일 대화상자를 열기 전에 Qt에서 지원하는 이미지들의 형식들을 알아내기 위해서 리스트 내장 방식으로 파일 포맷들을 리스트로 구성했다.

QImageReader.supportedImageFormats() 메소드를 실행시키면 지원하는 이미지 포맷들을 QByteArray 형의 리스트로 반환해준다. 이들을 ext로 하나씩 가져온 다음, QByteArray의 data 메소드를 통해서 문자열을 가져오고, decode 메소드를 통해서 유니코드로 바꾸었다.

이렇게 해서, 지정된 포맷으로 리스트를 생성하면 리스트가 완성된다. 예를 들어서, jpg 파일 (*.jpg)와 같은 문자열들이 포함되어 있게 된다.

그리고 모든 파일을 검색하는 부분을 추가하기 위해서, 따로 "모든 파일 (*.*)"을 리스트에 추가해주었다.

그 다음, QFileDialog 대화 상자를 생성하고, setFilters를 통해서 포맷을 지정해주고, 파일 대화 상자의 형식을 "열기" 방식으로 지정하기 위해 AcceptOpen을 사용해서 형식을 지정했다.

그 다음, 대화 상자를 실행 시키고, "확인"이 눌리면 대화 상자로부터 선택된 파일의 첫번째 값(파일의 절대 경로)을 가져와서 이미지를 불러오도록 했다.

참고로, 파일 포맷을 지정할 때, 하나의 이름에 여러 파일 확장자를 쓰려면 빈 공백으로 확장자를 구분해야 한다. 예를 들어, html 파일(*.htm *.html) 처럼 해야 한다.

그리고 파일 포맷으로 리스트 대신에 하나의 문자열을 넣을 수 있는데, 여러 타입들이 있을 때에는 ;; 을 통해서 구분 해야 한다. 예를 들어서, 텍스트 파일 (*.txt);;이미지 파일 (*.jpg *.bmp) 처럼 해야 한다.



이제 이미지를 열어보았고, 이미지 확대/축소를 해보겠다.
def ImageZoom(self):
        if self.image.isNull():
            return
        
        zoomDialog = QDialog(self)

        widthSpinBox = QSpinBox()
        widthSpinBox.setRange(0, 1000)
        widthSpinBox.setSuffix(" %")
        heightSpinBox = QSpinBox()
        heightSpinBox.setRange(0, 1000)
        heightSpinBox.setSuffix(" %")
        
        from math import ceil
        image = self.imageLabel.pixmap().toImage()
        widthZoom = ceil(image.width() * 100 / self.image.width())
        heightZoom = ceil(image.height() * 100 / self.image.height())
        widthSpinBox.setValue(widthZoom)
        heightSpinBox.setValue(heightZoom)

        sameZoomCheck = QCheckBox("같은 비율 유지")

        def ValueSameSet(value):
            if sameZoomCheck.isChecked():
                widthSpinBox.setValue(value)
                heightSpinBox.setValue(value)
        
        self.connect(widthSpinBox, SIGNAL("valueChanged(int)"),        
                        ValueSameSet)
        self.connect(heightSpinBox, SIGNAL("valueChanged(int)"),
                        ValueSameSet)
        self.connect(sameZoomCheck, SIGNAL("stateChanged(int)"),
                    lambda: ValueSameSet(widthSpinBox.value()))
        
        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | 
                                        QDialogButtonBox.Cancel)
        self.connect(buttonBox, SIGNAL("accepted()"), zoomDialog,
                        SLOT("accept()"))
        self.connect(buttonBox, SIGNAL("rejected()"), zoomDialog,
                        SLOT("reject()"))

        layout = QFormLayout()
        layout.addRow(QLabel("너비: "), widthSpinBox)
        layout.addRow(QLabel("높이: "), heightSpinBox)
        layout.addWidget(sameZoomCheck)
        layout.addWidget(buttonBox)

        zoomDialog.setLayout(layout)
        zoomDialog.setWindowTitle("이미지 확대/축소")

        if zoomDialog.exec_():
            widthZoom = widthSpinBox.value()
            heightZoom = heightSpinBox.value()
            width = self.image.width() * widthZoom / 100
            height = self.image.height() * heightZoom / 100
            image = self.image.scaled(width, height)
            self.imageLabel.setPixmap(QPixmap.fromImage(image))
꽤 긴 코드인데, 클래스 없이 대화 상자를 구성하다보니, 코드가 길어졌다.

먼저, 이미지가 현재 있는지 확인하기 위해서 self.image.isNull() 을 수행했다. 그 다음, 빈 대화상자 객체를 생성하고, 대화 상자에 위젯들을 추가해주었다.

가로 및 세로 확대를 위한 스핀 박스를 추가해주고, 현재 화면에 나타나는 이미지의 확대 비율을 알아내기 위해 pixmap을 다시 image 로 변환한 다음, 이미지의 가로 길이 / 원본 이미지의 가로 길이 * 100을 통해 이미지 비율을 %로 나타내주었다.

그 다음, 동일한 가로, 세로 비율을 위한 체크 박스를 추가해서, 하나의 비율을 조정했을 때, 똑같이 가로, 세로 비율이 움직이도록 해주었다.

그리고 확인, 취소 버튼을 추가해주고, 레이아웃을 지정해주었다. 여기까지가 단순히 대화 상자 배치를 위한 코드(5~49 라인)이다.

이제 대화 상자를 실행시키고, "확인"이 눌렸다면, 스핀 박스로부터 값을 얻어내서 원본 이미지에 scaled 메소드를 사용하여 새롭게 변경된 이미지를 생성해서 이를 레이블에 설정해주었다.

이렇게 하면 원본 이미지의 비율은 변경 없이 그대로 유지가 되어 있다. 그리고 화면에서는 새롭게 변경된 비율의 이미지가 나타나게 된다.


ps. 참고로 3.2.3 에서 확대/축소 변경 문제와 관련된 버그를 수정하였다.

댓글 없음:

댓글 쓰기