본문 바로가기
공부/python

[pyqt] 스크롤 영역 구현하기

by 고기 2023. 12. 25.

예전에 python ui 만들때는 간단하게 tkinter로 구현했었는데 이번에 만들 프로그램은 pyqt를 사용해서 구현해봤다.

 

pyqt를 사용한 이유는 프로그램 ui 구상 단계에서 tkinter로 구현할 수 없는 기능이 포함되어 있었던 것 같기도 한데... 만들다 보니까 그게 뭐였는지 까먹었단 말이지...?

그래서 지금와서 생각해보면 tkinter로 만들었으면 훨씬 빨리 만들었을 것 같기도 하고...

뭐, pyqt라는걸 사용해봤다는 것에 의의를 두자.

 

아무튼 pyqt를 사용해서 ui를 구현하면서 가장 어려웠던 게 스크롤 영역이다.

어차피 나중에 쓸 일 있으면 복붙하면 되니까 적당히 코드만 올려놔도 상관없겠지.

 

먼저 기본적인 스크롤 영역 생성하는 예제다.

'''
[99] scroll test 스크롤 예제 ok
'''
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QFrame, QVBoxLayout, QLabel, QWidget, QPushButton, QScrollArea, QGridLayout
from PyQt5.QtGui import QColor
import random

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.initUI()

    def initUI(self):
        # main window 설정
        self.setGeometry(100, 100, 1000, 800)
        self.setStyleSheet("background-color: #f0e4e9;")

        # frame_a 생성 및 스타일 설정
        frame_a = QFrame(self)
        frame_a.setGeometry(10, 10, 1000, 800)
        frame_a.setStyleSheet("background-color: #d0e6ff; border-radius: 15px;")

        # frame_b 생성 및 스타일 설정 (frame_a의 하위 프레임)
        frame_b = QFrame(frame_a)
        frame_b.setGeometry(100, 100, 800, 300)
        frame_b.setStyleSheet("background-color: #c4fff9; border-radius: 15px;")

        # QScrollArea 생성
        scroll_area = QScrollArea(frame_b)
        scroll_area.setGeometry(0, 0, 800, 300)

        # 스크롤 가능한 컨테이너 위젯 생성
        scroll_content = QWidget()
        scroll_area.setWidget(scroll_content)

        # 컨테이너 위젯에 격자 레이아웃 추가
        layout = QGridLayout(scroll_content)

        # 스크롤을 테스트하기 위한 내용 추가 (50개의 frame)
        for i in range(50):
            # 무작위 색상 생성
            color = QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
            # frame 생성 및 스타일 설정
            frame = QFrame()
            frame.setFixedSize(50, 50)
            frame.setStyleSheet(f"background-color: {color.name()};")
            layout.addWidget(frame, i // 5, i % 5)  # 격자 위치 설정

        # 스크롤 영역의 크기를 컨텐츠에 맞게 조정
        scroll_content.resize(scroll_content.sizeHint())
        
        # 수직 스크롤바 숨김
        scroll_area.setVerticalScrollBarPolicy(1)  # 1은 숨김을 의미

def main():
    app = QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

 

코드를 실행하면 50개의 위젯이 배치되며 위젯의 배치는 가로 5열씩 n줄로 설정했다.

 

내가 하고싶었던 것은 저 배치된 위젯의 내용이 변경되면 업데이트해서 보여주는 건데 그 과정이 꽤 힘들었단 말이지.

 

코드에서는 scroll_area라는 영역에 scroll_content를 보여주고 있다.

scroll_content에는 layout이라는 그리드 레이아웃을 사용해서 frame 위젯들을 격자 모양으로 배치허고 있다.

 

여기까지 봤을 때, 그냥 일반적으로 scroll_area의 내용을 변경하려면 그냥 scroll_content를 초기화 하면 되잖아?

scroll_content를 초기화해서 다시 scroll_area에 집어넣어주면 될 거라고 생각했다.

scroll_content = QWidget()
...

 

결론부터 말하면 안된다.

pyqt에서는 위젯에 포함된 자식 위젯이 있으면 부모 위젯이 삭제가 안된다나?

그럼 scroll_content의 자식 위젯들은 어떻게 삭제하는지 찾아보니 아래처럼 위젯 수만큼 하나씩 삭제해줘야 한다...네?

for i in reversed(range(self.layout.count())):
	self.layout.itemAt(i).widget().setParent(None)

 

어쩄든 우여곡절 끝에 자식 위젯을 삭제했다.

그럼 이제 다시 처음 하려고 했던 scroll_content를 삭제하고 scroll_area에 새로운 content를 붙이려고 했다.

 

처음에 높이가 100이고 5개의 위젯이 4열로 총 20개 위젯이 배치되어 있었고,

위젯의 개수가 늘어나서 5개의 위젯을 6열로 30개 위젯을 배치하라고 시키면 자동으로 높이를 늘려서 자연스러운 스크롤 영역을 구현해 줄 것이라고 기대했는데...

 

여전히 안된다.

역시 정확하게 왜 그런건지는 모르겠지만... scroll_area의 size가 자동으로 변경되지 않는 문제가 있다.

아래의 sizeHint() 가 변경된 레이아웃의 크기를 반영하는 게 아니라 이전에 계산된 크기를 그대로 반영한다고 하는데.

# 스크롤 영역의 크기를 컨텐츠에 맞게 조정
scroll_content.resize(scroll_content.sizeHint())

 

음 그러니까... 설명이 어렵네...

이렇게 된다.

 

이 문제를 해결하려면 scroll_area의 resizeable 속성을 활성화해주면 된다.

그리고 resize 속성을 활성화했으면 sizeHint() 메서드는 필요없으니 지워주자. 

# 자동으로 자식 위젯의 크기에 맞게 조절
scroll_area.setWidgetResizable(True)

 

먼저 구현이 완료된 코드는 이렇다.

'''
[99] scroll update 로직 ok
'''
from PyQt5.QtWidgets import QMainWindow, QApplication, QFrame, QPushButton, QScrollArea, QWidget, QGridLayout
from PyQt5.QtGui import QColor
import sys
import random

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.initUI()

    def initUI(self):
        self.setGeometry(100, 100, 1000, 800)
        self.setStyleSheet("background-color: #f0e4e9;")

        frame_a = QFrame(self)
        frame_a.setGeometry(10, 10, 1000, 800)
        frame_a.setStyleSheet("background-color: #d0e6ff; border-radius: 15px;")

        self.button = QPushButton('Change Content', frame_a)
        self.button.setGeometry(100, 400, 200, 50)
        self.button.clicked.connect(self.change_content)

        frame_b = QFrame(frame_a)
        frame_b.setGeometry(100, 100, 800, 300)
        frame_b.setStyleSheet("background-color: #c4fff9; border-radius: 15px;")

        self.scroll_area = QScrollArea(frame_b)
        self.scroll_area.setGeometry(0, 0, 500, 300)
        self.scroll_area.setWidgetResizable(True)

        self.scroll_content = QWidget()
        self.scroll_area.setWidget(self.scroll_content)

        self.layout = QGridLayout(self.scroll_content)
        self.cnt = 10

    def add_frames(self):
        for i in range(20):
            color = QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
            frame = QFrame()
            frame.setFixedSize(50, 50)
            frame.setStyleSheet(f"background-color: {color.name()};")
            self.layout.addWidget(frame, i // 5, i % 5) 

    def change_content(self):
        for i in reversed(range(self.layout.count())):
            self.layout.itemAt(i).widget().setParent(None)

        for i in range(self.cnt):
            color = QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
            frame = QFrame()
            frame.setFixedSize(50, 50)
            frame.setStyleSheet(f"background-color: {color.name()};")
            self.layout.addWidget(frame, i // 5, i % 5) 
    
        self.cnt += 5
        
        widget_count = self.layout.count()
        rows = (widget_count + 4) // 5 
        spacing = 10 
        self.layout.setVerticalSpacing(spacing)
        self.layout.setHorizontalSpacing(spacing)
        self.layout.setRowStretch(rows, 1)  

        self.layout.activate()
        self.scroll_area.ensureWidgetVisible(self.scroll_content)
        self.updateGeometry()
        
def main():
    app = QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

 

테스트를 위해 change content 버튼을 클릭하면 5n개의 위젯이 스크롤 영역에 업데이트 되도록 했다.

5개 -> 10개 -> 15개 -> ... 이런식이다.

 

사진으로만 보면 5개를 추가하고 그 다음 5개를 추가하는 식으로 보일텐데,

실제로는 5개 추가 -> 5개 제거 후 10개 추가 -> 10개 제거 후 15개 추가 -> ... 이런식이다.

 

마지막으로 resize option만 추가했을 때 위젯이 업데이트 될 때 공간이 scroll_area의 size에 맞춰서 자동으로 정렬되는 문제가 있다. 자동으로 정렬이라고 하면 듣기 좋지만 문제는 내가 원하는 정렬이 아니라는 점이지.

 

아래 사진과 같이 이상하게 정렬된다.

 

위에서부터 일렬로 차근차근 정렬되는 게 아니라 가운데 정렬이 되기 때문에 수직 간격을 조절하고 마지막 행이 빈 공간을 채우지 않도록 아래 코드를 추가해주면 된다.

widget_count = layout.count()
rows = (widget_count + 4) // 5 
spacing = 10 
layout.setVerticalSpacing(spacing)
layout.setHorizontalSpacing(spacing)
layout.setRowStretch(rows, 1)  

layout.activate()
scroll_area.ensureWidgetVisible(self.scroll_content)
updateGeometry()

 

끝!

댓글