주소록

주소록 예제에서는 프록시 모델을 사용하여 단일 모델의 데이터에 다양한 보기를 표시하는 방법을 보여 줍니다.

Screenshot of the Address Book example

이 예에서는 연락처를 알파벳순으로 9개 그룹으로 그룹화할 수 있는 주소록을 제공합니다: ABC, DEF, GHI, ..., ... , vw, ..., xyz. 이는 동일한 모델에서 여러 보기를 사용하여 이루어지며, 각 보기는 QSortFilterProxyModel 클래스의 인스턴스를 사용하여 필터링됩니다.

개요

주소록에는 5개의 클래스가 있습니다: MainWindow, AddressWidget, TableModel, NewAddressTab, AddDialog 입니다. MainWindow 클래스는 AddressWidget 을 중앙 위젯으로 사용하며 FileTools 메뉴를 제공합니다.

Diagram for Address Book example

AddressWidget 클래스는 QTabWidget 의 서브클래스로 예제에 표시된 10개의 탭(9개의 알파벳 그룹 탭과 NewAddressTab 의 인스턴스)을 조작하는 데 사용됩니다. NewAddressTab 클래스는 QWidget 의 서브클래스로 주소록이 비어 있을 때만 사용되며 사용자에게 연락처를 추가하라는 메시지를 표시합니다. AddressWidgetTableModel 의 인스턴스와 상호작용하여 주소록에 항목을 추가, 편집 및 제거합니다.

TableModel 는 데이터에 액세스하기 위한 표준 모델/보기 API를 제공하는 QAbstractTableModel 의 서브클래스입니다. 추가된 연락처 목록을 보유합니다. 그러나 이 데이터가 모두 단일 탭에 표시되지는 않습니다. 대신 QTableView 은 알파벳 그룹에 따라 동일한 데이터의 9가지 보기를 제공하는 데 사용됩니다.

QSortFilterProxyModel 는 각 연락처 그룹에 대한 연락처 필터링을 담당하는 클래스입니다. 각 프록시 모델은 QRegularExpression 을 사용하여 해당 알파벳 그룹에 속하지 않는 연락처를 필터링합니다. AddDialog 클래스는 사용자로부터 주소록에 대한 정보를 가져오는 데 사용됩니다. 이 QDialog 서브클래스는 연락처를 추가하기 위해 NewAddressTab, 연락처를 추가 및 편집하기 위해 AddressWidget 에 의해 인스턴스화됩니다.

TableModel 구현을 살펴보는 것부터 시작하겠습니다.

TableModel 클래스 정의

TableModel 클래스는 QAbstractTableModel 을 서브클래싱하여 연락처 목록의 데이터에 액세스할 수 있는 표준 API를 제공합니다. 이를 위해 구현해야 하는 기본 함수는 다음과 같습니다: rowCount(), columnCount(), data(), headerData() 입니다. TableModel이 편집 가능하려면 insertRows(), removeRows(), setData()flags() 함수의 구현을 제공해야 합니다.

struct Contact
{
    QString name;
    QString address;

    bool operator==(const Contact &other) const
    {
        return name == other.name && address == other.address;
    }
};

inline QDataStream &operator<<(QDataStream &stream, const Contact &contact)
{
    return stream << contact.name << contact.address;
}

inline QDataStream &operator>>(QDataStream &stream, Contact &contact)
{
    return stream >> contact.name >> contact.address;
}

class TableModel : public QAbstractTableModel
{
    Q_OBJECT

public:
    TableModel(QObject *parent = nullptr);
    TableModel(const QList<Contact> &contacts, QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent) const override;
    int columnCount(const QModelIndex &parent) const override;
    QVariant data(const QModelIndex &index, int role) const override;
    QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
    bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
    bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
    const QList<Contact> &getContacts() const;

private:
    QList<Contact> contacts;
};

편의를 위해 TableModel 의 자체 QList<Contact> 를 사용하는 기본 생성자와 QList<Contact> 를 인수로 사용하는 생성자 두 개가 사용됩니다.

TableModel 클래스 구현

헤더 파일에 정의된 대로 두 개의 생성자를 구현합니다. 두 번째 생성자는 매개변수 값을 사용하여 모델의 연락처 목록을 초기화합니다.

