본문 바로가기

PyQt GUI

PyQt GUI (6) : GUI창의 크기에 따라 위젯의 크기가 자동으로 커지게 하기

728x90

https://www.youtube.com/watch?v=3-ugvvigZ8A

지난 포스팅 <PyQt GUI (5) : 100줄만에 계산기 만들기>에서 계산기 어플리케이션을 만들어 보았습니다.

위 이미지와 같이 우리의 계산기는 단순한데요, 경우에 따라서는 창의 크기를 더 크거나 작게 할 필요가 있습니다. 그러나 우리의 계산기는 크기를 변경할 때, 문제가 2개가 있는데

 

(1) 계산기의 크기를 줄이려고 해도 작아지지 않는다

(2) 계산기의 크기를 크게 하면 수식/정답 입력 LineEdit과 사칙연산 버튼 사이에 공백이 생긴다

 

입니다. 실제로 계산기 창의 크기를 크게 하면, 

위 이미지와 같이 Solution 과 사칙연산 버튼 사이에 공간이 생긴다는 것을 볼 수 있습니다. 전체 창의 크기는 커졌지만(특히 세로방향으로), Equation/Solution LineEdit이나 버튼들은 크기가 세로방향으로 커지지 않아서, 창의 크기가 커진만큼 공백이 커진 것 입니다. 

 

우리가 특별히 계산기를 꾸미진 않았지만, 아무리 꾸미지 않았다고 하더라도 이렇게 모양이 이상하면 사용을 하는데 기분이 나쁩니다. 왜 이런 현상이 나타났는고하니... 우리는 위젯의 크기에 대해서는 아무런 설정을 하지 않았기 때문입니다. 

 

모든 위젯들은 기본 사이즈에 대한 정책(Size Policy)이 있습니다. 정책을 설정하지 않으면 기본값이 적용이 되고, 만일 위젯이 레이아웃에 포함 돼 있다면, 위젯을 포함한 레이아웃의 사이즈 정책을 따르게 됩니다. 위젯들의 기본 사이즈 정책을 알아 보도록 하겠습니다. 

pushbutton = QPushButton("button")
print("sizeHint :", pushbutton.sizeHint())
print("verticalPolicy :", pushbutton.sizePolicy().verticalPolicy())
print("horizontalPolicy :", pushbutton.sizePolicy().horizontalPolicy())

위와 같이 그냥 QPushButton 하나를 만들고,

 

.sizeHint()를 통해서 위젯의 사이즈 힌트

.sizePolicy().verticalPolicy()를 통해서 위젯의 세로방향 정책을

.sizePolicy().horizontalPolicy()를 통해서 위젯의 가로방향 정책을

 

출력하면 결과는 다음과 같습니다. 

sizeHint : PyQt5.QtCore.QSize(75, 23)
verticalPolicy : 0
horizontalPolicy : 1

즉, 우리가 위젯의 크기에 대해서 아무런 설정을 하지 않을 경우, 위젯의 "기본 크기"는 75px x 23px로 설정됩니다. 

따라서 레이아웃을 생성하고, 그 레이아웃에 QPushButton을 추가하고 프로그램을 실행하면 위와 같이 매우 작은 창이 뜨는데요, 위 창에서 QPushButton의 크기는 정확히 75px x 23px 사이즈가 됩니다. 

창의 크기를 가로, 세로 방향으로 크게 하면 가로 방향으로는 창의 크기만큼 위젯(버튼)의 크기가 커지지만, 세로 방향으로는 위젯의 크기가 커지지 않고 처음의 크기 그대로 고정 돼 있습니다. 이렇게, "창(레이아웃)의 크기에 따라서 위젯의 크기가 어떻게 변화하는가"는 sizePolicy().verticalPolicy(), sizePolicy().horizontalPolicy()를 통해서 확인할 수 있는데, 위에서 출력한 것과 같이 QPushButton 위젯의 경우 기본값은 각각 0, 1 입니다. 

위 표는 PyQt QsizePolicy를 설명하는 페이지에서 가져온 것 입니다. 

 

value = 0은 Fixed, value = 1는 Minimum으로, value = 0인 경우 위젯의 sizeHint()에 따라 위젯의 크기가 정해지고, value = 1의 경우 sizeHint()를 반영하긴 하지만 위젯의 크기는 커질 수 있습니다. 예시에서 PushButton을 언급했네요. QPushButton은 위와 같이 기본 설정이 돼 있기 때문에, 창(레이아웃)의 크기의 변화에 따라서 그렇게 반응한 것 입니다. 

 

따라서 창(레이아웃)의 크기에 따라서 위젯의 크기가 같이 변하게 하려면, setSizePolicy()를 통해서 위젯의 사이즈 정책을 설정해 주어야 합니다. 우리가 원하는 정책은 창의 크기가 커짐에 따라서 위젯들이 창을 공백 없이 가득 채우는 것이기 때문에 Expanding이 가장 적절합니다. 따라서 위 코드에서

