Qt官方示例解析-Address Book-基于单个数据模型在不同视图呈现不同数据

提要:Qt的这个示例主要讲的是使用代理模型,实现在不同的视图上面显示单个数据模型的数据
这个示例提供了一个地址簿,将联系人按照名称字母{"ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"}分成9个组。这是通过在同一个模型上使用多个视图实现的,每个视图都使用QSortFilterProxyModel类的一个实例进行过滤。地址簿包含5个类:MainWindow、AddressWidget、TableModel、NewAddressTab和AddDialog。MainWindow类使用AddressWidget作为其中心小部件,并提供文件和工具菜单。(与官方示例不同的地方是:MainWindow,使用AddressBook类继承了一下)

源码地址:https://gitee.com/mao_zg/AddressBook

官方结构图:


结构图

自己实现的结构图:
连接线我使用了依赖关系来连接

结构图

AddressWidget类是一个QTabWidget子类,用于操作示例中显示的10个选项卡:9个字母组选项卡和一个NewAddressTab实例。NewAddressTab类是QWidget的一个子类,它只在地址簿为空时使用,提示用户添加一些联系人。AddressWidget还与TableModel的实例进行交互,以添加、编辑和删除地址簿中的条目。

TableModelQAbstractTableModel的子类,它提供了访问数据的标准模型/视图API。它包含一个添加联系人列表。但是,这些数据在单个选项卡中并不都是可见的。相反,根据字母表组,QTableView被用来提供相同数据的9种不同视图。

QSortFilterProxyModel是负责过滤每个联系人组的联系人的类。每个代理模型使用一个QRegExp来过滤不属于相应字母组的联系人。AddDialog类用于从用户获取地址簿的信息。这个QDialog子类由NewAddressTab实例化以添加联系人,并由AddressWidget实例化以添加和编辑联系人。

在官方示例的基础之上,把MainWindow使用AddressBook继承了一下。

实现的话,按照从底层到上层的方式实现,那么先实现TableModel
TableModel类通过子类化QAbstractTableModel来提供标准API来访问联系人列表中的数据。为此必须实现的基本函数有:rowCount()、columnCount()、data()、headerData()。要使TableModel可编辑,它必须提供实现insertRows()、removeRows()、setData()flags()函数。

1、TableModel的定义

Contact是数据模型所使用和管理的数据

//记录地址簿数据
struct Contact
{
    QString strName;         
    QString strAddress;

    //重载等于操作符
    bool operator==(const Contact& oContact) const
    {
        return strName == oContact.strName && strAddress == oContact.strAddress;
    }
};

接下来是一段重载QDataStream的IO操作,这两个重载是为了实现读取、存储文件功能。

//输出
inline QDataStream& operator<<(QDataStream& stream,const Contact& oContact)
{
    return stream << oContact.strName << oContact.strAddress;
}

//输入
inline QDataStream& operator>>(QDataStream& stream, Contact& oContact)
{
    return stream >> oContact.strName >> oContact.strAddress;
}

我这里新增了一个枚举变量的定义,为了标识表格的列,避免代码中出现魔鬼数字,以及支撑后期列的扩展变化。

enum class AddressBookColumn
{
    name = 0,
    address
};

接下来是类的定义:
这里使用了两个构造函数,一个是使用TableModel自己的默认构造函数,另一个是使用QVector<Contact>作为参数的构造函数,这是为了方便起见。TableModel中的最后一个函数getContacts()返回QVector<Contact>对象,该对象保存通讯录中的所有联系人。

class TableModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    TableModel(QObject* parent = nullptr);
    TableModel(const QVector<Contact>& contacts, QObject* parent = nullptr);

    ~TableModel();

    virtual int rowCount(const QModelIndex& parent) const override;
    virtual int columnCount(const QModelIndex& parent) const override;
    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
    virtual QVariant headerData(int section, Qt::Orientation orientation,
        int role = Qt::DisplayRole) const override;
    virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
    virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
    virtual bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
    virtual bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;

    const QVector<Contact>& getContacts() const { return m_oContacts; };

private:
    QVector<Contact> m_oContacts;
};

2、TableModel的实现