TableModel::TableModel(QObject *parent)
    : QAbstractTableModel(parent)
{
}

TableModel::TableModel(const QList<Contact> &contacts, QObject *parent)
    : QAbstractTableModel(parent), contacts(contacts)
{
}

rowCount()columnCount() 함수는 모델의 차원을 반환합니다. rowCount() 의 값은 주소록에 추가된 연락처 수에 따라 달라지지만 columnCount() 의 값은 이름주소 열에 대한 공간만 필요하므로 항상 2입니다.

int TableModel::rowCount(const QModelIndex &parent) const
{
    return parent.isValid() ? 0 : contacts.size();
}

int TableModel::columnCount(const QModelIndex &parent) const
{
    return parent.isValid() ? 0 : 2;
}

data() 함수는 제공된 모델 인덱스의 내용에 따라 이름 또는 주소를 반환합니다. 모델 인덱스에 저장된 행 번호는 연락처 목록에서 항목을 참조하는 데 사용됩니다. 선택은 QItemSelectionModel 함수에 의해 처리되며 AddressWidget 에서 설명합니다.

QVariant TableModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    if (index.row() >= contacts.size() || index.row() < 0)
        return QVariant();

    if (role == Qt::DisplayRole) {
        const auto &contact = contacts.at(index.row());

        switch (index.column()) {
            case 0:
                return contact.name;
            case 1:
                return contact.address;
            default:
                break;
        }
    }
    return QVariant();
}

headerData() 함수는 테이블의 헤더, 이름주소를 표시합니다. 주소록에 번호가 매겨진 항목이 필요한 경우 이 예제에서 숨겨둔 세로 헤더를 사용할 수 있습니다( AddressWidget 구현 참조).

QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();

    if (orientation == Qt::Horizontal) {
        switch (section) {
            case 0:
                return tr("Name");
            case 1:
                return tr("Address");
            default:
                break;
        }
    }
    return QVariant();
}

insertRows() 함수는 새 데이터가 추가되기 전에 호출되며, 그렇지 않으면 데이터가 표시되지 않습니다. beginInsertRows()endInsertRows() 함수는 연결된 모든 뷰가 변경 사항을 인식하도록 하기 위해 호출됩니다.

bool TableModel::insertRows(int position, int rows, const QModelIndex &index)
{
    Q_UNUSED(index);
    beginInsertRows(QModelIndex(), position, position + rows - 1);

    for (int row = 0; row < rows; ++row)
        contacts.insert(position, { QString(), QString() });

    endInsertRows();
    return true;
}

removeRows() 함수는 데이터를 제거하기 위해 호출됩니다. 다시 beginRemoveRows() 및 endRemoveRows() 함수는 연결된 모든 뷰가 변경 사항을 인식하도록 하기 위해 호출됩니다.

bool TableModel::removeRows(int position, int rows, const QModelIndex &index)
{
    Q_UNUSED(index);
    beginRemoveRows(QModelIndex(), position, position + rows - 1);

    for (int row = 0; row < rows; ++row)
        contacts.removeAt(position);

    endRemoveRows();
    return true;
}

setData() 함수는 테이블에 행 단위가 아닌 항목 단위로 데이터를 삽입하는 함수입니다. 즉, 주소록의 한 행을 채우려면 각 행에 2개의 열이 있으므로 setData() 을 두 번 호출해야 합니다. dataChanged () 신호는 연결된 모든 뷰에 디스플레이를 업데이트하도록 알려주므로 이 신호를 보내는 것이 중요합니다.

bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (index.isValid() && role == Qt::EditRole) {
        const int row = index.row();
        auto contact = contacts.value(row);

        switch (index.column()) {
            case 0:
                contact.name = value.toString();
                break;
            case 1:
                contact.address = value.toString();
                break;
            default:
                return false;
        }
        contacts.replace(row, contact);
        emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});

        return true;
    }

    return false;
}

flags() 함수는 주어진 인덱스에 대한 항목 플래그를 반환합니다.

Qt::ItemFlags TableModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::ItemIsEnabled;

    return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
}

