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

이 예제는 연락처를 알파벳 순서대로 ABC, DEF, GHI, ..., VW, ..., XYZ의 9개 그룹으로 분류할 수 있는 주소록을 제공합니다. 이는 동일한 모델에 대한 여러 뷰를 사용하여 구현되며, 각 뷰는 ` QSortFilterProxyModel ` 클래스의 인스턴스를 사용하여 필터링됩니다.
개요
주소록에는 MainWindow, AddressWidget, NewAddressTab 및 AddDialog 등 4개의 클래스가 포함되어 있습니다. MainWindow 클래스는 AddressWidget 를 중심 위젯으로 사용하며, File 및 Tools 메뉴를 제공합니다.

AddressWidget 클래스는 QTabWidget 의 서브클래스로, 예제에 표시된 10개의 탭(9개의 알파벳 그룹 탭과 NewAddressTab 인스턴스)을 조작하는 데 사용됩니다. NewAddressTab 클래스는 QWidget 의 서브클래스로, 주소록이 비어 있을 때만 사용되며 사용자에게 연락처를 추가하도록 안내합니다. AddressWidget 또한 TableModel 의 인스턴스와 연동하여 주소록에 항목을 추가, 수정 및 삭제합니다.
struct Contact 는 연락처를 저장하며, QDataStream 를 통해 비교 및 직렬화 기능을 제공합니다:
struct Contact
{
private:
Q_GADGET
Q_PROPERTY(QString name MEMBER name)
Q_PROPERTY(QString address MEMBER address)
public:
QString name;
QString address;
friend bool comparesEqual(const Contact &lhs, const Contact &rhs) noexcept
{
return lhs.name == rhs.name && lhs.address == rhs.address;
}
friend Qt::strong_ordering compareThreeWay(const Contact &lhs, const Contact &rhs) noexcept
{
int cmp = lhs.name.compare(rhs.name);
if (cmp == 0)
cmp = lhs.address.compare(rhs.address);
return Qt::compareThreeWay(cmp, 0);
}
Q_DECLARE_STRONGLY_ORDERED(Contact)
};
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;
}TableModel 는 데이터에 접근하기 위한 표준 모델/뷰 API를 제공하는 QAbstractTableModel 의 하위 클래스입니다. 이 클래스는 추가된 연락처 목록을 보관합니다. 그러나 이 데이터가 단일 탭에 모두 표시되지는 않습니다. 대신, QTableView 는 알파벳 그룹에 따라 동일한 데이터에 대한 9가지 다른 보기를 제공하는 데 사용됩니다.
QSortFilterProxyModel 는 각 연락처 그룹에 대해 연락처를 필터링하는 역할을 담당하는 클래스입니다. 각 프록시 모델은 QRegularExpression 를 사용하여 해당 알파벳 그룹에 속하지 않는 연락처를 걸러냅니다. AddDialog 클래스는 주소록에 대한 정보를 사용자로부터 입력받기 위해 사용됩니다. 이 QDialog 의 서브클래스는 NewAddressTab 에 의해 인스턴스화되어 연락처를 추가하고, AddressWidget 에 의해 인스턴스화되어 연락처를 추가 및 편집합니다.
AddressWidget 클래스 정의
AddressWidget 클래스는 연락처를 추가, 편집 및 삭제하고, 연락처를 파일에 저장하거나 파일에서 불러오는 기능을 제공하므로, 기술적으로 이 예제에서 핵심이 되는 클래스입니다.
class AddressWidget : public QTabWidget
{
Q_OBJECT
public:
explicit AddressWidget(QWidget *parent = nullptr);
bool readFromFile();
bool writeToFile();
static QString fileName();
public slots:
void showAddEntryDialog();
void addEntry(const Contact &contact);
void editEntry();
void removeEntry();
signals:
void selectionChanged (const QItemSelection &selected);
private:
void setupTabs();
NewAddressTab *newAddressTab;
QList<Contact> contacts;
using Adapter = decltype(QRangeModelAdapter(std::ref(contacts)));
Adapter adapter;
};AddressWidget QTabWidget 을 상속하여 10개의 탭( 및 9개의 알파벳 그룹 탭)을 포함하고, ( 객체), (항목을 필터링하는 데 사용하는 객체), ( 객체)를 조작합니다.NewAddressTab table TableModel proxyModel QSortFilterProxyModel tableView QTableView
이 객체는 연락처를 QList 에 저장하고, QRangeModelAdapter 를 사용하여 이를 처리합니다.
우리는 QRangeModel::RowOptions 의 특수화를 통해 struct Contact 에 대한 행 헤더 값을 제공합니다:
template <>
struct QRangeModel::RowOptions<Contact>
{
inline static QVariant headerData(int section, int role);
};
QVariant QRangeModel::RowOptions<Contact>::headerData(int section, int role)
{
if (role == Qt::DisplayRole) {
switch (section) {
case 0:
return AddressWidget::tr("Name");
case 1:
return AddressWidget::tr("Address");
default:
break;
}
}
return {};
}AddressWidget 클래스 구현
AddressWidget 생성자는 부모 위젯을 받아 NewAddressTab, TableModel 및 QSortFilterProxyModel 을 인스턴스화합니다. 주소록이 비어 있음을 나타내는 데 사용되는 NewAddressTab 객체가 추가되고, 나머지 9개의 탭은 setupTabs() 을 통해 설정됩니다.
AddressWidget::AddressWidget(QWidget *parent)
: QTabWidget(parent),
newAddressTab(new NewAddressTab(this)),
adapter(std::ref(contacts))
{
connect(newAddressTab, &NewAddressTab::triggered, this, &AddressWidget::showAddEntryDialog);
addTab(newAddressTab, tr("Address Book"));
setupTabs();
}setupTabs() 함수는 AddressWidget 내의 9개 알파벳 그룹 탭, 테이블 뷰 및 프록시 모델을 설정하는 데 사용됩니다. 각 프록시 모델은 대소문자를 구분하지 않는 QRegularExpression 객체를 사용하여 관련 알파벳 그룹에 따라 연락처 이름을 필터링하도록 설정됩니다. 또한 테이블 뷰는 해당 프록시 모델의 sort() 함수를 사용하여 오름차순으로 정렬됩니다.
각 테이블 뷰의 selectionMode 는 QAbstractItemView::SingleSelection 로 설정되고, selectionBehavior 는 QAbstractItemView::SelectRows 로 설정되어, 사용자가 한 행의 모든 항목을 한 번에 선택할 수 있습니다. 각 QTableView 객체에는 선택된 인덱스를 추적하는 QItemSelectionModel 가 자동으로 할당됩니다.
void AddressWidget::setupTabs()
{
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(adapter.model());
proxyModel->setFilterRegularExpression(regExp);
proxyModel->setFilterKeyColumn(0);
auto *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 클래스는 AddressWidget 의 selectionChanged() 신호에 연결된 selectionChanged 신호를 제공합니다. 또한 QTabWidget::currentChanged() 신호를 AddressWidget 의 selectionChanged() 을 방출하는 람다 표현식에 연결합니다. 이러한 연결은 MainWindow 의 도구 메뉴에서 Edit Entry... 및 Remove Entry 동작을 활성화하는 데 필요합니다. 이에 대한 자세한 내용은 MainWindow 의 구현 부분에서 설명됩니다.
주소록의 각 테이블 뷰는 그룹의 QStringList 에서 가져온 관련 레이블과 함께 QTabWidget 에 탭으로 추가됩니다.

우리는 두 가지 addEntry() 함수를 제공합니다. 하나는 사용자 입력을 수신하기 위한 것이고, 다른 하나는 주소록에 새 항목을 추가하는 실제 작업을 수행하는 것입니다. newAddressTab 가 대화 상자를 띄우지 않고도 데이터를 삽입할 수 있도록 항목 추가 기능을 두 부분으로 나누었습니다.
첫 번째 ` addEntry() ` 함수는 MainWindow 의 ` Add Entry... ` 액션에 연결된 슬롯입니다. 이 함수는 ` AddDialog ` 객체를 생성한 다음, 두 번째 ` addEntry() ` 함수를 호출하여 연락처 정보를 table 에 실제로 추가합니다.
void AddressWidget::showAddEntryDialog()
{
AddDialog aDialog(this);
if (aDialog.exec() == QDialog::Accepted)
addEntry(aDialog.contact());
}주소록에 중복 항목이 등록되는 것을 방지하기 위해 두 번째 addEntry() 함수에서 기본적인 유효성 검사가 수행됩니다. TableModel 에서 언급한 바와 같이, 이것이 바로 getContacts() 게터 메서드가 필요한 이유 중 하나입니다.
void AddressWidget::addEntry(const Contact &contact)
{
if (contacts.contains(contact)) {
QMessageBox::information(this, tr("Duplicate Name"),
tr("The name \"%1\" already exists.").arg(contact.name));
return;
}
adapter.insertRow(adapter.rowCount(), contact);
removeTab(indexOf(newAddressTab));
const QChar firstChar = contact.name.at(0).toUpper();
for (int t = 0, tabCount = count(); t < tabCount; ++t) {
if (tabText(t).contains(firstChar)) {
setCurrentIndex(t);
break;
}
}
}모델에 동일한 이름의 항목이 아직 없는 경우, setData() 를 호출하여 이름과 주소를 첫 번째 및 두 번째 열에 삽입합니다. 그렇지 않은 경우, 사용자에게 알리기 위해 QMessageBox 를 표시합니다.
참고: 연락처가 추가되면 주소록이 더 이상 비어 있지 않으므로 newAddressTab 는 제거됩니다.
항목 편집은 연락처의 주소만 업데이트하는 방식입니다. 이 예제에서는 사용자가 기존 연락처의 이름을 변경할 수 없도록 되어 있습니다.
먼저, QTabWidget::currentWidget()를 사용하여 활성 탭의 QTableView 객체를 가져옵니다. 그런 다음 tableView 에서 selectionModel 를 추출하여 선택된 인덱스를 얻습니다.
void AddressWidget::editEntry()
{
auto *tableView = static_cast<QTableView *>(currentWidget());
auto *proxy = static_cast<QSortFilterProxyModel *>(tableView->model());
QItemSelectionModel *selectionModel = tableView->selectionModel();
const QModelIndexList indexes = selectionModel->selectedRows();
if (indexes.isEmpty())
return;
const int row = proxy->mapToSource(indexes.constFirst()).row();
const Contact contact = contacts.at(row);다음으로 사용자가 편집하려는 행의 데이터를 추출합니다. 이 데이터는 다른 창 제목을 가진 AddDialog 인스턴스에 표시됩니다. table 는 aDialog 의 데이터가 변경된 경우에만 업데이트됩니다.
AddDialog aDialog(this);
aDialog.setWindowTitle(tr("Edit a Contact"));
aDialog.editAddress(contact);
if (aDialog.exec() == QDialog::Accepted) {
const Contact newContact = aDialog.contact();
if (newContact != contact)
adapter[row] = newContact;
}
}
항목은 removeEntry() 함수를 사용하여 제거됩니다. 선택된 행은 QItemSelectionModel 객체인 selectionModel 을 통해 접근하여 제거됩니다. newAddressTab 은 사용자가 주소록의 모든 연락처를 제거한 경우에만 AddressWidget 에 다시 추가됩니다.
void AddressWidget::removeEntry()
{
auto *tableView = static_cast<QTableView *>(currentWidget());
auto *proxy = static_cast<QSortFilterProxyModel *>(tableView->model());
QItemSelectionModel *selectionModel = tableView->selectionModel();
const QModelIndexList indexes = selectionModel->selectedRows();
if (indexes.isEmpty())
return;
const int row = proxy->mapToSource(indexes.constFirst()).row();
adapter.removeRow(row);
if (adapter.rowCount() == 0)
insertTab(0, newAddressTab, tr("Address Book"));
}writeToFile() 함수는 주소록에 있는 모든 연락처를 포함하는 파일을 저장하는 데 사용됩니다. 이 파일은 사용자 정의 .dat 형식으로 저장됩니다. 연락처 목록의 내용은 QDataStream 를 사용하여 file 에 기록됩니다. 파일을 열 수 없는 경우, 관련 오류 메시지가 포함된 QMessageBox 가 표시됩니다.
bool AddressWidget::writeToFile()
{
QFile file(fileName());
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
tr("Cannot write to %1: %2").arg(QDir::toNativeSeparators(fileName()),
file.errorString()));
return false;
}
auto sortedContacts = contacts;
std::sort(sortedContacts.begin(), sortedContacts.end());
QDataStream out(&file);
out << sortedContacts;
return true;
}readFromFile() 함수는 이전에 writeToFile() 를 사용하여 저장된 주소록의 모든 연락처가 포함된 파일을 불러옵니다. QDataStream 는 .dat 파일의 내용을 연락처 목록으로 읽어들이는 데 사용되며, 각 연락처는 addEntry() 를 사용하여 추가됩니다.
bool AddressWidget::readFromFile()
{
QFile file(fileName());
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
tr("Cannot open %1: %2").arg(QDir::toNativeSeparators(fileName()),
file.errorString()));
return false;
}
QList<Contact> contactsIn;
QDataStream in(&file);
in >> contactsIn;
if (contactsIn.isEmpty()) {
QMessageBox::information(this, tr("No contacts in file"),
tr("The file you are attempting to open contains no contacts."));
}
for (const auto &contact : std::as_const(contactsIn))
addEntry(contact);
return true;
}NewAddressTab 클래스 정의
NewAddressTab 클래스는 주소록이 비어 있음을 사용자에게 알려주는 정보 탭을 제공합니다. 이 탭은 AddressWidget 의 구현에서 언급된 바와 같이 주소록의 내용에 따라 나타나거나 사라집니다.

