Python+Pyqt5开发设备模拟测试工具
本文分享一个自己学习pyqt5时写的一个C端设备模拟工具前端部分如何实现的。基于python3+pyqt5开发
目录
目录
工具介绍
测试工具
工具开发背景
测试一个设备接入平台,主要接入iot设备,多个上层应用团队涉及到设备控制、设备数据上报等功能,公司iot设备数量不够,需要通过工具模拟对应的设备方便开发团队进行相关功能调试。
工具设计
工具主要包含四个界面,基础配置页,设备详情页,工具设备页,工具说明页
基础配置页面要为填写mqtt IP,端口等输入栏和一个选择设备的图片滚动列表,进入工具便是基础配置页面,设置mqtt连接信息和要模拟哪些设备,点击开始,跳转到设备信息页面。
设备详情页面主要为展示设备模拟的设备和每个设备的基础信息,设备能上报的事件列表,设备实时收到和发送的消息。点击设备显示基础配置页选中的要模拟的设备,点击设备列表可以切换显示不同设备的设备详情,设备信息显示设备名、设备coede、id、保活时间等信息,点击连接后该设备会正式激活连接到设备接入平台,事件列表显示设备支持上报的事件,点击对应事件可以对应事件的json消息填充到聊天输入框,点击发送即可将对应消息发送出去,实时消息框显示该设备上报的消息和收到的消息,靠左的为设备上报的消息,靠右的为设备收到的消息。
工具设置页面主要用来填写一些工具通用的设置项。
工具说明页面主要用来显示工具使用文档。
实际使用场景
工具启动便是基础配置页面,设置mqtt连接信息和要模拟哪些设备,点击确定,调整设备详情页面,然后根据之前选中要模拟的设备生成对应的设备详情页面,点击设备详情页的连接按钮,设备就会模拟生成到对应的环境,所有设备的事件以及对应的消息mqtt格式都是工具中预设好的
系统主界面
框架目录
- image: 存放设备素材图片
- iot_class: 存放预设好的设备定义数据以及自行封装的各种组件
- pyqt: 工具程序
主界面思路
class MyWindow(QMainWindow):
"""主程序 工具窗口类"""
dev_list = [] # 程序要模拟的设备列表,记录程序运行过程中模拟的设备
bakdev_list = [] # 设备列表备份,后续用来进入设备详情页时判断选择的设备是否有变化,有变化需要重新生成设备详情页
chatList = [] # 设备详情页,聊天框对象列表,方便统一处理聊天消息宽度
pushbt = [] # 设备详情页,发送事件,方便窗口放大缩小时重新设置按钮位置靠右下角
imagelist = [] # 基本配置页 设备图片对象的列表,方便后续统一处理设备图片对象的选择标志位
flag_device = 0 # 基本配置页 全选标志位,标志点击全选按钮时效果为全选还是取消全选
flag_revise = 0 # 设备详情页 聊天框是否调整宽度标志位
def __init__(self):
super(QMainWindow, self).__init__()
self.setWindowTitle('IOT设备模拟工具')
size = self.geometry() # 设置主窗口显示在屏幕中心位置
screen = QDesktopWidget().screenGeometry()
self.setGeometry(int((screen.width() - size.width()) / 2), int((screen.height() - size.height()) / 2), 850, 600)
self.initUI()
def initUI(self):
"""主界面"""
# print(self.width(),self.height())
self.mainWidget = QWidget()
self.mainlayout = QHBoxLayout()
self.mainWidget.setLayout(self.mainlayout)
enum_qwidget = self.create_enum_qwidget() # 调用类方法生成左侧菜单栏
self.mainlayout.addWidget(enum_qwidget)
content_qwidget = QWidget() # 基础配置界面
self.mainlayout.addWidget(content_qwidget)
"""基础配置页"""
self.basecfgWidget = QWidget()
self.basecfgWidget.setLayout(self.baseConfig()) # 调用类方法生成基础配置页
"""设备信息页"""
self.deviceinfoWidget = QWidget()
self.deviceinfoWidget.setLayout(self.deviceInfo()) # 调用类方法生成基础配置页
"""系统设置页"""
self.sysConfigWidget = QWidget()
self.sysConfigWidget.setLayout(self.sysConfig()) # 调用类方法生成基础配置页
"""工具说明"""
self.describeWidget = QWidget()
self.describeWidget.setLayout(self.toolDescribe()) # 调用类方法生成基础配置页
"""系统主框架,四个qwidget搭载对应布局进行切换"""
self.layoutqueque = QStackedLayout(content_qwidget) # 生成堆栈布局,实现页面切换
self.layoutqueque.addWidget(self.basecfgWidget)
self.layoutqueque.addWidget(self.deviceinfoWidget)
self.layoutqueque.addWidget(self.sysConfigWidget)
self.layoutqueque.addWidget(self.describeWidget)
# 设置中心界面
self.setCentralWidget(self.mainWidget)
系统框架为一个横向布局,分为左侧菜单栏部分和右侧内容部分
左侧菜单栏,采用纵向布局,添加基础配置、设备信息、系统设置、工具说明四个跳转按钮
右侧内容栏,采用堆栈布局,堆栈中放置了四个页面,通过左侧菜单栏按钮点击来跳转页面
基础配置页
def controlDevlist(self, devicename, flag):
"""控制设备要模拟的设备列表,根据选择的设备进行增加或者删除"""
if flag == 1:
for i in devicelist:
if i["name"] == devicename:
self.dev_list.append(i)
else:
for i in devicelist:
if i["name"] == devicename:
self.dev_list.remove(i)
def basecfg_devicebutton(self):
"""基础配置-全选设备"""
if self.flag_device == 0:
list(map(lambda x: x.setStyleSheet("border:4px solid LawnGreen;"), self.imagelist))
for i in self.imagelist: # 设备图片控件标志位,标志位为1再次点击不会有绿色边框的选择效果
i.flag = 1
self.dev_list = devicelist # 程序要模拟的设备列表,全部
#list(map(lambda x:self.choosedev_list.append(x), imagename))
self.flag_device = 1 # 全选标志位,再次点击为取消全选
else:
list(map(lambda x: x.setStyleSheet("border:4px solid transparent;"), self.imagelist))
for i in self.imagelist: # 设备图片控件标志位,标志位为0再次点击会有绿色边框的选择效果
i.flag = 0
self.dev_list = [] # 程序要模拟的设备列表,情况
self.flag_device = 0 # 全选标志位,再次点击为全选
def baseConfig(self):
"""基础配置"""
basecfg_layout = QVBoxLayout() # 右侧详情布局
basecfg_layout.setSpacing(0) # 间距为0
basecfg_layout.setContentsMargins(0, 0, 0, 0) # 设置边距
"""开始按钮"""
startbt_qwidget = QWidget()
# startbt_qwidget.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
startbt_qwidget.setMaximumHeight(50)
testlayout = QHBoxLayout()
testlayout.setAlignment(Qt.AlignRight)
startbt = QPushButton("开始")
startbt.setFixedSize(100, 30)
startbt.clicked.connect(lambda: self.tip(flag=False)) # 开始按钮点击事件信号连接到显示tips函数
testlayout.addWidget(startbt)
startbt_qwidget.setLayout(testlayout)
"""连接信息"""
connect_qwidget = QWidget()
# connect_qwidget.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
connect_qwidget.setMaximumHeight(180)
basecfg_content_connect_layout = QVBoxLayout()
connec_text_layout = QHBoxLayout() # 文字部分
connec_text_layout.setAlignment(Qt.AlignCenter)
connect_text = QLabel("连接信息")
connec_text_layout.addWidget(connect_text)
form_layout = QFormLayout() # 连接信息部分
connecname = QLabel("连接地址")
connecnameEdit = QLineEdit("")
mqttip = QLabel("MqttIP")
mqttipEdit = QLineEdit("")
mqttport = QLabel("Mqtt端口")
mqttportEdit = QLineEdit("")
mqtttopic = QLabel("MqttTopic")
mqtttopicEdit = QLineEdit("")
form_layout.addRow(connecname, connecnameEdit)
form_layout.addRow(mqttip, mqttipEdit)
form_layout.addRow(mqttport, mqttportEdit)
form_layout.addRow(mqtttopic, mqtttopicEdit)
basecfg_content_connect_layout.addLayout(connec_text_layout)
connect_blank = QWidget()
connect_blank.setFixedHeight(10)
basecfg_content_connect_layout.addWidget(connect_blank)
basecfg_content_connect_layout.addLayout(form_layout)
connect_qwidget.setLayout(basecfg_content_connect_layout)
"""设备选择"""
device_qwidget = QWidget()
device_qwidget.setMaximumHeight(190)
# device_qwidget.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
basecfg_content_device_layout = QVBoxLayout() # 设备选择布局
basecfg_content_device_layout.setSpacing(0)
device_text_layout = QHBoxLayout() # 文字部分
device_text_layout.setAlignment(Qt.AlignCenter)
device_text = QLabel("设备选择")
device_text_layout.addWidget(device_text)
device_button_layout = QHBoxLayout() # 全选按钮
device_button_layout.setAlignment(Qt.AlignRight)
device_button = QPushButton("全选")
device_button.setFixedSize(100, 30)
device_button_layout.addWidget(device_button)
deviceQWidget = QWidget() # 设备滚动选择控件
deviceQWidget.setMinimumSize(990, 108)
for i in range(len(imagepath)):
image = frameLabel(parent=deviceQWidget, tips=imagename[i])
image.setPixmap(QPixmap(imagepath[i]).scaled(100, 100))
image.move(i * 110, 0)
image.signal.connect(self.controlDevlist) # 将组件frameLabel的信号连接到主程序类的要模拟设备列表的增删控制函数
self.imagelist.append(image) # 将所有图片对象存入一个列表,方便批量处理
device_button.clicked.connect(self.basecfg_devicebutton) # 将全选按钮信号连接全选函数上
scroll = QScrollArea() # 滚动区域
scroll.setFixedHeight(138)
scroll.setStyleSheet("border:4px solid transparent;")
scroll.setWidget(deviceQWidget)
#scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
#scroll.setWidgetResizable(False)
basecfg_content_device_layout.addLayout(device_text_layout)
basecfg_content_device_layout.addLayout(device_button_layout)
basecfg_content_device_layout.addWidget(scroll)
device_qwidget.setLayout(basecfg_content_device_layout)
"""加载到基础配置详情布局"""
basecfg_layout.addWidget(startbt_qwidget)
basecfg_blank_qwidget1 = QWidget() # 空白占位部件
basecfg_blank_qwidget1.setFixedHeight(20)
# basecfg_blank_qwidget1.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
basecfg_layout.addWidget(basecfg_blank_qwidget1)
basecfg_layout.addWidget(connect_qwidget)
basecfg_blank_qwidget2 = QWidget()
basecfg_blank_qwidget2.setFixedHeight(70)
# basecfg_blank_qwidget2.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
basecfg_layout.addWidget(basecfg_blank_qwidget2)
basecfg_layout.addWidget(device_qwidget)
basecfg_blank_qwidget3 = QWidget()
basecfg_layout.addWidget(basecfg_blank_qwidget3)
# basecfg_blank_qwidget3.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
"""加载到基础布局"""
return basecfg_layout
基础配置页面主要为填写mqtt IP,端口等输入栏和一个选择设备的图片滚动列表,可以设置相关连接信息和要模拟那几款设备,图片滚动列表分为两部分,一个pyqt自带的scroll组件和自己封装添加了鼠标点击事件的label组件组成设备选择滚动列表,通过for循环调用自己封装以后的label组件初始化一张张图片,然后嵌进一个qwidget中,最后将这个qwidget嵌进滚动组件scoll中,因为label本身没有点击事件,要实现点击图片要切换图片的边框颜色,绿色代表该设备已选中,所以只能自己封装label组件,然后重写鼠标点击事件
设备详情页
def set_eventjs(self, list, text):
"""设备信息页,设置上报事件内容"""
list[0].setText(text)
# self.update()
def deviceInfo(self):
"""设备信息"""
deviceinfo_layout = QHBoxLayout() # 设备信息整体布局
deviceinfo_layout.setContentsMargins(0, 0, 0, 0)
deviceinfo_devlist_qwidget = QWidget() # 设备列表
deviceinfo_devlist_qwidget.setFixedWidth(110)
# deviceinfo_devlist_qwidget.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
text = QLabel("设备列表", parent=deviceinfo_devlist_qwidget)
text.move(0, 0)
text.setFixedSize(150, 40)
pageIndex = 0
deviceinfo_device_qwidget = QWidget()
self.device_stackedLayout = QStackedLayout(deviceinfo_device_qwidget) # 新建堆栈布局,实现切换页面显示不同的设备详情
for i in self.dev_list: # 根据选择模拟的设备,创建设备详情界面
devicebutton = deviceLabel(parent=deviceinfo_devlist_qwidget, pageIndex=pageIndex)
devicebutton.setText(i["name"])
devicebutton.move(0, pageIndex*40+40)
devicebutton.setFixedSize(150, 40)
devicebutton.signal.connect(self.devicelist_click) # deviceLabel组件点击信号连接跳转设备详情页面函数上
self.device_stackedLayout.addWidget(self.create_deviceinfo(devicename=i["name"],deviceId=i["id"],
deviceCode=i["code"],event=i["event"])) #调用类方法生成不同的设备详情页面
pageIndex = pageIndex + 1
deviceinfo_layout.addWidget(deviceinfo_devlist_qwidget)
deviceinfo_layout.addWidget(deviceinfo_device_qwidget)
return deviceinfo_layout
def create_deviceinfo(self, devicename=None, deviceId=None, deviceCode=None, event=None):
"""设备信息-右侧设备详情页面"""
deviceinfo_device_qwidget = QWidget() # 设备详情
deviceinfo_device_layout = QVBoxLayout()
deviceinfo_device_info_qwidget = QWidget() # 设备基本信息
deviceinfo_device_info_qwidget.setFixedHeight(140)
deviceinfo_device_info_layout = QGridLayout()
deviceinfo_device_info_layout.setContentsMargins(0, 0, 0, 0)
deviceinfo_device_info_layout.addLayout(self.deviceinfo_info(text="设备名", default=devicename), 0, 0) # 调用类方法生成标题加输入栏布局
deviceinfo_device_info_layout.addLayout(self.deviceinfo_info(text="设备code", default=deviceCode), 0, 1)
deviceinfo_device_info_layout.addLayout(self.deviceinfo_info(text="设备id", default=deviceId), 1, 0)
deviceinfo_device_info_layout.addLayout(self.deviceinfo_info(text="保活时间", default="1800"), 1, 1)
deviceinfo_device_connec_layout = QHBoxLayout()
deviceinfo_device_connec_layout.setContentsMargins(0, 0, 0, 0)
deviceinfo_device_connecbt = QPushButton("连接")
deviceinfo_device_connecbt.setFixedSize(100, 30)
deviceinfo_device_connecbt.clicked.connect(lambda: self.tip(flag=True)) # 连接按钮点击事件信号连接到显示tips函数
deviceinfo_device_connec_layout.addStretch(1)
deviceinfo_device_connec_layout.addWidget(deviceinfo_device_connecbt)
deviceinfo_device_info_layout.addLayout(deviceinfo_device_connec_layout, 2, 1)
deviceinfo_device_info_qwidget.setLayout(deviceinfo_device_info_layout)
deviceinfo_device_control_layout = QHBoxLayout() # 设备控制
deviceinfo_device_control_eventlist_qwidget = QWidget() # 设备事件列表
deviceinfo_device_control_eventlist_qwidget.setFixedWidth(150)
text = QLabel("事件列表", parent=deviceinfo_device_control_eventlist_qwidget)
text.move(0, 0)
text.setFixedSize(150, 30)
eventjs = QTextEdit() # 设备事件内容显示区域
eventtemp = 0
eventBtList = []
if event:
for i in event: # 初始化设备事件
devicebutton = eventLabel(parent=deviceinfo_device_control_eventlist_qwidget, obj=eventjs)
devicebutton.setText(i["eventname"])
devicebutton.move(0, eventtemp*40+40)
devicebutton.setFixedSize(150, 40)
devicebutton.signal.connect(self.set_eventjs) # eventLabel组件点击信号连接到设置输入栏内容函数
eventBtList.append(devicebutton)
eventtemp += 1
deviceinfo_device_control_content_layout = QVBoxLayout() # 设备控制区域,显示指令、事件
deviceinfo_device_control_content_chatbox_qwidget = QWidget() # 显示收到的指令和发送的事件
deviceinfo_device_control_content_chatbox_qwidget.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
deviceinfo_device_control_content_chatbox_layout = QVBoxLayout()
deviceinfo_device_control_content_chatbox_layout.setContentsMargins(0, 0, 0, 0)
chatbox = MsgList() # 实时聊天消息显示框组件
chatbox.addTextMsg("Hello", True)
chatbox.addTextMsg("World!", False)
chatbox.addTextMsg("昨夜小楼又东风,春心泛秋意上心头,恰似故人远来载乡愁,今夜月稀掩朦胧,低声叹呢喃望星空,恰似回首终究一场梦,轻轻叹哀怨...", False) # 文字内容示例
chatbox.addTextMsg("With a gentle look on her face, she paused and said,她脸上带着温柔的表情,稍稍停顿了一下,便开始讲话", True)
chatbox.addTextMsg(
'{"num":"1","num2":"1","num6":{{"num3":"1","num4":"脸"}},{"num":"1","num2":"1","num6":{"num3":"1","num4":"1"}},'
'{"num3":"1","num4":"1"}},{"num":"1","num2":"1","num6":{"num3":"1","num4":"1","num3":"1","num4":"1"}},'
'{"num3":"1","num4":"1"}},{"num":"1","num2":"1","num6":{"num3":"1","num4":"脸","num3":"1","num4":"1"}},'
'{"num3":"1","num4":"1"}},{"num":"1","num2":"1","num6":{"num3":"1","num4":"1","num3":"1","num4":"1"}}'
'{"num3":"1","num4":"1"}},{"num":"1","num2":"1","num6":{"num3":"1","num4":"1","num3":"1","num4":"1"}},'
'{"num3":"1","num4":"1"}},{"num":"1","num2":"1","num6":{"num3":"1","num4":"1","num3":"1","num4":"1"}}}',
False)
self.chatList.append(chatbox) # 将所有实时消息框添加进列表,方便后续处理
deviceinfo_device_control_content_chatbox_layout.addWidget(chatbox)
deviceinfo_device_control_content_chatbox_qwidget.setLayout(deviceinfo_device_control_content_chatbox_layout)
deviceinfo_device_control_content_topic_qwidget = QWidget() # 显示事件发送topic
deviceinfo_device_control_content_topic_qwidget.setFixedHeight(40)
deviceinfo_device_control_content_topic_layout = QHBoxLayout()
deviceinfo_device_control_content_topic_qwidget.setLayout(deviceinfo_device_control_content_topic_layout)
topic = QLabel("topic:")
lineedit = QLineEdit()
lineedit.setText("iotHub")
deviceinfo_device_control_content_topic_layout.addWidget(topic)
deviceinfo_device_control_content_topic_layout.addWidget(lineedit)
deviceinfo_device_control_content_event_qwidget = QWidget() # 编辑发送事件消息
deviceinfo_device_control_content_event_qwidget.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
deviceinfo_device_control_content_event_qwidget.setFixedHeight(100)
deviceinfo_device_control_content_event_layout = QHBoxLayout()
deviceinfo_device_control_content_event_layout.setContentsMargins(0, 0, 0, 0)
eventjs.setFixedHeight(100)
deviceinfo_device_control_content_event_layout.addWidget(eventjs)
deviceinfo_device_control_content_event_qwidget.setLayout(deviceinfo_device_control_content_event_layout)
deviceInfo_eventbt = QPushButton("发送", parent=deviceinfo_device_qwidget) # 发送事件按钮
deviceInfo_eventbt.clicked.connect(lambda: chatbox.addTextMsg(eventjs.toPlainText()))
self.pushbt.append(deviceInfo_eventbt) # 将所有放松按钮添加进列表,方便统一处理
deviceInfo_eventbt.raise_() # 设置发送按钮最上层显示
deviceinfo_device_control_content_layout.addWidget(deviceinfo_device_control_content_chatbox_qwidget)
deviceinfo_device_control_content_layout.addWidget(deviceinfo_device_control_content_topic_qwidget)
deviceinfo_device_control_content_layout.addWidget(deviceinfo_device_control_content_event_qwidget)
deviceinfo_device_control_layout.addWidget(deviceinfo_device_control_eventlist_qwidget)
deviceinfo_device_control_layout.addLayout(deviceinfo_device_control_content_layout)
deviceinfo_device_layout.addWidget(deviceinfo_device_info_qwidget)
deviceinfo_device_layout.addLayout(deviceinfo_device_control_layout)
deviceinfo_device_qwidget.setLayout(deviceinfo_device_layout)
return deviceinfo_device_qwidget
设备信息页,基础布局采用横向布局,左侧为设备列表,右侧设备详情堆栈布局,设备列表为一个qwidget矩形区域,在其中添加自己通过label组件封装的按钮组件,主要封装的是鼠标移入移出时带有选中效果,自定义一个点击事件信号,连接到切换右侧堆栈布局的页面函数上,点击时将对应页面的位置作为信号参数来触发处理函数,可切换右侧设备页面,右侧堆栈布局根据类变量模拟设备列表数量来判断生成多少个页面,循环调用函数生成设备详情页面,包含设备基本信息、连接按钮、设备事件列表、实时消息框、发送消息按钮,设备信息包含设备id、设备code、设备名等,生成详情页面时会自动生成对应值,设备信息为一个网格布局里面添加四个横向布局实现,设备事件列表也为一个qwidget矩形区域,在其中添加自己封装的eventlabel组件,主要封装的是鼠标移入移出时带有选中效果,自定义一个点击事件信号,连接到右侧实时消息框输入框部分,点击时将对应事件的json串作为信号参数来触发处理函数,将对应的json字符串添加到聊天输入框内,实时消息区域包含一个实时显示收发的mqtt消息的聊天框以及下方消息输入框和发送按钮,聊天框采用一个qwidget矩形区域实现。 根据Qlistqwidget自己封装的消息列表组件,主要包含两个部分,一个是添加消息函数,第二个是调整消息宽度函数,这个矩形区域列表Qlistqwidget每个item也是根据qwidget封装的一个专门用来显示文字的textitem,主要封装了判定文字靠左还是靠右显示,还有文字显示的宽度,消息文字显示宽度判定这个是网上找的函数没有仔细看,原理就是没收到一条消息就会生成一个item调用添加消息函数添加到Qlistqwidget中显示,收到的消息靠右,主动发送的消息靠左,消息输入框包含发送到的mqtt topic以及具体的消息内容和发送那妞,输入框采用的一个文件输入组件QTextEdit,发送按钮为一个button,点击信号连接到Qlistqwidget消息列表组件的添加消息函数上,将文件输入组件QTextEdit里的内容添加到Qlistqwidget消息列表组件中显示
工具设置页
def sysConfig(self):
"""工具设备页"""
sysconfig_layout = QVBoxLayout()
sysconfig_layout.setAlignment(Qt.AlignTop)
sysconfig_layout.addWidget(self.myform(text="日志地址", default=r"安装目录\log")) # 调用类方法生成文字加输入栏控件
sysconfig_layout.addWidget(self.myform(text="Mqtt连接保活时间", default="默认3000ms"))
sysconfig_layout.addWidget(self.myform(text="Mqtt消息质量等级", default="默认消息质量等级1"))
return sysconfig_layout
工具设置页面就是一个纵向布局,添加了三个自己封装的文字和输入框组件,可以设置日志地址、mqtt连接保活时间、消息质量等级等等
工具说明页
def toolDescribe(self):
"""工具说明页"""
tooldes_layout = QVBoxLayout()
tooldes = QTextEdit()
tooldes.setText("工具说明文档")
tooldes.setFont(QFont("Times", 20, QFont.Normal))
tooldes_layout.addWidget(tooldes)
return tooldes_layout
工具说明页面没来得及作,设计就是一个很大的文本显示组件,到时候直接我在里面把说明文档写上去
其余重要功能实现
主程序类变量
dev_list = [] # 程序要模拟的设备列表,记录程序运行过程中模拟的设备
bakdev_list = [] # 设备列表备份,后续用来进入设备详情页时判断选择的设备是否有变化,有变化需要重新生成设备详情页
chatList = [] # 设备详情页,聊天框对象列表,方便统一处理聊天消息宽度
pushbt = [] # 设备详情页,发送事件,方便窗口放大缩小时重新设置按钮位置靠右下角
imagelist = [] # 基本配置页 设备图片对象的列表,方便后续统一处理设备图片对象的选择标志位
flag_device = 0 # 基本配置页 全选标志位,标志点击全选按钮时效果为全选还是取消全选
flag_revise = 0 # 设备详情页 聊天框是否调整宽度标志位
要模拟的设备列表devlist,在基础配置页选择要模拟的设备时会往这个列表添加设备,在初始化设备详情页时会遍历这个列表中的设备,按照事先定义的设备信息如设备事件这些生产设备详情页,设备图片列表imagelist,生成基础配置页时,根据预先配好的支持的模拟的设备列表,会往此列表中添加设备图片对象,此对象为自己的根据QLabel封装的对象,重新设置点击事件和选中未选中标志位,创建此列表是为了支持全选功能的设计,点击全选遍历这个列表让所有设备图片对象变成选中状态
左侧菜单栏
def create_enum_qwidget(self):
"""左侧菜单栏"""
enum_qwidget = QWidget()
# enum_qwidget.setStyleSheet("background-color: rgba(28, 28, 28, 0.9);")
enum_qwidget.setMinimumSize(100, 0)
enum_qwidget.setMaximumWidth(100)
enumbt1 = QPushButton("基础配置", enum_qwidget)
self.enumbt2 = QPushButton("设备信息", enum_qwidget)
self.enumbt2.setEnabled(False) # 设置设备详情界面按钮不可点击,等选择了设备之后才可以点击
enumbt3 = QPushButton("系统设置", enum_qwidget)
enumbt4 = QPushButton("工具说明", enum_qwidget)
enumbt1.setFixedSize(100, 30)
self.enumbt2.setFixedSize(100, 30)
enumbt3.setFixedSize(100, 30)
enumbt4.setFixedSize(100, 30)
enumbt1.move(0, 0)
self.enumbt2.move(0, 35)
enumbt3.move(0, 70)
enumbt4.move(0, 105)
# enumbt1.setStyleSheet("background-color:transparent;")
# enumbt2.setStyleSheet("background-color:transparent;")
# enumbt3.setStyleSheet("background-color:transparent;")
# enumbt4.setStyleSheet("background-color:transparent;")
enumbt1.clicked.connect(lambda: self.enumButtonClicked(0)) # 每个按钮点击信号连接切换页面函数
self.enumbt2.clicked.connect(lambda: self.enumButtonClicked(1))
enumbt3.clicked.connect(lambda: self.enumButtonClicked(2))
enumbt4.clicked.connect(lambda: self.enumButtonClicked(3))
return enum_qwidget
聊天消息框
class bubbleText(QLabel):
"""**文字的显示**主要是控件的大小调节,
起初准备用QTextEdit后来发现实现起来很难控制大小和混动条!
只能舍弃次用QLabel继承实现了,关于控件的水平大小采用控制字符数量的方法(ヘ(_ _ヘ)),
考虑到一个中文字符的宽度大概是3倍英文字符因此出现了checkContainChinese和splitStringByLen函数
(我也不记得哪儿抄来的方法了)。在输入调用super(BubbleText, self).__init__(myText)
前就把字符用\n分割好来显示"""
def __init__(self, listItem, listView, text=None, lr=True):
super(QLabel, self).__init__()
self.listItem = listItem
self.listView = listView
self.text = text
self.setWordWrap(True)
self.textwrap(self.text)
self.setStyleSheet("border:2px solid block;")
# self.adjustSize()
self.setMaximumWidth(400)
self.setFont(QFont("Times", 12, QFont.Normal))
self.lr = lr # 标志绘制左还是右
def enterEvent(self, e):
self.setStyleSheet("border:2px solid block;background-color: rgba(220, 220, 220, 0.8);")
def leaveEvent(self, e):
self.setStyleSheet("border:2px solid block;background-color: rgba(255, 255, 255, 1);")
def contextMenuEvent(self, e):
''' 右键菜单实现文本的复制和控件的删除'''
editUser = QAction(QIcon('icons/copy.png'), u'复制', self) # 第一个参数也可以给一个QIcon图标
editUser.triggered.connect(self.copyText)
# delUser = QAction(QIcon('icons/delete.png'), u'删除', self)
# delUser.triggered.connect(self.delTextItem) # 选中就会触发
menu = QMenu()
menu.addAction(editUser)
# menu.addAction(delUser)
menu.exec_(QCursor.pos()) # 全局位置比较好,使用e.pos()还得转换
e.accept() # 禁止弹出菜单事件传递到父控件中
def copyText(self, b):
cb = QApplication.clipboard()
cb.setText(self.text)
# def delTextItem(self, b):
# # print 'msg deleted'
# self.listView.takeItem(self.listView.indexFromItem(self.listItem).row())
def textwrap(self, text, length = 40):
myText = ""
nLen = 0
text2 = text
text = text.replace('\n', '$')
for s in text: # 控制文本每行最宽显示多少字符
if s != '$':
myText += s
nLen += 1
if nLen >= (length - 1):
myText += '\n'
nLen = 0
self.text = myText
self.setText(myText)
class textItem(QWidget):
'''显示文字的Widget内容,为了让消息可以删除增加listItem和list传递到文本控件'''
def __init__(self, listItem, listView, text=None, lr=True):
super().__init__()
hbox = QHBoxLayout()
self.text = bubbleText(listItem, listView, text, lr)
if lr is not True:
hbox.setContentsMargins(0, 2, 5, 2)
hbox.addWidget(self.text)
hbox.setAlignment(Qt.AlignRight)
else:
hbox.addWidget(self.text)
hbox.setAlignment(Qt.AlignLeft)
hbox.setContentsMargins(5, 2, 0, 2)
self.setLayout(hbox)
class MsgList(QListWidget):
"""消息消息列表的控件,支持增加文字消息和增加图片消息"""
itemlist = []
def __init__(self):
super(MsgList, self).__init__()
# 设置所有样式锁定
self.setStyleSheet(
"QListWidget::item{border:0px solid gray;background-color:transparent;padding:0px;color:transparent}"
"QListView::item:!enabled{background-color:transparent;color:transparent;border:0px solid gray;padding:0px 0px 0px 0px;}"
"QListWidget::item:hover{background-color:transparent;color:transparent;border:0px solid gray;padding:0px 0px 0px 0px;}"
"QListWidget::item:selected{background-color:transparent;color:transparent;border:0px solid gray;padding:0px 0px 0px 0px;}")
def addTextMsg(self, sz=None, lr=True):
if sz:
self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) # 修改滚动条为可随意滚动,默认为一个item滚动一次
it = QListWidgetItem(self)
item = textItem(it, self, sz, lr) # 增加必须指定本list和本item用于删除item
#print(item.sizeHint())
it.setSizeHint(item.sizeHint())
it.setFlags(Qt.ItemIsEnabled) # 设置Item不可选择
self.addItem(it)
self.setItemWidget(it, item)
self.setCurrentItem(it)
self.itemlist.append(item)
def resizeItem(self, width = 400): # 重新调整每个ietm宽度,以及item中每行显示多少文字
for i in self.itemlist:
i.text.textwrap(i.text.text, int(width/10)+1)
i.text.setMaximumWidth(width)
# i.text.update()
主要包含两个部分,一个是添加消息函数,第二个是调整消息宽度函数,这个矩形区域列表Qlistqwidget每个item也是根据qwidget封装的一个专门用来显示文字的textitem,主要封装了判定文字靠左还是靠右显示,还有文字显示的宽度,消息文字显示宽度判定这个是网上找的函数没有仔细看,原理就是没收到一条消息就会生成一个item调用添加消息函数添加到Qlistqwidget中显示,收到的消息靠右,主动发送的消息靠左,消息输入框包含发送到的mqtt topic以及具体的消息内容和发送那妞,输入框采用的一个文件输入组件QTextEdit,发送按钮为一个button,点击信号连接到Qlistqwidget消息列表组件的添加消息函数上,将文件输入组件QTextEdit里的内容添加到Qlistqwidget消息列表组件中显示
消息提示弹窗
def tip(self, tiptext=None, flag=True):
"""消息提示弹窗"""
tiptext = "Error: Connection refused: Bad username or password"
# self.msgtip = QWidget(self.mainWidget)
# self.msgtip.move(550, 0)
# self.msgtip.setStyleSheet("background-color: rgba(255, 255, 255, 1);")
# self.msgtip.setFixedWidth(200)
self.msgtext = QLabel() # 显示文字
self.msgtext.setText(tiptext)
self.msgtext.setParent(self)
self.msgtext.move(self.width(), 0)
if flag==True:
self.msgtext.setStyleSheet("background-color: rgba(255, 255, 255, 1);border:1px solid LawnGreen")
else:
self.msgtext.setStyleSheet("background-color: rgba(255, 255, 255, 1);border:1px solid red")
self.msgtext.setWordWrap(True)
self.msgtext.adjustSize() # 根据内容自适应大小
self.msgtext.setFixedWidth(350)
self.msgtext.setMinimumHeight(100)
#self.msgtext.setMaximumHeight(100)
self.msgtext.raise_()
self.msgtext.show()
animation = QPropertyAnimation(self.msgtext, b"geometry", self) # 动画效果组件,pyqt自带
startpos = self.msgtext.geometry()
newpos = QRect(startpos.x() - 350, startpos.y(), startpos.width(), startpos.height())
animation.setEndValue(newpos)
animation.setDuration(500)
self.timer = QTimer(self) # pyqt自带定时器控件,tips显示多少秒
self.timer.timeout.connect(self.tiptimer)
self.timer.start(3000)
animation.start()
#time.sleep(3000)
self.enumbt2.setEnabled(True) # 选择设备之后,设备详情界面才可以点击
self.enumButtonClicked(1)
def tiptimer(self):
"""消息弹窗定时关联函数"""
self.msgtext.close()
self.timer.stop()
工具右上角的消息提示,基础配置页面开始按钮和设备详情页的发送按钮点击信号连接到触发消息提示函数上,消息弹窗为一个展示文字的Qlabel,通过QPropertyAnimation设置了向右滑动而出的动画效果,主要就是通过动画组件自带设置组件起始位置和重点位置,以及滑动时间,然后触发提示弹窗出现并滑动到对应位置,然后通过QT自带的定时器,QTimer定时让消息提示组件显示三秒,QT中必须使用QTimer,不能用python自带的time模块定时,定时器自带时间到达信号,将该信号连接到关闭消失提示和停止定时器的函数上,这样三秒到了就会自动关闭消息提示和停止定时器
工具放大缩小适配
@log_exception # 程序崩溃时自动打印日志
def resizeEvent(self, QResizeEvent) -> None:
"""窗口放大缩小时,重新设置控件位置、尺寸"""
super().resizeEvent(QResizeEvent)
list(map(lambda x:x.move(self.width() - 349, self.height() - 57), self.pushbt)) # 重新调整发送按钮位置
list(map(lambda x:x.raise_(), self.pushbt)) # 设置按钮最上层显示
if hasattr(self , "msgtext"):
self.msgtext.move(self.width()-350, 0)
if self.flag_revise == 0:
self.flag_revise = 1
else:
list(map(lambda x:x.resizeItem(width=self.width() - int(self.width()*9/17)), self.chatList))
self.show()
整个工具大部分组件都是通过布局来实现放大缩小适配,但基础配置页的全选按钮位置和设备详情页的聊天消息框的宽度、每段消息宽度以及消息tips弹窗位置是我重写了主窗口调整大小事件函数,这个是主窗口自带的函数,每次工具大小发生变化时会自动发送对应信号来触发这个函数,我再函数里重写设置了基础配置页的全选按钮位置和设备详情页的聊天消息框的宽度、每段消息宽度的以及消息tips弹窗的位置,方便窗口变大变小时工具整体显示更好看,其余的组件在窗口变大变小时的控制就靠布局来自动控制,就这三个是我手动控制的,全选按钮位置是因为之前我没找到自动设置靠右上角对齐的方法, 所以通过这个方法手动重新设置全选按钮的位置,其实布局是能直接设置靠右上角对齐的。
自定义组件文件iot_class.py
# 预先定义的模拟设备信息列表
devicelist = [
{"name": "朱雀智能锁", "code": "A7BDAE05004B1200", "id":"1","event": [{"eventname":"开锁成功", "eventjson":"{'name':'开锁成功'}"},{"eventname":"开锁失败,密码错误", "eventjson":"{'name':'开锁成功'}"}]},
{"name": "智悦单路开关", "code": "C9BDAE05004B2200", "id":"2","event": None},
{"name": "漏水检测器", "code": "B2CCAE05004B6600", "id":"3","event": [{"eventname":"漏水告警", "eventjson":"{'name':'漏水告警'}"}]},
{"name": "调光开关", "code": "B2CCAE05004B6600", "id":"3","event": [{"eventname":"漏水告警", "eventjson":"{'name':'漏水告警'}"}]},
{"name": "PM检测器", "code": "B2CCAE05004B6600", "id":"3","event": [{"eventname":"漏水告警", "eventjson":"{'name':'漏水告警'}"}]},
{"name": "智悦插座", "code": "B2CCAE05004B6600", "id":"3","event": [{"eventname":"漏水告警", "eventjson":"{'name':'漏水告警'}"}]},
{"name": "窗帘电机", "code": "B2CCAE05004B6600", "id":"3","event": [{"eventname":"漏水告警", "eventjson":"{'name':'漏水告警'}"}]},
{"name": "红外探测器", "code": "B2CCAE05004B6600", "id":"3","event": [{"eventname":"漏水告警", "eventjson":"{'name':'漏水告警'}"}]},
{"name": "门窗磁感应器", "code": "B2CCAE05004B6600", "id":"3","event": [{"eventname":"漏水告警", "eventjson":"{'name':'漏水告警'}"}]},
]
def log_exception(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
"""日志装饰器,程序自动崩溃时打印日志到test.log并保存到本地"""
logger = logging.getLogger("test_log")
logger.setLevel(logging.INFO)
fh = logging.FileHandler("test.log")
fmt = "\n[%(asctime)s-%(name)s-%(levelname)s]: %(message)s"
formatter = logging.Formatter(fmt)
fh.setFormatter(formatter)
logger.addHandler(fh)
try:
fn(*args, **kwargs)
except Exception as e:
logger.exception("[Error in {}] msg: {}".format(__name__, str(e)))
raise
return wrapper
class frameLabel(QLabel):
"""带有点击事件的Qlabel"""
signal = pyqtSignal(str, int)
def __init__(self, parent=None, tips=None):
super().__init__(parent)
self.flag = 0
self.tips = tips
self.setToolTip(tips)
def mousePressEvent(self, e): # 重载鼠标点击事件
if self.flag == 0:
self.setStyleSheet("border:4px solid LawnGreen;")
self.signal.emit(self.tips, 1)
self.flag = 1
else:
self.setStyleSheet("border:4px solid transparent;")
self.signal.emit(self.tips, 0)
self.flag = 0
class eventLabel(QLabel):
signal = pyqtSignal(list, str)
"""带有点击、悬停事件的Qlabel"""
def __init__(self, parent=None, obj=None):
super().__init__(parent)
self.obj = obj
def mousePressEvent(self, e): # 重载鼠标点击事件
self.signal.emit([self.obj], self.text())
def leaveEvent(self, e): # 鼠标离开label
self.setStyleSheet("background-color: rgba(240, 240, 240, 1);")
def enterEvent(self, e): # 鼠标移入label
self.setStyleSheet("background-color: rgba(205, 201, 201, 1);")
class deviceLabel(QLabel):
signal = pyqtSignal(int)
"""带有点击、悬停事件的Qlabel"""
def __init__(self, parent=None, pageIndex=None):
super().__init__(parent)
self.pageIndex = pageIndex
def mousePressEvent(self, e): # 重载鼠标点击事件
self.signal.emit(self.pageIndex)
def leaveEvent(self, e): # 鼠标离开label
self.setStyleSheet("background-color: rgba(240, 240, 240, 1);")
def enterEvent(self, e): # 鼠标移入label
self.setStyleSheet("background-color: rgba(205, 201, 201, 1);")
存放预先定义好的模拟设备详细信息以及根据实际需要自行封装的组件