Qt::ItemIsEditable 플래그를 설정한 이유는 TableModel 을 편집할 수 있도록 허용하고 싶기 때문입니다. 이 예제에서는 QTableView 객체의 편집 기능을 사용하지 않지만 다른 프로그램에서 모델을 재사용할 수 있도록 여기서는 편집 기능을 활성화했습니다.

TableModel, getContacts() 의 마지막 함수는 주소록에 있는 모든 연락처를 포함하는 QList<Contact> 객체를 반환합니다. 나중에 이 함수를 사용하여 연락처 목록을 가져와 기존 항목을 확인하고 연락처를 파일에 쓴 다음 다시 읽습니다. 자세한 설명은 AddressWidget 을 참조하세요.

const QList<Contact> &TableModel::getContacts() const
{
    return contacts;
}

주소 위젯 클래스 정의

AddressWidget 클래스는 연락처를 추가, 편집 및 제거하고 연락처를 파일에 저장하고 파일에서 로드하는 함수를 제공하므로 기술적으로 이 예제와 관련된 주요 클래스입니다.

class AddressWidget : public QTabWidget
{
    Q_OBJECT

public:
    AddressWidget(QWidget *parent = nullptr);
    void readFromFile();
    void writeToFile();

public slots:
    void showAddEntryDialog();
    void addEntry(const QString &name, const QString &address);
    void editEntry();
    void removeEntry();

signals:
    void selectionChanged (const QItemSelection &selected);

private:
    void setupTabs();

    inline static QString fileName =
        QStandardPaths::standardLocations(QStandardPaths::TempLocation).value(0)
        + QStringLiteral("/addressbook.dat");
    TableModel *table;
    NewAddressTab *newAddressTab;
};

AddressWidgetQTabWidget 를 확장하여 10개의 탭(NewAddressTab 과 9개의 알파벳 그룹 탭)을 보관하고 table, TableModel 객체, proxyModel, 항목을 필터링하는 데 사용하는 QSortFilterProxyModel 객체, tableView, QTableView 객체도 조작합니다.

주소 위젯 클래스 구현

AddressWidget 생성자는 부모 위젯을 받아들이고 NewAddressTab, TableModelQSortFilterProxyModel 을 인스턴스화합니다. 주소록이 비어 있음을 나타내는 데 사용되는 NewAddressTab 객체가 추가되고 나머지 9개 탭은 setupTabs() 으로 설정됩니다.

AddressWidget::AddressWidget(QWidget *parent)
    : QTabWidget(parent),
      table(new TableModel(this)),
      newAddressTab(new NewAddressTab(this))
{
    connect(newAddressTab, &NewAddressTab::sendDetails,
        this, &AddressWidget::addEntry);

    addTab(newAddressTab, tr("Address Book"));

    setupTabs();
}

setupTabs() 함수는 AddressWidget 에서 9개의 알파벳 그룹 탭, 테이블 보기 및 프록시 모델을 설정하는 데 사용됩니다. 각 프록시 모델은 대소문자를 구분하지 않는 QRegularExpression 개체를 사용하여 관련 알파벳 그룹에 따라 연락처 이름을 필터링하도록 설정됩니다. 또한 테이블 보기는 해당 프록시 모델의 sort() 함수를 사용하여 오름차순으로 정렬됩니다.

각 테이블 보기의 selectionModeQAbstractItemView::SingleSelection 으로 설정되고 selectionBehaviorQAbstractItemView::SelectRows 으로 설정되어 사용자가 한 행의 모든 항목을 동시에 선택할 수 있습니다. 각 QTableView 객체에는 선택된 인덱스를 추적하는 QItemSelectionModel 이 자동으로 제공됩니다.

