ต่อจาก
บทที่ ๒๓
ในบทนี้จะเป็นการอธิบายรายละเอียดเกี่ยวกับเรื่องของสัญญาณ (signal) ที่เกิดขึ้นภายใน widget ซึ่งเป็นเรื่องที่ค่อนข้างเข้าใจยาก แต่หากเข้าใจก็จะทำให้เข้าใจความหมายของการใช้การเขียนกำหนดคำสั่งด้วย .connect() ดังที่ทำใน widget ชนิดต่างๆในบทที่ผ่านมาได้ และสามารถไปประยุกต์ใช้ทำอะไรได้มากขึ้น
ว่าด้วยเรื่องออบเจ็กต์สัญญาณ
ปกติแล้วใน widget ชนิดต่างๆจะมีออบเจ็กต์ตัวสัญญาณชนิดต่างๆอยู่ ซึ่งถ้าใช้เมธอด .connect ที่ตัวออบเจ็กต์นั้นก็จะเชื่อมต่อฟังก์ชันที่ต้องการให้ทำเมื่อเกิดสัญญาณนั้นขึ้นได้
ตัว_widget.สัญญาณ.connect(ฟังก์ชันที่จะให้ทำ)
เช่นใน QPushButton ก็จะมีสัญญาณ .clicked .pressed .released ดังที่เขียนถึงไปใน
บทที่ ๔ ซึ่งเมื่อเชื่อมต่อฟังก์ชันโดยใส่ลงใน .click.connect ก็จะทำให้ฟังก์ชันนั้นถูกเรียกใช้เมื่อเวลาที่ปุ่มถูกคลิก เป็นต้น
และในทำนองเดียวกัน widget ชนิดอื่นเช่นๆ QLineEdit มี .retuenEnter .textEdited .editFinished .textChanged เป็นต้น ดังที่ได้เขียนถึงไปใน
บทที่ ๙
ที่จริงแล้วตัวออบเจ็กต์สัญญาณนั้นคือออบเจ็กต์ของคลาส PyQt5.QtCore.pyqtBoundSignal และ .connect ก็เป็นเมธอดหนึ่งของออบเจ็กต์คลาสนั้น
เพื่อให้เห็นภาพ ลองสร้างปุ่ม QPushButton ขึ้นมาแล้วมาดูที่ .clicked
import sys
from PyQt5.QtWidgets import QApplication,QPushButton
qAp = QApplication(sys.argv)
pumkot = QPushButton()
print(pumkot.clicked.__class__) # <class 'PyQt5.QtCore.pyqtBoundSignal'>
print(pumkot.clicked.connect) # <built-in method connect of PyQt5.QtCore.pyqtBoundSignal object at 0x7fa26c5824b0>
จะเห็นว่า QPushButtonใclicked นั้นเป็นข้อมูลชนิด pyqtBoundSignal และมีเมธอด .connect อยู่
การทำให้เกิดสัญญาณขึ้นมาเอง {.emit}
ออบเจ็กต์สัญญาณนั้นนอกจากจะมีเมธอด .connect เอาไว้เชื่อมกับฟังก์ชันที่ต้องการให้ทำเมื่อเกิดสัญญาณขึ้นมาแล้ว ยังมีเมธอด .emit ซึ่งเอาไว้ปล่อยสัญญาณ เพื่อให้ฟังก์ชันที่ตั้งไว้ใน .connect นั้นทำงานด้วย
โดยปกติแล้วตัว widget จะทำการเรียกเมธอด .emit ขึ้นมาเองระหว่างเกิด event ต่างๆ เราจึงมักไม่ต้องเรียกเมธอด .emit โดยตรง
เช่นถ้าเป็น QPushButton เมื่อปุ่มถูกคลิกก็จะมีการปล่อยสัญญาณ ทำให้ฟังก์ชันใน .connect ทำงาน แต่หากต้องการจะให้อยู่ดีๆฟังก์ชันนั้นทำงานขึ้นมาเองโดยไม่ต้องกดปุ่ม เราก็อาจเรียก .emit เองโดยตรงได้
ตัวอย่างเช่นลองสร้าง QPushButton ขึ้นมาแล้ว .clicked.connect ตั้งฟังก์ชัน แล้วใช้ .clicked.emit ให้ฟังก์ชันนั้นทำงานทันที
import sys
from PyQt5.QtWidgets import QApplication,QPushButton
qAp = QApplication(sys.argv)
pumkot = QPushButton()
pumkot.clicked.connect(lambda: print('ปุ่มถูกกด'))
pumkot.clicked.emit() # ทำงานเหมือนเวลาถูกคลิก
ลองดูตัวอย่างที่เห็นภาพชัดขึ้น คือเช่นสร้างปุ่ม QPushButton กับช่องติ๊ก QCheckBox ขึ้นมา แล้วตั้งให้เวลาที่กดช่องติ๊กแล้วมีการทำงานเหมือนตอนปุ่มถูกกดไปด้วย
import sys
from PyQt5.QtWidgets import QApplication,QWidget,QPushButton,QCheckBox,QHBoxLayout
class Natang(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Tahoma; font-size: 19px;')
hbl = QHBoxLayout()
self.setLayout(hbl)
self.pumkot = QPushButton('ปุ่มกด')
hbl.addWidget(self.pumkot)
self.pumkot.clicked.connect(self.pumthukkot)
self.chongtick = QCheckBox('ช่องติ๊ก')
hbl.addWidget(self.chongtick)
self.chongtick.toggled.connect(self.chongthuktick)
self.show()
def pumthukkot(self): # เมื่อปุ่มถูกกด
print('ปุ่มถูกกด')
def chongthuktick(self): # เมื่อช่องถูกติ๊ก
print('ช่องถูกติ๊ก')
self.pumkot.clicked.emit() # ให้ปล่อยสัญญาณเหมือนตอนปุ่มถูกกดไปด้วย
qAp = QApplication(sys.argv)
natang = Natang()
qAp.exec_()
ก็จะได้หน้าต่างที่มีปุ่มกดและช่องติ๊ก ถ้าหากกดปุ่มก็จะขึ้นข้อความว่า "ปุ่มถูกกด" แต่ถ้ากดที่ช่องติ๊ก จะขึ้นข้อความ "ช่องถูกติ๊ก" แล้วตามมาด้วย "ปุ่มถูกกด" เพราะในฟังก์ชันที่กำหนดให้ทำเมื่อถูกติ๊กนั้นมีการใช้ .emit ปล่อยสัญญาณที่เกิดขึ้นเหมือนตอนปุ่มถูกคลิกด้วย อย่างไรก็ตาม ปุ่มไม่ได้ถูกกดจริงๆ แค่มีสัญญาณเหมือนตอนถูกกดออกมาเพื่อสั่งให้ทำฟังก์ชันที่เชื่อมต่ออยู่เท่านั้น
การสร้างออบเจ็กต์สัญญาณขึ้นมาใหม่เอง {pyqtSignal}
นอกจากออบเจ็กต์สัญญาณที่มีอยู่ในตัว widget แต่ละชนิดเองแล้ว เรายังสามารถสร้างสัญญาณขึ้นมาใหม่ใส่ให้ตัว widget ได้ โดยการที่เมื่อเขียนคลาสใหม่ขึ้นมาให้ทำการสร้างตัวสัญญาณแล้วตั้งให้เป็นแอตทริบิวต์ของคลาสไปด้วย
ออบเจ็กต์ตัวสัญญาณสร้างได้จากคลาส PyQt5.QtCore.pyqtSignal
ตัวอย่างเช่นสร้างสัญญาณตัวหนึ่งขึ้นมา แล้วให้มันทำงานขึ้นมาเมื่อปุ่มถูกกดหรือช่องถูกติ๊ก อาจเขียนได้แบบนี้
import sys
from PyQt5.QtWidgets import QApplication,QWidget,QPushButton,QCheckBox,QVBoxLayout
from PyQt5.QtCore import pyqtSignal
class Natang(QWidget):
sanyan = pyqtSignal() # สร้างสัญญาณชื่อ sanyan ขึ้นมา
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Tahoma; font-size: 19px;')
vbl = QVBoxLayout()
self.setLayout(vbl)
self.pumkot = QPushButton('ปุ่มกด')
vbl.addWidget(self.pumkot)
self.pumkot.clicked.connect(self.pumthukkot)
self.chongtick = QCheckBox('ช่องติ๊ก')
vbl.addWidget(self.chongtick)
self.chongtick.toggled.connect(self.chongthuktick)
self.sanyan.connect(self.dairapsanyan) # สิ่งที่จะให้ทำเมื่อมีการปล่อยสัญญาณ
self.show()
def pumthukkot(self):
print('ปุ่มถูกกด')
self.sanyan.emit() # เรียกใช้สัญญาณ
def chongthuktick(self):
print('ช่องถูกติ๊ก')
self.sanyan.emit()
def dairapsanyan(self):
print('ปล่อยสัญญาณ') # เรียกใช้สัญญาณ
qAp = QApplication(sys.argv)
natang = Natang()
qAp.exec_()
อีกตัวอย่าง เช่นสร้างลูกเลื่อน ๒ อัน ให้เปรียบเทียบค่าใน ๒ อันนี้แล้วแสดงผลตรงกลาง โดยให้ทำตอนที่เปลี่ยนค่าในตัวเลื่อนตัวใดตัวหนึ่ง
import sys
from PyQt5.QtWidgets import QApplication,QWidget,QLabel,QSlider,QHBoxLayout
from PyQt5.QtCore import pyqtSignal,Qt
class Natang(QWidget):
thiap = pyqtSignal() # สร้างสัญญาณชื่อ thiap
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Tahoma; font-size: 17px;')
hbl = QHBoxLayout()
self.setLayout(hbl)
self.tualuean1 = QSlider() # แถบเลื่อนซ้าย
hbl.addWidget(self.tualuean1)
self.tualuean1.valueChanged.connect(self.thiap.emit) # ปล่อยสัญญาณเมื่อเปลี่ยนค่าในตัวเลื่อนขวา
self.khatang = QLabel('') # ข้อความตรงกลาง แสดงค่าต่าง
hbl.addWidget(self.khatang)
self.khatang.setFixedSize(180,50)
self.khatang.setAlignment(Qt.AlignCenter)
self.thiap.connect(self.muealuean)
self.tualuean2 = QSlider() # แถบเลื่อนขวา
hbl.addWidget(self.tualuean2)
self.tualuean2.valueChanged.connect(self.thiap.emit) # ปล่อยสัญญาณเมื่อเปลี่ยนค่าในตัวเลื่อนขวา
self.show()
def muealuean(self):
tangkan = self.tualuean1.value()-self.tualuean2.value() # ค่าต่าง
if(tangkan>0):
self.khatang.setText('ซ้ายมากกว่าขวา %d'%tangkan)
elif(tangkan<0):
self.khatang.setText('ขวามากกว่าซ้าย %d'%(abs(tangkan)))
else:
self.khatang.setText('เท่ากัน')
qAp = QApplication(sys.argv)
natang = Natang()
qAp.exec_()
ไขความข้องใจบางอย่าง
จากตัวอย่างที่ผ่านมาจะเห็นว่าใช้วิธีการสร้างคลาสโดยรับทอดจาก QWidget ขึ้นมา แล้วจึงใส่ pyqtSignal เป็นแอตทริบิวต์ในนั้น
แต่จริงๆแล้ว pyqtSignal ที่สร้างขึ้นมานั้นไม่จำเป็นต้องเป็นของ QWidget ก็ได้ แค่ใส่ในคลาสที่รับทอดจากคลาส PyQt5.QtCore.QObject ก็พอ
QObject เป็นคลาสหลักของออบเจ็กต์ต่างๆที่ใช้ใน pyqt รวมถึง QWidget เองก็เป็นซับคลาสของ QObject นี่ด้วย
ลองเขียน GUI ในตัวอย่างก่อนหน้าที่มีปุ่มกดกับช่องติ๊ก โดยเขียนแบบไม่สร้างคลาสของ widget ขึ้นมาใหม่ก็ได้ ดังนี้
import sys
from PyQt5.QtWidgets import QApplication,QWidget,QPushButton,QCheckBox,QVBoxLayout
from PyQt5.QtCore import pyqtSignal,QObject
class Q_O(QObject): # สร้างคลาสของ QObject แล้วใส่ pyqtSignal เป็นแอตทริบิวต์ข้างในนั้น
sanyan = pyqtSignal()
def pumthukkot():
print('ปุ่มถูกกด')
qo.sanyan.emit()
def chongthuktick():
print('ช่องถูกติ๊ก')
qo.sanyan.emit()
def dairapsanyan():
print('ปล่อยสัญญาณ')
qAp = QApplication(sys.argv)
natang = QWidget()
natang.setStyleSheet('font-family: Tahoma; font-size: 19px;')
qo = Q_O() # สร้างออบเจ็กต์ของคลาสที่เราสร้างขึ้นมา
qo.sanyan.connect(dairapsanyan) # เชื่อมต่อสัญญาณ
vbl = QVBoxLayout()
natang.setLayout(vbl)
pumkot = QPushButton('ปุ่มกด')
vbl.addWidget(pumkot)
pumkot.clicked.connect(pumthukkot)
chongtick = QCheckBox('ช่องติ๊ก')
vbl.addWidget(chongtick)
chongtick.toggled.connect(chongthuktick)
natang.show()
qAp.exec_()
อนึ่ง อ่านมาถึงตรงนี้อาจชวนให้สงสัยว่าทำไมต้องอุตส่าห์สร้าง pyqtSignal เป็นแอตทริบิวต์ของคลาสด้วย สร้าง pyqtSignal เป็นตัวแปรแยกขึ้นมาเฉยๆไม่ได้หรือ
แต่จริงๆทำอย่างนั้นไม่ได้ เพราะ pyqtSignal ที่สร้างขึ้นมาเฉยๆโดยไม่ได้ใส่ในคลาสของ QObject หรือ QWidget ใดๆนั้นจะไม่ทำงาน
เช่นลองดู จะเห็นว่า pyqtSignal ที่สร้างขึ้นมานั้นไม่มีเมธอด .emit
sanyan = pyqtSignal()
sanyan.emit() # ได้ AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'emit'
นอกจากนี้ ต่อให้สร้างออบเจ็กต์แล้วใส่เป็นแอตทริบิวต์ในตัวออบเจ็กต์แบบนี้ก็ไม่ได้เช่นกัน
class Q_O(QObject):
def __init__(self):
self.sanyan = pyqtSignal()
qo = Q_O()
qo.sanyan.emit() # ได้ AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'emit'
ตัวนั้นโดยทั่วไปแล้ววิธีการใช้นั้นจึงจำเป็นต้องสร้างให้เป็นแอตทริบิวต์ของคลาสที่รับทอดจาก QObject โดยตรง แบบนี้
class Q_O(QObject):
sanyan = pyqtSignal()
qo = Q_O()
qo.sanyan.emit() # ไม่มีปัญหา
print(qo.sanyan.__class__) # ได้ <class 'PyQt5.QtCore.pyqtBoundSignal'>
print(hasattr(qo.sanyan,'emit')) # ได้ True
print(hasattr(qo.sanyan,'connect')) # ได้ True
จะเห็นว่า pyqtSignal ที่ใส่เป็นแอตทริบิวต์ในคลาสจะกลายเป็นออบเจ็กต์ในคลาส pyqtBoundSignal ซึ่งจะมีเมธอด .emit และ .connect
ในขณะที่ถ้าสร้างขึ้นมาเฉยๆ ก็จะยังเป็นคลาส pyqtSignal เฉยๆอยู่ ซึ่งไม่มีเมธอด มีเมธอด .emit และ .connect
sanyan = pyqtSignal()
print(sanyan.__class__) # <class 'PyQt5.QtCore.pyqtSignal'>
print(hasattr(sanyan,'emit')) # False
print(hasattr(sanyan,'connect')) # False
นอกจากนี้ อาจมีคนคิดว่าสร้าง pyqtBoundSignal ขึ้นมาโดยตรงเลย ไม่ต้องสร้างด้วย pyqtSignal ได้หรือไม่ ดังนั้นอาจลองทดสอบดู ดังนี้
from PyQt5.QtCore import pyqtBoundSignal
sanyan = pyqtBoundSignal()
sanyan.emit()
ผลที่ได้จะพบว่าโปรแกรมค้างแน่นิ่งไปเลย ดังนั้นจึงสรุปได้ว่าสร้าง pyqtBoundSignal โดยตรงก็ไม่ได้เช่นกัน
กรณีที่สัญญาณมีพารามิเตอร์
ในตัวอย่างที่ผ่านมาจะเห็นว่าสร้างขึ้นมาแต่สัญญาณที่ไม่มีพารามิเตอร์ เหมือนอย่าง clicked ของ QPushButton แต่ว่าโดยทั่วไปแล้วสัญญาณจะมีพารามิเตอร์ด้วย เช่น valueChanged ใน QSpinBox จะมีพารามิเตอร์ตัวหนึ่ง คือค่าหลังเปลี่ยนแปลง
ลองสร้าง QSpinBox ขึ้นมาแล้วใช้ .emit โดยตรงแทนการไปเปลี่ยนค่าในแถบเลื่อนเอง กรณีนี้จะต้องใส่ค่าลงไปในวงเล็บด้วย เช่น
import sys
from PyQt5.QtWidgets import QApplication,QSpinBox
def f(n):
print('@'*n)
qAp = QApplication(sys.argv)
klong = QSpinBox()
klong.valueChanged.connect(f)
klong.valueChanged.emit(3) # ได้ @@@
klong.valueChanged.emit(5) # ได้ @@@@@
klong.valueChanged.emit() # ได้ TypeError: valueChanged(self, int) signal has 1 argument(s) but 0 provided
สัญญาณที่เราสร้างขึ้นมาใหม่เองนั้นก็สามารถทำให้มีพารามิเตอร์ได้เช่นกัน โดยให้ใส่ชนิดของตัวแปรลงในวงเล็บหลัง pyqtSignal เช่น
from PyQt5.QtCore import pyqtSignal,QObject
class Q_O(QObject): # สร้างสัญญาณให้รับพารามิเตอร์เป็นตัวแปร str และ int
sanyan = pyqtSignal(str,int)
def f(s,n): # ฟังก์ชันที่จะเชื่อมเข้ากับสัญญาณนี้ก็ใส่พารามิเตอร์ ๒ ตัวด้วย
print(s*n)
qo = Q_O()
qo.sanyan.connect(f)
qo.sanyan.emit('xy',4) # ได้ xyxyxyxy
qo.sanyan.emit('กขค',3) # ได้ กขคกขคกขค
ชนิดของตัวแปรในที่นี้นอกจากชนิดพื้นฐานพวก int, float, str แล้ว ยังอาจเป็นคลาสของพวก widget ได้ด้วย
เช่นลองดูตัวอย่างการใช้งาน เช่นสร้างแถบเลื่อนขึ้นมา ๒ อันแล้วสร้างสัญญาณที่รับพารามิเตอร์เป็นตัวแถบเลื่อน แล้วเอาค่าจากในแถบเลื่อนตัวนั้นมาแสดง
import sys
from PyQt5.QtWidgets import QApplication,QWidget,QLabel,QSlider,QPushButton,QHBoxLayout,QVBoxLayout
from PyQt5.QtCore import pyqtSignal,Qt
class Natang(QWidget):
dukha = pyqtSignal(QSlider)
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Tahoma; font-size: 17px;')
vbl = QVBoxLayout()
self.setLayout(vbl)
self.khatang = QLabel('')
vbl.addWidget(self.khatang)
self.dukha.connect(self.muealuean)
hbl = QHBoxLayout()
vbl.addLayout(hbl)
hbl.addWidget(QLabel('ก.'))
self.tualuean1 = QSlider() # ตัวเลื่อน ก.
hbl.addWidget(self.tualuean1)
self.tualuean1.setRange(-5,5)
self.tualuean1.setValue(-2)
self.tualuean1.setOrientation(Qt.Horizontal)
self.pumkot1 = QPushButton('ดูค่า') # ปุ่มข้างตัวเลื่อน ข.
hbl.addWidget(self.pumkot1)
self.pumkot1.clicked.connect(lambda: self.dukha.emit(self.tualuean1))
hbl = QHBoxLayout()
vbl.addLayout(hbl)
hbl.addWidget(QLabel('ข.'))
self.tualuean2 = QSlider() # ตัวเลื่อน ข.
hbl.addWidget(self.tualuean2)
self.tualuean2.setOrientation(Qt.Horizontal)
self.tualuean2.setRange(-50,50)
self.tualuean2.setValue(10)
self.pumkot2 = QPushButton('ดูค่า') # ปุ่มข้างตัวเลื่อน ข.
hbl.addWidget(self.pumkot2)
self.pumkot2.clicked.connect(lambda: self.dukha.emit(self.tualuean2))
self.show()
def muealuean(self,tualuean):
if(tualuean==self.tualuean1):
self.khatang.setText('ตัวเลื่อน ก. %d'%(tualuean.value()))
else:
self.khatang.setText('ตัวเลื่อน ข. %d'%(tualuean.value()))
qAp = QApplication(sys.argv)
natang = Natang()
qAp.exec_()
ได้แถบเลื่อน ๒ อันแบบนี้ ลองกดที่ปุ่มทางขวาของตัวเลื่อนไหนก็จะเป็นการแสดงค่าภายในตัวเลื่อนนั้น
การดูว่าออบเจ็กต์ตัวไหนเป็นตัวส่งสัญญาณ {.sender}
เมื่อมีการปล่อยสัญญาณเกิดขึ้นภายในตัว widget ข้อมูลของแหล่งปล่อยสัญญาณนั้นจะถูกเก็บไว้ด้วย โดยสามารถใช้เมธอด .sender เพื่อเอาออบเจ็กต์ของตัวปล่อยสัญญาณนั้นมาได้
เช่น ลองสร้างปุ่ม ๒ ปุ่มขึ้นมา แล้วให้เชื่อมต่อ .clicked.connect กับฟังก์ชันเดียวกัน แบบนี้ไม่ว่ากดปุ่มไหนก็จะเกิดการทำฟังก์ชันนั้นขึ้นเหมือนกัน แบบนี้ก็จะไม่มีข้อแตกต่าง แต่หากใช้ .sender() จะรู้ได้ว่าสัญญาณมาจากปุ่มไหน ทำให้แสดงผลต่างกันได้
import sys
from PyQt5.QtWidgets import QApplication,QWidget,QPushButton,QVBoxLayout
class Natang(QWidget):
def __init__(self):
super().__init__()
vbl = QVBoxLayout()
self.setLayout(vbl)
self.pum1 = QPushButton('ก')
vbl.addWidget(self.pum1)
self.pum1.clicked.connect(self.thukkot)
self.pum2 = QPushButton('ข')
vbl.addWidget(self.pum2)
self.pum2.clicked.connect(self.thukkot)
self.show()
def thukkot(self): # ฟังก์ชันนี้จะทำงานไม่ว่าจะกดปุ่ม ก. หรือ ข.
pum = self.sender() # ดูว่าปุ่มที่ถูกกดคือปุ่มไหน
if(pum==self.pum1):
print('ปุ่ม "ก" ถูกกด')
else:
print('ปุ่ม "ข" ถูกกด')
qAp = QApplication(sys.argv)
natang = Natang()
qAp.exec_()
ก็จะได้หน้าต่างแบบนี้ พอกดที่ปุ่มไหนก็จะขึ้นข้อความว่าปุ่มนั้นถูกกด
อ่านบทถัดไป >>
บทที่ ๒๕