본문 바로가기

PyQt GUI

PyQt GUI (19) 윈도우 아이콘 속성창과 동일한 대화창 만들기

728x90

PC에 PC용 카카오톡을 설치하고 바탕화면에 생성되는 카카오톡 아이콘의 속성창을 열었습니다. 왼쪽이 바로 윈도우 기본 속성창 입니다. 오른쪽은 PyQt를 이용하여 최대한 윈도우 속성창에 비슷하게 만든 대화창(QDialog)입니다. 차이가 있긴 하지만, "거의 같다"라고 할 수 있을 정도로 유사한 대화창을 만들었습니다. 이러한 대화창을 만들기 위해서는 기본적으로 동일한 위젯과 레이아웃을 생성하여 배치하는 것도 중요하지만, 위젯과 레이아웃에 다양한 옵션을 설정하여 세세한 부분까지 유사하게 맞추는 작업이 필요합니다. 이번 포스팅에서는 위젯과 레이아웃을 꾸밀 수 있는 설정에 대해서 알아 보도록 하겠습니다. 몇몇 간단한 설정만 해 준다면, 개발을 하려고 하는 벤치마크 프로그램과 매우 유사한 형태의 UI를 만들 수 있습니다. 

 

1. 전체적인 레이아웃 파악하기

 

-벤치마크 하려고 하는 UI의 전체적인 구조를 파악해야 합니다. 간단한 대화창을 만드는 것이니 QDialog를 이용하면 충분합니다. QMainWindow는 필요 없습니다. 

-메인 레이아웃에는 탭(Tab) 위젯이 배치됩니다. 총 6개의 탭이 필요합니다. 

-또한 메인 레이아웃에는 확인, 취소, 적용을 위한 버튼이 필요합니다. 이 버튼들은 하나의 QHBoxLayout에 담을 수 있습니다. 

-메인 레이아웃은 탭 위젯과 버튼을 담고 있는 QHBoxLayout을 세로로 배치하고 있습니다. 따라서 메인 레이아웃은 QVBoxLayout을 사용해야 합니다. 

 

위 전체 구성을 구현하는 코드는 아래와 같습니다. 

 

import sys

# 필요한 라이브러리는 편의상 모두 불러 놓음
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class Main(QDialog): # QDialog로 만들자
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        # 메인 레이아웃 생성
        main_layout = QVBoxLayout()

        # 탭 위젯 생성
        tab = QTabWidget()

        # 버튼 생성 후 button_layout에 배치
        button_layout = QHBoxLayout()
        button_layout.addWidget(QPushButton("확인"))
        button_layout.addWidget(QPushButton("취소"))
        button_layout.addWidget(QPushButton("적용(A)"))

        # 탭 위젯과 button_layout을 메인 레이아웃에 배치
        main_layout.addWidget(tab)
        main_layout.addLayout(button_layout)

        # 메인 레이아웃 설정
        # 창의 크기는 420 x 565로 설정
        self.setLayout(main_layout)
        self.resize(420, 565)
        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = Main()
    sys.exit(app.exec_())

이 코드를 실행하면 아래와 같은 창을 얻을 수 있습니다. 

탭의 내용물(?)을 구성하지 않았고, 확인-취소-적용 버튼의 크기와 위치를 설정하지 않아서 벤치마크 UI와는 다소 차이가 있지만 기능적인 면에서는 동일한 기능을 수행할 수 있는 UI를 생성하였습니다. 

 

2. 필요한 탭 위젯 생성 : 내용물 채우기

벤치마크 UI의 탭 위젯의 내용입니다. 이와 유사한 UI를 구성하기 위해서는

 

-QFormLayout을 사용하면 됩니다.

-FormLayout의 label 값은 파일 형식, 설명, 위치, 크기, 디스크 할당 크기 등이 되며, field값은 바로가기(.lnk), KakaoTalk 등이 됩니다. 

-FormLayout의 행에는 가끔식 회색의 경계선이 있습니다.

-맨 위 부분에는 카카오톡 아이콘 그림과 LineEdit이 있습니다. 

 

