效果预览

代码
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_())