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())
相关推荐
胡耀超28 分钟前
霍夫圆变换全面解析(OpenCV)
人工智能·python·opencv·算法·计算机视觉·数据挖掘·数据安全
doupoa39 分钟前
Fabric 服务端插件开发简述与聊天事件监听转发
运维·python·fabric
How_doyou_do1 小时前
备战菊厂笔试4
python·算法·leetcode
(・Д・)ノ2 小时前
python打卡day27
开发语言·python
小oo呆3 小时前
【学习心得】Jupyter 如何在conda的base环境中其他虚拟环境内核
python·jupyter·conda
小白学大数据3 小时前
Scrapy框架下地图爬虫的进度监控与优化策略
开发语言·爬虫·python·scrapy·数据分析
浊酒南街3 小时前
TensorFlow之微分求导
人工智能·python·tensorflow
立秋67894 小时前
用Python绘制梦幻星空
开发语言·python·pygame
alpszero4 小时前
YOLO11解决方案之对象裁剪探索
人工智能·python·计算机视觉·yolo11
白云千载尽4 小时前
相机、雷达标定工具,以及雷达自动标定的思路
python·自动驾驶·ros