本篇博客是该软件的源代码介绍。从软件设计的角度详细分析设计架构。不涉及到机械设计的专业知识。阅读完整篇博客后,用户应该能够动手实现自己的设计软件了。如果你对机械设计部分的实现感兴趣,也可以直接看源码,代码的注释非常丰富。
软件及源代码获取途径见:机械设计软件-Qt实现(1)-CSDN博客
总体框架
源代码主要分为3个部分—主界面设计,设计表类,设计类。采用了模块化设计。主界面设计的重点是Qt数据库模块的使用和模型视图结构。设计表类利用到了多态,继承和Qt的信号与槽机制。设计类则使用了策略模式,将设计UI和设计信息拆分开,方便程序的拓展。
下文中提到主界面类的一律以MechanicalDesign代指,设计表类对应DesignVec,设计类对应Design。设计UI对应DesignUI,设计信息对应DesignInfo。实际设计中也是如此。
主界面介绍
图片如下:
设计好数据库文件如下,直接用我设计好的也行。
具体实现
这里删除了几乎所有槽函数的声明。
class MechanicalDesign : public QMainWindow
{
Q_OBJECT
private:
DesignVec* m_Vec; //设计表类
QSqlDatabase DB; //数据库连接
QSqlTableModel *tabModel = nullptr; //数据模型,考虑到用户可能没有打开数据库
QItemSelectionModel *selModel; //选择模型
QDataWidgetMapper *dataMapper; //数据映射
//状态栏标签
QLabel * labDate; //设计日期
QLabel* labCount; //设计数量
QLabel* labInfo; //设计说明
//区别开历史设计和本次设计
int base = 0; //历史设计的数量
public:
explicit MechanicalDesign(QWidget *parent = nullptr);
~MechanicalDesign();
private slots:
void do_addDesign(int index); //往数据库中添加一个设计
private:
void OpenTable(); //打开数据表
void do_currentRowChanged(const QModelIndex¤t, const QModelIndex& previous);
void showRecordCount(); //显示设计数量
private:
Ui::MechanicalDesign *ui;
};
QSqlDatabase对象是与实际的数据建立连接的。QSqlTableModel对象是为模型视图机构中的“模型”。视图就是上图中“设计信息”框中的那个图表——QTableView对象。QItemSelectionModel对象(selModel)记录当前所选中的模型。QDataWidgetMapper对象(dataMapper)会将数据库文件中的字段映射到对应组件中,这里为文本框。鼠标点击不同设计时,“显示主要设计”文本框中会显示不同数据,这里便用到了selModel和dataMapper。
具体代码如下,省略了构造函数中的不重要代码。
MechanicalDesign::MechanicalDesign(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MechanicalDesign)
{
ui->setupUi(this);
m_Vec = new DesignVec(this); //创建设计表对象
this->resize(750,500);
//完成一个设计时,添加一条设计信息
connect(m_Vec,&DesignVec::finishADesign,this,&MechanicalDesign::do_addDesign);
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows); //行选择
ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection);//单项选择
ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); //不可编辑
ui->tableView->setAlternatingRowColors(true);
}
void MechanicalDesign::OpenTable()
{//创建数据模型,打开数据表
tabModel = new QSqlTableModel(this,DB);
tabModel->setTable("designInfo");
tabModel->setEditStrategy(QSqlTableModel::OnManualSubmit); //数据保存方式
tabModel->setSort(tabModel->fieldIndex("Type"),Qt::AscendingOrder); //排序方式
if(!(tabModel->select())){ //查询数据失败
QMessageBox::critical(this,"错误信息","打开数据表错误,错误信息:\n"+tabModel->lastError().text());
return;
}
//显示记录条数
showRecordCount();
base = tabModel->rowCount(); //历史设计的数量
//设置字段显示标题
tabModel->setHeaderData(tabModel->fieldIndex("Label"),Qt::Horizontal,"设计标签");
tabModel->setHeaderData(tabModel->fieldIndex("Type"),Qt::Horizontal,"设计种类");
tabModel->setHeaderData(tabModel->fieldIndex("Level"),Qt::Horizontal,"精度");
tabModel->setHeaderData(tabModel->fieldIndex("Material"),Qt::Horizontal,"材料");
tabModel->setHeaderData(tabModel->fieldIndex("Meme"),Qt::Horizontal,"主要设计信息");
//创建选择模型
selModel = new QItemSelectionModel(tabModel,this);
//当前行变化时,发射currentRowChanged信号
connect(selModel,&QItemSelectionModel::currentRowChanged,this,&MechanicalDesign::do_currentRowChanged);
//模型视图结构
ui->tableView->setModel(tabModel);
ui->tableView->setSelectionModel(selModel);
ui->tableView->setColumnHidden(tabModel->fieldIndex("Meme"),true);
//创建界面组件与模型字段的数据映射
dataMapper = new QDataWidgetMapper(this);
dataMapper->setModel(tabModel);
dataMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
//与具体字段的映射
dataMapper->addMapping(ui->textEdit,tabModel->fieldIndex("Meme")); //显示主要设计信息
dataMapper->toFirst(); //移动到首记录
//更新界面组件的使能状态
ui->actOpenDB->setEnabled(false);
ui->actSave->setEnabled(true);
}
void MechanicalDesign::do_addDesign(int index)
{//在数据库中添加一条设计信息
if(tabModel == nullptr){
return; //没有打开数据库的话
}
DesignInfo* tmpInfo = m_Vec->infoFromIndex(index); //返回一条设计信息
if(tmpInfo == nullptr)//判断返回信息是否有效
return;
QSqlRecord rec = tabModel->record(); //获取一条空记录,只有字段定义
int val = (tabModel->rowCount() + 1)%100 * 1000 + QRandomGenerator::global()->bounded(11,999);
//随机生成一段标签序列号
QString Label = QDate::currentDate().toString("yyyy-MM-dd")+"-"+QString::number(val);
rec.setValue(tabModel->fieldIndex("Label"),Label);
rec.setValue(tabModel->fieldIndex("Type"),tmpInfo->type());
rec.setValue(tabModel->fieldIndex("Level"),tmpInfo->level());
rec.setValue(tabModel->fieldIndex("Material"),tmpInfo->material());
rec.setValue(tabModel->fieldIndex("Meme"),tmpInfo->info());
tabModel->insertRecord(tabModel->rowCount(),rec); //插入数据模型的最后
selModel->clearSelection();
QModelIndex curIndex = tabModel->index(tabModel->rowCount()-1,1);
selModel->setCurrentIndex(curIndex,QItemSelectionModel::Select);//设置当前行
showRecordCount();
ui->actSave->setEnabled(true); //使能保存按钮
}
构造函数分为两部分,一是创建DesignVec对象,并将DesignVec的信号finishADesign,与槽函数do_addDesign连接。二是设计tableView的属性。
一个设计完成后,应该将设计信息显示到左侧的“设计信息”中。这里利用了DesignVec的infoFromIndex接口获取对应的设计信息。selModel要更新当前选择的设计(最新的一个)。
主界面设计的关键部分就这些。其它操作的实现,如显示“不同设计”,直接参考源代码即可。
设计表类介绍
DesignVec类可以说是本项目中最关键的部分,充分体现了面向对象的设计思想。DesignVec类的存在使得主界面类不需要关心设计类是如何工作的,主界面只需要调用对应的接口即可。
具体实现
class DesignVec : public QObject
{
Q_OBJECT
private:
QList<Design*> m_vec; //使用多态,以容器储存设计类指针
public:
explicit DesignVec(QObject *parent = nullptr);
QString showInfo(int index); //显示单行设计信息
QStringList showAll(); //显示所有设计信息
void addDesign(int type); //添加设计
void deleteDesign(int index); //删除设计
void deleteAll(){m_vec.clear();} //删除所有设计
DesignInfo* infoFromIndex(int index); //返回设计信息指针
enum DesignType {TGearDesign, TAxleDesign, TBearingDesign, TKeyDesign};//枚举变量
signals:
void finishADesign(int index); //完成一个设计后,自动发送该信号
private slots:
void do_finishADesign(); //对应的槽函数
};
Design * tmpDesign = nullptr;
switch(type){
case TGearDesign:
tmpDesign = new GearDesign(this);break;
case TAxleDesign:
tmpDesign = new AxleDesign(this);break;
case TKeyDesign:
tmpDesign = new KeyDesign(this);break;
case TBearingDesign:
tmpDesign = new BearingDesign(this);break;
default:
break;
}
if(tmpDesign != nullptr){
m_vec.append(tmpDesign); //添加设计对象
//建立连接
connect(tmpDesign,&Design::finishADesign,this,&DesignVec::do_finishADesign);
tmpDesign->startDesign(); //启动设计UI
}
这里定义了一个枚举值:DesignType,方便调用addDesign函数。还定义了一个QList对象m_vec,用来统一管理所有的设计类,元素类型为Design*。为什么能这么做?因为不同的设计类都继承自Design类,实现了类型统一。
因为使用了继承,所有每一个设计类都有一个相同的公共接口,DesignVec类只需调用接口即可,无需关心操作的到底是何种设计。这里用到了对接口编程,而不是对实现编程,当然,也用到了多态,理解成动态绑定也行。
主界面中,一个设计完成后,应该将设计信息显示到左侧的“设计信息”中,利用DesignVec的infoFromIndex接口获取了对应的设计信息。但DesignVec如何知道哪个设计完成了呢?可以利用观察者模式。让DesignVec成为观察者。这里我使用的是Qt的信号与槽机制。通过sender函数获取信号的发送者。再利用循环得出信号发送者的在DesignVec中的位置。
void DesignVec::do_finishADesign()
{//需要知道信息发送者的在设计表中的位置
Design* object = static_cast<Design*>(sender()); //信号发送者
int index = -1; //对应的位置
for(int i = 0; i < m_vec.count();i++){
if(object == m_vec.at(i)){
index = i;
break;
}
}
emit finishADesign(index); //发送完成一个设计的信号
}
设计类介绍
class Design : public QObject{ //抽象基类
Q_OBJECT
protected:
DesignInfo* m_info; //设计信息
DesignUI* m_UI; //设计UI
signals: //发送信号
void finishADesign(); //完成设计
public slots:
void do_finishDesign();
public:
explicit Design(QObject *parent = nullptr);
virtual void startDesign() = 0; //开始设计,纯虚函数
virtual DesignInfo* info() const {return nullptr;}//返回设计信息
};
//设计UI类
class DesignUI : public QMainWindow{
Q_OBJECT
protected:
DesignInfo* m_info;
signals:
void finishDesign(); //完成设计信号
public:
explicit DesignUI(QWidget *parent = nullptr);
~DesignUI();
virtual void startDesign(DesignInfo* info){} //两个虚函数
protected:
virtual void reFresh(){}
};
Design中有两个对象DesignUI,和DesignInfo。DesignUI负责显示UI和计算参数,DesignInfo负责保存设计结论,提供公共接口startDesign。内部启动UI对象。因为Design类是抽象基类,所以子类必须实现自己的startDesign函数。DesignUI中有一个DesignInfo对象,Design类中又有一个DesignInfo对象,这是为啥?主要是因为主界面显示设计时,需要用到DesignInfo对象,但主界面类不应直接操作DesignUI类。如果Design类给DesignUI传入一个DesignInfo对象,启动设计。DesignVec再从Design类中获取DesignInfo对象即可解决这个问题。
如果创建了一个新的设计类,例如GearDesign,只需要重新实现一个UI类,如果保存不同格式的设计信息,实现一个Info类即可。这里把一个Design要进行的工作交给DesignUI和DesignInfo实现,Design类不必关心他们是如何实现的,只需调用对应接口。这样一来,程序就变得很灵活了。所使用的就是策略模式。
DesignInfo的实现非常简单,这里就不贴出代码了。其它设计类的代码也很简单,因为我把不同设计的重点都放在DesignUI中了。当然还有图表的显示。
有些设计需要等待用户输入完一些参数后才能继续下一步操作。那如何知道用户输完信息没?我一开始的设想是用一个while循环判断一个bool变量,用户按下按钮后更改bool变量,进行下一步。但实际操作中,UI界面没有出现,Debug后发现是线程卡在while循环中了,用多线程可以解决这个问题,但有点杀鸡用牛刀的嫌疑了。最后的实现是把一部分操作放到按钮对应的槽函数中。点击按钮,继续进行设计。
后记
源代码的介绍就到此为止了。UI界面的设计其实也是一个很重要的部分,因为关系到软件好不好使用。熟练使用Qt Designer是关键。
2024/9/5 于长沙