PyQt6 / PySide 6 实现可拖拽的多标签页 web 浏览器【1】(有 Bug)

声明:

本项目代码来自以下两个项目

PyQt 5 / PySide 2 实现 QTabWidget 的拖入拖出功能
https://github.com/akihito-takeuchi/qt-draggable-tab-widget
SimPyWeb X ------ 使用PyQt5以及QWebEngineView构建网页浏览器

Bug:

存在很多问题:

1. 新拖拽的窗口无法新建标签页;
2. 旧窗口无法关闭;

......

代码:

main.py

python 复制代码
from PySide6.QtCore import QUrl, QSize, QTimer
from PySide6.QtGui import QIcon, QPixmap, QAction
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWidgets import QToolBar, QLineEdit, QProgressBar, QLabel, QMainWindow, QTabWidget, QStatusBar, QWidget
from PySide6 import QtWidgets, QtGui, QtCore
from typing_extensions import Literal

Signal = QtCore.Signal
Slot = QtCore.Slot

class TabInfo:
    def __init__(self, widget=None, text=None, icon=None,
                 tool_tip=None, whats_this=None):
        self.widget = widget
        self.text = text
        self.icon = icon
        self.tool_tip = tool_tip
        self.whats_this = whats_this


class DraggableTabWidget(QtWidgets.QTabWidget):
    tab_widget_instances_ = []

    def __init__(self, parent=None):
        super().__init__(parent)
        tab_bar = DraggableTabBar(self)
        self.setTabBar(tab_bar)
        tab_bar.createWindowRequested.connect(self.createNewWindow)
        self.setMovable(True)
        self.setTabsClosable(True)
        DraggableTabWidget.tab_widget_instances_.append(self)

    def event(self, event):
        if event.type() == QtCore.QEvent.Type.DeferredDelete:
            DraggableTabWidget.tab_widget_instances_.remove(self)
        return super().event(event)

    @Slot(QtCore.QRect, TabInfo)
    def createNewWindow(self, win_rect, tab_info):
        new_window = BrowserWindow(mode="push")
        new_window.tabs.addTab(
            tab_info.widget,
            tab_info.icon,
            tab_info.text)
        new_window.tabs.setTabToolTip(0, tab_info.tool_tip)
        new_window.tabs.setTabWhatsThis(0, tab_info.whats_this)
        new_window.show()
        new_window.setGeometry(win_rect)
        return new_window


