IT News

파이썬 암호화페

post tistory 2021. 4. 3.
반응형

03-4 PyQt 기초

PyQt ?

지금까지는 파이썬 코드를 실행하면 그림 3-24와 같이 화면에 텍스트가 출력되는 프로그램을 구현했습니다. 키보드만을 사용하는 이러한 형태의 프로그램을 TUI (Text-based User Interface) 프로그램이라고 합니다. 또는 콘솔 프로그램이라고 부릅니다.

 


그림 3-24 TUI 프로그램

콘솔 기반 프로그램은 키보드만으로 프로그램을 제어해야 하므로 사용하기 어려운 단점이 있습니다. 여러분은 주로 그림 3-25와 같이 마우스를 사용할 수 있는 GUI (Graphical User Interface) 기반 프로그램을 사용했을 겁니다. 프로그램 개발자 측면에서 보면 GUI 프로그램이 조금 더 복잡하지만, 사용자 측면에서 보면 GUI 기반 프로그램이 더 익숙하고 사용하기 편합니다.

 


그림 3-25 GUI 프로그램의 예

파이썬에서 GUI 프로그래밍을 하려면 어떻게 해야 할까요? 정답은 바로 '모듈'입니다. 파이썬이 제공하는 모듈을 사용해서 쉽게 GUI 프로그램을 만들 수 있습니다. 파이썬에서 주로 사용되는 GUI 모듈은 다음과 같이 세 가지 정도 있습니다.

  • TkInter
  • wxPython
  • PyQt

TkInter는 파이썬의 공식 설치 파일에 포함되어 있어 모듈의 추가 설치 없이 GUI 프로그래밍이 가능합니다. 또한 다른 GUI 모듈에 비해 간단해서 배우기가 쉽습니다. 하지만 오래전에 모듈 개발이 완료돼서 UI 디자인이 구식이며 최신 트렌드와 맞지 않습니다.

wxPython과 PyQt는 파이썬 공식 설치 파일에는 포함되어 있지 않아 추가 설치가 필요합니다. wxPython과 PyQt 두 가지 모두 널리 사용되는데, 본 도서에서는 'PyQt'를 사용하겠습니다. pyQt는 아나콘다 배포판에 포함되어 있어 아나콘다를 설치했다면 바로 프로그래밍을 시작할 수 있기 때문입니다.

PyQt가 무엇인지 잠깐 살펴보겠습니다. PyQt는 riverbankcomputing에서 개발한 파이썬 모듈입니다. Py는 Python을 의미하고 Qt는 GUI 프로그래밍을 위한 라이브러리를 의미합니다. 파이썬에서 라이브러리는 모듈들의 집합이라고 생각하면 쉽습니다. Qt는 버전 4와 버전 5가 많이 사용되며, 아나콘다에는 최신 버전인 PyQt5가 포함되어 있습니다. pyQt5 모듈을 사용해서 GUI 프로그램을 개발해 보겠습니다.

클래스 복습

PyQt는 대부분 클래스를 사용합니다. PyQt에 대해 배우기 전에 먼저 클래스의 기본 내용을 복습해 봅시다. 다음은 Car 클래스를 정의하는 코드입니다.

1: class Car: 2: def __init__(self, model, year): 3: self.model = model 4: self.year = year

def키워드는 함수를 정의하는 파이썬 키워드였지요? 클래스 안에 정의된 함수를 특별히 메서드라고 부른다고 했습니다. 그중에서도 __init__은 클래스에서 특별한 기능을 하는 메서드로 '초기화자'로 불립니다. 초기화자는 클래스로부터 객체가 생성될 때 자동으로 호출됩니다.

위 코드에서 초기화자를 살펴보면 Car라는 클래스로부터 객체를 생성할 때 자동차의 모델 (model)과 연식 (year)을 입력받습니다. 함수의 기본은 어떤 입력을 받아 동작(기능)을 수행한 후 값을 리턴하는 것이죠? 따라서 초기화자에서 어떤 값을 받는 것은 매우 자연스러운 일입니다. 입력받은 model과 year 값을 객체가 바인딩하기 위해서 self.model = model, self.year = year라고 코딩하는 겁니다. self가 이해가 되지 않는 분들은 다시 클래스에 대해서 복습을 하고 이어서 읽기 바랍니다.

여기까지는 Car 클래스를 정의한 것입니다. 클래스의 정의만으로는 어떤 코드도 실행되지 않습니다. 자동차 생산 공장으로 비유하면, 자동차에 대한 설계도를 그린 것과 같습니다. 설계도로 실제 자동차를 만들어야 자동차를 사용할 수 있겠죠. 다음 코드와 같이 클래스 이름을 적고 ()를 붙여주면 클래스의 객체가 생성됩니다. 다만 Car 클래스는 객체를 생성하고자 할 때 model과 year를 입력해야 합니다. 2017년식 소나타와 2018년식 G80을 만들고 sonata와 g80 변수에 각각 바인딩했습니다.

6: sonata = Car("SONATA", 2017) 7: g80 = Car("G80", 2018)

클래스로부터 객체가 생성되면, 해당 객체는 고유의 메모리 공간을 갖습니다. 그리고 변수가 메모리 공간을 가리키는 것이죠. 객체를 바인딩하는 변수에 (예: sonata, g80)에 점 (.)을 찍으면 해당 객체에 접근하여 객체에 저장된 값이나 클래스의 메서드를 이용할 수 있습니다. 다음 코드를 살펴봅시다. sonata 변수에 점 (.)을 찍은 후 model과 year 값을 출력합니다. g80 변수에도 점 (.)을 찍은 후 model과 year 값을 출력할 수 있습니다.

09: print(sonata.model, sonata.year) 10: print(g80.model, g80.year)

지금까지 작성한 전체 코드는 다음과 같습니다.

# ch03/03_11.py 01: class Car: 02: def __init__(self, model, year): 03: self.model = model 04: self.year = year 05: 06: sonata = Car("SONATA", 2017) 07: g80 = Car("G80", 2018) 08: 09: print(sonata.model, sonata.year) 10: print(g80.model, g80.year)

코드를 실행하면 다음과 같이 소나타와 G80의 모델명과 연식이 출력되는 것을 확인할 수 있습니다.

C:\Anaconda3\python.exe "C:/Users/brayden/Desktop/pyqt00.py" SONATAR 2017 G80 2018

 

 

PyQt 기초

이번에는 PyQt를 사용해서 코드를 작성해 보겠습니다. PyCharm을 사용해서 먼저 아래의 코드를 작성한 후 실행해 봅시다.

# ch03/03_12.py 1: import sys 2: from PyQt5.QtWidgets import * 3: 4: app = QApplication(sys.argv) 5: label = QLabel("Hello") 6: label.show() 7: app.exec_()

콘솔에 출력되던 것과 달리 그림 3-26과 같이 윈도우가 나타나고, 윈도우 안에 'Hello'라는 문자열이 출력됩니다. 윈도우의 우측 상단에 'X' 버튼을 누르면 프로그램이 종료됩니다.

 


그림 3-26 PyQt 프로그램

처음으로 사용해본 PyQt 관련 코드를 살펴보겠습니다. 처음 두 라인은 사용할 모듈을 가져오는 코드입니다. 모듈은 '파이썬 파일'이므로 어딘가에 sys.py 파일이 있을 겁니다. PyQt5는 모듈 규모가 커서 디렉터리 안에 여러 파일 (모듈)이 있는 구조입니다. 그래서 디렉터리의 계층 구조를 명시하기 위해 PyQt5.QtWidget으로 작성합니다. PyQt5가 디렉터리 이름입니다. 즉, from PyQt5.QtWidgets import * 의 의미는 PyQt5 디렉터리에 있는 QtWidgets.py 파일로부터 모든 것을 import하라는 것입니다.

