效果预览

代码
import sys
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton, QLineEdit, QApplication
from PyQt5.QtCore import Qt, QTimer, QPointF, QPoint, QRectF
from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPolygonF
class BubblePopup(QWidget):
def __init__(self, text, parent=None, direction="bottom"):
super().__init__(parent)
self.direction = direction # top, bottom, left, right
self.arrow_size = 8 # 箭头大小
self.margin = 5 # 间距
# 1. 窗口属性
self.setWindowFlags(Qt.ToolTip | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
# 2. 布局
layout = QVBoxLayout(self)
self.label = QLabel(text)
self.label.setStyleSheet("color: white; padding: 5px; font-size: 12px;")
# 根据方向给 Label 留出箭头的边距
p = self.arrow_size + 5
if direction == "bottom":
layout.setContentsMargins(5, p, 5, 5)
elif direction == "top":
layout.setContentsMargins(5, 5, 5, p)
elif direction == "left":
layout.setContentsMargins(5, 5, p, 5)
elif direction == "right":
layout.setContentsMargins(p, 5, 5, 5)
layout.addWidget(self.label)
# 3. 自动关闭
self.close_timer = QTimer(self)
self.close_timer.timeout.connect(self.close)
self.close_timer.start(3000)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(50, 50, 50, 230)) # 深灰色背景
rect = QRectF(self.rect())
path = QPainterPath()
# 根据方向调整主体矩形范围,给箭头留位
if self.direction == "bottom":
rect.setTop(rect.top() + self.arrow_size)
elif self.direction == "top":
rect.setBottom(rect.bottom() - self.arrow_size)
elif self.direction == "left":
rect.setRight(rect.right() - self.arrow_size)
elif self.direction == "right":
rect.setLeft(rect.left() + self.arrow_size)
# 绘制圆角矩形主体
path.addRoundedRect(rect, 8, 8)
# 绘制三角形箭头
arrow = QPolygonF()
center_h = rect.width() / 2
center_v = rect.height() / 2
if self.direction == "bottom":
arrow.append(QPointF(center_h - self.arrow_size, rect.top()))
arrow.append(QPointF(center_h, 0))
arrow.append(QPointF(center_h + self.arrow_size, rect.top()))
elif self.direction == "top":
arrow.append(QPointF(center_h - self.arrow_size, rect.bottom()))
arrow.append(QPointF(center_h, self.height()))
arrow.append(QPointF(center_h + self.arrow_size, rect.bottom()))
elif self.direction == "left":
arrow.append(QPointF(rect.right(), center_v - self.arrow_size))
arrow.append(QPointF(self.width(), center_v))
arrow.append(QPointF(rect.right(), center_v + self.arrow_size))
elif self.direction == "right":
arrow.append(QPointF(rect.left(), center_v - self.arrow_size))
arrow.append(QPointF(0, center_v))
arrow.append(QPointF(rect.left(), center_v + self.arrow_size))
path.addPolygon(arrow)
painter.drawPath(path)
def enterEvent(self, event): self.close_timer.stop()
def leaveEvent(self, event): self.close_timer.start(3000)
@staticmethod
def show_message(widget, text, direction="bottom"):
popup = BubblePopup(text, direction=direction)
popup.adjustSize() # 先根据文字调整大小再计算位置
# 计算全局坐标
w_p = widget.mapToGlobal(QPoint(0, 0))
w_w = widget.width()
w_h = widget.height()
p_w = popup.width()
p_h = popup.height()
if direction == "bottom":
pos = w_p + QPoint((w_w - p_w)//2, w_h + 2)
elif direction == "top":
pos = w_p + QPoint((w_w - p_w)//2, -p_h - 2)
elif direction == "left":
pos = w_p + QPoint(-p_w - 2, (w_h - p_h)//2)
elif direction == "right":
pos = w_p + QPoint(w_w + 2, (w_h - p_h)//2)
popup.move(pos)
popup.show()
widget._bubble = popup
# --- Demo 示例 ---
class Demo(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("气泡弹窗测试")
layout = QVBoxLayout(self)
self.edit = QLineEdit()
btn_right = QPushButton("向右侧弹出提示")
btn_right.clicked.connect(lambda: BubblePopup.show_message(self.edit, "请输入正确格式!", "right"))
btn_top = QPushButton("向上方弹出提示")
btn_top.clicked.connect(lambda: BubblePopup.show_message(self.edit, "这里是上方提示", "top"))
btn_bottom = QPushButton("向下方弹出提示")
btn_bottom.clicked.connect(lambda: BubblePopup.show_message(self.edit, "这里是下方提示", "bottom"))
btn_left = QPushButton("向左侧弹出提示")
btn_left.clicked.connect(lambda: BubblePopup.show_message(self.edit, "这里是左侧提示", "left"))
layout.addWidget(self.edit)
layout.addWidget(btn_right)
layout.addWidget(btn_top)
layout.addWidget(btn_bottom)
layout.addWidget(btn_left)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Demo()
window.show()
sys.exit(app.exec_())