实现头文件中定义的两个构造函数。第二个构造函数使用参数值初始化模型中的联系人列表。
由于本示例的列是固定的两列,所以这里增加了一个常量来定义列的个数,后期增加列的话直接修改该常量即可

static const int c_nColumnCnt = 2;
TableModel::TableModel(QObject * parent /*= nullptr*/)
    : QAbstractTableModel(parent)
{

}

TableModel::TableModel(const QVector<Contact> & contacts, QObject * parent /*= nullptr*/)
    :QAbstractTableModel(parent), m_oContacts(contacts)
{

}

官方原话:rowCount()columnCount()函数返回模型的维数。然而,rowCount()的值将根据添加到地址簿的联系人数量而变化,columnCount()的值总是2,因为我们只需要名称和地址列的空间。
官方示例的实现代码:

官方代码

我的写法:

int TableModel::rowCount(const QModelIndex& parent) const
{
    //行数会根据数据量而变化
    return m_oContacts.size();
}

int TableModel::columnCount(const QModelIndex& parent) const
{
    //官方示例这里给了数值2,不符合代码规范,这里定义一个常量,未来扩展列数,比如添加一个邮编列,只需要
    //修改常量的值就好
    return c_nColumnCnt;
}

没有必要写成官方那样复杂,行数就是数据量,而列数又是一个固定值。所以直接返回即可。

data()函数根据提供的模型索引的内容返回名称或地址。模型索引中存储的行号用于引用联系人列表中的项。

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

    if (Qt::DisplayRole == role)
    {
        //预防越界访问
        if (index.row() > rowCount(index) ||
            index.row() < 0)
        {
            return {};
        }

        const auto& oContact = m_oContacts.at(index.row());

        switch ((AddressBookColumn)index.column())
        {
        case AddressBookColumn::name:
            return oContact.strName;
        case AddressBookColumn::address:
            return oContact.strAddress;
        default:
            break;
        }
    }

    return QVariant();
}

headerData()函数的作用是:显示表的标题,“Name”“Address”

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

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

insertRows()函数的作用是:在添加新数据之前调用insertRows()函数,否则数据将不会显示。调用beginInsertRows()endInsertRows()函数以确保所有连接的视图都知道这些更改。该函数是提供给添加联系人的功能使用的,在插入数据之前,先在表格内添加一行,然后容器添加一条空记录。

bool TableModel::insertRows(int row, int count, const QModelIndex& parent)
{
    Q_UNUSED(parent);
    beginInsertRows(parent, row, row + count - 1);

    for (int i = 0; i < count; ++i)
    {
        m_oContacts.insert(row, { QString(), QString() });
    }

    endInsertRows();
    return true;
}

调用removeRows()函数来删除数据。再次调用beginRemoveRows()endRemoveRows(),以确保所有连接的视图都知道这些更改。
写的时候需要注意一下,begin、end在插入删除上函数较为类似,不要写反了。

bool TableModel::removeRows(int row, int count, const QModelIndex& parent)
{
    Q_UNUSED(parent);
    beginRemoveRows(parent, row, row + count - 1);
    for (int i = 0; i < count; ++i)
    {
        m_oContacts.removeAt(row);
    }

    endRemoveRows();
    return true;
}

setData()函数的作用是:向表中逐项而不是逐行插入数据。这意味着要填充地址本中的一行,必须调用两次setData(),因为每一行有两列。
发出dataChanged()信号很重要,因为它告诉所有连接的视图更新它们的显示。
同时需要关注一下返回值,如果返回值写的有问题,数据刷新就会存在问题。
insertRows()是在容器内插入了一行空行,那么setData()函数就是给当前新插入的一行空行写入数据。

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

        switch (AddressBookColumn(index.column()))
        {
        case AddressBookColumn::name:
            oContact.strName = value.toString();
            break;
        case AddressBookColumn::address:
            oContact.strAddress = value.toString();
            break;
        default:
            return false;
        }

        m_oContacts.replace(row, oContact);
        emit dataChanged(index, index, { Qt::DisplayRole, Qt::EditRole });

        return true;
    }
    
    return false;
}