1: import sys 2: from PyQt5.QtWidgets import *

sys 모듈을 임포트 했으니 우리는 sys.argv와 같이 코딩할 수 있습니다. 여기서 sys는 모듈 이름이고 argv는 sys라는 파이썬 파일에 있는 리스트입니다. 모듈이 어렵게 느껴진다면 그림 3-27과 같이 모듈로 이동해서 그 코드를 살펴보기 바랍니다. PyCharm에서는 모듈 이름에 커서를 위치한 후 F12키를 누르면 해당 모듈로 이동합니다. 다시 원래 코드로 돌아오려면 Alt 키와 왼쪽 화살표 (←)를 동시에 누르면 됩니다.

 


그림 3-27 sys 모듈

QtWidgets 모듈에는 GUI 프로그래밍을 위한 여러 클래스가 이미 정의되어 있습니다. QLabel은 그중 하나입니다. 표 3-2에서 왼쪽의 PyQt 코드와 오른쪽의 Car 클래스 코드를 비교해 봅시다. Car 클래스로부터 객체를 생성한 것과 같이 QtWidget 모듈에 정의된 QLabel 클래스의 객체를 생성합니다. 객체를 생성할 때 QLabel 윈도우에 출력될 문자열인 'Hello'를 넘겨준 겁니다. 생성된 객체는 label이라는 변수가 바인딩하고 있습니다. 따라서 label이라는 변수 이름에 점 (.)을 찍으면 객체 내의 변수나 메서드에 접근할 수 있겠지요? QLabel 클래스에는 show()라는 메서드가 있는데 이 메서드를 호출하면 윈도우가 화면에 나타나는 겁니다.

표 3-2 PyQt와 Car 클래스 비교

PyQtCar 클래스

label = QLabel("Hello")
label.show()
class Car:
    def init(self, model, year):
        self.model = model
        self.year = year

sonata = Car("SONATA", 2017)
g80 = Car("G80", 2018)

다음 코드를 살펴봅시다. QLabel 클래스에 대한 객체를 생성하고 show() 메서드를 호출했습니다. 그러나 실행을 해보면 정상적으로 윈도우가 출력되지 않습니다.

import sys from PyQt5.QtWidgets import * label = QLabel("Hello") label.show()

그리고 PyCharm에서 실행 로그를 확인해보면 'exit code 1'로 정상적으로 프로그램이 종료되지 않았음을 확인할 수 있습니다.

C:\Anaconda3\python.exe "C:/Users/brayden/Desktop/PyCoin v0.1/pyqt01.py" Process finished with exit code 1

PyQt로 프로그래밍을 작성할 때는 일반적으로 다음의 두 가지가 필요합니다.

1) QApplication 클래스의 인스턴스 2) 이벤트 루프

지금까지 우리가 작성한 파이썬 코드의 실행을 살펴보면 파이썬 인터프리터(interpreter)가 코드를 실행할 때 1번째 라인부터 코드를 실행한 후 더 실행할 코드가 없으면 프로그램을 종료합니다. 이와달리 GUI 프로그래밍은 윈도우가 화면에 출력되는데 이때 사용자가 'X' 버튼을 클릭하기 전까지는 프로그램이 종료되지 않습니다. 이처럼 GUI 프로그램이 'X' 버튼을 누를 때까지 종료되지 않고 계속 실행되려면 for나 while가 같은 '루프'가 필요한데 GUI 프로그램에서는 이를 '이벤트 루프'라고 합니다.

PyQt에서 이벤트 루프는 QApplication 클래스의 exec_() 메서드를 호출함으로써 생성할 수 있습니다. 여러분들이 이벤트 루프를 직접 만들 필요 없이 QApplication의 인스턴스를 생성하고 메서드만 호출하면 끝입니다. QApplication 인스턴스는 이벤트 루프뿐만 아니라 PyQt 클래스들을 사용하기 위해서라도 미리 정의돼 있어야 합니다. 에러가 발생한 코드의 4번 라인에 QApplication 객체를 생성하는 코드를 추가해봅시다.

1: import sys 2: from PyQt5.QtWidgets import * 3: 4: app = QApplication(sys.argv) 5: label = QLabel("Hello") 6: label.show()

QApplication 클래스의 인스턴스를 먼저 생성한 후에는 다음과 같이 정상적으로 프로그램이 종료됨을 확인할 수 있습니다. 코드에서 이벤트 루프를 생성하지 않았기 때문에 파이썬 인터프리터는 5줄의 코드를 순서대로 실행한 후 종료되게 됩니다.

C:\Anaconda3\python.exe "C:/Users/brayden/Desktop/PyCoin v0.1/pyqt01.py" Process finished with exit code 0

마지막으로 이벤트 루프를 생성하는 exec_() 메서드를 호출하는 코드를 추가해 봅시다. 코드를 실행하면 정상적으로 윈도우가 출력되고 윈도우를 닫을 때까지 프로그램이 계속해서 실행되는 것을 확인할 수 있습니다.

import sys from PyQt5.QtWidgets import * app = QApplication(sys.argv) # QApplication 객체 생성 label = QLabel("Hello") label.show() app.exec_() # 이벤트 루프 생성

PyQt 기반의 코드는 그림 3-28과 같이 세 부분으로 구분할 수 있습니다. QApplication 객체를 생성하는 첫 번째 단계는 pyQt에서 제공하는 클래스를 사용할 수 있도록 내부적으로 초기화를 수행합니다. 다음으로 실제 화면에 출력될 윈도우를 구성하는 코드를 작성합니다. 앞의 예제에서 QLabel에 대한 객체를 생성하고 show() 메서드를 호출하는 코드가 여기에 해당합니다. 마지막으로 이벤트 루프를 생성해서 프로그램이 종료되지 않고 실행될 수 있도록 합니다.

 


그림 3-28 PyQt 이벤트 루프

이번에는 QLabel 클래스 대신 QPushButton 클래스를 사용해봅시다. QPushButton 객체를 바인딩하는 변수 이름은 변경하지 않아도 되지만 버튼을 바인딩하고 있으므로 변수 이름도 적당히 'btn'으로 변경했습니다.

# ch03/03_13.py import sys from PyQt5.QtWidgets import * app = QApplication(sys.argv) btn = QPushButton("Hello") # 버튼 객체 생성 btn.show() app.exec_() # 이벤트 루프 생성

Ch03/03_13.py 코드를 실행하면 버튼이 하나 있는 윈도우가 출력되는 것을 확인할 수 있습니다. 이처럼 화면에 보이는 부분을 변경하고자 한다면 QApplication 객체를 생성한 후 그리고 이벤트 루프 생성 전에 코드를 작성하면 됩니다.

 

 

위젯과 윈도우

그림 3-29는 GUI 프로그램에서 사용할 수 있는 여러 가지 컴포넌트를 보여줍니다. 앞서 만들어본 버튼과 같은 컴포넌트를 PyQt에서는 위젯이라고 부릅니다. 위젯은 사용자 인터페이를 구성하는 가장 기본적인 부품 역할을 합니다.

 


그림 3-29 PyQt 위젯

PyQt에는 수많은 위젯이 존재합니다. 여러분은 GUI 프로그램을 만들 때 필요한 위젯을 선택해서 쓰는 겁니다. 여기서 위젯을 선택한다는 것은 PyQt에 정의된 클래스 (예: QLabel) 객체를 생성한다는 뜻입니다. 위젯은 다른 위젯에 포함될 수 있는데, 다른 위젯에 포함되지 않은 최상위 위젯을 특별히 윈도우라고 부릅니다. PyQt에서 윈도우를 생성할 때는 QMainWindow 클래스나 QDialog 클래스를 사용합니다.

