Qt官方示例解析-Address Book-基于單個數(shù)據(jù)模型在不同視圖呈現(xiàn)不同數(shù)據(jù)

提要:Qt的這個示例主要講的是使用代理模型,實現(xiàn)在不同的視圖上面顯示單個數(shù)據(jù)模型的數(shù)據(jù)
這個示例提供了一個地址簿,將聯(lián)系人按照名稱字母{"ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ"}分成9個組。這是通過在同一個模型上使用多個視圖實現(xiàn)的,每個視圖都使用QSortFilterProxyModel類的一個實例進行過濾。地址簿包含5個類:MainWindow、AddressWidget、TableModel、NewAddressTab和AddDialog。MainWindow類使用AddressWidget作為其中心小部件,并提供文件和工具菜單。(與官方示例不同的地方是:MainWindow,使用AddressBook類繼承了一下)

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

官方結(jié)構(gòu)圖:


結(jié)構(gòu)圖

自己實現(xiàn)的結(jié)構(gòu)圖:
連接線我使用了依賴關(guān)系來連接

結(jié)構(gòu)圖

AddressWidget類是一個QTabWidget子類,用于操作示例中顯示的10個選項卡:9個字母組選項卡和一個NewAddressTab實例。NewAddressTab類是QWidget的一個子類,它只在地址簿為空時使用,提示用戶添加一些聯(lián)系人。AddressWidget還與TableModel的實例進行交互,以添加、編輯和刪除地址簿中的條目。

TableModelQAbstractTableModel的子類,它提供了訪問數(shù)據(jù)的標準模型/視圖API。它包含一個添加聯(lián)系人列表。但是,這些數(shù)據(jù)在單個選項卡中并不都是可見的。相反,根據(jù)字母表組,QTableView被用來提供相同數(shù)據(jù)的9種不同視圖。

QSortFilterProxyModel是負責(zé)過濾每個聯(lián)系人組的聯(lián)系人的類。每個代理模型使用一個QRegExp來過濾不屬于相應(yīng)字母組的聯(lián)系人。AddDialog類用于從用戶獲取地址簿的信息。這個QDialog子類由NewAddressTab實例化以添加聯(lián)系人,并由AddressWidget實例化以添加和編輯聯(lián)系人。

在官方示例的基礎(chǔ)之上,把MainWindow使用AddressBook繼承了一下。

實現(xiàn)的話,按照從底層到上層的方式實現(xiàn),那么先實現(xiàn)TableModel。
TableModel類通過子類化QAbstractTableModel來提供標準API來訪問聯(lián)系人列表中的數(shù)據(jù)。為此必須實現(xiàn)的基本函數(shù)有:rowCount()、columnCount()、data()、headerData()。要使TableModel可編輯,它必須提供實現(xiàn)insertRows()、removeRows()、setData()flags()函數(shù)。

1、TableModel的定義

Contact是數(shù)據(jù)模型所使用和管理的數(shù)據(jù)

//記錄地址簿數(shù)據(jù)
struct Contact
{
    QString strName;         
    QString strAddress;

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

接下來是一段重載QDataStream的IO操作,這兩個重載是為了實現(xiàn)讀取、存儲文件功能。

//輸出
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;
}

我這里新增了一個枚舉變量的定義,為了標識表格的列,避免代碼中出現(xiàn)魔鬼數(shù)字,以及支撐后期列的擴展變化。

enum class AddressBookColumn
{
    name = 0,
    address
};

接下來是類的定義:
這里使用了兩個構(gòu)造函數(shù),一個是使用TableModel自己的默認構(gòu)造函數(shù),另一個是使用QVector<Contact>作為參數(shù)的構(gòu)造函數(shù),這是為了方便起見。TableModel中的最后一個函數(shù)getContacts()返回QVector<Contact>對象,該對象保存通訊錄中的所有聯(lián)系人。

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的實現(xiàn)

實現(xiàn)頭文件中定義的兩個構(gòu)造函數(shù)。第二個構(gòu)造函數(shù)使用參數(shù)值初始化模型中的聯(lián)系人列表。
由于本示例的列是固定的兩列,所以這里增加了一個常量來定義列的個數(shù),后期增加列的話直接修改該常量即可

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()函數(shù)返回模型的維數(shù)。然而,rowCount()的值將根據(jù)添加到地址簿的聯(lián)系人數(shù)量而變化,columnCount()的值總是2,因為我們只需要名稱和地址列的空間。
官方示例的實現(xiàn)代碼:

官方代碼

我的寫法:

int TableModel::rowCount(const QModelIndex& parent) const
{
    //行數(shù)會根據(jù)數(shù)據(jù)量而變化
    return m_oContacts.size();
}

int TableModel::columnCount(const QModelIndex& parent) const
{
    //官方示例這里給了數(shù)值2,不符合代碼規(guī)范,這里定義一個常量,未來擴展列數(shù),比如添加一個郵編列,只需要
    //修改常量的值就好
    return c_nColumnCnt;
}

沒有必要寫成官方那樣復(fù)雜,行數(shù)就是數(shù)據(jù)量,而列數(shù)又是一個固定值。所以直接返回即可。

