基于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)
相关推荐
databook1 天前
从手动检查到自动监控:一个数据质量工作流的实现
后端·python·数据分析
北冥有鱼被烹1 天前
砷化镓与磷化铟:光模块的地基——从化学键到AI缺货潮的底层逻辑
人工智能·python
for_ever_love__1 天前
线性回归与梯度下降——从零手写一个模型
python·机器学习·线性回归·梯度下降
code_whiter1 天前
7.自动化测试常用函数
python·功能测试
小园子的小菜1 天前
Python协程深度解析:从原理演进到实战避坑
后端·python
砚底藏山河1 天前
量化实战:截面因子有效性检验(IC 分析与分层回测)
java·python·金融·maven
GitFun1 天前
给策略加了条-8%止损线,11年只触发1次
python·股票·量化交易
马六六i1 天前
市面上正规的IP驱动产业新场景新工具有哪些
大数据·网络·python·tcp/ip
I Am a robert girl1 天前
稀有事件估计的迭代去对齐:从源码视角拆解重要性采样新范式
开发语言·python·机器学习·重要性采样·蒙特卡洛方法·稀有事件估计
天若有情6731 天前
开源|Comfort Lang v1.0.1,一款主打清爽交互的新型命令式编程语言
python·开源·交互·编程语言·项目分享·repl·comfort lang