资讯动态

PySide6 QMainWindow实战:主窗口五件套搭建与布局恢复

发布时间:2026/10/18 9:08:41 来源:尧图企业网站定制
PySide系列写到第七篇QMainWindow终于登场。很多朋友学了QLabel、QPushButton、QLineEdit之后会觉得每个控件都认识但真要写一个能交差的桌面程序还是不知道从哪下手。QMainWindow就是那套“能交差的房子骨架”——菜单栏、工具栏、状态栏、停靠面板外加中间一大块工作区它全帮你搭好了。这个类适合所有做工具类软件的开发者无论你是做内部管理工具、图像批处理、数据报表客户端还是个人效率小软件基本上都绕不开它。今天这篇我打算抛开官方文档的干巴巴叙述直接从实际窗口拆开揉碎讲最后给你一份可以直接复制改用的模板。1. QMainWindow到底解决了什么问题1.1 为什么非要有一个主窗口框架用QWidget也能弹出一个窗口也可以在窗口上摆按钮、放输入框为什么Qt还要单独设计一个QMainWindow这个问题我刚学的时候也困惑过。答案其实很简单桌面软件绝大多数不是单页面而是“壳 页面”的结构。壳负责承载菜单、工具栏、状态栏这些全局性的东西页面才是真正的业务操作区域。QWidget适合做页面本身而QMainWindow天生就是那个壳。想象你打开一个记事本上面有菜单、有工具栏、底下有字数状态中间是一块白纸。如果你用QWidget从零搭这套东西也不是做不到但你需要自己处理菜单栏的定位、工具栏的拖拽、窗口缩放时各区域如何联动、菜单项和工具栏按钮怎么共用一个动作。这些麻烦事QMainWindow内部已经安排好了一套成熟的布局策略你要做的只是往对应区域里填东西。从Qt的类层次来看QMainWindow继承自QWidget所以它本质上还是一个窗口但它额外提供了一个私有的布局系统。这个系统把窗口划分成几个固定区域菜单栏区、工具栏区、停靠窗格区、状态栏区以及最中间的中心部件区。中心部件是唯一必须设置的区域其他几个区域都可以不出现但它们一旦出现布局就会自动处理不需要你手动算坐标。1.2 和普通QWidget摆在窗口上的本质区别普通QWidget的布局是“内容决定位置”你放一个QVBoxLayout控件从上往下排窗口跟着内容走。QMainWindow的布局则是“区域决定内容”你先定好哪个区放什么剩下的位置关系由框架保证。举个例子在QWidget里你创建一个菜单栏可以用QVBoxLayout先把menuBar放在顶部再把content放在下面代码如下所示。from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QMenuBar, QTextEdit app QApplication([]) w QWidget() layout QVBoxLayout(w) bar QMenuBar(w) editor QTextEdit(w) layout.addWidget(bar) layout.addWidget(editor) w.show() app.exec()这段代码能跑但菜单栏实际上只是“碰巧”贴在了顶部。一旦你把窗口高度拉大菜单栏和编辑器之间的距离不会发生任何规则性的适配你再想往菜单栏右边加一个“视图”菜单按钮还得手动找坐标。QMainWindow就不一样你直接调用menuBar()获得系统管理的菜单栏调用setCentralWidget设置中心内容区框架会始终把菜单栏固定在顶部把中心区域撑满剩余空间并且两者之间没有手动计算的距离。说到底QMainWindow省掉的是你反复处理“顶部栏和内容区相对位置”的重复劳动而且它还把菜单、工具栏、状态栏这几种桌面软件高频区域做成了“接口式”的访问方式。你越早习惯这个思维后面写多窗口、多文档软件就越轻松。2. 搭建主窗口五件套2.1 中心部件setCentralWidget不能省QMainWindow上如果有菜单栏、工具栏、状态栏它们的尺寸依赖中心区域来计算。中心区域如果没设置默认是一个空白QWidget窗口打开后中间一块灰白的无效区域用户感觉很奇怪。所以只要你想做一个能用的窗口第一步永远是setCentralWidget。但是很多人会在这里踩第一个坑如果我中心区域要放好几个控件怎么办直接setCentralWidget一个布局不行因为setCentralWidget只接受QWidget不接受QLayout。正确做法是先创建一个QWidget作为容器然后在这个容器里设置布局再把容器交给setCentralWidget。from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QPushButton class MainWindow(QMainWindow): def __init__(self): super().__init__() center QWidget() layout QVBoxLayout(center) btn QPushButton(点击我) layout.addWidget(btn) self.setCentralWidget(center)如果中心区域需要左右分栏就直接用QSplitter需要多页面就用QTabWidget需要显示表格就放QTableView。QMainWindow不会限制中心区域的内容它只会把最合适的空间全部交给这个部件。我个人的建议中心部件不要直接setCentralWidget业务控件而是先包一层QWidget哪怕暂时只有一个控件也包一层。这不算过度设计因为后续插入状态提示、浮动面板、遮罩层的时候你可以在容器上做统一控制不用反过来再改中心部件结构。2.2 菜单栏从menuBar到QAction菜单栏是QMainWindow最大的功能区之一调用menuBar()就能拿到菜单栏实例。你不应该在多个地方重复调用menuBar()Qt一直返回同一个对象你在构造方法里拿到后用一个属性存起来就行。菜单的核心不是QMenu而是QAction。很多人一开始堆了一堆QMenu发现菜单项点击没反应就是因为信号全挂在QMenu上了。正确的做法是先创建QAction给这个动作关联槽函数再把QAction添加到QMenu里。菜单栏本身只负责展示QMenu只管把动作分组成列表真正携带业务逻辑的是动作对象。from PySide6.QtGui import QAction, QKeySequence exit_action QAction(退出, self) exit_action.setShortcut(QKeySequence.StandardKey.Quit) exit_action.setStatusTip(关闭当前窗口) exit_action.triggered.connect(self.close) file_menu self.menuBar().addMenu(文件) file_menu.addAction(exit_action)QAction还有一个好处是它可以在多个界面复用。菜单里放一个“打开”工具栏里也放一个“打开”还有右键菜单也放一个它们都指向同一个QAction。信号连接只需要做一次后续所有入口都会触发同一个逻辑。这是桌面软件里很常见的模式也是QAction存在的意义。习惯上菜单栏的顶层菜单不会直接挂按钮动作而是用addMenu挂子菜单。比如“文件”下面有“新建”“打开”“导出”“导出”下面还可以再嵌套一层。QAction和QMenu混合使用只要层级别太深用户不会反感。我自己写代码时菜单层级最多到两层第三层在桌面软件里很难被用户发现。2.3 工具栏和菜单共用一套动作工具栏本质上是一个可视化的快捷入口集合里面放的主要是QToolButton。但在Qt的框架下你不需要手动创建QToolButton只要往QToolBar里addAction它就会自动生成一个按钮。创建工具栏建议先给工具条起一个对象名尤其以后要保存布局状态时没有唯一对象名工具箱的位置、浮动状态可能保存错乱。from PySide6.QtWidgets import QToolBar toolbar QToolBar(主工具栏, self) toolbar.setObjectName(MainToolBar) toolbar.setMovable(False) toolbar.setIconSize(QSize(20, 20)) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon) self.addToolBar(toolbar) toolbar.addAction(self.new_action) toolbar.addAction(self.open_action) toolbar.addSeparator() toolbar.addAction(self.save_action)设置文字图标下置、图标大小这些细节很多人会忽略但实际使用体验差别很大。如果你只添加文字不加图标整个工具条会显得死板如果你的动作有标准图标直接用QStyle.StandardPixmap或者系统图标库比到处找图标文件更省事。工具栏默认是可以拖出来变成浮动小窗口的。有些内部工具不需要这个功能可以在setMovable(False)关掉但要注意这也会让你的工具栏无法按用户习惯调整顺序。这个取舍要看你具体产品定位不是越还原默认越好。2.4 状态栏showMessage和addWidget要分清状态栏是桌面软件里最容易被人遗忘又最难做得细节到位的地方。QMainWindow.statusBar()返回状态栏实例它对外的接口主要有两类一类是临时的提示信息另一类是常驻的小部件。临时提示最常见的是动作悬停提示比如“打开文件”“保存当前内容”。在QAction上设置setStatusTip之后鼠标悬停到菜单项或工具栏按钮上状态栏就会自动显示对应的文字这个功能不需要你自己连接信号。self.statusBar().showMessage(就绪, 2000)showMessage可以带一个时间参数单位是毫秒。写法如上面代码状态栏会先显示“就绪”两秒后自动清空。这种形式适合一次性反馈比如保存完成、复制成功。如果你希望状态栏一直显示某个信息比如当前文档字数、当前行号那就不是showMessage的事了。你需要创建一个QLabel或者QProgressBar然后addWidget进去。addPermanentWidget是状态栏的右侧常驻区比如一些软件右下角的“准备好了”就放这里。self.status_label QLabel(共 0 行) self.statusBar().addPermanentWidget(self.status_label)一个很常见的错误是把QLabel直接setText在窗口上或者每次更新都重新创建控件。状态栏的控件应该只创建一次后续只调用setText更新文本。我在实际项目里发现状态栏也会因为某些临时消息抢走常驻文字而闪烁这时候需要做一个取舍要么只用临时消息要么只用常驻控件不要在同一时刻混用否则用户看不清你在提示什么。2.5 Dock窗格浮动面板的正确打开方式QDockWidget是QMainWindow隐藏的大杀器。它可以让一个面板停靠在窗口的上下左右也可以拖出来成为独立浮动窗口。很多专业软件里的“图层”“属性”“资源管理器”面板在Qt里都是用QDockWidget实现的。用起来并不复杂from PySide6.QtWidgets import QDockWidget, QListWidget dock QDockWidget(文件列表, self) dock.setObjectName(FileDock) list_widget QListWidget() list_widget.addItems([文档1.txt, 文档2.py, 配置.json]) dock.setWidget(list_widget) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)addDockWidget的第二个参数是停靠位置左侧就用LeftDockWidgetArea右侧就是RightDockWidgetArea上下对应Top和Bottom。Dock一旦添加用户可以自己拖动它改变停靠方向也可以把它拖出来变成独立窗口。控制Dock的细节有几个地方容易出问题。setFeatures控制用户能对Dock做什么比如是否需要可关闭、可移动、可浮动。如果你想做一个始终锁死在右侧的面板可以写成dock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable)但这会带来一个体验问题Dock一旦被用户关闭菜单里如果没有重新开启的入口它就再也找不回来了。所以正常的做法是在“视图”菜单里放一个可以勾选的QAction通过Dock的可见性状态同步这个动作的勾选状态用户在关闭后又可以重新打开。3. 代码实例从零拼出一个可复用的主窗口模板3.1 整体结构设计思路与其讲一堆零碎API不如直接用一份可以改的模板来讲。下面这个窗口模拟的是一个小型文本编辑器中心是文本编辑区左侧有一个文件列表Dock顶部有菜单和工具栏底部有状态栏。你复制到PySide6环境里就能跑。from PySide6.QtWidgets import ( QApplication, QMainWindow, QTextEdit, QToolBar, QDockWidget, QListWidget, QLabel, QWidget, QVBoxLayout ) from PySide6.QtGui import QAction, QKeySequence from PySide6.QtCore import Qt, QSize import sys class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(PySide6 主窗口模板) self.resize(1000, 700) self._create_actions() self._create_central() self._create_menus() self._create_toolbars() self._create_statusbar() self._create_docks() def _create_actions(self): self.new_action QAction(新建, self) self.new_action.setShortcut(QKeySequence.StandardKey.New) self.new_action.triggered.connect(self._on_new) self.save_action QAction(保存, self) self.save_action.setShortcut(QKeySequence.StandardKey.Save) self.save_action.triggered.connect(self._on_save) self.exit_action QAction(退出, self) self.exit_action.setShortcut(QKeySequence.StandardKey.Quit) self.exit_action.triggered.connect(self.close) self.toggle_dock_action QAction(显示文件列表, self) self.toggle_dock_action.setCheckable(True) self.toggle_dock_action.setChecked(True) self.toggle_dock_action.triggered.connect(self._on_toggle_dock) def _create_central(self): self.editor QTextEdit() self.setCentralWidget(self.editor) def _create_menus(self): menubar self.menuBar() file_menu menubar.addMenu(文件) file_menu.addAction(self.new_action) file_menu.addAction(self.save_action) file_menu.addSeparator() file_menu.addAction(self.exit_action) view_menu menubar.addMenu(视图) view_menu.addAction(self.toggle_dock_action) def _create_toolbars(self): toolbar QToolBar(主工具栏, self) toolbar.setObjectName(MainToolBar) toolbar.setMovable(False) toolbar.setIconSize(QSize(20, 20)) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) toolbar.addAction(self.new_action) toolbar.addAction(self.save_action) self.addToolBar(toolbar) def _create_statusbar(self): self.status_label QLabel(就绪) self.statusBar().addWidget(self.status_label) self.statusBar().showMessage(窗口加载完成, 3000) def _create_docks(self): self.file_dock QDockWidget(文件列表, self) self.file_dock.setObjectName(FileDock) self.file_list QListWidget() self.file_list.addItems([readme.md, main.py, config.json]) self.file_dock.setWidget(self.file_list) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.file_dock) self.file_dock.visibilityChanged.connect(self.toggle_dock_action.setChecked) def _on_new(self): self.editor.clear() self.statusBar().showMessage(已新建文档, 2000) def _on_save(self): self.statusBar().showMessage(保存成功, 2000) def _on_toggle_dock(self, checked): self.file_dock.setVisible(checked) if __name__ __main__: app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec())我刻意把每个区域的创建动作拆成了独立的_create_xxx方法这样在真正项目里你可以直接在一个方法里替换业务控件不会把几百行代码堆到一个__init__里。很多初学者喜欢把初始化全写在一起看起来上百行实际上很难修改。按区域分方法是QMainWindow项目里我最推荐的组织方式。3.2 动作绑定与信号槽注意事项上面代码里new_action、save_action都连接了triggered信号。这里有个细节QAction的triggered信号默认会传递一个布尔值这个布尔值代表动作是否被选中。如果你的槽函数不需要接收参数直接connect到一个无参函数没问题如果你连接了一个带参数的函数比如_check_exit(bool)那就要仔细处理这个布尔值。checkable的QAction更要注意。比如toggle_dock_action用户点击菜单项时它的checked状态先翻转然后触发triggered信号槽函数里收到checked参数再去控制Dock的可见性。但Dock可能被用户直接点关闭按钮关掉这时Dock的visibilityChanged信号触发了又把toggle_dock_action的选中状态同步过去。这种双向同步如果不做就会出现菜单里显示“显示文件列表”钩子但Dock明明已经打开的错乱状态。另一种常见错误是连续多次连接同一个QAction的triggered信号导致每次点击动作执行两遍方法。尤其是在工具栏和菜单都addAction同一个动作时Qt不会重复发送信号但如果你在手写代码时不小心把同一动作的triggered连接了两次点击一次就会回调两次。排查的时候不要只看界面先把信号连接数捋一遍。3.3 中心区域如何继续扩展布局模板里中心区域只有一个QTextEdit真实项目几乎都会更复杂。比如要做一个文本编辑器中央可能是标签页做图像软件中央可能是画布加图层面板。这种时候不要直接改QTextEdit而是构造一个中心容器用布局把多个控件组合起来。from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget center QWidget() layout QVBoxLayout(center) tabs QTabWidget() tabs.addTab(QTextEdit(), 未命名) layout.addWidget(tabs) self.setCentralWidget(center)中心容器设立后窗口大小变化时QTabWidget会自适应填满剩余空间其他不需要跟随伸缩的小控件可以用固定高度或最大高度限制。比较关键的一点是QMainWindow的中心区域不要和菜单栏、工具栏混在一个布局里中心区域就是QMainWindow自己的一个区域你只管往这个区域塞一个“大组件”。至于是不是用QWidget包一层其实可以看习惯我建议包一层因为以后替换中心内容时只需要改这一个变量。4. 实际项目中的布局与状态保存经验4.1 让用户记住上次窗口的样子桌面工具型软件用户最烦的一件事就是每次打开都要重新调窗口大小、重新拖一遍工具栏、重新展开侧边栏。QMainWindow自带了保存和恢复布局的能力比你自己存x、y坐标可靠得多。保存窗口几何信息用saveGeometry保存菜单栏、工具栏、Dock布局用saveState。两套数据建议一起存。窗口几何信息包括尺寸、位置、是否最大化布局信息包括哪些Dock显示了、每个Dock在哪个方向、工具栏在哪个位置。from PySide6.QtCore import QSettings settings QSettings(你的公司, 工具名称) settings.setValue(geometry, self.saveGeometry()) settings.setValue(windowState, self.saveState())读取时一般放在窗口显示之前。geometry settings.value(geometry) if geometry: self.restoreGeometry(geometry) state settings.value(windowState) if state: self.restoreState(state)这里有一点要特别注意restoreGeometry返回的是QByteArray在QSettings里它可能会被自动转成QVariant。PySide6里一般需要做类型转换你直接传回restoreGeometry通常没问题但如果你存储的是base64后的字符串就必须先转成QByteArray否则会报类型错误。4.2 保存时机和恢复时机的坑保存时机最好选在closeEvent里而不是窗口关闭之后。因为窗口关闭后对象可能已经销毁再调saveGeometry可能拿不到正确数据。def closeEvent(self, event): settings QSettings(你的公司, 工具名称) settings.setValue(geometry, self.saveGeometry()) settings.setValue(windowState, self.saveState()) super().closeEvent(event)恢复时机也很有讲究。restoreState必须等所有Dock和ToolBar都创建完成并加入窗口后才能调用。如果你在__init__里先调restoreState再创建Dock那Dock的布局就恢复不了。所以在模板代码里创建顺序应该是先创建动作再创建Dock和工具栏最后才能调用restoreState。另外如果你在窗口中添加了新的Dock但旧版本保存的state里没有这个DockQt会把它放到默认位置。如果你删除了一个旧Dock保存的state里有它的标识Qt会忽略不存在的部分这个机制在升级版本时还挺宽容的。关键点是每个Dock和ToolBar都要setObjectName否则保存的state无法精确关联恢复时可能错乱。这个是文档里很难第一时间注意到的细节。4.3 多个Dock如何控制排列当窗口里不止一个Dock时问题就来了A在左边B也在左边怎么让它们上下堆叠而不是互相挤占你可以用splitDockWidget来指定两个Dock的相对位置。比如先把A停靠在左边再把B分裂在A的下方self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock_a) self.splitDockWidget(dock_a, dock_b, Qt.Orientation.Vertical)splitDockWidget的第三个参数是方向Vertical表示上下堆叠Horizontal表示左右并排。这个操作必须在两个Dock都已经addDockWidget之后调用否则会失败。还有一种常用布局是让DockA和DockB在一个区域里以标签页形式显示这时就调用tabifyDockWidgetself.tabifyDockWidget(dock_a, dock_b)tabify之后两个Dock会共享同一个空间底下出现标签页切换。这种模式很适合做“图层/属性”这一类的侧边面板。如果之后你想再把某个Dock从标签页中拆出来直接双击它的标签区域它就会变成浮动窗口。这里的交互逻辑很接近现代IDE但纯粹靠代码控制时需要先调好顺序否则用户视觉上会觉得很乱。5. 常见问题与排查记录5.1 菜单栏不显示或者菜单项灰掉先确认你是不是把菜单栏藏起来了。有人会为了做无边框窗口调用menuBar().setVisible(False)后面想恢复时忘了打开于是菜单栏怎么也不出现。这时直接查setVisible和hide调用即可。菜单项是灰掉的通常是QAction的enabled状态为False。有些框架代码里为了做权限控制会在初始化时把动作禁用掉后面忘记恢复。你可以在菜单栏动作的triggered信号处临时打印一下enabled值来定位。菜单项变灰还可能是动作没有被添加到任何QMenu只在工具栏里addAction了。QAction的visible状态不影响菜单显示只有把它加入某个菜单菜单才会出现对应项。5.2 triggered和toggled到底用哪个这是QAction新手最容易混淆的问题。triggered在动作被触发时发出toggled在动作的checkable状态变化时发出。普通QAction点击一次triggered会触发而toggled只在checked状态被switch时触发。如果QAction不是checkable它一般不会发出toggled因为它的选中状态始终是False。所以当你做一个“打开/隐藏侧边栏”的切换菜单项时要使用checkable并连接triggered因为用户点击动作Qt会自动切换checked状态后触发triggered。如果连接toggled在某些状态恢复的场景下也会触发但信号参数含义更别扭。简单经验业务逻辑用triggered界面状态同步用toggled。5.3 关掉Dock后再也找不到它这个问题几乎每个项目都会遇到。用户点Dock右上角X把面板关掉然后想找回来菜单里没有“显示xxx”最后只能重启程序。解决办法是给Dock设定一个对应的view action像模板里的toggle_dock_action那样放在“视图”菜单里。注意Dock关闭时它不会自动销毁只是隐藏了所以setVisible(True)就能再次显示。如果你想在Dock关闭时执行一些保存操作可以把逻辑放在visibilityChanged信号的槽函数里。不过这个信号在Dock第一次创建时也会触发刚启动时就可能出现一次False逻辑里要做好过滤不然会误触发。5.4 窗口关闭后程序不退出QMainWindow关闭后如果不调用app.setQuitOnLastWindowClosed(True)默认情况下最后一个窗口关闭时应用会退出。但如果你创建了多个顶层窗口、或者有系统托盘、或者存在非Qt事件循环的子线程窗口关闭后程序可能仍然驻留后台。排查顺序是先看是否有其他顶层窗口再看是否创建了QSystemTrayIcon最后看有没有没有结束的子线程。还有一个容易忽略的点如果主窗口的closeEvent里调用了event.ignore()关闭事件被忽略程序自然退不了。这往往是有意为之比如提示用户“内容未保存”。你在排查时先看closeEvent里是不是有忽略逻辑再看别的因素。5.5 子控件开在局部变量上导致闪退这种问题在PySide6里很典型。很多人只在方法内部创建了一个QTextEdit没有把它赋值给self然后setCentralWidget。看起来没问题因为setCentralWidget内部会把widget添加为子对象。但有时你会在局部变量里创建QMenu、QToolBar甚至Dock并且没有调用addAction、addToolBar、addDockWidget只是把它挂到某个parent下面这时候如果方法执行完Python端的对象引用被回收Qt的C对象可能被提前销毁后期访问就会出现崩溃或者无提示空白。正确的做法是凡是希望窗口长期持有的对象都用self.xxx保存引用如果只是临时工具条用addToolBar返回的指针就可以不用额外保存。这条金科玉律适用所有PySide界面对象。6. 给系列新人的一句实在话QMainWindow并不复杂复杂的是你搭建窗口时混乱的初始化顺序。我在实际开发中有一个很小的习惯每次新建QMainWindow子类都会先把saveGeometry和saveState接上再往窗口里塞东西。这样一个上午敲出来的工具第二天打开依然记着上次的位置和工具栏摆放用户感知差别很大。这也是我为什么把QMainWindow放在系列第七篇的原因——它不是压轴但它是大多数真正生产力软件的起点。所以不要急着去炫什么自定义无边框窗体先把这五件套用熟后续很多窗口特效都建立在这套结构之上。下一篇我会拆一个真正带多文档标签页的编辑器出来到时候你再看这份模板会发现今天这些API刚好够用一样都不会白学。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