效果预览

代码
import sys
from PyQt5.QtWidgets import QApplication, QLineEdit, QVBoxLayout, QWidget, QLabel
from PyQt5.QtCore import Qt, QPropertyAnimation, QPoint, QEasingCurve
class CupertinoFloatingField(QWidget):
def __init__(self, placeholder="手机号 / 邮箱", parent=None):
super().__init__(parent)
self.setFixedHeight(50) # 紧凑的高度
# 1. 样式定义
self.STYLE_NORMAL = "border: 1px solid #D1D1D6; background-color: #F2F2F7;"
self.STYLE_FOCUS = "border: 2px solid #007AFF; background-color: #FFFFFF;"
# 2. 创建输入框
self.input_field = QLineEdit(self)
# 输入框占满容器,但顶部留出 10px 的空间给标签中位线
self.input_field.setGeometry(0, 10, 320, 40)
# self.input_field.setMinimumSize(0, 40)
self.input_field.setStyleSheet(
f"QLineEdit {{ {self.STYLE_NORMAL} border-radius: 8px; padding: 0 12px; font-size: 14px; }}")
# 3. 创建悬浮标签
self.label = QLabel(placeholder, self)
self.label.setAttribute(Qt.WA_TransparentForMouseEvents)
# 核心:背景色与窗口背景一致,形成“遮断”效果
self.label.setStyleSheet("color: #8E8E93; font-size: 14px; background-color: transparent; padding: 0 4px;")
# 坐标计算 (x, y)
# Down: 居中在输入框内
self.label_pos_down = QPoint(12, 22)
# Up: 骑在边框线上 (y 坐标正好是输入框顶边 10px 附近)
self.label_pos_up = QPoint(10, 2)
self.label.move(self.label_pos_down)
# 4. 动画
self.anim = QPropertyAnimation(self.label, b"pos")
self.anim.setDuration(150)
self.anim.setEasingCurve(QEasingCurve.OutQuad)
# 5. 重写事件
self.input_field.focusInEvent = self._on_focus_in
self.input_field.focusOutEvent = self._on_focus_out
def _on_focus_in(self, event):
self.input_field.setStyleSheet(
f"QLineEdit {{ {self.STYLE_FOCUS} border-radius: 8px; padding: 0 12px; font-size: 14px; }}")
self.label.setStyleSheet("color: #007AFF; font-size: 11px; font-weight: bold; background-color: white;")
self.anim.setEndValue(self.label_pos_up)
self.anim.start()
QLineEdit.focusInEvent(self.input_field, event)
def _on_focus_out(self, event):
if not self.input_field.text():
self.input_field.setStyleSheet(
f"QLineEdit {{ {self.STYLE_NORMAL} border-radius: 8px; padding: 0 12px; font-size: 14px; }}")
self.label.setStyleSheet("color: #8E8E93; font-size: 14px; background-color: transparent;")
self.anim.setEndValue(self.label_pos_down)
self.anim.start()
else:
# 有文字但失去焦点,保持上方,但颜色变灰,背景保持白色以遮挡线条
self.input_field.setStyleSheet(
f"QLineEdit {{ {self.STYLE_NORMAL} border-radius: 8px; padding: 0 12px; font-size: 14px; }}")
self.label.setStyleSheet("color: #8E8E93; font-size: 11px; background-color: white;")
QLineEdit.focusOutEvent(self.input_field, event)
class Demo(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Cupertino Notch Label")
self.setFixedSize(400, 250)
self.setStyleSheet("background-color: white;") # 必须是白色,以配合标签背景
layout = QVBoxLayout(self)
layout.setSpacing(25)
layout.addWidget(CupertinoFloatingField("User Name"))
layout.addWidget(CupertinoFloatingField("Password"))
layout.setContentsMargins(40, 40, 40, 40)
if __name__ == "__main__":
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())