pushbutton.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

과 같이 pushbutton의 사이즈 정책을 설정할 수 있습니다. 이 경우 창의 크기에 따라서 PushButton이 창을 가득 채우면서 함께 커짐을 확인할 수 있습니다. 

네, 우리가 원하는 대로 됐습니다. 이제 그러면 계산기 어플리케이션의 모든 위젯에 위와 같이 사이즈 정책을 정의 해 주면 됩니다. 

 

그런데, 모든 위젯에 하나하나 

.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

를 적용하기란 매우 귀찮은 일 인데요, 이 경우에는 클래스 상속을 통해서 새로운 위젯 클래스를 만들고, 이 위젯 클래스에 위와 같은 사이즈 정책을 적용하는 방법이 있습니다. 예를들어서 QPushButton을 생성할 때, 기본적으로 위와 같은 사이즈 정책을 적용하기 위해서는 Custom 된 QPushButton을 아래와 같이 생성하면 됩니다. 

class QPushButton(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

새롭게 생성된 QPushButton의 이름을 기존의 QPushButton과 동일하게 했기 때문에, 기존의 QPushButton을 "덮어쓰기" 했다고도 볼 수 있습니다. 위 코드에서

self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

를 추가했기 때문에 QPushButton으로 생성되는 PushButton은 일괄적으로 레이아웃의 크기에 따라 가로, 세로 방향의 크기가 커지게 됩니다. 

 

우리의 계산기 코드를 다시 실행 한 뒤, 창의 크기를 크게 하면, 

와 같이 창의 크기가 커짐에 따라서 PushButton들이 함께 커져서 공백 없이 레이아웃 전체를 채우고 있음을 확인할 수 있습니다. 위와 같은 수정을 QLineEdit과 QLabel에도 할 수 있습니다. 

이 경우, 위 그림과 같이 Equation, Solution의 LineEdit역시 세로방향 길이가 길어진 것을 확인 할 수 있습니다. 

 

한 가지 불만족스러운 것은 사칙연산 버튼에 비해서 숫자 버튼의 크기가 커지지 않았다는 것 인데요, 이 이유는 레이아웃간 크기가 커지는 비율을 특별히 설정하지 않았기 때문입니다. 사칙연산 버튼은 하나의 QHBoxLayout으로 돼 있고, 숫자 버튼은 하나의 QGridLayout으로 돼 있는데, 두 레이아웃이 세로방향으로 커지는 비율이 같게 설정 돼 있습니다. 이 경우, GridLayout의 버튼의 (세로방향) 갯수가 4개이기 때문에, 숫자 버튼이 커지는 비율은 사칙연산 버튼이 커지는 비율의 1/4 밖에 되지 않습니다. 

 

따라서, 레이아웃이 커지는 비율을 설정해 주어야 하는데요, 이는 

        main_layout.addLayout(layout_equation_solution, stretch = 1)
        main_layout.addLayout(layout_operation, stretch = 1)
        main_layout.addLayout(layout_clear_equal, stretch = 1)
        main_layout.addLayout(layout_number, stretch = 4)

와 같이 각 레이아웃을 main_layout에 추가할 때 stretch = 비율 을 통해서 설정할 수 있습니다. 위와 같이 설정하는 경우 각 레이아웃이 커지는 비율은 1 : 1 : 1 : 4로, 위 세 레이아웃이 1만큼 커질 때, 맨 아래 숫자 버튼을 담고 있는 레이아웃은 4만큼 커지게 됩니다. 숫자 버튼을 담고 있는 레이아웃은 실제로는 세로방향으로 4개의 버튼을 담고 있기 때문에, 버튼으로만 본다면 모든 버튼이 같은 비율로 커지게 되는 것 입니다. 

 

코드를 위와 같이 수정하고 프로그램을 실행한 뒤, 창의 크기를 크게 하면, 

와 같이 모든 버튼의 세로 방향 크기가 같게 커짐을 확인할 수 있습니다. 위 이미지만 봐서는 Equation / Solution 입력 LineEdit의 세로 방향 크기가 조금 작은것 같은데, 원하는 경우에는 stretch 값을 좀 더 크게 해서 비율을 조정할 수 있습니다. 

 

이번 포스팅에서는 가장 간단한 형태의 사이즈 정책에 대해서 알아보았습니다. 실제로 사이즈 정책은 매우 다양하기 때문에, 우리가 정확히 원하는 형태의 사이즈 정책을 설정하기 위해서는 좀 더 디테일한 메뉴얼을 확인해야 할 필요가 있습니다. https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum 와 같이 공식 Documentation 사이트를 참고하는 것이 좋습니다. 

 

코드 수정 이후, 최종 버전의 코드는 아래와 같습니다. 

import sys
from PyQt5.QtWidgets import *

### 사이즈 정책을 설정한 새로운 class를 생성합니다. ###
class QPushButton(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

class QLabel(QLabel):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

class QLineEdit(QLineEdit):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

class Main(QDialog):
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        main_layout = QVBoxLayout()

        ### 각 위젯을 배치할 레이아웃을 미리 만들어 둠
        layout_operation = QHBoxLayout()
        layout_clear_equal = QHBoxLayout()
        layout_number = QGridLayout()
        layout_equation_solution = QFormLayout()

        ### 수식 입력과 답 출력을 위한 LineEdit 위젯 생성
        label_equation = QLabel("Equation: ")
        label_solution = QLabel("Solution: ")
        self.equation = QLineEdit("")
        self.solution = QLineEdit("")

        print(self.equation.sizeHint())
        print(self.equation.sizePolicy().horizontalPolicy())

        ### layout_equation_solution 레이아웃에 수식, 답 위젯을 추가
        layout_equation_solution.addRow(label_equation, self.equation)
        layout_equation_solution.addRow(label_solution, self.solution)

        ### 사칙연상 버튼 생성
        button_plus = QPushButton("+")
        button_minus = QPushButton("-")
        button_product = QPushButton("x")
        button_division = QPushButton("/")

        ### 사칙연산 버튼을 클릭했을 때, 각 사칙연산 부호가 수식창에 추가될 수 있도록 시그널 설정
        button_plus.clicked.connect(lambda state, operation = "+": self.button_operation_clicked(operation))
        button_minus.clicked.connect(lambda state, operation = "-": self.button_operation_clicked(operation))
        button_product.clicked.connect(lambda state, operation = "*": self.button_operation_clicked(operation))
        button_division.clicked.connect(lambda state, operation = "/": self.button_operation_clicked(operation))

        ### 사칙연산 버튼을 layout_operation 레이아웃에 추가
        layout_operation.addWidget(button_plus)
        layout_operation.addWidget(button_minus)
        layout_operation.addWidget(button_product)
        layout_operation.addWidget(button_division)

        ### =, clear, backspace 버튼 생성
        button_equal = QPushButton("=")
        button_clear = QPushButton("Clear")
        button_backspace = QPushButton("Backspace")

        ### =, clear, backspace 버튼 클릭 시 시그널 설정
        button_equal.clicked.connect(self.button_equal_clicked)
        button_clear.clicked.connect(self.button_clear_clicked)
        button_backspace.clicked.connect(self.button_backspace_clicked)

        ### =, clear, backspace 버튼을 layout_clear_equal 레이아웃에 추가
        layout_clear_equal.addWidget(button_clear)
        layout_clear_equal.addWidget(button_backspace)
        layout_clear_equal.addWidget(button_equal)

        ### 숫자 버튼 생성하고, layout_number 레이아웃에 추가
        ### 각 숫자 버튼을 클릭했을 때, 숫자가 수식창에 입력 될 수 있도록 시그널 설정
        number_button_dict = {}
        for number in range(0, 10):
            number_button_dict[number] = QPushButton(str(number))
            number_button_dict[number].clicked.connect(lambda state, num = number:
                                                       self.number_button_clicked(num))
            if number >0:
                x,y = divmod(number-1, 3)
                layout_number.addWidget(number_button_dict[number], x, y)
            elif number==0:
                layout_number.addWidget(number_button_dict[number], 3, 1)

        ### 소숫점 버튼과 00 버튼을 입력하고 시그널 설정
        button_dot = QPushButton(".")
        button_dot.clicked.connect(lambda state, num = ".": self.number_button_clicked(num))
        layout_number.addWidget(button_dot, 3, 2)

        button_double_zero = QPushButton("00")
        button_double_zero.clicked.connect(lambda state, num = "00": self.number_button_clicked(num))
        layout_number.addWidget(button_double_zero, 3, 0)

        ### 각 레이아웃을 main_layout 레이아웃에 추가
        main_layout.addLayout(layout_equation_solution, stretch=1)
        main_layout.addLayout(layout_operation, stretch=1)
        main_layout.addLayout(layout_clear_equal, stretch=1)
        main_layout.addLayout(layout_number, stretch=4)

        self.setLayout(main_layout)
        self.show()

    #################
    ### functions ###
    #################
    def number_button_clicked(self, num):
        equation = self.equation.text()
        equation += str(num)
        self.equation.setText(equation)

    def button_operation_clicked(self, operation):
        equation = self.equation.text()
        equation += operation
        self.equation.setText(equation)

    def button_equal_clicked(self):
        equation = self.equation.text()
        solution = eval(equation)
        self.solution.setText(str(solution))

    def button_clear_clicked(self):
        self.equation.setText("")
        self.solution.setText("")

    def button_backspace_clicked(self):
        equation = self.equation.text()
        equation = equation[:-1]
        self.equation.setText(equation)

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