HMI项目的输入/显示类部件,比如一个QSpinBox或者QLineEdit,用于设定电机转速,它的实时更新来自于下位机实体PLC的通信,设置完新的数值后,再把新数值通信传回下位机实体PLC,也就是上位机中常见的"读/写"双向操作。如果只是简单将实体PLC的变量与QSpinBox的设定显示值连接,那么有可能数字还没设完就可能被周期更新的下位数据刷新掉,或者是只设置了一半,半成品的设置值被传递到了下位PLC,这显然不符合工程的实际需求。
为此,自定义数字输入/显示类部件,主要具备以下特性:
1、当部件获得焦点,开始设定修改输入值,下位通信的内容不再更新部件的显示值,直至设置完毕。
2、用Esc和回车键来控制本次输入的有效性。
3、只有按下回车键后,当前的设定值才刷新下位PLC。
以QLineEdit为例,场景重现的代码:
python
from PySide6.QtCore import QTimer, Signal, Qt
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit
def add_i():
i = int(plc_vari.text())
i += 1
plc_vari.setText(str(i))
# --------测试Demo--------
if __name__ == "__main__":
app = QApplication([])
w = QWidget()
lay = QVBoxLayout(w)
# 模拟下位PLC数据
plc_vari = QLineEdit()
plc_vari.setText("0")
# 上位画面中的输入框
line = QLineEdit()
line.setText("0")
timer = QTimer()
timer.timeout.connect(add_i)
timer.start(1000)
lay.addWidget(line)
lay.addWidget(plc_vari)
w.resize(400, 150)
w.show()
# 双向连接(hmi上位画面对PLC数据的读写)
plc_vari.textChanged.connect(line.setText)
line.textChanged.connect(plc_vari.setText)
app.exec()
改进后的代码:
python
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtWidgets import QLineEdit, QApplication, QWidget, QVBoxLayout, QSpinBox, QDoubleSpinBox
class HmiQDoubleSpinBox(QDoubleSpinBox):
editConfirmed = Signal(str)
def __init__(self, bond_vari=None, parent=None):
super().__init__(parent)
self._is_editing = False
self._original_value = 0.0
if bond_vari:
bond_vari.textChanged.connect(lambda x:self.setValue(float(x)))
self.editConfirmed.connect(bond_vari.setText)
def focusInEvent(self, event):
"""获得焦点,开始编辑会话,保存原始内容"""
super().focusInEvent(event)
self._original_value = self.value()
self._is_editing = True
def setValue(self, val: float):
"""
重写setText:
用户正在编辑时,外部调用setText直接忽略;
内部提交确认时,先关闭编辑标记再调用父类setText
"""
if self._is_editing:
# 处于编辑模式,外来信号/代码调用setText不生效
return
super().setValue(val)
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Escape:
# Esc:放弃修改,恢复原始内容,结束编辑
self._is_editing = False
super().setValue(self._original_value)
self.clearFocus()
event.accept()
return
elif key in (Qt.Key_Return, Qt.Key_Enter):
# 回车确认:接受当前输入内容,结束编辑,触发editConfirmed
new_value = self.value()
self._is_editing = False
super().setValue(new_value)
self.editConfirmed.emit(str(new_value))
self.clearFocus()
event.accept()
return
super().keyPressEvent(event)
def focusOutEvent(self, event):
"""失去焦点:回退原始值,取消编辑"""
if self._is_editing:
self._is_editing = False
super().setValue(self._original_value)
super().focusOutEvent(event)
def add_i():
i = float(label.text())
i += 0.1
label.setText(str(i))
# --------测试Demo--------
if __name__ == "__main__":
app = QApplication([])
w = QWidget()
lay = QVBoxLayout(w)
label = QLineEdit()
label.setText("0")
line = HmiQDoubleSpinBox(bond_vari=label)
line.setValue(0)
timer = QTimer()
timer.timeout.connect(add_i)
timer.start(1000)
lay.addWidget(line)
lay.addWidget(label)
w.resize(400, 150)
w.show()
app.exec()
最终的项目代码:
python
################################################################################
# 定制的窗口部件
################################################################################
from PySide6.QtCore import Signal, Qt
from PySide6.QtWidgets import QLineEdit, QWidget, QSpinBox, QDoubleSpinBox
# 自定义的QLineEdit
class HmiLineEdit(QLineEdit):
hmi_textChanged = Signal(str)
def __init__(self, arg1=None, parent=None):
"""
兼容Qt Designer提升控件两种调用形式
HmiLineEdit(text:str, parent)
HmiLineEdit(parent)
"""
if isinstance(arg1, str):
# 第一个参数是文本
super().__init__(arg1, parent)
else:
# 第一个参数是parent控件
super().__init__(parent=arg1)
def focusInEvent(self, event):
"""获得焦点,开始编辑会话,保存原始内容"""
super().focusInEvent(event)
self._original_text = self.text()
self._is_editing = True
def setText(self, text: str):
"""
重写setText:
用户正在编辑时,外部调用setText直接忽略;
内部提交确认时,先关闭编辑标记再调用父类setText
"""
if self._is_editing:
# 处于编辑模式,外来信号/代码调用setText不生效
return
super().setText(text)
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Escape:
# Esc:放弃修改,恢复原始内容,结束编辑
self._is_editing = False
super().setText(self._original_text)
self.clearFocus()
event.accept()
return
elif key in (Qt.Key_Return, Qt.Key_Enter):
# 回车确认:接受当前输入内容,结束编辑,触发editConfirmed
new_text = self.text()
self._is_editing = False
super().setText(new_text)
self.hmi_textChanged.emit(new_text)
self.clearFocus()
event.accept()
return
super().keyPressEvent(event)
def focusOutEvent(self, event):
"""失去焦点:回退原始值,取消编辑"""
if self._is_editing:
self._is_editing = False
super().setText(self._original_text)
super().focusOutEvent(event)
# 自定义的QSpinBox
class HmiSpinBox(QSpinBox):
hmi_valueChanged = Signal(int)
def __init__(self, parent=None):
super().__init__(parent)
self._is_editing = False
def focusInEvent(self, event):
"""获得焦点,开始编辑会话,保存原始内容"""
super().focusInEvent(event)
self._original_value = self.value()
self._is_editing = True
def setValue(self, value: int):
"""
重写setText:
用户正在编辑时,外部调用setValue直接忽略;
内部提交确认时,先关闭编辑标记再调用父类setValue
"""
if self._is_editing:
# 处于编辑模式,外来信号/代码调用setValue不生效
return
super().setValue(value)
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Escape:
# Esc:放弃修改,恢复原始内容,结束编辑
self._is_editing = False
super().setValue(self._original_value)
self.clearFocus()
event.accept()
return
elif key in (Qt.Key_Return, Qt.Key_Enter):
# 回车确认:接受当前输入内容,结束编辑,触发editConfirmed
new_text = self.text()
self._is_editing = False
super().setValue(int(new_text))
self.hmi_valueChanged.emit(int(new_text))
self.clearFocus()
event.accept()
return
super().keyPressEvent(event)
def focusOutEvent(self, event):
"""失去焦点:回退原始值,取消编辑"""
if self._is_editing:
self._is_editing = False
super().setValue(self._original_value)
super().focusOutEvent(event)
# 自定义的QSpinBox
class HmiDoubleSpinBox(QDoubleSpinBox):
hmi_valueChanged = Signal(float)
def __init__(self, parent=None):
"""
兼容Qt Designer提升控件两种调用形式
HmiLineEdit(text:str, parent)
HmiLineEdit(parent)
"""
super().__init__(parent)
self._is_editing = False
def focusInEvent(self, event):
"""获得焦点,开始编辑会话,保存原始内容"""
super().focusInEvent(event)
self._original_value = self.value()
self._is_editing = True
def setValue(self, value: float):
"""
重写setValue:
用户正在编辑时,外部调用setValue直接忽略;
内部提交确认时,先关闭编辑标记再调用父类setValue
"""
if self._is_editing:
# 处于编辑模式,外来信号/代码调用setValue不生效
return
super().setValue(value)
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_Escape:
# Esc:放弃修改,恢复原始内容,结束编辑
self._is_editing = False
super().setValue(self._original_value)
self.clearFocus()
event.accept()
return
elif key in (Qt.Key_Return, Qt.Key_Enter):
# 回车确认:接受当前输入内容,结束编辑,触发editConfirmed
new_text = self.text()
self._is_editing = False
super().setValue(float(new_text))
self.hmi_valueChanged.emit(float(new_text))
self.clearFocus()
event.accept()
return
super().keyPressEvent(event)
def focusOutEvent(self, event):
"""失去焦点:回退原始值,取消编辑"""
if self._is_editing:
self._is_editing = False
super().setValue(self._original_value)
super().focusOutEvent(event)