Windows开发:关于微软媒体基础(Microsoft Media Foundation)

本文深入介绍了微软的MediaFoundation框架,它是Windows平台的下一代多媒体处理技术。MediaFoundation支持流媒体、压缩算法,如无损和有损压缩,以及各种媒体容器格式,如MP4和ASF。文章详细阐述了基础概念,如流、压缩、媒体容器和格式,并提及了增强视频渲染器(EVR)的功能,包括其在视频显示和多流混合中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


注:文章由作者翻译和资料整理,转载请注明出处

序言

介绍

Microsoft Media Foundation是适用于Windows的下一代多媒体平台,使开发人员,消费者和内容提供商能够以更强的健壮性,无与伦比的质量和无缝交互来拥抱新一轮的高级内容。
Media Foundation需要Windows Vista或更高版本。它使用组件对象模型(COM),并且需要C / C ++。Microsoft不提供Media Foundation的托管API。
Media Foundation API是Windows SDK的一部分。要开发Media Foundation应用程序,请安装最新版本的Windows SDK

可用的基础工具

基础概念 Essential Concepts

流 Streams

流,是一个统一格式的媒体数据序列。其中最常见的是音频流和视频流,但是一个流也可以包含几乎任何种类的数据,包括文本、脚本命令、以及静止图片。在这篇文章里的术语“流”并不特指网络传输中的流。用于本地播放的媒体文件也包含“流”。
通常一个媒体文件要么包含单一的音频流,要么是一个视频流和一个音频流。然而一个媒体文件也可能包含很多个相同类型的流。例如一个视频文件可能包含多种语言的音频流。在运行过程中应用程序去选择播放哪一种语言的音频。

压缩 Compression

压缩是指通过删除冗余信息来减少数据流大小的过程。
压缩算法分为两大类:

  • 无损压缩:使用无损算法,复原后的数据与原始数据相同
  • 有损压缩:使用有损算法,复原后的数据是原始数据的近似值,但不是准确值

在大多数其他领域中,有损压缩是不可接受的。(想象一下电子表格的“近似值”!)但是有两个原因,有损压缩方案非常适合音频和视频。
第一个原因与人类感知的物理学有关。当我们听复杂的声音(例如音乐录音)时,该声音中包含的某些信息是听不到的。借助信号处理理论,可以分析和分离无法感知的频率。可以消除这些频率,而不会产生感知影响。尽管重建的音频将与原始音频不完全匹配,但它对听众来说听起来是一样的。类似的原理也适用于视频。
其次,根据预期目的,声音或图像质量的某些下降可能是可以接受的。例如,在电话中,音频通常被高度压缩。结果足以进行电话交谈-但却不能用来听音乐。
压缩也称为编码,将进行编码的设备称为编码器。反向过程是解码,该设备自然称为解码器。编码器和解码器的总称是编解码器。编解码器可以用硬件或软件来实现。

媒体容器 Media Containers

我们很少将原始音频或视频流存储为文件,或者直接通过网络发送。一方面,如果不事先知道要使用哪个编解码器,就不可能解码这样的流。因此,媒体文件通常至少包含以下某些元素:

  • 用于描述流的数量,以及每个流的格式等的文件头。
  • 允许对内容进行随机访问的索引。
  • 描述内容的元数据(例如,文件的作者或标题)。
  • 数据包头,以实现网络传输或随机访问。

本文档使用术语“container ”来描述流,标头,索引,元数据等的整个包。之所以使用术语“容器”而不是“文件”,是因为某些容器格式是为直播而设计的。应用程序可以实时生成容器,而无需将其存储到文件中。

媒体容器的早期示例是AVI文件格式。其他示例包括MP4和高级系统格式(ASF)。可以通过文件扩展名(例如.mp4)或MIME类型来标识容器。

下图显示了媒体容器的典型结构。该图不代表任何特定格式;因为每种格式的细节差异很大。
在这里插入图片描述
请注意,图中所示的结构是分层的,标头信息出现在容器的开头。这种结构是许多(但不是全部)容器格式的代表。还要注意,数据部分包含交错的音频和视频数据包。这种交织在媒体容器中很常见。
术语多路复用是指对音频和视频流进行打包并将包交织到容器中的过程。从打包数据重新组合流的反向过程称为多路分解