나만의 윈도우 클래스 정의하기

앞서 살펴봤던 코드는 동작을 이해하기 쉬웠지만 복잡한 UI를 구성하기는 어려운 구조입니다. GUI 프로그램은 윈도우에 여러 위젯을 배치하는 것인데 이를 효과적으로 하기 위해서는 클래스를 사용하는 것이 편리합니다. 화면에 출력되는 UI를 구성하기 위한 클래스(MyWindow)를 정의해 봅시다.

# ch03/03_14.py 01: import sys 02: from PyQt5.QtWidgets import * 03: 04: class MyWindow(QMainWindow): 05: def __init__(self): 06: super().__init__() 07: 08: 09: app = QApplication(sys.argv) 10: window = MyWindow() 11: window.show() 12: app.exec_()

ch03/03_14.py 코드를 실행하면 화면에 그림 3-30과 같은 윈도우가 출력됩니다. 외형 상으로는 앞서 만들었던 QLabel 및 QPushButton 위젯과는 큰 차이가 없어 보입니다.

 


그림 3-30 윈도우

이제 코드를 살펴봅시다. 모듈을 임포트한 후에 4번 라인에서 MyWindow 클래스를 정의했습니다. 중요한 점은 최상위 위젯 윈도우를 위한 QMainWindow 클래스를 상속받은 것입니다. 윈도우를 처음부터 만드는 것은 어렵기 때문에, pyQt가 제공하는 클래스를 상속받아 쉽게 윈도우를 만들었습니다. 이런 편리함 때문에 '클래스'를 사용하는 것입니다.

MyWindow 클래스의 초기화자에 super().__init__() 이라는 코드가 있습니다. 여기서 super()는 파이썬의 내장 함수 (파이썬이 설치되면 기본적으로 제공되는 함수)입니다. MyWindow 클래스는 QMainWindow 클래스를 상속받는데 자식 클래스가 부모 클래스에 정의된 함수를 호출하려면 'self.부모클래스_메서드()' 처럼 적으면 됩니다. 그런데 __init__() 이라는 초기화자는 자식 클래스에도 있고 부모 클래스에도 있습니다. 이 경우 self.__init__() 이라고 적으면 자식 클래스 (MyWindow)의 초기화자를 먼저 호출하게 됩니다. 따라서 부모 클래스에 정의된 초기화자를 명시적으로 호출하려고 상속 구조에서 부모 클래스를 찾아서 리턴해주는 super()를 적은 후__init__() 메서드를 호출하는 겁니다.

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

만약 위 코드가 이해가 잘 안 된다면 다음과 같이 코드를 변경해 봅시다. super()를 통해서 부모 클래스의 객체를 얻어오고 이를 parent라는 변수로 바인딩합니다. 변수가 어떤 객체를 바인딩하고 있다면 그 변수에 점 (.)을 찍으면 클래스의 메서드를 호출할 수 있었지요? 그래서 parent.__init__()을 사용할 수 있는 겁니다.

class MyWindow(QMainWindow): def __init__(self): #super().__init__() parent = super() parent.__init__()

그렇다면 MyWindow 클래스의 초기화자에서 왜 부모 클래스인 QMainWindow의 초기화자를 호출해야 할까요? 다음과 같이 부모 클래스의 초기화자를 호출하지 않으면 에러가 발생합니다.

class MyWindow(QMainWindow): def __init__(self): #super().__init__() pass C:\Anaconda3\python.exe "C:/Users/brayden/Desktop/PyCoin v0.1/pyqt02.py" Traceback (most recent call last): File "C:/Users/brayden/Desktop/PyCoin v0.1/pyqt02.py", line 12, in <module> window.show() RuntimeError: super-class __init__() of type MyWindow was never called Process finished with exit code 1

자식 클래스가 부모 클래스를 상속받는다고 해서 항상 자식 클래스에서 부모 클래스의 초기화자를 호출할 필요는 없습니다. 다만 PyQt의 QMainWindow 클래스는 이 과정이 필요한 겁니다. 여러분은 그냥 "PyQt가 제공하는 클래스를 상속받는 경우 부모 클래스의 초기화자를 명시적으로 호출해줘야 한다."라고 생각하면 됩니다.

클래스를 정의했다고 클래스의 인스턴스가 생성되는 것은 아닙니다. 객체를 생성하기 위해서는 클래스의 이름에 ()를 붙여주면 됐었습니다. 앞서 정의한 MyWindow의 초기화자에서 self 말고는 다른 인자가 존재하지 않기 때문에 MyWindow()만 호출합니다. 생성된 객체를 window라는 이름의 변수에 바인딩합시다. 그리고 window라는 변수에 점 (.)을 찍고 show() 메서드를 호출해주면 여러분이 정의한 윈도우가 화면에 출력됩니다. 그렇다면 show() 메서드는 어디에 있는걸까요? MyWindow 클래스가 상속받은 QMainWindow 클래스에 정의되어 있습니다.

app = QApplication(sys.argv) window = MyWindow() window.show() app.exec_()

다음은 지금까지 배운 GUI 프로그램의 “기본 코드” 전체입니다. 기본 코드는 pyQt 공식 홈페이지 등에서도 쉽게 구할 수 있기 때문에 외울 필요가 없습니다. GUI 프로그램을 작성할 때 복사해서 여러분의 코드를 추가하면서 프로그램을 완성해 나가면 됩니다. 비록 코드를 외울 필요는 없지만 코드들의 의미와 담당하는 기능 정도는 이해하고 있어야 합니다.

01: import sys 02: from PyQt5.QtWidgets import * 03: 04: class MyWindow(QMainWindow): 05: def __init__(self): 06: super().__init__() 07: 08: app = QApplication(sys.argv) 09: window = MyWindow() 10: window.show() 11: app.exec_()

마지막 편집일시 : 2021년 3월 27일 8:45 오후

 

03-5 PyQt 윈도우 꾸미기

윈도우 크기 조절

앞서 PyQt에서 위젯이 배치되는 가장 기본이 되는 위젯을 '윈도우'라고 했습니다. 이번 절에서는 윈도우의 크기 및 윈도우 출력되는 위치를 변경해 보겠습니다. 기본 코드에서 MyWindow 클래스를 정의할 때 QMainWindow 클래스를 상속받았습니다. 따라서 부모 클래스인 QMainWindow에 정의되어 있는 메서드를 사용할 수 있습니다. QMainWindow 클래스에는 윈도우의 크기 및 출력 위치를 변경하는 setGeometry() 메서드가 정의돼 있습니다.

자식 클래스 MyWindow에서 setGeometry() 메서드로 윈도우의 크기 및 출력 위치를 변경해 봅시다.

# ch03/03_15.py 01: import sys 02: from PyQt5.QtWidgets import * 03: 04: 05: class MyWindow(QMainWindow): 06: def __init__(self): 07: super().__init__() 08: self.setGeometry(100, 200, 300, 400) 09: 10: 11: app = QApplication(sys.argv) 12: window = MyWindow() 13: window.show() 14: app.exec_()

메서드의 인자로 4개의 정숫값을 넘겨주는데 순서대로 모니터에 왼쪽 상단으로부터 윈도우가 출력되는 x축 위치, y축 위치, 윈도우의 너비, 윈도우의 높이를 의미합니다. 이해가 안 되는 분들은 4개의 값들을 변경하면서 프로그램을 실행해 보시기 바랍니다.

 


그림 3-31 윈도우 크기 및 출력 좌표 조정

윈도우 타이틀바 변경하기