void AddressWidget::setupTabs()
{
    using namespace Qt::StringLiterals;
    const auto groups = { "ABC"_L1, "DEF"_L1, "GHI"_L1, "JKL"_L1, "MNO"_L1, "PQR"_L1,
                          "STU"_L1, "VW"_L1, "XYZ"_L1 };

    for (QLatin1StringView str : groups) {
        const auto regExp = QRegularExpression(QLatin1StringView("^[%1].*").arg(str),
                                               QRegularExpression::CaseInsensitiveOption);

        auto proxyModel = new QSortFilterProxyModel(this);
        proxyModel->setSourceModel(table);
        proxyModel->setFilterRegularExpression(regExp);
        proxyModel->setFilterKeyColumn(0);

        QTableView *tableView = new QTableView;
        tableView->setModel(proxyModel);
        tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
        tableView->horizontalHeader()->setStretchLastSection(true);
        tableView->verticalHeader()->hide();
        tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
        tableView->setSelectionMode(QAbstractItemView::SingleSelection);
        tableView->setSortingEnabled(true);

        connect(tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
                this, &AddressWidget::selectionChanged);

        connect(this, &QTabWidget::currentChanged, this, [this, tableView](int tabIndex) {
            if (widget(tabIndex) == tableView)
                emit selectionChanged(tableView->selectionModel()->selection());
        });

        addTab(tableView, str);
    }
}

QItemSelectionModel 클래스는 AddressWidgetselectionChanged() 신호에 연결된 selectionChanged 신호를 제공합니다. 또한 QTabWidget::currentChanged() 신호를 AddressWidgetselectionChanged() 를 방출하는 람다 표현식에 연결합니다. 이러한 연결은 MainWindow 의 도구 메뉴에서 Edit Entry...Remove Entry 액션을 활성화하는 데 필요합니다. MainWindow 의 구현에 자세히 설명되어 있습니다.

주소록의 각 테이블 보기는 그룹의 QStringList 에서 얻은 관련 레이블을 사용하여 QTabWidget 에 탭으로 추가됩니다.

Signals and Slots Connections

두 개의 addEntry() 함수를 제공합니다: 하나는 사용자 입력을 받는 데 사용되는 것이고, 다른 하나는 주소록에 새 항목을 추가하는 실제 작업을 수행하는 것입니다. newAddressTab 에서 대화 상자를 띄우지 않고도 데이터를 삽입할 수 있도록 항목 추가를 두 부분으로 나누었습니다.

첫 번째 addEntry() 함수는 MainWindowAdd Entry... 액션에 연결된 슬롯입니다. 이 함수는 AddDialog 객체를 생성한 다음 두 번째 addEntry() 함수를 호출하여 실제로 table 에 연락처를 추가합니다.

void AddressWidget::showAddEntryDialog()
{
    AddDialog aDialog;

    if (aDialog.exec())
        addEntry(aDialog.name(), aDialog.address());
}

주소록의 중복 항목을 방지하기 위해 두 번째 addEntry() 함수에서 기본 유효성 검사가 수행됩니다. TableModel 에서 언급했듯이 이것이 바로 게터 메서드 getContacts() 가 필요한 이유 중 하나입니다.

void AddressWidget::addEntry(const QString &name, const QString &address)
{
    if (!name.front().isLetter()) {
        QMessageBox::information(this, tr("Invalid name"),
            tr("The name must start with a letter."));
    } else if (!table->getContacts().contains({ name, address })) {
        table->insertRows(0, 1, QModelIndex());

        QModelIndex index = table->index(0, 0, QModelIndex());
        table->setData(index, name, Qt::EditRole);
        index = table->index(0, 1, QModelIndex());
        table->setData(index, address, Qt::EditRole);
        removeTab(indexOf(newAddressTab));
    } else {
        QMessageBox::information(this, tr("Duplicate Name"),
            tr("The name \"%1\" already exists.").arg(name));
    }
}

모델에 이미 같은 이름의 항목이 없는 경우 setData() 을 호출하여 이름과 주소를 첫 번째와 두 번째 열에 삽입합니다. 그렇지 않으면 QMessageBox 을 표시하여 사용자에게 알립니다.

참고: 주소록이 더 이상 비어 있지 않으므로 연락처가 추가되면 newAddressTab 주소는 제거됩니다.

이 예에서는 사용자가 기존 연락처의 이름을 변경할 수 없으므로 항목 편집은 연락처의 주소만 업데이트하는 방법입니다.

먼저 QTabWidget::currentWidget()을 사용하여 활성 탭의 QTableView 개체를 가져옵니다. 그런 다음 tableView 에서 selectionModel 을 추출하여 선택한 인덱스를 얻습니다.