class DraggableTabBar(QtWidgets.QTabBar):
    createWindowRequested = Signal(QtCore.QRect, TabInfo)

    initializing_drag_ = False
    drag_tab_info_ = TabInfo()
    dragging_widget_ = None

    def __init__(self, parent=None):
        super().__init__(parent)
        self.click_point = QtCore.QPoint()
        self.can_start_drag = False
        self.a = 0

    def mousePressEvent(self, event):
        cls = DraggableTabBar
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            current_index = self.tabAt(event.pos())
            parent = self.parent()
            parent.setCurrentIndex(current_index)
            current_widget = parent.currentWidget()
            cls.drag_tab_info_ = TabInfo(
                current_widget, self.tabText(current_index),
                self.tabIcon(current_index), self.tabToolTip(current_index),
                self.tabWhatsThis(current_index))
            cls.dragging_widget_ = None
            self.click_point = event.pos()
            self.can_start_drag = False
            self.grabMouse()
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        cls = DraggableTabBar
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            if cls.initializing_drag_:
                if self.parent().indexOf(cls.drag_tab_info_.widget) <= 0:
                    cls.dragging_widget_ = cls.drag_tab_info_.widget
                    cls.dragging_widget_.setParent(None)
                    cls.dragging_widget_.setWindowFlags(
                        QtCore.Qt.WindowType.FramelessWindowHint)
                else:
                    cls.dragging_widget_ = self.window()
                cls.initializing_drag_ = False
                cls.dragging_widget_.window().raise_()
            else:
                if cls.dragging_widget_:
                    win_rect = cls.dragging_widget_.geometry()
                    win_rect.moveTo(event.globalPos())
                    idx = self.parent().indexOf(cls.drag_tab_info_.widget)
                    if idx >= 0:
                        self.parent().removeTab(idx)
                    self.createWindowRequested.emit(win_rect, cls.drag_tab_info_)
                    self.destroyUnnecessaryWindow()
                cls.dragging_widget_ = None
                cls.drag_tab_info_ = TabInfo()
                self.releaseMouse()
        self.click_point = QtCore.QPoint()
        self.can_start_drag = False

        super().mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        cls = DraggableTabBar
        if cls.drag_tab_info_.widget is None:
            return

        if not self.can_start_drag:
            moved_length = (event.pos() - self.click_point).manhattanLength()
            self.can_start_drag = moved_length > QtWidgets.QApplication.startDragDistance()

        if cls.dragging_widget_:
            for bar_inst in cls._tabBarInstances():
                bar_region = bar_inst.visibleRegion()
                bar_region.translate(bar_inst.mapToGlobal(QtCore.QPoint(0, 0)))
                if bar_region.contains(event.globalPos()):
                    if (bar_inst == self):
                        self.startTabMove()
                        event.accept()
                        return
                    else:
                        self.releaseMouse()
                        bar_inst.grabMouse()
                        event.accept()
                        return
        widget_rect = self.geometry()
        widget_rect.moveTo(0, 0)
        if widget_rect.contains(event.pos()):
            super().mouseMoveEvent(event)
        elif cls.dragging_widget_ is None and self.can_start_drag:
            # start dragging
            self.startDrag()
            event.accept()
            return

        if cls.dragging_widget_:
            cls.dragging_widget_.move(event.globalPos() + QtCore.QPoint(1, 1))
            cls.dragging_widget_.show()

    def startDrag(self):
        cls = DraggableTabBar
        if self.count() > 1:
            parent = self.parent()
            idx = parent.indexOf(cls.drag_tab_info_.widget)
            parent.removeTab(idx)
            cls.drag_tab_info_.widget.setParent(None)
        cls.dragging_widget_ = None
        cls.initializing_drag_ = True
        release_event = self.createMouseEvent(
            QtCore.QEvent.Type.MouseButtonRelease,
            self.mapFromGlobal(QtGui.QCursor.pos()))
        QtWidgets.QApplication.postEvent(self, release_event)

    def createMouseEvent(self, event_type, pos=QtCore.QPoint()):
        if pos.isNull():
            global_pos = QtGui.QCursor.pos()
        else:
            global_pos = self.mapToGlobal(pos)
        modifiers = QtWidgets.QApplication.keyboardModifiers()

        event = QtGui.QMouseEvent(
            event_type, pos, global_pos,
            QtCore.Qt.MouseButton.LeftButton, QtCore.Qt.MouseButton.LeftButton, modifiers)
        return event

    def startTabMove(self):
        cls = DraggableTabBar
        global_pos = QtGui.QCursor.pos()
        pos = self.mapFromGlobal(global_pos)

        if cls.drag_tab_info_.widget.parent() is not None:
            parent = cls.drag_tab_info_.widget.parent().parent()
            idx = parent.indexOf(cls.drag_tab_info_.widget)
            parent.removeTab(idx)
            parent.window().hide()

        idx = self.tabAt(pos)
        self.insertCurrentTabInfo(idx)
        cls.dragging_widget_ = None
        cls.drag_tab_info_ = TabInfo()

        press_event = self.createMouseEvent(
            QtCore.QEvent.Type.MouseButtonPress, self.tabRect(idx).center())
        QtWidgets.QApplication.postEvent(self, press_event)
        self.destroyUnnecessaryWindow()
        self.window().raise_()

    def destroyUnnecessaryWindow(self):
        cls = DraggableTabBar
        for bar_inst in cls._tabBarInstances():
            if bar_inst.count() == 0 \
                    and (not bar_inst.isVisible() or bar_inst.parent().parent() is None):
                bar_inst.deleteLater()
                bar_inst.parent().parent().close()

    def insertCurrentTabInfo(self, idx):
        cls = DraggableTabBar
        parent = self.parent()
        parent.insertTab(
            idx,
            cls.drag_tab_info_.widget,
            cls.drag_tab_info_.icon,
            cls.drag_tab_info_.text
        )
        parent.setTabToolTip(idx, cls.drag_tab_info_.tool_tip)
        parent.setTabWhatsThis(idx, cls.drag_tab_info_.whats_this)
        parent.setCurrentWidget(cls.drag_tab_info_.widget)

    @classmethod
    def _tabBarInstances(cls):
        return [w for w in QtWidgets.QApplication.allWidgets() if w.__class__ == cls]


