QT5.14.2自带Examples:Address Book

配套视频讲解:https://www.bilibili.com/video/BV1uf4y1U7aa/

地址簿示例

地址簿示例展示了如何使用代理模式,基于同一个模型数据展示不同的视图。
运行效果
这个例子提供了一个地址簿,支持将联系人按人名的头字母划分为9组: ABC, DEF, GHI, … , VW, …, XYZ。该功能通过QSortFilterProxyModel 类对模型进行过滤,生成9个代理模型对象,分别将过滤后的数据交给9个视图对象进行显示。对象图如下所示:
在这里插入图片描述

概述

本示例包含五个类: MainWindow, AddressWidget, TableModel, NewAddressTab 和 AddDialog。MainWindow 类使用AddressWidget 作为 central widget 并提供了 File 和 Tools 菜单.
在这里插入图片描述
AddressWidget 是 QTabWidget 的子类,用于实现10个tab:9个字母分组,和1个NewAddressTab ( QWidget 的子类当模型中数据为空时会显示 )。AddressWidget 还负责与TableModel实例的交互工作,包含增加、编辑、删除地址簿中的条目。
TableModel 是 QAbstractTableModel 的子类,提供了标准的 model/view API 操作模型中的数据。数据为一个联系人列表,每一个条目包含一个结构体{联系人名称,地址}。但是,这个数据并没有展现在一个单独的tab中,而且是通过字母分组,展示到了9个tab中,每个tab包含一个QTableView 对象用户显示过滤后的数据。
QSortFilterProxyModel用户过滤联系人数据。每一个代理模型使用QRegExp(正则表达式)过滤掉不属于该组的条目。是QDialog的子类,用于从用户输入获取地址簿的信息。 在NewAddressTab 和 AddressWidget 中可以被调用生成,进行数据输入。

实现步骤

- 生成工程

启动Creator4.11.1
文件->新建文件或项目
创建一个Qt Widgets Application项目
在这里插入图片描述
后面按要求一路点下去,给项目起一个自己的名字,例如:myAddressBook。
在details步骤里,取消Generate form选项,UI的内容我都将采用代码实现。入下图所示:
在这里插入图片描述
Kits步骤里,一定要选择一个已有的编译器,入下图所示。多选几个也可以。例如,在我机器里,没有安装MSVC2017,如果只选择了该选项,在编译的时候将无法编程生成源文件。这里我选择了QT内置的WinGW编译器
在这里插入图片描述
完成后,将会看到一些默认的源文件,点击运行试一试效果:
在这里插入图片描述

-新建类

按上面概述的描述,先新建下面四个类的源文件(MainWindow已经有了):
AddressWidget, TableModel, NewAddressTab 和 AddDialog。
右键工程名称,选择Add New…,创建相应的C++ Class文件具体如下所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里为了方便,所有的基类都选择QWidget。基类不是QWidget的后面可以在代码里进行修改。
重复上面操作,构出所需的所有类。如果现在不想一起创建,也可以后面再补充。
在这里插入图片描述

-源码分析

构造一个软件,我们应该从内往外,先创建TableModel,AddDialog,NewAddressTab,这些部分好比是构造外层的必需品; 然后再完成AddressWidget和MainWindow。
但为了测试、学习方便。我们可以先把MainWindow的框架搭建起来,有些目前用不到的函数可以先写入出头文件,然后注释掉,后面再补充完善。

从外往里的构建方式,往往会造成软件结构的混乱,在现实编程中应该尽量采用从内往外的方式。也就是先准备好源部件,再进行组装。最后按下开关,见证奇迹(当然,如何对模块进行测试又是一门大学问)。但从内往外的编程方式作为教学方法会不够直观,所以这里我们还是采用从外往里的构建过程。
有些程序员,项目初期往往令老板和客户抓狂。因为前期,甚至中后期都看不到什么效果,只能听程序员汇报完成了某一个模块,感觉都是准备工作,后面还遥遥无期。但最终的成品总是调理更清晰,bug也会比较少,总工期更短。

MainWindow