data()函數(shù)根據(jù)提供的模型索引的內(nèi)容返回名稱或地址。模型索引中存儲的行號用于引用聯(lián)系人列表中的項。

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

    if (Qt::DisplayRole == role)
    {
        //預(yù)防越界訪問
        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()函數(shù)的作用是:顯示表的標題,“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()函數(shù)的作用是:在添加新數(shù)據(jù)之前調(diào)用insertRows()函數(shù),否則數(shù)據(jù)將不會顯示。調(diào)用beginInsertRows()endInsertRows()函數(shù)以確保所有連接的視圖都知道這些更改。該函數(shù)是提供給添加聯(lián)系人的功能使用的,在插入數(shù)據(jù)之前,先在表格內(nèi)添加一行,然后容器添加一條空記錄。

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

調(diào)用removeRows()函數(shù)來刪除數(shù)據(jù)。再次調(diào)用beginRemoveRows()endRemoveRows(),以確保所有連接的視圖都知道這些更改。
寫的時候需要注意一下,begin、end在插入刪除上函數(shù)較為類似,不要寫反了。

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()函數(shù)的作用是:向表中逐項而不是逐行插入數(shù)據(jù)。這意味著要填充地址本中的一行,必須調(diào)用兩次setData(),因為每一行有兩列。
發(fā)出dataChanged()信號很重要,因為它告訴所有連接的視圖更新它們的顯示。
同時需要關(guān)注一下返回值,如果返回值寫的有問題,數(shù)據(jù)刷新就會存在問題。
insertRows()是在容器內(nèi)插入了一行空行,那么setData()函數(shù)就是給當前新插入的一行空行寫入數(shù)據(jù)。

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()函數(shù)的作用是:返回給定索引的項標志
設(shè)置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類在技術(shù)上是本例中涉及的主要類,因為它提供了添加、編輯和刪除聯(lián)系人、將聯(lián)系人保存到文件中以及從文件中加載聯(lián)系人的功能

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的實現(xiàn)

AddressWidget構(gòu)造函數(shù)接受一個父小部件并實例化NewAddressTab、TableModel和QSortFilterProxyModel。添加NewAddressTab對象(用于指示地址簿為空),其余9個選項卡使用setupTabs()設(shè)置。
注意: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();
}

這里就先跳轉(zhuǎn)到NewAddressTab的定義與實現(xiàn),因為AddressWidget依賴它。

4.1、NewAddressTab定義

NewAddressTab類提供一個提供信息的選項卡,告訴用戶地址簿是空的。它根據(jù)地址簿的內(nèi)容是否為空來控制顯示和消失。
界面效果如圖:

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的信號,這個信號就是添加聯(lián)系人所發(fā)出的信號,主要用來通知視圖刷新數(shù)據(jù)以及存儲新增數(shù)據(jù)。

4.2、NewAddressTab實現(xiàn)

構(gòu)造函數(shù)實例化addButton、descriptionLabel并將addButton的信號連接到addEntry()槽。
addEntry()函數(shù)與AddressWidgetaddEntry()類似,因為這兩個函數(shù)都實例化了一個AddDialog對象。通過發(fā)出sendDetails()信號,提取對話框中的數(shù)據(jù)并將其發(fā)送到AddressWidgetaddEntry()槽。

這個AddDialog就是實現(xiàn)添加數(shù)據(jù)的對話框,在NewAddressTab、AddressWidget中都有調(diào)用。

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

啊,這里又出現(xiàn)了一個AddDialog,這個在之前也沒有定義過,那么我們還需要定義它,不然無法通過編譯不是嗎?

4.3、AddDialog定義

AddDialog類擴展了QDialog,并為用戶提供QLineEditQTextEdit,以便將聯(lián)系人數(shù)據(jù)(姓名、地址)輸入地址簿。
實現(xiàn)后的界面如下圖:

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實現(xiàn)

AddDialog的構(gòu)造函數(shù)設(shè)置用戶界面,創(chuàng)建必要的小部件并將它們放置到布局中。
大家注意QGridLayout,這個網(wǎng)格布局,對齊方式比較常用,各個控件之間的間隔、對齊調(diào)整起來較為費時。
界面布局這里使用了網(wǎng)格、垂直、水平三種布局方式,在做界面設(shè)計的時候,這三種布局是非常常用的。而且布局除了可以添加QWidget之外也可以添加其他Layout
setWindowTitle()該函數(shù)是用來設(shè)置窗體標題的,我們這里給了一個常量,標題可以設(shè)置成參數(shù)傳遞進來,這樣可以做成一個可定制窗體

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