를 고려하면 됩니다. 위 내용을 담고 있는 Tab을 만들어야 하는데, Tab에 대해서는 지난 포스팅에서 설명한 바 있습니다. 탭은 아래와 같은 코드로 생성할 수 있습니다. 

    def kakao_icon_label():
    	# 카카오톡 아이콘 이미지를 담고 있는 라벨 생성
        image_label = QLabel()
        pixmap = QPixmap("./kakao_icon.svg")
        pixmap = pixmap.scaledToHeight(35) # 이미지의 높이를 35픽셀로 설정
        image_label.setPixmap(pixmap)
        return image_label

    def makeHLine():
    	# 수평 경계선 생성
        line = QFrame()
        line.setFrameShape(QFrame.HLine)
        line.setFrameShadow(QFrame.Sunken)
        return line

    def return_general_tab(self):
        formlayout = QFormLayout()
        
        image_label = kakao_icon_label()
        lineedit1 = QLineEdit("카카오톡")
        
        formlayout.addRow(image_label, lineedit1)
        formlayout.addRow(makeHLine())
        formlayout.addRow("파일 형식:", QLabel("바로 가기(.lnk)"))
        formlayout.addRow("설명:", QLabel("KakaoTalk"))
        formlayout.addRow(makeHLine())
        formlayout.addRow("위치:", QLabel("C:\\Users\\Public\\Desktop"))
        formlayout.addRow("크기:", QLabel("1.17KB (1,202 바이트)"))
        formlayout.addRow("디스크 할당 크기:", QLabel("4.00KB (4,096 바이트)"))
        formlayout.addRow(makeHLine())
        formlayout.addRow("만든 날짜:", QLabel("‎2021‎년 ‎7‎월 ‎22‎일 ‎목요일, ‏‎오후 2:17:46"))
        formlayout.addRow("수정한 날짜:", QLabel("‎2021‎년 ‎7‎월 ‎22‎일 ‎목요일, ‏‎오후 2:17:46"))
        formlayout.addRow("엑세스한 날짜:", QLabel("‎2021‎년 ‎7‎월 ‎22‎일 ‎목요일, ‏‎오후 2:17:46"))
        formlayout.addRow(makeHLine())

        property = QHBoxLayout()
        property.addWidget(QCheckBox("읽기 전용(R)"))
        property.addWidget(QCheckBox("숨김(H)"))
        property.addWidget(QPushButton("고급(D)..."))
        formlayout.addRow("특성:", property)

        widget = QWidget()
        widget.setLayout(formlayout)

        scroll_area = QScrollArea()
        scroll_area.setWidget(widget)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setWidgetResizable(True)

        return scroll_area

위 코드에서 사용한 위젯들이나 위젯을 사용하는 방법에 대해서는 지난 포스팅에서 설명한 바 있으니, 각 부분 부분이 이해가 안되면 이전 포스팅을 참고하면 됩니다. 

 

return_general_tab()은 scroll_area를 반환하는데, 이 scroll_area 위젯을 메인 탭 위젯에 탭으로 추가하면 됩니다. 메인 탭에 추가하는 것은 

    # 탭 위젯 생성
    tab = QTabWidget()
    general_tab = self.return_general_tab()
    tab.addTab(general_tab, "일반")

와 같습니다. 

 

아마도 "수평 경계선"은 처음 나온것 같긴한데, 위와 같은 방식으로 수평 경계선 makeHLine을 생성할 수 있고, QFormLayout에 추가하여 원하는 위치에 경계선을 추가할 수 있습니다. QFormLayout에 다양한 종류의 정보를 입/출력하는 row가 있을 경우, 비슷한 내용을 담은 row를 구분하기 위해서 수평 경계선을 추가할 수 있습니다. 위 코드를 실행하면 아래의 왼쪽과 같은 대화창을 얻습니다. 오른쪽은 비교를 위한 실제 윈도우의 속성창 입니다. 

 

 

내용적으로만 보면 두 대화창이 "같은 대화창" 이라고 볼 수 있으나, 비쥬얼의 측면에서 본다면 두 대화창은 아직 많이 다릅니다. 다른 것들을 하나 하나 언급하면,

 

-전체적인 색이 다릅니다. 윈도위 속성창의 탭은 흰색 배경이지만, 현재까지 만든 대화창의 탭의 배경은 회색입니다. 

-QFormLayout의 각 row의 간격이 서로 다릅니다. 

-하나의 row에서 key값과 label값의 간격이 다릅니다. 

-맨 아래 확인-취소-적용 버튼의 크기, 여백, 배치가 다릅니다. 