NewAddressTab 클래스는 QWidget 를 상속하며, QLabel 및 QPushButton 메서드를 포함합니다.
class NewAddressTab : public QWidget
{
Q_OBJECT
public:
explicit NewAddressTab(QWidget *parent = nullptr);
signals:
void triggered();
};NewAddressTab 클래스 구현
생성자는 ` addButton`, ` descriptionLabel `를 인스턴스화하고, ` addButton`의 신호를 ` triggered() ` 신호에 연결합니다.
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::triggered);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(descriptionLabel, 0, Qt::AlignCenter);
mainLayout->addWidget(addButton, 0, Qt::AlignCenter);
}
AddDialog 클래스 정의
AddDialog 클래스는 QDialog 을 상속하며, 사용자가 주소록에 데이터를 입력할 수 있도록 QLineEdit 와 QTextEdit 를 제공합니다.
class AddDialog : public QDialog
{
Q_OBJECT
public:
explicit AddDialog(QWidget *parent = nullptr);
Contact contact() const;
void setContact(const Contact &c);
void editAddress(const Contact &c);
private slots:
void updateEnabled();
private:
QDialogButtonBox *buttonBox;
QLineEdit *nameText;
QPlainTextEdit *addressText;
};
AddDialog 클래스 구현
AddDialog 의 생성자는 사용자 인터페이스를 설정하여 필요한 위젯을 생성하고 레이아웃에 배치합니다.
AddDialog::AddDialog(QWidget *parent)
: QDialog(parent),
buttonBox(new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, this)),
nameText(new QLineEdit),
addressText(new QPlainTextEdit)
{
auto *formLayout = new QFormLayout;
formLayout->addRow(tr("Name"), nameText);
formLayout->addRow(tr("Address"), addressText);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(formLayout);
mainLayout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(nameText, &QLineEdit::textChanged, this, &AddDialog::updateEnabled);
connect(addressText, &QPlainTextEdit::textChanged, this, &AddDialog::updateEnabled);
setWindowTitle(tr("Add a Contact"));
updateEnabled();
}
void AddDialog::updateEnabled()
{
Contact c = contact();
const bool valid = !c.name.isEmpty() && c.name.front().isLetter() && !c.address.isEmpty();
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
}
Contact AddDialog::contact() const
{
return { nameText->text().trimmed(), addressText->toPlainText().trimmed() };
}
void AddDialog::setContact(const Contact &c)
{
nameText->setText(c.name);
addressText->setPlainText(c.address);
updateEnabled();
}
void AddDialog::editAddress(const Contact &c)
{
nameText->setReadOnly(true);
setContact(c);
}대화 상자에 원하는 동작을 부여하기 위해, OK 및 Cancel 버튼을 대화 상자의 accept() 및 reject() 슬롯에 연결합니다. 이 대화 상자는 이름과 주소 정보를 담는 용도로만 사용되므로, 다른 기능을 구현할 필요는 없습니다.
MainWindow 클래스 정의
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 를 중심 위젯으로 사용하며, File 메뉴에 Open, Close 및 Exit 액션을 제공하고, Tools 메뉴에는 Add Entry..., Edit Entry... 및 Remove Entry 액션을 제공합니다.
MainWindow 클래스 구현
MainWindow 의 생성자는 AddressWidget을 인스턴스화하고, 이를 중앙 위젯으로 설정한 다음 createMenus() 함수를 호출합니다.
MainWindow::MainWindow()
: QMainWindow(),
addressWidget(new AddressWidget)
{
setCentralWidget(addressWidget);
createMenus();
setWindowTitle(tr("Address Book"));
}createMenus() 함수는 File 및 Tools 메뉴를 설정하고, 각 액션과 해당 슬롯을 연결합니다. Edit Entry... 및 Remove Entry 액션은 빈 주소록에서는 수행할 수 없으므로 기본적으로 비활성화되어 있습니다. 이 액션들은 하나 이상의 연락처가 추가된 경우에만 활성화됩니다.
void MainWindow::createMenus()
{
QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
auto *openAct = new QAction(QIcon::fromTheme(QIcon::ThemeIcon::DocumentOpen),
tr("&Open..."), this);
openAct->setShortcut(QKeySequence(QKeySequence::Open));
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);
}모든 액션의 신호를 각 슬롯에 연결하는 것 외에도, ` AddressWidget`의 ` selectionChanged() ` 신호를 ` updateActions() ` 슬롯에 연결합니다.
openFile() 함수는 주소록 연락처를 포함하는 사용자 정의 addressbook.dat 파일을 엽니다. 이 함수는 File 메뉴의 openAct 에 연결된 슬롯입니다.
void MainWindow::openFile()
{
if (addressWidget->readFromFile())
statusBar()->showMessage(tr("Read %1").arg(QDir::toNativeSeparators(AddressWidget::fileName())));
}saveFile() 기능은 주소록 연락처를 포함할 사용자 정의 addressbook.dat 파일을 저장합니다. 이 기능은 File 메뉴의 saveAct 에 연결된 슬롯입니다.
void MainWindow::saveFile()
{
if (addressWidget->writeToFile())
statusBar()->showMessage(tr("Wrote %1").arg(QDir::toNativeSeparators(AddressWidget::fileName())));
}updateActions() 함수는 주소록의 내용에 따라 Edit Entry... 및 Remove Entry 을 활성화하거나 비활성화합니다. 주소록이 비어 있으면 이러한 동작이 비활성화되고, 그렇지 않으면 활성화됩니다. 이 함수는 AddressWidget 의 selectionChanged() 신호에 연결된 슬롯입니다.
void MainWindow::updateActions(const QItemSelection &selection)
{
const QModelIndexList indexes = selection.indexes();
removeAct->setEnabled(!indexes.isEmpty());
editAct->setEnabled(!indexes.isEmpty());
}main() 함수
주소록의 main 함수는 이벤트 루프를 실행하기 전에 QApplication 를 인스턴스화하고 MainWindow 를 엽니다.
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow mw;
const auto availableGeometry = mw.screen()->availableGeometry();
mw.resize(availableGeometry.width() / 3, availableGeometry.height() / 3);
mw.show();
return QApplication::exec();
}© 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.

