본문 바로가기
Mastering-GUI-Programming

Handling Events with Signals and Slots

by 자동매매 2023. 3. 8.

Support for Signals and Slots : https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html

Using Qt Designer : https://www.riverbankcomputing.com/static/Docs/PyQt5/designer.html

 

 

 

신호 슬롯 기본 사항  

object1.signalName.connect(object2.slotName)

  • 슬롯신호를 수신하고 이에 응답하여 작동할 수 있는 객체 메서드입니다. 이벤트에 대한 애플리케이션의 응답을 구성하기 위해 신호를 슬롯에 연결합니다.
  • 신호이벤트 유형에 대한 응답으로 방출될 수 있는 개체의 특수 속성입니다. 이벤트는 사용자 작업, 시간 제한 또는 비동기 메서드 호출 완료와 같은 것일 수 있습니다.

QObject 을 상속한 모든 클래스는  신호를 보내고 받을 수 있습니다. 각각의 다른 클래스에는 해당 클래스의 기능에 적합한 자체 신호 및 슬롯 세트가 있습니다.

 

import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc


class MainWindow(qtw.QWidget):

    def __init__(self):
        super().__init__()
        self.setLayout(qtw.QVBoxLayout())

        # 시그널을 슬롯에 연결
        self.quitbutton = qtw.QPushButton('Quit')
        self.quitbutton.clicked.connect(self.close)
        self.layout().addWidget(self.quitbutton)
        # 위와 동일 기능
        # self.quitbutton = qtw.QPushButton('Quit', clicked=self.close) 
        # self.layout().addWidget(self.quitbutton)

        # 데이터를 갖은 signal를 데이터를 받는 slot에 연결
        self.entry1 = qtw.QLineEdit()
        self.entry2 = qtw.QLineEdit()
        self.layout().addWidget(self.entry1)
        self.layout().addWidget(self.entry2)
        self.entry1.textChanged.connect(self.entry2.setText)

        # signal을 파이썬 콜러블에 연결하기
        self.entry2.textChanged.connect(print)

        # 하나의 signal를 다른 signal에 연결
        self.entry1.editingFinished.connect(lambda: print('editing finished'))
        self.entry2.returnPressed.connect(self.entry1.editingFinished)
        
        # textChanged는  문자열을 내보내고 clicked는 부울을 내뿜기 때문에 작동하지 않습니다. 
        # self.entry1.textChanged.connect(self.quitbutton.clicked)

        # This won't work, because of signal doesn't send enough args
        self.badbutton = qtw.QPushButton("Bad")
        self.layout().addWidget(self.badbutton)
        self.badbutton.clicked.connect(self.needs_args)

        # This will work, even though the signal sends extra args
        self.goodbutton = qtw.QPushButton("Good")
        self.layout().addWidget(self.goodbutton)
        self.goodbutton.clicked.connect(self.no_args)


        self.show()

    def needs_args(self, arg1, arg2, arg3):
        pass

    def no_args(self):
        print('I need no arguments')

if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    # it's required to save a reference to MainWindow.
    # if it goes out of scope, it will be destroyed.
    mw = MainWindow()
    sys.exit(app.exec())

 

QMetaobject를 이용한 signal/slot생성

void QMetaObject::connectSlotsByName ( QObject * object ) [static]

Searches recursively for all child objects of the given object, and connects matching signals from them to slots of object that follow the following form:

 void on_<object name>_<signal name>(<signal parameters>);

<사용 예>

import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtGui as qtg
from PyQt5 import QtCore as qtc

class TimeForm(qtw.QWidget):

    submitted = qtc.pyqtSignal(qtc.QTime)

    def __init__(self):
        super().__init__()
        self.setLayout(qtw.QHBoxLayout())
        #self.time_inp = qtw.QTimeEdit(self)
        self.time_inp = qtw.QTimeEdit(self, objectName='time_inp')
        self.layout().addWidget(self.time_inp)
        qtc.QMetaObject.connectSlotsByName(self)

    def on_time_inp_editingFinished(self):
        self.submitted.emit(self.time_inp.time())
        # self.destroy()

class MainWindow(qtw.QWidget):

    def __init__(self):
        """MainWindow constructor.

        This widget will be our main window.
        We'll define all the UI components in here.
        """
        super().__init__()
        # Main UI code goes here
        self.tf = TimeForm()
        self.tf.show()

        self.tf.submitted.connect(lambda x: print(x))

        # End main UI code
        self.show()


