ARTICLE DETAIL

资讯详情

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

C# GDI+ 在图片上绘制文字的完整实战指南

C# GDI+ 在图片上绘制文字的完整实战指南 简介面向C#开发者的图片文字绘制完整示例以RAR压缩包形式提供解决图像处理、报表生成等场景中向图片添加文字的需求。资源共有42个文件以12个C#源码文件为主体涵盖窗体逻辑、程序入口与设计器代码另含4个资源文件、项目解决方案文件和可执行程序并有部分配置、缓存与说明文档整体压缩后仅67KB小巧且完整。已有411人学习浏览适合初学GDI绘图或需要快速实现图片标注的开发者。示例基于System.Drawing命名空间演示了Bitmap加载图像、Graphics创建画布、Font设置字体、SolidBrush指定颜色及DrawString绘制文本的完整流程并涉及坐标原点与异常处理等细节。通过该源码可直观掌握在图片上写字的全部环节便于直接复用或改造集成到自己的项目中。 作为长期做C#后端和桌面端开发的工程师我几乎每周都会遇到“在图片上加点文字”的需求。最早是给公司活动海报批量加日期水印后来是帮运营同事给商品图自动生成价格标签再后来是做验证码生成器、给截图自动加来源标注。折腾得多了干脆整理出一套可直接复用的完整实例也就是这次要展开讲的项目C#在图片上绘制文字的完整源码。这篇文章会把GDI绘图的底层机制、完整实操步骤、中文乱码和清晰度这些高频坑位以及居中排版、多行文字、旋转文字等进阶玩法全部拆开讲清楚适合刚开始接触GDI的新手也适合想快速抄一套可用代码的WinForm或ASP.NET开发者。1. 为什么要在图片上绘制文字从水印到动态出图很多人第一次接触“C#绘制文字”是在做水印功能。确实给图片加水印是最典型的场景博客配图自动加上站点域名电商详情页批量生成价格标签用户上传的头像自动打上一行昵称或者内部系统给报告截图标注时间和操作人。但深入做下去你会发现绘制文字的价值远不止“加个水印”这么简单。另一类高频需求是动态生成图片。比如企业微信机器人定时推送的日报卡片如果直接用HTML转图片依赖浏览器环境资源开销大如果提前做好模板图片再用C#在指定坐标插入动态数据比如今日销售额、订单数量、排名变化响应速度快且完全可控。再比如网站验证码传统做法是用专门库生成但用GDI自己画反而更灵活——背景噪点、干扰线、字符间距都可以自己控制。还有一类场景是图片标注工具。我做过一个内部截图管理工具用户在截图上框选区域后需要把选区的坐标、尺寸、颜色值直接画在图片上方便设计同学沟通。这种场景下C#的System.Drawing就是最顺手的工具不依赖任何第三方组件一台Windows服务器就能跑。值得特别说明的是System.Drawing在.NET Framework时代是Windows专属的GDI封装到了.NET 6/7/8时代微软把它拆成System.Drawing.Common包并增加了对非Windows平台的支持限制。做ASP.NET Core服务时如果部署在Linux容器里需要额外注意一下System.Drawing.Common在非Windows系统上默认不可用需要设置System.Drawing.EnableUnixSupport环境变量才能跑而且官方文档明确标注这个兼容模式“不是长期支持方向”。所以如果你要长期维护跨平台服务可以考虑用SkiaSharp替代但如果你像我一样主要做Windows桌面工具或Windows Server上的内部服务System.Drawing依然是最简单直接的选择。无论你属于哪类需求核心逻辑都是差不多的加载图片、创建画布Graphics对象、设置字体和画刷、画出文字、保存输出。这套流程理解以后换什么场景都是同一套思路。2. 底层绘制机制Graphics对象没有你想象的那么难C#绘制图片文字说到底就是GDI那套东西。GDI是Windows提供的2D图形绘制接口System.Drawing是对它的封装。很多人一听“图形编程”就头大其实在图片上写字只需要掌握三个核心要素。第一个要素是Bitmap也就是内存中的图像载体。你可以通过new Bitmap(width, height)创建一张空白图也可以用Bitmap.FromFile(path)加载一张已有的图片。这个对象说白了就是一张画布所有绘制操作最终都会落到这张画布上。需要注意Bitmap本身不负责绘制绘制动作要交给Graphics。第二个要素是Graphics它是真正的画笔执行者。通过Graphics.FromImage(bitmap)把某个Bitmap“绑定”到画笔上之后所有DrawString、DrawLine、DrawRectangle操作都会直接画到这张图上。理解Graphics和Bitmap的关系可以类比成画家和画布——Graphics是拿笔的手Bitmap是铺在桌上的画布每次绘制都是手在画布上落笔。一个Graphics对象同一时刻只能绑定一张Bitmap想画到另一张图就重新FromImage一次。第三个要素是Font和Brush。Font定义字体、字号、字型粗体、斜体等Brush定义文字的颜色也可以理解成“颜料的质地”。最简单的用法是new SolidBrush(Color.Red)创建一个纯色画刷也可以用new LinearGradientBrush做渐变字用TextureBrush做纹理字。画完文字后画刷记得Dispose。关于资源释放的坑我在后面的避坑章节里详细讲。绘制文字的核心方法就一个graphics.DrawString(string text, Font font, Brush brush, float x, float y);x和y是文字左上角的位置坐标。如果只知道区域而不知道坐标系可以先构造一个RectangleF用重载方法DrawString(text, font, brush, rectangleF)让文字自动排布在这个区域内配合StringFormat可以控制水平居中、垂直居中、右对齐等。这里有个新手常见的误区——坐标和DPI的关系。如果你的图片是300DPI的高清图用DrawString(text, font, brush, 0, 0)画出来的文字会特别小因为GDI默认按96DPI的逻辑单位去度量字号。解决办法是画之前调整Graphics的PageUnit为GraphicsUnit.Pixel并且根据实际DPI比例放大Font的Size。比如一张300DPI的图上想写字号24pt的文字实际应该用24 * 300 / 72像素的Font来画。我在做高清海报写入时经常要用这个转换否则出来的文字和设计稿对不上。3. 完整实例拆解从读取图片到写出带文字的成品图现在进入正题。下面这套代码是我日常最常用的完整实例功能是加载一张底图在指定位置绘制主标题和副标题最后保存为新文件。不需要任何第三方NuGet包新建一个控制台项目就能跑。using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; class Program { static void Main(string[] args) { string inputPath C:\temp\background.png; string outputPath C:\temp\output.png; using (Bitmap canvas new Bitmap(inputPath)) { using (Graphics g Graphics.FromImage(canvas)) { // 高质量模式设置 g.SmoothingMode SmoothingMode.AntiAlias; g.InterpolationMode InterpolationMode.HighQualityBicubic; g.PixelOffsetMode PixelOffsetMode.HighQuality; g.TextRenderingHint TextRenderingHint.AntiAliasGridFit; // 画主标题 using (Font mainFont new Font(微软雅黑, 32, FontStyle.Bold, GraphicsUnit.Pixel)) using (Brush mainBrush new SolidBrush(Color.White)) { // 先用半透明黑色矩形垫底增加文字可读性 RectangleF mainRect new RectangleF(50, 50, 600, 60); using (Brush bgBrush new SolidBrush(Color.FromArgb(100, 0, 0, 0))) { g.FillRectangle(bgBrush, mainRect); } StringFormat mainFormat new StringFormat(); mainFormat.Alignment StringAlignment.Near; // 水平左对齐 mainFormat.LineAlignment StringAlignment.Center; // 垂直居中 g.DrawString(9PM 项目例会 - 2026年度计划, mainFont, mainBrush, mainRect, mainFormat); mainFormat.Dispose(); } // 画副标题 using (Font subFont new Font(微软雅黑, 18, FontStyle.Regular, GraphicsUnit.Pixel)) using (Brush subBrush new SolidBrush(Color.LightGray)) { g.DrawString(整理时间2026-01-05, subFont, subBrush, new PointF(50, 130)); } } canvas.Save(outputPath, ImageFormat.Png); } Console.WriteLine(绘制完成 outputPath); } }这段代码看起来简单但有四个细节是我特意写进去的新手很容易漏。第一using关键字四处包围。Bitmap、Graphics、Font、Brush全部实现IDisposable不释放的话文件被占用、内存持续增长、GDI句柄泄漏这些问题会陆续找上门。尤其是跑批量处理比如一次给500张图加水印时不释放句柄的直接后果就是程序跑着跑着突然抛OutOfMemoryException其实不是真的内存不够而是GDI句柄耗尽了。我见过同事在生产环境踩这个坑排查了一下午才发现是Bitmap没有Dispose。第二SmoothingMode.AntiAlias和TextRenderingHint.AntiAliasGridFit同时设置。前者影响矢量图形边缘的抗锯齿后者影响文字渲染的清晰度。只设置前者不设置后者文字边缘会有明显的“毛刺”只设置后者不设置前者画矩形、线条时边缘是锯齿。二者搭配是绘制质量最稳定的组合。不过要注意如果文字字号很小比如12像素以下AntiAliasGridFit反而会让笔画模糊这种情况下建议改成TextRenderingHint.SingleBitPerPixelGridFit来保证可读性。第三先画半透明矩形垫底。很多人画文字直接DrawString遇到底图颜色复杂、文字和背景对比度不够时文字就会“糊”在图上看不见。我的做法是先FillRectangle画一个半透明黑色块再在这个矩形上画文字这样无论底图是什么颜色文字都能保持可读性。透明度用Color.FromArgb(alpha, r, g, b)控制alpha值100左右比较适中既能衬出文字又不会完全遮住底图内容。第四Font构造函数里我用了GraphicsUnit.Pixel。如果省略这个参数默认是Point1/72英寸在96DPI屏幕上32pt约等于43像素写出来比你预期的大一圈。用Pixel单位后字号和坐标体系完全一致坐标设多少就是多少像素心理负担小很多。运行环境方面如果你用.NET 6需要先安装NuGet包System.Drawing.Common然后在项目文件.csproj里加上ItemGroup PackageReference IncludeSystem.Drawing.Common Version8.0.0 / /ItemGroup.NET Framework 4.7.2及以下版本不需要额外装包直接就能用。这个区别我经常忘了提导致有读者copy代码后报“找不到System.Drawing命名空间”的错误。4. 中文乱码、锯齿边缘与清晰度最容易翻车的三个细节这块是我实际回答问题最多的地方。三条主线中文显示不全、画出来文字模糊、保存格式不对导致背景异常。我一个个讲。中文乱码或显示成方框绝大多数情况是字体问题——GDI绘制时找不到你指定的字体或者该字体的字符集不完整。解决方案其实很简单用FontFamily检查字体是否存在再决定是否回退。比如private static Font GetChineseFont(float size, FontStyle style) { string[] preferredFonts { 微软雅黑, SimHei, SimSun, Arial }; foreach (string fontName in preferredFonts) { if (FontFamily.Families.Any(f f.Name fontName)) { return new Font(fontName, size, style, GraphicsUnit.Pixel); } } return new Font(Arial, size, style, GraphicsUnit.Pixel); }这段代码在Windows Server上特别有用——很多精简版服务器系统没有微软雅黑只有宋体和新宋体。如果写死“微软雅黑”而系统里不存在DrawString不会报错但画出来的文字全是方框。通过检测字体存在性做回退至少能保证文字不变成乱码。还有一点很多人不知道PrivateFontCollection可以加载项目目录下的字体文件比如pm.ttf这样程序发布后不受系统字体安装情况影响。我在做客户端工具时几乎必用这个技巧把用到的字体文件放资源目录初始化时加载确保每台机器显示效果一致。字体安装到系统后还需要注意字体文件本身是否完整。中文字体文件动辄十几MB有些精简版字库只保留常用汉字遇到生僻人名、特殊符号直接画成空白。我之前做一个报名系统用户姓名里有个“龑”字画出来就是空白排查了半天才发现问题出在字库上。解决办法是给用户提供姓名用字的字体校验流程或者干脆换用全字库字体如思源黑体。第二个高频坑是模糊。文字画出来发虚、发朦胧通常有三种原因。第一种是Graphics的TextRenderingHint设置不正确GDI默认是SystemDefault在部分环境会走系统字体平滑逻辑导致边缘发虚设置为AntiAliasGridFit会比较稳。第二种是Bitmap本身的分辨率偏低比如从网上下载的缩略图只有72DPI画上去的文字再清晰也受到像素总量限制。第三种是保存时压缩格式导致的画质下降尤其是保存为JPEG时如果设置的Quality参数比较低比如50文字边缘必然糊建议保存文字为主的图片时使用PNG格式。第三个坑是保存格式造成的背景异常。如果你从Bitmap.FromFile加载一张带透明通道的PNG然后直接Save为JPG透明区域会变成黑色。这不是代码逻辑错误而是JPEG格式本身不支持透明通道保存时系统默认用黑色填充。解决办法是先把透明区域填充为白色或者你的业务背景色再保存using (Bitmap bmp32 new Bitmap(canvas.Width, canvas.Height, PixelFormat.Format32bppArgb)) { using (Graphics g Graphics.FromImage(bmp32)) { g.Clear(Color.White); g.DrawImage(canvas, 0, 0); // 后续绘制和保存 } }如果你的图片本身是JPEG没有透明通道那直接Save就行这个坑不存在。还有一个和“清晰度”强相关但常被忽略的点高DPI屏幕下的坐标偏移。在Windows 10/11上如果程序没有声明DPI感知系统会对界面做缩放但GDI绘制的图片不受界面DPI感知影响所以画出来的图不会因为屏幕缩放而变模糊。不过如果是WinForm里实时预览绘图结果PictureBox控件的尺寸可能因为DPI缩放和图片实际像素不一致导致预览看着模糊但保存出来的文件其实很清晰。遇到这种情况先看保存的文件别急着调绘制代码。5. 进阶玩法居中排版、多行文字与文字旋转基础绘制会了之后有几个高频进阶需求——文字居中、多行自动换行、旋转文字比如斜的水印。这几个可以做得很优雅下面分别讲。5.1 居中排版用StringFormat而不是自己算坐标手动计算文字宽度再居中还要考虑字体度量容易出偏差。GDI里最稳的居中方式是配合RectF和StringFormat。RectangleF centerRect new RectangleF(0, 0, canvas.Width, canvas.Height); using (StringFormat format new StringFormat()) { format.Alignment StringAlignment.Center; // 水平居中 format.LineAlignment StringAlignment.Center; // 垂直居中 g.DrawString(居中文字, font, brush, centerRect, format); }这个方式比手动算坐标好用得多。注意StringAlignment.Center是“对象居中于矩形区域”不是“居中于图片坐标系原点”。你把矩形区域定义在图片的某个局部范围就可以实现“在这个区域内居中”灵活性很高。例如做一个头像生成器把用户名绘制在预设的矩形框中间用DrawString(text, font, brush, avatarRect, format)就能直接实现。调整avatarRect的X、Y、Width、Height就能控制文字显示在不同位置不用改任何别的逻辑。5.2 多行文字让System.Drawing自己去做换行多行文字最朴素的做法是写代码时自己拼\n然后设置StringFormat的FormatFlags。但GDI其实有原生支持DrawString在遇到\n时会换行如果启用StringFormatFlags.NoWrap则不会换行超出区域的部分被裁剪。更实用的是配合RectangleF让它自动换行——DrawString在绘制时会把文字约束在矩形区域内超过区域自动换行不需要手动处理。需要注意的是换行后行距由Font的行距决定GDI的行距通常略微小于字体设计值的120%。如果对行距有严格要求的场景比如做图文混排卡片建议用手动绘制方式先算每一行文字的高度和坐标逐行DrawString行距自己控制。这个更可控代价是代码量稍多。简洁的自动换行版RectangleF bodyRect new RectangleF(50, 200, 500, 300); StringFormat bodyFormat new StringFormat(); bodyFormat.Alignment StringAlignment.Near; bodyFormat.LineAlignment StringAlignment.Near; g.DrawString(longText, bodyFont, bodyBrush, bodyRect, bodyFormat);这个方式适合“内容长度可变但排版要求不高”的场景多出来的文字会超出矩形底部被裁掉或者根据具体设置自动缩小字体。我个人在画长文案时更常做字号自适应——先MeasureString估算文字尺寸如果超出区域就缩小字号通常试几次就能找到合适的字号。5.3 文字旋转改变Graphics的变换状态旋转文字最常见的是水印需求——把“内部资料”四个字斜着铺在整张图中间。方法是用Graphics.TranslateTransform和RotateTransformg.TranslateTransform(canvas.Width / 2f, canvas.Height / 2f); g.RotateTransform(-30f); using (StringFormat format new StringFormat()) { format.Alignment StringAlignment.Center; format.LineAlignment StringAlignment.Center; g.DrawString(内部资料, watermarkFont, watermarkBrush, 0, 0, format); } g.ResetTransform();注意这里的顺序先平移到图片中心点再旋转坐标系然后DrawString传0,0就是在旋转后的坐标系原点处绘制。整个操作结束后记得ResetTransform否则后续绘制都会带着这个位移和旋转角度很容易画出“飞”到图片外头的线条。还可以做多个水印平铺的效果配合循环把同一句文字多次RotateTransform并移动位置效果类似平铺印章。但要注意平铺次数多时GDI会逐条绘制性能上要控制好次数。5.4 更多可尝试的方向URL链接写入、图片验证码、带渐变色的文字写到这里C#在图片上绘制文字的核心知识就讲得差不多了。最后再分享一个方向把绘制逻辑封装成模板方法通过参数切换文字颜色、字体、位置、透明度为不同场景复用。我常常把常用的绘制风格做成一组扩展方法比如DrawTextWithShadow文字阴影、DrawTextWithOutline描边字、DrawWatermark斜水印。描边字是绘制两遍——先用深色画笔在大一号的坐标画一遍再用前景色在正常坐标画一遍看起来就有描边效果。阴影字也是一样先画偏移的灰色文字再画原位的白色文字。这些技巧组合起来别说是水印和验证码做一个简单的图片分享卡片生成器都不成问题。我自己最近就在做一个小工具读取Excel里的产品数据自动生成带产品名、价格、活动标签的分享海报整个项目从设计到跑通也就两个周末的事核心逻辑全是本文讲的这套东西。C#图片绘制文字说到底并没有太多玄机——理解Bitmap、Graphics、Font、Brush四者的关系掌握常见坐标布局方式再处理好字体和资源释放你就能应对绝大多数实际需求。遇到问题记得先检查字体是否存在、Graphics是否设置了抗锯齿、是否释放了资源、保存格式是否匹配场景这四件事能帮你解决九成的异常情况。希望这篇能帮你减少一些踩坑时间。如果遇到具体的绘制问题欢迎留言交流。本文还有配套的精品资源点击获取
返回列表