我们先来分析一下主窗口的源码,具体见注释。

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include "addresswidget.h"
#include <QMainWindow>
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    //注意这里没有析构函数,因为例子里没有,这里我也就删掉了。
    
//代码里采用了QT5的信号槽语法,下面这一行可以删掉
private slots:
  //  这部分内容需要构建完addressWidget以后会用到,先注释掉。
 // void updateActions(const QItemSelection &selection);
    void openFile();
    void saveFile();
private:
    void createMenus();
    //注意这里是一个指针,在使用前,需要new一个对象出来才可以。否则会出现运行错误
    AddressWidget *addressWidget;
    //下面两个个action需要作为类的私有数据,是因为菜单项生成后,
    //后面还行需要控制菜单项是否可用(setEnabled)。所以不能像其他菜单项一样,new完以后完全交给父窗口维护。
    QAction *editAct;
    QAction *removeAct;
};
#endif // MAINWINDOW_H

在这里插入图片描述

#include "mainwindow.h"
#include <QtWidgets>
MainWindow::MainWindow(QWidget *parent)
//addressWidget原本只是个空指针,必须先赋值。虽然很简单,但容易漏掉。
    : QMainWindow(parent), addressWidget(new AddressWidget)
{
    setCentralWidget(addressWidget);
    createMenus();
    setWindowTitle(tr("Address Book"));
}
//createMenus的代码比较简单,就不详细写注释了。
//此时我们的AddressWidget类并没有完成,所以注释掉了需要AddressWidget的部分。
void MainWindow::createMenus()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));

    QAction *openAct = new QAction(tr("&Open..."), this);
    fileMenu->addAction(openAct);
    connect(openAct, &QAction::triggered, this, &MainWindow::openFile);

    QAction *saveAct = new QAction(tr("&Save As..."), this);
    fileMenu->addAction(saveAct);
    connect(saveAct, &QAction::triggered, this, &MainWindow::saveFile);

    fileMenu->addSeparator();

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

    QMenu *toolMenu = menuBar()->addMenu(tr("&Tools"));

    QAction *addAct = new QAction(tr("&Add Entry..."), this);
    toolMenu->addAction(addAct);
//    connect(addAct, &QAction::triggered,
//            addressWidget, &AddressWidget::showAddEntryDialog);

    editAct = new QAction(tr("&Edit Entry..."), this);
    editAct->setEnabled(false);
    toolMenu->addAction(editAct);
//    connect(editAct, &QAction::triggered, addressWidget, &AddressWidget::editEntry);

    toolMenu->addSeparator();

    removeAct = new QAction(tr("&Remove Entry"), this);
    removeAct->setEnabled(false);
    toolMenu->addAction(removeAct);
  //  connect(removeAct, &QAction::triggered, addressWidget, &AddressWidget::removeEntry);

  //  connect(addressWidget, &AddressWidget::selectionChanged,
  //          this, &MainWindow::updateActions);
}
//openFile,saveFile是将文件内容导入内存中,以及将内存数据保持到文件中
//具体内容交给了addressWidget去完成。
void MainWindow::openFile()
{
//    QString fileName = QFileDialog::getOpenFileName(this);
//    if (!fileName.isEmpty())
//        addressWidget->readFromFile(fileName);
}

void MainWindow::saveFile()
{
//    QString fileName = QFileDialog::getSaveFileName(this);
//    if (!fileName.isEmpty())
//        addressWidget->writeToFile(fileName);
}

//updateActions函数会再addressWidget有条目被选中是被调用。默认disable的编辑和删除菜单项将会enable
//void MainWindow::updateActions(const QItemSelection &selection)
//{
//    QModelIndexList indexes = selection.indexes();

//    if (!indexes.isEmpty()) {
//        removeAct->setEnabled(true);
//        editAct->setEnabled(true);
//    } else {
//        removeAct->setEnabled(false);
//        editAct->setEnabled(false);
//    }
//}

AddressWidget

由于我们采用的是从外到里的方式,构建AddressWidget所需的TableModel和NewAddressTab还没有完成。因此,我们这里只是先把界面的中需要出现的10个tab显示出来。

