개요

Qt GUI 개발을 하면서 지속적으로 등장하게 될 시그널과 슬롯의 정의를 다시 한번 명확하게 짚고 넘어감으로써 앞으로 능숙하게 써먹을 준비 갖추기

시그널은 from, 슬롯은 to의 성격을 가지는 개념.

시그널

  • 특정 이벤트 발생시에 내보내어짐 (emit을 이용하여)

시그널 자체는 public으로 접근 가능한 함수이며 따라서 어디서든 접근이 가능하지만, 이 시그널을 정의한 클래스 및 서브클래스에서만 호출할 것을 권장함.

또한 시그널은 별도의 구현 절차가 없다. 무슨 말이냐면, emit sigNoname(param); 식으로 시그널을 발생시키기만 하면, 그 다음에 할 일은 슬롯으로 넘겨서 슬롯 내부에서 로직을 구현하면 되기 때문이다.

다음은 심플한 QObject 기반 클래스 예제다.

#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};

슬롯

특정 이벤트 발생 후 시그널이 emit 되면, 연결된 슬롯에서 이어서 사전에 정의된 작업을 수행한다.

하나의 시그널에는 다수의 슬롯이 연결될 수 있다.

위에서 언급했듯, 시그널과 달리 슬롯은 시그널과 함께 받은 인자를 가지고 무슨 작업을 해줄지 정의해줘야 한다. 위의 Counter 클래스 예제를 통해 정의된 setValue 슬롯을 내부적으로 다음과 같이 정의할 수 있다.

void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}

위의 예제에서는 갱신된 값을 인자로 하여 슬롯 내부에서 다시 한번 시그널을 내보내도록 정의하고 있다.

시그널과 슬롯 연결해 주기

그렇다면 이 둘을 어떻게 연결을 해줘야 실질적으로 우리가 기대하는 동작을 할 수 있나?

QObject에서는 connect 함수를 이용해서 둘을 연결하도록 지원하고 있다. Qt 6버전 기준으로 connect 함수는 다음과 같은 6종류가 지원되고 있다. 보다 자세한 내용은 여기 참조.

  • QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
  • QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
  • QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const char *method, Qt::ConnectionType type = Qt::AutoConnection) const
  • template <typename PointerToMemberFunction> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)
  • template <typename PointerToMemberFunction, typename Functor> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
  • template <typename PointerToMemberFunction, typename Functor> QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection)

현 단계에서는 이 중에서 맨 위의 함수만 확인하고 넘어간다. 왜냐면 현재 프로젝트에서 connect는 이 방식의 호출로 구현이 되고 있었기 때문이다.

파라미터 순서대로 들여다 보면

1. 어떤 객체에서
2. 어떤 시그널을 호출 했을 때
3. 어떤 객체의
4. 어떤 슬롯에서 이를 받는다
5. 그리고 커넥션 타입은 ㅇㅇㅇ와 같다

와 같이 정의되어 있다. 여기서 중요한 점은, 시그널 및 슬롯을 파라미터로 전달할 때 각각 SIGNALSLOG 매크로를 사용해서 던져야 한다는 점이다.

커넥션 타입에는 여섯 종류가 제공되고 있는데, 이는 시그널과 슬롯이 연결된 방식을 정의한다. 다시 말하면 시그널 발생 시 이를 슬롯에 즉시 던지는지, 혹은 기다렸다가 던지는지 등을 정리하고 있다. 현재 크립토 프로젝트 내에서 쓰이고 있는 Qt::QueuedConnection은 그중 하나인데, 이 타입에 대한 설명은 다음과 같다.

The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.

정확한 내용 이해가 되지 않아서 내용 해석이 이상할 수 있음. 얼른 이해하고 가다듬기

이 설명에 의하면,

  • 이 연결방식을 채택한 경우, 슬롯은 시그널 호출을 받는 스레드 (receiver thread)의 이벤트 루프로 컨트롤이 넘겨질 때 호출이 되며
  • 슬롯은 리시버 스레드 내에서 실행된다.

현재 크립토 코드 내에서 다음과 같이 호출하고 있음을 확인할 수 있다.

QObject::connect(this, SIGNAL(sigLog1(QString)), this, SLOT(slotLog1(QString)), Qt::QueuedConnection);

그러면 위에 있는 Counter의 시그널과 슬롯은 다음과 같이 연결해 주면 될 것이다. Counter 객체 내에서 이를 호출한다고 가정한다.

QObject::connect(this, SIGNAL(valueChanged(int)), this, SLOT(setValue(int)), Qt::QueuedConnection);
반응형

+ Recent posts