格式 Formats

在数字媒体中,格式一词并不明确。格式可以指代编码的类型(例如H.264视频)或容器的类型(例如MP4)。这种区别通常会使普通用户感到困惑。媒体格式的名称并不总是有用。例如,MP3既指编码格式(MPEG-1音频第3层)又指文件格式。
但是,区别很重要,因为读取媒体文件实际上涉及两个阶段:

  1. 首先,必须对容器进行解析。在大多数情况下,直到完成此步骤才能知道流的数量和每个流的格式。
  2. 接下来,如果流被压缩,则必须使用适当的解码器对其进行解码。

这种情况下自然衍生了一种软件设计模式,使用单独的组件来解析容器和解码流。另外,此方法适合于插件模型,以便第三方可以提供自己的解析器和编解码器。在Windows上,组件对象模型(COM)提供了一种将API与实现分开的标准方法,这是任何插件模型所必需的,这也是Media Foundation使用COM接口的原因之一。
下图显示了用于读取媒体文件的组件:
在这里插入图片描述
编写媒体文件还需要两个步骤:

  1. 编码未压缩的音频/视频数据。
  2. 将压缩数据放入特定的容器格式。

下图显示了用于写入媒体文件的组件:
在这里插入图片描述

增强的视频渲染器 Enhanced Video Renderer(EVR)

增强的视频渲染器(EVR)是在用户的监视器上显示视频的组件。存在两种版本的EVR:

  • EVR媒体接收器,用于Media Foundation应用程序。
  • EVR筛选器,用于DirectShow应用程序。

两种版本都使用相同的内部对象来渲染视频,并且它们共享许多相同的接口。
EVR最多可以混合16个视频流。第一个输入流称为参照流。参照流通常在z-order的首位。
(窗口的z-order表明了在一堆堆叠的窗口中该窗口的位置。窗口的堆叠是沿bai着一条虚轴—Z轴,从屏幕里向外延伸的。在z-order顶端的窗口将会压在其他窗口上。在z-order底部的窗口将会被其他窗口所压。 系统在一个单一的表中维护z-order 。根据窗口是不是顶级窗口,父窗口,子窗口,系统把他们添加到z-order中。顶级窗口将会堆叠在所有非顶级窗口之上,不管它是否是活动窗口或者是前台窗口。)
任何其他流称为子流,并在参照流的顶部混合。应用程序可以更改子流的z-order,但是任何子流都不能在z-order中排在首位。
图形驱动程序确定支持哪些视频格式,但通常仅限于以下几种:

  • 参照流:逐行或交错式的没有单个像素alpha值的YUV(例如NV12或YUY2),或渐进式RGB。
  • 子流:含逐像素alpha值的渐进式YUV,例如AYUV或AI44。

可用的子流格式可能取决于参考流的格式。
在内部,EVR使用称为混合器(mixer)的对象将来自输入流的帧合成到一个表面上进行渲染。混合器还执行去隔行和色彩校正。混合器最终输出混合后的视频帧。再通过第二个对象演示器(presenter)将视频帧渲染到屏幕。演示器安排渲染帧的时间并管理Direct3D设备。应用程序可以提供混合器或演示器的自定义实现。
在这里插入图片描述