#ifndef ADDRESSWIDGET_H
#define ADDRESSWIDGET_H
#include "newaddresstab.h"
#include "tablemodel.h"
#include <QItemSelection>
#include <QTabWidget>
//注意,这里基类是QTabWidget
class AddressWidget : public QTabWidget
{
    Q_OBJECT
public:
    AddressWidget(QWidget *parent = nullptr);
private:
	//为tabwidget添加tabs。后面我还将在这里设置model和view。
    void setupTabs();

    TableModel *table;
    NewAddressTab *newAddressTab;
};

#endif // ADDRESSWIDGET_H

#include "addresswidget.h"
#include "adddialog.h"
#include <QtWidgets>

AddressWidget::AddressWidget(QWidget *parent) : QTabWidget(parent),
    table(new TableModel(this)),
    newAddressTab(new NewAddressTab(this))
{   //添加一个tab,改tab只有在地址簿数据为空的时候才会出现。
    addTab(newAddressTab, tr("Address Book"));
    setupTabs();
}
//只是简单的添加了9个字母组分类的tabs。
void AddressWidget::setupTabs()
{
    const auto groups = { "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VW", "XYZ" };
    for (const QString &str : groups) {
        QTableView *tableView = new QTableView;
        addTab(tableView, str);
    }
}

现在来看看运行结果吧:
在这里插入图片描述
AddressWidget 是与用户交互的主要部分,也是本示例中最关键的部分,目前我们只完成了一小部分。下面我们将先完成所需的“元器件”( TableModel , NewAddressTab ),再回来补充完善剩下的部分。

NewAddressTab

NewAddressTab 的实现比较简单,只是在界面中显示一段提示信息,加上一个Add按钮。Add按钮的功能实现需要AddDialog的实现,这部分功能我们将后面完善。

#ifndef NEWADDRESSTAB_H
#define NEWADDRESSTAB_H

#include <QWidget>

class NewAddressTab : public QWidget
{
    Q_OBJECT
public:
    explicit NewAddressTab(QWidget *parent = nullptr);
    void addEntry();

signals:
//sendDetails这里只是一个singal,不需要在cpp文件中实现。只需要通过信号槽机制,把参数传出去就行。
    void sendDetails(const QString &name, const QString &address);
};

#endif // NEWADDRESSTAB_H

#include "newaddresstab.h"
#include "adddialog.h"
#include <QtWidgets>
NewAddressTab::NewAddressTab(QWidget *parent) : QWidget(parent)
{
    auto descriptionLabel = new QLabel(tr("There are currently no contacts in your address book. "
                                          "\nClick Add to add new contacts."));

    auto addButton = new QPushButton(tr("Add"));

    connect(addButton, &QAbstractButton::clicked, this, &NewAddressTab::addEntry);

    auto mainLayout = new QVBoxLayout;
    mainLayout->addWidget(descriptionLabel);
    mainLayout->addWidget(addButton, 0, Qt::AlignCenter);

    setLayout(mainLayout);
}

void NewAddressTab::addEntry()
{
//    AddDialog aDialog;

//    if (aDialog.exec())
//        emit sendDetails(aDialog.name(), aDialog.address());
}

在这里插入图片描述

TableModel

总算到了TableModel,TableModel本该是最早构建的内容。没有model,就没有数据,也就View不出任何东西。
QT的model/view机制,使得model中的数据发生变化时,views会自动更新。TableModel管理的是整个电话簿的数据,在AddressWidget,添加tabs的过程中我们将为9个tab创建9个代理model,与9个view一一对应。具体参见上面的对象图。

#ifndef TABLEMODEL_H
#define TABLEMODEL_H