if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    # it's required to save a reference to MainWindow.
    # if it goes out of scope, it will be destroyed.
    mw = MainWindow()
    sys.exit(app.exec())

 

사용자 지정 신호를 사용하여 간에 데이터 공유

 

 

import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc


class FormWindow(qtw.QWidget):

    # 단일 변수 유형을 전달하는 대신 두 개의 변수 유형 목록을 전달합니다. 
    submitted = qtc.pyqtSignal([str], [int, str])

    def __init__(self):
        super().__init__()
        self.setLayout(qtw.QVBoxLayout())

        self.edit = qtw.QLineEdit()
        self.submit = qtw.QPushButton('Submit', clicked=self.onSubmit)

        self.layout().addWidget(self.edit)
        self.layout().addWidget(self.submit)

    def onSubmit(self):
        if self.edit.text().isdigit():
            text = self.edit.text()
            self.submitted[int, str].emit(int(text), text)
        else:
            self.submitted[str].emit(self.edit.text())
        self.close()

class MainWindow(qtw.QWidget):

    def __init__(self):
        super().__init__()
        self.setLayout(qtw.QVBoxLayout())

        self.label = qtw.QLabel('Click "change" to change this text.')
        self.change = qtw.QPushButton("Change", clicked=self.onChange)
        self.layout().addWidget(self.label)
        self.layout().addWidget(self.change)
        self.show()
    
    # Overloading signals and slots
    @qtc.pyqtSlot()
    def onChange(self):
        self.formwindow = FormWindow()
        #self.formwindow.submitted.connect(self.label.setText)
        self.formwindow.submitted[str].connect(self.onSubmittedStr)
        self.formwindow.submitted[int, str].connect(self.onSubmittedIntStr)
        self.formwindow.show()

    @qtc.pyqtSlot(str)
    def onSubmittedStr(self, string):
        self.label.setText(string)

    @qtc.pyqtSlot(int, str)
    def onSubmittedIntStr(self, integer, string):
        text = f'The string {string} becomes the number {integer}'
        self.label.setText(text)


if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    # it's required to save a reference to MainWindow.
    # if it goes out of scope, it will be destroyed.
    mw = MainWindow()
    sys.exit(app.exec())

 

Automating our calendar form

- app 입력한 이벤트를 저장하는 방법이 필요합니다.

- All Day checkbox는 확인란을 선택하면 time entry 비활성화됩니다.

- 달력에서 날짜를 선택하면 이벤트 목록이 해당 날짜의 이벤트로 채워집니다.

- event list에서 이벤트를 선택하면 양식에 이벤트 세부 정보가 채워집니다.

- 추가/업데이트를 클릭하면  이벤트가 선택된 경우 저장된 이벤트 세부 정보가 업데이트되고,

                                              그렇지 않은 경우 새 이벤트가 추가됩니다.

- Delete를 클릭하면 선택한 이벤트가 제거됩니다.

- 이벤트를 선택하지 않으면 Delete를 비활성화해야 합니다.

- New...  카테고리는 새 카테고리를 입력 할 수있는 대화 상자를 열어야합니다.

              하나를 입력하기로 선택한 경우 선택해야 합니다.

 

 

import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc


class CategoryWindow(qtw.QWidget):
    """A basic dialog to demonstrate inter-widget communication"""

    # when submitted, we'll emit this signal
    # with the entered string
    submitted = qtc.pyqtSignal(str)

    def __init__(self):
        super().__init__(None, modal=True)

        self.setLayout(qtw.QVBoxLayout())
        self.layout().addWidget(
            qtw.QLabel('Please enter a new catgory name:')
            )
        self.category_entry = qtw.QLineEdit()
        self.layout().addWidget(self.category_entry)

        self.submit_btn = qtw.QPushButton(
            'Submit',
            clicked=self.onSubmit
            )
        self.layout().addWidget(self.submit_btn)
        self.cancel_btn = qtw.QPushButton(
            'Cancel',
            # Errata:  The book contains this line:
            #clicked=self.destroy
            # It should call self.close instead, like so:
            clicked=self.close
            )
        self.layout().addWidget(self.cancel_btn)
        self.show()

    @qtc.pyqtSlot()
    def onSubmit(self):
        if self.category_entry.text():
            self.submitted.emit(self.category_entry.text())
        self.close()


