PyQt 예제 1.7.2

이번 글에서는 시그널을 직접 생성해볼 것이다.
레이아웃 설정 부분 펼치기
-__version__ = "1.6.2"
+__version__ = "1.7.1"
 
 class Form(QDialog):
     def __init__(self, parent=None):
         super().__init__(parent)
 
-        self.label = QLabel("Hello, PyQt!")
-        self.labelFormat = "{}"
+        simple_partName = QLabel("Simple Widget")
+        self.simple_label = QLabel("Hello, PyQt!")
+        self.simple_labelFormat = "{}"
 
-        self.lineEdit = QLineEdit("This is LineEdit Widget")
-        self.boldCheckBox = QCheckBox("굵게(&B)")
-        initButton = QPushButton("초기화(&I)")
-        initButton.setAutoDefault(False)
-        closeButton = QPushButton("닫기(&C)")
-        closeButton.setAutoDefault(False)
+        self.simple_lineEidt = QLineEdit("This is LineEdit Widget")
+        self.simple_boldCheckBox = QCheckBox("굵게(&B)")
+        simple_initButton = QPushButton("초기화(&I)")
+        simple_initButton.setAutoDefault(False)
         
-        buttonLayout = QHBoxLayout()
-        buttonLayout.addWidget(initButton)
-        buttonLayout.addWidget(closeButton)
-
-        layout = QGridLayout()
-        layout.addWidget(self.label, 0, 0, 1, 2)
-        layout.addWidget(self.lineEdit, 1, 0, 1, 2)
-        layout.addWidget(self.boldCheckBox, 2, 0)
-        layout.addLayout(buttonLayout, 3, 0)
+        simple_buttonLayout = QHBoxLayout()
+        simple_buttonLayout.addWidget(simple_initButton)
+        simple_buttonLayout.addStretch()
+
+        simple_layout = QGridLayout()
+        simple_layout.addWidget(simple_partName, 0, 0, 1, 2)
+        simple_layout.addWidget(self.simple_label, 1, 0, 1, 2)
+        simple_layout.addWidget(self.simple_lineEidt, 2, 0, 1, 2)
+        simple_layout.addWidget(self.simple_boldCheckBox, 3, 0)
+        simple_layout.addLayout(simple_buttonLayout, 4, 0)
+
+        signal_partName = QLabel("Signal Test")
+        signal_spinCountLabel1 = QLabel("0")
+        signal_spinCountLabel2 = QLabel("0")
+        signal_spinCountLabel3 = QLabel("0")
+        signal_spinBox = MySignalSpinBox()
+
+        signal_layout = QVBoxLayout()
+        signal_layout.addWidget(signal_partName)
+        signal_layout.addWidget(signal_spinCountLabel1)
+        signal_layout.addWidget(signal_spinCountLabel2)
+        signal_layout.addWidget(signal_spinCountLabel3)
+        signal_layout.addWidget(signal_spinBox)
+        signal_layout.addStretch()
+
+        layout = QHBoxLayout()
+        layout.addLayout(simple_layout)
+        layout.addSpacing(30)
+        layout.addLayout(signal_layout)
+
         self.setLayout(layout)
접기
-        self.connect(self.lineEdit, SIGNAL("returnPressed()"),
-                        self.UpdateLabel)
-        self.connect(self.boldCheckBox, SIGNAL("stateChanged(int)"),
-                        self.LabelBold)
-        self.connect(initButton, SIGNAL("clicked()"), self.Initialize)
-        self.connect(closeButton, SIGNAL("clicked()"),
-                        self, SLOT("reject()"))
+        self.connect(self.simple_lineEidt, SIGNAL("returnPressed()"),
+                        self.Simple_UpdateLabel)
+        self.connect(self.simple_boldCheckBox, SIGNAL("stateChanged(int)"),
+                        self.Simple_LabelBold)
+        self.connect(simple_initButton, SIGNAL("clicked()"), self.Simple_Initialize)
+        self.connect(signal_spinBox, SIGNAL("countup(QString)"), 
+                        signal_spinCountLabel1, SLOT("setText(QString)"))
+        self.connect(signal_spinBox, SIGNAL("countup(QString)"), 
+                        signal_spinCountLabel2.setText)
+        self.connect(signal_spinBox, SIGNAL("countup"), 
+                        signal_spinCountLabel3.setText)
 
         self.setWindowTitle("Main Dialog")
 
-    def UpdateLabel(self):
-        self.label.setText(self.labelFormat.format(self.lineEdit.text()))
+    def Simple_UpdateLabel(self):
+        self.simple_label.setText(self.simple_labelFormat.format(
+                    self.simple_lineEidt.text()))
 
-    def LabelBold(self):
-        if self.boldCheckBox.isChecked():
-            self.labelFormat = "{}"
+    def Simple_LabelBold(self):
+        if self.simple_boldCheckBox.isChecked():
+            self.simple_labelFormat = "{}"
         else:
-            self.labelFormat = "{}"
-        self.UpdateLabel()
-
-    def Initialize(self):
-        self.labelFormat = "{}"
-        self.boldCheckBox.setCheckState(Qt.Unchecked)
-        self.lineEdit.setText("This is LineEdit Widget")
-        self.label.setText("Hello, PyQt!")
+            self.simple_labelFormat = "{}"
+        self.Simple_UpdateLabel()
+
+    def Simple_Initialize(self):
+        self.simple_labelFormat = "{}"
+        self.simple_boldCheckBox.setCheckState(Qt.Unchecked)
+        self.simple_lineEidt.setText("This is LineEdit Widget")
+        self.simple_label.setText("Hello, PyQt!")
     