class BrowserEngineView(QWebEngineView):
    tabs = []

    def __init__(self, Main, parent=None):
        super(BrowserEngineView, self).__init__(parent)
        self.mainWindow = Main

    def createWindow(self, QWebPage_WebWindowType):
        webview = BrowserEngineView(self.mainWindow)
        tab = BrowserTab(self.mainWindow)
        tab.browser = webview
        tab.setCentralWidget(tab.browser)
        self.tabs.append(tab)
        self.mainWindow.add_new_tab(tab)
        return webview


class BrowserTab(QMainWindow):
    def __init__(self, Main, parent=None):
        super(BrowserTab, self).__init__(parent)
        self.mainWindow = Main
        self.browser = BrowserEngineView(self.mainWindow)
        self.browser.load(QUrl("https://cn.bing.com"))
        self.setCentralWidget(self.browser)
        self.navigation_bar = QToolBar('Navigation')
        self.navigation_bar.setIconSize(QSize(24, 24))
        self.navigation_bar.setMovable(False)
        self.addToolBar(self.navigation_bar)
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)

        self.back_button = QAction(QIcon('Assets/back.png'), '后退', self)
        self.next_button = QAction(QIcon('Assets/forward.png'), '前进', self)
        self.stop_button = QAction(QIcon('Assets/stop.png'), '停止', self)
        self.refresh_button = QAction(QIcon('Assets/refresh.png'), '刷新', self)
        self.home_button = QAction(QIcon('Assets/home.png'), '主页', self)
        self.enter_button = QAction(QIcon('Assets/enter.png'), '转到', self)
        self.add_button = QAction(QIcon('Assets/new.png'), '新建标签页', self)
        self.ssl_label1 = QLabel(self)
        self.ssl_label2 = QLabel(self)
        self.url_text_bar = QLineEdit(self)
        self.url_text_bar.setMinimumWidth(300)
        self.progress_bar = QProgressBar()
        self.progress_bar.setMaximumWidth(120)
        self.set_button = QAction(QIcon('Assets/setting.png'), '设置', self)
        self.navigation_bar.addAction(self.back_button)
        self.navigation_bar.addAction(self.next_button)
        self.navigation_bar.addAction(self.stop_button)
        self.navigation_bar.addAction(self.refresh_button)
        self.navigation_bar.addAction(self.home_button)
        self.navigation_bar.addAction(self.add_button)
        self.navigation_bar.addSeparator()
        self.navigation_bar.addWidget(self.ssl_label1)
        self.navigation_bar.addWidget(self.ssl_label2)
        self.navigation_bar.addWidget(self.url_text_bar)
        self.navigation_bar.addAction(self.enter_button)
        self.navigation_bar.addSeparator()
        self.navigation_bar.addWidget(self.progress_bar)
        self.navigation_bar.addAction(self.set_button)
        self.status_icon = QLabel()
        self.status_icon.setScaledContents(True)
        self.status_icon.setMaximumHeight(24)
        self.status_icon.setMaximumWidth(24)
        self.status_icon.setPixmap(QPixmap("Assets/main.png"))
        self.status_label = QLabel()
        self.status_label.setText(self.mainWindow.version + " - SimPyWeb X")
        self.status_bar.addWidget(self.status_icon)
        self.status_bar.addWidget(self.status_label)

    def navigate_to_url(self):
        s = QUrl(self.url_text_bar.text())
        if s.scheme() == '':
            s.setScheme('http')
        self.browser.load(s)

    def navigate_to_home(self):
        s = QUrl("https://www.baidu.com/")
        self.browser.load(s)

    def renew_urlbar(self, s):
        prec = s.scheme()
        if prec == 'http':
            self.ssl_label1.setPixmap(QPixmap("Assets/unsafe.png").scaledToHeight(24))
            self.ssl_label2.setText(" 不安全 ")
            self.ssl_label2.setStyleSheet("color:red;")
        elif prec == 'https':
            self.ssl_label1.setPixmap(QPixmap("Assets/safe.png").scaledToHeight(24))
            self.ssl_label2.setText(" 安全 ")
            self.ssl_label2.setStyleSheet("color:green;")
        self.url_text_bar.setText(s.toString())
        self.url_text_bar.setCursorPosition(0)

    def renew_progress_bar(self, p):
        self.progress_bar.setValue(p)