이번에는 윈도우의 타이틀바를 변경해보겠습니다. 정확히는 타이틀바에 표시되는 텍스트를 변경하는 겁니다. 그림 3-32에서 python이라고 표시된 부분이 타이틀입니다. 타이틀바에는 보통 프로그램의 이름을 표시하는데, 기본값은 'python'입니다.

 


그림 3-32 윈도우 기본 타이틀바

윈도우 타이틀바의 텍스트를 변경하는 메서드는 setWindowTitle()입니다. 이 역시 QMainWindow 클래스에 정의되어 있습니다. 다음과 같이 윈도우 타이틀바의 텍스트를 'PyQt'로 변경해 봅시다. 출력하고자 하는 문자열을 setWindowTitle() 메서드의 인자로 넘겨주면 됩니다.

class MyWindow(QMainWindow): def __init__(self): super().__init__() self.setGeometry(100, 200, 300, 200) self.setWindowTitle("PyQt")

이번에는 타이틀바에 표시되는 아이콘을 변경해 보겠습니다. 아이콘을 다음 사이트들로부터 저장합니다. 이때 16x16 크기의 png 포맷으로 아이콘을 다운로드하면 됩니다.

다운로드한 아이콘 파일을 파이썬 코드와 동일 디렉터리로 이동합니다. 윈도우에서 아이콘의 설정은 setWindowIcon() 메서드를 사용합니다. setWindowIcon() 메서드는 QIcon 클래스의 인스턴스를 인자로 받습니다. QIcon 클래스의 인스턴스를 생성할 때는 초기화자로 아이콘 파일의 경로를 문자열로 넘겨주면 됩니다. 현재 예에서는 소스코드와 아이콘 파일이 같은 디렉터리에 있기 때문에 파일 이름만 적어주면 됩니다. QIcon 클래스는 QtGui 파일에 정의되어 있기 때문에 from PyQt5.QtGui import *를 추가해야 합니다.

# ch03/03_16.py import sys from PyQt5.QtWidgets import * from PyQt5.QtGui import * class MyWindow(QMainWindow): def __init__(self): super().__init__() self.setGeometry(100, 200, 300, 200) self.setWindowTitle("PyQt") self.setWindowIcon(QIcon("icon.png"))

업데이트 한 코드를 실행하면 그림 3-33과 같이 윈도우의 타이틀바에 아이콘 파일이 출력되는 것을 확인할 수 있습니다. 참고로 아이콘은 여러분이 제작할 프로그램을 특징을 잘 보여주는 그림으로 하는 것이 좋습니다.

 


그림 3-33 아이콘 및 타이틀바 변경

 

 

버튼 추가하기

이번 절에서는 윈도우에 버튼을 추가해보겠습니다. 윈도우에 버튼을 추가하려면 윈도우가 생성될 때 버튼을 만들어주면 됩니다. 즉, MyWindow 클래스의 초기화자에서 버튼 객체를 생성합니다. 초기화자는 객체가 생성될 때 자동으로 호출되는 것을 기억하세요. 버튼의 생성은 QPushButton 클래스를 사용하면 됐지요?

MyWindow 클래스를 다음과 같이 수정해 봅시다. QPushButton 객체를 생성할 때 첫 번째 인자는 버튼에 출력될 문자열을 넘겨주면 됩니다. 두 번째 인자는 버튼이 표시될 위젯을 의미합니다. 우리는 버튼이 윈도우 위에 표시되길 원하는데 윈도우는 self라는 변수가 바인딩하고 있으므로 self를 넘겨주면 됩니다.

# ch03/03_17.py 01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5.QtGui import * 04: 05: 06: class MyWindow(QMainWindow): 07: def __init__(self): 08: super().__init__() 09: self.setGeometry(100, 200, 300, 200) 10: self.setWindowTitle("PyQt") 11: self.setWindowIcon(QIcon("icon.png")) 12: 13: btn = QPushButton("버튼1", self) 14: 15: 16: app = QApplication(sys.argv) 17: window = MyWindow() 18: window.show() 19: app.exec_()

Ch03/03_17.py 코드를 실행하면 그림 3-34와 같이 윈도우 위로 버튼이 생성된 것을 확인할 수 있습니다.

 


그림 3-34 버튼 위젯 추가

윈도우에 출력되는 버튼의 위치는 QPushButton 클래스의 move() 메서드를 사용해서 지정합니다. (10, 10)은 윈도우 창 내에서 버튼이 출력될 x 축 좌표와 y 축 좌표입니다. 변경된 코드를 실행하면 버튼의 위치가 변경됨을 확인할 수 있습니다.

btn = QPushButton("버튼1", self) btn.move(10, 10)

버튼을 하나 더 만들어 봅시다. MyWindow 클래스의 초기화자에서 QPushButton 클래스의 인스턴스를 하나 더 만들어 주기면 됩니다. 그리고 버튼이 같은 위치에 출력되지 않도록 버튼이 출력되는 위치를 move() 메서드를 사용해 변경합니다.

# ch03/03_18.py class MyWindow(QMainWindow): def __init__(self): super().__init__() self.setGeometry(100, 200, 300, 200) self.setWindowTitle("PyQt") self.setWindowIcon(QIcon("icon.png")) btn = QPushButton("버튼1", self) btn.move(10, 10) btn2 = QPushButton("버튼2", self) btn2.move(10, 40)

Ch03/03_18.py 코드를 실행하면 그림 3-35와 같이 윈도우에 버튼이 두 개가 출력됩니다. 버튼은 정상적으로 생성됐지만 버튼을 클릭해도 아무런 변화가 없습니다.

 


그림 3-35 버튼 위젯 추가

버튼에 클릭 이벤트 추가하기

이번 절에서는 버튼에 클릭 이벤트를 추가해 보겠습니다. 사용자가 버튼을 클릭하면 'clicked' 이벤트가 발생합니다. 'clicked' 이벤트가 발생할 때 어떤 메서드가 호출되게 하려면 이벤트와 메서드를 연결해야 합니다. 다음 코드는 버튼 객체 (btn)가 클릭될 때 (clicked) MyWindow 클래스에 정의된 btn_clicked 메서드가 호출되도록 연결해줍니다.

btn = QPushButton("버튼1", self) btn.move(10, 10) btn.clicked.connect(self.btn_clicked)

버튼이 클릭되면 btn_clicked 메서드가 자동으로 호출됩니다.

# ch03/03_19.py class MyWindow(QMainWindow): def __init__(self): super().__init__() self.setGeometry(100, 200, 300, 200) self.setWindowTitle("PyQt") self.setWindowIcon(QIcon("icon.png")) btn = QPushButton("버튼1", self) btn.move(10, 10) btn.clicked.connect(self.btn_clicked) def btn_clicked(self): print("버튼 클릭")

MyWindow 클래스를 새로 정의했으니 코드를 재 실행해 봅시다. 윈도우에서 '버튼 1'을 클릭하면 화면에 '버튼 클릭'이 출력됩니다.

C:\Anaconda3\python.exe C:/Users/brayden/PycharmProjects/untitled1/run.py 버튼 클릭 버튼 클릭

사용자가 버튼 객체를 클릭할 때 'btn_clicked' 메서드가 호출되는 과정을 복습해 봅시다. 아래 그림처럼 윈도우에는 여러 위젯이 배치될 수 있습니다. 위젯 별로 정의된 이벤트가 있는데 버튼에는 대표적으로 'clicked' 이벤트가 있습니다.

QApplication() 객체를 생성한 후 exec_() 메서드를 호출하면 이벤트 루프가 생성되는데 이벤트 루프는 루프를 돌고 있다가 사용자가 이벤트를 발생시키면 (예: 버튼 클릭) 이벤트에 연결된 메서드를 호출해주는 역할을 합니다.