#include <QAbstractTableModel>
#include <QVector>
//! [0]
//结构Contact包含名字和地址
struct Contact
{
    QString name;
    QString address;
    //这里重载了==号,只有名称和地址都相同的时候,我们才认为他们相等。
    bool operator==(const Contact &other) const
	{
           return name == other.name && address == other.address;
     }
};
//对<<符号的重载,将内存的数据存入文件中时需要使用。
inline QDataStream &operator<<(QDataStream &stream, const Contact &contact)
{
    return stream << contact.name << contact.address;
}
//对>>符号的重载,文件中的数据导入到内存中时需要使用。
inline QDataStream &operator>>(QDataStream &stream, Contact &contact)
{
    return stream >> contact.name >> contact.address;
}
//注意这里基类要写成QAbstractTableModel
class TableModel : public QAbstractTableModel
{
    Q_OBJECT
    
public:
    TableModel(QObject *parent = nullptr);
    //源码中有下面的构造函数,但本例中没有用到可以删掉。
    //TableModel(const QVector<Contact> &contacts, QObject *parent = nullptr);
    
    int rowCount(const QModelIndex &parent) const override;
    int columnCount(const QModelIndex &parent) const override;
    QVariant data(const QModelIndex &index, int role) const override;
    QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
     //源码中有ItemFlags,但本例中没有用到可以删掉。
   // Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
    bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
    bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
    //这里就是存放的数据。
    const QVector<Contact> &getContacts() const;
    
private:
    QVector<Contact> contacts;
};

#endif // TABLEMODEL_H
#include "tablemodel.h"

TableModel::TableModel(QObject *parent)
    : QAbstractTableModel(parent)
{
}
//行数,如果不存在父级索引,表示没有数据返回0。否则返回条目的数量。
//contacts是一个vector容器,通过size()函数可以获得容器中的条目个数。
int TableModel::rowCount(const QModelIndex &parent) const
{
    return parent.isValid() ? 0 : contacts.size();
}
//列数,与行数类似。列数只可能为0,或2(名称,地址)
int TableModel::columnCount(const QModelIndex &parent) const
{
    return parent.isValid() ? 0 : 2;
}
//返回model中的某一个数据,并不是一个条目,而是表格里的一个项。
//我们的model是基于QAbstractTableModel,数据的索引类似二维数组的下标。
QVariant TableModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();
    
    if (index.row() >= contacts.size() || index.row() < 0)
        return QVariant();
    
    if (role == Qt::DisplayRole) {
        const auto &contact = contacts.at(index.row());
        
        switch (index.column()) {
        case 0:
            return contact.name;
        case 1:
            return contact.address;
        default:
            break;
        }
    }
    return QVariant();
}
//获取表头项
QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();
    
    if (orientation == Qt::Horizontal) {
        switch (section) {
        case 0:
            return tr("Name");
        case 1:
            return tr("Address");
        default:
            break;
        }
    }
    return QVariant();
}
//插入数据(起点,行数,父级索引)
bool TableModel::insertRows(int position, int rows, const QModelIndex &index)
{
	//没有实质性的作用,用来避免编译器警告,对于tree来说,index需要使用,我们这里是table,index用不上。
    Q_UNUSED(index);
    //必须吧contacts的操作,包含在begin和end中,这样对应的views才会自动更新。
    beginInsertRows(QModelIndex(), position, position + rows - 1);
    //插入时,name和address都为空字符串。需要配合setData一起使用。
    for (int row = 0; row < rows; ++row)
        contacts.insert(position, { QString(), QString() });
    
    endInsertRows();
    return true;
}
//与插入类似
bool TableModel::removeRows(int position, int rows, const QModelIndex &index)
{
    Q_UNUSED(index);
    beginRemoveRows(QModelIndex(), position, position + rows - 1);
    
    for (int row = 0; row < rows; ++row)
        contacts.removeAt(position);
    
    endRemoveRows();
    return true;
}
//注意这里的数据角色需要时Qt::EditRole
//通过索引找到需要修改的项,通过value修改它。
//使用方式举例:setData(index, name, Qt::EditRole);
bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (index.isValid() && role == Qt::EditRole) {
        const int row = index.row();
        auto contact = contacts.value(row);
        
        switch (index.column()) {
        case 0:
            contact.name = value.toString();
            break;
        case 1:
            contact.address = value.toString();
            break;
        default:
            return false;
        }
        contacts.replace(row, contact);
        //需要通知数据的使用者,数据发生了变化。views才会更新数据。dataChanged是基类的函数。
        //topLeft, bottomRight两个index划出需要更新的范围。
        emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
        
        return true;
    }
    
    return false;
}

