신호 및 슬롯
소개
GUI 프로그래밍에서는 하나의 위젯을 변경할 때 다른 위젯에 알림을 보내야 하는 경우가 많습니다. 더 일반적으로는 모든 종류의 객체가 서로 통신할 수 있기를 원합니다. 예를 들어 사용자가 Close 버튼을 클릭하면 창에 있는 close() 함수가 호출되기를 원할 수 있습니다.
다른 툴킷은 콜백을 사용하여 이러한 종류의 통신을 달성합니다. 콜백은 함수에 대한 포인터이므로 처리 함수에서 특정 이벤트에 대해 알려주려면 처리 함수에 다른 함수(콜백)에 대한 포인터를 전달합니다. 그러면 처리 함수는 적절한 경우 콜백을 호출합니다. 이 방법을 사용하는 성공적인 프레임워크가 존재하지만 콜백은 직관적이지 않을 수 있으며 콜백 인수의 유형 정확성을 보장하는 데 문제가 있을 수 있습니다.
신호 및 슬롯
Qt에는 콜백 기법에 대한 대안이 있습니다: 바로 신호와 슬롯을 사용하는 것입니다. 특정 이벤트가 발생하면 신호가 발생합니다. Qt의 위젯에는 미리 정의된 신호가 많이 있지만, 언제든지 위젯을 서브클래싱하여 자신만의 신호를 추가할 수 있습니다. 슬롯은 특정 신호에 대한 응답으로 호출되는 함수입니다. Qt의 위젯에는 미리 정의된 슬롯이 많이 있지만, 관심 있는 신호를 처리할 수 있도록 위젯을 서브클래싱하고 자신만의 슬롯을 추가하는 것이 일반적입니다.
신호 및 슬롯 메커니즘은 유형 안전합니다: 신호의 시그니처는 수신 슬롯의 시그니처와 일치해야 합니다. (실제로 슬롯은 추가 인수를 무시할 수 있기 때문에 수신하는 신호보다 짧은 서명을 가질 수 있습니다). 서명이 호환되므로 함수 포인터 기반 구문을 사용할 때 컴파일러가 유형 불일치를 감지하는 데 도움을 줄 수 있습니다. 문자열 기반 시그널 및 슬롯 구문은 런타임에 유형 불일치를 감지합니다. 신호와 슬롯은 느슨하게 결합되어 있습니다: 신호를 방출하는 클래스는 어떤 슬롯이 신호를 수신하는지 알지도 신경쓰지도 않습니다. Qt의 신호와 슬롯 메커니즘은 신호를 슬롯에 연결하면 적절한 타이밍에 신호의 파라미터와 함께 슬롯이 호출되도록 보장합니다. 신호와 슬롯은 모든 유형의 인자를 얼마든지 받을 수 있습니다. 완전히 형식 안전합니다.
QObject 또는 그 서브클래스 중 하나(예: QWidget)를 상속하는 모든 클래스는 신호와 슬롯을 포함할 수 있습니다. 신호는 객체가 다른 객체가 흥미를 가질 수 있는 방식으로 상태를 변경할 때 객체에서 방출됩니다. 이것이 객체가 통신을 위해 하는 모든 일입니다. 객체는 자신이 방출하는 신호를 다른 객체가 수신하는지 여부를 알거나 신경 쓰지 않습니다. 이것이 진정한 정보 캡슐화이며, 객체가 소프트웨어 구성 요소로 사용될 수 있도록 보장합니다.
슬롯은 신호를 수신하는 데 사용할 수 있지만 일반적인 멤버 함수이기도 합니다. 객체가 자신의 신호를 수신하는지 여부를 알지 못하는 것처럼 슬롯은 자신에게 연결된 신호가 있는지 여부를 알지 못합니다. 이를 통해 Qt로 진정으로 독립적인 컴포넌트를 만들 수 있습니다.
하나의 슬롯에 원하는 만큼의 신호를 연결할 수 있으며, 하나의 신호는 필요한 만큼의 슬롯에 연결할 수 있습니다. 심지어 한 신호를 다른 신호에 직접 연결할 수도 있습니다. (이렇게 하면 첫 번째 신호가 방출될 때마다 두 번째 신호가 즉시 방출됩니다.)
신호와 슬롯은 함께 강력한 컴포넌트 프로그래밍 메커니즘을 구성합니다.
신호
신호는 객체의 클라이언트나 소유자가 관심을 가질 만한 방식으로 내부 상태가 변경되었을 때 객체에서 발생합니다. 신호는 공용 액세스 함수이며 어디에서나 방출할 수 있지만 신호와 그 하위 클래스를 정의하는 클래스에서만 방출하는 것이 좋습니다.
신호가 방출되면 보통 일반 함수 호출과 마찬가지로 신호에 연결된 슬롯이 즉시 실행됩니다. 이 경우 신호와 슬롯 메커니즘은 GUI 이벤트 루프와 완전히 독립적입니다. emit
문 뒤에 오는 코드의 실행은 모든 슬롯이 반환된 후에 발생합니다. queued connections 을 사용할 때는 상황이 약간 다릅니다. 이 경우 emit
키워드 뒤에 오는 코드는 즉시 계속되고 슬롯은 나중에 실행됩니다.
하나의 신호에 여러 개의 슬롯이 연결되어 있는 경우 신호가 전송되면 연결된 순서대로 슬롯이 차례로 실행됩니다.
신호는 moc에 의해 자동으로 생성되며 .cpp
파일에서 구현해서는 안 됩니다.
인자에 대한 참고 사항: 경험에 따르면 신호와 슬롯은 특별한 유형을 사용하지 않는 것이 재사용 가능성이 더 높습니다. QScrollBar::valueChanged ()가 가상의 QScrollBar::Range와 같은 특수 유형을 사용하는 경우 QScrollBar 용으로 특별히 설계된 슬롯에만 연결할 수 있습니다. 서로 다른 입력 위젯을 함께 연결하는 것은 불가능합니다.
슬롯
슬롯은 슬롯에 연결된 신호가 방출될 때 호출됩니다. 슬롯은 일반적인 C++ 함수이며 일반적으로 호출할 수 있으며, 신호가 연결될 수 있다는 것이 유일한 특징입니다.
슬롯은 일반 멤버 함수이므로 직접 호출할 때는 일반 C++ 규칙을 따릅니다. 하지만 슬롯은 컴포넌트의 액세스 레벨에 관계없이 신호-슬롯 연결을 통해 호출할 수 있습니다. 즉, 임의의 클래스 인스턴스에서 방출된 신호가 관련 없는 클래스의 인스턴스에서 프라이빗 슬롯을 호출할 수 있습니다.
또한 슬롯을 가상으로 정의할 수도 있는데, 이는 실제로 매우 유용합니다.
콜백에 비해 신호와 슬롯은 유연성이 증가하기 때문에 속도가 약간 느리지만 실제 애플리케이션에서는 그 차이가 크지 않습니다. 일반적으로 일부 슬롯에 연결된 신호를 방출하는 것은 가상 함수를 호출하지 않고 수신기를 직접 호출하는 것보다 약 10배 정도 느립니다. 이는 연결 객체를 찾고, 모든 연결을 안전하게 반복하고(즉, 방출하는 동안 후속 수신기가 파괴되지 않았는지 확인), 모든 매개변수를 일반적인 방식으로 마샬링하는 데 필요한 오버헤드입니다. 가상 함수가 아닌 함수 호출 10번은 많은 것처럼 들릴 수 있지만, 예를 들어 new
또는 delete
연산보다 훨씬 적은 오버헤드입니다. 백그라운드에서 new
또는 delete
이 필요한 문자열, 벡터 또는 목록 연산을 수행하는 경우 신호 및 슬롯 오버헤드는 전체 함수 호출 비용에서 매우 적은 비율만 차지합니다. 슬롯에서 시스템 호출을 수행하거나 10개 이상의 함수를 간접적으로 호출하는 경우에도 마찬가지입니다. 신호 및 슬롯 메커니즘의 단순성과 유연성은 사용자가 눈치채지 못할 정도의 오버헤드를 감수할 만한 가치가 있습니다.
signals
또는 slots
이라는 변수를 정의하는 다른 라이브러리는 Qt 기반 애플리케이션과 함께 컴파일할 때 컴파일러 경고 및 오류를 일으킬 수 있다는 점에 유의하세요. 이 문제를 해결하려면 문제가 되는 전처리기 심볼을 #undef
.
작은 예제
최소한의 C++ 클래스 선언을 읽어볼 수 있습니다:
class Counter { public: Counter() { m_value = 0; } int value() const { return m_value; } void setValue(int value); private: int m_value; };
QObject-기반의 작은 클래스는 다음과 같을 수 있습니다:
#include <QObject> class Counter : public QObject { Q_OBJECT // Note. The Q_OBJECT macro starts a private section. // To declare public members, use the 'public:' access modifier. 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; };
QObject-기반 버전은 동일한 내부 상태를 가지며, 상태에 액세스하는 공용 메서드를 제공하지만 신호와 슬롯을 사용하는 컴포넌트 프로그래밍을 추가로 지원합니다. 이 클래스는 valueChanged()
이라는 신호를 방출하여 외부 세계에 상태가 변경되었음을 알릴 수 있으며, 다른 객체가 신호를 보낼 수 있는 슬롯을 가지고 있습니다.
시그널이나 슬롯을 포함하는 모든 클래스는 선언 맨 위에 Q_OBJECT 을 언급해야 합니다. 또한 QObject 에서 (직간접적으로) 파생되어야 합니다.
슬롯은 애플리케이션 프로그래머가 구현합니다. 다음은 Counter::setValue()
슬롯의 가능한 구현 예시입니다:
void Counter::setValue(int value) { if (value != m_value) { m_value = value; emit valueChanged(value); } }
emit
라인은 새 값을 인수로 사용하여 객체에서 valueChanged()
신호를 전송합니다.
다음 코드 조각에서는 두 개의 Counter
객체를 만들고 첫 번째 객체의 valueChanged()
신호를 QObject::connect()를 사용하여 두 번째 객체의 setValue()
슬롯에 연결합니다:
Counter a, b; QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue); a.setValue(12); // a.value() == 12, b.value() == 12 b.setValue(48); // a.value() == 12, b.value() == 48
a.setValue(12)
을 호출하면 a
이 valueChanged(12)
신호를 내보내고, b
이 setValue()
슬롯에서 이를 수신합니다(즉, b.setValue(12)
이 호출됩니다). 그런 다음 b
는 동일한 valueChanged()
신호를 방출하지만 b
의 valueChanged()
신호에 연결된 슬롯이 없으므로 신호가 무시됩니다.
setValue()
함수는 값을 설정하고 value != m_value
경우에만 신호를 내보냅니다. 이렇게 하면 순환 연결의 경우 무한 루핑을 방지할 수 있습니다(예: b.valueChanged()
이 a.setValue()
에 연결된 경우).
기본적으로 모든 연결에 대해 하나의 신호가 전송되며, 중복 연결의 경우 두 개의 신호가 전송됩니다. disconnect () 호출 한 번으로 이러한 모든 연결을 끊을 수 있습니다. Qt::UniqueConnection type 을 전달하면 중복 연결이 아닌 경우에만 연결이 이루어집니다. 이미 중복(동일한 객체의 동일한 슬롯에 정확히 동일한 신호)이 있는 경우 연결이 실패하고 false
으로 반환됩니다.
이 예는 개체가 서로에 대한 정보를 몰라도 함께 작동할 수 있음을 보여줍니다. 이를 활성화하려면 개체를 서로 연결하기만 하면 되며, 간단한 QObject::connect() 함수 호출이나 uic의 자동 연결 기능을 사용하여 연결할 수 있습니다.
실제 예제
다음은 멤버 함수가 없는 간단한 위젯 클래스의 헤더 예시입니다. 이는 자체 애플리케이션에서 신호와 슬롯을 활용하는 방법을 보여주기 위한 것입니다.
#ifndef LCDNUMBER_H #define LCDNUMBER_H #include <QFrame> class LcdNumber : public QFrame { Q_OBJECT
LcdNumber
는 QFrame 및 QWidget 를 통해 대부분의 신호-슬롯 지식이 있는 QObject 을 상속합니다. 이는 기본 제공 QLCDNumber 위젯과 다소 유사합니다.
Q_OBJECT 매크로는 전처리기에 의해 확장되어 moc
에 의해 구현되는 여러 멤버 함수를 선언합니다. " LcdNumber
에 대한 정의되지 않은 vtable 참조"와 같은 컴파일러 오류가 발생하면 moc를 실행하거나 링크 명령에 moc 출력을 포함하는 것을 잊었을 가능성이 높습니다.
public: LcdNumber(QWidget *parent = nullptr); signals: void overflow();
클래스 생성자와 public
멤버 뒤에 signals
클래스를 선언합니다. LcdNumber
클래스는 불가능한 값을 표시하라는 요청을 받으면 overflow()
이라는 신호를 보냅니다.
오버플로를 신경 쓰지 않거나 오버플로가 발생할 수 없다는 것을 알고 있다면 overflow()
신호를 무시할 수 있습니다(즉, 어떤 슬롯에도 연결하지 마세요).
반면에 숫자가 오버플로우될 때 두 개의 다른 에러 함수를 호출하고 싶다면 신호를 두 개의 다른 슬롯에 연결하면 됩니다. Qt는 (연결된 순서대로) 두 함수를 모두 호출합니다.
public slots: void display(int num); void display(double num); void display(const QString &str); void setHexMode(); void setDecMode(); void setOctMode(); void setBinMode(); void setSmallDecimalPoint(bool point); }; #endif
슬롯은 다른 위젯의 상태 변화에 대한 정보를 가져오는 데 사용되는 수신 함수입니다. LcdNumber
는 위의 코드에서 알 수 있듯이 표시되는 숫자를 설정하는 데 사용합니다. display()
은 나머지 프로그램과 클래스의 인터페이스의 일부이므로 슬롯은 공용입니다.
예제 프로그램 중 일부는 QScrollBar 의 valueChanged() 신호를 display()
슬롯에 연결하여 LCD 번호에 스크롤 막대의 값을 계속 표시합니다.
display()
는 과부하 상태이므로 신호를 슬롯에 연결할 때 Qt가 적절한 버전을 선택한다는 점에 유의하세요. 콜백을 사용하면 5개의 다른 이름을 찾아서 직접 유형을 추적해야 합니다.
기본 인수가 있는 신호와 슬롯
신호와 슬롯의 시그니처에는 인수가 포함될 수 있으며 인수는 기본값을 가질 수 있습니다. QObject::destroyed ()를 생각해 보세요:
void destroyed(QObject* = nullptr);
QObject 가 삭제되면 QObject::destroyed() 신호를 내보냅니다. 삭제된 QObject 에 대한 참조가 매달려 있을 수 있는 이 신호를 포착하여 이를 정리하고 싶습니다. 적절한 슬롯 서명이 될 수 있습니다:
void objectDestroyed(QObject* obj = nullptr);
신호를 슬롯에 연결하려면 QObject::connect()를 사용합니다. 신호와 슬롯을 연결하는 방법에는 여러 가지가 있습니다. 첫 번째는 함수 포인터를 사용하는 것입니다:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
QObject::connect()를 함수 포인터와 함께 사용하면 몇 가지 장점이 있습니다. 첫째, 컴파일러가 신호의 인수가 슬롯의 인수와 호환되는지 확인할 수 있습니다. 필요한 경우 컴파일러에서 인수를 암시적으로 변환할 수도 있습니다.
함수나 C++11 람다에 연결할 수도 있습니다:
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });
이 두 경우 모두 connect() 호출 시 컨텍스트로 this 을 제공합니다. 컨텍스트 객체는 수신자가 어느 스레드에서 실행되어야 하는지에 대한 정보를 제공합니다. 컨텍스트를 제공하면 수신자가 컨텍스트 스레드에서 실행되도록 보장하므로 이는 중요합니다.
발신자 또는 컨텍스트가 파괴되면 람다의 연결이 끊어집니다. 신호가 방출될 때 함수 내부에 사용된 모든 객체가 여전히 살아있는지 확인해야 합니다.
신호를 슬롯에 연결하는 다른 방법은 QObject::connect()와 SIGNAL
및 SLOT
매크로를 사용하는 것입니다. SIGNAL()
및 SLOT()
매크로에 인수를 포함할지 여부에 대한 규칙은 인수가 기본값인 경우 SIGNAL()
매크로에 전달된 서명이 SLOT()
매크로에 전달된 서명보다 인수가 적어서는 안 된다는 것입니다.
이 모든 것이 작동합니다:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*))); connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed())); connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
하지만 이것은 작동하지 않습니다:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
...왜냐하면 슬롯은 신호가 보내지 않을 QObject 을 기대하기 때문입니다. 이 연결은 런타임 오류를 보고합니다.
이 QObject::connect() 오버로드를 사용할 때 컴파일러는 신호 및 슬롯 인수를 검사하지 않습니다.
고급 신호 및 슬롯 사용
신호 발신자에 대한 정보가 필요할 수 있는 경우, Qt는 신호를 보낸 객체에 대한 포인터를 반환하는 QObject::sender() 함수를 제공합니다.
람다 표현식은 사용자 정의 인수를 슬롯에 전달할 수 있는 편리한 방법입니다:
connect(action, &QAction::triggered, engine, [=]() { engine->processAction(action->text()); });
타사 신호와 슬롯에 Qt 사용하기
타사 신호/슬롯 메커니즘과 함께 Qt를 사용할 수 있습니다. 심지어 같은 프로젝트에서 두 메커니즘을 모두 사용할 수도 있습니다. 그렇게 하려면 CMake 프로젝트 파일에 다음을 작성합니다:
target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)
qmake 프로젝트(.pro) 파일에 다음을 작성해야 합니다:
CONFIG += no_keywords
이 이름은 타사 라이브러리(예: Boost)에서 사용되므로 Qt에 moc 키워드 signals
, slots
, emit
를 정의하지 말라고 지시합니다. 그런 다음 no_keywords
플래그가 있는 Qt 신호와 슬롯을 계속 사용하려면 소스에서 Qt moc 키워드의 모든 사용을 해당 Qt 매크로 Q_SIGNALS (또는 Q_SIGNAL), Q_SLOTS (또는 Q_SLOT) 및 Q_EMIT 로 대체하면 됩니다.
Qt 기반 라이브러리의 시그널과 슬롯
Qt 기반 라이브러리의 공용 API는 signals
와 slots
대신 Q_SIGNALS
와 Q_SLOTS
키워드를 사용해야 합니다. 그렇지 않으면 QT_NO_KEYWORDS
를 정의하는 프로젝트에서 이러한 라이브러리를 사용하기가 어렵습니다.
이 제한을 적용하기 위해 라이브러리 작성자는 라이브러리를 빌드할 때 전처리기 정의 QT_NO_SIGNALS_SLOTS_KEYWORDS
를 설정할 수 있습니다.
이 정의는 라이브러리 구현에서 다른 Qt 관련 키워드를 사용할 수 있는지 여부에 영향을 주지 않고 시그널과 슬롯을 제외합니다.
QLCDNumber, QObject::connect(), 메타 객체 시스템 및 Qt의 속성 시스템도참조하십시오 .
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.