지금까지는 클래스에 메서드가 있는 경우 사용자가 객체를 생성한 후 객체를 통해 메서드를 직접 호출했습니다. 그러나 GUI 프로그래밍에서는 이벤트를 발생될 때 이벤트 루프가 해당 이벤트에 연결되어 있는 메서드를 호출해줍니다. 이처럼 프로그래머가 직접 메서드를 호출하는 것이 아니라 이벤트 루프가 메서드를 호출하기 때문에 '콜백 함수'라고 부릅니다.

 


그림 3-36 콜백 함수 개념

 

 

03-6 PyQt Qt Designer

Qt Designer 사용하기

다음 코드는 앞서 여러분이 구현했던 프로그램입니다. 이 코드는 크게 세가지 부분으로 구분할 수 있습니다.

  • 위젯 생성 코드
  • 이벤트 처리 코드
  • QApplication 객체 생성 및 이벤트 루프 생성 코드

01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5.QtGui import * 04: 05: 06: class MyWindow(QMainWindow): 07: def __init__(self): 08: super().__init__() 09: # 위젯 생성 코드 10: self.setGeometry(100, 200, 300, 200) 11: self.setWindowTitle("PyQt") 12: self.setWindowIcon(QIcon("icon.png")) 13: 14: btn = QPushButton("버튼1", self) 15: btn.move(10, 10) 16: btn.clicked.connect(self.btn_clicked) 17: 18: # 이벤트 처리 코드 19: def btn_clicked(self): 20: print("버튼 클릭") 21: 22: # QApplication 객체 생성 및 이벤트 루프 생성 코드 23: app = QApplication(sys.argv) 24: window = MyWindow() 25: window.show() 26: app.exec_()

이벤트 처리 코드나 QApplication 객체 생성 및 이벤트 루프를 생성하는 코드는 프로그래머가 직접 작성해야 합니다. 하지만 버튼과 같은 고정된 위젯들은 프로그램을 사용해서 쉽게 생성할 수 있습니다. 예를 들어 파워포인트를 사용해서 그림을 그리듯이 UI를 만들어 냅니다. 아나콘다가 제공하는 Qt Designer 툴을 사용해 보겠습니다.

 


그림 3-37 Qt Designer

아나콘다 설치 디렉터리에서 designer.exe 파일을 클릭하면 Qt Designer가 실행됩니다. 1장의 파이썬 설치하기 매뉴얼을 따랐다면 아래 경로에서 designer.exe를 찾을 수 있습니다.

C:\Anaconda3\Library\bin

새 폼창에서 그림 3-38과 같이 'Main Window'를 선택한 후 '생성' 버튼을 클릭합니다.

 


그림 3-38 새 폼 생성

위젯 상자에서 Push Button은 앞서 배웠던 QPushButton 클래스입니다. 파이썬 코드로는 QPushButton 클래스의 인스턴스를 생성했는데 Qt Designer에서는 단순히 위젯 상자에서 Push Button을 선택한 후 그림 3-39와 같이 'Main Window'로 드래그 앤 드롭하면 됩니다. 코드로 구현하는 것보다 훨씬 간단하죠?

 


그림 3-39 버튼 위젯 추가

버튼 위젯을 더블 클릭한 후 버튼에 표시될 텍스트를 '클릭'으로 변경해줍니다. 키보드의 Ctrl + R 키를 누르면 그림 3-40과 같이 Main Window의 최종 결과물을 미리 확인할 수 있습니다. 위젯을 배치하면서 중간중간 미리 보기 기능을 통해 최종 결과를 해보세요.

 


그림 3-40 버튼 위젯 추가

버튼 위젯의 배치가 끝났다면 작업한 UI 결과물을 저장해야 합니다. Qt Designer에서 ‘파일 → 다른 이름으로 저장’ 메뉴를 클릭하여 특정 디렉터리 (예: 윈도우 바탕화면)에 저장해줍니다. 필자는 윈도우 바탕화면에 mywindow라는 이름으로 저장했습니다.

UI 파일 사용하기

이번에는 Qt Designer를 사용해서 UI를 구성했던 mywindow.ui 파일을 사용해서 GUI 프로그램을 작성해 보겠습니다. Qt Designer의 결과물인 mywindow.ui 파일은 파이썬 코드와 같은 디렉터리에 위치해야 합니다. 다음 코드를 실행하여 결과를 확인해 봅시다.

# ch03/03_20.py 01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5 import uic 04: 05: form_class = uic.loadUiType("mywindow.ui")[0] 06: 07: 08: class MyWindow(QMainWindow, form_class): 09: def __init__(self): 10: super().__init__() 11: self.setupUi(self) 12: 13: 14: app = QApplication(sys.argv) 15: window = MyWindow() 16: window.show() 17: app.exec_()

Ch03/03_20.py 코드에서는 버튼을 생성하는 코드(QPushButton 클래스의 인스턴스 생성)가 없지만 그림 3-41을 보면 Qt Designer에서 만든 버튼이 위치하는 것을 확인할 수 있습니다.

 


그림 3-41 UI 파일을 사용해서 윈도우 만들기

uic 모듈의 loadUiType() 메서드는 Qt Designer의 결과물인 mywindow.ui 파일을 읽어서 파이썬 클래스 코드를 만듭니다.

from PyQt5 import uic form_class = uic.loadUiType("mywindow.ui")[0]

MyWindow 클래스는 QMainWindow와 form_class로부터 다중 상속을 받고, 초기화자 (__init__)에서 setupUi() 메서드를 호출합니다. setupUi()는 form_class에 정의된 메서드로 Qt Designer에서 만든 클래스들을 초기화합니다.

class MyWindow(QMainWindow, form_class): def __init__(self): super().__init__() self.setupUi(self)

Qt Designer의 결과물인 *.ui 파일은 다음과 같이 XML이라는 언어로 되어 있습니다. PyQt5에 uic 모듈의 loadUiType() 메서드는 XML 파일을 읽어서 이를 파이썬 클래스로 만들어주는 역할을 합니다. 그래서 우리가 Qt Designer를 통해서 위젯을 배치해도 파이썬 코드로 직접 위젯 객체를 생성하는 것과 같은 결과물을 얻을 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MainWindow</class> <widget class="QMainWindow" name="MainWindow"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>339</width> <height>237</height> </rect> </property> <property name="windowTitle"> <string>MainWindow</string> </property> <widget class="QWidget" name="centralwidget"> <widget class="QPushButton" name="pushButton"> <property name="geometry"> <rect> <x>20</x> <y>20</y> <width>75</width> <height>23</height> </rect> </property> <property name="text"> <string>클릭</string> </property> </widget> </widget> <widget class="QMenuBar" name="menubar"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>339</width> <height>21</height> </rect> </property> </widget> <widget class="QStatusBar" name="statusbar"/> </widget> <resources/> <connections/> </ui>

 

 

이벤트 추가하기

앞서 우리는 버튼의 이벤트를 처리하는 방법에 대해 배웠습니다. 버튼 위젯을 btn이라는 변수에 바인딩하고 connect 메서드를 사용해서 이벤트 발생 시 처리할 메서드를 연결했습니다.

btn = QPushButton("클릭", self) btn.clicked.connect(self.method)

하지만 Qt Designer를 통해 위젯을 생성하면, XML로 작성되는 위젯을 파이썬 변수에 바인딩할 수 없게 됩니다. 이러한 문제를 해결하기 위해 Qt Designer는 위젯을 바인딩할 변수를 지정하는 기능을 제공합니다. 그림 3-42와 같이 버튼 위젯을 선택한 후 '속성 편집기' 항목을 보면 objectName에 'pushButton'이라고 되어 있는 것을 확인할 수 있습니다. 이 값이 파이썬에서 버튼 이벤트를 정의할 때 사용할 변수의 이름입니다.

 


