Sur cette page

Carnet d'adresses

L'exemple du carnet d'adresses montre comment utiliser des modèles proxy pour afficher différentes vues sur les données d'un même modèle.

Carnet d'adresses contenant les contacts et les adresses

Cet exemple propose un carnet d'adresses permettant de regrouper les contacts par ordre alphabétique en 9 groupes : ABC, DEF, GHI, …, VW, …, XYZ. Pour ce faire, on utilise plusieurs vues sur le même modèle, chacune étant filtrée à l'aide d'une instance de la classe ` QSortFilterProxyModel `.

Présentation

Le carnet d'adresses contient 4 classes : ` MainWindow`, ` AddressWidget`, ` NewAddressTab ` et ` AddDialog`. La classe ` MainWindow ` utilise ` AddressWidget ` comme widget central et fournit les menus ` File ` et ` Tools `.

Principales classes et éléments du carnet d'adresses

La classe AddressWidget est une sous-classe de QTabWidget qui sert à manipuler les 10 onglets affichés dans l'exemple : les 9 onglets de groupes alphabétiques et une instance de NewAddressTab. La classe NewAddressTab est une sous-classe de QWidget qui n'est utilisée que lorsque le carnet d'adresses est vide, invitant alors l'utilisateur à ajouter des contacts. AddressWidget interagit également avec une instance de TableModel pour ajouter, modifier et supprimer des entrées dans le carnet d’adresses.

struct Contact stocke un contact et assure la comparaison et la sérialisation via 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 est une sous-classe de QAbstractTableModel qui fournit l’API modèle/vue standard pour accéder aux données. Elle contient une liste des contacts ajoutés. Cependant, ces données ne sont pas toutes visibles dans un seul onglet. À la place, QTableView est utilisé pour fournir 9 vues différentes des mêmes données, en fonction des groupes alphabétiques.

QSortFilterProxyModel est la classe chargée de filtrer les contacts pour chaque groupe de contacts. Chaque modèle proxy utilise un ` QRegularExpression ` pour filtrer les contacts qui n’appartiennent pas au groupe alphabétique correspondant. La classe ` AddDialog ` est utilisée pour obtenir des informations de l’utilisateur concernant le carnet d’adresses. Cette sous-classe de ` QDialog ` est instanciée par ` NewAddressTab ` pour ajouter des contacts, et par ` AddressWidget ` pour ajouter et modifier des contacts.

Définition de la classe AddressWidget

La classe AddressWidget est techniquement la classe principale de cet exemple, car elle fournit des fonctions permettant d’ajouter, de modifier et de supprimer des contacts, ainsi que d’enregistrer les contacts dans un fichier et de les charger à partir d’un fichier.

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 extends QTabWidget afin de contenir 10 onglets (NewAddressTab et les 9 onglets de groupes alphabétiques) et manipule également table, l’objet TableModel, proxyModel, l’objet QSortFilterProxyModel que nous utilisons pour filtrer les entrées, et tableView, l’objet QTableView.

Il stocke les contacts dans un « QList » et utilise un « QRangeModelAdapter » pour opérer sur celui-ci.

Nous fournissons les valeurs d’en-tête de ligne pour struct Contact via une spécialisation de QRangeModel::RowOptions:

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 {};
}

Implémentation de la classe `AddressWidget`

Le constructeur d’ AddressWidget accepte un widget parent et instancie NewAddressTab, TableModel et QSortFilterProxyModel. L’objet NewAddressTab, qui sert à indiquer que le carnet d’adresses est vide, est ajouté, et les 9 autres onglets sont configurés à l’aide de 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();
}

La fonction ` setupTabs() ` est utilisée pour configurer les 9 onglets de groupes alphabétiques, les vues de tableau et les modèles de proxy dans ` AddressWidget`. Chaque modèle de proxy est à son tour configuré pour filtrer les noms de contacts en fonction du groupe alphabétique correspondant à l’aide d’un objet ` QRegularExpression ` insensible à la casse. Les vues de tableau sont également triées par ordre croissant à l’aide de la fonction ` sort()` du modèle de proxy correspondant.

La propriété ` selectionMode ` de chaque vue de table est définie sur ` QAbstractItemView::SingleSelection ` et la propriété ` selectionBehavior ` sur ` QAbstractItemView::SelectRows`, ce qui permet à l’utilisateur de sélectionner simultanément tous les éléments d’une même ligne. Chaque objet ` QTableView ` se voit automatiquement attribuer un ` QItemSelectionModel ` qui assure le suivi des index sélectionnés.

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);
    }
}