void AddressWidget::editEntry()
{
    QTableView *temp = static_cast<QTableView*>(currentWidget());
    QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model());
    QItemSelectionModel *selectionModel = temp->selectionModel();

    const QModelIndexList indexes = selectionModel->selectedRows();
    QString name;
    QString address;
    int row = -1;

    for (const QModelIndex &index : indexes) {
        row = proxy->mapToSource(index).row();
        QModelIndex nameIndex = table->index(row, 0, QModelIndex());
        QVariant varName = table->data(nameIndex, Qt::DisplayRole);
        name = varName.toString();

        QModelIndex addressIndex = table->index(row, 1, QModelIndex());
        QVariant varAddr = table->data(addressIndex, Qt::DisplayRole);
        address = varAddr.toString();
    }

다음으로 사용자가 편집하려는 행에서 데이터를 추출합니다. 이 데이터는 다른 창 제목을 가진 AddDialog 인스턴스에 표시됩니다. tableaDialog 의 데이터가 변경된 경우에만 업데이트됩니다.

    AddDialog aDialog;
    aDialog.setWindowTitle(tr("Edit a Contact"));
    aDialog.editAddress(name, address);

    if (aDialog.exec()) {
        const QString newAddress = aDialog.address();
        if (newAddress != address) {
            const QModelIndex index = table->index(row, 1, QModelIndex());
            table->setData(index, newAddress, Qt::EditRole);
        }
    }
}

Screenshot of Dialog to Edit a Contact

항목은 removeEntry() 함수를 사용하여 제거됩니다. 선택한 행은 QItemSelectionModel 객체( selectionModel)를 통해 액세스하여 제거됩니다. newAddressTab 은 사용자가 주소록의 모든 연락처를 제거한 경우에만 AddressWidget 에 다시 추가됩니다.

void AddressWidget::removeEntry()
{
    QTableView *temp = static_cast<QTableView*>(currentWidget());
    QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model());
    QItemSelectionModel *selectionModel = temp->selectionModel();

    const QModelIndexList indexes = selectionModel->selectedRows();

    for (QModelIndex index : indexes) {
        int row = proxy->mapToSource(index).row();
        table->removeRows(row, 1, QModelIndex());
    }

    if (table->rowCount(QModelIndex()) == 0)
        insertTab(0, newAddressTab, tr("Address Book"));
}

writeToFile() 함수는 주소록의 모든 연락처가 포함된 파일을 저장하는 데 사용됩니다. 파일은 사용자 지정 .dat 형식으로 저장됩니다. 연락처 목록의 내용은 QDataStream 을 사용하여 file 에 기록됩니다. 파일을 열 수 없는 경우 관련 오류 메시지와 함께 QMessageBox 이 표시됩니다.

void AddressWidget::writeToFile()
{
    QFile file(fileName);

    if (!file.open(QIODevice::WriteOnly)) {
        QMessageBox::information(this, tr("Unable to open file"), file.errorString());
        return;
    }

    QDataStream out(&file);
    out << table->getContacts();
}

readFromFile() 함수는 이전에 writeToFile() 을 사용하여 저장한 주소록의 모든 연락처가 포함된 파일을 로드합니다. QDataStream.dat 파일의 내용을 연락처 목록으로 읽는 데 사용되며, 각 연락처는 addEntry() 을 사용하여 추가됩니다.

void AddressWidget::readFromFile()
{
    QFile file(fileName);

    if (!file.open(QIODevice::ReadOnly)) {
        QMessageBox::information(this, tr("Unable to open file"),
            file.errorString());
        return;
    }

    QList<Contact> contacts;
    QDataStream in(&file);
    in >> contacts;

    if (contacts.isEmpty()) {
        QMessageBox::information(this, tr("No contacts in file"),
                                 tr("The file you are attempting to open contains no contacts."));
    } else {
        for (const auto &contact: std::as_const(contacts))
            addEntry(contact.name, contact.address);
    }
}

새 주소 탭 클래스 정의

NewAddressTab 클래스는 주소록이 비어 있음을 사용자에게 알려주는 정보 탭을 제공합니다. AddressWidget 의 구현에 언급된 대로 주소록의 내용에 따라 나타나고 사라집니다.

