技术: Qt 实践(汽车销量管理)

熟悉客户端 GUI 编程, 一个 Qt 综合小案例。(2018年 重新整理了一下)

简介

本练习参考&改造于 电子工业出版社, 第16章 “汽车销售管理系统”。
(配套书籍中讲的非常简单,核心代码给与了实现,但是实际上跑起来各种出错)

目的,纯练手或者作为新手的练习项目,至于为啥选”汽车”主题,可能兴趣使然吧.

涉及到的技术:

  • 多窗口切换
  • stacked widget/Tool Box容器
  • 级联菜单
  • Qt数据库操作(mysql)
  • XML读写
  • 界面设计, 布局, 图像绘制(直方图)
  • 自定义控件等等

功能简单,代码量不多,重点也不在业务逻辑上(汽车销售业务的子集),中间穿插了其他的事情,不是一口气写完的。
总之 just for fun。

环境配置

准备数据

可以用代码先导入数据,我这边儿还是直接用 sql 语句直接导入了数据,方便。

sql 语句: (没有使用外键约束,人为控制)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CREATE DATABASE IF NOT EXISTS qt_car DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

USE qt_car;

## 品牌表

CREATE TABLE IF NOT EXISTS brand(id int PRIMARY KEY AUTO_INCREMENT, name varchar(255)) ENGINE=InnoDB DEFAULT CHARSET=utf8;;

### 插入数据

INSERT INTO brand(name) values('宝马');
INSERT INTO brand(name) values('保时捷');
INSERT INTO brand(name) values('雷克萨斯');


## 产品表

CREATE TABLE IF NOT EXISTS product (id int PRIMARY KEY AUTO_INCREMENT, brand varchar(255), name varchar(255), price int, sum int, sell int, last int) ENGINE=InnoDB DEFAULT CHARSET=utf8;

### 插入数据

INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('宝马', '5系', 56, 50, 18, 32);
INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('宝马', '7系', 176, 90, 8, 82);
INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('宝马', '3系', 33, 90, 40, 50);
INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('保时捷', 'Panamera', 200, 30, 18, 12);
INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('保时捷', '911', 56, 33, 1, 32);
INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('保时捷', 'Macan', 58, 110, 11, 99);
INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('雷克萨斯', 'IS', 56, 50, 18, 32);
INSERT INTO product(brand, name, price, sum, sell, last) VALUES ('雷克萨斯', 'NX', 156, 50, 18, 32);

效果:

说明:也可以程序中操作数据库,不过一般开发,没人这么干

环境说明

macOS + Qt5.5 + Mysql5.7(远端数据库) + Clang-900.0.39.2

代码实现

主要功能切换都在主菜单上: 车辆管理 + 销售统计.

车辆管理

级联下拉框

spinBox下拉框联动

提交写入数据库, 取消撤销操作.

整体效果:

XML读写正常:

读写日志正常:

该部分的核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle("Merlin's Qt Car");

//一开始就可以锁定在第一个页面(直接调用槽函数)
on_actionCar_triggered();

//初始化界面的数据 "车辆管理"
//1.连接数据库
connectDB();
//2.初始化数据
initData();

//3.创建日志xml
DomXML::createXML("demo.xml");

//模拟添加
#if 0
QStringList list;
list << "宝马" << "5系" << "38" << "2" << "39";
DomXML::appendXML("demo.xml", list);
#endif

//初始化统计 Model
createChartModelView();
}

/*车辆管理*/
void MainWindow::on_actionCar_triggered()
{
//切换到车辆管理界面
ui->stackedWidget->setCurrentWidget(ui->car);
ui->label->setText("车辆管理");
}

/*销售统计*/
void MainWindow::on_actionCalc_triggered()
{
//切换到销售统计页面
ui->stackedWidget->setCurrentWidget(ui->calc);
ui->label->setText("销售统计");

//初始化图标数据界面
qDebug() << "显示统计界面";
showChart();
}

//连接数据库
void MainWindow::connectDB()
{
//添加数据库
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
db.setHostName("192.168.10.103");
db.setUserName("root");
db.setPassword("admin");
db.setDatabaseName("qt_car");

//打开数据库
if (!db.open()) { //失败的话
QMessageBox::warning(this, "错误警告", "打开数据库失败: "+db.lastError().text());
return;
}
}

//初始化数据
void MainWindow::initData()
{
//直接用 model 初始化
QSqlQueryModel *queryModel = new QSqlQueryModel(this);
queryModel->setQuery("select name from brand"); //模型里面设置 sql 语句, 并存储其运行结果

ui->comboBoxFactory->setModel(queryModel);
ui->lineEditTotal->setEnabled(false); //总价
}