La classe ` QItemSelectionModel ` fournit un signal ` selectionChanged ` qui est connecté au signal ` selectionChanged() ` de ` AddressWidget`. Nous connectons également le signal ` QTabWidget::currentChanged()` à l’expression lambda qui émet également le signal ` selectionChanged() ` de ` AddressWidget`. Ces connexions sont nécessaires pour activer les actions « Edit Entry... » et « Remove Entry » dans le menu « Tools » de ` MainWindow`. Cela est expliqué plus en détail dans l’implémentation de ` MainWindow`.

Chaque vue de table du carnet d’adresses est ajoutée sous forme d’onglet à la page QTabWidget avec le libellé correspondant, obtenu à partir de la méthode QStringList des groupes.

Connexions des signaux et des emplacements du carnet d'adresses

Nous fournissons deux fonctions « addEntry() » : l’une destinée à accepter les saisies de l’utilisateur, et l’autre qui effectue la tâche proprement dite d’ajout de nouvelles entrées au carnet d’adresses. Nous divisons la responsabilité de l’ajout d’entrées en deux parties afin de permettre à « newAddressTab » d’insérer des données sans avoir à afficher de boîte de dialogue.

La première fonction d’ addEntry() est un slot lié à l’action « Add Entry... » de l’ MainWindow. Cette fonction crée un objet ` AddDialog `, puis appelle la deuxième fonction d’ addEntry() pour ajouter effectivement le contact à ` table`.

void AddressWidget::showAddEntryDialog()
{
    AddDialog aDialog(this);

    if (aDialog.exec() == QDialog::Accepted)
        addEntry(aDialog.contact());
}

Une validation de base est effectuée dans la deuxième fonction addEntry() afin d’éviter les doublons dans le carnet d’adresses. Comme mentionné pour TableModel, c’est en partie pour cette raison que nous avons besoin de la méthode getter 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;
        }
    }
}

Si le modèle ne contient pas encore d’entrée portant le même nom, nous appelons setData() pour insérer le nom et l’adresse dans les première et deuxième colonnes. Dans le cas contraire, nous affichons un message d’erreur ( QMessageBox ) pour en informer l’utilisateur.

Remarque : la page newAddressTab est supprimée dès qu’un contact est ajouté, car le carnet d’adresses n’est alors plus vide.

La modification d'une entrée permet uniquement de mettre à jour l'adresse du contact, car l'exemple ne permet pas à l'utilisateur de modifier le nom d'un contact existant.

Tout d’abord, nous récupérons l’objet ` QTableView ` de l’onglet actif à l’aide de ` QTabWidget::currentWidget()`. Ensuite, nous extrayons l’objet ` selectionModel ` de l’objet ` tableView ` pour obtenir les index sélectionnés.

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);

Ensuite, nous extrayons les données de la ligne que l’utilisateur souhaite modifier. Ces données s’affichent dans une instance de ` AddDialog ` dont le titre de fenêtre est différent. Le ` table ` n’est mis à jour que si des modifications ont été apportées aux données contenues dans ` 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;
    }
}

Boîte de dialogue permettant de modifier le nom et l'adresse

Les entrées sont supprimées à l’aide de la fonction removeEntry(). La ligne sélectionnée est supprimée en y accédant via l’objet QItemSelectionModel, selectionModel. L’ newAddressTab n’est rajouté au AddressWidget que si l’utilisateur supprime tous les contacts du carnet d’adresses.

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"));
}

La fonction writeToFile() permet d'enregistrer un fichier contenant tous les contacts du carnet d'adresses. Le fichier est enregistré dans un format personnalisé .dat. Le contenu de la liste des contacts est écrit dans file à l'aide de QDataStream. Si le fichier ne peut pas être ouvert, une QMessageBox s'affiche avec le message d'erreur correspondant.

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;
}

La fonction readFromFile() charge un fichier contenant tous les contacts du carnet d’adresses, précédemment enregistré à l’aide de writeToFile(). QDataStream est utilisé pour lire le contenu d’un fichier .dat dans une liste de contacts, et chacun d’entre eux est ajouté à l’aide de 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;
}

Définition de la classe NewAddressTab

La classe NewAddressTab fournit un onglet informatif indiquant à l’utilisateur que le carnet d’adresses est vide. Il apparaît et disparaît en fonction du contenu du carnet d’adresses, comme mentionné dans l’implémentation de AddressWidget.

Onglet permettant d'ajouter de nouveaux contacts

La classe NewAddressTab hérite de QWidget et contient une méthode QLabel ainsi qu’une méthode QPushButton.

class NewAddressTab : public QWidget
{
    Q_OBJECT

public:
    explicit NewAddressTab(QWidget *parent = nullptr);

signals:
    void triggered();
};

Implémentation de la classe `NewAddressTab`

Le constructeur instancie les classes ` addButton` et ` descriptionLabel `, puis relie le signal ` addButton` au signal ` 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);
}

