基于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)
相关推荐
论文复现现场15 小时前
DeepSeek V4.1 Flash 和 V4 Pro 有什么区别?API 价格、性能与迁移建议
java·服务器·人工智能·python·大模型api·deepseek·deepseek v4.1
量化炼金 (CodeAlchemy)15 小时前
【量化研究】美股历史数据获取:Yahoo Finance 转 Zorro 回测格式
python·量化研究·量化交易
小嘉丶学长15 小时前
Day13 · 列表与元组
linux·运维·服务器·开发语言·python
小小张说故事15 小时前
Python 的 int 为什么不会溢出?拆开 PyLongObject 和它背后那串变长数组
python
威哥爱编程16 小时前
Python 智能体开发实战 03:聊十轮就“失忆”,账单还翻倍?管好上下文,Token 立省一半
python·agent
情 九16 小时前
MCP 开发环境搭建:Mac 与 Windows 跑通 Python 服务和 stdio 客户端
windows·python·macos·mcp
SEO_juper16 小时前
Bash 交叉验证 robots.txt 和 sitemap:两个文件自相矛盾,页面永远收录不了
开发语言·python·node.js·json·bash
日日大王学编程16 小时前
LangGraph 基本用法详解:从入门到实战
python·语言模型·langchain
威哥爱编程16 小时前
Python 智能体开发实战 04:模型答得挺漂亮,程序却读不懂?让回答直接变成能入库的 JSON
python·agent
全栈陈序员16 小时前
【Python】基础语法入门(三十)—— Web 开发入门:用 Flask 搭建第一个网站
前端·python·flask