그림 3-42 객체 탐색기에서 변수 이름 확인하기

버튼 객체를 바인딩하는 변수 'pushButton'으로 다음과 같이 이벤트를 처리하는 메서드를 연결합니다. 윈도우에 배치되는 버튼이기 때문에 self.pushButton이라고 코드를 작성한 것에 유의하세요. 그리고 버튼에서 클릭 이벤트가 발생할 때 btn_clicked 메서드가 호출되도록 MyWindow에 추가합니다.

class MyWindow(QMainWindow, form_class): def __init__(self): super().__init__() self.setupUi(self) self.pushButton.clicked.connect(self.btn_clicked) def btn_clicked(self): print("버튼 클릭")

전체 코드는 다음과 같습니다. 파이썬 코드와 mywindow.ui 파일이 같은 디렉터리에 있어야 정상적으로 코드가 실행됩니다.

# ch03/03_21.py 01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5 import uic 04: 05: form_class = uic.loadUiType("mywindow.ui")[0] 06: 07: 08: class MyWindow(QMainWindow, form_class): 09: def __init__(self): 10: super().__init__() 11: self.setupUi(self) 12: self.pushButton.clicked.connect(self.btn_clicked) 13: 14: def btn_clicked(self): 15: print("버튼 클릭") 16: 17: app = QApplication(sys.argv) 18: window = MyWindow() 19: window.show() 20: app.exec_()

윈도우의 '클릭' 버튼을 누르면 btn_clicked() 메서드가 호출돼서 '버튼 클릭'이라는 문자열이 화면에 출력됩니다. 이처럼 Qt Designer를 사용하여 위젯을 생성하더라도 위젯의 이벤트를 처리할 수 있습니다. 다만 Qt Designer에서 위젯의 이름을 확인하는 과정이 필요합니다.

 


그림 3-43 버튼에 대한 이벤트 처리

코빗 시세 조회기 만들기

이번에는 코빗에서 비트코인 현재가를 조회하는 시세 조회기 프로그램을 만들어 보겠습니다. 가장 먼저 할 일은 Qt Designer를 사용해서 그림 3-44와 같은 UI를 만드는 것입니다.

 


그림 3-44 코빗 시세 조회기 UI 디자인

Qt Designer에서 Main Window를 생성한 후 그림 3-45와 같이 QLabel, QLineEdit, QPushButton 객체를 적절히 배치합니다. 위젯 배치가 적당히 됐다면 Ctrl + R을 눌러 미리 보기 기능을 통해 생성될 윈도우를 미리 확인할 수 있습니다. 작업한 UI 파일을 적당한 경로에 저장해주세요.

 


그림 3-45 객체 탐색기

다음으로 파이썬에서 UI 파일을 읽어 윈도우를 출력해주는 코드를 구현해야 합니다. 이 부분은 외울 필요 없이 기존 코드를 ‘복사/붙여넣기’ 하면 됩니다. 매매 알고리즘 등의 중요한 코드에 집중합시다.

01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5 import uic 04: 05: form_class = uic.loadUiType("window.ui")[0] 06: 07: class MyWindow(QMainWindow, form_class): 08: def __init__(self): 09: super().__init__() 10: self.setupUi(self) 11: 12: 13: app = QApplication(sys.argv) 14: window = MyWindow() 15: window.show() 16: app.exec_()

위 코드를 실행할 때 파이썬 코드와 UI 파일인 'window.ui' 파일이 같은 폴더에 위치해야 합니다. 그리고 UI 파일 이름이 window.ui가 아닌 경우 파일 이름을 수정해서 사용해야 합니다.

 


그림 3-46 코빗 시세 조회기 실행화면

이번에는 조회 버튼을 누르면 “조회 버튼 클릭”이라는 문자열을 콘솔에 출력하도록 코드를 수정해 보겠습니다. 우선 '조회 버튼 클릭' 문자열을 화면에 출력하는 inquiry 메서드를 구현합니다. 다음으로 클릭 이벤트가 발생했을 때 메서드가 호출되도록 메서드와 이벤트를 연결합니다. MyWindow 클래스의 초기화자(init)에 clicked 이벤트 처리 코드를 추가해줍니다. 앞으로 버튼 위젯이 클릭되면 자동으로 현재 윈도우 객체(self)의 inquiry 메서드가 호출될 것입니다.

# ch03/03_22.py class MyWindow(QMainWindow, form_class): def __init__(self): super().__init__() self.setupUi(self) self.pushButton.clicked.connect(self.inquiry) def inquiry(self): print("조회 버튼 클릭")

프로그램을 다시 실행한 후 버튼을 클릭해 봅시다. 다음과 같이 콘솔 창에 '조회 버튼 클릭'이라는 문자열이 출력됩니다.

C:\Anaconda3\python.exe C:/Users/brayden/Documents/GitHub/book-cryptocurrency/ch03/03_13.py 조회 버튼 클릭 조회 버튼 클릭 조회 버튼 클릭

이벤트와 메서드의 연결을 확인하는 용도로 콘솔 창에 문자열을 출력해 봤습니다. inquiry 메서드 안에서 코빗 현재가를 얻어 오도록 수정해 봅시다. 이를 위해서 필자들이 만든 모듈인 pykorbit 모듈을 설치합니다. 먼저 윈도우의 시작 메뉴에서 Anaconda3을 찾은 후 Anaconda Prompt를 실행합니다. 그림 3-47과 같은 창이 팝업되면 ‘pip install pykorbit’ 명령을 입력해서 모듈을 설치합니다.

 


그림 3-47 pykorbit 모듈 설치

코빗뿐만 아니라 업비트, 빗썸 모듈의 사용법을 8장에서 자세히 다룹니다. 현재 단계에서는 pykorbit 모듈에 있는 함수를 호출하면 비트코인의 현재가를 쉽게 얻을 수 있는데 이를 위해서 pykorbit 모듈을 설치했다고 생각하면 됩니다.

# ch03/03_23.py 01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5 import uic 04: import pykorbit 05: 06: form_class = uic.loadUiType("window.ui")[0] 07: 08: class MyWindow(QMainWindow, form_class): 09: def __init__(self): 10: super().__init__() 11: self.setupUi(self) 12: self.pushButton.clicked.connect(self.inquiry) 13: 14: def inquiry(self): 15: price = pykorbit.get_current_price("BTC") 16: print(price)

라인 4: pykorbit 모듈을 사용하기 위해 먼저 임포트합니다.
라인 15: pykorbit 모듈의 get_current_price() 함수를 호출합니다. 함수의 인자로 "BTC"를 넘겨주면 비트코인에 대한 현재가를 얻어옵니다. 얻어온 비트코인 현재가는 price 변수가 바인딩합니다.
라인 16: price 변수가 바인딩하는 값을 화면에 출력합니다.

위 코드를 실행한 후 버튼 위젯을 클릭하면 다음과 같이 비트코인의 현재가가 출력됩니다.

C:\Anaconda3\python.exe C:/Users/brayden/Documents/GitHub/book-cryptocurrency/ch03/03_14.py 7365000.0 7365000.0 7365000.0

이번에는 pykorbit 모듈을 통해 얻어온 현재가를 QLineEdit 위젯에 출력해 보도록 하겠습니다. inquiry 메서드의 price 변수가 바인딩하는 값을 lineEdit의 setText() 메서드로 넘겨줍니다. setText 메서드는 문자열을 입력받기 때문에 price (float 타입)를 문자열로 형 변환하는 것을 주의해야 합니다.

# ch03/03_24.py 14: def inquiry(self): 15: price = pykorbit.get_current_price("BTC") 16: self.lineEdit.setText(str(price))