const QVector<Contact> &TableModel::getContacts() const
{
    return contacts;
}


AddDialog

AddDialog是一个零时窗口,用户修改或添加条目。

#ifndef ADDDIALOG_H
#define ADDDIALOG_H

#include <QDialog>
class QLineEdit;
class QTextEdit;

class AddDialog : public QDialog
{
    Q_OBJECT
    
public:
    AddDialog(QWidget *parent = nullptr);
    
    QString name() const;
    QString address() const;
    void editAddress(const QString &name, const QString &address);
    
private:
    QLineEdit *nameText;
    QTextEdit *addressText;
};

#endif // ADDDIALOG_H

#include "adddialog.h"
#include <QtWidgets>
AddDialog::AddDialog(QWidget *parent) : QDialog(parent),
        nameText(new QLineEdit),
    addressText(new QTextEdit)
{
    auto nameLabel = new QLabel(tr("Name"));
    auto addressLabel = new QLabel(tr("Address"));
    auto okButton = new QPushButton(tr("OK"));
    auto cancelButton = new QPushButton(tr("Cancel"));

    auto gLayout = new QGridLayout;
    gLayout->setColumnStretch(1, 2);
    gLayout->addWidget(nameLabel, 0, 0);
    gLayout->addWidget(nameText, 0, 1);

    gLayout->addWidget(addressLabel, 1, 0, Qt::AlignLeft|Qt::AlignTop);
    gLayout->addWidget(addressText, 1, 1, Qt::AlignLeft);

    auto buttonLayout = new QHBoxLayout;
    buttonLayout->addWidget(okButton);
    buttonLayout->addWidget(cancelButton);

    gLayout->addLayout(buttonLayout, 2, 1, Qt::AlignRight);

    auto mainLayout = new QVBoxLayout;
    mainLayout->addLayout(gLayout);
    setLayout(mainLayout);

    connect(okButton, &QAbstractButton::clicked, this, &QDialog::accept);
    connect(cancelButton, &QAbstractButton::clicked, this, &QDialog::reject);

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

QString AddDialog::name() const
{
    return nameText->text();
}

QString AddDialog::address() const
{
    return addressText->toPlainText();
}

void AddDialog::editAddress(const QString &name, const QString &address)
{
    nameText->setReadOnly(true);
    nameText->setText(name);
    addressText->setPlainText(address);
}

NewAddressTab(2)

现在回到NewAddressTab,补充完善addEntry()函数(取消我们之前加上的注释即可)。
点击Add之后我们将可以看到我们的AddDialog窗口,输入信息,点击OK后,名称和地址将会通过信号槽机制发送出去。真正执行插入操作的是AddressWidget::addEntry
在这里插入图片描述

AddressWidget(2)

现在构建AddressWidget所需的源部件都已经准备完善。可以来看看完整的代码了。

#ifndef ADDRESSWIDGET_H
#define ADDRESSWIDGET_H
#include "newaddresstab.h"
#include "tablemodel.h"
#include <QItemSelection>
#include <QTabWidget>
//注意,这里基类是QTabWidget
class AddressWidget : public QTabWidget
{
    Q_OBJECT
public:
    AddressWidget(QWidget *parent = nullptr);
    void readFromFile(const QString &fileName);
    void writeToFile(const QString &fileName);

public slots:
    void showAddEntryDialog();
    void addEntry(const QString &name, const QString &address);
    void editEntry();
    void removeEntry();

signals:
    void selectionChanged (const QItemSelection &selected);
private:
    //为tabwidget添加tabs。后面我还将在这里设置model和view。
    void setupTabs();

    TableModel *table;
    NewAddressTab *newAddressTab;
};

#endif // ADDRESSWIDGET_H


#include "addresswidget.h"
#include "adddialog.h"

#include <QtWidgets>

AddressWidget::AddressWidget(QWidget *parent)
    : QTabWidget(parent),
    table(new TableModel(this)),
    newAddressTab(new NewAddressTab(this))
{
	//newAddressTab中点击add按钮,将会出发AddressWidget::addEntry
    connect(newAddressTab, &NewAddressTab::sendDetails,
            this, &AddressWidget::addEntry);

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

    setupTabs();
}

void AddressWidget::showAddEntryDialog()
{
    AddDialog aDialog;
	//和newAddressTab中点击add按钮效果一样。都是将AddDialog的信息,发送给AddressWidget::addEntry
    if (aDialog.exec())
        addEntry(aDialog.name(), aDialog.address());
}

void AddressWidget::addEntry(const QString &name, const QString &address)
{
    if (!table->getContacts().contains({ name, address })) {
    	//index的行总是0,也就是从表头插入
        table->insertRows(0, 1, QModelIndex());
		
        QModelIndex index = table->index(0, 0, QModelIndex());
        table->setData(index, name, Qt::EditRole);
        index = table->index(0, 1, QModelIndex());
        table->setData(index, address, Qt::EditRole);
        //此时数据条目不为空,newAddressTab没必要存在了。
        removeTab(indexOf(newAddressTab));
    } else {
        QMessageBox::information(this, tr("Duplicate Name"),
                                 tr("The name \"%1\" already exists.").arg(name));
    }
}

void AddressWidget::editEntry()
{
    QTableView *temp = static_cast<QTableView*>(currentWidget());
    //我们已经在setuptabs里为view设置了model,所以我们知道view的类型为代理model
    QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model());
    //QTableView内置QItemSelectionModel,记录被选中的indexes。
    QItemSelectionModel *selectionModel = temp->selectionModel();
    const QModelIndexList indexes = selectionModel->selectedRows();
    QString name;
    QString address;
    int row = -1;
    
	//从model中提取数据
	//这里只能单选,所以可以不用for。自己使用const QModelIndex &index =indexes.first();效果一样
    for (const QModelIndex &index : indexes) {
   		 //通过mapToSource,可以找到真正的原始模型中的数据
        row = proxy->mapToSource(index).row();
        QModelIndex nameIndex = table->index(row, 0, QModelIndex());
        QVariant varName = table->data(nameIndex, Qt::DisplayRole);
        name = varName.toString();

        QModelIndex addressIndex = table->index(row, 1, QModelIndex());
        QVariant varAddr = table->data(addressIndex, Qt::DisplayRole);
        address = varAddr.toString();
    }
	//把上面获取的数据输入到AddDialog
    AddDialog aDialog;
    aDialog.setWindowTitle(tr("Edit a Contact"));
    aDialog.editAddress(name, address);
	//修改值,保存回model。只能修改address
    if (aDialog.exec()) {
        const QString newAddress = aDialog.address();
        if (newAddress != address) {
            const QModelIndex index = table->index(row, 1, QModelIndex());
            //setData中会调用 emit dataChanged,通知views更新数据。
            table->setData(index, newAddress, Qt::EditRole);
        }
    }
}

