Qt实现气泡弹窗(PyQt5)

效果预览

PixPin_2026-01-20_14-31-35

代码

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_())
posted @ 2026-01-20 14:33  乌合之众  阅读(3)  评论(0)    收藏  举报
clear