수정한 코드를 실행해 봅시다. 조회 버튼을 클릭하면 그림 3-48과 같이 비트코인의 실시간 현재가가 QLineEdit 위젯에 출력됩니다.

 


그림 3-48 코빗 시세 조회기

 

 

QTimer

앞서 만들어본 코빗 시세 조회기는 한 가지 불편한 점이 있습니다. 바로 현재 금액을 알고 싶을 때마다 '조회' 버튼을 클릭해야 한다는 점이죠. 사실 이렇게 프로그램을 만들면 아무도 이 프로그램을 사용하지 않을 겁니다. 어떻게 하면 일정 시간마다 자동으로 현재가를 조회하고 이를 QLineEdit 위젯에 출력할 수 있을까요? 일정한 시간마다 되풀이되는 작업을 수행하고자 할 때는 PyQt의 QTimer를 사용합니다.

 


그림 3-49 QTimer를 이용한 시간 출력

QTimer를 이해하기 위해 간단한 코드를 작성해 봅시다. 다음 코드는 그림 3-49같이 현재 시간을 왼쪽 아래에 출력하는 프로그램입니다. 매초 주기적으로 시간을 문자열로 출력해서 시계처럼 보이도록 눈속임합니다.

QTimer를 사용하려면 윈도우가 생성될 때 QTimer 객체를 생성해야 합니다. 생성한 객체에는 interval을 추가 설정합니다. interval이란 얼마나 자주 이벤트가 발생하는지를 의미합니다. 예를 들어, 1초에 한 번씩 또는 5초에 한 번씩이라면 1초 또는 5초가 interval에 해당합니다. 다음 코드는 QTimer 객체를 만들고 interval로 1초로 설정합니다.

self.timer = QTimer(self) self.timer.start(1000)

QPushButton에 'clicked'라는 이벤트가 있듯이 QTimer에는 'timeout'이라는 이벤트가 있습니다. 'timeout' 이벤트는 앞서 설정한 interval마다 발생하는 이벤트입니다. 예를 들어 아래의 코드와 같이 interval을 1초로 설정한 QTimer 객체를 만들면 1초에 한 번씩 timeout 이벤트가 발생하고 이벤트 루프는 1초에 한 번 self.timeout()이라는 메서드를 호출합니다.

self.timer = QTimer(self) self.timer.start(1000) self.timer.timeout.connect(self.timeout)

MyWindow 클래스의 timeout 메서드는 다음과 같이 구현합니다. 먼저 QTime 객체의 currentTime() 메서드를 호출하여 현재 시각을 얻어온 후 이를 문자열 타입으로 변경합니다. 다음으로 윈도우의 상태바에 얻어온 현재 시간을 출력합니다. timeout 메서드가 이벤트 루프에 의해 1초에 한 번씩 호출되기 때문에 시계처럼 동작합니다.

def timeout(self): cur_time = QTime.currentTime() str_time = cur_time.toString("hh:mm:ss") self.statusBar().showMessage(str_time)

아래는 전체 코드입니다.

# ch03/03_25.py 01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5.QtCore import * 04: 05: 06: class MyWindow(QMainWindow): 07: def __init__(self): 08: super().__init__() 09: self.timer = QTimer(self) 10: self.timer.start(1000) 11: self.timer.timeout.connect(self.timeout) 12: 13: def timeout(self): 14: cur_time = QTime.currentTime() 15: str_time = cur_time.toString("hh:mm:ss") 16: self.statusBar().showMessage(str_time) 17: 18: 19: app = QApplication(sys.argv) 20: window = MyWindow() 21: window.show() 22: app.exec_()

QTimer를 배웠으니 앞서 만든 예제를 업데이트해 보겠습니다. Qt Designer에서는 버튼 위젯을 삭제하고 저장합니다. 파이썬 코드에서는 버튼 클릭 이벤트를 정의한 자리에 Qtimer를 추가하고 inquiry 메서드와 연결합니다. 이제는 1초 주기로 inquiry 메서드가 호출됩니다. inquiry 메서드 안에서는 비트 코인의 가격을 가져와서 lineEdit에 출력합니다.

# ch03/03_26.py 01: import sys 02: from PyQt5.QtWidgets import * 03: from PyQt5 import uic 04: from PyQt5.QtCore import * 05: import pykorbit 06: 07: form_class = uic.loadUiType("window.ui")[0] 08: 09: class MyWindow(QMainWindow, form_class): 10: def __init__(self): 11: super().__init__() 12: self.setupUi(self) 13: 14: self.timer = QTimer(self) 15: self.timer.start(1000) 16: self.timer.timeout.connect(self.inquiry) 17: 18: def inquiry(self): 19: cur_time = QTime.currentTime() 20: str_time = cur_time.toString("hh:mm:ss") 21: self.statusBar().showMessage(str_time) 22: price = pykorbit.get_current_price("BTC") 23: self.lineEdit.setText(str(price)) 24: 25: app = QApplication(sys.argv) 26: window = MyWindow() 27: window.show() 28: app.exec_()

마지막 편집일시 : 2021년 3월 27일 8:51 오후

 

03-7 PyQt 시그널 슬롯

PyQt 사용자 정의 시그널/슬롯

PyQt는 위젯에 정의된 이벤트를 시그널(signal)이라고 부르고 이벤트가 발생할 때 호출되는 함수나 메서드를 슬롯(slot)이라고 부릅니다. 예를 들어 btn 이라는 변수가 바인딩하는 QPushButton이 클릭될 때 btn_clicked()라는 메서드가 호출되도록 하려면 다음과 같이 코딩해주면 됐었지요?

btn.clicked.connect(self.btn_clicked)

만약 버튼에 마우스 왼쪽 버튼 클릭에 대한 이벤트가 아니라 마우스 오른쪽 버튼에 대한 클릭 이벤트가 왔을 때 어떤 동작을 수행하고자 하면 어떻게 해야할까요? 이미 QPushButton 위젯에 'right_button_clicked'라는 시그널이 정의되어 있는걸까요? 아쉽게도 버튼에 정의된 'clicked'라는 이벤트는 마우스 왼쪽 버튼이나 오른쪽 버튼을 구분하지 않습니다. 사용자가 버튼 위젯에서 어떤 마우스 버튼이라도 클릭하면 이벤트가 발생하게 됩니다.

PyQt는 프로그래머가 원하는 이벤트(시그널)을 직접 정의할 수 있도록 해줍니다. 이번 절에서는 여러분이 직접 시그널을 정의하고 해당 시그널을 처리하는 슬롯을 연결하는 방법에 대해 배워보도록 하겠습니다.

먼저 다음 코드를 실행해 봅시다. 코드가 실행되면 윈도우가 출력되고 'signal1 emitted'라는 문자열이 출력됩니다.

import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * class MySignal(QObject): signal1 = pyqtSignal() def run(self): self.signal1.emit() class MyWindow(QMainWindow): def __init__(self): super().__init__() mysignal = MySignal() mysignal.signal1.connect(self.signal1_emitted) mysignal.run() @pyqtSlot() def signal1_emitted(self): print("signal1 emitted") app = QApplication(sys.argv) window = MyWindow() window.show() app.exec_()

동작을 확인했다면 이제 코드를 살펴 봅시다. 사용자가 정의 시그널 (clicked와 같은 이벤트를 직접 만드는 것을 의미)을 만들기 위해서 MySignal이라는 이름의 클래스를 정의합니다. 그리고 클래스에서 pyqtSignal 객체를 생성해주면 됩니다. 여기서 주의할점은 pyQtSignal 객체를 인스턴스 변수로 만드는 것이 아니라 클래스 변수로 만들어야 한다는 점입니다. 현재 pyqtSignal() 객체를 signal1 이라는 변수가 바인딩하고 있는데 여기서 'signal1'이 시그널(이벤트)의 이름이 됩니다. QPushButton에는 'clicked'라는 시그널이 있었지요? 여러분이 만든 MySignal이라는 클래스에는 'signal1'이라는 시그널이 있는겁니다.

