2) PyQt 복습하기
이번 절에서는 12.2절에서 설명한 PyQt 기초를 복습하겠습니다. PyQt가 잘 기억나지 않는다면 잠깐 12.2절을 먼저 읽어보기 바랍니다.
PyQt는 따로 한 권 분량의 책이 있을 만큼 복잡하고 규모가 큰 라이브러리입니다. 따라서 이 책에서 PyQt를 모두 다룰 수는 없습니다. 대신 알고리즘 트레이딩 프로그램 UI를 만들 수 있을 만큼만 공부하겠습니다.
PyQt는 분명히 복잡한 라이브러리이긴 하지만 PyQt 기반의 GUI 프로그램의 핵심 구성 요소는 그림 16.4와 같이 정리할 수 있습니다. 먼저 화면에 출력되는 UI 부분이 있습니다. 이는 PyQt가 기본적으로 제공하는 위젯(Widget) 클래스의 객체를 생성해서 만들 수 있습니다. 수많은 위젯 중에서 여러분이 사용할 위젯을 선택하고 각 위젯 클래스의 사용법을 익혀야 합니다.
두 번째는 이벤트 루프입니다. PyQt에서는 QApplication 객체에서 exec_ 메서드를 호출해 이벤트 루프를 생성합니다. 여러분은 이벤트 루프를 생성만 할 뿐 실제로 이벤트 루프에서 여러분이 관여하는 일은 많지 않습니다.
세 번째는 이벤트를 처리할 함수 또는 메서드를 구현하는 부분입니다. 버튼과 같은 위젯을 클릭하면 해당 위젯은 'clicked'라는 시그널(signal)을 발생시킵니다. 여러분은 'clicked'라는 시그널이 발생했을 때 호출되는 함수 또는 메서드를 구현해야 합니다. PyQt에서는 특정 시그널이 발생했을 때 호출되는 함수 또는 메서드를 슬롯(slot)이라고 합니다. 참고로 다른 프로그래밍 언어에서는 콜백 함수(callback function)라고 부르기도 합니다.
PyQt에서는 위젯에서 발생하는 시그널에 대해 어떤 슬롯으로 처리할지에 대해 미리 등록함으로써 특정 위젯에서 시그널이 발생했을 때 이벤트 루프가 미리 연결된 슬롯을 자동으로 호출하게 돼 있습니다.
GUI 프로그램의 전반적인 실행 흐름에 대해 정리해 보겠습니다. 그림 16.4에서 사용자가 버튼 위젯을 클릭하면 'clicked'라는 시그널이 발생합니다. GUI 프로그램의 시작과 함께 생성돼 있던 이벤트 루프는 해당 시그널에 등록된 슬롯을 호출함으로써 사용자가 발생시킨 이벤트를 처리합니다. 이벤트 루프는 사용자가 프로그램을 종료한다는 시그널을 보내기 전까지는 계속해서 이와 같은 방식으로 시그널과 연결된 슬롯을 호출함으로써 이벤트를 처리하는 역할을 합니다.
그림 16.4 PyQt의 UI-시그널-슬롯-이벤트 루프의 관계
PyQt의 전반적인 동작 방식을 복습했으니, 이번에는 간단한 코드를 통해 이벤트 루프를 생성하는 QApplication 객체를 살펴보겠습니다. 앞서 설명한 것처럼 그림 16.4의 이벤트 루프는 QApplication 클래스의 객체를 생성한 후 exec_ 메서드를 호출하는 순간 생성됩니다.
한번 생성된 이벤트 루프는 사용자가 윈도우를 닫을 때까지 실행되면서 위젯에서 발생한 시그널을 처리하는 슬롯을 호출하는 역할을 합니다. 이벤트 루프의 생성과 종료 시점을 확인해보기 위해 다음과 같이 코드를 작성해 보겠습니다.
import sys
from PyQt5.QtWidgets import *
app = QApplication(sys.argv)
label = QLabel("Hello, PyQt")
label.show()
print("Before event loop")
app.exec_()
print("After event loop")
작성된 코드에서는 이벤트 루프를 생성하는 코드의 앞쪽과 뒤쪽에 문자열이 출력되게 했습니다. 코드를 실행하면 그림 16.5와 같이 QLabel 위젯이 나타나고 콘솔 창에는 'Before event loop'라는 문자열이 출력됩니다.
여기서 한 가지 중요한 점은 파이썬 코드는 파일의 위에서부터 아래쪽으로 순차적으로 실행되는데 app.exec_ 앞쪽의 코드는 실행됐지만 그다음 줄의 코드는 아직 실행되지 않았다는 점입니다. 이는 exec_ 메서드가 호출되면서 이벤트 루프가 생성됐고 이 때문에 프로그램이 계속해서 이벤트 루프 안에서 실행되고 있기 때문입니다.
그림 16.5 이벤트 루프 생성(소스 코드: book/ch16/01.py)
이제 화면에 출력된 QLabel 위젯을 닫아 보겠습니다. 윈도우의 'X' 부분을 눌러 QLabel 위젯을 종료하면 exec_ 메서드 호출 시점에 생성됐던 이벤트 루프가 종료되면서 그다음 줄의 코드가 실행됩니다. 따라서 그림 16.6과 같이 콘솔 창에 'After event loop'라는 문자열이 출력됨을 확인할 수 있습니다.
그림 16.6 이벤트 루프 종료
이번에는 시그널과 슬롯을 연결하는 부분에 대한 코드를 작성해 보겠습니다. PyQt는 위젯의 종류에 따라 발생 가능한 기본 시그널이 정의돼 있습니다. 예를 들어, QPushButton 위젯은 마우스 클릭을 했을 때 'clicked'라는 시그널이 발생합니다.
시그널이 발생했을 때 호출되는 함수 또는 메서드를 슬롯이라 부른다고 했습니다. 슬롯을 구현했다면 시그널과 슬롯을 연결만 해주면 됩니다. 시그널이 발생했을 때 연결된 슬롯을 호출하는 역할은 앞서 살펴본 이벤트 루프가 알아서 처리해줍니다.
다음 코드에서는 QPushButton 위젯을 생성하고 해당 위젯에서 'clicked' 시그널이 발생하면 호출되는 슬롯인 clicked_slot 함수를 구현했고 시그널과 슬롯을 이벤트 루프를 생성하기 전에 미리 연결했습니다.
import sys
from PyQt5.QtWidgets import *
def clicked_slot():
print('clicked')
app = QApplication(sys.argv)
btn = QPushButton("Hello, PyQt")
btn.clicked.connect(clicked_slot)
btn.show()
app.exec_()
위 코드를 실행해 보면 그림 16.7과 같이 QPushButton 객체가 화면에 출력됩니다. 버튼을 클릭하면 콘솔 창에 'clicked'라는 문자열이 출력됨을 확인할 수 있습니다. 이는 QPushButton 위젯에서 'clicked' 이벤트가 발생하면 'clicked_slot'이라는 이름의 함수가 호출되도록 미리 연결해 뒀기 때문입니다.
여기서 한 가지 주의할 점은 슬롯의 구현과 시그널-슬롯의 연결이 모두 이벤트 루프를 생성하는 app.exec_ 메서드 호출보다 먼저 수행돼야 한다는 점입니다.
그림 16.7 간단한 시그널-슬롯 연결의 예
지금까지 이벤트 루프 생성과 시그널-슬롯의 연결에 대해 복습했습니다. 이번에는 위젯을 이용해 UI를 구성하는 부분에 대해 복습하겠습니다. 이번에 구현할 프로그램은 그림 16.8과 같습니다.
그림 16.8 윈도우 및 이벤트 처리
PyQt에서 위젯은 UI를 구성하는 핵심 요소입니다. PyQt에서는 모든 위젯이 최상위 위젯을 의미하는 윈도우가 될 수 있습니다. 그러나 대부분의 프로그램에서는 QMainWindow나 QDialog 클래스를 사용해 윈도우를 생성합니다.
import sys
from PyQt5.QtWidgets import *
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setupUI()
def setupUI(self):
self.setWindowTitle("Review")
btn1 = QPushButton("Click me", self)
btn1.move(20, 20)
btn1.clicked.connect(self.btn1_clicked)
def btn1_clicked(self):
QMessageBox.about(self, "message", "clicked")
if __name__ == "__main__":
app = QApplication(sys.argv)
mywindow = MyWindow()
mywindow.show()
app.exec_()
예제 16.1 QMainWindow를 이용한 윈도우 구성(소스코드: book/ch16/03.py)
그림 16.8 프로그램의 전체 코드는 예제 16.1과 같습니다. 코드가 기존 코드에 비해 복잡해 보이지만 각 부분을 나눠서 이해하면 쉽게 파악할 수 있습니다. 가장 먼저 살펴볼 부분은 QApplication 객체를 생성하고 이벤트 루프를 만드는 부분입니다.
먼저 QApplication 객체인 app을 생성하고 exec_ 메서드를 호출해서 이벤트 루프를 생성합니다. 그리고 이벤트 루프를 생성하기 전에 MyWindow라는 클래스의 객체를 생성하는 것을 확인할 수 있습니다.
if __name__ == "__main__":
app = QApplication(sys.argv)
mywindow = MyWindow()
mywindow.show()
app.exec_()
앞서 배운 내용을 상기해보면 이벤트 루프를 생성하기 전에는 UI를 구성해야 하고 UI를 구성하는 기본 단위인 위젯에 대해 시그널과 슬롯을 연결해야 했습니다.
UI가 간단하다면 몇 줄의 코드만으로도 구현할 수 있겠지만 UI가 복잡한 경우에는 클래스를 사용하는 것이 좋습니다. 특히 클래스를 사용하면 기존 위젯 클래스를 상속함으로써 여러분만의 클래스를 새롭게 정의할 수 있습니다.
MyWindow 클래스의 객체는 이벤트 루프가 실행되기 전에 생성되며 이때 자동으로 생성자가 호출됩니다. 따라서 코드를 분석할 때는 먼저 생성자 부분부터 살펴보는 것이 좋습니다. 다음 코드를 살펴보면 MyWindow 클래스는 QMainWindow 클래스를 상속받고 있습니다.
자식 클래스인 MyWindow가 부모 클래스인 QMainWindow를 상속받으면 QMainWindow 클래스에 정의된 모든 메서드나 프로퍼티를 사용할 수 있습니다. 클래스의 상속은 우리 생활에서 보면 부모님을 잘 만나서 태어날 때부터 ‘금수저’를 물고 태어나는 것과 비슷합니다.
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setupUI()
MyWindow 클래스는 단순히 부모 클래스를 상속할 뿐만 아니라 자기 자신의 UI를 변경하는 메서드인 setupUI를 호출합니다. 물론 QPushButton과 같은 객체 생성 코드를 모두 MyWindow 클래스의 생성자에 넣을 수도 있지만, 따로 메서드로 구성해서 코드의 가독성을 좋게 합니다. 여기서 중요한 코드는 'clicked'라는 시그널과 이를 처리하는 슬롯인 self.btn1_clicked를 연결하는 코드입니다.
def setupUI(self):
self.setWindowTitle("Review")
btn1 = QPushButton("Click me", self)
btn1.move(20, 20)
btn1.clicked.connect(self.btn1_clicked)
QPushButton에서 'clicked' 시그널이 발생할 때 자동으로 호출되는 슬롯인 self.btn1_clicked는 다음과 같이 MyWindow 클래스의 메서드로 구현합니다.
def btn1_clicked(self):
QMessageBox.about(self, "message", "clicked")
'Python' 카테고리의 다른 글
[Python]Hilbert Transform (0) | 2023.09.05 |
---|---|
[Python]How to import a module from a parent module or a submodule) (0) | 2023.08.28 |
초보자를 위한 Python GUI 프로그래밍 - PyQt5 (0) | 2023.08.16 |
[Python] Pandas DataFrame (0) | 2023.07.21 |
[Python] 파이썬으로 배우는 알고리즘 트레이딩 (0) | 2023.07.21 |
댓글