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())
相关推荐
Hylan_J2 小时前
【VSCode】MicroPython环境配置
ide·vscode·python·编辑器
莫忘初心丶2 小时前
在 Ubuntu 22 上使用 Gunicorn 启动 Flask 应用程序
python·ubuntu·flask·gunicorn
失败尽常态5235 小时前
用Python实现Excel数据同步到飞书文档
python·excel·飞书
2501_904447745 小时前
OPPO发布新型折叠屏手机 起售价8999
python·智能手机·django·virtualenv·pygame
青龙小码农5 小时前
yum报错:bash: /usr/bin/yum: /usr/bin/python: 坏的解释器:没有那个文件或目录
开发语言·python·bash·liunx
大数据追光猿5 小时前
Python应用算法之贪心算法理解和实践
大数据·开发语言·人工智能·python·深度学习·算法·贪心算法
Leuanghing5 小时前
【Leetcode】11. 盛最多水的容器
python·算法·leetcode
xinxiyinhe7 小时前
如何设置Cursor中.cursorrules文件
人工智能·python
诸神缄默不语7 小时前
如何用Python 3自动打开exe程序
python·os·subprocess·python 3
橘子师兄7 小时前
分页功能组件开发
数据库·python·django