//品牌下拉框槽函数
void MainWindow::on_comboBoxFactory_currentIndexChanged(const QString &arg1)
{
if (arg1 == "") {
ui->comboBoxBrand->clear();
ui->lineEditTotal->clear();
ui->lineEditTotal->clear();
ui->labelLast->setText("0");
ui->spinBox->setValue(0); //数量选择框
} else {
QSqlQuery sql;
QString sqlStr = QString("select name from product where brand ='%1'").arg(arg1);
sql.exec(sqlStr);

ui->comboBoxBrand->clear();// 先清空产品

while (sql.next()) {
QString productName = sql.value(0).toString();
ui->comboBoxBrand->addItem(productName);
}
}
}

//产品下拉框改变时
void MainWindow::on_comboBoxBrand_currentIndexChanged(const QString &arg1)
{
QSqlQuery sql;
QString sqlStr = QString("select price,last from product where brand ='%1' and name = '%2'")
.arg(ui->comboBoxFactory->currentText()).arg(arg1);
sql.exec(sqlStr);

ui->spinBox->setValue(0); //清空上次选择的数量
ui->btnSure->setEnabled(false); //提交按钮失效
ui->lineEditTotal->clear(); //清除上次计算的总价


//应该只有一行数据
while (sql.next()) {
//报价
int price = sql.value("price").toInt();
//剩余数量
int last = sql.value("last").toInt();

//更新 ui (单价, 以及剩余数量)
ui->lineEditPrice->setText(QString::number(price));
ui->labelLast->setText(QString::number(last));
}

}

/*spinBox 联动: 卖出的数量,剩余数量*/
void MainWindow::on_spinBox_valueChanged(int count)
{

QString brandStr = ui->comboBoxFactory->currentText(); //获取品牌
QString proStr = ui->comboBoxBrand->currentText(); //获取的车型

QSqlQuery query;
QString sql = QString("select last from product where brand = '%1' and name = '%2'")
.arg(brandStr).arg(proStr);

query.exec(sql);
int last;
while (query.next()) {
last = query.value("last").toInt(); //真正的剩余
}

int tmpNum = last - count;
//如果已经最大量了,就直接退出, ui->spinBox->setMaximum();
if (tmpNum < 0 ) {
QMessageBox::warning(this,"数量非法","请确保销量不超过剩余库存量");
ui->spinBox->setValue(last);
ui->labelLast->setText("0");
} else {
ui->labelLast->setText(QString::number(tmpNum));
}

//设置提交按钮可用
if(ui->spinBox->value()>0) {
ui->btnSure->setEnabled(true);
} else {
ui->btnSure->setEnabled(false);
}


//同时计算总价
int price = ui->lineEditPrice->text().toInt(); //报价
int cnt = ui->spinBox->value();
int sum = price * cnt;
ui->lineEditTotal->setText(QString::number(sum));

}

/*确定录入*/
void MainWindow::on_btnSure_clicked()
{
//获取信息
int cnt = ui->spinBox->value(); // 获取销售数量
int last = ui->labelLast->text().toInt(); //获取剩余数量

if (cnt <= 0 ) {
ui->btnSure->setEnabled(false);
QMessageBox::information(this,"提交错误", "提交数量非法");
return;
}

//获取数据库的销量
QSqlQuery query;
QString sql = QString("select sell from brand where brand='%1' and name='%2'")
.arg(ui->comboBoxFactory->currentText())
.arg(ui->comboBoxBrand->currentText());
query.exec(sql);

int sell;
while( query.next()) {
sell = query.value("sell").toInt();
}
//更新数据库,剩余数量, 销售总量
sell += cnt;

sql = QString("update product set sell=%1, last=%2 where brand='%3' and name='%4'")
.arg(sell)
.arg(last)
.arg(ui->comboBoxFactory->currentText())
.arg(ui->comboBoxBrand->currentText());
query.exec(sql);

//把确认后的数据更新到 XML 中
QStringList list;
list << ui->comboBoxFactory->currentText()
<< ui->comboBoxBrand->currentText()
<< ui->lineEditPrice->text()
<< QString::number(cnt)
<< ui->lineEditTotal->text();

DomXML::appendXML("demo.xml",list); //已经写入了XML

//把当日销量, 显示在 右边儿 textEdit 上
QStringList readList[5];
DomXML::readXML("demo.xml", readList);
//组装字符串,然后显示出来
for (int i=0; i< readList[0].size(); i++) {
QString str = QString("%1:%2: 卖出了%3, 单价:%4, 总价:%5")
.arg(readList[0].at(i))
.arg(readList[1].at(i))
.arg(readList[2].at(i))
.arg(readList[3].at(i))
.arg(readList[4].at(i));
qDebug() << str.toUtf8().data();
ui->textEdit->append(str); //显示
}


QMessageBox::information(this,"成功","数据已经入库");
//然后重新初始化一次
on_comboBoxBrand_currentIndexChanged(ui->comboBoxBrand->currentText());

}

//取消录入
void MainWindow::on_btnCancel_clicked()
{
//直接调用 initData()
//initData();

QString currentProduct = ui->comboBoxBrand->currentText();
on_comboBoxBrand_currentIndexChanged(currentProduct);

}

代码中,很多地方都直接忽略的错误处理。

销售统计

这里采用了两个自定义控件: 直方图.

