本篇文章将会全面介绍我们“武汉大学建筑知识系统”的最后一个版本 1.5.1
。带领大家从头到尾梳理一遍我在写这个项目的思路顺序,因此在这篇文章将不会再详细介绍具体的实现方法,大家可以回到之前的文章来看每一部分的详细讲解,具体链接见文章末尾的传送门。
作为一款图片识别类的软件,我们需要提供的功能必须有识别用户上传图片的能力,以及要有对用户上传图片作出的反馈。其次为了丰富软件内容,我们增加了能让用户浏览系统中有的建筑信息的功能,让不熟悉武汉大学的用户通过该软件了解武汉大学的经典建筑,也为游客提供了来武汉大学欣赏游玩的地点。
因此对于这个版本,我们只有 3
个主要界面以及一个退出按钮, 3
个主要界面分别是识别、浏览以及关于界面,下面将分开介绍 3
个主要界面、退出按钮以及数据库。
那么识别、浏览、关于以及退出都用 pushButton
实现,放在一个菜单栏里面,位于整个窗口的左侧。
对于一款知识型软件,从名字就知道里面有很多知识,那么这些知识存在哪里呢?那就是数据库啦!数据库有很多,从 Excel
、txt
到 sqlite3
、MySQL
等等,那么为了简单,我们这里采用 Excel
存放原始数据,而在软件中用 sqlite3
进行操作(因为 sqlite3
对于数据的操作比 Excel
快)。
创建数据库
def buildDatabase():
"""
创建数据库:buildings.db
:return: NULL
"""
conn = sqlite3.connect('buildings.db') # 如果没有这个数据库,则创建这个数据库,然后连接数据库
cur = conn.cursor() # 对数据库操作就是对cur操作
# 数据库定义各属性(创建一个表)
cur.execute('''
CREATE TABLE IF NOT EXISTS buildings(
id INT not null,
name_pre TEXT not null,
name_cn TEXT not null,
category TEXT not null,
introduction TEXT not null,
location TEXT not null,
location_url TEXT not null,
PRIMARY KEY (id));''')
conn.commit() # 保存数据库(因为新建了一个表)
conn.close() # 关闭数据库
向数据库中插入数据
def insertValue(content):
"""
向数据库中插入内容
:param content: 是一个列表,为要插入的数据内容
:return: NULL
"""
conn = sqlite3.connect('buildings.db') # 连接数据库
cur = conn.cursor() # 对数据库进行操作
cur.execute('''SELECT COUNT(*) FROM buildings;''') # 计算当前数据库已有数据行数
row_count = int(cur.fetchone()[0]) # 返回当前数据库已有数据行数
if content[0] >= row_count: # 如果插入的数据不在数据库中(大于数据库行的数据),则允许插入
# 此处存在bug,即若数据在原数据excel中间插入,无法在新数据库中插入此条信息,而是插入最后一条信息(重复插入)
# 不过由于此数据库由我们自行创建,所以可以人为避免此情况发生(不清楚是否重复插入时,可以将.db文件删除,再运行此脚本,将产生新的正确的数据库)
# 插入 id, name_pre, name_cn, category, introduction, location, location_url
cur.execute('''INSERT INTO buildings(
id, name_pre, name_cn, category, introduction, location, location_url)
VALUES (?, ?, ?, ?, ?, ?, ?);''', content)
conn.commit() # 保存数据库(因为新建了一个表)
conn.close() # 关闭数据库
查找数据
def gainContent_name_pre(name_pre):
"""
获取数据库内容
:param name_pre: 要查询的建筑预测名
:return: contents,一个列表(要查询的建筑在数据库中的所有信息)
"""
run()
conn = sqlite3.connect('buildings.db')
cur = conn.cursor()
cur.execute(f'''
SELECT * FROM buildings
WHERE name_pre = '{name_pre}';''')
contents = cur.fetchone()
conn.close()
return contents
def run():
"""
每次查询前都要确保数据库存在且是最新数据
:return: NULL
"""
buildDatabase()
df = pd.read_excel('buildings.xlsx', engine="openpyxl") # 读取建筑信息
for i in range(len(df)):
value = [int(df.at[i, 'id']), df.at[i, 'name_pre'], df.at[i, 'name_cn'],
df.at[i, 'category'], df.at[i, 'introduction'], df.at[i, 'location'], df.at[i, 'location_url']]
insertValue(value)
注:再次强调,这里的代码是每次运行软件都会从 Excel
中读取数据,那么其实这里是浪费时间的。但由于是课程项目,所以我会把所有操作都写上来,但在实际开发应用过程中,用户只能看到 .db
的数据库文件,即这里的插入数据操作都不存在(插入数据操作应该是软件发布前就完成的),只有查询操作!
接下来将讲解各界面的搭建思路以及业务代码实现,那么下面给出在 designer
中左侧菜单栏的设计样式:
这种布局也是很多电脑软件或者网页的一种布局形式!
同学们可能自己在 designer
设计的时候背景是透明的,不像我的是灰黑色的,其实我是对于这个 frame
添加了 QSS
样式,QSS
代码如下:
QFrame { background-color: rgba(85, 85, 85, 255); color: rgba(85, 85, 85, 255);}
这里用的 rgba
而不是 rgb
,是因为 rgba
多了一个可以设置透明度的参数,即 rgba(r,g,b,透明度)
,四个参数的设置范围都是 0-255
,透明度数值越底表示约透明,这里其实可以用 rgb
代替,因为我设置的透明度是 255
,相当于完全不透明,而 rgb
默认就是完全不透明的!
另外这份 QSS
代码其实只要设置 background-color
就可以,后面的 color
可以删除。
注:大家可能有时候无法在图层上面右键进行改变样式表操作,那么可以在对象查看器里面右键操作,这样就不怕因为布局设定好后无法锁定到要修改的控件上面了!
而对于按钮也可以设置 QSS
样式,这样让我们界面更好看一些,而按钮基本上有 3
种模式:按下、悬浮以及当前(选中 / 未选中)状态。
所以选中相应的 QSS
样式代码为:
QPushButton { font:14pt "Agency FB";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}
QPushButton:hover { background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}
QPushButton:pressed { font:14pt "Agency FB";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}
未选中相应的 QSS
样式代码为:
QPushButton { font:14pt "Agency FB";background-color: rgba(34, 34, 34, 0); color: rgb(255, 255, 255);}
QPushButton:hover { background-color: rgba(68, 68, 68, 255); color: rgb(255, 255, 255);}
QPushButton:pressed { font:14pt "Agency FB";background-color: rgba(69, 139, 116, 150); color: rgb(255, 255, 255);}
对于识别界面,我们的主要功能就是提供一个地方给用户上传图片文件并将此图片保存下来供模型预测使用,此时我们将用 pushButton
作为上传图片的载体。那么用户上传完图片后就要进行预测,预测的信息我们将会用 textBrowser
来展示文字信息,而图片信息则要用到 Label
来展示。那么预测信息显示后,用户可能对这个地方有兴趣,因此我们可以添加一个导航功能,那个这个功能载体我们同样用到 pushButton
来实现。这样就能得到一个基本的布局了,如下图所示:
那么界面设计好了,那就是功能的实现啦,下面将分别介绍上传图片、预测并显示预测结果以及导航的代码怎么写。
上传图片
前面我们也讲过了,用 PyQt5
的 PyQt5.QtWidgets.QFileDialog.getOpenFileName
就可以获取用户文件了,但是我们需要判断用户上传的文件是不是图片文件,所以在得到用户上传的图片之后,我们需要判别文件类型,最后就是要将此图片保存下来(这样我们之后预测就可以从指定路径获取图片文件,不过还有一种方法就是不保存用户图片,而是记录用户上传图片的所在路径,但是这有个弊端,就是用户可能上传过程中将此文件删除或者转移,那么就会出现 ,而且我们在初始化时由于用户还没有上传图片,所以我们只能从指定路径中获取图片显示,即“用户未上传图片”)。bug
,所以我并不建议使用此方法
同时在预测过程中是需要一定时间的,所以在 loading
过程中,我们要给预测图片添加加载动图,于是代码如下:
class QmyWidget(QWidget):
......
# 在识别界面打开图片
@pyqtSlot()
def on_btn_openimage_identify_clicked(self):
filename, _ = QFileDialog.getOpenFileName(self, '打开文件', QDir.currentPath())
if filename:
image_reader = QImageReader(filename)
image_reader.setAutoTransform(True) # 启用自动旋转
image = image_reader.read()
if image.isNull():
QMessageBox.information(self, '打开图片', '不能加载文件%s.\n请打开图片文件!如后缀为.jpg .png的文件。' % filename)
return
filePath = os.getcwd()
print(f'预测图片已保存({filePath}' + '\\images\\predict\\predict.jpg)')
image.save('.\\images\\predict\\predict.jpg', "JPG", 100)
# 更新用户上传图片
self.ui.label_userimage.setPixmap(QPixmap("ui\\../images/predict/predict.jpg"))
# 更新预测图片
# self.ui.label_pre.setPixmap(QPixmap("ui\\../images/background/loading.png"))
self.movie = QMovie("./images/background/loading.gif")
self.ui.label_pre.setMaximumSize(QSize(124, 124))
self.ui.label_pre.setMovie(self.movie)
self.movie.start()
预测并显示预测结果
图片上传成功后,接下来就是预测了。因为我们不清楚预测时间要多久,所以以防万一(防止宕机)我们用子线程来完成预测操作,主线程来维持 UI
界面的运行。
那么子线程预测操作与用户上传图片代码结合如下:
class QmyWidget(QWidget):
......
# 在识别界面打开图片
@pyqtSlot()
def on_btn_openimage_identify_clicked(self):
filename, _ = QFileDialog.getOpenFileName(self, '打开文件', QDir.currentPath())
if filename:
image_reader = QImageReader(filename)
image_reader.setAutoTransform(True) # 启用自动旋转
image = image_reader.read()
if image.isNull():
QMessageBox.information(self, '打开图片', '不能加载文件%s.\n请打开图片文件!如后缀为.jpg .png的文件。' % filename)
return
filePath = os.getcwd()
print(f'预测图片已保存({filePath}' + '\\images\\predict\\predict.jpg)')
image.save('.\\images\\predict\\predict.jpg', "JPG", 100)
# 更新用户上传图片
self.ui.label_userimage.setPixmap(QPixmap("ui\\../images/predict/predict.jpg"))
# 更新预测图片
# self.ui.label_pre.setPixmap(QPixmap("ui\\../images/background/loading.png"))
self.movie = QMovie("./images/background/loading.gif")
self.ui.label_pre.setMaximumSize(QSize(124, 124))
self.ui.label_pre.setMovie(self.movie)
self.movie.start()
# 删除导航网址
global url
url = ''
# 显示文字 --> 预测中...
_translate = QCoreApplication.translate
self.ui.pre_text.setHtml(_translate("Form",
"\n"
"\n"
"预测中,请稍后...
"))
# 开启子线程完成图片预测
print("开启子线程")
self.workThread = WorkThread()
self.work()
def work(self):
# 子线程开始
self.workThread.start()
# 当获得循环完毕的信号时,停止计数
self.workThread.trigger.connect(self.timeStop)
def timeStop(self):
print(f'更改图片: {pre_name}\n')
# 运行数据库查找功能(通过建筑名找它的图片位置 以及 导航网址 以及 建筑信息)
# --------------------------------------
global content
content = gainContent_name_pre(pre_name)
# content = gainContent_name_pre(pre_name[:-1])
# --------------------------------------
# 更改图片
self.ui.label_pre.setPixmap(QPixmap(f"./images/buildings/{content[1]}.jpg"))
self.ui.label_pre.setMaximumSize(QSize(711, 400))
# 更新导航网址
global url
url = content[6]
# 更新预测建筑的信息
# _translate = QCoreApplication.translate
# self.ui.pre_text.setHtml(_translate("Form",
# "\n"
# "\n"
# f"建筑名:{content[2]}
\n"
# f"类 别:{content[3]}
\n"
# "简 介:
\n"
# f" {content[4]}
\n"
# f"地 址:{content[5]}
"))
self.ui.pre_text.setText("\n"
f"建筑名:{content[2]}
\n"
f"类 别:{content[3]}
\n"
"简 介:
\n"
f" {content[4]}
\n"
f"地 址:{content[5]}
")
class WorkThread(QThread):
trigger = pyqtSignal()
def __int__(self):
super(WorkThread, self).__init__()
def run(self):
print('开始预测!')
# 接入模型预测
global pre_name
pre_name = pre()
# 循环完毕后发出信号
print(f'预测结束,预测建筑为: {pre_name}')
self.trigger.emit()
导航
由于一开始用户并未预测,所以导航应该是不能用的,那么之前提到过几种解决方法,分别是:
(1)方法一:导航按钮要设置不可用,同学们可自行实现,我给的源代码用的是方法二。那么按钮怎么设置不可用呢,见下面代码:
self.ui.btn_navigate_identify.setVisible(False) # 直接隐藏按钮
或者
self.ui.btn_navigate_identify.setEnabled(False) # 按钮看得见,但是无法点击
(2)方法二,直接定义一个全局变量来存放 url
,初始值为空,那么 QDesktopServices.openUrl(_url)
操作就不会打开任何网页啦~
从预测的代码那里可以看到在预测结束后就更新了导航的链接地址,所以下面直接展示导航按钮的跳转代码:
class QmyWidget(QWidget):
......
# 在识别界面打开导航
@pyqtSlot()
def on_btn_navigate_identify_clicked(self):
_url = QUrl(url)
QDesktopServices.openUrl(_url)
那么这里也给出上传图片以及导航的 QSS
样式表:
QPushButton {
background-color: #ffffff;
border: 1px solid #dcdfe6;
padding: 10px;
border-radius: 20px;
}
QPushButton:hover {
background-color: #ecf5ff;
color: #409eff;
}
QPushButton:pressed, QPushButton:checked {
border: 1px solid #3a8ee6;
color: #409eff;
}
对于浏览界面,是不是其实和识别界面差不多,就是有一张图片显示建筑照片,还有一个文本框显示建筑文字信息,要有一个滑动框显示所有的建筑按钮,最后也要有个导航按钮,那么下面展示在 designer
中设计的浏览界面样式:
那么滑动框的按钮添加以及点击滑动框内按钮的信号链接代码如下:
class QmyWidget(QWidget):
......
def __init__(self, parent=None):
......
# 添加按钮
buildings_name_pre = gain_name_pre()
for name in buildings_name_pre:
self.ui.listWidget.addItem(name)
# 添加信号
self.ui.listWidget.itemClicked.connect(self.clicked)
self.ui.listWidget.item(0).setSelected(True) # 默认选择第一个按钮
def clicked(self, item):
global browse_name
browse_name = item.text()
# _translate = QCoreApplication.translate
content = gainContent_name_pre(item.text())
self.ui.label_browse.setPixmap(QPixmap(f"./images/buildings/{content[1]}.jpg"))
# self.ui.textBrowser.setHtml(_translate("Form",
# "\n"
# "\n"
# f"建筑名:{content[2]}
\n"
# f"类 别:{content[3]}
\n"
# "简 介:
\n"
# f" {content[4]}
\n"
# f"地 址:{content[5]}
"))
self.ui.textBrowser.setText("\n"
f"建筑名:{content[2]}
\n"
f"类 别:{content[3]}
\n"
"简 介:
\n"
f" {content[4]}
\n"
f"地 址:{content[5]}
")
而导航就简单了,只需要获取当前按钮对于的建筑信息中的 url
就可以了,代码如下:
# 在浏览界面打开导航
@pyqtSlot()
def on_btn_navigate_browse_clicked(self):
name = browse_name
_url = QUrl(gainContent_name_pre(name)[6])
QDesktopServices.openUrl(_url)
这个界面主要是写一些对于我们这款软件的一些注意事项、开发团队、更新日志等内容,由大家自己决定,甚至可以删除此页面!
那么界面及每个界面的功能都实现了,最后就是菜单栏的按钮和右侧的 3
个界面的跳转以及退出按钮的实现了,代码也很简单,如下:
# 进入识别界面
@pyqtSlot()
def on_btn_identify_clicked(self):
self.btn_style_init()
self.ui.btn_identify.setStyleSheet(
"QPushButton { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:hover { background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:pressed { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}")
self.ui.stackedWidget.setCurrentIndex(0)
# 进入浏览界面
@pyqtSlot()
def on_btn_browse_clicked(self):
self.btn_style_init()
self.ui.btn_browse.setStyleSheet(
"QPushButton { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:hover { background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:pressed { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}")
self.ui.stackedWidget.setCurrentIndex(1)
# 进入关于界面
@pyqtSlot()
def on_btn_about_clicked(self):
self.btn_style_init()
self.ui.btn_about.setStyleSheet(
"QPushButton { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:hover { background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:pressed { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}")
self.ui.stackedWidget.setCurrentIndex(2)
# 退出界面
@pyqtSlot()
def on_btn_quit_clicked(self):
self.btn_style_init()
self.ui.btn_quit.setStyleSheet(
"QPushButton { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:hover { background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}\n"
"QPushButton:pressed { font:14pt \"Agency FB\";background-color: rgba(69, 139, 116, 255); color: rgb(255, 255, 255);}")
filePath = '.\\images\\background\\predict.png'
image = QImage(filePath)
image.save('.\\images\\predict\\predict.jpg', "JPG", 100)
self.close()
当然由于这个项目是我只花了一个月时间从不知道 PyQt5
是什么到使用此框架开发出“武汉大学建筑知识系统”的,所以里面的代码还是存在很多可以优化的地方的,这里也给出我认为可以优化的一些地方供同学们自行练习:
(1)将左侧菜单栏的一个个 pushButton
用我之前提到的滑动框来实现,从而去除按钮切换时需要改变的 style
代码;
(2)修改控件 QSS
样式或者重构系统界面,让界面更好看;
(3)尝试自定义控件,减少代码里面相同控件的样式设置代码;
(4)由于导航需要消耗用户流量,所以可以在使用前发出弹窗告知用户,或者在导航按钮旁边再加一个 checkBox
按钮来记录用户是否运行联网导航;
(5)将地图嵌入到系统当中,不用额外打开用户浏览器网页。
以上是我个人觉得可以优化的地方,如何同学们有其它好想法可以在评论区指出,供大家一同学习!
最后给出前面几篇介绍“武汉大学建筑知识系统”的文章,因为这篇文章是基于前面的文章写的,所以这篇文章没看懂的同学可以看看前面的前置文章,了解到相关的思路以及实现方法再来看我们这篇文章就会很轻松了!
【PyQt5 实战项目1】武汉大学建筑知识系统–思路分享1(总体概述)
【PyQt5 实战项目1】武汉大学建筑知识系统–思路分享2(软件版本1.0.0介绍之打开图片)
【PyQt5 实战项目1】武汉大学建筑知识系统–思路分享3(软件版本1.0.0介绍之显示预测的建筑信息、图片)
【PyQt5 实战项目1】武汉大学建筑知识系统–思路分享4(软件版本1.1.2介绍之新界面搭建与界面跳转)
【PyQt5 实战项目1】武汉大学建筑知识系统–思路分享5(如何用代码修改图片与文字以及数据库的使用)
【PyQt5 实战项目1】武汉大学建筑知识系统–思路分享6(优化浏览界面以及为我们的控件添加QSS样式)
【PyQt5 实战项目1】武汉大学建筑知识系统–思路分享7(导航设置、多线程预测以及一些零碎的小知识点)
那么本篇文章到这里就结束啦,“武汉大学建筑知识系统”的分享也到这里就结束了,希望大家都能够拿到一个好分数,再见~