C++ 타입의 속성을 QML에 노출하기
QML은 C++ 코드에 정의된 기능을 통해 쉽게 확장할 수 있습니다. QML 엔진이 Qt 메타 객체 시스템과 긴밀하게 통합되어 있기 때문에, ` QObject` 파생 클래스나 ` Q_GADGET ` 타입을 통해 적절하게 노출된 모든 기능은 QML 코드에서 접근할 수 있습니다. 이를 통해 C++ 데이터와 함수를 QML에서 직접 사용할 수 있으며, 대개 수정할 필요가 거의 없거나 전혀 없습니다.
QML 엔진은 메타 객체 시스템을 통해 QObject 인스턴스를 내부적으로 분석할 수 있는 기능을 갖추고 있습니다. 즉, 모든 QML 코드는 QObject 에서 파생된 클래스의 인스턴스에 대해 다음과 같은 멤버에 접근할 수 있습니다:
- 속성
- 메서드(공개 슬롯이거나 ` Q_INVOKABLE` 플래그가 지정된 경우)
- 시그널
(또한, ` Q_ENUM`로 선언된 열거형(enum)도 사용할 수 있습니다. 자세한 내용은 ‘QML과 C++ 간의 데이터형 변환’을 참조하십시오.)
일반적으로, QObject 에서 파생된 클래스가 QML 유형 시스템에 등록되었는지 여부와 관계없이 QML에서 이러한 요소들에 접근할 수 있습니다. 그러나 엔진이 추가적인 유형 정보에 접근해야 하는 방식으로 클래스를 사용해야 하는 경우(예를 들어, 클래스 자체가 메서드 매개변수나 속성으로 사용되거나, 해당 클래스의 열거형 중 하나가 이러한 방식으로 사용되는 경우)에는 클래스를 등록해야 할 수도 있습니다. 컴파일 시점에 분석될 수 있는 것은 등록된 유형뿐이므로, QML에서 사용하는 모든 유형에 대해 등록을 권장합니다.
Q_GADGET 유형의 경우, 알려진 공통 기본 클래스에서 파생되지 않아 자동으로 사용할 수 없기 때문에 등록이 필수입니다. 등록하지 않으면 해당 유형의 속성과 메서드에 접근할 수 없습니다.
DEPENDENCIES 옵션을 사용하여 qt_add_qml_module 호출에 종속성을 추가함으로써, 다른 모듈의 C++ 유형을 자신의 모듈에서 사용할 수 있게 할 수 있습니다. 예를 들어, QML을 통해 노출된 C++ 타입이 ` QColor `를 메서드 인자 및 반환 값으로 사용할 수 있도록 ` QtQuick `에 의존하고 싶을 수 있습니다. ` QtQuick `는 ` QColor `를 값 타입 `color`로 노출합니다. 이러한 의존성은 런타임에 자동으로 추론될 수 있지만, 이에 전적으로 의존해서는 안 됩니다.
또한, 이 문서에서 다루는 여러 중요한 개념들은 ‘C++을 사용하여 QML 확장 기능 작성하기 ’ 튜토리얼에서 시연되고 있음을 참고하십시오.
C++ 및 다양한 QML 통합 방법에 대한 자세한 내용은 C++ 및 QML 통합 개요 페이지를 참조하십시오.
데이터 유형 처리 및 소유권
C++에서 QML로 전송되는 모든 데이터(속성 값, 메서드 매개변수 또는 반환값, 신호 매개변수 값 등)는 QML 엔진에서 지원하는 유형이어야 합니다.
기본적으로 엔진은 여러 Qt C++ 유형을 지원하며, QML에서 사용될 때 이를 적절하게 자동 변환할 수 있습니다. 또한, QML 유형 시스템에 등록된 C++ 클래스는 데이터 유형으로 사용할 수 있으며, 적절하게 등록된 경우 해당 열거형도 마찬가지로 사용할 수 있습니다. 자세한 내용은 ‘QML과 C++ 간의 데이터 유형 변환’을 참조하십시오.
또한, C++에서 QML로 데이터를 전송할 때는 데이터 소유권 규칙이 고려됩니다. 자세한 내용은 ‘데이터 소유권’을 참조하십시오.
속성 노출
Q_PROPERTY() 매크로를 사용하여 ` QObject`에서 파생된 모든 클래스에 대해 속성을 지정할 수 있습니다. 속성은 읽기 함수가 연관되어 있고, 선택적으로 쓰기 함수가 있을 수 있는 클래스 데이터 멤버입니다.
QObject 에서 파생된 클래스나 Q_GADGET 클래스의 모든 속성은 QML에서 접근할 수 있습니다.
예를 들어, 아래는 author 속성을 가진 Message 클래스입니다. Q_PROPERTY 매크로 호출에 명시된 대로, 이 속성은 author() 메서드를 통해 읽을 수 있으며, setAuthor() 메서드를 통해 쓸 수 있습니다:
참고: Q_PROPERTY 유형에 대해서는 typedef나 using을 사용하지마십시오 . 이는 moc을 혼란스럽게 하여 특정 유형 비교가 실패할 수 있습니다.
다음과 같이 작성하지 마십시오:
using FooEnum = Foo::Enum;
class Bar : public QObject
{
Q_OBJECT
Q_PROPERTY(FooEnum enum READ enum WRITE setEnum NOTIFY enumChanged)
};유형을 직접 참조하십시오:
class Bar : public QObject
{
Q_OBJECT
Q_PROPERTY(Foo::Enum enum READ enum WRITE setEnum NOTIFY enumChanged)
};Message 를 사용할 수 있게 하려면 C++에서는 ` QML_ELEMENT `을, CMake에서는 ` qt_add_qml_module`을 사용해야 합니다.
class Message : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
public:
void setAuthor(const QString &a)
{
if (a != m_author) {
m_author = a;
emit authorChanged();
}
}
QString author() const
{
return m_author;
}
signals:
void authorChanged();
private:
QString m_author;
};Message 의 인스턴스를 MyItem.qml 이라는 파일에 required 속성으로 전달하면 이를 사용할 수 있게 됩니다:
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
QQuickView view;
Message msg;
view.setInitialProperties({{"msg", &msg}});
view.setSource(QUrl::fromLocalFile("MyItem.qml"));
view.show();
return app.exec();
}그러면 MyItem.qml 에서 author 속성을 읽어올 수 있습니다:
// MyItem.qml
import QtQuick
Text {
required property Message msg
width: 100; height: 100
text: msg.author // invokes Message::author() to get this value
Component.onCompleted: {
msg.author = "Jonah" // invokes Message::setAuthor()
}
}QML과의 상호 운용성을 극대화하기 위해, 쓰기 가능한 모든 속성에는 속성 값이 변경될 때마다 발송되는 관련 NOTIFY 신호가 있어야 합니다. 이를 통해 속성을 속성 바인딩과 함께 사용할 수 있는데, 속성 바인딩은 종속 속성 중 하나의 값이 변경될 때마다 해당 속성을 자동으로 업데이트하여 속성 간의 관계를 강제하는 QML의 핵심 기능입니다.
위의 예제에서, author 속성에 연관된 NOTIFY 신호는 Q_PROPERTY() 매크로 호출에 명시된 대로 authorChanged 입니다. 즉, Message::setAuthor()에서 작성자가 변경될 때와 같이 이 신호가 발송될 때마다, QML 엔진에 author 속성과 관련된 모든 바인딩을 업데이트해야 한다는 알림이 전달되며, 이에 따라 엔진은 Message::author() 를 다시 호출하여 text 속성을 업데이트하게 됩니다.
만약 author 속성이 쓰기 가능하지만 관련 NOTIFY 신호가 없다면, text 값은 Message::author() 에서 반환된 초기값으로 초기화되지만, 이 속성에 대한 이후의 변경 사항으로는 업데이트되지 않을 것입니다. 또한, QML에서 이 속성에 바인딩을 시도할 경우 엔진으로부터 런타임 경고가 발생합니다.
참고: NOTIFY 신호의 이름은 <property>Changed로 지정하는것이 좋습니다 . 여기서 <property> 는 속성의 이름입니다. QML 엔진에서 생성되는 관련 속성 변경 신호 핸들러는 관련 C++ 신호의 이름과 상관없이 항상 on<Property>Changed 형태를 취하므로, 혼동을 피하기 위해 신호 이름을 이 규칙에 따라 지정하는 것이 좋습니다.
Notify 신호 사용 시 유의사항
루프나 과도한 평가를 방지하기 위해, 개발자는 속성 값이 실제로 변경된 경우에만 속성 변경 신호가 발송되도록 해야 합니다. 또한, 특정 속성이나 속성 그룹이 자주 사용되지 않는 경우, 여러 속성에 동일한 NOTIFY 신호를 사용하는 것이 허용됩니다. 이때 성능 저하가 발생하지 않도록 주의하여 적용해야 합니다.
NOTIFY 신호가 존재하면 약간의 오버헤드가 발생합니다. 객체 생성 시점에 속성 값이 설정되고 이후에는 변경되지 않는 경우가 있습니다. 가장 일반적인 예로는 하위 객체를 포함하는 읽기 전용 속성이 있으며, 이는 대개 그룹화된 속성 구문을 통해 액세스됩니다. 이 경우 하위 객체는 한 번 할당된 후 소유자가 삭제될 때만 해제됩니다. 이러한 경우에는 NOTIFY 신호 대신 CONSTANT 속성을 속성 선언에 추가할 수 있습니다.
CONSTANT 속성은 클래스 생성자에서만 값이 설정되고 최종 확정되는 속성에 대해서만 사용해야 합니다. 바인딩에서 사용하려는 다른 모든 속성에는 대신 NOTIFY 신호를 지정해야 합니다.
객체 유형을 갖는 속성
객체 유형의 속성은 해당 객체 유형이 QML 유형 시스템에 적절하게 등록되어 있는 경우에만 QML에서 접근할 수 있습니다.
예를 들어, ` Message ` 유형에는 ` MessageBody*` 유형의 ` body ` 속성이 있을 수 있습니다:
class Message : public QObject
{
Q_OBJECT
Q_PROPERTY(MessageBody* body READ body WRITE setBody NOTIFY bodyChanged)
public:
MessageBody* body() const;
void setBody(MessageBody* body);
};
class MessageBody : public QObject
{
Q_OBJECT
Q_PROPERTY(QString text READ text WRITE text NOTIFY textChanged)
// ...
}Message 유형이 QML 유형 시스템에 등록되어 QML 코드에서 객체 유형으로 사용할 수 있다고 가정해 봅시다:
Message {
// ...
}MessageBody 유형이 유형 시스템에도 등록되어 있다면, QML 코드 내에서 Message 의 body 속성에 MessageBody 를 할당할 수 있게 됩니다:
Message {
body: MessageBody {
text: "Hello, world!"
}
}객체-리스트 유형을 가진 속성
QObject 에서 파생된 유형의 목록을 포함하는 속성도 QML에 노출할 수 있습니다. 그러나 이를 위해서는 속성 유형으로 QList<T> 대신 QQmlListProperty 를 사용해야 합니다. 이는 QList 가 QObject 에서 파생된 유형이 아니기 때문에, 목록이 수정될 때 신호 알림과 같은 필수적인 QML 속성 특성을 Qt 메타 객체 시스템을 통해 제공할 수 없기 때문입니다.
예를 들어, 아래의 MessageBoard 클래스에는 QQmlListProperty 타입의 messages 속성이 있으며, 이 속성은 Message 인스턴스 목록을 저장합니다:
class MessageBoard : public QObject
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<Message> messages READ messages)
public:
QQmlListProperty<Message> messages();
private:
static void append_message(QQmlListProperty<Message> *list, Message *msg);
QList<Message *> m_messages;
};MessageBoard::messages() 함수는 QList<T> m_messages 멤버를 사용하여 QQmlListProperty 객체를 생성하고 반환하며, QQmlListProperty 생성자가 요구하는 대로 적절한 목록 수정 함수를 전달합니다:
QQmlListProperty<Message> MessageBoard::messages()
{
return QQmlListProperty<Message>(this, 0, &MessageBoard::append_message);
}
void MessageBoard::append_message(QQmlListProperty<Message> *list, Message *msg)
{
MessageBoard *msgBoard = qobject_cast<MessageBoard *>(list->object);
if (msg)
msgBoard->m_messages.append(msg);
}QQmlListProperty 의 템플릿 클래스 유형(이 경우 Message )은 QML 유형 시스템에 등록되어 있어야 한다는 점에 유의하십시오.
그룹화된 속성
font 과 같은 값 유형이든 객체 유형이든 상관없이, 유형 자체에 하위 속성이 있는 모든 속성은 그룹화된 속성 구문을 사용하여 조작할 수 있습니다. 그룹화된 속성은 유형의 속성 집합을 설명하는 관련 속성 그룹을 노출하는 데 유용합니다.
예를 들어, ` Message::author ` 속성이 단순한 문자열이 아니라 ` MessageAuthor ` 유형이며, ` name ` 및 ` email`이라는 하위 속성을 가지고 있다고 가정해 봅시다:
class MessageAuthor : public QObject
{
Q_PROPERTY(QString name READ name WRITE setName)
Q_PROPERTY(QString email READ email WRITE setEmail)
public:
...
};
class Message : public QObject
{
Q_OBJECT
Q_PROPERTY(MessageAuthor* author READ author)
public:
Message(QObject *parent)
: QObject(parent), m_author(new MessageAuthor(this))
{
}
MessageAuthor *author() const {
return m_author;
}
private:
MessageAuthor *m_author;
};이 경우 author 속성은 QML의 그룹화된 속성 구문을 사용하여 다음과 같이 작성할 수 있습니다:
Message {
author.name: "Alexandra"
author.email: "alexandra@mail.com"
}author 는 객체형 속성이므로, 그룹화된 할당 연산자는 MessageAuthor 객체를 생성하지 않습니다. 이 할당 연산자는 Message::author() 가 반환하는 어떤 객체에든 적용됩니다. 이로 인해 몇 가지 결과가 발생합니다:
- 하위 속성 이름은 실행 시점에 우연히 그 안에 저장된 객체의 유형이 아니라, 속성의 선언된 유형(여기서는
MessageAuthor)을 기준으로 해결됩니다.Message의 서로 다른 인스턴스화는 서로 다른 유형의author객체를 생성할 수 있습니다. QML 컴포넌트를 생성할 때 신뢰할 수 있는 유일한 것은 선언된 유형뿐입니다. - 그룹화된 할당이 적용될 때 해당 객체는 반드시 존재해야 합니다. 그렇지 않으면 엔진이
Cannot set properties on author as it is null와 유사한 오류를 발생시킵니다. 위와 같이 소유자의 생성자에서 객체를 생성하는 것이 이를 보장하는 가장 간단한 방법이지만, 첫 번째 접근 시 객체를 생성하는 게터(getter)를 사용하는 것도 마찬가지로 효과적입니다. - 객체의 수명은 C++ 구현에 따라 결정됩니다. 그룹화된 속성 구문은 객체를 생성하거나 소멸시키지 않습니다.
위 예제와 같이 ` author `를 읽기 전용으로 선언하면, QML 코드가 ` MessageAuthor ` 객체를 대체하는 것을 추가로 방지할 수 있습니다. 만약 ` author `에 ` WRITE ` 액세서가 있다면, 다음과 같이 새로운 객체를 할당하는 것도 가능합니다:
Message {
author: MessageAuthor {
name: "Alexandra"
email: "alexandra@mail.com"
}
}동일한 객체 정의 내에서 동일한 속성에 대해 두 가지 형식을 함께 사용할 수는 없습니다. author 에 객체를 할당하고, 동시에 같은 Message 내에서 author.name 을 작성하면 오류가 발생합니다. 그룹화된 속성을 다른 범위에 두면 이 제한을 우회할 수 있습니다. 하지만 이렇게 하면 생성자에 의해 생성된 객체와 명시적으로 할당된 객체, 두 개의 ` MessageAuthor ` 객체가 존재하게 되어 결과가 혼란스러워집니다. 그룹화된 속성이 어느 객체에 적용될지는 바인딩 평가 순서에 의해서만 결정됩니다.
따라서 권장되는 방법은 다음과 같습니다:
- 그룹화된 속성으로 사용되는 객체는 읽기 전용으로 유지하십시오.
- 그룹 자체의 기본 속성도 조작해야 하는 경우에는 그룹화된 속성 구문을 사용하지 마십시오.
font 와 같은 값형 속성의 경우, 엔진이 값을 읽은 후 수정하고 다시 기록하기 때문에 그룹화된 할당을 사용하려면 해당 속성이 쓰기 가능해야 합니다. 하지만 여전히 혼란의 여지가 있다는 점은 문제입니다. font 전체를 제자리에서 재할당한 다음 다른 곳에서 font.bold 속성을 조작하는 경우, 바인딩 평가 순서에 따라 어느 작업이 먼저 수행되는지, 그리고 그 결과 bold 의 값이 어떻게 결정되는지가 결정됩니다.
메서드 노출 (Qt 슬롯 포함)
QObject 에서 파생된 유형의 모든 메서드는 다음 조건을 충족할 경우 QML 코드에서 접근할 수 있습니다:
- Q_INVOKABLE() 매크로로 표시된 public 메서드
- 공개 Qt 슬롯인 메서드
예를 들어, 아래의 ` MessageBoard ` 클래스에는 ` Q_INVOKABLE ` 매크로로 표시된 ` postMessage() ` 메서드와, 공용 슬롯인 ` refresh() ` 메서드가 있습니다:
class MessageBoard : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
Q_INVOKABLE bool postMessage(const QString&msg) {
qDebug() << "Called the C++ method with" << msg;
return true;
}
public slots:
void refresh() {
qDebug() << "Called the C++ slot";
}
};MessageBoard 의 인스턴스가 파일 MyItem.qml 의 필수 속성으로 설정된 경우, MyItem.qml 는 아래 예제와 같이 두 메서드를 호출할 수 있습니다:
| C++ | |
| QML |
C++ 메서드에 ` QObject* ` 유형의 매개변수가 있는 경우, 해당 매개변수 값은 ` id ` 객체나 해당 객체를 참조하는 ` var ` JavaScript 값을 사용하여 QML에서 전달할 수 있습니다.
QML은 오버로드된 C++ 함수의 호출을 지원합니다. 이름이 같지만 인수가 다른 C++ 함수가 여러 개 있는 경우, 제공된 인수의 개수와 유형에 따라 올바른 함수가 호출됩니다.
C++ 메서드에서 반환된 값은 QML 내의 JavaScript 표현식을 통해 액세스할 때 JavaScript 값으로 변환됩니다.
C++ 메서드와 'this' 객체
한 객체에서 C++ 메서드를 가져와 다른 객체에 대해 호출하고 싶을 수 있습니다. Example 라는 QML 모듈 내의 다음 예제를 살펴보겠습니다:
| C++ | |
| QML | |
적절한 main.cpp 파일에서 이 QML 코드를 로드하면 "invoked on parent"가 출력되어야 합니다. 하지만 오랫동안 존재해 온 버그로 인해 실제로는 출력되지 않습니다. 역사적으로 C++ 기반 메서드의 'this' 객체는 해당 메서드와 불가분의 관계로 묶여 있었습니다. 기존 코드에서 이 동작을 변경하면 'this' 객체가 여러 곳에서 암시적으로 사용되고 있기 때문에 미묘한 오류가 발생할 수 있습니다. Qt 6.5부터는 올바른 동작을 명시적으로 선택하여 C++ 메서드가 'this' 객체를 수락하도록 할 수 있습니다. 이를 위해 Qml 문서에 다음 프래그마를 추가하십시오:
pragma NativeMethodBehavior: AcceptThisObject이 줄을 추가하면 위의 예제가 예상대로 작동합니다.
toString() 오버라이딩
toString이라는 이름의 Q_INVOKABLE 메서드(인수 없음)를 정의하면, 해당 메서드가 JavaScript의 기본 toString 구현 대신 객체를 문자열로 변환하는 데 사용됩니다.
시그널 노출
QObject 에서 파생된 유형의 모든 공개 시그널은 QML 코드에서 액세스할 수 있습니다.
QML 엔진에서는 QML에서 사용되는 QObject 에서 파생된 유형의 모든 신호에 대해 신호 핸들러를 자동으로 생성합니다. 신호 핸들러의 이름은 항상 on<Signal> 형식이며, 여기서 <Signal> 은 신호의 이름으로, 첫 글자는 대문자로 표기됩니다. 신호를 통해 전달된 모든 매개변수는 신호 핸들러 내에서 매개변수 이름을 통해 사용할 수 있습니다.
예를 들어, MessageBoard 클래스에 subject 라는 단일 매개변수를 갖는 newMessagePosted() 신호가 있다고 가정해 봅시다:
class MessageBoard : public QObject
{
Q_OBJECT
public:
// ...
signals:
void newMessagePosted(const QString &subject);
};MessageBoard 유형이 QML 유형 시스템에 등록되어 있다면, QML에서 선언된 MessageBoard 객체는 onNewMessagePosted 라는 신호 핸들러를 사용하여 newMessagePosted() 신호를 수신하고, subject 매개변수의 값을 확인할 수 있습니다:
MessageBoard {
onNewMessagePosted: (subject)=> console.log("New message received:", subject)
}속성 값 및 메서드 매개변수와 마찬가지로, 신호 매개변수의 유형은 QML 엔진에서 지원하는 유형이어야 합니다. 자세한 내용은 ‘QML과 C++ 간의 데이터 유형 변환’을 참조하십시오. (등록되지 않은 유형을 사용해도 오류는 발생하지 않지만, 핸들러에서는 해당 매개변수 값에 접근할 수 없습니다.)
클래스에는 동일한 이름을 가진 여러 신호가 있을 수 있지만, QML 신호로 접근할 수 있는 것은 마지막 신호뿐입니다. 동일한 이름을 가지지만 매개변수가 다른 신호들은 서로 구별할 수 없다는 점에 유의하십시오.
‘ QML Type Registration Macros ’ 및 ‘C++에서 QML 유형 정의’항목도 참조하십시오 .
© 2026 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.