flags()函数的作用是:返回给定索引的项标志
设置Qt::ItemIsEditable标志,因为希望允许编辑TableModel。虽然在本例中没有使用QTableView对象的编辑特性,但是在这里启用了它们,这样就可以在其他程序中重用这个模型。

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

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

3、AddressWidget的定义

AddressWidget类在技术上是本例中涉及的主要类,因为它提供了添加、编辑和删除联系人、将联系人保存到文件中以及从文件中加载联系人的功能

class AddressWidget : public QTabWidget
{
    Q_OBJECT
public:
    AddressWidget(QWidget* parent = nullptr);
    ~AddressWidget();

    void readFromFile(const QString& strFile);
    void writeToFile(const QString& strFile);

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

    TableModel* m_pTableModel = nullptr;
    NewAddressTab* m_pNewAddressTab = nullptr;
};

4、AddressWidget的实现

AddressWidget构造函数接受一个父小部件并实例化NewAddressTab、TableModel和QSortFilterProxyModel。添加NewAddressTab对象(用于指示地址簿为空),其余9个选项卡使用setupTabs()设置。
注意:NewAddressTab在这之前没有定义

AddressWidget::AddressWidget(QWidget * parent /*= nullptr*/)
    :QTabWidget(parent),m_pTableModel(new TableModel(this)),
    m_pNewAddressTab(new NewAddressTab(this))
{
    connect(m_pNewAddressTab, &NewAddressTab::sendDetails, this, &AddressWidget::addEntry);

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

    setupTabs();
}

这里就先跳转到NewAddressTab的定义与实现,因为AddressWidget依赖它。

4.1、NewAddressTab定义

NewAddressTab类提供一个提供信息的选项卡,告诉用户地址簿是空的。它根据地址簿的内容是否为空来控制显示和消失。
界面效果如图:

NewAddressTab

NewAddressTab类扩展了QWidget并包含QLabelQPushButton

class NewAddressTab : public QWidget
{
    Q_OBJECT
public:
    NewAddressTab(QWidget* parent = nullptr);
    ~NewAddressTab();

public slots:
    void addEntry();

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

从代码上面可以看到有一个sendDetails的信号,这个信号就是添加联系人所发出的信号,主要用来通知视图刷新数据以及存储新增数据。

4.2、NewAddressTab实现

构造函数实例化addButton、descriptionLabel并将addButton的信号连接到addEntry()槽。
addEntry()函数与AddressWidgetaddEntry()类似,因为这两个函数都实例化了一个AddDialog对象。通过发出sendDetails()信号,提取对话框中的数据并将其发送到AddressWidgetaddEntry()槽。

这个AddDialog就是实现添加数据的对话框,在NewAddressTab、AddressWidget中都有调用。

image.png

NewAddressTab::NewAddressTab(QWidget * parent /*= nullptr*/)
    :QWidget(parent)
{
    auto pDescriptionLabel = new QLabel(tr("There are currently no contacts in your address book. "
        "\nClick Add to add new contacts."), this);
    auto pAddBtn = new QPushButton(tr("Add"), this);

    auto pMainLayout = new QVBoxLayout(this);
    pMainLayout->addWidget(pDescriptionLabel);
    pMainLayout->addWidget(pAddBtn, 0, Qt::AlignCenter);

    setLayout(pMainLayout);

    connect(pAddBtn, &QPushButton::clicked, this, &NewAddressTab::addEntry);
}

NewAddressTab::~NewAddressTab()
{
}

void NewAddressTab::addEntry()
{
    AddDialog oDialog;
    if (oDialog.exec() == QDialog::Accepted)
    {
        sendDetails(oDialog.name(), oDialog.address());
    }
}

啊,这里又出现了一个AddDialog,这个在之前也没有定义过,那么我们还需要定义它,不然无法通过编译不是吗?

4.3、AddDialog定义

AddDialog类扩展了QDialog,并为用户提供QLineEditQTextEdit,以便将联系人数据(姓名、地址)输入地址簿。
实现后的界面如下图:

AddDialog

class AddDialog : public QDialog
{
    Q_OBJECT
public:
    AddDialog(QWidget* parent = nullptr);
    ~AddDialog();

