十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#截图工具开发:WinForm全屏覆盖与GDI+屏幕捕获实战

C#截图工具开发:WinForm全屏覆盖与GDI+屏幕捕获实战 简介一款使用C#语言开发的电脑截图小工具适合需要快速屏幕捕获的普通用户以及希望学习WinForms图形交互的初中级开发者。工具实现了鼠标拖拽选取、八个控制点调整选区、移动选框等核心操作对应描述中的Point、Rectangle与Moving状态字段代码思路清晰。压缩包共50个文件大小仅377KB包含18个cs源码、5个resx界面资源、5个resources资源文件并附带exe可执行程序、pdb调试符号及config配置目录按主程序、截图窗体、设置界面、帮助文档等模块组织便于按需查阅。目前已有521人学习下载。借助完整Visual Studio项目结构和GDI绘图逻辑读者既能直接拿到可用工具也能拆解选区命中测试、鼠标事件处理等典型WinForms编程技巧适合作为桌面小工具开发或C#图形编程的参考范例。1. 为什么自己写C#截图工具从WinForm到全屏覆盖市面上的截图工具要么捆绑安装要么联网上传截图想找一个启动快、不驻留、能自定义保存路径的轻量方案并不容易。这个C#截图小工具没有依赖第三方库核心就是WinForm全屏窗口 GDI位图操作代码结构清晰适合二次改造。它解决了两个常见痛点一是截图时不能直观看到当前鼠标划过区域的精确尺寸二是保存前没法调整选区边缘。整个实现可以拆成三个部分全屏覆盖窗口负责视觉反馈鼠标状态机负责选框交互ImageHelper负责把选中区域从屏幕位图中抠出来。这套设计对刚接触C#图形编程的人友好对做上位机或桌面工具集成的人来说也是一份可以快速移植的参考实现。2. 选框交互鼠标状态机与八个控制点2.1 鼠标事件和状态字段的对应关系截图功能落地在MainCut.cs里核心思路不是用一个拖拽控件而是让一个透明窗口覆盖整个屏幕然后在窗口的鼠标事件里动态计算选区。先看设计里几个关键字段它们决定了交互的流畅度private Point m_ptCurrent; // 当前鼠标位置 private Point m_ptTempForMove; // 移动选框时的临时基准点 private Rectangle m_rectClip; // 限定鼠标活动的区域通常等于屏幕工作区 private Rectangle[] m_rectDots new Rectangle[8]; // 八个控制点 protected bool m_bMoving; // 是否在移动整个选框 protected bool m_bChangeWidth; // 是否在改变宽度 protected bool m_bChangeHeight; // 是否在改变高度 protected bool m_bMouseHover; // 鼠标是否悬停在选框边缘 private bool _IsDrawed; // 是否已经完成了第一次选区 private Image _Image; // 保存从屏幕捕获的原始图像这里m_bMoving、m_bChangeWidth、m_bChangeHeight三个开关构成了一个简单的状态机。为什么不用枚举因为对角线拉伸时宽度和高度是同时变化的布尔位的组合比枚举更灵活。m_ptTempForMove在移动选框时记录按下鼠标那一刻的坐标目的是计算位移量避免每次移动都从零累加。鼠标按下的处理逻辑要区分三种情况点击位置落在选框内部时进入移动状态落在边缘控制点区域时进入尺寸调整状态落在选框外部时重新开始绘制新选区。用简单的条件判断来路由protected override void OnMouseDown(MouseEventArgs e) { if (e.Button ! MouseButtons.Left) return; int hit HitTest(e.Location); if (hit 0) { // 命中控制点准备拉伸 m_bChangeWidth (hit 0 || hit 2 || hit 4 || hit 6); // 左右两侧 m_bChangeHeight (hit 1 || hit 3 || hit 5 || hit 7); // 上下两侧 m_ptTempForMove e.Location; } else if (m_rectClip.Contains(e.Location) _IsDrawed) { // 在现有选区内按下准备整体移动 m_bMoving true; m_ptTempForMove e.Location; } else { // 开始新的选区 m_rectClip.Location e.Location; m_rectClip.Size Size.Empty; _IsDrawed true; } base.OnMouseDown(e); }HitTest用来判断鼠标是否落在八个控制点上控制点的布局顺序通常按“左上、上、右上、右、右下、下、左下、左”编号。这个顺序不是随便定的后续在计算光标形状和拉伸方向时可以用同一个索引映射到Cursor类型上。m_rectClip在这里既作为最终选区也作为鼠标活动范围的限制器当拖动超出屏幕边界时鼠标会被钳制在工作区内避免选框跑到屏幕外导致剪切异常。2.2 移动与改变尺寸的增量计算鼠标移动事件是截图交互里最容易写乱的部分。初版常见的错误是直接把鼠标当前位置赋给选区这样会导致选框“跳变”。正确做法是计算增量protected override void OnMouseMove(MouseEventArgs e) { if (m_bMoving) { int dx e.X - m_ptTempForMove.X; int dy e.Y - m_ptTempForMove.Y; m_rectClip.Offset(dx, dy); m_ptTempForMove e.Location; Invalidate(); return; } if (m_bChangeWidth || m_bChangeHeight) { Rectangle r m_rectClip; // 根据命中点调整边界这里以右下角为例 if (m_bChangeWidth) r.Width e.X - r.Left; if (m_bChangeHeight) r.Height e.Y - r.Top; // 处理反向拖动保证 Left/Top 始终小于 Right/Bottom if (r.Width 0) { r.Left e.X; r.Width -r.Width; } if (r.Height 0) { r.Top e.Y; r.Height -r.Height; } m_rectClip Rectangle.Intersect(r, m_rectClipBound); // 限制在屏幕边界内 Invalidate(); return; } // 未按下时更新鼠标悬停状态用于改变光标形状 m_bMouseHover HitTest(e.Location) 0 _IsDrawed; base.OnMouseMove(e); }这里的Invalidate()会触发OnPaint重绘重绘时要做两件事绘制半透明遮罩突出选区以及绘制八个控制点的白色小方块。控制点大小要跟鼠标坐标对齐不能简单画一个固定大小的矩形否则高DPI下会偏位。常见做法是用LogicalToDeviceUnits换算或者直接基于Graphics的PageUnit重新计算。2.3 控制点的命中测试与光标切换命中测试的精度直接影响手感。控制点检测宽容度通常比视觉方块大比如视觉方块是6x6像素但命中区域扩大到10x10这样用户不用精准对准小方块也能拖拽。实现时用一个简单的循环private int HitTest(Point p) { for (int i 0; i 8; i) { Rectangle rc m_rectDots[i]; rc.Inflate(4, 4); // 扩大热区 if (rc.Contains(p)) return i; } // 如果命中选框边缘也算作拉伸 if (m_rectClip.Contains(p) OnBorder(p)) return 8; return -1; }OnBorder判断点是否落在选框边缘的像素带内这样即使用户没点中角落也能通过边缘拖动调整大小。光标切换则根据命中点的索引八个方向映射到SizeNS、SizeWE、SizeNWSE、SizeNESW四种光标形状。这套逻辑和普通的自定义控件缩放没有本质区别但因为是全屏窗口需要注意焦点问题窗口必须设置ControlBox false、FormBorderStyle None、WindowState Maximized同时保证TopMost true才不会被其他窗口遮挡。3. 图像捕获与ImageHelper实现3.1 用CopyFromScreen捕获屏幕位图选框确定之后下一步就是把屏幕内容抓下来。ImageHelper.cs里封装了核心的截图方法用GDI的CopyFromScreen把指定矩形区域拷贝到位图public static Image CaptureScreen(Rectangle region) { Bitmap bmp new Bitmap(region.Width, region.Height, PixelFormat.Format24bppRgb); using (Graphics g Graphics.FromImage(bmp)) { g.CopyFromScreen(region.Location, Point.Empty, region.Size); } return bmp; }CopyFromScreen的第一个参数是屏幕坐标源点第二个参数是目标位图的偏移原点第三个参数是拷贝尺寸。这里有个关键细节如果截图窗口自身覆盖在屏幕上直接拷贝会把截图窗口的遮罩层也抓进去。解决办法是在捕获前隐藏主窗口等待一帧再拷贝this.Hide(); Thread.Sleep(50); // 等窗口彻底消失 Image img ImageHelper.CaptureScreen(m_rectClip); this.Show();Thread.Sleep(50)是工程上常见的做法但不完美频繁切换会有闪烁感。替代方案是捕获时判断窗口是否在选区内如果是就用BitBlt配合CAPTUREBLT标志绕过它。不过对于大多数场景先隐藏再截图已经够用毕竟截图工具不追求高速连拍。3.2 矩形裁剪与二次编辑边界ImageHelper里除了CaptureScreen还会提供一个裁剪方法。其实CopyFromScreen已经按矩形区域抓取所谓“裁剪”更多是处理选区坐标变换。当用户在全屏窗口上绘制选区时窗口坐标系和屏幕坐标系在正常情况下是相等的但在多显示器或设置了缩放的情况下需要调用Control.PointToScreen进行转换public static Rectangle ToScreenRect(Control win, Rectangle r) { Point tl win.PointToScreen(r.Location); return new Rectangle(tl, r.Size); }如果截图工具不是全屏窗口而是嵌入在某个面板里这个转换就是必须的。常见误用是直接拿窗口的相对坐标去CopyFromScreen结果截出来的图位置偏移。另外当用户完成选区后有的实现会跳转到第二个窗口进行编辑如画框、加文字那个编辑窗口也需要保留原始选区的屏幕坐标而不是重新计算。3.3 保存格式与文件名生成保存逻辑在CutPicSet.cs里管理它负责把用户选择的格式和目录配置应用到位图。保存到文件用Save方法需要根据扩展名选择编码器避免默认格式不对导致图片质量下降public static void SaveToFile(Image img, string path) { string ext Path.GetExtension(path).ToLower(); ImageFormat format ext switch { .png ImageFormat.Png, .jpg or .jpeg ImageFormat.Jpeg, .bmp ImageFormat.Bmp, _ ImageFormat.Png }; img.Save(path, format); }保存前要考虑位图是否被锁定。有些截图工具会把屏幕捕获的Bitmap一直攥在手里第二次截图时原来的Bitmap没有被Dispose导致内存越涨越高。常见做法是每次截图完成后立刻用using或显式Dispose释放临时位图只把最终结果转成剪贴板数据或保存文件。4. 边缘情况多显示器、DPI缩放与热键冲突截图工具在单显示器上运行良好但换到多显示器或高分屏环境就容易出问题。先看DPI缩放当系统缩放比例是125%或150%时WinForm默认按逻辑像素运行此时从屏幕抓到的物理像素范围和鼠标坐标不一致。解决办法是声明PerMonitorV2清单并调用SetProcessDpiAwarenessContext让窗口感知真实像素[System.Runtime.InteropServices.DllImport(user32.dll)] static extern bool SetProcessDpiAwarenessContext(IntPtr value); // 在Main方法最早处调用 SetProcessDpiAwarenessContext((IntPtr)(-4)); // -4 PerMonitorV2如果不做这个声明截图区域会整体右移下移边界出现一圈白边。多显示器环境下Screen.PrimaryScreen.Bounds只能拿到主屏幕全屏窗口应该遍历Screen.AllScreens合并成一个虚拟矩形作为覆盖范围。具体实现是取所有屏幕Bounds的并集然后设置窗体位置和大小Rectangle virtualScreen SystemInformation.VirtualScreen; this.Bounds virtualScreen;这样鼠标在扩展屏上也能正常绘制选区。热键冲突是另一个容易忽略的点很多截图工具用PrintScreen作为触发键但这个键本身会被系统保留用于全屏截图。如果要让按下PrintScreen后直接启动截图界面需要注册全局热键而不是简单监听KeyDown。全局热键用RegisterHotKey注册VK_SNAPSHOT并在WndProc里处理WM_HOTKEY消息protected override void WndProc(ref Message m) { const int WM_HOTKEY 0x0312; if (m.Msg WM_HOTKEY m.WParam.ToInt32() HOTKEY_ID) { // 启动截图注意此时要隐藏主窗体重置状态 StartCapture(); } base.WndProc(ref m); }注册热键前还要检查是否被其他程序占用RegisterHotKey返回false时给出提示。最后提一个调试技巧如果截图结果偏黑或偏白先检查是不是Graphics对象的CompositingMode设置成了SourceCopy导致透明区域被覆盖如果选区边缘有锯齿感可以把Graphics.SmoothingMode设为AntiAlias再画选框。整个项目没有复杂的第三方依赖把这几个边缘场景修好日常截图体验已经能逼近系统自带工具。本文还有配套的精品资源点击获取
返回列表