-카카오톡 아이콘의 크기와 위치가 약간 다릅니다. 또한 "카카오톡"을 표현하는 LineEdit의 크기가 다릅니다.

 

입니다. 이것들은 위젯의 위치를 하나 하나 조정하고, 위젯을 꾸미는 것과 관련이 있는데, 이제 부터는 위젯을 꾸미는 것을 해 보도록 하겠습니다. 

 

탭의 배경색을 변경하기 위해서는 탭에 설정된 팔레트 속성을 변경하면 됩니다. 아래와 같은 팔레트 설정을 하면 탭의 배경색을 흰색(투명한 색)으로 변경할 수 있습니다. 

        pal = tab.palette() ### tab의 팔레트 속성
        pal.setColor(QPalette.Window, Qt.transparent) ### 팔레트의 색상 지정
        tab.setPalette(pal) ### 생상을 지정한 팔레트를 텝의 팔레트로 설정

이 세 줄을 메인 코드의 tab을 생성한 이후에 추가하면 됩니다. PyQt의 QWidget에는 palette라는 속성이 있는데, 이 속성은 위젯의 색을 바꿀 때 사용할 수 있습니다. QTab 위젯은 QWidget을 상속한 것으로 당연히 QTab에도 palette 속성이 있습니다. Palette에 대해서는 다음 포스팅에서 더 디테일하게 다루도록 하겠습니다. 

 

QFormLayout에서 row와 row의 간격 혹은 하나의 row에서 key와 field의 간격은 각각 setHorizontalSpacing과 setVerticalSpacing을 통해 설정할 수 있습니다. 윈도우 속성창에서 수평 방향 간격과 수직 방향 간격에 어떤 값을 사용했는지는 알 수 없으나, Trial & Error를 통해서 가장 비슷한 결과를 보여주는 두 값을 찾았습니다. 아래와 같은 두 라인을 return_general_tab에서 formlayout을 생성한 다음 줄에 넣으면 됩니다. 

        formlayout.setHorizontalSpacing(45)
        formlayout.setVerticalSpacing(15)

 

윈도우 속성 창의 맨 아래 확인-취소-속성은 오른쪽에 몰려 있습니다. 하지만 위에서 만든 대화창에서 확인-속성-취소는 양쪽 정렬로 배치 돼 있고, 그 크기는 대화창의 가로 길이의 1/3 씩을 차지하고 있습니다. 이를 윈도우 속성 창 처럼 만들기 위해서는 레이아웃 내에서 위젯들의 위치를 설정해야 하는데, 이 경우에는 setAlignment 를 사용할 수 있습니다. 위 메인 코드에서 button_layout을 생성하고 버튼들을 배치한 다음에 아래와 같은 한 줄을 추가하면 됩니다. 

        button_layout.setAlignment(Qt.AlignRight)

 

맨 위에 나오는 "카카오톡"이 써져있는 LineEdit의 크기와 위치를 또 윈도우 속성 창의 크기와 위치에 맞게 변경해야 합니다. 이는 위젯의 크기와 위치를 조정하는 간단한 코딩인데, lineedit1를 생성한 다음에 아래 세 줄을 추가 하면 됩니다. 

        lineedit1.setFixedHeight(27)
        lineedit1.setTextMargins(0, 5, 0, 0)
        lineedit1.setAlignment(Qt.AlignTop)

27, 5와 같은 디테일한 숫자는 최대한 윈도우 속성 창과 유사하도록 하게 하는 숫자 입니다. 

 

때먹은 것이 있는데, 대화창 맨 윗 부분에 나오는 제목과 맨 윗 부분 왼쪽에 나오는 아이콘을 설정하지 않았습니다. initUI의 맨 아랫부분에 다음의 두 줄을 추가하면 됩니다.

        self.setWindowTitle("카카오톡 속성")
        self.setWindowIcon(QIcon("./kakao_icon.svg"))

 

여기까지 생성한 대화창을 실행하면 아래와 같습니다. 비교를 위해서 오른쪽에는 실제 윈도우 속성 창을 캡쳐하였습니다. 

"똑같다"라고는 할 수 없지만, "거의 똑같다" 라고 정도는 할 수 있는 수준입니다. 실제 윈도우 속성 창을 만들었을 때의 정확한 코딩과 폰트, 사이즈 등을 알지 못하기 때문에 완전히 똑같게 만들기는 사실 불가능합니다. 아직 다른 부분이 좀 있어서 개선할 여지는 있지만, 약간의 개선을 위해서 많은 코딩이 필요하기 때문에 "일반" 탭을 생성하는 것은 여기서 마치도록 하겠습니다. 추가적으로 더 유사하게 만들고 싶으신 분은 CSS StyleSheet등을 이용하여 더 유사하게 만들 수 도 있습니다. 

 