Appels de fonction permettant d'ajouter un nouveau contact

Définition de la classe AddDialog

La classe « AddDialog » étend la classe « QDialog » et met à la disposition de l’utilisateur une instance « QLineEdit » et une instance « QTextEdit » pour saisir des données dans le carnet d’adresses.

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;
};

Boîte de dialogue permettant d'ajouter un nouveau nom et une nouvelle adresse

Implémentation de la classe AddDialog

Le constructeur de la classe ` AddDialog` configure l'interface utilisateur, en créant les widgets nécessaires et en les plaçant dans des dispositions.

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);
}

Pour conférer à la boîte de dialogue le comportement souhaité, nous connectons les boutons « OK » et « Cancel » aux slots « accept()» et « reject()» de la boîte de dialogue. Étant donné que la boîte de dialogue sert uniquement de conteneur pour les informations de nom et d’adresse, nous n’avons pas besoin d’implémenter d’autres fonctions pour celle-ci.

Définition de la classe MainWindow

La classe MainWindow hérite de QMainWindow et implémente les menus et les actions nécessaires à la gestion du carnet d’adresses.

Les options « Ouvrir », « Enregistrer » et « Quitter » du menu « Fichier »« Ajouter une entrée », « Modifier une entrée » et « Supprimer une entrée » dans le menu « Outils »
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;
};

La classe MainWindow utilise un AddressWidget comme widget central et fournit au menu Fichier les actions « Open », « Close » et « Exit », ainsi qu’au menu « Tools » les actions « Add Entry... », « Edit Entry... » et « Remove Entry ».

Implémentation de la classe MainWindow

Le constructeur de MainWindow instancie AddressWidget, le définit comme widget central et appelle la fonction createMenus().

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

La fonction ` createMenus() ` configure les menus ` File ` et ` Tools `, en reliant les actions à leurs slots respectifs. Les actions ` Edit Entry... ` et ` Remove Entry ` sont désactivées par défaut, car elles ne peuvent pas être exécutées sur un carnet d’adresses vide. Elles ne sont activées que lorsqu’un ou plusieurs contacts ont été ajoutés.

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);
}

Outre la connexion des signaux de toutes les actions à leurs slots respectifs, nous connectons également le signal « selectionChanged() » de « AddressWidget » à son slot « updateActions() ».

La fonction « openFile() » ouvre un fichier « addressbook.dat » personnalisé contenant les contacts du carnet d’adresses. Cette fonction est un slot relié à « openAct » dans le menu « File ».

void MainWindow::openFile()
{
    if (addressWidget->readFromFile())
        statusBar()->showMessage(tr("Read %1").arg(QDir::toNativeSeparators(AddressWidget::fileName())));
}

La fonction « saveFile() » enregistre un fichier addressbook.dat personnalisé qui contiendra les contacts du carnet d’adresses. Cette fonction est un emplacement lié à l’option « saveAct » du menu « File ».

void MainWindow::saveFile()
{
    if (addressWidget->writeToFile())
        statusBar()->showMessage(tr("Wrote %1").arg(QDir::toNativeSeparators(AddressWidget::fileName())));
}

La fonction « updateActions() » active et désactive les actions « Edit Entry... » et « Remove Entry » en fonction du contenu du carnet d’adresses. Si le carnet d’adresses est vide, ces actions sont désactivées ; dans le cas contraire, elles sont activées. Cette fonction est un slot connecté au signal « selectionChanged() » de la fonction « AddressWidget ».

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

    removeAct->setEnabled(!indexes.isEmpty());
    editAct->setEnabled(!indexes.isEmpty());
}

Fonction main()

La fonction principale du carnet d'adresses instancie QApplication et ouvre un MainWindow avant d'exécuter la boucle d'événements.

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();
}

Exemple de projet sur code.qt.io

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