美文网首页
Qt官方示例解析-Address Book-基于单个数据模型在不

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

作者: Sky_Mao | 来源:发表于2020-04-17 19:39 被阅读0次

    提要: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);
        }
    }
    

    相关文章

      网友评论

          本文标题:Qt官方示例解析-Address Book-基于单个数据模型在不

          本文链接:https://www.haomeiwen.com/subject/vjtuvhtx.html