
这次的内容很具体一个 GUI 项目从新建项目、界面布局到事件处理再到无边框窗口是一整套桌面客户端开发的前半程链路。很多人卡在“窗口能做出来但拖不动 / 缩放不了 / 事件不生效”这类问题上本质上不是单个 API 的问题而是事件分发、布局结构、窗口标志三者的配合出了问题。这篇文章把这条链路完整走一遍先建项目再布界面然后接事件最后落地无边框窗口并给出一套可直接运行的 PySide6 示例代码。读者照着一遍跑下来就能把“新建项目、界面布局”和“事件、无边框窗口”这两个环节串起来。1. 核心能力速览能力项说明主题定位教程型PySide6 / Qt 桌面应用中的项目创建、界面布局、事件处理、无边框窗口实现前置知识Python 基础、Qt 框架基本使用技术栈PySide6 / PyQt6跨平台支持 Windows / Linux / macOS关键内容 1新建项目的工程骨架、目录规划和入口代码组织关键内容 2界面布局的常用模式和布局层级设计关键内容 3事件处理的机制、事件分发链路和事件过滤器关键内容 4无边框窗口的原理、自定义标题栏、拖动、缩放、最大化最小化控制运行方式命令行 python 启动代码纯本地运行适用场景自定义皮肤客户端、桌面工具窗口、播放器、编辑器外壳、需要统一视觉风格的业务系统这次示例基于 PySide6代码可以直接复制运行。如果你用的是 PyQt6把导入包名从PySide6改成PyQt6即可API 绝大部分一致。2. 新建项目工程骨架与开发环境2.1 新建项目前要确定的三件事创建工程之前先分清需求不然写到一半容易返工窗口是标准系统窗口还是无边框自定义窗口。这个决定窗口标志怎么设置也决定标题栏要不要自己写。界面是固定大小还是跟随窗口缩放。这个决定布局策略固定布局、比例布局、还是混合布局。交互是简单按钮点击还是有复杂事件链路。这个决定用信号槽连接就行还是需要事件过滤器、事件重写。5.2 小节的核心是“新建项目、界面布局”但这两件事最终都要和事件、无边框窗口配合。所以建议先搭建一个最小工程再一步一扩展。2.2 PySide6 环境准备先确认 Python 版本推荐 3.9 及以上版本。# 创建虚拟环境可选但建议 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 安装 PySide6 pip install PySide6 # 如需设计器 pip install pyside6-tools安装完成后可以验证一下版本python -c import PySide6; print(PySide6.__version__)能看到版本号说明基础环境就绪。2.3 最小工程骨架新建一个目录比如frameless_demo里面的结构可以这样划分frameless_demo/ ├── main.py ├── ui/ │ ├── __init__.py │ ├── main_window.py │ └── title_bar.py ├── core/ │ ├── __init__.py │ └── event_filter.py └── resources/ └── styles/ └── app.qssmain.py是程序入口负责创建应用实例、加载全局样式、启动主窗口。ui/main_window.py放主窗口结构。ui/title_bar.py放自定义标题栏。core/event_filter.py放事件过滤器、全局事件处理逻辑。resources/styles放 QSS 样式文件。最开始可以不拆那么细但建议至少区分“入口文件”和“界面文件”。下面是最小入口代码先跑通窗口显示import sys from PySide6.QtWidgets import QApplication, QWidget class MainWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle(新建项目骨架) self.resize(900, 600) if __name__ __main__: app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec())这个骨架确认能显示窗口后面就可以往里面加布局和事件逻辑了。3. 界面布局先搭骨架再填充细节界面布局的核心思想是不要用绝对坐标硬编码每个控件位置而是用“布局容器”自动管理控件排列。这样窗口缩放时控件会按照既定策略跟随变化。3.1 常用布局器PySide6 里最常用的是这几种布局器类名常见场景垂直布局QVBoxLayout上下排列表单、列表、按钮组水平布局QHBoxLayout左右排列按钮、工具栏网格布局QGridLayout表格输入、多列表单表单布局QFormLayout“标签 输入框”式表单堆叠布局QStackedLayout多页面切换、Tab 内容区用一个示例来演示“垂直 水平”的嵌套布局。典型场景是顶部一个输入框中间一个文本区底部一行按钮from PySide6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTextEdit, QLineEdit ) class LayoutDemo(QWidget): def __init__(self): super().__init__() self.setWindowTitle(布局演示) self.resize(800, 600) # 垂直根布局 root QVBoxLayout(self) # 顶部输入框 self.title_edit QLineEdit() self.title_edit.setPlaceholderText(输入标题) # 中间文本区第二个参数 1 表示伸缩因子 self.content_edit QTextEdit() # 底部按钮区域使用水平布局 bottom QHBoxLayout() btn_ok QPushButton(确定) btn_cancel QPushButton(取消) bottom.addStretch() bottom.addWidget(btn_ok) bottom.addWidget(btn_cancel) root.addWidget(self.title_edit) root.addWidget(self.content_edit, 1) root.addLayout(bottom) if __name__ __main__: app QApplication([]) window LayoutDemo() window.show() app.exec()这里重点看addWidget(self.content_edit, 1)里的伸缩因子。伸缩因子的意思是窗口变大时这部分空间优先被哪个控件拿走。如果希望底部按钮始终固定在底部、中间区域自适应拉伸就给中间区域一个非零伸缩因子。3.2 网格布局跨行跨列表单场景里网格布局比连续 addWidget 更直观from PySide6.QtWidgets import QGridLayout, QPushButton grid QGridLayout() # addWidget(控件, 行, 列, 占几行, 占几列) grid.addWidget(QPushButton((0,0)), 0, 0) grid.addWidget(QPushButton((0,1)), 0, 1) grid.addWidget(QPushButton(跨两行), 0, 2, 2, 1) grid.addWidget(QPushButton((2,0)), 2, 0, 1, 2)跨行跨列很多时候用在不规则的仪表盘布局或工具栏分组里。网格布局也是“界面布局”里最值得花时间掌握的布局器。3.3 控件尺寸策略布局器能工作是因为每个控件遵循一个“尺寸策略”。这个策略告诉布局器控件是固定大小还是可以拉伸还是可以压缩。常用的策略值策略行为Fixed固定尺寸不随布局变化Minimum有最小尺寸不会小于 minimumSizeExpanding尽量占用更多空间Preferred优先用“舒适”尺寸空间不够时可压缩Ignored布局时忽略控件打算的大小设置方式label.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed )第一个参数是水平策略第二个是垂直策略。无边框窗口自定义标题栏时左侧标题标签往往用 Expanding右侧按钮组用 Fixed这样按钮不会随着窗口变宽而拉长。3.4 界面的视觉层级“界面布局”不只是摆放控件还要考虑视觉层级。无边框窗口里通常是这样的层级整个窗口 └── 根垂直布局 ├── 自定义标题栏固定高度 ├── 工具栏可选 └── 内容区水平布局 ├── 左侧导航栏固定宽度 └── 右侧内容面板伸缩填充对应到代码就是root QVBoxLayout(self) # 根布局 root.addWidget(title_bar) # 自定义标题栏 root.addLayout(content, 1) # 内容区这种“先定主骨架、再拆局部布局”的方法在 5.2 “新建项目、界面布局”环节里是最实用的一条经验。千万别一开始就同时拉几十个控件进同一个布局。4. 事件处理从按钮点击到完整事件链路0 事件是 GUI 应用的核心。按钮点击、鼠标移动、键盘输入、窗口改变大小这些全部通过事件驱动。搞清楚事件在 Qt 中的完整流动路径才能处理无边框窗口里的拖动、缩放这类“特殊交互”。4.1 事件与信号槽的关系很多人学到这里会被两个概念绕晕QEvent 和 Signal/Slot信号槽。QEvent 是 Qt 内部的事件对象鼠标按下、键盘输入、窗口关闭等都会生成对应的 QEvent。Signal 是控件对外暴露的“通知”按钮点击时发出clicked信号用户连接这个信号到槽函数。简化的理解是底层事件被 Qt 处理之后翻译成更上层的信号。平时写业务逻辑多数时候连接信号槽就够了但想拦截、改写事件行为就要直接处理 QEvent。按钮点击的标准写法button.clicked.connect(self.on_button_clicked) def on_button_clicked(self): print(按钮被点击)这是最常用的事件处理方式。4.2 事件分发链路在一个窗口里鼠标点击产生的事件会经过以下路径QApplication.notify └── QObject.event └── 控件的事件处理函数 ├── QWidget.mousePressEvent ├── QWidget.mouseReleaseEvent └── ...notify是全局入口负责把事件发给目标对象。目标对象的event()方法接收事件并做一次类型分发最终调用具体的处理函数比如mousePressEvent、keyPressEvent。如果想拦截某个控件的事件有两个入口重写event()方法。给该控件安装事件过滤器。事件过滤器更通用不用改控件本身的类适合全局拦截和统计。4.3 事件过滤器类似“停止事件冒泡”用过前端的人对“事件冒泡”比较熟悉。前端里一个按钮点击后事件会逐级冒泡到父节点可以用stopPropagation()阻止。Qt 里没有完全一样的冒泡机制但事件过滤器承担了类似“提前拦截”的职责。通过installEventFilter给控件装过滤器之后该控件的所有事件都会先经过过滤器的eventFilter方法。如果eventFilter返回True事件就会被拦下不再继续传递给控件返回False则继续。from PySide6.QtCore import QObject, QEvent from PySide6.QtWidgets import QPushButton class ClickFilter(QObject): def eventFilter(self, obj, event): if event.type() QEvent.Type.MouseButtonPress: print(f拦截到鼠标按下目标对象: {obj.objectName()}) # 这里可以根据条件决定是否继续传递 # return True 表示事件被吞掉控件收不到鼠标按下事件 # return False 表示放行让事件继续走默认逻辑 return True return super().eventFilter(obj, event) # 使用 button QPushButton(测试按钮) button.setObjectName(testButton) filter_ ClickFilter(button) button.installEventFilter(filter_)安装事件过滤器对“无边框窗口拖动”也有用把过滤器装到标题栏的所有子控件上可以保证鼠标在标题栏任何一个按钮/标签上按下时都能触发窗口拖动逻辑不用每个控件单独重写鼠标事件。4.4 常见事件类型事件类型处理函数典型场景鼠标按下mousePressEvent点击、选中、拖拽起点鼠标移动mouseMoveEvent拖动窗口、拖拽控件鼠标释放mouseReleaseEvent拖拽结束、点击完成鼠标双击mouseDoubleClickEvent双击标题栏最大化鼠标滚轮wheelEvent缩放、滚动页面键盘按下keyPressEvent快捷键、方向键移动窗口尺寸变化resizeEvent重排版式、保存窗口大小窗口关闭closeEvent确认退出、保存数据触摸屏场景下还有touchEvent逻辑类似鼠标事件但支持多指。Qt 默认会把触摸事件转换为一套鼠标事件如果追求多指手势体验需要单独处理touchEvent。4.5 接受与忽略事件事件处理函数里经常看到两行代码event.accept() event.ignore()accept()表示事件被处理了Qt 不会再往上传递。ignore()表示事件未被处理Qt 会尝试把事件交给父控件。在无边框窗口里这个机制很关键。比如标题栏处理了鼠标事件但某个子按钮没有处理事件就可能继续向上传给父级标题栏导致点击按钮时窗口也跟着移动。要避免这种问题明确在需要拦截的地方调用event.accept()。5. 无边框窗口去掉系统标题栏之后5.1 无边框窗口的原理默认情况下窗口管理器会为窗口添加系统标题栏和边框。无边框窗口要做的事情就是告诉窗口管理器“不要画系统标题栏”然后用自己写的控件代替。在 PySide6 里设置方式如下from PySide6.QtCore import Qt self.setWindowFlags(Qt.WindowType.FramelessWindowHint)设置无边框后系统标题栏消失但系统自带的窗口拖动、缩放、最大化最小化按钮也一并消失。这些能力全部需要手动实现。5.2 自定义标题栏自定义标题栏通常是一个普通控件高度固定内部放“窗口标题 最小化 最大化/还原 关闭”按钮。from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QWidget, QHBoxLayout, QPushButton, QLabel ) class TitleBar(QWidget): minimize_signal Signal() maximize_signal Signal() close_signal Signal() def __init__(self, title: str ): super().__init__() self.setFixedHeight(40) layout QHBoxLayout(self) layout.setContentsMargins(12, 0, 4, 0) self.title_label QLabel(title) self.btn_min QPushButton(—) self.btn_max QPushButton(□) self.btn_close QPushButton(×) for btn in (self.btn_min, self.btn_max, self.btn_close): btn.setFixedSize(36, 28) layout.addWidget(self.title_label) layout.addStretch() layout.addWidget(self.btn_min) layout.addWidget(self.btn_max) layout.addWidget(self.btn_close) self.btn_min.clicked.connect(self.minimize_signal) self.btn_max.clicked.connect(self.maximize_signal) self.btn_close.clicked.connect(self.close_signal)需要特别注意一点如果无边框窗口被最小化系统任务栏里可能不会自动出现恢复入口。很多开发者在无边框窗口里点最小化之后就再也找不回窗口这是非常常见的坑。建议在真实项目里使用QSystemTrayIcon做系统托盘或确保任务栏仍然保留窗口入口。5.3 手动实现窗口拖动去掉系统边框后窗口无法通过系统标题栏拖动。常见做法是重写鼠标事件手动计算偏移量from PySide6.QtCore import QPoint from PySide6.QtGui import QMouseEvent from PySide6.QtCore import Qt class TitleBar(QWidget): def __init__(self, title: str ): super().__init__(title) self._drag_offset None def mousePressEvent(self, event: QMouseEvent): if event.button() Qt.MouseButton.LeftButton: # 记录按下时鼠标与窗口左上角的偏移 self._drag_offset ( event.globalPosition().toPoint() - self.window().pos() ) event.accept() def mouseMoveEvent(self, event: QMouseEvent): if event.buttons() Qt.MouseButton.LeftButton: if self._drag_offset is not None: self.window().move( event.globalPosition().toPoint() - self._drag_offset ) event.accept() def mouseReleaseEvent(self, event: QMouseEvent): self._drag_offset None event.accept()这段代码放在标题栏控件上。当鼠标在标题栏区域按下并移动时整个窗口跟着移动。如果你的 Qt 版本较新也可以使用更简洁的系统级接口def mousePressEvent(self, event: QMouseEvent): if event.button() Qt.MouseButton.LeftButton: self.window().startSystemMove() event.accept()startSystemMove()会调用操作系统自带的窗口移动逻辑效果更顺滑。缺点是不同的 Qt 版本以及不同操作系统的行为存在差异保守方案还是手动计算偏移。5.4 无边框窗口的缩放无边框窗口没有系统边框自然也不能通过拖拽窗口边缘来改变大小。实现缩放有几个思路在窗口边缘放置不可见的“热区”控件检测鼠标进入热区后设置鼠标形状按下时调用startSystemResize()或手动调整窗口尺寸。在右下角放一个QSizeGrip系统自动提供右下角调整大小手柄。最简单的方式是QSizeGripfrom PySide6.QtWidgets import QSizeGrip, QVBoxLayout, QWidget window QWidget() layout QVBoxLayout(window) # 将 QSizeGrip 放在布局底部右侧 grip QSizeGrip(window) layout.addWidget(grip, 0, Qt.AlignBottom | Qt.AlignRight)不过QSizeGrip只支持右下角。如果要做“边缘任意拖拽缩放”需要自己写事件逻辑主要处理mouseMoveEvent中鼠标是否靠近窗口边缘、按下后根据光标位置调整窗口的setGeometry。5.5 双击标题栏最大化无边框窗口常见的交互是双击标题栏切换最大化/还原。实现方式是在标题栏里重写mouseDoubleClickEventdef mouseDoubleClickEvent(self, event: QMouseEvent): if event.button() Qt.MouseButton.LeftButton: window self.window() if window.isMaximized(): window.showNormal() else: window.showMaximized() event.accept() return super().mouseDoubleClickEvent(event)6. 综合示例无边框窗口 界面布局 事件处理把前面几个知识点合并成一个可直接运行的程序。这个示例包含无边框窗口。自定义标题栏支持拖动、双击最大化。最小化、最大化、关闭按钮。左侧菜单栏和右侧内容区布局。一个“模拟点击事件”按钮触发事件回写到事件日志列表。6.1 完整代码import sys from datetime import datetime from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QMouseEvent from PySide6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QSizeGrip ) class TitleBar(QWidget): minimize_signal Signal() maximize_signal Signal() close_signal Signal() def __init__(self, title: str ): super().__init__() self.setObjectName(titleBar) self.setFixedHeight(40) layout QHBoxLayout(self) layout.setContentsMargins(12, 0, 4, 0) self.title_label QLabel(title) self.btn_min QPushButton(—) self.btn_max QPushButton(□) self.btn_close QPushButton(×) for btn in (self.btn_min, self.btn_max, self.btn_close): btn.setFixedSize(36, 28) layout.addWidget(self.title_label) layout.addStretch() layout.addWidget(self.btn_min) layout.addWidget(self.btn_max) layout.addWidget(self.btn_close) self.btn_min.clicked.connect(self.minimize_signal) self.btn_max.clicked.connect(self.maximize_signal) self.btn_close.clicked.connect(self.close_signal) self._drag_offset None def mousePressEvent(self, event: QMouseEvent): if event.button() Qt.MouseButton.LeftButton: self._drag_offset event.globalPosition().toPoint() - self.window().pos() event.accept() def mouseMoveEvent(self, event: QMouseEvent): if event.buttons() Qt.MouseButton.LeftButton: if self._drag_offset is not None: self.window().move(event.globalPosition().toPoint() - self._drag_offset) event.accept() def mouseReleaseEvent(self, event: QMouseEvent): self._drag_offset None event.accept() def mouseDoubleClickEvent(self, event: QMouseEvent): if event.button() Qt.MouseButton.LeftButton: window self.window() if window.isMaximized(): window.showNormal() else: window.showMaximized() event.accept() return super().mouseDoubleClickEvent(event) class FramelessWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle(无边框窗口演示) self.setWindowFlags(Qt.WindowType.FramelessWindowHint) self.resize(900, 600) self.setMinimumSize(700, 450) # 根布局 root QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) # 标题栏 self.title_bar TitleBar(无边框窗口与界面布局演示) self.title_bar.minimize_signal.connect(self.showMinimized) self.title_bar.maximize_signal.connect(self.toggle_maximize) self.title_bar.close_signal.connect(self.close) root.addWidget(self.title_bar) # 内容区 content QHBoxLayout() content.setContentsMargins(12, 12, 12, 12) content.setSpacing(12) # 左侧菜单 left_panel QVBoxLayout() left_panel.setSpacing(8) btn_home QPushButton(首页) btn_event QPushButton(事件列表) btn_setting QPushButton(设置) for btn in (btn_home, btn_event, btn_setting): btn.setFixedHeight(36) left_panel.addWidget(btn_home) left_panel.addWidget(btn_event) left_panel.addWidget(btn_setting) left_panel.addStretch() # 右侧内容 right_panel QVBoxLayout() title_label QLabel(事件日志) self.log_list QListWidget() btn_simulate QPushButton(触发一条模拟点击事件) btn_simulate.setFixedHeight(36) right_panel.addWidget(title_label) right_panel.addWidget(self.log_list, 1) right_panel.addWidget(btn_simulate) content.addLayout(left_panel) content.addLayout(right_panel, 1) root.addLayout(content, 1) # 右下角缩放手柄 self.size_grip QSizeGrip(self) self.size_grip.setFixedSize(18, 18) root.addWidget(self.size_grip, 0, Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignRight) # 事件 btn_simulate.clicked.connect(lambda: self.append_log(模拟点击事件触发)) # 给左侧菜单按钮也接上事件 btn_home.clicked.connect(lambda: self.append_log(点击了首页按钮)) btn_event.clicked.connect(lambda: self.append_log(点击了事件列表按钮)) btn_setting.clicked.connect(lambda: self.append_log(点击了设置按钮)) def toggle_maximize(self): if self.isMaximized(): self.showNormal() else: self.showMaximized() def append_log(self, text: str): now datetime.now().strftime(%H:%M:%S) self.log_list.addItem(f[{now}] {text}) if __name__ __main__: app QApplication(sys.argv) window FramelessWindow() window.show() sys.exit(app.exec())6.2 运行方式python main.py如果代码写在main.py直接运行即可。6.3 验证结果运行后从无边框窗口中应验证以下几点窗口没有系统标题栏和系统边框整体外观完全由 QSS 和自定义控件决定。点击标题栏空白区域拖动时整个窗口跟随移动。双击标题栏空白处窗口在最大化与正常大小之间切换。点击标题栏右侧三个按钮分别执行最小化、最大化/还原、关闭。点击“事件列表”里的按钮日志区出现对应记录。拖动右下角QSizeGrip可缩放窗口。7. 常见问题与排查问题现象可能原因排查方式解决方案无边框窗口点最小化后找不回来无边框窗口没有系统标题栏和任务栏入口查看任务管理器是否还有进程线程确认 taskbar 显示策略用 QSystemTrayIcon 做托盘恢复入口不要直接 showMinimized先判断是否有托盘拖动窗口出现闪烁手动 move 频繁触发 resize / paint检查是否在拖动中更新了布局或触发大量重绘用 startSystemMove()拖动过程中避免执行耗时操作点击标题栏按钮时窗口也一起移动事件没有被 accept()在按钮对应事件里打断传递给按钮安装事件过滤器并返回 True确认按钮点击后没有继续冒泡给父级带无边框窗口时 resize 边缘不生效只设置了 FramelessWindowHint没有实现边缘缩放检查是否有 QSizeGrip加 QSizeGrip或手动实现边缘热区检测事件过滤器不生效过滤器对象被垃圾回收检查 filter 对象引用是否还在把过滤器对象保存为控件或窗口的属性滚动日志时事件卡顿append_log 频率过高QListWidget 每次都触发刷新观察日志区 item 数量限制日志条数超过一定数量移除最早条目无边框窗口任务栏右键没有系统菜单FramelessWindowHint 去掉了系统菜单对比普通窗口行为可以保留 Qt.WindowSystemMenuHint 或在自定义标题栏里实现菜单在 Linux / Windows 上拖动表现不一致系统窗口管理器对无边框处理不同在不同平台分别测试优先用 startSystemMove()手动 move 方案需要兼容各平台8. 最佳实践与进一步优化8.1 布局优先坐标后置无边框窗口的自定义外观依赖布局能力。所有控件尽量放进布局器不要用setGeometry硬定位。布局器会自动处理窗口缩放、不同分辨率下的控件位置这是“界面布局”里最实用的一条准则。8.2 事件链路的统一入口无边框窗口里的事件链路比普通窗口复杂。给标题栏的所有子控件安装统一事件过滤器能集中处理“鼠标按下——拖动—释放——双击”这类窗口级操作不用在每个控件里重复写鼠标事件。可以像这样把事件过滤器挂在标题栏上self.installEventFilter(global_filter)标题栏监听所有进入的子事件在eventFilter里判断if event.type() QEvent.Type.MouseButtonPress: # 统一处理按下事件这样写代码量会明显减少排查问题时也只有一个集中入口。8.3 无边框窗口的视觉一致性无边框窗口的优势是视觉统一但代价是实现细节多。建议把整体样式用 QSS 统一维护#titleBar { background-color: #2d2d2d; } #titleLabel { color: #f0f0f0; font-size: 14px; } QPushButton { border: none; background-color: transparent; border-radius: 4px; padding: 4px; } QPushButton:hover { background-color: #3d3d3d; }QSS 文件放在resources/styles/app.qss入口加载with open(resources/styles/app.qss, encodingutf-8) as f: app.setStyleSheet(f.read())8.4 触发无边框窗口的联动测试无边框窗口常见的联动场景点击最大化后再拖动标题栏应自动还原为普通状态。双击最大化后再双击应还原成上次窗口大小。窗口缩放过程中布局里的事件日志区域和按钮区不能互相挤压。系统分辨率改变时窗口不要超出屏幕范围。把这几个场景写成测试清单每次改动后过一遍能省下大量返工时间。8.5 后续扩展方向到这里无边框窗口、事件处理、界面布局这条链路已经完整跑通。继续扩展的方向包括窗口圆角与阴影效果无边框窗口可以配合半透明背景和 QPainter 绘制圆角做出类似现代客户端的视觉效果。系统托盘与全局快捷键适合常驻后台的工具类应用。多页面切换把右侧内容区改成QStackedWidget配合左侧菜单做页面切换。更复杂的全局事件例如扫码枪输入、全局快捷键监听都可以通过事件过滤器或系统级事件钩子实现。这次的核心是先把“新建项目、界面布局”跑起来再把事件和无边框窗口接上。基础链路通了后面的高级功能都是在这些机制上叠加。