前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Qt官方示例解析-Address Book-基于单个数据模型在不同视图呈现不同数据

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

作者头像
Sky_Mao
发布2020-07-24 10:12:43
5.1K0
发布2020-07-24 10:12:43
举报

提要: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继承了一下。

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

1、TableModel的定义

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

代码语言:javascript
复制
//记录地址簿数据
struct Contact
{
    QString strName;         
    QString strAddress;

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

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

代码语言:javascript
复制
//输出
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;
}

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

代码语言:javascript
复制
enum class AddressBookColumn
{
    name = 0,
    address
};

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

代码语言:javascript
复制
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的实现

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

代码语言:javascript
复制
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,因为我们只需要名称和地址列的空间。 官方示例的实现代码:

官方代码

我的写法:

代码语言:javascript
复制
int TableModel::rowCount(const QModelIndex& parent) const
{
    //行数会根据数据量而变化
    return m_oContacts.size();
}

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

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

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

代码语言:javascript
复制
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”

代码语言:javascript
复制
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()函数以确保所有连接的视图都知道这些更改。该函数是提供给添加联系人的功能使用的,在插入数据之前,先在表格内添加一行,然后容器添加一条空记录。

代码语言:javascript
复制
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在插入删除上函数较为类似,不要写反了。

代码语言:javascript
复制
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()函数就是给当前新插入的一行空行写入数据。

代码语言:javascript
复制
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对象的编辑特性,但是在这里启用了它们,这样就可以在其他程序中重用这个模型。

代码语言:javascript
复制
Qt::ItemFlags TableModel::flags(const QModelIndex& index) const
{
    if (!index.isValid())
    {
        return Qt::ItemIsEnabled;
    }

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

3、AddressWidget的定义

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

代码语言:javascript
复制
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在这之前没有定义

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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()该函数是用来设置窗体标题的,我们这里给了一个常量,标题可以设置成参数传递进来,这样可以做成一个可定制窗体

代码语言:javascript
复制
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"));
}

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

代码语言:javascript
复制
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项无法进行编辑,要把它设置为只读。

代码语言:javascript
复制
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来跟踪所选的索引。

代码语言:javascript
复制
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()函数来实际将联系人添加到表中。

代码语言:javascript
复制
void AddressWidget::showAddEntryDialog()
{
    AddDialog oDialog;
    if (oDialog.exec() == QDialog::Accepted)
    {
        addEntry(oDialog.name(), oDialog.address());
    }
}

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

代码语言:javascript
复制
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来获取被选中的索引。

代码语言:javascript
复制
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

代码语言:javascript
复制
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重载输入、输入操作符。

代码语言:javascript
复制
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

代码语言:javascript
复制
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()函数。

代码语言:javascript
复制
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操作在默认情况下是禁用的,因为这样的操作不能在一个空的地址簿上执行。只有在添加一个或多个联系人时才启用它们。

代码语言:javascript
复制
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()信号。

代码语言:javascript
复制
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实现了 打开的功能就是用来打开保存功能存储的文件,保存就是把地址簿中的联系人数据存储为文件,数据是二进制流数据。

代码语言:javascript
复制
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);
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、TableModel的定义
  • 2、TableModel的实现
  • 3、AddressWidget的定义
  • 4、AddressWidget的实现
    • 4.1、NewAddressTab定义
      • 4.2、NewAddressTab实现
        • 4.3、AddDialog定义
          • 4.4、AddDialog实现
          • 5、addressBook定义
          • 6、addressBook实现
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档