简介这是一份面向C#初学者与游戏编程爱好者的推箱子小游戏完整源码工程适合用来练习WinForms界面开发、事件处理与游戏状态管理。压缩包共76个文件约135KB包含10个cs源文件、4个resx与resources资源文件、11个bmp位图素材、13个dat与15个way关卡及通关记录文件另有sln解决方案、csproj工程文件与可执行程序结构完整可直接编译运行。功能上支持撤销CtrlZ、重做CtrlY、选关、存盘读盘通关后自动将最佳步数保存为level.way文件既能回放演示整个推箱过程也便于向朋友分享战绩玩家还可自行设计关卡。作者预留了自动寻路深度优先、链表与栈、鼠标操作与通关光荣榜等扩展方向留待读者动手补全。目前已有964人学习下载适合作为课程设计或自学练手项目。1. 一个把“推箱子”拆开揉碎讲清楚的 C# 小游戏源码很多人第一次接触游戏编程都是从推箱子开始的——规则简单到一句话能说完但真动手写才发现地图怎么存、人物怎么动、箱子推到墙角怎么判断死局每一步都是坑。这份 C# 推箱子小游戏源代码就是把这些坑一个个填平之后的产物。它用最朴素的 WinForms 加 GDI 绘制没有引入任何游戏引擎整个项目结构清晰到新手能一行行读懂熟手也能直接拿它当网格类游戏的骨架去改。如果你正在找一份能跑起来、能看懂、还能顺手改成自己版本的 C# 小游戏源码这份推箱子代码值得你花一个下午拆一遍。2. 项目定位与技术选型为什么用 WinForms 而不是游戏引擎2.1 这个项目到底解决什么问题推箱子Sokoban的核心玩法是玩家在网格地图上推动箱子把所有箱子推到目标点就算过关。听起来简单但它天然包含几个游戏编程的基础命题——网格坐标系统、碰撞检测、状态管理、关卡数据序列化、撤销与重开。这份源码把这些命题全部落在一个可运行的桌面程序里没有多余的框架噪音。它适合两类人一类是刚学完 C# 基础语法、想找个完整项目练手的新手另一类是想快速搭一个网格类小游戏原型、不想被 Unity 那套东西拖慢节奏的开发者。项目本身不依赖数据库、不依赖网络、不依赖任何第三方 NuGet 包双击解决方案就能编译运行这种“零外部依赖”的特性在练手项目里非常难得。从功能覆盖上看它实现了完整的推箱子闭环多关卡切换、步数统计、撤销上一步、重新开始当前关卡、通关判定。这些功能看起来不起眼但每一个背后都有对应的数据结构支撑后面拆代码时会逐一展开。2.2 技术栈拆解WinForms GDI 的轻量组合项目用的是 .NET Framework 下的 WinForms绘图走 GDI。这个选型在今天的眼光看有点“老派”但对推箱子这种 2D 网格游戏来说反而是最省事的路子。原因很直接推箱子的画面就是一张张固定大小的格子图不需要 3D 渲染、不需要物理引擎、不需要粒子特效GDI 的Graphics.DrawImage和Graphics.FillRectangle完全够用。常见做法是把每个格子画成一个固定像素的方块比如 32×32 或 48×48地图有多大画布就按格子数乘出来。这种“格子即像素”的映射方式让坐标换算变得极其简单——鼠标点在哪、人物走到哪都是一次整除运算的事。技术栈可以归纳成这么几层层次技术作用界面层WinForms Form Panel承载画布、接收键盘事件绘制层GDI Graphics逐格绘制墙壁、地板、箱子、目标点、人物逻辑层纯 C# 类地图解析、移动判定、胜利检测数据层文本关卡文件用字符矩阵描述每一关的布局这个分层不是硬性架构而是代码自然形成的边界。逻辑层完全不碰 UI意味着你可以把同一套判定逻辑搬到控制台程序甚至 Web 后端里跑这也是后面二次开发时最值得利用的一点。2.3 关卡数据的字符矩阵表示法推箱子的关卡数据怎么存是这个项目第一个值得学的设计。源码里用的是最经典的字符矩阵方案每个关卡就是一个字符串数组每个字符代表一种地形或物件。// 关卡数据每个字符串是一行字符含义见下方注释 private readonly string[] Level1 new string[] { ########, # #, # .$ #, # #, # $ #, # . #, ######## }; // # 墙壁 空格 地板 . 目标点 $ 箱子 玩家 * 箱子已在目标点 玩家站在目标点这段数据里#是墙.是目标点$是箱子是玩家。解析的时候遍历每个字符把坐标和类型塞进对应的集合里。这种表示法的好处是关卡可以直接手写、肉眼可读改关卡不用打开任何编辑器记事本就能干。参数说明字符矩阵的行数决定地图高度每行字符串长度决定宽度解析时要保证所有行等长否则后面按width * y x算索引会错位。我一般会在解析入口加一个长度校验行长度不一致直接抛异常比运行到一半画面错乱要好排查得多。3. 核心模块拆解从地图解析到移动判定的完整链路3.1 地图解析把字符矩阵变成可操作的数据结构解析关卡是整个游戏的起点。源码里的做法是定义几个集合来存不同物件的位置玩家位置单独用两个 int 存行列坐标。public class LevelData { public int Width; // 地图宽度列数 public int Height; // 地图高度行数 public ListPoint Walls new ListPoint(); // 墙壁坐标集合 public ListPoint Targets new ListPoint(); // 目标点坐标集合 public ListPoint Boxes new ListPoint(); // 箱子坐标集合 public Point Player; // 玩家当前坐标 public void Parse(string[] rows) { Height rows.Length; Width rows[0].Length; for (int y 0; y Height; y) { for (int x 0; x Width; x) { char c rows[y][x]; switch (c) { case #: Walls.Add(new Point(x, y)); break; case .: Targets.Add(new Point(x, y)); break; case $: Boxes.Add(new Point(x, y)); break; case : Player new Point(x, y); break; case *: Boxes.Add(new Point(x, y)); Targets.Add(new Point(x, y)); break; case : Player new Point(x, y); Targets.Add(new Point(x, y)); break; } } } } }逻辑说明外层循环走行y内层走列x这样遍历顺序和字符串数组的存储顺序一致不会出现行列颠倒的经典错误。*和这两个复合字符要同时往两个集合里塞数据这是新手最容易漏的地方——箱子已经在目标点上它既是箱子也是目标点判定胜利时两个集合都得算对。参数说明Point用的是System.Drawing.Pointx 是列、y 是行和屏幕坐标一致。后面所有移动判定都基于这个坐标系千万别在某个地方把 x、y 写反否则会出现“按上键人物往左走”这种玄学 bug。3.2 移动判定一次按键背后的四步检查玩家按方向键之后程序要做的事情比想象中多。源码里的移动逻辑可以拆成四步算出目标格、判断是不是墙、判断有没有箱子、箱子能不能推。public bool TryMove(int dx, int dy) { Point next new Point(Player.X dx, Player.Y dy); // 第一步撞墙直接返回 if (Walls.Contains(next)) return false; // 第二步目标格有箱子尝试推箱子 if (Boxes.Contains(next)) { Point boxNext new Point(next.X dx, next.Y dy); // 箱子前方是墙或另一个箱子推不动 if (Walls.Contains(boxNext) || Boxes.Contains(boxNext)) return false; // 执行推箱子更新箱子位置 Boxes.Remove(next); Boxes.Add(boxNext); } // 第三步更新玩家位置 Player next; return true; }逻辑说明这段代码的顺序很关键。先判墙、再判箱子、最后才移动玩家任何一步失败都直接返回 false保证状态不会被改到一半。推箱子那一段先Remove旧位置再Add新位置用的是ListPoint的引用相等判断Point是值类型Contains比较的是坐标值所以能正确命中。参数说明dx和dy是方向增量上是(0,-1)、下是(0,1)、左是(-1,0)、右是(1,0)。这个约定要和键盘事件里的映射保持一致源码里是在KeyDown事件里做 switch 分发四个方向各调一次TryMove。这里有个容易翻车的点Boxes.Contains(next)用的是线性查找箱子数量少的时候无所谓但如果关卡里箱子特别多每帧都线性扫一遍会有性能损耗。常见做法是额外维护一个二维数组或字典做 O(1) 查询不过对推箱子这种规模线性查找完全够用没必要提前优化。3.3 胜利判定所有箱子是否都在目标点上胜利判定的逻辑一句话能说完每个箱子所在的位置是否都在目标点集合里。public bool IsWin() { foreach (Point box in Boxes) { if (!Targets.Contains(box)) return false; } return true; }逻辑说明遍历箱子集合只要有一个箱子不在目标点上就返回 false全部命中才返回 true。这个判定在每次成功移动之后调用一次返回 true 就弹出通关提示并加载下一关。参数说明这里依赖的是Targets集合里存了所有目标点坐标包括*和解析出来的那些。如果解析阶段漏了复合字符这里就会误判——箱子明明在目标点上程序却说没赢这种 bug 排查起来很折磨人所以解析和判定这两块要对照着看。3.4 撤销功能用栈保存每一步的状态快照撤销是推箱子体验里很重要的一环源码里用栈来实现。每成功移动一步就把当前玩家位置和箱子位置集合压栈撤销时弹栈恢复。private StackGameState history new StackGameState(); private void SaveState() { history.Push(new GameState { Player this.Player, Boxes new ListPoint(this.Boxes) // 深拷贝避免引用共享 }); } public void Undo() { if (history.Count 0) return; GameState state history.Pop(); Player state.Player; Boxes state.Boxes; }逻辑说明GameState是一个简单的快照类存玩家坐标和箱子集合的副本。关键在new ListPoint(this.Boxes)这一句——必须新建一个列表不能直接赋引用否则后续移动修改的是同一个列表撤销回去的状态也跟着变了这是引用类型最经典的坑。参数说明栈的容量没有上限一关走几百步也就几百个快照内存压力可以忽略。如果关卡特别大、步数特别多可以考虑只存增量移动方向和是否推了箱子撤销时反向执行但那是优化项初版用全量快照最直观。4. 绘制与交互GDI 逐格渲染和键盘事件绑定4.1 双缓冲绘制消除闪烁的关键一行WinForms 里直接往 Panel 上画快速重绘时会闪。源码里开了双缓冲这是桌面小游戏必做的一步。// 在窗体或绘制控件的构造函数里开启双缓冲 this.DoubleBuffered true; this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);逻辑说明DoubleBuffered true让绘制先发生在后台缓冲区画完再整体贴到屏幕上避免逐格绘制过程中被用户看到中间状态。AllPaintingInWmPaint和UserPaint配合使用把绘制统一收到OnPaint里处理。参数说明这三个样式标志是组合使用的少一个效果就打折扣。如果绘制控件是自定义的 Panel 子类在子类构造函数里设置如果是直接在 Form 上画就在 Form 构造函数里设置。这一步不做画面每走一步闪一下体验直接崩掉。4.2 逐格渲染一个 for 循环画完整个地图绘制逻辑的核心是一个双层循环按行列遍历每个格子根据类型选不同的画法。protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; int cell 32; // 每格 32 像素 for (int y 0; y level.Height; y) { for (int x 0; x level.Width; x) { Rectangle rect new Rectangle(x * cell, y * cell, cell, cell); Point p new Point(x, y); if (level.Walls.Contains(p)) g.FillRectangle(Brushes.DimGray, rect); // 墙 else g.FillRectangle(Brushes.WhiteSmoke, rect); // 地板 if (level.Targets.Contains(p)) g.FillEllipse(Brushes.OrangeRed, rect); // 目标点 if (level.Boxes.Contains(p)) g.FillRectangle(Brushes.SaddleBrown, rect); // 箱子 if (level.Player p) g.FillEllipse(Brushes.SteelBlue, rect); // 玩家 } } }逻辑说明每个格子先铺底墙或地板再叠目标点、箱子、玩家。绘制顺序决定了图层关系玩家画在最后保证站在目标点上时人物可见。用纯色图形代替图片资源是为了让源码不依赖外部素材文件下载下来就能跑。参数说明cell是格子像素尺寸改大改小只影响显示不影响逻辑坐标。Rectangle的构造参数是(x, y, width, height)这里的 x、y 是像素坐标等于格子坐标乘 cell。如果想让画面更精致可以把FillRectangle换成DrawImage加载贴图逻辑层完全不用动。4.3 键盘事件方向键映射与重绘触发交互部分绑定在窗体的KeyDown事件上方向键映射到四个方向增量移动成功后调Invalidate()触发重绘。private void Form_KeyDown(object sender, KeyEventArgs e) { int dx 0, dy 0; switch (e.KeyCode) { case Keys.Up: dy -1; break; case Keys.Down: dy 1; break; case Keys.Left: dx -1; break; case Keys.Right: dx 1; break; case Keys.Z: Undo(); Invalidate(); return; // Z 键撤销 case Keys.R: RestartLevel(); Invalidate(); return; default: return; } SaveState(); // 先存快照 if (level.TryMove(dx, dy)) { stepCount; if (level.IsWin()) ShowWinAndNextLevel(); } else { history.Pop(); // 移动失败撤销刚存的快照 } Invalidate(); // 请求重绘 }逻辑说明先存快照再尝试移动移动失败就把快照弹掉保证历史栈里只有有效步骤。Invalidate()只是标记区域需要重绘真正的绘制发生在下一次OnPaint这是 WinForms 的标准刷新机制。参数说明e.KeyCode是物理按键枚举方向键对应Keys.Up/Down/Left/Right。如果窗体上有其他控件抢焦点KeyDown可能收不到常见做法是把窗体的KeyPreview设为 true让窗体优先接收按键。5. 避坑与排查跑起来之后最容易撞上的五个问题5.1 画面闪烁严重走一步闪一下现象每次按方向键整个画面明显闪一下快速连按更明显。原因没有开启双缓冲GDI 逐格绘制的过程被用户直接看到了。解决在窗体或绘制控件的构造函数里设置DoubleBuffered true和SetStyle的三个标志具体代码见 4.1 节。如果绘制在 Panel 子类里设置要写在子类构造函数写在 Form 上对子控件不生效。5.2 撤销之后箱子位置错乱现象按 Z 撤销人物回去了但箱子位置和撤销前一样或者多个箱子叠在一起。原因保存快照时直接赋了Boxes的引用没有深拷贝后续移动修改的是同一个列表。解决快照里用new ListPoint(this.Boxes)新建副本恢复时整体替换。这个坑我在别的项目里也踩过引用类型做快照十有八九会翻车。5.3 箱子推到目标点上胜利判定却不触发现象肉眼看着所有箱子都在目标点上了程序没反应。原因关卡数据里用了*箱子在目标点或玩家在目标点这类复合字符解析时只处理了单一字符目标点集合里缺了这些坐标。解决解析的 switch 里补上*和分支同时往箱子集合和目标点集合里加数据。改完拿一个含复合字符的关卡跑一遍确认IsWin返回 true。5.4 方向键没反应或者人物往反方向走现象按上键人物不动或者按左键人物往右走。原因两种可能——一是KeyDown事件没绑上或被其他控件抢了焦点二是dx、dy的映射写反了把行列和屏幕坐标搞混。解决先确认窗体KeyPreview true且事件已绑定再对照 3.2 节的增量约定检查 switch 分支上(0,-1)、下(0,1)、左(-1,0)、右(1,0)一个都不能错。5.5 换关卡之后地图错位或越界现象加载第二关时画面只画了一半或者抛数组越界异常。原因不同关卡的字符串数组行长度不一致解析时按第一行长度算宽度后面按width * y x取字符就越界了。解决解析入口加校验遍历所有行确认长度等于rows[0].Length不一致直接抛带行号的异常。关卡数据是手写的这种低级错误很常见早校验早省心。6. 二次开发把推箱子改成你自己的网格游戏拿到这份源码之后最值钱的改造方向不是加皮肤而是把逻辑层和表现层彻底解耦让它变成一个通用的网格游戏骨架。具体做法是把LevelData里的TryMove、IsWin抽到一个独立的GameLogic类里这个类只依赖坐标和集合不引用任何System.Windows.Forms或System.Drawing的绘制 API。抽完之后同一套判定逻辑可以套到控制台版本、甚至一个简单的 Web 前端上UI 只负责把状态画出来。第二个改造点是关卡数据的加载方式。现在关卡硬编码在源码里改成从外部文本文件读取每行一个关卡文件用空行分隔。这样改关卡不用重新编译策划或者你自己用记事本就能调。// 从文件加载多关卡每个关卡之间用空行分隔 public static Liststring[] LoadLevels(string path) { var levels new Liststring[](); var current new Liststring(); foreach (string line in File.ReadAllLines(path)) { if (string.IsNullOrWhiteSpace(line)) { if (current.Count 0) { levels.Add(current.ToArray()); current.Clear(); } } else current.Add(line); } if (current.Count 0) levels.Add(current.ToArray()); return levels; }这段代码的关键在于空行作为关卡分隔符读完之后每个string[]就是一个完整关卡直接喂给Parse方法。参数上要注意文件编码统一用 UTF-8避免中文路径或特殊字符出问题。第三个方向是加“死局检测”。推箱子里箱子被推到墙角且不在目标点上这关就废了但玩家可能还在傻推。常见做法是在每次推箱子之后检查箱子是否贴墙、贴墙的两侧是否也是墙或边界满足条件就提示“当前局面无解建议撤销”。这个检测不需要搜索算法纯几何判断就能覆盖大部分死局实现成本低、体验提升明显。验证改造是否成功我一般用三个关卡做回归一个普通关卡验证基本移动和胜利判定一个含*、复合字符的关卡验证解析一个箱子多到需要撤销的关卡验证历史栈。三个都过基本就稳了。说个自己的教训早些年改这类网格游戏我总想着先把画面做好看结果逻辑层和绘制层缠在一起后来想加个新玩法改一处崩三处。从那以后我每次动这类项目都强制先把逻辑抽干净、跑通控制台版本再回头接 UI。希望这份拆解能帮你少走点弯路源码拿去跑一遍、改一版比看十篇文章都管用。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
C#推箱子游戏源码解析:WinForms实现与核心逻辑 简介:这是一份面向C#初学者与游戏编程爱好者的推箱子小游戏完整源码工程,基于WinForms实现,可用于课程设计参考、C#桌面开发练手或小游戏二次开发。压缩包共76个文件,约135KB,包含10个cs源文件、4个resx与resources资源… · 2026/11/5 18:45:45
README.md 文件的核心价值与实战模板分享 1. 项目概述:README.md 文件的核心价值每个项目都该有个好README——这话我从业十年说了不下百次。上周帮团队Review代码库时又发现三个项目只有空荡荡的README模板,这种场景实在太熟悉了。README.md远不止是个说明文件,它是项目的门面、开发… · 2026/11/5 1:46:54
FastSVDD:MATLAB加速SVDD异常检测训练实战 简介:FastSVDD 是支持向量数据描述(SVDD)算法的一种高效实现,面向从事异常检测、单类分类与故障诊断等方向的研究人员和工程师,尤其适合希望在 MATLAB 环境中快速验证模型、降低训练计算成本的中高级学习者。资源包共 … · 2026/11/5 2:03:54
EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析 1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被… · 2026/11/5 16:49:35
Rust原来很简单二:所有权系统 Part 2 核心篇 —— 所有权系统
本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理
6… · 2026/11/5 16:26:08
基于Hadoop+Spark的民宿智能推荐系统设计与实现 1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享… · 2026/11/5 14:59:25
ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
UART接口24位ADC搭配RS485,解决远距离温度监控难题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07