+class MySignalSpinBox(QSpinBox):
+    changedCount = 0
+
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self.connect(self, SIGNAL("valueChanged(int)"), self.CountChanged)
+
+    def CountChanged(self):
+        self.changedCount += 1
+        self.emit(SIGNAL("countup(QString)"), str(self.changedCount))
+        self.emit(SIGNAL("countup"), str(self.changedCount))
+
 if __name__ == "__main__":
     app = QApplication(sys.argv)
     form = Form()
상당한 양의 코드가 추가되었다. 레이아웃 설정 부분은 이미 다루어본 내용들이므로 접어두었다.

중요한 부분은 12 ~ 18 라인의 커넥트 부분이다. 각 커넥트 메소드마다 SIGNAL로 countup 이라는 시그널을 받고 있다.

이 시그널은 기본적으로 제공되는 시그널이 아니라 직접 생성한 시그널이다. 시그널을 생성하는 곳은 맨 아래에 추가 class 에서 시그널을 생성하고 있다.

QSpinBox를 상속한 다음, 생성자에서 원래 제공되는 valueChanged(int) 시그널을 자신의 메소드인 CountChanged에 연결했다. 그리고 메소드에서는 클래스 변수를 증가시키도록 했다. 그리고 아래에 새로운 메소드가 있는데, emit 메소드를 통해서 새로운 시그널을 보낼 수 있다.

보낼 시그널은 countup(QString) 시그널이고, 보내지는 인자는 str(self.chagnedCount) 이다. 또, 그 아래에 countup 시그널을 보내고 있고, 같은 인자를 포함하고 있다.


다시 위로 올라가서 보면, 첫번째 커넥트에서는 countup(QString) 시그널을 받고 있다. 이렇게 괄호의 인자와 함께 보내지는 시그널은 C++ 객체로 변환이 되기 때문에 기본적으로 제공되는 Qt 슬롯과 연결이 가능하다.

따라서 setText(QString) 메소드와 커넥트를 시킬 수 있다. 마찬가지로 C++ 객체로 변환이 된다는 것은 다시 파이썬 객체로 변환 될 수 있으므로 파이썬 메소드와도 커넥트 시킬 수 있다. 이것이 그 아래 커넥트 메소드이다.

마지막 커넥트는 시그널에 아무런 인자 표시가 되어 있지 않지만 인자를 보내고는 있다. 이런 방식으로 쓰인 시그널을 "short-circuit" 시그널이라고 한다. 이는 바로 파이썬 객체로 넘겨지기 때문에 기본 적으로 제공되는 Qt 슬롯과 연결될 수 없다. 따라서 파이썬 메소드와만 커넥트가 가능하다.


정리하면, 괄호 인자와 함께 보내지는 시그널들은 기본적으로 제공되는 Qt 시그널(이전에 다룬 시그널들)이거나 "short-circuit" 시그널이 아닌 "non-short-circuit" 시그널(이번에 새로 만든 시그널들)이다. 이들은 모두 C++ 객체로 변환 될 수 있어야 한다.

그리고 괄호 인자 없이 보내지는 시그널은 short-circuit 시그널이고, 파이썬 객체를 직접 넘기게 된다. 따라서 Qt 슬롯에 커넥트 할 수 없다.


이러한 이유 때문에, Qt 시그널이나 non-short-circuit 파이썬 시그널들은 C++ 로 객체가 변환되는 오버헤드가 요구된다. 하지만 short-circuit 파이썬 시그널의 경우에는 이러한 변환이 필요 없으므로 오버헤드를 피할 수 있다.


마지막으로, 시그널과 슬롯을 연결할 때, 당연한 것이기도 하지만, 보내는 인자와 받는 인자 사이의 타입이 맞아야 한다.

그리고 괄호 인자와 함께 쓰는 non-short-circuit 파이썬 시그널에서도 PyQt_PyObject 라는 인자를 사용함으로써 C++로 변환될 수 없는 파이썬 객체를 넘길 수도 있다.


최종 정리: PyQt 레퍼런스의 old style signals slots에 보면 아래와 같이 정리가 되어 있다.

QtCore.QObject.connect(a, QtCore.SIGNAL('QtSig()'), pyFunction)
QtCore.QObject.connect(a, QtCore.SIGNAL('QtSig()'), pyClass.pyMethod)
QtCore.QObject.connect(a, QtCore.SIGNAL('QtSig()'), b, QtCore.SLOT('QtSlot()'))
QtCore.QObject.connect(a, QtCore.SIGNAL('PySig()'), b, QtCore.SLOT('QtSlot()'))
QtCore.QObject.connect(a, QtCore.SIGNAL('PySig'), pyFunction)

1~3 번이 Qt 시그널, 4번은 non-short-circuit 파이썬 시그널, 5번은 short-circuit 파이썬 시그널이다.


추가로 덧붙이면, PyQt 4.5 이상에서는 새로운 스타일의 시그널-슬롯이 있다. 이 부분은 책에도 안 나오는 내용이기 때문에, 이후에 배우게 되면 따로 다루겠다.

댓글 없음:

댓글 쓰기