搞怪小程序这个方向其实特别适合拿来练手桌面端开发。原因很简单功能不用太复杂但交互要足够有趣用户点一下就得有反馈这对UI响应、事件绑定、资源打包的要求一点都不低。我这次用PySide6做界面、PyInstaller做打包从写代码到生成一个能直接双击运行的 exe中间踩了不少坑也积累了一些在官方文档里不太容易找到的经验。如果你刚学完 Python 基础语法想找一个既有成就感又能把GUI、打包、资源管理串起来的项目或者你已经写过一些脚本但不知道怎么交付给不懂技术的朋友用那这篇内容应该能帮到你。我会把整个项目的设计思路、关键代码、打包参数、以及那些只有真正打包过才懂的细节全部摊开讲。1. 为什么搞怪小程序是PySide6入门的理想载体1.1 搞怪类程序的核心诉求即时反馈与低门槛交互搞怪小程序和传统的工具类软件有个本质区别工具类软件追求功能完整、流程严谨而搞怪类程序追求的是点一下就有反应反应还得让人意外。这个特点决定了它在技术选型上的偏好——界面要轻、启动要快、交互要直接。我见过不少人一上来就用PySide6写记事本、写计算器写到一半就被各种信号槽、布局嵌套搞晕了。搞怪小程序不一样它的功能边界很清晰一个按钮、一个标签、一段动画或者音效用户点完就笑一下任务就完成了。这种小闭环特别适合用来理解Qt的核心机制因为你可以把全部注意力放在事件怎么触发界面怎么响应上而不用分心去处理复杂的业务逻辑。从另一个角度看搞怪程序天然带有分享属性。你做完一个能逗人笑的小东西发给朋友对方双击就能用这种正反馈对初学者来说非常重要。而要做到发给朋友就能用就绕不开打包这个环节这恰好又把PyInstaller带进来了。所以这个项目虽然小但覆盖的知识链路是完整的Python逻辑 → PySide6界面 → 资源管理 → PyInstaller打包 → 独立exe交付。1.2 PySide6相比Tkinter在搞怪场景下的优势Python自带的Tkinter能写界面这点没错但放到搞怪小程序这个场景里它的短板就很明显了。Tkinter的控件样式比较朴素想做圆角按钮、渐变背景、自定义字体这些效果得费很大劲去画Canvas而且不同系统上的渲染效果还不一致。搞怪程序恰恰很吃视觉效果界面丑了搞怪的效果就打了对折。PySide6是Qt的官方Python绑定控件体系成熟样式表QSS用起来跟写CSS差不多圆角、阴影、渐变、动画都能比较轻松地实现。更重要的是PySide6自带QPropertyAnimation、QSoundEffect这类做动效和音效的类搞怪程序需要的突然弹出来抖一下响一声这些效果用现成的API就能搞定不用自己造轮子。还有一个实际考虑PySide6的跨平台一致性比Tkinter好。你在Windows上调试好的界面拿到macOS或者Linux上跑布局和样式基本不会走样。这对于想把小程序分享给不同系统朋友的场景来说省了很多适配的麻烦。1.3 项目功能规划从点一下就笑到完整交互闭环我在动手之前先列了个功能清单避免写着写着跑偏。搞怪小程序不需要功能多但每个功能都要能独立跑通形成完整的交互闭环。我的规划是这样的主界面一个居中的大按钮配一句引导文案比如点我试试。触发反馈点击按钮后界面出现随机搞怪效果可能是文字突变、窗口抖动、或者弹出一个表情。音效配合部分效果配上短音效增强搞怪的冲击力。重置机制效果展示完后能回到初始状态方便反复玩。退出方式提供一个不显眼但能正常关闭的入口避免用户找不到关闭按钮。这个清单看起来简单但每一项都对应着具体的PySide6知识点布局管理、信号槽连接、定时器、动画、资源加载。把这几块吃透PySide6的基础就算过关了。2. 环境搭建与PySide6安装中的常见障碍2.1 Python版本选择与虚拟环境的必要性在装PySide6之前Python版本的选择是有讲究的。PySide6对Python版本有明确要求太老的版本装不上太新的版本有时候wheel还没跟上。我个人的经验是用Python 3.9到3.11之间的版本最稳妥这个区间内PySide6的预编译包最齐全装起来基本不会遇到编译错误。虚拟环境这一步千万别省。我见过太多人图省事直接往全局环境里装结果项目A依赖的库版本和项目B冲突最后两个都跑不起来。用venv建一个独立环境几行命令的事python -m venv guaienv # Windows激活 guaienv\Scripts\activate # macOS/Linux激活 source guaienv/bin/activate激活之后命令行前面会出现环境名这时候装的包都只在这个环境里生效干净利落。这个习惯一旦养成后面做任何Python项目都会轻松很多。2.2 未安装PySide6报错的排查思路很多人第一次运行PySide6代码时会遇到类似未安装PySide6请运行python -m pip install pyside6的提示。这个报错本身已经把解决方案写在里面了但实际排查时还是有几个容易忽略的点。第一确认你激活的是正确的虚拟环境。有时候你在A环境里装了PySide6但运行代码时用的是B环境的解释器自然找不到。可以在命令行里执行python -c import PySide6; print(PySide6.__version__)能打印出版本号就说明当前解释器能识别到。第二pip的版本可能太旧。老版本pip在解析PySide6这种依赖较多的包时容易出问题先升级一下python -m pip install --upgrade pip。第三网络问题导致的下载中断。PySide6的包体积不小下载过程中如果网络不稳定可能装到一半失败但pip有时不会明确报错。这种情况重新执行一次安装命令加上--no-cache-dir参数强制重新下载。提示安装PySide6时建议用python -m pip install pyside6而不是直接pip install pyside6前者能确保用的是当前Python解释器对应的pip避免多版本Python环境下装错地方。2.3 IDE配置VSCode与PyCharm的选择VSCode和PyCharm都能写PySide6项目选哪个看个人习惯。VSCode轻量、启动快配合Python插件和Pylance代码补全和跳转体验很好适合喜欢简洁环境的人。PyCharm对Qt项目的支持更深入一些比如它能识别.ui文件对信号槽的跳转也更友好但启动慢、占内存。不管用哪个关键是把解释器配置对。在VSCode里按CtrlShiftP输入Python: Select Interpreter选中你刚才建的虚拟环境。PyCharm里在Settings的Project Interpreter里指向虚拟环境的python.exe。配置对了之后import PySide6就不会出现红色波浪线运行也不会报找不到模块。3. 搞怪效果的核心实现信号槽、定时器与动画3.1 主窗口骨架与布局管理PySide6的布局管理是新手最容易卡住的地方。我的建议是先用QVBoxLayout把主窗口的垂直结构搭出来需要水平排列的地方再嵌QHBoxLayout。搞怪小程序的主界面结构很简单上面一个显示搞怪文字的标签中间一个大按钮下面一个状态提示。from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton from PySide6.QtCore import Qt class FunnyWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle(搞怪小程序) self.resize(400, 300) layout QVBoxLayout() self.label QLabel(准备好了吗) self.label.setAlignment(Qt.AlignCenter) self.label.setStyleSheet(font-size: 20px; color: #333;) self.button QPushButton(点我试试) self.button.setFixedHeight(60) self.button.setStyleSheet( QPushButton { font-size: 18px; background-color: #4CAF50; color: white; border-radius: 10px; } QPushButton:hover { background-color: #45a049; } ) layout.addWidget(self.label) layout.addWidget(self.button) self.setLayout(layout)这段代码里setAlignment控制文字居中setStyleSheet用QSS设置样式setFixedHeight固定按钮高度。QSS的写法和CSS很像但要注意Qt的样式表对某些CSS属性支持有限比如box-shadow就不支持想做阴影得用QGraphicsDropShadowEffect。3.2 信号槽机制让按钮真正活起来信号槽是Qt的灵魂也是PySide6里最需要理解的概念。简单说信号是某件事发生了槽是发生这件事时要做的事connect就是把两者绑在一起。搞怪小程序里按钮的clicked信号连到我们自定义的处理函数上self.button.clicked.connect(self.on_button_click)这里有个新手常犯的错误连接时写成self.on_button_click()加了括号。加了括号表示立即调用这个函数把返回值当作槽传进去这显然不对。正确的写法是只写函数名不加括号让Qt在信号触发时去调用它。on_button_click里就是搞怪逻辑的入口。我设计了一个随机效果机制每次点击从效果列表里随机挑一个执行import random def on_button_click(self): effects [self.effect_shake, self.effect_text_change, self.effect_popup] random.choice(effects)()这种随机挑一个的设计让每次点击都有新鲜感比固定一个效果有意思得多。而且从代码结构上看每个效果独立成一个函数后面想加新效果只要往列表里塞就行扩展性很好。3.3 窗口抖动效果的实现细节窗口抖动是搞怪程序里的经典效果实现起来不难但有几个细节要注意。核心思路是用QPropertyAnimation对窗口的pos属性做快速往复的动画from PySide6.QtCore import QPropertyAnimation, QPoint def effect_shake(self): self.original_pos self.pos() self.anim QPropertyAnimation(self, bpos) self.anim.setDuration(300) # 构造抖动路径 for i in range(5): offset 10 if i % 2 0 else -10 self.anim.setKeyValueAt(i / 5, self.original_pos QPoint(offset, 0)) self.anim.setKeyValueAt(1.0, self.original_pos) self.anim.start()这里用setKeyValueAt设置关键帧让窗口在原始位置左右快速偏移。bpos是属性名的字节串形式这是PySide6的写法要求。抖动结束后一定要把窗口位置还原到original_pos否则窗口会越抖越偏。注意动画对象要保存为实例属性self.anim不能只用局部变量。因为局部变量在函数结束后会被回收动画可能还没播完就没了导致效果异常。3.4 文字突变与音效配合文字突变效果就是点击后标签文字突然变成一句搞怪的话同时改变颜色和字号制造视觉冲击def effect_text_change(self): funny_texts [你点我干嘛, 再点一下试试, 哈哈哈上当了, 我是不会告诉你的] colors [#e74c3c, #9b59b6, #e67e22, #1abc9c] self.label.setText(random.choice(funny_texts)) self.label.setStyleSheet(ffont-size: 28px; color: {random.choice(colors)}; font-weight: bold;) # 2秒后恢复 QTimer.singleShot(2000, self.reset_label)QTimer.singleShot是个很好用的工具它能在指定毫秒数后执行一次函数不用自己建定时器对象。这里用它实现2秒后自动恢复让程序能反复玩。音效方面PySide6提供了QSoundEffect适合播放短音效。用法是先设置音源文件再调用play()from PySide6.QtMultimedia import QSoundEffect from PySide6.QtCore import QUrl self.sound QSoundEffect() self.sound.setSource(QUrl.fromLocalFile(sound.wav)) self.sound.play()音效文件建议用wav格式兼容性最好。mp3虽然体积小但在某些环境下QSoundEffect支持不稳定。音效时长控制在1秒以内太长会拖慢搞怪的节奏感。4. PyInstaller打包从脚本到独立exe的完整过程4.1 打包命令的核心参数拆解PyInstaller的打包命令看起来参数很多但常用的就那么几个。搞怪小程序最基础的打包命令是pyinstaller --onefile --windowed --name 搞怪小程序 main.py这三个参数各有讲究。--onefile把所有依赖打包成单个exe文件方便分发但启动时会先把内容解压到临时目录所以启动速度比多文件模式慢一点。--windowed等价于--noconsole让程序运行时不弹出黑色命令行窗口GUI程序必须加这个否则用户会看到一个黑框很不专业。--name指定生成的exe名字不指定的话默认用脚本名。如果程序里有图片、音效这些资源文件还得加--add-data参数。Windows下的格式是源路径;目标路径注意是分号不是冒号macOS和Linux下才是冒号pyinstaller --onefile --windowed --add-data sound.wav;. --add-data icon.png;. main.py这里的.表示打包后资源放在exe解压目录的根下。程序里读取资源时要用相对路径而且要考虑打包后的路径变化这个后面会详细讲。4.2 资源文件路径处理打包后找不到文件的根因这是PyInstaller打包最容易踩的坑没有之一。开发时你用sound.wav这样的相对路径能正常读取但打包成exe后程序运行时的当前目录变了相对路径就失效了报文件找不到。根本原因是PyInstaller打包后程序运行时的临时解压目录和开发时的目录结构不一样。解决办法是用sys._MEIPASS这个属性来判断运行环境import sys import os def resource_path(relative_path): if hasattr(sys, _MEIPASS): # 打包后的运行环境 base_path sys._MEIPASS else: # 开发环境 base_path os.path.abspath(.) return os.path.join(base_path, relative_path)用的时候把资源路径都过一遍这个函数resource_path(sound.wav)。这样不管是在开发环境还是打包后都能正确找到文件。sys._MEIPASS是PyInstaller在运行时注入的属性指向临时解压目录hasattr判断它是否存在就能区分两种环境。提示这个resource_path函数建议每个PyInstaller项目都备一份属于通用工具函数。写一次以后所有项目都能用。4.3 打包体积优化与启动速度权衡PySide6打包出来的exe体积不小动辄七八十兆甚至上百兆这是因为Qt的库文件本身就大。如果在意体积有几个优化方向。第一用--exclude-module排除用不到的模块。PySide6默认会打包很多子模块但搞怪小程序可能只用到Widgets、Core、Multimedia这几个。可以尝试排除一些明显用不到的比如--exclude-module PySide6.QtWebEngineCore。不过这个要小心排错了会导致程序运行时报错得逐个测试。第二用UPX压缩。UPX是个可执行文件压缩工具PyInstaller支持调用它来压缩打包结果。装上UPX后加--upx-dir参数指向UPX目录体积能压下来一些。但UPX压缩有时会触发杀毒软件误报这是个权衡。第三接受现实。说实话PySide6项目的体积优化空间有限与其花大量时间抠体积不如把精力放在功能上。七八十兆的exe在现在这个时代不算什么用户下载安装包动辄几百兆都习惯了。启动速度方面--onefile模式因为要解压首次启动会慢几秒。如果特别在意启动速度可以改用多文件模式去掉--onefile生成一个文件夹里面包含exe和依赖启动快但分发时要整个文件夹一起给。4.4 杀毒软件误报与兼容性处理PyInstaller打包的exe被杀毒软件误报是个老生常谈的问题。原因是PyInstaller的打包机制和某些恶意软件的打包方式有相似之处杀毒软件基于行为特征就容易误判。应对方法有几个。一是给exe加上图标和版本信息让它看起来更正规能降低误报率。加图标用--iconicon.ico加版本信息需要写一个版本文件用--version-file指定。二是如果误报严重可以考虑用代码签名证书给exe签名但这个要花钱个人项目一般不做。兼容性方面要注意打包环境和目标运行环境的一致性。在64位系统上打包的exe拿到32位系统上跑不了。如果目标用户系统版本比较老最好在相近的环境里打包。另外Windows 7对较新版本的PySide6支持有限如果确实要支持Win7得用老版本的PySide6和Python。5. 那些打包之后才会暴露的问题5.1 中文路径与编码问题中文路径是个隐蔽的坑。开发时如果项目放在中文目录下可能一切正常但打包后程序读取资源时如果路径里有中文某些情况下会出问题。根源在于编码处理不一致。我的建议是项目路径和资源文件名尽量用英文从源头上避开这个问题。如果实在要用中文确保在读取文件时显式指定编码比如open(path, encodingutf-8)。对于PyInstaller打包还要注意命令行里的中文参数在某些终端下会乱码打包时最好在英文路径下操作。5.2 多显示器与高DPI缩放适配高DPI屏幕现在很普及但PySide6程序在高DPI下默认可能会出现界面模糊或者控件大小不对的问题。解决办法是在程序启动前设置高DPI缩放策略from PySide6.QtCore import Qt from PySide6.QtWidgets import QApplication QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) app QApplication(sys.argv)PassThrough策略让缩放比例按实际值处理避免四舍五入导致的模糊。这个设置要在创建QApplication之前调用顺序错了不生效。多显示器场景下如果程序窗口在副屏上坐标计算可能和主屏不一样。搞怪程序的抖动效果如果涉及绝对坐标要基于窗口当前位置计算而不是写死坐标这样在哪个屏幕上都正常。5.3 打包后音效不播放的排查链路音效在开发时正常打包后不响这个问题的排查要按顺序来。第一步确认音效文件确实被打包进去了可以解压exe用7-Zip之类的工具看看临时目录里有没有音效文件。第二步确认代码里用的是resource_path处理过的路径而不是写死的相对路径。第三步确认音效格式是wavmp3在某些打包环境下解码器可能缺失。如果这三步都没问题那可能是QSoundEffect的加载是异步的程序启动后立即播放可能音源还没加载完。解决办法是监听statusChanged信号等状态变成Ready再允许播放或者在程序启动后延迟一小段时间再初始化音效。5.4 反复打包时的缓存清理PyInstaller有缓存机制第二次打包时会复用上次的一些中间文件加快速度。但有时候代码改了打包出来还是旧的行为这就是缓存没更新导致的。遇到这种情况删掉项目目录下的build文件夹和.spec文件重新打包。.spec文件是PyInstaller根据你的命令生成的配置文件里面记录了打包参数。如果你反复用不同的参数打包建议直接编辑.spec文件然后用pyinstaller xxx.spec来打包这样参数管理更清晰也不容易出错。6. 从能跑到好用几个提升体验的细节6.1 窗口居中与初始状态设计程序启动时窗口默认出现在屏幕左上角这个体验不好。让窗口居中显示几行代码的事from PySide6.QtGui import QGuiApplication def center_window(self): screen QGuiApplication.primaryScreen().geometry() window self.geometry() x (screen.width() - window.width()) // 2 y (screen.height() - window.height()) // 2 self.move(x, y)在__init__里调用一次窗口就居中了。初始状态的设计也很重要搞怪程序一打开应该给人一种准备好了快来玩的感觉按钮要醒目引导文案要清晰别让用户面对一个空白窗口不知所措。6.2 防止用户误操作导致程序卡死搞怪程序里如果有动画或者定时器用户快速连点按钮可能会触发多个动画同时运行导致界面卡顿甚至卡死。解决办法是在动画运行期间禁用按钮动画结束后再启用def on_button_click(self): self.button.setEnabled(False) # 执行效果... QTimer.singleShot(500, lambda: self.button.setEnabled(True))这样用户连点也不会出问题等效果播完按钮才恢复可点。这个细节虽然小但能明显提升程序的稳定性。6.3 给程序加上图标和版本信息一个没有图标的exe看起来就像半成品。给程序加图标在打包时用--icon参数指定.ico文件。注意图标文件必须是.ico格式不能直接用.png可以用在线工具或者Pillow库转换。版本信息能让exe在属性里显示版本号、公司名这些看起来更正规。写一个版本信息文件.txt格式有固定结构打包时用--version-file指定。虽然搞怪程序不需要多正式但加上这些细节分享给朋友时对方会觉得你做得挺用心。6.4 分享给朋友前的自测清单在把exe发给朋友之前我一般会过一遍这个清单在一台没装Python的电脑上测试确认能独立运行。测试所有搞怪效果确认每个都能正常触发。测试音效确认打包后能正常播放。测试反复点击确认不会卡死。测试关闭程序确认能正常退出没有残留进程。检查exe体积确认在可接受范围内。这个清单看着简单但每次都能帮我发现一两个遗漏的问题。尤其是在没装Python的电脑上测试这一条很多人会忽略结果发出去的exe在别人电脑上因为缺依赖跑不起来。7. 关于这个项目后续能怎么玩搞怪小程序做完之后其实还有很多可以扩展的方向。比如加一个整蛊模式点击后弹出全屏的假报错窗口过几秒自动消失或者加一个猜谜功能随机出题让用户猜猜对了有奖励动画。这些扩展都不难核心还是信号槽和动画那套东西只是组合方式变了。从技术学习的角度这个项目跑通之后你可以顺着往下学几个方向。一是学QThread把耗时的操作放到后台线程避免界面卡死这是做正经桌面应用的必备技能。二是学QSettings给程序加上配置保存功能记住用户的选择。三是学QSystemTrayIcon让程序能最小化到系统托盘这是很多工具类软件的标配。我个人在实际操作中的体会是PySide6的学习曲线前陡后缓刚开始被信号槽、布局、QSS这些东西绕得头晕但一旦理解了它们各自的分工后面写起来就很顺。而PyInstaller的坑主要集中在资源路径和打包参数上踩过一遍之后基本就是复制粘贴改改参数的事。这个搞怪小程序虽然简单但它把这两块都串起来了作为练手项目性价比很高。
企业数字化 ERP 产品动态
相关推荐
knowledge-catalog 语义模型保真度指南:push / pull 各环节保留了什么、丢掉了什么 数据目录AI Agent人工智能知识管理示例工程 【免费下载链接】knowledge-catalog Google Cloud Knowledge Catalog Tools and Samples 项目地址: https://gitcode.com/gh_mirrors/kn/knowledge-catalog 点击查看 免费下载 本文是 toolbox/mdcode/docs/semantic-mode… · 2026/9/25 3:00:08
RocketRide 集成 IBM watsonx:llm_ibm_watson 节点配置、调用与源码原理全解析 【免费下载链接】rocketride-server High-performance AI pipeline engine with a C core and 50 Python-extensible nodes. Build, debug, and scale LLM workflows with 13 model providers, 8 vector databases, and agent orchestration, all from your IDE. Includes VS C… · 2026/9/25 3:00:08
高质量博文生成指南:如何有效提供项目信息与关键词摘要 我注意到您还没有提供具体的项目标题、项目正文、关键词和摘要描述等信息。无法在缺少这些核心输入的情况下生成对应的博文。请您按照以下格式提供信息,我会立即为您创作:项目标题: [您的标题]
项目正文: [较为零散、不完整的原始描述亦可]
关键词: [关键… · 2026/9/25 3:31:26
源师兄软串口排错清单:扩展屏不显示、乱码的8个常见原因与修复方案 源师兄软串口排错清单:扩展屏不显示、乱码的8个常见原因与修复方案 【免费下载链接】software-serial-module 源师兄扩展项目: 软串口模块 | 由源师兄组织创建 项目地址: https://gitcode.com/yuanshixiong/software-serial-module
源师兄软串口模块… · 2026/9/25 3:31:26
HTTP头大小写引发的静默故障:从协议到Nginx、Go、Node.js的排查与规范 1. 问题现场:一个头名字引发的“静默故障”先讲一个我实际处理过的线上故障。用户调我们的网关接口,用一个自定义头X-Auth-Token做鉴权。本地用 Postman 测,一切正常;换到 Java 客户端调,服务端日志里永远取不到这个头… · 2026/9/25 3:31:13
计量芯片封装选型:面积、功能与良率的三重权衡 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 3:31:13
SYN Flood实验:用WinXP复现TCP半开连接攻击原理 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 3:31:07
TwinCAT3运动控制:MC_Power与MC_Home功能块的工程应用实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 3:31:07
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37