이 페이지에서

Qt Quick 프로그래밍 시작하기: 알람 애플리케이션

알람 애플리케이션을 기반으로 한 Qt Quick 튜토리얼.

이 튜토리얼은 Qt Quick 와 Qt Quick Controls 를 소개하는 차원에서 간단한 알람 애플리케이션을 개발하는 방법을 보여줍니다.

이 튜토리얼에서는 알람을 추가, 수정 또는 삭제할 수 있습니다. 알람은 지정된 날짜에 작동할 수 있으며, 이후 일련의 날짜에 반복되도록 설정할 수도 있습니다. 이 애플리케이션은 일반적으로 안드로이드 폰에서 볼 수 있는 알람 애플리케이션과 유사합니다.

예제 실행

다음 경로에서 예제를 실행할 수 있습니다.

Alarms 프로젝트 생성

이 섹션에서는 Qt Creator 에서 프로젝트를 생성하는 방법을 설명합니다. 여기서는 Qt Creator 에서 자동으로 생성되는 파일과, 프로그래머가 Qt Creator 또는 다른 편집기에서 직접 만들어야 하는 두 개의 파일에 대해 다룹니다. 후자의 두 파일은 이 튜토리얼의 소스 코드에 포함되어 있습니다.

참고: Qt Creator 의UI 텍스트와 생성된 파일의 내용은 사용 중인 Qt Creator 버전에 따라 달라집니다.

Qt Creator

Qt Creator 에서 새 프로젝트를 설정할 때는 프로젝트 생성 과정을 단계별로 안내하는 마법사가 도움을 줍니다. 이 마법사는 해당 프로젝트 유형에 필요한 설정을 입력하도록 안내하고, 프로젝트를 자동으로 생성해 줍니다.

‘Alarms’ 프로젝트를 생성하려면 File > New Project > Application (Qt) > Qt Quick Application > ‘ Choose ’을 선택하십시오. ‘ Name ’ 필드에 ‘alarms’를 입력하고 마법사의 지침을 따르십시오. CMake 이외의 빌드 시스템을 사용하거나 Qt 6보다 낮은 버전을 사용하려면 ‘ Qt Quick Application (compact)’을 선택하십시오.

새 프로젝트 설정하기

프로젝트 위치 설정

Qt Quick 애플리케이션 마법사는 다음 소스 파일이 포함된 프로젝트를 생성합니다:

소스 파일용도
CMakeLists.txt프로젝트 파일
main.cpp애플리케이션의 주요 C++ 코드 파일입니다.
Main.qml애플리케이션의 주요 QML 코드 파일입니다. 이 파일에서 사용자 정의 QML 유형(AlarmDialog, AlarmModel, AlarmDelegate 및 TumblerDelegate)을 인스턴스화할 것입니다.

마법사는 아래의 main.cpp 파일에 코드를 생성합니다. 이 코드 블록은 고해상도(High DPI) 스케일링을 활성화하고 app 및 engine 을 선언합니다. 그런 다음 엔진이 메인 QML 파일을 로드합니다.

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    QObject::connect(
            &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
            []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);

추가 소스 파일

소스 파일목적
qtquickcontrols2.confMaterial 스타일을 Dark 테마와 함께 선택합니다.
AlarmDialog.qml새 알람을 추가하기 위한 대화 상자를 정의합니다.
AlarmDelegate.qml앱의 메인 화면 레이아웃을 정의합니다.
AlarmModel.qml알람 데이터를 저장하는 데 사용되는 ListModel 을 정의합니다.
TumblerDelegate.qml텀블러(Tumblers)의 그래픽 레이아웃을 정의합니다.
qml.qrcmain.cpp 및 프로젝트 파일을 제외한 소스 파일의 이름이 포함된 리소스 파일입니다.
qtquickcontrols2.conf

다음 코드 조각은 Material 스타일에서 Dark 테마를 설정하는 방법을 보여줍니다:

[Controls]
Style=Material
[Material]
Theme=Dark
Accent=Red
Main.qml

mainWindow ApplicationWindow 의 QML 유형인 [...], 이 앱의 루트 항목입니다.