提供兩個接口函數(shù),以獲取界面輸入,封裝自身屬性。

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這個函數(shù)是提供給添加使用的,當?shù)刂凡局幸呀?jīng)存在聯(lián)系人數(shù)據(jù)的時候,編輯、修改已有數(shù)據(jù),這些數(shù)據(jù)需要顯示在界面中同時Name項無法進行編輯,要把它設(shè)置為只讀。

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,繞了這么久,現(xiàn)在可以回到AddressWidget的實現(xiàn)了。
setupTabs()函數(shù)用于在AddressWidget中設(shè)置9個字母組選項卡、表視圖和代理模型。每個代理模型依次設(shè)置為使用不區(qū)分大小寫的QRegExp對象根據(jù)相關(guān)字母表組過濾聯(lián)系人名稱。表視圖也使用相應(yīng)的代理模型的sort()函數(shù)按升序排序。每個表視圖的selectionMode被設(shè)置為QAbstractItemView::SingleSelection(只能單選), selectionBehavior被設(shè)置為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); //設(shè)置選擇模式 按行選擇
        pTab->horizontalHeader()->setStretchLastSection(true); //最后一個選項是否占用剩余所有空間
        pTab->verticalHeader()->hide(); //隱藏垂直標頭
        pTab->setEditTriggers(QAbstractItemView::NoEditTriggers); //設(shè)置編輯框不可編輯
        pTab->setSelectionMode(QAbstractItemView::SingleSelection);
        pTab->setSortingEnabled(true); // Enabled生效就立即執(zhí)行排序

        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()信號連接到發(fā)出AddressWidgetselectionChanged()的lambda表達式。
這兩個信號是給菜單中的Edit Entry、Remove Entry兩個Action使用的,這兩個Action會根據(jù)選擇的變化而進行刷新可用狀態(tài),當沒有選擇數(shù)據(jù)的時候,這兩個Action是灰顯不可用的狀態(tài),反之就是可用狀態(tài)。

地址簿中的每個表視圖都作為附簽添加到QTabWidget,并帶有相關(guān)的標簽,這些標簽是從組的QStringList中獲得的。

image.png

我們提供了兩個addEntry()函數(shù):一個用于接受用戶輸入,另一個用于執(zhí)行向地址簿添加新條目的實際任務(wù)。我們將添加條目的職責(zé)分為兩部分
,以允許newAddressTab插入數(shù)據(jù),而不必彈出一個對話框。
第一個addEntry()函數(shù)是一個槽,函數(shù)名為:showAddEntryDialog,它連接到主窗口的
"Add Entry" Action。該函數(shù)創(chuàng)建一個AddDialog對象,然后調(diào)用第二個addEntry()函數(shù)來實際將聯(lián)系人添加到表中。

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

基本驗證在第二個addEntry()函數(shù)中完成,以防止地址簿中的重復(fù)條目。正如在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));
    }
}

如果模型還沒有包含具有相同名稱的條目,則調(diào)用setData()將名稱和地址插入第一列和第二列。否則,我們將顯示一個QMessageBox來通知用戶。
注意:一旦添加了聯(lián)系人,newAddressTab將被刪除,因為地址簿不再為空。

editEntry只是更新聯(lián)系人地址的一種方式,因為示例不允許用戶更改現(xiàn)有聯(lián)系人的名稱。
首先,我們使用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函數(shù),就是在這里調(diào)用的

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

實現(xiàn)效果如下圖:

image.png

使用removeEntry()函數(shù)刪除條目。通過QItemSelectionModel對象selectionModel訪問被選中的行,從而刪除它。只有當用戶刪除了地址簿中的所有聯(lián)系人時,才會將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()函數(shù)的作用是:保存一個包含通訊錄中所有聯(lián)系人的文件。文件以自定義的.dat格式保存。聯(lián)系人列表的內(nèi)容使用QDataStream寫入文件。如果文件無法打開,則會顯示一個QMessageBox,并顯示相關(guān)的錯誤消息。
readFromFile()函數(shù)的作用是:加載一個包含通訊錄中所有聯(lián)系人的文件,該通訊錄以前是使用writeToFile()保存的。QDataStream用于將.dat文件的內(nèi)容讀入聯(lián)系人列表,每個聯(lián)系人都是使用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定義

主窗體主要實現(xiàn)了,把AddressWidget窗體作為主窗體的中心界面,然后創(chuàng)建兩個菜單,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實現(xiàn)

addressBook的構(gòu)造函數(shù)實例化AddressWidget,將其設(shè)置為其中心小部件,并調(diào)用createMenus()函數(shù)。

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

createMenus()函數(shù)設(shè)置File、Open菜單,將操作連接到它們各自的槽。兩個編輯條目Edit EntryRemove Entry操作在默認情況下是禁用的,因為這樣的操作不能在一個空的地址簿上執(zhí)行。只有在添加一個或多個聯(lián)系人時才啟用它們。

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(); //此函數(shù)添加一個分隔符

    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的響應(yīng)信號,綁定到了AddressWidgetshowAddEntryDialog槽上面。

updateActions()函數(shù)的作用是:根據(jù)地址簿的內(nèi)容決定禁用啟用Edit EntryRemove Entry。如果地址簿為空,則禁用這些操作;否則,它們是啟用的。這個函數(shù)是一個插槽連接到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實現(xiàn)了
打開的功能就是用來打開保存功能存儲的文件,保存就是把地址簿中的聯(lián)系人數(shù)據(jù)存儲為文件,數(shù)據(jù)是二進制流數(shù)據(jù)。

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);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容