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())
相关推荐
akhfuiigabv1 小时前
使用Neo4j-Cypher-FT实现自然语言查询图数据库
数据库·python·oracle·neo4j
繁依Fanyi1 小时前
828华为云征文|华为Flexus云服务器搭建OnlyOffice私有化在线办公套件
服务器·开发语言·前端·python·算法·华为·华为云
zhangfeng11331 小时前
在 PyTorch 中,除了 pad_sequence 还有哪些其他处理序列数据的函数?时间序列数据 预处理
人工智能·pytorch·python·深度学习
python1562 小时前
Python Numpy布尔数组在数据分析中的应用
python·数据分析·numpy
AIAdvocate2 小时前
力扣-96.不同的二叉搜索树 题目详解
python·算法·动态规划
luthane2 小时前
python 实现entropy熵算法
python·算法·概率论
akhfuiigabv2 小时前
探索Timescale Vector与Postgres数据库的融合:AI应用的新选择
数据库·人工智能·python
hakesashou2 小时前
ruby和python哪个好学
开发语言·python·ruby
NiNg_1_2342 小时前
Python协程详解
开发语言·python
黑白子20002 小时前
python定时任务,定时爬取水质和天气
开发语言·python