void AddressWidget::removeEntry()
{
    QTableView *temp = static_cast<QTableView*>(currentWidget());
    QSortFilterProxyModel *proxy = static_cast<QSortFilterProxyModel*>(temp->model());
    QItemSelectionModel *selectionModel = temp->selectionModel();

    const QModelIndexList indexes = selectionModel->selectedRows();

    for (QModelIndex index : indexes) {
        int row = proxy->mapToSource(index).row();
        table->removeRows(row, 1, QModelIndex());
    }

    if (table->rowCount(QModelIndex()) == 0)
        insertTab(0, newAddressTab, tr("Address Book"));
}

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

    for (const QString &str : groups) {
   		//正则表达式,以str任意一个开头的单词
        const auto regExp = QRegularExpression(QString("^[%1].*").arg(str),
                                               QRegularExpression::CaseInsensitiveOption);

        auto proxyModel = new QSortFilterProxyModel(this);
        proxyModel->setSourceModel(table);
        proxyModel->setFilterRegularExpression(regExp);
        proxyModel->setFilterKeyColumn(0);
		//上面是models,下面是views
        QTableView *tableView = new QTableView;

        tableView->setModel(proxyModel);
        tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
        tableView->horizontalHeader()->setStretchLastSection(true);
        tableView->verticalHeader()->hide();
        tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
        tableView->setSelectionMode(QAbstractItemView::SingleSelection);
        tableView->setSortingEnabled(true);
	//当QTabWidget当前选项发生变化时,需要selectionChanged通知菜单(编辑,删除)
	//MainWindow::updateActions会被执行。修改菜单项的状态。
        connect(tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
                this, &AddressWidget::selectionChanged);
//当选中切的tab发送变化时,tabview会清除选中状态。但我们的菜单并不知道,编辑,删除仍然可用。
//这时候进行编辑,删除会找不到index而造成程序的crash。所以需要使用emit selectionChanged
        connect(this, &QTabWidget::currentChanged, this, [this, tableView](int tabIndex) {
            if (widget(tabIndex) == tableView)
                emit selectionChanged(tableView->selectionModel()->selection());
        });

        addTab(tableView, str);
    }
}
//文件的读写依赖于TableModel中<<和>>符号的重载。
void AddressWidget::readFromFile(const QString &fileName)
{
    QFile file(fileName);

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

    QVector<Contact> contacts;
    QDataStream in(&file);
    in >> contacts;

    if (contacts.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(contacts))
            addEntry(contact.name, contact.address);
    }
}

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

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

    QDataStream out(&file);
    out << table->getContacts();
}