Screenshot of NewAddressTab

NewAddressTab 클래스는 QWidget 을 확장하고 QLabelQPushButton 을 포함합니다.

class NewAddressTab : public QWidget
{
    Q_OBJECT

public:
    NewAddressTab(QWidget *parent = nullptr);

public slots:
    void addEntry();

signals:
    void sendDetails(const QString &name, const QString &address);
};

NewAddressTab 클래스 구현

생성자는 addButton, descriptionLabel 을 인스턴스화하고 addButton 의 신호를 addEntry() 슬롯에 연결합니다.

NewAddressTab::NewAddressTab(QWidget *parent)
    : QWidget(parent)
{
    auto descriptionLabel = new QLabel(tr("There are currently no contacts in your address book. "
                                          "\nClick Add to add new contacts."));

    auto addButton = new QPushButton(tr("Add"));

    connect(addButton, &QAbstractButton::clicked, this, &NewAddressTab::addEntry);

    auto mainLayout = new QVBoxLayout;
    mainLayout->addWidget(descriptionLabel);
    mainLayout->addWidget(addButton, 0, Qt::AlignCenter);

    setLayout(mainLayout);
}

addEntry() 함수는 AddressWidgetaddEntry() 와 유사하며, 두 함수 모두 AddDialog 객체를 인스턴스화한다는 점에서 유사합니다. 대화 상자의 데이터는 추출되어 sendDetails() 신호를 방출하여 AddressWidgetaddEntry() 슬롯으로 전송됩니다.

void NewAddressTab::addEntry()
{
    AddDialog aDialog;

    if (aDialog.exec())
        emit sendDetails(aDialog.name(), aDialog.address());
}

AddDialog 클래스 정의

AddDialog 클래스는 QDialog 을 확장하고 사용자에게 주소록에 데이터를 입력할 수 있는 QLineEditQTextEdit 을 제공합니다.

class AddDialog : public QDialog
{
    Q_OBJECT

public:
    AddDialog(QWidget *parent = nullptr);

    QString name() const;
    QString address() const;
    void editAddress(const QString &name, const QString &address);

private:
    QLineEdit *nameText;
    QTextEdit *addressText;
};

AddDialog 클래스 구현

AddDialog 의 생성자는 사용자 인터페이스를 설정하고 필요한 위젯을 생성하여 레이아웃에 배치합니다.

AddDialog::AddDialog(QWidget *parent)
    : QDialog(parent),
      nameText(new QLineEdit),
      addressText(new QTextEdit)
{
    auto nameLabel = new QLabel(tr("Name"));
    auto addressLabel = new QLabel(tr("Address"));
    auto okButton = new QPushButton(tr("OK"));
    auto cancelButton = new QPushButton(tr("Cancel"));

    auto gLayout = new QGridLayout;
    gLayout->setColumnStretch(1, 2);
    gLayout->addWidget(nameLabel, 0, 0);
    gLayout->addWidget(nameText, 0, 1);

    gLayout->addWidget(addressLabel, 1, 0, Qt::AlignLeft|Qt::AlignTop);
    gLayout->addWidget(addressText, 1, 1, Qt::AlignLeft);

    auto buttonLayout = new QHBoxLayout;
    buttonLayout->addWidget(okButton);
    buttonLayout->addWidget(cancelButton);

    gLayout->addLayout(buttonLayout, 2, 1, Qt::AlignRight);

    auto mainLayout = new QVBoxLayout;
    mainLayout->addLayout(gLayout);
    setLayout(mainLayout);

    connect(okButton, &QAbstractButton::clicked, this, &QDialog::accept);
    connect(cancelButton, &QAbstractButton::clicked, this, &QDialog::reject);

    setWindowTitle(tr("Add a Contact"));
}

QString AddDialog::name() const
{
    return nameText->text();
}

QString AddDialog::address() const
{
    return addressText->toPlainText();
}

void AddDialog::editAddress(const QString &name, const QString &address)
{
    nameText->setReadOnly(true);
    nameText->setText(name);
    addressText->setPlainText(address);
}

