基于QtPy (PySide6) 的PLC-HMI工程实战记录(七)创建适合HMI项目的数字输入/显示类部件

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)
相关推荐
MartinYeung526 分钟前
[论文学习]MPIB:医疗提示注入基准——针对LLM临床安全性的系统性评估
人工智能·python·学习
用户83562907805130 分钟前
使用 Python 为 PowerPoint 演示文稿添加评论
后端·python
阿图灵1 小时前
OpenCV 绘图五件套:画圆、文本、线段、矩形与椭圆
图像处理·人工智能·python·opencv·计算机视觉·绘图
web行路人1 小时前
AI全栈之旅 · 第一周总结
python
slacker-kian1 小时前
HuggingFace API加载模型超时:用 ModelScopeAPI 替代
人工智能·python·transformer·huggingface·blip·modelscope·blipprocessor
七夜zippoe2 小时前
DolphinDB 高可用部署实战:从容灾设计到故障自动转移
开发语言·python·高可用·容灾·dolphindb
l1258652 小时前
# LangGraph Deep Research Agent 全流程设计:多轮研究、人机协同与真实来源管理
数据库·人工智能·python·算法·自然语言处理·oracle·langchain
青少儿编程课堂2 小时前
图形化编程实战:智能交通灯调度台,一个作品讲透循环、条件与广播
c++·python·算法·bfs·信息学竞赛