다음 포스팅에서는 "바로 가기", "호완성" 등 다른 탭도 만들어 보도록 하겠습니다. 아래는 전체 코드 입니다. 

import sys

# 필요한 라이브러리는 편의상 모두 불러 놓음
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

def kakao_icon_label():
    image_label = QLabel()
    pixmap = QPixmap("./kakao_icon.svg")
    pixmap = pixmap.scaledToHeight(35)
    image_label.setPixmap(pixmap)
    return image_label

def makeHLine():
    line = QFrame()
    line.setFrameShape(QFrame.HLine)
    line.setFrameShadow(QFrame.Sunken)
    #line.setStyleSheet("border:gray")
    return line

class Main(QDialog): # QDialog로 만들자
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        # 메인 레이아웃 생성
        main_layout = QVBoxLayout()

        # 탭 위젯 생성
        tab = QTabWidget()
        general_tab = self.return_general_tab()
        tab.addTab(general_tab, "일반")

        # 버튼 생성 후 button_layout에 배치
        button_layout = QHBoxLayout()
        button_layout.addWidget(QPushButton("확인"))
        button_layout.addWidget(QPushButton("취소"))
        button_layout.addWidget(QPushButton("적용(A)"))
        button_layout.setAlignment(Qt.AlignRight)

        pal = tab.palette()
        pal.setColor(QPalette.Window, Qt.transparent)
        tab.setPalette(pal)

        # 탭 위젯과 button_layout을 메인 레이아웃에 배치
        main_layout.addWidget(tab)
        main_layout.addLayout(button_layout)

        # 메인 레이아웃 설정
        # 창의 크기는 420 x 565로 설정
        self.setLayout(main_layout)
        self.setWindowTitle("카카오톡 속성")
        self.setWindowIcon(QIcon("./kakao_icon.svg"))
        self.resize(420, 565)
        self.show()


    def return_general_tab(self):
        formlayout = QFormLayout()
        formlayout.setHorizontalSpacing(45)
        formlayout.setVerticalSpacing(15)

        image_label = kakao_icon_label()
        lineedit1 = QLineEdit("카카오톡")
        lineedit1.setFixedHeight(27)
        lineedit1.setTextMargins(0, 5, 0, 0)
        lineedit1.setAlignment(Qt.AlignTop)

        formlayout.addRow(image_label, lineedit1)
        formlayout.addRow(makeHLine())
        formlayout.addRow("파일 형식:", QLabel("바로 가기(.lnk)"))
        formlayout.addRow("설명:", QLabel("KakaoTalk"))
        formlayout.addRow(makeHLine())
        formlayout.addRow("위치:", QLabel("C:\\Users\\Public\\Desktop"))
        formlayout.addRow("크기:", QLabel("1.17KB (1,202 바이트)"))
        formlayout.addRow("디스크 할당 크기:", QLabel("4.00KB (4,096 바이트)"))
        formlayout.addRow(makeHLine())
        formlayout.addRow("만든 날짜:", QLabel("‎2021‎년 ‎7‎월 ‎22‎일 ‎목요일, ‏‎오후 2:17:46"))
        formlayout.addRow("수정한 날짜:", QLabel("‎2021‎년 ‎7‎월 ‎22‎일 ‎목요일, ‏‎오후 2:17:46"))
        formlayout.addRow("엑세스한 날짜:", QLabel("‎2021‎년 ‎7‎월 ‎22‎일 ‎목요일, ‏‎오후 2:17:46"))
        formlayout.addRow(makeHLine())

        property = QHBoxLayout()
        property.addWidget(QCheckBox("읽기 전용(R)"))
        property.addWidget(QCheckBox("숨김(H)"))
        property.addWidget(QPushButton("고급(D)..."))
        formlayout.addRow("특성:", property)

        widget = QWidget()
        widget.setLayout(formlayout)

        scroll_area = QScrollArea()
        scroll_area.setWidget(widget)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setWidgetResizable(True)

        return scroll_area




if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = Main()
    sys.exit(app.exec_())
728x90