MainWindow(2)

最后再完善一下MainWindow,就可以大功告成了!!!
这里面的内容,都是对AddressWidget的调用,看代码就很清楚了,不需要太多解释。

#include "mainwindow.h"
#include <QtWidgets>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), addressWidget(new AddressWidget)
{
    setCentralWidget(addressWidget);
    createMenus();
    setWindowTitle(tr("Address Book"));
}

void MainWindow::createMenus()
{
    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));

    QAction *openAct = new QAction(tr("&Open..."), this);
    fileMenu->addAction(openAct);
    connect(openAct, &QAction::triggered, this, &MainWindow::openFile);

    QAction *saveAct = new QAction(tr("&Save As..."), this);
    fileMenu->addAction(saveAct);
    connect(saveAct, &QAction::triggered, this, &MainWindow::saveFile);

    fileMenu->addSeparator();

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

    QMenu *toolMenu = menuBar()->addMenu(tr("&Tools"));

    QAction *addAct = new QAction(tr("&Add Entry..."), this);
    toolMenu->addAction(addAct);
    connect(addAct, &QAction::triggered,
            addressWidget, &AddressWidget::showAddEntryDialog);

    editAct = new QAction(tr("&Edit Entry..."), this);
    editAct->setEnabled(false);
    toolMenu->addAction(editAct);
    connect(editAct, &QAction::triggered, addressWidget, &AddressWidget::editEntry);

    toolMenu->addSeparator();

    removeAct = new QAction(tr("&Remove Entry"), this);
    removeAct->setEnabled(false);
    toolMenu->addAction(removeAct);
    connect(removeAct, &QAction::triggered, addressWidget, &AddressWidget::removeEntry);
	//修改菜单项的状态
    connect(addressWidget, &AddressWidget::selectionChanged,
            this, &MainWindow::updateActions);
}

void MainWindow::openFile()
{
    QString fileName = QFileDialog::getOpenFileName(this);
    if (!fileName.isEmpty())
        addressWidget->readFromFile(fileName);
}

void MainWindow::saveFile()
{
    QString fileName = QFileDialog::getSaveFileName(this);
    if (!fileName.isEmpty())
        addressWidget->writeToFile(fileName);
}

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

    if (!indexes.isEmpty()) {
        removeAct->setEnabled(true);
        editAct->setEnabled(true);
    } else {
        removeAct->setEnabled(false);
        editAct->setEnabled(false);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_41958234/article/details/106185901