MySignal 클래스에는 run() 이라는 메서드가 있는데 해당 메서드에서는 우리가 정의한 시그널인 'signal1'에서 emit() 메서드를 호출합니다. (signal1은 pyqtSignal 클래스의 인스턴스이므로 pyqtSignal 클래스에 정의된 emit() 메서드를 호출할 수 있답니다.)

class MySignal(QObject): signal1 = pyqtSignal() # class variable def run(self): self.signal1.emit()

이번에는 MyWindow 클래스를 살펴봅시다. MyWindow 클래스의 생성자에서 앞서 여러분이 정의한 MySignal 클래스의 객체를 생성합니다. 이것은 QPushButton 객체를 생성하는 것과 동일한겁니다. 그리고 MySignal에 정의된 시그널인 'signal1'이 발생하면 MyWindow 클래스에 정의된 'signal1_emitted' 메서드가 호출되도록 시그널과 슬롯을 연결해줬습니다. 마지막으로 생성자에서는 MySignal 클래스에 정의된 run() 메서드를 호출합니다.

MySignal 객체로부터 'signal1' 이벤트가 발생하면 이벤트 루프는 signal1_emitted()라는 메서드를 호출해줍니다. 그렇다면 'signal1' 이벤트는 언제 발생할까요? 정답은 run() 메서드가 호출될 때 emit() 메서드를 호출하는데 그 순간에 'signal1' 이벤트가 발생합니다. MyWindow 클래스의 정의된 'signal1_emitted' 메서드를 보면 이전과 달리 '@pyqtSlot()'이라는 데커레이터가 있습니다. 파이썬 데커레이터에 대한 설명은 이 책의 범위를 벗어나므로 독자 여러분은 "시그널과 슬롯을 연결할 때 데커레이터를 적어주면 더 좋다" 정도로만 이해하시면 되겠습니다. 데커레이터는 말 그대로 '장식자'라는 의미로 메서드에 추가로 어떤 장식을 해둔 것이라고 생각하면 됩니다.

class MyWindow(QMainWindow): def __init__(self): super().__init__() mysignal = MySignal() mysignal.signal1.connect(self.signal1_emitted) mysignal.run() @pyqtSlot() def signal1_emitted(self): print("signal1 emitted")

그림으로 표현하면 다음과 같습니다. MySignal 객체가 emit()를 호출하여 시그널을 발생시키면 MyWindow에 정의된 메서드(슬롯)가 호출됩니다.

PyQt의 시그널/슬롯을 이용하면 특정 시점에 어떤 클래스에서 다른 클래스로 데이터를 쉽게 보내줄 수 있습니다. 그렇다면 이런게 왜 필요할까요? 여러분은 앞서 타이머를 사용해서 비트코인의 금액을 얻어왔습니다. 그런데 타이머를 사용한 프로그램은 코드가 간단하기는 하지만 비트코인 금액을 얻어올 때는 다른 작업을 동시에 수행할 수 없었습니다. 만일 비트코인 금액을 조회하는데 1초 이상 소요가 된다면 1초 동안 GUI는 갱신되지 않고 멈춰있는 것처럼 보이게 됩니다. 이런 문제를 해결하기 위해 우리는 스레드라는 기술을 사용할겁니다. 스레드를 사용하면 동시에 여러 가지 일을 처리할 수 있습니다. 예를 들어 비트코인, 리플, 이더리움과 같은 여러 개의 가상화폐 금액을 동시에 조회하더라도 GUI가 멈춰있지 않고 동시에 다른 이벤트도 처리할 수 있게 됩니다.

타이머를 사용하는 경우에는 금액을 조회할 때 MyWindow는 멈춰있기 때문에 금액이 조회된 후에 그 값을 가져다가 값을 출력하는 겁니다. 이와 달리 스레드를 사용하게 되면 금액을 조회하는 일과 얻어온 값을 출력하는 일이 거의 동시에 진행되기 때문에 조회된 금액을 MyWindow 클래스로 보내주는 방법이 필요합니다. 여기서 바로 여러분들이 지금 배우고 있는 사용자 정의 시그널/슬롯을 사용하는 겁니다.

 

 

시그널로 데이터 보내기

이번에는 다음 그림과 같이 시그널을 통해서 한 객체로부터 다른 객체로 값을 보내보겠습니다. 가상화폐 금액 조회기 같은 프로그램을 만들 때 한 클래스에서는 금액을 조회하고 다른 클래스에서는 조회된 값을 GUI에 값을 출력하는 경우를 생각해 봅시다. 이 경우 금액을 조회하는 역할의 클래스는 금액 조회가 완료되자마자 시그널을 발생시키면 됩니다.

 

emit(1, 2)

MySignal

MyWindow

그림 3-50 사용자 정의 시그널로 데이터 보내기

먼저 다음 코드를 실행시켜서 동작을 확인해 봅시다. 앞서 사용자 정의 시그널의 예제와 다른 점은 'signal2'라는 이름의 시그널을 발생할 때 정수 값 두 개를 같이 보낼 수 있다는 점입니다. 이벤트 루프는 시그널이 발생하면 미리 정의된 슬롯을 호출해주게 되는데 이때 시그널로부터 온 값을 슬롯으로 보내주게 됩니다.

import sys from PyQt5.QtWidgets import * from PyQt5.QtCore import * class MySignal(QObject): signal1 = pyqtSignal() signal2 = pyqtSignal(int, int) def run(self): self.signal1.emit() self.signal2.emit(1, 2) class MyWindow(QMainWindow): def __init__(self): super().__init__() mysignal = MySignal() mysignal.signal1.connect(self.signal1_emitted) mysignal.signal2.connect(self.signal2_emitted) mysignal.run() @pyqtSlot() def signal1_emitted(self): print("signal1 emitted") @pyqtSlot(int, int) def signal2_emitted(self, arg1, arg2): print("signal2 emitted", arg1, arg2) app = QApplication(sys.argv) window = MyWindow() window.show() app.exec_()

먼저 MySignal 이라는 클래스를 살펴봅시다. 'signal2'라는 이름의 시그널 객체를 만들었는데 이때 전달하고자하는 데이터 타입을 기술한점이 다릅니다. 그리고 emit() 메서드를 호출하여 시그널을 발생시킬 때 슬롯으로 전달하고자 하는 값을 넘겨주는 점에서 차이가 있습니다.

class MySignal(QObject): signal1 = pyqtSignal() signal2 = pyqtSignal(int, int) def run(self): self.signal1.emit() self.signal2.emit(1, 2)

다음은 MyWindow 클래스에 정의된 슬롯(메서드)를 살펴봅시다. 슬롯은 시그널이 발생할 때 호출당하는 콜백 함수였지요? 이때 시그널로부터 데이터가 전송되기 때문에 이를 메서드의 인자를 통해 받아주면 됩니다. 그리고 데커레이터에서도 타입을 적어주면 됩니다.

@pyqtSlot(int, int) def signal2_emitted(self, arg1, arg2): print("signal2 emitted", arg1, arg2)

마지막 편집일시 : 2021년 3월 27일 8:56 오후

 

728x90
반응형

'IT News' 카테고리의 다른 글

upbit 업비트 websocket설치  (0) 2021.04.18
업비트 ip 받아오기 key발급받기  (0) 2021.04.18
반도체  (0) 2021.03.27
wol 접속방법  (0) 2021.02.11
visual studio exe화일만들기  (0) 2020.12.08

댓글

💲 추천 글