class BrowserWindow(QWidget):
    name = "SimPyWeb X"
    version = "3.0"
    date = "2020.1.26"

    def __init__(self, mode: Literal["main", "push"] = "main"):
        super().__init__()
        self.setWindowTitle(self.name + " " + self.version)
        self.setWindowIcon(QIcon('Assets/main.png'))
        self.resize(1200, 900)
        self.tabs = DraggableTabWidget(self)
        self.tabs.setTabsClosable(True)
        self.tabs.setMovable(True)
        self.tabs.tabCloseRequested.connect(self.close_current_tab)
        self.tabs.currentChanged.connect(lambda i: self.setWindowTitle(self.tabs.tabText(i) + " - " + self.name))
        if mode == "main":
            self.init_tab = BrowserTab(self)
            self.init_tab.browser.load(QUrl("https://cn.bing.com"))
            self.add_new_tab(self.init_tab)

    def add_blank_tab(self):
        blank_tab = BrowserTab(self)
        self.tabs.parent().add_new_tab(blank_tab)

    def add_new_tab(self, tab):
        i = self.tabs.addTab(tab, "")
        self.tabs.setCurrentIndex(i)
        self.tabs.setTabIcon(i,QIcon('Assets/main.png'))
        tab.back_button.triggered.connect(tab.browser.back)
        tab.next_button.triggered.connect(tab.browser.forward)
        tab.stop_button.triggered.connect(tab.browser.stop)
        tab.refresh_button.triggered.connect(tab.browser.reload)
        tab.home_button.triggered.connect(tab.navigate_to_home)
        tab.enter_button.triggered.connect(tab.navigate_to_url)
        tab.add_button.triggered.connect(self.add_blank_tab)
        tab.url_text_bar.returnPressed.connect(tab.navigate_to_url)
        tab.browser.urlChanged.connect(tab.renew_urlbar)
        tab.browser.loadProgress.connect(tab.renew_progress_bar)
        tab.browser.titleChanged.connect(lambda title: (

            self.tabs.setTabText(i, title) if len(title) <= 11 else self.tabs.setTabText(i, title[:9]+"..."),
            self.tabs.setTabToolTip(i, title),
            self.setWindowTitle(self.tabs.tabText(i) + " - " + self.name)))
        #tab.browser.iconChanged.connect(self.tabs.setTabIcon(i, tab.browser.icon()))

    def close_current_tab(self, i):
        if self.tabs.count() > 1:
            self.tabs.removeTab(i)
        else:
            self.close()

    def resizeEvent(self, event):
        self.tabs.setGeometry(0, 0, self.width(), self.height())

init.py

python 复制代码
import sys
from PySide6.QtWidgets import QApplication
from main import BrowserWindow

if __name__ == '__main__':
    app = QApplication(sys.argv)
    MainWindow = BrowserWindow()
    MainWindow.show()
    sys.exit(app.exec())
相关推荐
独行soc7 小时前
2025年渗透测试面试题总结-18(题目+回答)
android·python·科技·面试·职场和发展·渗透测试
S01d13r7 小时前
gunicorn + flask 处理高并发请求
python·flask·gunicorn
杜子不疼.7 小时前
《Python列表和元组:从入门到花式操作指南》
开发语言·python
pan0c237 小时前
数据处理与统计分析 —— numpy入门
python·numpy
max5006007 小时前
基于桥梁三维模型的无人机检测路径规划系统设计与实现
前端·javascript·python·算法·无人机·easyui
秋氘渔8 小时前
综合案例:Python 函数知识整合 — 学生成绩管理系统
开发语言·python
AI 嗯啦9 小时前
SQL详细语法教程(三)mysql的函数知识
android·开发语言·数据库·python·sql·mysql
databook9 小时前
把数学对象画出来:Manim Mobject类库速查手册
python·数学·动效
图灵学术计算机论文辅导10 小时前
傅里叶变换+attention机制,深耕深度学习领域
人工智能·python·深度学习·计算机网络·考研·机器学习·计算机视觉
ruleslol10 小时前
python30-正则表达式
python·正则表达式