class MainWindow(qtw.QWidget):

    events = {}

    def __init__(self):
        """MainWindow constructor. """
        super().__init__()
        # Configure the window
        self.setWindowTitle("My Calendar App")
        self.resize(800, 600)


        # Create our widgets
        self.calendar = qtw.QCalendarWidget()
        self.event_list = qtw.QListWidget()
        self.event_title = qtw.QLineEdit()
        self.event_category = qtw.QComboBox()
        self.event_time = qtw.QTimeEdit(qtc.QTime(8, 0))
        self.allday_check = qtw.QCheckBox('All Day')
        self.event_detail = qtw.QTextEdit()
        self.add_button = qtw.QPushButton('Add/Update')
        self.del_button = qtw.QPushButton('Delete')

        # Configure some widgets

        # Add event categories
        self.event_category.addItems(
            ['Select category…', 'New…', 'Work',
             'Meeting', 'Doctor', 'Family']
            )
        # disable the first category item
        self.event_category.model().item(0).setEnabled(False)

        # Arrange the widgets
        main_layout = qtw.QHBoxLayout()
        self.setLayout(main_layout)
        main_layout.addWidget(self.calendar)
        # Calendar expands to fill the window
        self.calendar.setSizePolicy(
            qtw.QSizePolicy.Expanding,
            qtw.QSizePolicy.Expanding
        )
        right_layout = qtw.QVBoxLayout()
        main_layout.addLayout(right_layout)
        right_layout.addWidget(qtw.QLabel('Events on Date'))
        right_layout.addWidget(self.event_list)
        # Event list expands to fill the right area
        self.event_list.setSizePolicy(
            qtw.QSizePolicy.Expanding,
            qtw.QSizePolicy.Expanding
            )

        # Create a sub-layout for the event view/add form
        event_form = qtw.QGroupBox('Event')
        right_layout.addWidget(event_form)
        event_form_layout = qtw.QGridLayout()
        event_form_layout.addWidget(self.event_title, 1, 1, 1, 3)
        event_form_layout.addWidget(self.event_category, 2, 1)
        event_form_layout.addWidget(self.event_time, 2, 2,)
        event_form_layout.addWidget(self.allday_check, 2, 3)
        event_form_layout.addWidget(self.event_detail, 3, 1, 1, 3)
        event_form_layout.addWidget(self.add_button, 4, 2)
        event_form_layout.addWidget(self.del_button, 4, 3)
        event_form.setLayout(event_form_layout)



        ##################
        # Connect Events #
        ##################

        # disable time when "all day" is checked.
        self.allday_check.toggled.connect(self.event_time.setDisabled)

        # Populate the event list when the calendar is clicked
        self.calendar.selectionChanged.connect(self.populate_list)

        # Populate the event form when an item is selected
        self.event_list.itemSelectionChanged.connect(self.populate_form)

        # Save event when save is hit
        self.add_button.clicked.connect(self.save_event)

        # connect delete button
        self.del_button.clicked.connect(self.delete_event)

        # Enable 'delete' only when an event is selected
        self.event_list.itemSelectionChanged.connect(
            self.check_delete_btn)
        self.check_delete_btn()

        # check for selection of "new…" for category
        self.event_category.currentTextChanged.connect(self.on_category_change)

        self.show()

    def clear_form(self):
        self.event_title.clear()
        self.event_category.setCurrentIndex(0)
        self.event_time.setTime(qtc.QTime(8, 0))
        self.allday_check.setChecked(False)
        self.event_detail.setPlainText('')

    def populate_list(self):
        # As reported by github user eramey16, we need the following line
        # to unselect list items since the selected index may not exist
        # in the new list.  This line is not in the book code.
        self.event_list.setCurrentRow(-1)

        self.event_list.clear()
        self.clear_form()
        date = self.calendar.selectedDate()
        for event in self.events.get(date, []):
            time = (
                event['time'].toString('hh:mm')
                if event['time']
                else 'All Day'
            )
            self.event_list.addItem(f"{time}: {event['title']}")

    def populate_form(self):
        self.clear_form()
        date = self.calendar.selectedDate()
        event_number = self.event_list.currentRow()
        
        # 이벤트가 선택되지 않은 경우, QListWidgetcurrentRow()는 -1의 값을 리턴하고
        # 이 경우 양식을 비워두고 돌아갑니다.
        if event_number == -1:
            return

        event_data = self.events.get(date)[event_number]

        self.event_category.setCurrentText(event_data['category'])
        if event_data['time'] is None:
            self.allday_check.setChecked(True)
        else:
            self.event_time.setTime(event_data['time'])
        self.event_title.setText(event_data['title'])
        self.event_detail.setPlainText(event_data['detail'])

    def save_event(self):
        event = {
            'category': self.event_category.currentText(),
            'time': (
                None
                if self.allday_check.isChecked()
                else self.event_time.time()
                ),
            'title': self.event_title.text(),
            'detail': self.event_detail.toPlainText()
            }

        date = self.calendar.selectedDate()
        event_list = self.events.get(date, [])
        event_number = self.event_list.currentRow()

        # if no events are selected, this is a new event
        if event_number == -1:
            event_list.append(event)
        else:
            event_list[event_number] = event

        event_list.sort(key=lambda x: x['time'] or qtc.QTime(0, 0))
        self.events[date] = event_list
        self.populate_list()

    def delete_event(self):
        date = self.calendar.selectedDate()
        row = self.event_list.currentRow()
        del(self.events[date][row])
        self.event_list.setCurrentRow(-1)
        self.clear_form()
        self.populate_list()

    def check_delete_btn(self):
        self.del_button.setDisabled(self.event_list.currentRow() == -1)

    def on_category_change(self, text):
        if text == 'New…':
            self.dialog = CategoryWindow()
            self.dialog.submitted.connect(self.add_category)
            self.event_category.setCurrentIndex(0)

    def add_category(self, category):
        self.event_category.addItem(category)
        self.event_category.setCurrentText(category)

