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
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
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())