    QString name() const;
    QString address() const;

    void editAddress(const QString& strName, const QString& strAddress);

private:
    QLineEdit* m_pNameEdit = nullptr;
    QTextEdit* m_pAddressEdit = nullptr;
};

4.4、AddDialog实现

AddDialog的构造函数设置用户界面,创建必要的小部件并将它们放置到布局中。
大家注意QGridLayout,这个网格布局,对齐方式比较常用,各个控件之间的间隔、对齐调整起来较为费时。
界面布局这里使用了网格、垂直、水平三种布局方式,在做界面设计的时候,这三种布局是非常常用的。而且布局除了可以添加QWidget之外也可以添加其他Layout
setWindowTitle()该函数是用来设置窗体标题的,我们这里给了一个常量,标题可以设置成参数传递进来,这样可以做成一个可定制窗体

AddDialog::AddDialog(QWidget * parent /*= nullptr*/)
    :QDialog(parent), m_pNameEdit(new QLineEdit(this)), m_pAddressEdit(new QTextEdit(this))
{
    auto pNameLab = new QLabel("name", this);
    auto pAddressLab = new QLabel("address", this);
    auto pOkBtn = new QPushButton("OK", this);
    auto pCancelBtn = new QPushButton("Cancel", this);

    auto pLayout = new QGridLayout(this);
    pLayout->setColumnStretch(1, 2);
    pLayout->addWidget(pNameLab, 0, 0);
    pLayout->addWidget(m_pNameEdit, 0, 1);
    pLayout->addWidget(pAddressLab, 1, 0, Qt::AlignLeft | Qt::AlignTop);  //左对齐、顶部对齐
    pLayout->addWidget(m_pAddressEdit, 1, 1, Qt::AlignLeft);

    auto pBtnLayout = new QHBoxLayout(this);
    pBtnLayout->addWidget(pOkBtn);
    pBtnLayout->addWidget(pCancelBtn);

    pLayout->addLayout(pBtnLayout, 2, 1, Qt::AlignRight);  //右对齐
    
    auto pMainLayout = new QVBoxLayout(this);
    pMainLayout->addLayout(pLayout);

    setLayout(pMainLayout);

    connect(pOkBtn, &QAbstractButton::clicked, this, &QDialog::accept);
    connect(pCancelBtn, &QAbstractButton::clicked, this, &QDialog::reject);

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

提供两个接口函数,以获取界面输入,封装自身属性。

QString AddDialog::name() const
{
    if (nullptr == m_pNameEdit)
    {
        return {};
    }

    return m_pNameEdit->text();
}

QString AddDialog::address() const
{
    if (nullptr == m_pAddressEdit)
    {
        return {};
    }

    return m_pAddressEdit->toPlainText();
}

editAddress这个函数是提供给添加使用的,当地址簿中已经存在联系人数据的时候,编辑、修改已有数据,这些数据需要显示在界面中同时Name项无法进行编辑,要把它设置为只读。

void AddDialog::editAddress(const QString& strName, const QString& strAddress)
{
    if (nullptr != m_pNameEdit)
    {
        m_pNameEdit->setReadOnly(true);
        m_pNameEdit->setText(strName);
    }
    
    if (nullptr != m_pAddressEdit)
    {
        m_pAddressEdit->setPlainText(strAddress);
    }
}

OK,绕了这么久,现在可以回到AddressWidget的实现了。
setupTabs()函数用于在AddressWidget中设置9个字母组选项卡、表视图和代理模型。每个代理模型依次设置为使用不区分大小写的QRegExp对象根据相关字母表组过滤联系人名称。表视图也使用相应的代理模型的sort()函数按升序排序。每个表视图的selectionMode被设置为QAbstractItemView::SingleSelection(只能单选), selectionBehavior被设置为QAbstractItemView::SelectRows(按行选择),允许用户同时选择一行中的所有项。每个QTableView对象都会自动给出一个QItemSelectionModel来跟踪所选的索引。

void AddressWidget::setupTabs()
{
    const auto oGroup = { "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ" };

    for (const auto& itemTab : oGroup)
    {
        const auto regExp = QRegularExpression(QString("^[%1].*").arg(itemTab), QRegularExpression::CaseInsensitiveOption);
        auto pProxyModel = new QSortFilterProxyModel(this);
        pProxyModel->setSourceModel(m_pTableModel);
        pProxyModel->setFilterRegularExpression(regExp);
        pProxyModel->setFilterKeyColumn((int)AddressBookColumn::name);

        QTableView* pTab = new QTableView(this);
        pTab->setModel(pProxyModel);
        pTab->setSelectionBehavior(QAbstractItemView::SelectRows); //设置选择模式 按行选择
        pTab->horizontalHeader()->setStretchLastSection(true); //最后一个选项是否占用剩余所有空间
        pTab->verticalHeader()->hide(); //隐藏垂直标头
        pTab->setEditTriggers(QAbstractItemView::NoEditTriggers); //设置编辑框不可编辑
        pTab->setSelectionMode(QAbstractItemView::SingleSelection);
        pTab->setSortingEnabled(true); // Enabled生效就立即执行排序

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

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

        addTab(pTab, itemTab);
    }
}

QItemSelectionModel类提供一个selectionChanged信号,该信号连接到AddressWidgetselectionChanged()信号。
我们还将QTabWidget::currentChanged()信号连接到发出AddressWidgetselectionChanged()的lambda表达式。
这两个信号是给菜单中的Edit Entry、Remove Entry两个Action使用的,这两个Action会根据选择的变化而进行刷新可用状态,当没有选择数据的时候,这两个Action是灰显不可用的状态,反之就是可用状态。

地址簿中的每个表视图都作为附签添加到QTabWidget,并带有相关的标签,这些标签是从组的QStringList中获得的。

image.png

我们提供了两个addEntry()函数:一个用于接受用户输入,另一个用于执行向地址簿添加新条目的实际任务。我们将添加条目的职责分为两部分
,以允许newAddressTab插入数据,而不必弹出一个对话框。
第一个addEntry()函数是一个槽,函数名为:showAddEntryDialog,它连接到主窗口的
"Add Entry" Action。该函数创建一个AddDialog对象,然后调用第二个addEntry()函数来实际将联系人添加到表中。

void AddressWidget::showAddEntryDialog()
{
    AddDialog oDialog;
    if (oDialog.exec() == QDialog::Accepted)
    {
        addEntry(oDialog.name(), oDialog.address());
    }
}

基本验证在第二个addEntry()函数中完成,以防止地址簿中的重复条目。正如在TableModel中提到的,这是我们需要getter方法getContacts()的部分原因。

void AddressWidget::addEntry(const QString& name, const QString& address)
{
    if (!m_pTableModel->getContacts().contains({name, address}))
    {
        m_pTableModel->insertRows(0, 1, QModelIndex());
        QModelIndex index = m_pTableModel->index(0, 0, QModelIndex());
        m_pTableModel->setData(index, name, Qt::EditRole);
        index = m_pTableModel->index(0, 1, QModelIndex());
        m_pTableModel->setData(index, address, Qt::EditRole);

        removeTab(indexOf(m_pNewAddressTab));  //当添加了一条地址后,添加地址的tab就被移除
    }
    else
    {
        QMessageBox::information(this, tr("Duplicate Name"),
            tr("The name \"%1\" already exists.").arg(name));
    }
}

如果模型还没有包含具有相同名称的条目,则调用setData()将名称和地址插入第一列和第二列。否则,我们将显示一个QMessageBox来通知用户。
注意:一旦添加了联系人,newAddressTab将被删除,因为地址簿不再为空。

editEntry只是更新联系人地址的一种方式,因为示例不允许用户更改现有联系人的名称。
首先,我们使用QTabWidget::currentWidget()获取活动选项卡的QTableView对象。然后我们从tableView中提取selectionModel来获取被选中的索引。

void AddressWidget::editEntry()
{
    QTableView* pTempView = static_cast<QTableView*>(currentWidget());
    if (nullptr == pTempView)
    {
        return;
    }

    QSortFilterProxyModel* pSortProxyModel = static_cast<QSortFilterProxyModel*>(pTempView->model());
    if (nullptr == pSortProxyModel)
    {
        return;
    }

    QItemSelectionModel* pSelectModel = pTempView->selectionModel();

    const QModelIndexList oIndexList = pSelectModel->selectedRows();
    QString strName = "";
    QString strAddress = "";
    int nRow = -1;

    for (const auto& oIndex : oIndexList)
    {
        nRow = pSortProxyModel->mapToSource(oIndex).row();
        QModelIndex oNameIndex = m_pTableModel->index(nRow, 0, {});
        QVariant name = m_pTableModel->data(oNameIndex, Qt::DisplayRole);
        strName = name.toString();

        QModelIndex oAddressIndex = m_pTableModel->index(nRow, 1, {});
        QVariant address = m_pTableModel->data(oAddressIndex, Qt::DisplayRole);
        strAddress = address.toString();
    }

    AddDialog oDialog;
    oDialog.setWindowTitle(tr("Edit a Contact"));
    oDialog.editAddress(strName, strAddress);  //上文中说到的AddDialog中的editAddress函数,就是在这里调用的

    if (oDialog.exec() == QDialog::Accepted)
    {
        const QString strNewAddress = oDialog.address();
        if (strNewAddress != strAddress)
        {
            const QModelIndex oIndex = m_pTableModel->index(nRow, 1, {});
            m_pTableModel->setData(oIndex, strNewAddress, Qt::EditRole);
        }
    }
}

实现效果如下图:

image.png

使用removeEntry()函数删除条目。通过QItemSelectionModel对象selectionModel访问被选中的行,从而删除它。只有当用户删除了地址簿中的所有联系人时,才会将newAddressTab重新添加到AddressWidget

void AddressWidget::removeEntry()
{
    QTableView* pTempView = static_cast<QTableView*>(currentWidget());
    if (nullptr == pTempView)
    {
        return;
    }

    QSortFilterProxyModel* pSortProxyModel = static_cast<QSortFilterProxyModel*>(pTempView->model());
    if (nullptr == pSortProxyModel)
    {
        return;
    }

    QItemSelectionModel* pSelectModel = pTempView->selectionModel();

    const QModelIndexList oIndexList = pSelectModel->selectedRows();

    for (const auto& oIndex : oIndexList)
    {
        int nRow = pSortProxyModel->mapToSource(oIndex).row();
        m_pTableModel->removeRows(nRow, 1, {});
    }

    if (m_pTableModel->rowCount({}) == 0)
    {
        insertTab(0, m_pNewAddressTab, tr("Address Book"));
    }
}

writeToFile()函数的作用是:保存一个包含通讯录中所有联系人的文件。文件以自定义的.dat格式保存。联系人列表的内容使用QDataStream写入文件。如果文件无法打开,则会显示一个QMessageBox,并显示相关的错误消息。
readFromFile()函数的作用是:加载一个包含通讯录中所有联系人的文件,该通讯录以前是使用writeToFile()保存的。QDataStream用于将.dat文件的内容读入联系人列表,每个联系人都是使用addEntry()添加的。这里就用到了开始的时候定义的QDataStream重载输入、输入操作符。

void AddressWidget::readFromFile(const QString& strFile)
{
    QFile file(strFile);

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

    QVector<Contact> oContacts;
    QDataStream oStream(&file);
    oStream >> oContacts;

    if (oContacts.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 : qAsConst(oContacts)) //qAsConst == std::as_const() 
        {
            addEntry(contact.strName, contact.strAddress);
        }
    }
}

void AddressWidget::writeToFile(const QString& strFile)
{
    QFile file(strFile);

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

    QDataStream oStream(&file);
    oStream << m_pTableModel->getContacts();
}

5、addressBook定义

主窗体主要实现了,把AddressWidget窗体作为主窗体的中心界面,然后创建两个菜单,File、Tools,分别有Open、Save As、Add Entry、Edit Entry、Remove Entry等Action

class addressBook : public QMainWindow
{
    Q_OBJECT

public:
    addressBook(QWidget *parent = Q_NULLPTR);

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

private:
    void createMenus();
private:
    Ui::addressBookClass ui;
    AddressWidget* m_pAddWidget = nullptr;
    QAction* m_pEditAction = nullptr;
    QAction* m_pRemoveAction = nullptr;
};

6、addressBook实现

addressBook的构造函数实例化AddressWidget,将其设置为其中心小部件,并调用createMenus()函数。

addressBook::addressBook(QWidget *parent)
    : QMainWindow(parent), m_pAddWidget(new AddressWidget(this))
{
    setCentralWidget(m_pAddWidget);
    setWindowTitle(tr("Address Book"));
    createMenus();
    /*ui.setupUi(this);*/
}

createMenus()函数设置File、Open菜单,将操作连接到它们各自的槽。两个编辑条目Edit EntryRemove Entry操作在默认情况下是禁用的,因为这样的操作不能在一个空的地址簿上执行。只有在添加一个或多个联系人时才启用它们。

void addressBook::createMenus()
{
    //添加文件菜单以及Action
    QMenu* pFileMenu = menuBar()->addMenu(tr("File"));
    QAction* pOpenAct = new QAction(tr("&Open..."), this);
    pFileMenu->addAction(pOpenAct);
    connect(pOpenAct, &QAction::triggered, this, &addressBook::openFile);

    QAction* pSaveAct = new QAction(tr("&Save As..."), this);
    pFileMenu->addAction(pSaveAct);
    connect(pSaveAct, &QAction::triggered, this, &addressBook::saveFile);

    pFileMenu->addSeparator(); //此函数添加一个分隔符

    QAction* pExitAct = new QAction(tr("E&xit"), this);
    pFileMenu->addAction(pExitAct);
    connect(pExitAct, &QAction::triggered, this, &QWidget::close);

    //添加工具菜单以及Action
    QMenu* pToolsMenu = menuBar()->addMenu(tr("&Tools"));

    QAction* pAddAct = new QAction(tr("&Add Entry..."), this);
    pToolsMenu->addAction(pAddAct);
    connect(pAddAct, &QAction::triggered, m_pAddWidget, &AddressWidget::showAddEntryDialog);

    m_pEditAction = new QAction(tr("&Edit Entry..."), this);
    pToolsMenu->addAction(m_pEditAction);
    connect(m_pEditAction, &QAction::triggered, m_pAddWidget, &AddressWidget::editEntry);

    pToolsMenu->addSeparator();

    m_pRemoveAction = new QAction(tr("&Remove Entry..."), this);
    pToolsMenu->addAction(m_pRemoveAction);
    connect(m_pRemoveAction, &QAction::triggered, m_pAddWidget, &AddressWidget::removeEntry);

    connect(m_pAddWidget, &AddressWidget::selectionChanged, this, &addressBook::updateActions);
}

除了将所有动作的信号连接到它们各自的插槽之外,我们还将AddressWidgetselectionChanged()信号连接到它的updateActions()插槽。
Add Entry Action的响应信号,绑定到了AddressWidgetshowAddEntryDialog槽上面。

updateActions()函数的作用是:根据地址簿的内容决定禁用启用Edit EntryRemove Entry。如果地址簿为空,则禁用这些操作;否则,它们是启用的。这个函数是一个插槽连接到AddressWidgetselectionChanged()信号。

void addressBook::updateActions(const QItemSelection& oSelection)
{
    QModelIndexList oIndexs = oSelection.indexes();

    if (!oIndexs.isEmpty())
    {
        m_pEditAction->setEnabled(true);
        m_pRemoveAction->setEnabled(true);
    }
    else
    {
        m_pEditAction->setEnabled(false);
        m_pRemoveAction->setEnabled(false);
    }
}

那么最后就是打开和保存文件的Action实现了
打开的功能就是用来打开保存功能存储的文件,保存就是把地址簿中的联系人数据存储为文件,数据是二进制流数据。

void addressBook::openFile()
{
    QString strFile = QFileDialog::getOpenFileName(this);
    if (!strFile.isEmpty())
    {
        m_pAddWidget->readFromFile(strFile);
    }
}

void addressBook::saveFile()
{
    QString strFile = QFileDialog::getSaveFileName(this);

    if (!strFile.isEmpty())
    {
        m_pAddWidget->writeToFile(strFile);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容