대화 상자에 원하는 동작을 부여하기 위해 OKCancel 버튼을 대화 상자의 accept() 및 reject() 슬롯에 연결합니다. 대화 상자는 이름과 주소 정보를 담는 컨테이너 역할만 하므로 다른 함수를 구현할 필요가 없습니다.

메인창 클래스 정의

MainWindow 클래스는 QMainWindow 을 확장하여 주소록을 조작하는 데 필요한 메뉴와 동작을 구현합니다.

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

private slots:
    void updateActions(const QItemSelection &selection);
    void openFile();
    void saveFile();

private:
    void createMenus();

    AddressWidget *addressWidget;
    QAction *editAct;
    QAction *removeAct;
};

MainWindow 클래스는 AddressWidget 을 중앙 위젯으로 사용하며 Open, CloseExit 액션이 있는 파일 메뉴와 Add Entry..., Edit Entry...Remove Entry 액션이 있는 Tools 메뉴를 제공합니다.

메인윈도우 클래스 구현

MainWindow 의 생성자는 AddressWidget을 인스턴스화하여 중앙 위젯으로 설정하고 createMenus() 함수를 호출합니다.

MainWindow::MainWindow()
    : QMainWindow(),
      addressWidget(new AddressWidget)
{
    setCentralWidget(addressWidget);
    createMenus();
    setWindowTitle(tr("Address Book"));
}

createMenus() 함수는 FileTools 메뉴를 설정하여 액션을 각각의 슬롯에 연결합니다. Edit Entry...Remove Entry 작업은 모두 빈 주소록에서는 수행할 수 없으므로 기본적으로 비활성화되어 있습니다. 하나 이상의 연락처가 추가된 경우에만 활성화됩니다.

void MainWindow::createMenus()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));

    QAction *openAct = new QAction(tr("&Open..."), this);
    fileMenu->addAction(openAct);
    connect(openAct, &QAction::triggered, this, &MainWindow::openFile);
    ...

    editAct = new QAction(tr("&Edit Entry..."), this);
    editAct->setEnabled(false);
    toolMenu->addAction(editAct);
    connect(editAct, &QAction::triggered, addressWidget, &AddressWidget::editEntry);

    toolMenu->addSeparator();

    removeAct = new QAction(tr("&Remove Entry"), this);
    removeAct->setEnabled(false);
    toolMenu->addAction(removeAct);
    connect(removeAct, &QAction::triggered, addressWidget, &AddressWidget::removeEntry);

    connect(addressWidget, &AddressWidget::selectionChanged,
        this, &MainWindow::updateActions);
}

모든 작업의 신호를 각각의 슬롯에 연결하는 것 외에도 AddressWidgetselectionChanged() 신호도 updateActions() 슬롯에 연결합니다.

openFile() 함수는 주소록 연락처가 포함된 사용자 지정 addressbook.dat 파일을 엽니다. 이 함수는 File 메뉴에서 openAct 에 연결된 슬롯입니다.

void MainWindow::openFile()
{
    addressWidget->readFromFile();
}

saveFile() 함수는 주소록 연락처가 포함된 사용자 지정 addressbook.dat 파일을 저장합니다. 이 기능은 File 메뉴에서 saveAct 에 연결된 슬롯입니다.

void MainWindow::saveFile()
{
    addressWidget->writeToFile();
}

updateActions() 함수는 주소록의 내용에 따라 Edit Entry...Remove Entry 을 활성화 및 비활성화합니다. 주소록이 비어 있으면 이러한 작업이 비활성화되고, 그렇지 않으면 활성화됩니다. 이 함수는 AddressWidgetselectionChanged() 신호에 연결되는 슬롯입니다.

void MainWindow::updateActions(const QItemSelection &selection)
{
    QModelIndexList indexes = selection.indexes();

    if (!indexes.isEmpty()) {
        removeAct->setEnabled(true);
        editAct->setEnabled(true);
    } else {
        removeAct->setEnabled(false);
        editAct->setEnabled(false);
    }
}

main() 함수

주소록의 메인 함수는 QApplication 를 인스턴스화하고 이벤트 루프를 실행하기 전에 MainWindow 를 엽니다.

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow mw;
    mw.show();
    return app.exec();
}

예제 프로젝트 @ code.qt.io

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