ApplicationWindow {
    id: window
    width: 400
    height: 500
    visible: true

ListView alarmListView 는 alarmModel 의 데이터와 alarmDelegate 에 정의된 레이아웃을 결합합니다.

    ListView {
        id: alarmListView
        anchors.fill: parent
        model: AlarmModel {}
        delegate: AlarmDelegate {}
    }

RoundButton ( addAlarmButton)을 클릭하면 새로운 알람을 추가할 수 있습니다. 이를 클릭하면 Dialog 화면( alarmDialog)이 열립니다.

    RoundButton {
        id: addAlarmButton
        text: "+"
        anchors.bottom: alarmListView.bottom
        anchors.bottomMargin: 8
        anchors.horizontalCenter: parent.horizontalCenter
        onClicked: alarmDialog.open()
    }

    AlarmDialog {
        id: alarmDialog
        x: Math.round((parent.width - width) / 2)
        y: Math.round((parent.height - height) / 2)
        alarmModel: alarmListView.model
    }
AlarmDialog.qml

이 대화 상자 화면에는 시간과 분을 각각 설정할 수 있는 Tumbler 이 있는 RowLayout 과, 일, 월, 연도를 각각 설정할 수 있는 Tumbler이 있는 또 다른 RowLayout 이 있습니다.

    contentItem: RowLayout {
        RowLayout {
            id: rowTumbler

            Tumbler {
                id: hoursTumbler
                model: 24
                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
            Tumbler {
                id: minutesTumbler
                model: 60
                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
        }

        RowLayout {
            id: datePicker

            Layout.leftMargin: 20

            readonly property var days: [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

            Tumbler {
                id: dayTumbler

                function updateModel() {
                    // Populate the model with days of the month. For example: [0, ..., 30]
                    var previousIndex = dayTumbler.currentIndex
                    var array = []
                    var newDays = datePicker.days[monthTumbler.currentIndex]
                    for (let i = 1; i <= newDays; ++i)
                        array.push(i)
                    dayTumbler.model = array
                    dayTumbler.currentIndex = Math.min(newDays - 1, previousIndex)
                }

                Component.onCompleted: updateModel()

                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
            Tumbler {
                id: monthTumbler

                onCurrentIndexChanged: dayTumbler.updateModel()

                model: 12
                delegate: TumblerDelegate {
                    text: alarmDialog.locale.standaloneMonthName(modelData, Locale.ShortFormat)
                }
            }
            Tumbler {
                id: yearTumbler

                // This array is populated with the next three years. For example: [2018, 2019, 2020]
                readonly property var years: (function() {
                    var currentYear = new Date().getFullYear()
                    return [0, 1, 2].map(function(value) { return value + currentYear; })
                })()

                model: years
                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
        }
    }
}

이 대화 상자에서 ‘확인(OK) ’을 클릭하면 입력된 데이터가 ‘ alarmModel ’에 추가됩니다:

    onAccepted: {
        alarmModel.append({
            "hour": hoursTumbler.currentIndex,
            "minute": minutesTumbler.currentIndex,
            "day": dayTumbler.currentIndex + 1,
            "month": monthTumbler.currentIndex + 1,
            "year": yearTumbler.years[yearTumbler.currentIndex],
            "activated": true,
            "label": "",
            "repeat": false,
            "daysToRepeat": [
                { "dayOfWeek": 0, "repeat": false },
                { "dayOfWeek": 1, "repeat": false },
                { "dayOfWeek": 2, "repeat": false },
                { "dayOfWeek": 3, "repeat": false },
                { "dayOfWeek": 4, "repeat": false },
                { "dayOfWeek": 5, "repeat": false },
                { "dayOfWeek": 6, "repeat": false }
            ],
        })
    }
    onRejected: alarmDialog.close()
AlarmDelegate.qml

메인 화면의 각 알람은 ItemDelegate 객체입니다. ItemDelegate root 에는 메인 화면과 상세 화면의 모든 필드가 포함되어 있습니다. 상세 화면의 필드는 알람을 클릭한 후, 즉 root.checked 의 상태가 true 일 때만 표시됩니다.

ItemDelegate {
    id: root
    width: parent.width
    checkable: true

    required property int index
    required property int hour
    required property int minute
    required property int day
    required property int month
    required property int year
    required property bool activated
    required property string label
    required property bool repeat
    required property list<var> daysToRepeat

    onClicked: ListView.view.currentIndex = index

    contentItem: ColumnLayout {
        spacing: 0

        RowLayout {
            ColumnLayout {
                id: dateColumn

                readonly property date alarmDate: new Date(
                    root.year, root.month - 1, root.day, root.hour, root.minute)

                Label {
                    id: timeLabel
                    font.pixelSize: (Qt.application as Application).font.pixelSize * 2
                    text: dateColumn.alarmDate.toLocaleTimeString(root.locale, Locale.ShortFormat)
                }
                RowLayout {
                    Label {
                        id: dateLabel
                        text: dateColumn.alarmDate.toLocaleDateString(root.locale, Locale.ShortFormat)
                    }
                    Label {
                        id: alarmAbout
                        text: "⸱ " + root.label
                        visible: root.label.length > 0 && !root.checked
                    }
                }
            }
            Item {
                Layout.fillWidth: true
            }
            Switch {
                checked: root.activated
                Layout.alignment: Qt.AlignTop
                onClicked: root.activated = checked
            }
        }
        CheckBox {
            id: alarmRepeat
            text: qsTr("Repeat")
            checked: root.repeat
            visible: root.checked
            onToggled: root.repeat = checked
        }
        Flow {
            visible: root.checked && root.repeat
            Layout.fillWidth: true

            Repeater {
                id: dayRepeater
                model: root.daysToRepeat
                delegate: RoundButton {
                    required property int dayOfWeek
                    required property bool repeat
                    text: Qt.locale().dayName(dayOfWeek, Locale.NarrowFormat)
                    flat: true
                    checked: repeat
                    checkable: true
                    Material.background: checked ? Material.accent : "transparent"
                    onToggled: repeat = checked
                }
            }
        }

        TextField {
            id: alarmDescriptionTextField
            placeholderText: qsTr("Enter description here")
            cursorVisible: true
            visible: root.checked
            text: root.label
            onTextEdited: root.label = text
        }
        Button {
            id: deleteAlarmButton
            text: qsTr("Delete")
            visible: root.checked
            onClicked: root.ListView.view.model.remove(root.ListView.view.currentIndex, 1)
        }
    }
}
AlarmModel.qml

이 QML 파일에는 알람 데이터를 관리하는 ListModel 인 alarmModel 의 정의가 포함되어 있습니다.

이 파일은 예시 알람이 포함된 5개의 ` ListElements ` 객체를 생성합니다.

import QtQuick

// Populate the model with some sample data.
ListModel {
    id: alarmModel

    ListElement {
        hour: 6
        minute: 0
        day: 2
        month: 8
        year: 2018
        activated: true
        label: "Wake up"
        repeat: true
        daysToRepeat: [
            ListElement { dayOfWeek: 0; repeat: false },
            ListElement { dayOfWeek: 1; repeat: false },
            ListElement { dayOfWeek: 2; repeat: false },
            ListElement { dayOfWeek: 3; repeat: false },
            ListElement { dayOfWeek: 4; repeat: false },
            ListElement { dayOfWeek: 5; repeat: false },
            ListElement { dayOfWeek: 6; repeat: false }
        ]
    }
    ListElement {
        hour: 6
        minute: 0
        day: 3
        month: 8
        year: 2018
        activated: true
        label: "Wake up"
        repeat: true
        daysToRepeat: [
            ListElement { dayOfWeek: 0; repeat: true },
            ListElement { dayOfWeek: 1; repeat: true },
            ListElement { dayOfWeek: 2; repeat: true },
            ListElement { dayOfWeek: 3; repeat: true },
            ListElement { dayOfWeek: 4; repeat: true },
            ListElement { dayOfWeek: 5; repeat: false },
            ListElement { dayOfWeek: 6; repeat: false }
        ]
    }
    ListElement {
        hour: 7
        minute: 0
        day: 3
        month: 8
        year: 2018
        activated: false
        label: "Exercise"
        repeat: true
        daysToRepeat: [
            ListElement { dayOfWeek: 0; repeat: true },
            ListElement { dayOfWeek: 1; repeat: true },
            ListElement { dayOfWeek: 2; repeat: true },
            ListElement { dayOfWeek: 3; repeat: true },
            ListElement { dayOfWeek: 4; repeat: true },
            ListElement { dayOfWeek: 5; repeat: true },
            ListElement { dayOfWeek: 6; repeat: true }
        ]
    }
    ListElement {
        hour: 5
        minute: 15
        day: 1
        month: 9
        year: 2018
        activated: true
        label: ""
        repeat: false
        daysToRepeat: [
            ListElement { dayOfWeek: 0; repeat: false },
            ListElement { dayOfWeek: 1; repeat: false },
            ListElement { dayOfWeek: 2; repeat: false },
            ListElement { dayOfWeek: 3; repeat: false },
            ListElement { dayOfWeek: 4; repeat: false },
            ListElement { dayOfWeek: 5; repeat: false },
            ListElement { dayOfWeek: 6; repeat: false }
        ]
    }
    ListElement {
        hour: 5
        minute: 45
        day: 3
        month: 9
        year: 2018
        activated: false
        label: ""
        repeat: false
        daysToRepeat: [
            ListElement { dayOfWeek: 0; repeat: false },
            ListElement { dayOfWeek: 1; repeat: false },
            ListElement { dayOfWeek: 2; repeat: false },
            ListElement { dayOfWeek: 3; repeat: false },
            ListElement { dayOfWeek: 4; repeat: false },
            ListElement { dayOfWeek: 5; repeat: false },
            ListElement { dayOfWeek: 6; repeat: false }
        ]
    }
}
TumblerDelegate.qml

TumblerDelegate는 텀블러의 그래픽 속성을 정의합니다.

import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material

Text {
    required property int modelData
    required property int index
    text: modelData
    color: Tumbler.tumbler.Material.foreground
    font: Tumbler.tumbler.font
    opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2)
    horizontalAlignment: Text.AlignHCenter
    verticalAlignment: Text.AlignVCenter
}

새 알람 추가

시작 화면 하단에는 알람을 추가할 수 있는 버튼이 있습니다. 이 버튼을 클릭하면 ‘새 알람 추가’ 대화 상자가 열립니다.

    RoundButton {
        id: addAlarmButton
        text: "+"
        anchors.bottom: alarmListView.bottom
        anchors.bottomMargin: 8
        anchors.horizontalCenter: parent.horizontalCenter
        onClicked: alarmDialog.open()
    }

새 알람 대화 상자:

알람 설정하기

모든 필드는 ` Tumbler ` QML 유형을 사용하여 입력됩니다. ` OK`를 누르면 텀블러에서 선택된 값이 ` alarmModel`에 기록됩니다.

    contentItem: RowLayout {
        RowLayout {
            id: rowTumbler

            Tumbler {
                id: hoursTumbler
                model: 24
                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
            Tumbler {
                id: minutesTumbler
                model: 60
                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
        }

        RowLayout {
            id: datePicker

            Layout.leftMargin: 20

            readonly property var days: [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

            Tumbler {
                id: dayTumbler

                function updateModel() {
                    // Populate the model with days of the month. For example: [0, ..., 30]
                    var previousIndex = dayTumbler.currentIndex
                    var array = []
                    var newDays = datePicker.days[monthTumbler.currentIndex]
                    for (let i = 1; i <= newDays; ++i)
                        array.push(i)
                    dayTumbler.model = array
                    dayTumbler.currentIndex = Math.min(newDays - 1, previousIndex)
                }

                Component.onCompleted: updateModel()

                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
            Tumbler {
                id: monthTumbler

                onCurrentIndexChanged: dayTumbler.updateModel()

                model: 12
                delegate: TumblerDelegate {
                    text: alarmDialog.locale.standaloneMonthName(modelData, Locale.ShortFormat)
                }
            }
            Tumbler {
                id: yearTumbler

                // This array is populated with the next three years. For example: [2018, 2019, 2020]
                readonly property var years: (function() {
                    var currentYear = new Date().getFullYear()
                    return [0, 1, 2].map(function(value) { return value + currentYear; })
                })()

                model: years
                delegate: TumblerDelegate {
                    text: alarmDialog.formatNumber(modelData)
                }
            }
        }
    }
}

알람 편집

특정 알람을 클릭하면 상세 화면에서 해당 알람을 편집할 수 있습니다.

요일별로 다른 알람 설정

알람을 클릭하면 root.checked 가 true 로 설정되어 상세 화면의 필드가 표시됩니다.

visible: root.checked

다른 요일에도 알람이 트리거되도록 하려면 ‘ alarmRepeat ’을 선택하십시오. 리피터는 요일별로 선택 가능한 ‘ RoundButton ’을 표시합니다.

        Flow {
            visible: root.checked && root.repeat
            Layout.fillWidth: true

            Repeater {
                id: dayRepeater
                model: root.daysToRepeat
                delegate: RoundButton {
                    required property int dayOfWeek
                    required property bool repeat
                    text: Qt.locale().dayName(dayOfWeek, Locale.NarrowFormat)
                    flat: true
                    checked: repeat
                    checkable: true
                    Material.background: checked ? Material.accent : "transparent"
                    onToggled: repeat = checked
                }
            }
        }

알람 설명을 수정하면, 이후 메인 화면에 해당 내용이 반영됩니다.

        TextField {
            id: alarmDescriptionTextField
            placeholderText: qsTr("Enter description here")
            cursorVisible: true
            visible: root.checked
            text: root.label
            onTextEdited: root.label = text
        }

알람 삭제

상기 설명한 상세 화면에는 알람을 삭제하는 버튼이 있습니다. ‘ onClicked ’ 이벤트가 발생하면, 현재 ListElement 가 alarmModel 에서 삭제됩니다.

        Button {
            id: deleteAlarmButton
            text: qsTr("Delete")
            visible: root.checked
            onClicked: root.ListView.view.model.remove(root.ListView.view.currentIndex, 1)
        }

다음 단계

이 앱에는 알람에 소리나 진동을 추가하는 코드가 없으며, 알람을 어떤 형식이나 데이터베이스에도 저장하지 않습니다. 프로젝트에 이러한 기능을 추가하여 실력을 시험해 보세요. 데이터 저장은 JSON 형식으로 수행할 수 있습니다.

소스 파일

코드.qt.io의 예제 프로젝트

참조: Qt의 JSON 지원, 모든 Qt 예제, Qt Quick 예제 및 튜토리얼.

© 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.