以模块的方式组织进来的,可重用到其他项目.

直接在 第二个界面,提升组件为 “PieView” 即可,见下图。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*ui构造的时候就应该设置 model*/
void MainWindow::createChartModelView()
{
chartModel = new QStandardItemModel(this);
chartModel->setColumnCount(2);
chartModel->setHeaderData(1, Qt::Horizontal, QString("品牌"));
chartModel->setHeaderData(0, Qt::Vertical, QString("销售数量"));

//PieView *pieChart = new PieView;

ui->pieChart->setModel(chartModel);

QItemSelectionModel *selectModel = new QItemSelectionModel(chartModel);
ui->pieChart->setSelectionModel(selectModel);

}

/*菜单跳转的时候调用*/
void MainWindow::showChart()
{
QSqlQuery query;
query.exec(QString("select name, sell from product where brand='%1'")
.arg(ui->comboBoxFactory->currentText()));
chartModel->removeRows(0,chartModel->rowCount(QModelIndex()),QModelIndex());
int row = 0;

while(query.next()){
int r = qrand()%256;
int g = qrand()%256;
int b = qrand()%256;

chartModel->insertRows(row,1,QModelIndex());
chartModel->setData(chartModel->index(row,0,QModelIndex()),query.value(0).toString());
chartModel->setData(chartModel->index(row,1,QModelIndex()),query.value(1).toInt());
chartModel->setData(chartModel->index(row,0,QModelIndex()),QColor(r,g,b),Qt::DecorationRole);
row++;
}
}

/*销售统计*/
void MainWindow::on_actionCalc_triggered()
{
//切换到销售统计页面
ui->stackedWidget->setCurrentWidget(ui->calc);
ui->label->setText("销售统计");

//初始化图标数据界面
qDebug() << "显示统计界面";
showChart();

}

效果大致如下:

但是自定义 UI 是个技术活,需要为当前项目定制或者微调。不展开。

该控件的核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//.h文件中声明
private:
QItemSelectionModel *selections;
QList<QRegion> RegionList;

//.cpp代码中实现核心逻辑 (由于是在主界面绘制,所以重写 paintEvent)
void PieView::paintEvent(QPaintEvent *)
{
QPainter painter(viewport());
painter.setPen(Qt::black);
int x0=40;
int y0=250;
//y坐标轴
painter.drawLine(x0,y0,40,30);
painter.drawLine(38,32,40,30);
painter.drawLine(40,30,42,32);
painter.drawText(20,15,tr("销售数量"));
for(int i=1;i<5;i++)
{
painter.drawLine(-1,-i*50,1,-i*50);
painter.drawText(-20,-i*50,tr("%1").arg(i*5));
}
//x 坐标轴
painter.drawLine(x0,y0,540,250);
painter.drawLine(538,248,540,250);
painter.drawLine(540,250,538,252);
painter.drawText(445,240,tr("品牌"));

int pos=x0+60;
int row;
for(row=0;row<model()->rowCount(rootIndex());row++)
{
QModelIndex index=model()->index(row,0,rootIndex());
QString dep=model()->data(index).toString();

painter.drawText(pos,y0+20,dep);
pos+=50;
}
int posN=x0+60;
for(row=0;row<model()->rowCount(rootIndex());row++)
{
QModelIndex index=model()->index(row,1,rootIndex());
int sell=model()->data(index).toDouble();

int width=10;
QModelIndex colorIndex = model()->index(row,0,rootIndex());
QColor color = QColor(model()->data(colorIndex,Qt::DecorationRole).toString());
if(selections->isSelected(index))
painter.setBrush(QBrush(color,Qt::Dense3Pattern));
else
painter.setBrush(QBrush(color));

painter.drawRect(QRect(posN,y0-sell*5,width,sell*5));
QRegion regionM(posN,y0-sell*5,width,sell*5);
RegionList<<regionM;

posN+=50;
}
}

QRegion PieView::itemRegion(QModelIndex index)
{
QRegion region;
if (index.column() == 1) // 销售数量
region = RegionList[index.row()];
return region;
}

QModelIndex PieView::indexAt(const QPoint &point) const
{
QPoint newPoint(point.x(),point.y());
QRegion region;
foreach(region,RegionList) // 销售数量-列
{
if (region.contains(newPoint))
{
int row = RegionList.indexOf(region);
QModelIndex index = model()->index(row,1,rootIndex());
return index;
}
}
return QModelIndex();
}

小结

本来只是基于 电子工业出版社 这本书的练习代码,但是整个做下来。
几乎 Qt 关键的技术点都用到了, 收获还是不错的;至于离 Qt 高手的境界,还有很远的路呀。

btw: 那本书中错误不少呀。。。

如需完整代码,请邮件 gmail我。

文章目录
  1. 1. 简介
  2. 2. 环境配置
    1. 2.1. 准备数据
    2. 2.2. 环境说明
  3. 3. 代码实现
    1. 3.1. 车辆管理
    2. 3.2. 销售统计
  4. 4. 小结
|