if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    # it's required to save a reference to MainWindow.
    # if it goes out of scope, it will be destroyed.
    mw = MainWindow()
    sys.exit(app.exec())

[참조]

디스크에 데이터를 유지할 수 있지만 원하는 경우 이러한 기능을 추가 할 수 있습니다. dict의 각 항목은 date 객체를 키로 사용하며 해당 날짜의 모든 이벤트에 대한 세부 정보를 포함하는 dict 객체 목록을 포함합니다.

데이터의 레이아웃은 다음과 같습니다.

events = { 
          QDate: {
              'title': "String title of event",
              'category': "String category of event",
              'time': QTime() or None if "all day",
              'detail': "String details of event"
              }
          }

 

self.allday_check.toggled.connect(self.event_time.setDisabled)

QCheckBox.toggled 신호는 체크박스가 켜지거나 꺼질 때마다 방출되며, 체크박스가 (변경 후) 선택 취소되었는지(False) 또는 체크되었는지(True)를 나타내는 부울을 보냅니다. 이것은 setDisabled에 잘 연결되어 True에서 위젯을 비활성화하거나 False에서 활성화합니다.

 

event_list.sort(key=lambda x: x['time'] or qtc.QTime(0, 0)) 
self.events[date] = event_list
self.populate_list()

method 완료하려면 시간 값을 사용하여 목록을 정렬합니다. All day checked이벤트에 None를 사용하고 있으므로 정렬에서 0:00QTime로 대체하여 먼저 정렬됩니다.

 

### .ui files을 이용한 구현

1. ui files을 Python로 변환하여 사용

$ pyuic5 ui_fileName -o py_fileName

 

메인파일에서

1) 변환파일 import

from designer import calendar_form

2) 초기화 메서드에서 class 인스턴스 생성하여 setupUI메서드 실행

    def __init__(self):
        """MainWindow constructor. """
        super().__init__()
        self.ui = calendar_form.Ui_Dialog()
        self.ui.setupUi(self)

 

calendar_form_수정.py
0.01MB
calendar_form_수정.ui
0.00MB
test.py
0.01MB

 

 

2. .ui files 자체 이용

이 방법의 단점은 변환 시간이 추가되지만 빌드가 간단하고 유지 관리할 파일이 적다는 이점이 있다는 것입니다. 이는 GUI 디자인을 자주 반복할 수 있는 초기 개발 중에 취하는 좋은 방법입니다.

 

1) uic import

from PyQt5 import uic

2) 메인에서 ui class 상속

form_class = uic.loadUiType(path)[0]

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

[참조]

MW_Ui, MW_Base = uic.loadUiType('calendar_form.ui')

loadUiType() .ui파일의 경로를 가져와서 생성된 UI class와 이 class의 기반이되는 Qt baseclass (이 경우 QWidget)를 포함하는 튜플을 반환합니다.

- 메인에서 사용

class MainWindow(MW_Base, MW_Ui):

'Mastering-GUI-Programming' 카테고리의 다른 글

Styling Qt Applications  (0) 2023.03.08
Building Applications with QMainWindow  (0) 2023.03.08
Building Forms with QtWidgets  (0) 2023.03.08
Deep Dive into PyQt  (0) 2023.03.08

댓글