把一下PyQt5的代码变成PyQt6的代码 代码: from PyQt5.QtCore import QUrl, Qt, QTranslator, QLocale, QLibraryInfo from PyQt5.QtGui import QFont, QIcon, QKeySequence from PyQt5.QtWidgets import (QApplication, QMainWindow, QToolBar, QLineEdit, QAction, QStatusBar, QMessageBox, QTabWidget) from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage, QWebEngineSettings from datetime import datetime import sys,os os.environ["QT_MULTIMEDIA_PREFERRED_PLUGINS"] = "windowsmediafoundation" os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--enable-gpu-rasterization --enable-zero-copy" os.environ["QT_MEDIA_BACKEND"] = "ffmpeg" class CustomWebView(QWebEngineView): def __init__(self, parent=None,window=None): super().__init__(parent) # 捕获链接悬停事件获取URL self.window = window self.page().linkHovered.connect(self.save_hovered_url) self.hovered_url = QUrl() self.page().profile().setHttpUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") settings = self.settings() settings.setAttribute(QWebEngineSettings.PlaybackRequiresUserGesture, False) # 允许自动播放 settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) # 启用插件 settings.setAttribute(QWebEngineSettings.WebGLEnabled, True) # 支持WebGL settings.setAttribute(QWebEngineSettings.LocalStorageEnabled, True) # 启用本地存储 settings.setAttribute(QWebEngineSettings.JavascriptCanOpenWindows, True) def save_hovered_url(self, url): """保存鼠标悬停的链接地址""" self.hovered_url = QUrl(url) def createWindow(self, type): """重写窗口创建方法""" url = QUrl(self.url().toString()) if self.hovered_url.isValid(): try: self.window.add_new_tab(self.hovered_url) self.load(url) except: self.load(self.hovered_url) return self # 返回当前视图实例 def save_screenshot(self): current_time = datetime.now() photo_name = current_time.strftime("%Y%m%d_%H%M%S_%f") + '.png' save_folder = r'save\photo' os.makedirs(save_folder, exist_ok=True) save_path = os.path.join(save_folder, photo_name) photo = self.grab() photo.save(rf'save\photo\{photo_name}') class BrowserWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PyQt5 浏览器") self.setGeometry(100, 100, 1200, 800) # 创建标签页 self.tabs = QTabWidget() self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect(self.close_tab) self.setCentralWidget(self.tabs) # 创建初始标签页 self.add_new_tab(QUrl('https://www.baidu.com'), "主页") # 创建工具栏 self.create_toolbar() # 创建菜单栏 self.create_menus() # 创建状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) # 显示窗口 self.show() def add_new_tab(self, url=None, label="新标签页"): """添加新标签页""" if url is None: url = QUrl('https://www.baidu.com') browser = CustomWebView(window=self) settings = browser.settings() settings.setFontFamily(QWebEngineSettings.StandardFont, 'Microsoft YaHei UI') settings.setFontFamily(QWebEngineSettings.FixedFont, "Courier New") browser.setUrl(url) browser.urlChanged.connect(self.update_urlbar) browser.loadProgress.connect(self.update_progress) browser.loadFinished.connect(self.update_title) index = self.tabs.addTab(browser, label) self.tabs.setCurrentIndex(index) return browser def close_tab(self, index): """关闭标签页""" if self.tabs.count() < 2: return self.tabs.removeTab(index) def create_toolbar(self): """创建工具栏""" toolbar = QToolBar("导航工具栏") toolbar.setMovable(False) self.addToolBar(toolbar) # 后退按钮 back_btn = QAction(QIcon("back.png"), "后退", self) back_btn.setStatusTip("后退到上一页") back_btn.triggered.connect(lambda: self.current_browser().back()) toolbar.addAction(back_btn) # 前进按钮 forward_btn = QAction(QIcon("forward.png"), "前进", self) forward_btn.setStatusTip("前进到下一页") forward_btn.triggered.connect(lambda: self.current_browser().forward()) toolbar.addAction(forward_btn) # 刷新按钮 reload_btn = QAction(QIcon("reload.png"), "刷新", self) reload_btn.setStatusTip("刷新当前页面") reload_btn.triggered.connect(lambda: self.current_browser().reload()) toolbar.addAction(reload_btn) # 主页按钮 home_btn = QAction(QIcon("home.png"), "主页", self) home_btn.setStatusTip("返回主页") home_btn.triggered.connect(self.navigate_home) toolbar.addAction(home_btn) toolbar.addSeparator() # 地址栏 self.urlbar = QLineEdit() self.urlbar.returnPressed.connect(self.navigate_to_url) toolbar.addWidget(self.urlbar) # 停止加载按钮 stop_btn = QAction(QIcon("stop.png"), "停止", self) stop_btn.setStatusTip("停止加载当前页面") stop_btn.triggered.connect(lambda: self.current_browser().stop()) toolbar.addAction(stop_btn) def create_menus(self): """创建菜单栏""" # 文件菜单 file_menu = self.menuBar().addMenu("文件(&F)") new_tab_action = QAction(QIcon("new-tab.png"), "新建标签页", self) new_tab_action.setShortcut(QKeySequence.AddTab) new_tab_action.triggered.connect(lambda: self.add_new_tab()) file_menu.addAction(new_tab_action) new_window_action = QAction(QIcon("new-window.png"), "新建窗口", self) new_window_action.setShortcut("Ctrl+N") new_window_action.triggered.connect(self.new_window) file_menu.addAction(new_window_action) file_menu.addSeparator() save_page_action = QAction(QIcon("save.png"), "保存页面", self) save_page_action.setShortcut("Ctrl+S") save_page_action.triggered.connect(self.save_page) file_menu.addAction(save_page_action) file_menu.addSeparator() exit_action = QAction(QIcon("exit.png"), "退出", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # 编辑菜单 edit_menu = self.menuBar().addMenu("编辑(&E)") copy_action = QAction(QIcon("copy.png"), "复制", self) copy_action.setShortcut("Ctrl+C") copy_action.triggered.connect(self.copy) edit_menu.addAction(copy_action) paste_action = QAction(QIcon("paste.png"), "粘贴", self) paste_action.setShortcut("Ctrl+V") paste_action.triggered.connect(self.paste) edit_menu.addAction(paste_action) cut_action = QAction(QIcon("cut.png"), "剪切", self) cut_action.setShortcut("Ctrl+X") cut_action.triggered.connect(self.cut) edit_menu.addAction(cut_action) # 查看菜单 view_menu = self.menuBar().addMenu("查看(&V)") zoom_in_action = QAction(QIcon("zoom-in.png"), "放大", self) zoom_in_action.setShortcut("Ctrl++") zoom_in_action.triggered.connect(self.zoom_in) view_menu.addAction(zoom_in_action) zoom_out_action = QAction(QIcon("zoom-out.png"), "缩小", self) zoom_out_action.setShortcut("Ctrl+-") zoom_out_action.triggered.connect(self.zoom_out) view_menu.addAction(zoom_out_action) reset_zoom_action = QAction(QIcon("zoom-reset.png"), "重置缩放", self) reset_zoom_action.setShortcut("Ctrl+0") reset_zoom_action.triggered.connect(self.reset_zoom) view_menu.addAction(reset_zoom_action) # 历史菜单 history_menu = self.menuBar().addMenu("历史(&H)") back_action = QAction(QIcon("back.png"), "后退", self) back_action.setShortcut("Alt+Left") back_action.triggered.connect(lambda: self.current_browser().back()) history_menu.addAction(back_action) forward_action = QAction(QIcon("forward.png"), "前进", self) forward_action.setShortcut("Alt+Right") forward_action.triggered.connect(lambda: self.current_browser().forward()) history_menu.addAction(forward_action) # 帮助菜单 help_menu = self.menuBar().addMenu("帮助(&H)") about_action = QAction(QIcon("about.png"), "关于", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def current_browser(self): """获取当前标签页的浏览器实例""" return self.tabs.currentWidget() def navigate_home(self): """导航到主页""" self.current_browser().setUrl(QUrl("https://www.baidu.com")) def navigate_to_url(self): """导航到地址栏中的URL""" url = self.urlbar.text() if not url.startswith(('http://', 'https://')): url = 'http://' + url self.current_browser().setUrl(QUrl(url)) def update_urlbar(self, q): """更新地址栏显示""" self.urlbar.setText(q.toString()) self.urlbar.setCursorPosition(0) def update_progress(self, progress): """更新加载进度""" if progress < 100: self.status_bar.showMessage(f"加载中: {progress}%") else: self.status_bar.clearMessage() def update_title(self): """更新标签页标题""" title = self.current_browser().page().title() self.setWindowTitle(f"{title} - PyQt5 浏览器") self.tabs.setTabText(self.tabs.currentIndex(), title[:15] + '...' if len(title) > 15 else title) def new_window(self): """创建新窗口""" new_window = BrowserWindow() new_window.show() def save_page(self): """保存页面""" self.current_browser().save_screenshot() self.status_bar.showMessage("保存成功", 3000) def copy(self): """复制操作""" self.current_browser().page().triggerAction(QWebEnginePage.Copy) def paste(self): """粘贴操作""" self.current_browser().page().triggerAction(QWebEnginePage.Paste) def cut(self): """剪切操作""" self.current_browser().page().triggerAction(QWebEnginePage.Cut) def zoom_in(self): """放大页面""" self.current_browser().setZoomFactor(self.current_browser().zoomFactor() + 0.1) def zoom_out(self): """缩小页面""" self.current_browser().setZoomFactor(self.current_browser().zoomFactor() - 0.1) def reset_zoom(self): """重置缩放""" self.current_browser().setZoomFactor(1.0) def show_about(self): """显示关于对话框""" QMessageBox.about(self, "关于 PyQt5 浏览器", "这是一个使用 PyQt5 创建的浏览器\n\n" "版本: 1.0\n" "支持基本浏览功能、多标签页和菜单栏操作") if __name__ == "__main__": app = QApplication(sys.argv) app.setApplicationName("PyQt5 浏览器") app.setWindowIcon(QIcon("browser.png")) app.setFont(QFont('Microsoft YaHei UI', 10, QFont.Normal)) translator = QTranslator() locale = QLocale.system().name() if translator.load("qt_"+ locale, QLibraryInfo.location(QLibraryInfo.TranslationsPath)): app.installTranslator(translator) # 设置默认样式 app.setStyle("Fusion") window = BrowserWindow() sys.exit(app.exec_())
最新发布
07-16
<think>我们正在将之前编写的PyQt5浏览器代码迁移到PyQt6,并且要求添加标签页功能(tabbed interface)。下面我们将完成以下任务: 1. 将PyQt5代码转换为PyQt6 2. 添加多标签页支持(使用QTabWidget) 3. 确保菜单栏和导航功能在标签页中正常工作 迁移注意事项: - 导入模块:将PyQt5改为PyQt6 - 部分类的位置调整(例如,QtWebEngineWidgets在PyQt6中保持不变,但需要从PyQt6导入) - 信号和槽:在PyQt6中,信号名称可能略有变化,但大多数保持不变。注意:PyQt6中一些信号参数可能有变化,但在这个例子中,我们使用的信号(如urlChanged, loadFinished)在PyQt6中与PyQt5一致。 我们将创建一个支持多标签页的浏览器,每个标签页都是一个QWebEngineView。同时,菜单栏中的链接应该能够在当前活动标签页中打开。 步骤: 1. 创建主窗口,使用QTabWidget作为中心部件。 2. 初始时创建一个标签页(浏览器实例)。 3. 在菜单栏的“导航”菜单中添加网站链接,这些链接将在当前活动标签页中打开。 4. 实现新建标签页功能(通过菜单项和快捷键Ctrl+T)。 5. 实现关闭当前标签页功能(通过快捷键Ctrl+W)。 6. 更新工具栏和地址栏,使其与当前活动标签页同步。 注意:当切换标签页时,地址栏和窗口标题应该更新为当前标签页的URL。 我们将对之前的代码进行重构,将每个标签页封装为一个BrowserTab类(继承自QWidget),包含一个QWebEngineView和相关的状态。但为了简化,我们可以将每个标签页的视图直接放入QTabWidget中,并在主窗口中管理状态。 由于代码较长,我们将分步骤实现: 1. 导入模块:使用PyQt6 2. 创建主窗口类,初始化QTabWidget 3. 创建第一个标签页 4. 重构菜单栏和工具栏,使其操作当前活动标签页 5. 实现标签页切换时的状态同步 具体实现如下:</think># PyQt5 到 PyQt6 浏览器迁移指南(含标签页功能) 下面我将详细介绍如何将 PyQt5 浏览器应用迁移到 PyQt6,并添加标签页功能。迁移过程需要特别注意模块导入变化和API差异[^1]。 ## 完整迁移代码(PyQt6 + 标签页) ```python import sys from PyQt6.QtCore import QUrl, Qt from PyQt6.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QStatusBar, QMessageBox, QToolBar, QLineEdit, QTabWidget, QVBoxLayout, QWidget, QPushButton) from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtGui import QAction, QIcon, QKeySequence, QFont class BrowserTab(QWidget): """单个浏览器标签页""" def __init__(self, parent=None): super().__init__(parent) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # 创建浏览器组件 self.browser = QWebEngineView() self.browser.setUrl(QUrl("https://www.google.com")) layout.addWidget(self.browser) # 地址栏 self.url_bar = QLineEdit() self.url_bar.setPlaceholderText("输入网址或搜索内容...") self.url_bar.setFont(QFont("Arial", 10)) self.url_bar.returnPressed.connect(self.navigate_from_bar) layout.addWidget(self.url_bar) # 连接信号 self.browser.urlChanged.connect(self.update_url) self.browser.loadStarted.connect(lambda: parent.status_bar.showMessage("加载中...")) self.browser.loadFinished.connect( lambda success: parent.status_bar.showMessage("加载完成" if success else "加载失败", 3000)) def navigate_to(self, url): """导航到指定URL""" if not url.startswith(('http://', 'https://')): url = 'https://' + url self.browser.setUrl(QUrl(url)) self.url_bar.setText(url) def navigate_from_bar(self): """从地址栏导航""" self.navigate_to(self.url_bar.text()) def update_url(self, url): """更新URL显示""" self.url_bar.setText(url.toString()) class BrowserWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PyQt6 多标签浏览器") self.setGeometry(100, 100, 1200, 800) # 创建中心部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(0, 0, 0, 0) # 创建标签页组件 self.tabs = QTabWidget() self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect(self.close_tab) self.tabs.currentChanged.connect(self.update_window_title) main_layout.addWidget(self.tabs) # 添加新标签页按钮 new_tab_btn = QPushButton("+") new_tab_btn.setFixedSize(30, 30) new_tab_btn.clicked.connect(self.add_new_tab) self.tabs.setCornerWidget(new_tab_btn, Qt.Corner.TopRightCorner) # 创建状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) # 创建菜单栏 self.create_menu_bar() # 添加初始标签页 self.add_new_tab() def create_menu_bar(self): """创建菜单栏及其功能""" # 创建主菜单栏 menu_bar = self.menuBar() # macOS特殊处理 if sys.platform == 'darwin': menu_bar.setNativeMenuBar(False) # === 文件菜单 === file_menu = menu_bar.addMenu("文件(&F)") new_tab_action = QAction("新建标签页(&N)", self) new_tab_action.setShortcut(QKeySequence("Ctrl+T")) new_tab_action.triggered.connect(self.add_new_tab) file_menu.addAction(new_tab_action) close_tab_action = QAction("关闭当前标签页(&C)", self) close_tab_action.setShortcut(QKeySequence("Ctrl+W")) close_tab_action.triggered.connect(self.close_current_tab) file_menu.addAction(close_tab_action) file_menu.addSeparator() exit_action = QAction("退出(&X)", self) exit_action.setShortcut(QKeySequence("Ctrl+Q")) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # === 导航菜单 === nav_menu = menu_bar.addMenu("导航(&N)") # 添加常用网站 sites = [ ("百度", "https://www.baidu.com"), ("腾讯", "https://www.qq.com"), ("淘宝", "https://www.taobao.com"), ("知乎", "https://www.zhihu.com"), ("GitHub", "https://github.com"), ("Python官网", "https://www.python.org"), ("PyQt文档", "https://www.riverbankcomputing.com/static/Docs/PyQt6/") ] for name, url in sites: action = QAction(name, self) action.triggered.connect(lambda checked, u=url: self.navigate_in_current_tab(u)) nav_menu.addAction(action) # === 标签页菜单 === tab_menu = menu_bar.addMenu("标签页(&T)") next_tab_action = QAction("下一个标签页", self) next_tab_action.setShortcut(QKeySequence("Ctrl+Tab")) next_tab_action.triggered.connect(self.next_tab) tab_menu.addAction(next_tab_action) prev_tab_action = QAction("上一个标签页", self) prev_tab_action.setShortcut(QKeySequence("Ctrl+Shift+Tab")) prev_tab_action.triggered.connect(self.prev_tab) tab_menu.addAction(prev_tab_action) # === 帮助菜单 === help_menu = menu_bar.addMenu("帮助(&H)") about_action = QAction("关于浏览器(&A)", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def add_new_tab(self, url=None): """添加新标签页""" tab = BrowserTab(self) index = self.tabs.addTab(tab, "新标签页") self.tabs.setCurrentIndex(index) # 设置标签图标 tab.browser.iconChanged.connect(lambda icon: self.tabs.setTabIcon(index, icon)) # 设置标签标题 tab.browser.titleChanged.connect(lambda title: self.tabs.setTabText(index, title[:15] + "..." if len(title) > 15 else title)) if url: tab.navigate_to(url) def close_current_tab(self): """关闭当前标签页""" current_index = self.tabs.currentIndex() if self.tabs.count() > 1: self.tabs.removeTab(current_index) else: self.close() def close_tab(self, index): """关闭指定标签页""" if self.tabs.count() > 1: self.tabs.removeTab(index) else: self.close() def navigate_in_current_tab(self, url): """在当前标签页导航到URL""" current_tab = self.tabs.currentWidget() if current_tab: current_tab.navigate_to(url) def next_tab(self): """切换到下一个标签页""" count = self.tabs.count() if count > 1: current_index = self.tabs.currentIndex() self.tabs.setCurrentIndex((current_index + 1) % count) def prev_tab(self): """切换到上一个标签页""" count = self.tabs.count() if count > 1: current_index = self.tabs.currentIndex() self.tabs.setCurrentIndex((current_index - 1) % count) def update_window_title(self, index): """更新窗口标题""" if index >= 0: tab = self.tabs.widget(index) if tab and tab.browser: self.setWindowTitle(f"{tab.browser.title()} - PyQt6 浏览器") def show_about(self): """显示关于信息""" QMessageBox.about(self, "关于 PyQt6 浏览器", "PyQt6 多标签浏览器\n版本: 2.0\n支持多标签页浏览\n支持常用网站快捷导航") if __name__ == "__main__": app = QApplication(sys.argv) window = BrowserWindow() window.show() sys.exit(app.exec()) ``` ## PyQt5 到 PyQt6 迁移关键点 ### 1. 模块导入差异 ```python # PyQt5 from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QMainWindow from PyQt5.QtWebEngineWidgets import QWebEngineView # PyQt6 from PyQt6.QtCore import QUrl from PyQt6.QtWidgets import QMainWindow from PyQt6.QtWebEngineWidgets import QWebEngineView ``` ### 2. 枚举值变化 ```python # PyQt5 self.setCornerWidget(btn, Qt.TopRightCorner) # PyQt6 self.setCornerWidget(btn, Qt.Corner.TopRightCorner) ``` ### 3. 信号连接语法 ```python # PyQt5 (旧语法) self.browser.urlChanged.connect(self.update_url) # PyQt6 (推荐使用新语法) self.browser.urlChanged.connect(lambda url: self.update_url(url)) ``` ### 4. macOS 菜单栏处理 ```python # macOS 需要禁用原生菜单栏 if sys.platform == 'darwin': menu_bar.setNativeMenuBar(False) ``` ## 标签页实现原理 ### 1. 标签页管理架构 ``` QMainWindow ├── QTabWidget (标签页容器) │ ├── BrowserTab 1 (自定义标签页组件) │ │ ├── QWebEngineView (浏览器) │ │ └── QLineEdit (地址栏) │ ├── BrowserTab 2 │ └── ... └── QStatusBar ``` ### 2. 标签页生命周期管理 - **创建标签页**:`add_new_tab()` 方法创建新的 `BrowserTab` 实例 - **关闭标签页**:`close_tab()` 处理标签页关闭逻辑 - **标签页切换**:`currentChanged` 信号更新窗口标题 ### 3. 标签页同步机制 ```python # 图标同步 tab.browser.iconChanged.connect(lambda icon: self.tabs.setTabIcon(index, icon)) # 标题同步 tab.browser.titleChanged.connect(lambda title: self.tabs.setTabText(index, title)) ``` ## 数学原理:标签页管理算法 标签页导航使用循环队列原理实现标签切换: $$ \text{nextIndex} = (currentIndex + 1) \mod N $$ $$ \text{prevIndex} = (currentIndex - 1 + N) \mod N $$ 其中: - $currentIndex$ 是当前标签页索引 - $N$ 是标签页总数 - $\mod$ 是取模运算,确保索引在有效范围内[^2] ## 运行效果 1. 主窗口显示带完整菜单栏的多标签浏览器 2. 标签页顶部显示网站图标和标题 3. 右上角"+"按钮添加新标签页 4. "导航"菜单可快速访问常用网站 5. "标签页"菜单支持标签切换快捷键 6. 每个标签页有独立地址栏 7. 状态栏显示页面加载状态
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值