Qt实现多行可展开编辑器(PyQt5)

效果预览

PixPin_2026-05-07_13-41-32

代码

import sys
from PyQt5.QtWidgets import (QApplication, QLineEdit, QTextEdit, QVBoxLayout,
                             QWidget, QFrame)
from PyQt5.QtCore import QTimer, Qt, QEvent, QRect


class MultiLineExtension(QFrame):
    """模拟 QComboBox 下拉效果的编辑层"""

    def __init__(self, parent=None):
        super().__init__(parent)
        # 设置窗口属性:弹出窗口、无边框、顶层显示
        self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
        self.setFrameShape(QFrame.StyledPanel)
        self.setLineWidth(1)

        # 内部布局
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        self.editor = QTextEdit()
        # 允许 Tab 键切换焦点,而不是输入制表符
        self.editor.setTabChangesFocus(True)
        # 禁止输入富文本
        self.editor.setAcceptRichText(False)
        # 禁止回车换行
        # self.editor.setLineWrapMode(QTextEdit.NoWrap)
        layout.addWidget(self.editor)

        # 阴影效果(可选)
        self.setStyleSheet("QFrame { border: 1px solid #C0C0C0; background: white; }")

    def text(self):
        return self.editor.toPlainText()

    def setText(self, text):
        self.editor.setPlainText(text)


class ComboBoxStyleEdit(QLineEdit):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.popup = MultiLineExtension()

        # 状态锁:防止焦点归还时再次触发弹出
        self._ignore_focus_check = False

        # 核心:实时同步信号连接
        self.popup.editor.textChanged.connect(self.sync_text_to_lineedit)
        # 实时监控:单行输入时检查是否溢出
        self.textChanged.connect(self.check_overflow_and_popup)
        # 安装事件过滤器,监听弹出层的事件
        self.popup.installEventFilter(self)

    def sync_text_to_lineedit(self):
        """当多行框内容改变时,实时更新单行框"""
        new_text = self.popup.editor.toPlainText()
        # 将换行符替换为空格,避免单行框出现乱码或不显示
        display_text = new_text  # .replace('\n', ' ')

        # 只有内容不同才更新,避免多余的渲染开销
        if self.text() != display_text:
            # 阻塞信号防止死循环
            self.blockSignals(True)
            self.setText(display_text)
            self.blockSignals(False)

    def is_text_overflowing(self):
        """计算文本像素宽度是否超过输入框可视宽度"""
        fm = self.fontMetrics()
        text_width = fm.horizontalAdvance(self.text())
        # 留出约 15 像素的边距余量
        return text_width > (self.width() - 15)

    def check_overflow_and_popup(self):
        """在单行输入过程中,如果打字打多了,自动弹出"""
        if self.hasFocus() and self.is_text_overflowing():
            self.show_popup()

    def focusInEvent(self, event):
        # 核心逻辑:检查是否需要忽略本次检测(打破死循环)
        if self._ignore_focus_check:
            self._ignore_focus_check = False
            super().focusInEvent(event)
            return
        # 点击进入时,如果内容已经很长了,弹出
        if self.is_text_overflowing():
            # 稍微延迟,确保点击事件处理完毕后再弹出
            QTimer.singleShot(50, self.show_popup)
        super().focusInEvent(event)

    def show_popup(self):
        # 1. 同步内容到多行框
        self.popup.editor.setPlainText(self.text())

        # 2. 计算位置
        rect = self.rect()
        bottom_left = self.mapToGlobal(rect.bottomLeft())
        self.popup.setGeometry(QRect(bottom_left.x(), bottom_left.y(),
                                     self.width(), 150))

        # 3. 先显示,再清除自己的焦点,最后延迟把焦点移交给内部编辑器
        self.popup.show()
        # 弹出时清除单行框焦点
        self.clearFocus()  # 剥夺 QLineEdit 的焦点,防止它拦截键盘

        # 使用 singleShot 确保在窗口完全显示后,焦点能成功落到 QTextEdit 上
        QTimer.singleShot(50, self.focus_inner_editor)

    def focus_inner_editor(self):
        self.popup.activateWindow()  # 激活弹出窗口
        self.popup.editor.setFocus()
        # 光标移至末尾,方便继续输入
        cursor = self.popup.editor.textCursor()
        cursor.movePosition(cursor.End)
        self.popup.editor.setTextCursor(cursor)

    def eventFilter(self, obj, event):
        # 当弹出层由于点击外部或按键隐藏时
        if obj == self.popup and event.type() == QEvent.Hide:
            # 1. 开启锁定
            self._ignore_focus_check = True
            # 2. 归还焦点给单行框
            self.setFocus()
            # 3. 额外保险:如果用户没有操作,一秒后重置锁(防止永久失效)
            QTimer.singleShot(1000, self._reset_lock)

        # 处理 Esc 键
        if obj == self.popup.editor and event.type() == QEvent.KeyPress:
            if event.key() == Qt.Key_Escape:
                self.popup.hide()
                return True
        return super().eventFilter(obj, event)

    def _reset_lock(self):
        self._ignore_focus_check = False


# --- Demo ---


class Demo(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("可展开的多行编辑框")
        l = QVBoxLayout(self)
        self.e = ComboBoxStyleEdit()
        l.addWidget(self.e)
        l.addWidget(QLineEdit("普通的 QLineEdit"))
        l.addStretch()
        self.resize(300, 200)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Demo()
    w.show()
    sys.exit(app.exec_())

posted @ 2026-05-07 13:43  乌合之众  阅读(14)  评论(0)    收藏  举报
clear