简介:一套基于C# WinForms的图片裁剪工具源码,面向需要在桌面应用中集成截图或裁剪功能的.NET开发者,主要用于在窗体中通过带手柄的矩形选区交互式裁剪图片,操作方式接近ACDSee,适合工具类软件的二次开发或学习参考。压缩包共包含32个文件,体积仅51KB,核心逻辑集中在Form1.cs、FrameObj.cs等11个cs文件中,另附resx窗体布局、csproj工程配置以及可直接运行的exe程序,便于先运行查看效果再对照源码理解。已有663人学习下载。阅读源码可掌握GDI+绘制选区、鼠标事件处理、手柄调整与图像裁剪的完整思路,覆盖从框选、缩放选区到最终生成裁剪图片的闭环流程。同时,Form1与FrameObj两个类清晰分离主界面与矩形框对象,配合属性设置和资源文件可了解WinForms工程的模块划分,既适合初学者学习交互控件设计,也可作为图片编辑功能的模块化改造参考。
1. 从一张图里"抠"出想要的那块:这个需求比听起来麻烦一点
做WinForm桌面工具的朋友,迟早会遇到这样一个需求:让用户从一张大图里自己框选一块区域,保存成独立的图片文件。最典型的场景是上位机里的截图配置、票据扫描件的区域提取、图片素材的批量预处理,或者干脆就是一个简易修图工具里的"裁剪"按钮。
这东西第一眼看上去很简单——不就是画个矩形框,然后按框把图截出来吗?但真正动手写的时候会发现,坑全藏在细节里:裁剪框怎么跟随鼠标平滑移动?显示框和原始图像的尺寸不一致时怎么换算?PictureBox缩放模式下坐标怎么偏移?还有个最容易被忽略的问题——你画上去的矩形框和底下的图片是不是在同一套坐标系里?
这篇文章我就把C# WinForm里实现图片裁剪的完整方案拆开讲透,从界面搭建、裁剪框交互、坐标换算到保存输出,每一段都会讲清楚"为什么这么做",顺带把我在实际项目里踩过的坑和绕过的弯都交代出来。无论你是刚接触WinForm的新手,还是已经写过几个桌面工具的老手,照着这套思路走,大概率能少折腾两天。
2. 先定方案:PictureBox为主、自绘裁剪框为辅,还是全自绘?
动手写代码之前,先花几分钟想清楚控件架构。这个决定影响后面所有逻辑的复杂度,改起来也伤筋动骨,所以值得提前说透。
2.1 两种常见做法对比
网上搜C# WinForm图片裁剪,出来的方案大概分两类。
第一类是纯PictureBox流:把图片显示在PictureBox里,SizeMode设为Zoom,然后用一个Panel或者Label之类的东西模拟裁剪框,靠拖动它来选区域,最后截图时用Control.DrawToBitmap抓取区域。这套方案实现简单,但问题也明显:DrawToBitmap抓到的是控件区域位图,分辨率受屏幕显示像素限制,原图是4000x3000的话,裁出来可能只有几百乘几百像素,细节全丢了,做素材裁剪完全不能用。
第二类是我要重点讲的方案:PictureBox只负责显示图片,裁剪框用自绘的方式画在图片上,交互逻辑通过鼠标事件自己控制,最终裁剪直接用原始Bitmap数据操作。这套方案的优点是选区精度不依赖屏幕分辨率,原图多大就能裁多大,而且视觉反馈灵活,可以画遮罩、网格线、边角手柄。缺点是代码量多一些,坐标换算要想清楚。
我自己的习惯是选第二类,因为图片裁剪这种功能一旦做出来,使用者大概率会拿它处理高分辨率原图,如果你一开始贪图省事用DrawToBitmap,后面被骂"裁出来的图怎么这么糊"时还得返工。
2.2 最终控件结构
整个窗体的控件树很简单:
- Form主窗体,承载菜单或按钮(打开图片、保存裁剪结果)
- Panel容器,设置AutoScroll = true,用于容纳PictureBox
- PictureBox,承载图片显示,SizeMode = Zoom,负责鼠标事件
- 状态栏或Label(可选),实时显示鼠标坐标、选区尺寸
这里有个重要的设计选择:PictureBox放在Panel里并且开启AutoScroll,是为了支持大图浏览。如果图片显示尺寸超过窗口可视区,Panel自动出现滚动条,PictureBox的坐标系也会随滚动条偏移——这个细节后面换算坐标时一定要考虑到,否则鼠标点和实际图片位置会对不上。
注意:Panel.AutoScroll开启后,PictureBox的Location在滚动状态下会变成负值,鼠标事件拿到的坐标是相对PictureBox本身的,不是相对可视区。处理滚动场景时,建议用PictureBox.PointToClient(Cursor.Position)来换算,而不是直接用e.Location,不然滚动之后裁剪框会"漂"。
3. 裁剪框交互:核心是三个鼠标事件,但细节决定手感
裁剪框的交互逻辑,说白了就是三件事:按下鼠标开始画框、移动鼠标调整框、松开鼠标确定框。如果你还想支持"拖动已有裁剪框换位置"和"拖边角调整大小",就得再加一些命中检测逻辑。为了控制篇幅,我先把最基础的"按下画新框"讲清楚,再补充进阶的拖动调整方案。
3.1 鼠标事件的状态机设计
裁剪框从交互角度看是一个三态状态机:空闲(没有框)、正在画框(鼠标按下未松开)、框已确定(可以重新画或拖动)。用一个枚举来管理很清晰:
public enum CropState { Idle, // 无裁剪框 Drawing, // 正在画框 Dragging, // 拖动整个框 Resizing // 调整框大小 }在MouseDown里判断当前状态:如果鼠标落在已有裁剪框内部,进入Dragging;如果落在边框附近,进入Resizing;如果在框外,就清掉旧框,进入Drawing并记录起点。MouseMove里根据状态更新对应的矩形参数,然后调用Invalidate()触发重绘。MouseUp里将状态切回Idle或保持"有框"状态。
private void pictureBox_MouseDown(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Left) return; // 如果已有裁剪框且鼠标在框内,进入拖拽模式 if (_cropRect.Contains(e.Location)) { _state = CropState.Dragging; _dragStart = e.Location; _dragOrigin = _cropRect.Location; return; } // 否则开启新选框 _state = CropState.Drawing; _startPoint = e.Location; _cropRect = new Rectangle(e.Location, Size.Empty); }MouseMove里的逻辑重点是:每次移动都要重新计算Rectangle,并且要防止用户向左上角拖动时矩形变成负数宽高。Rectangle.FromLTRB这个静态方法能在任何拖动方向下生成正的宽高,比手动判断省事得多:
private void pictureBox_MouseMove(object sender, MouseEventArgs e) { if (_state == CropState.Drawing) { _cropRect = Rectangle.FromLTRB( Math.Min(_startPoint.X, e.X), Math.Min(_startPoint.Y, e.Y), Math.Max(_startPoint.X, e.X), Math.Max(_startPoint.Y, e.Y)); pictureBox.Invalidate(); } else if (_state == CropState.Dragging) { int dx = e.X - _dragStart.X; int dy = e.Y - _dragStart.Y; _cropRect.Location = new Point(_dragOrigin.X + dx, _dragOrigin.Y + dy); pictureBox.Invalidate(); } }3.2 绘图细节:遮罩加选区,层次感立刻出来
画框不只是画一个矩形边框。专业一点的裁剪工具,选区外的部分会加一层半透明遮罩,选区本身加高亮边框和四角手柄。这样用户一眼就能看清"我选中的是哪块,没选中的是哪块"。
实现遮罩的常规思路是:先用半透明黑色填充整个PictureBox,然后再把选区部分"挖空"。GDI+里可以用GraphicsPath和ExcludeClip实现,核心代码大概是这样的:
private void pictureBox_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; if (_cropRect.IsEmpty) return; // 用路径排除选区实现挖空 using (GraphicsPath path = new GraphicsPath()) { path.AddRectangle(new Rectangle(0, 0, pictureBox.Width, pictureBox.Height)); path.AddRectangle(_cropRect); g.SetClip(path, System.Drawing.Drawing2D.CombineMode.Exclude); using (SolidBrush mask = new SolidBrush(Color.FromArgb(120, 0, 0, 0))) { g.FillRectangle(mask, 0, 0, pictureBox.Width, pictureBox.Height); } g.ResetClip(); } // 绘制选区边框 using (Pen pen = new Pen(Color.White, 1.5f)) { g.DrawRectangle(pen, _cropRect); } // 绘制四角手柄 // ... }这里有个容易忽略的细节:GraphicsPath.AddRectangle(整个控件) 和 AddRectangle(裁剪框) 两个矩形相加,SetClip配合CombineMode.Exclude之后,填充范围就是"整块减去裁剪框",效果正好是选区外变暗、选区内清晰。
如果你想让遮罩区域的网格线、九宫格辅助线也出来,可以在挖空之后手动从_cropRect边缘等距画线。但注意线和框都最好在PictureBox的Paint事件里画,并且把PictureBox的DoubleBuffered设为true,不然拖动鼠标时画面会闪得很厉害。
// 在窗体构造函数或Load事件里开启双缓冲 typeof(PictureBox).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) .SetValue(pictureBox, true);除了双缓冲,还有一个优化细节:如果图片显示尺寸很大(比如超过4000像素宽),每次Invalidate都重绘整个PictureBox会产生明显卡顿。可以用Invalidate(_cropRect)或者把上次绘制的区域一并纳入重绘范围,只刷新裁剪框周边的区域,性能会好很多。不过这个优化在一般应用场景里感知不强,只在处理超长超大图时必须做。
3.3 进阶:裁好的框怎么"微调"
如果只做"画新框"功能,交互上其实够用了,但用户在真实使用中会觉得别扭——不小心画歪了就得重新画。所以通常要支持两种微调:一是把鼠标移到框内拖动整个框,二是拖动边框或四角调整尺寸。
拖动整个框的逻辑在3.1那节已经给了,核心是MouseDown时判断鼠标在不在_cropRect内,然后记录起点和框的原始左上角位置,MouseMove里用偏移量重新计算框位置,同时要注意不让框超出PictureBox边界。
调整尺寸稍微复杂一点,需要做"命中测试":根据鼠标离框四边和四角多近,判断当前是调整哪条边。我通常设定一个8像素的容差范围,即鼠标在边框8像素以内就算命中,然后标记当前需要调整的边(Left、Right、Top、Bottom分别用标志位表示)。
private const int HANDLE_SIZE = 6; // 手柄尺寸 private const int HIT_TOLERANCE = 8; // 命中容差 private bool IsNearEdge(Point p, int edgeFlag) { bool hit = false; if ((edgeFlag & EdgeFlags.Left) != 0 && Math.Abs(p.X - _cropRect.Left) <= HIT_TOLERANCE) hit = true; if ((edgeFlag & EdgeFlags.Right) != 0 && Math.Abs(p.X - _cropRect.Right) <= HIT_TOLERANCE) hit = true; // Top / Bottom 同理 return hit; }鼠标命中边之后,记得把Cursor改成SizeWE、SizeNS或SizeNWSE等样式,给用户直观的"此处可拖"提示。这个属于体验细节,虽然不写也能用,但写了之后手感完全不同。
4. 坐标换算:看似最简单、实则最容易翻车的一环
现在裁剪框有了,用户也框好了一块区域,接下来要做的就是从Bitmap里把选中的像素取出来。这一步的核心是坐标换算。为什么要有换算?因为屏幕上显示的图片经过了Scale缩放,特别是PictureBox的SizeMode设为Zoom时,显示尺寸和原始图片尺寸很可能不是1:1,你MouseDown拿到的坐标是"控件坐标",而要从Bitmap里切像素需要的是"图片原始坐标",两个坐标系必须对齐。
4.1 Zoom模式下的"显示区域"怎么算
先明确一个概念:SizeMode = Zoom时,PictureBox会按比例缩放图片,保持宽高比居中显示,图片四周可能有留白。如果你直接在MouseDown的e.Location上用这个坐标去索引Bitmap,在图片被缩放时100%会偏;如果图片放大显示,裁出来的区域会往左上角偏;如果缩小显示,裁出来的区域会比预期大。
正确的换算是:先算出图片实际显示在控件里的那块矩形(ScaleImageRect),再把鼠标坐标相对这个矩形的偏移量,按缩放比例映射回原图坐标。
private Rectangle CalcImageDisplayRect(PictureBox pb, Image img) { if (img == null) return Rectangle.Empty; double imgRatio = (double)img.Width / img.Height; double ctrlRatio = (double)pb.Width / pb.Height; int w, h, x, y; if (imgRatio > ctrlRatio) { // 图片更宽,宽对齐,高留白 w = pb.Width; h = (int)(pb.Width / imgRatio); x = 0; y = (pb.Height - h) / 2; } else { // 图片更高,高对齐,宽留白 h = pb.Height; w = (int)(pb.Height * imgRatio); y = 0; x = (pb.Width - w) / 2; } return new Rectangle(x, y, w, h); }4.2 从控件坐标到原图坐标的换算公式
有了显示矩形,换算公式就清楚了:
// cropRectInDisplay 是裁剪框在控件坐标系下的矩形(只截取图片显示区域内的部分) Rectangle ConvertToImageRect(Rectangle cropInDisplay, Rectangle displayRect, Image img) { double scaleX = (double)img.Width / displayRect.Width; double scaleY = (double)img.Height / displayRect.Height; int imgX = (int)((cropInDisplay.X - displayRect.X) * scaleX); int imgY = (int)((cropInDisplay.Y - displayRect.Y) * scaleY); int imgW = (int)(cropInDisplay.Width * scaleX); int imgH = (int)(cropInDisplay.Height * scaleY); // 边界保护 imgX = Math.Max(0, imgX); imgY = Math.Max(0, imgY); imgW = Math.Min(img.Width - imgX, imgW); imgH = Math.Min(img.Height - imgY, imgH); return new Rectangle(imgX, imgY, imgW, imgH); }这段代码在几乎所有WinForm图片裁剪场景里都是核心逻辑,值得你把它封装成一个公共方法。注意边界保护:用户画的框可能略微超出图片显示区域,如果裁剪框左上角落在留白区(displayRect之外的区域),换算出的坐标可能是负数或超过图片边界,直接拿去用会抛异常或产生黑边。裁剪前必须做和上面一样的clamp。
4.3 滚动条场景下的坐标修正
如果你的图片尺寸很大,加了Panel.AutoScroll滚动条,情况会更微妙。MouseDown事件里e.Location是相对PictureBox左上角的坐标,PictureBox本身在滚动时会整体平移,所以这个坐标不受滚动影响,可以直接用。但是如果你用了另一种方案——把PictureBox放进Panel并修改它的Location来调整可视区域,那就要特别注意换算。
一个比较稳妥的做法是在窗体加一个变量记录"当前图片在屏幕上的实际显示位置",每次滚动或缩放时更新它,然后所有鼠标坐标都统一用PointToClient转成相对于PictureBox的坐标再处理。这样逻辑统一,不容易出错。
经验之谈:滚动条场景我建议直接用PictureBox.PointToClient(Control.MousePosition)来获取鼠标坐标,而不是直接用e.Location。原因很简单:e.Location永远相对于触发事件的控件本身,但如果你在PictureBox外面还有别的控件参与了事件冒泡,或者你用了Panel的滚动偏移,e.Location很容易和实际显示位置错位。用PointToClient从根本上绕开了这个问题。
5. 真正"裁"下去:从Bitmap切割到保存文件的完整流程
坐标换算完,裁剪本身其实就是一个简单的Bitmap切割操作。但这里也有两个分支能选,我分别说下利弊。
5.1 方案一:Clone切割
Bitmap.Clone(Rectangle, PixelFormat)是最直接的方式,指定一个矩形区域,返回一张新图,性能也不错。注意第二个参数PixelFormat建议用原图的PixelFormat,避免颜色格式转换导致色偏。
Bitmap CropImage(Bitmap source, Rectangle cropRect) { // 加锁读取原图像素,确保裁剪区域不越界 Rectangle bounds = new Rectangle(0, 0, source.Width, source.Height); Rectangle intersectRect = Rectangle.Intersect(bounds, cropRect); if (intersectRect.IsEmpty) { throw new ArgumentException("裁剪区域不在图片范围内"); } Bitmap result = source.Clone(intersectRect, source.PixelFormat); return result; }5.2 方案二:DrawImage重绘
另一种方式是用Graphics.DrawImage把原图指定区域画到一张新Bitmap上:
Bitmap CropImageWithDraw(Bitmap source, Rectangle cropRect) { Bitmap result = new Bitmap(cropRect.Width, cropRect.Height, PixelFormat.Format24bppRgb); using (Graphics g = Graphics.FromImage(result)) { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImage(source, new Rectangle(0, 0, cropRect.Width, cropRect.Height), cropRect, GraphicsUnit.Pixel); } return result; }DrawImage方案比Clone灵活的地方在于:它可以在裁剪的同时做尺寸调整(比如把原图的200x200区域缩放成100x100输出),这在做缩略图时很有用。如果只是纯裁剪不缩放,Clone的性能和清晰度都更好,优先选Clone。
5.3 保存文件:格式推断与质量参数
裁剪完成后用SaveFileDialog让用户选择保存路径,然后根据文件扩展名推断图片格式:
void SaveCropedImage(Bitmap crop, string path) { string ext = Path.GetExtension(path).ToLowerInvariant(); ImageFormat format = ImageFormat.Png; switch (ext) { case ".jpg": case ".jpeg": format = ImageFormat.Jpeg; break; case ".bmp": format = ImageFormat.Bmp; break; case ".gif": format = ImageFormat.Gif; break; case ".png": default: format = ImageFormat.Png; break; } crop.Save(path, format); }这里有个小坑:如果保存成JPG格式且不指定质量参数,默认的保存质量虽然能看,但锯齿和压缩痕迹会比较明显。建议用EncoderParameters指定质量:
if (format == ImageFormat.Jpeg) { using (EncoderParameters eps = new EncoderParameters(1)) using (EncoderParameter ep = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 92L)) { eps.Param[0] = ep; crop.Save(path, GetJpegCodec(), eps); } } else { crop.Save(path, format); }6. 高DPI和缩放场景:WinForm裁剪最常见的暗坑
关于高DPI的问题,我在这块栽过跟头,必须拿出来单独说。
很多开发者开发机上显示设置是100%缩放,但用户电脑可能是125%或150%缩放。WinForm程序如果不做DPI感知声明,系统会默认按虚拟化缩放处理,导致两个严重后果:一是界面字体和控件模糊,二是鼠标坐标和实际绘图坐标错位——你点下去的位置和实际事件上报的坐标相差一个缩放系数,裁剪框画出来总是偏的。
解决办法是在入口处声明DPI感知。最简单的方式是改app.manifest,加上:
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> </windowsSettings> </application>加了这段之后,程序会按真实DPI渲染,界面清晰了,鼠标坐标也和显示一致了。
但如果你的程序需要同时兼容老的虚拟化DPI行为(比如有些旧控件库不支持PerMonitorV2),那就要在逻辑里自己处理DPI缩放系数。获取缩放比:
float dpiScale = this.DeviceDpi / 96f;然后所有涉及坐标和绘制的地方都要乘上这个系数。这是最麻烦的处理方式,能不用尽量不用,直接在manifest里声明DPIAware才是正道。
7. 我踩过的几个坑和对应的处理方案
代码讲完了,说几个我实际项目中遇到过的坑,每个都让我排查了不少时间,提前写好帮你避开。
7.1 裁剪框"画不直":Paint事件里的坐标系没对上
有一次遇到的bug是:鼠标在PictureBox上画矩形,框的右边界和底边界总比鼠标位置偏了一段距离。排查下来发现,是PictureBox的SizeMode改成了StretchImage,图片被拉伸填满整个控件,显示坐标和图片坐标之间不仅缩放比例不同,而且还不等比(宽高比变了)。解决办法是始终用Zoom模式,或者在StretchImage下单独计算横向和纵向的scaleX、scaleY(这两个值可能不相等,公式前面那套仍然适用,只是displayRect变成了整个控件区域)。
7.2 拖动时闪烁严重
闪烁的根本原因是每次Invalidate都触发了整个PictureBox的重绘。解决思路有三个:一是开启DoubleBuffered(治标,通常能解决80%的闪烁);二是重写PictureBox的OnPaintBackground为空方法,把图片绘制也挪到OnPaint里统一管理;三是精确指定重绘区域,只Invalidate被影响的矩形。三个叠加效果最好,但一般前两个已经够用。
7.3 裁剪框显示在留白区域时,获得超出图片范围的坐标
这个前面已经重点讲过,核心就是边界保护。如果不做,用户把框拖到图片外,裁剪时会在Bitmap.Clone里抛出ArgumentException。处理好边界之后,程序可以优雅地把框"吸附"回图片范围内,或者对越界部分自动剪裁。
7.4 撤销与重复选框
用户画错框想重画,这是最常见的操作。最简单的方案是允许用户在已有框的情况下继续画新框(老框自动消失)。但如果你希望用户能"取消当前框"退出裁剪状态,可以监听Esc键,在KeyDown里把_cropRect重置为空,并刷新界面:
protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Escape) { _cropRect = Rectangle.Empty; _state = CropState.Idle; pictureBox.Invalidate(); e.Handled = true; } base.OnKeyDown(e); }如果想做得更完善,可以用一个Stack 做撤销栈,每次画完框压栈,Ctrl+Z弹栈恢复上一个框。这个功能对频繁调整选区的用户非常友好,实际开发中建议加上。
7.5 SaveFileDialog默认路径和文件名设置
保存对话框的默认文件名可以自动带上原图的文件名加后缀,比如"原图名_cropped.png",这样用户就不会保存成"未命名.png"了。这个细节虽然小,但能让程序显得专业很多。
8. 从"能用"到"好用":几个提升体验的小功能
最后写几个追加的小功能,它们单独看都不复杂,但合在一起,整个裁剪工具就从"能跑"变成了"顺手"。
实时显示选区尺寸和位置:在状态栏Label里显示"X: 100, Y: 100, W: 500, H: 300",用户在精准裁剪时特别有用。坐标换算成原图坐标,这样用户知道裁出来每边大概多少像素。
支持滚轮缩放图片:在PictureBox的MouseWheel事件里调整图片的显示缩放比例,配合裁剪框一起工作。典型场景是用户想把裁切线精确对上图片里的某个物体边缘,原图大而屏幕小,就需要放大局部来微调。实现思路是维护一个scale变量,显示时手动计算DrawImage的目标矩形,而不是依赖SizeMode。
双击裁剪框确认裁剪:很多图片处理软件的交互都是这样。双击事件里直接触发裁剪保存逻辑,比用户去找"保存"按钮更流畅。
private void pictureBox_DoubleClick(object sender, EventArgs e) { if (_cropRect.IsEmpty) return; // 执行裁剪并保存 PerformCrop(); }- 把裁剪框绘制逻辑封装成一个独立的UserControl。如果你要在多个项目里复用这个功能,这一步是值得的。把Client矩形、图片对象、裁剪框状态全部封装到控件内部,对外暴露CropCompleted事件和CurrentCropRect属性,业务方只需要绑定事件拿结果就行。我在做上位机项目时把这个控件抽出来,后续两个项目直接拖上去用,省了很多重复工作。
WinForm做图片裁剪并不难,难的是把坐标系理清楚、交互做顺手。把所有逻辑拆开看,核心其实就三件事:裁剪框在显示层怎么画、鼠标交互怎么管理、显示坐标怎么映射回原图坐标。把这三件事想明白,代码量虽然不小,但每行都是确定性的,照着跑就能出结果。
我最后再提醒一句:如果你只是临时用一用,不想自己造轮子,GitHub上有不少开源的WinForm图片裁剪控件可以直接引用,但一旦遇到项目定制需求(比如批量裁剪、特殊遮罩样式、区域记忆),还是绕不开自己理解这套坐标换算逻辑。把这篇文章里的核心思路吃透,你再去看那些开源控件的源码,会突然觉得它们也不神秘了。
本文还有配套的精品资源,点击获取