OpenCV C++中文显示难题:零依赖GDI+方案实现与避坑指南

📅 2026/7/17 4:46:07
OpenCV C++中文显示难题:零依赖GDI+方案实现与避坑指南
1. 项目概述为什么OpenCV C显示中文是个“坑”如果你用OpenCV C做过图像处理想在图片上叠加个中文水印或者给检测框标个中文标签大概率会一头撞上这个经典难题显示出来的中文全是“”或者一堆乱码方块。这几乎是每个国内C开发者使用OpenCV时必经的“新手村”考验。OpenCV作为一个强大的计算机视觉库其核心cv::putText函数在设计之初主要面向拉丁字母体系对中文、日文、韩文等复杂字符集的支持几乎是零。这背后的原因并不复杂OpenCV默认使用的字体渲染引擎非常简单它只处理单字节的ASCII字符对于需要多字节编码如GBK、UTF-8和复杂字形渲染的中文它就无能为力了。网上常见的解决方案十有八九会指向Freetype库。Freetype确实是个专业的字体渲染引擎能完美解决这个问题。但它的引入意味着你需要额外配置一个第三方库修改CMakeLists.txt处理跨平台的编译问题尤其在Windows上对于只是想快速加个中文标签的简单项目或者对于构建环境有严格限制的团队来说这无疑增加了不小的复杂度。有没有一种更轻量、更“原生”的方法不引入额外依赖就能让OpenCV显示出清晰的中文呢答案是肯定的。本文将带你深入一种被许多实战项目验证过的“零依赖”解决方案它直接利用Windows系统的GDI API或Linux/macOS系统的相关图形接口将文字先绘制到内存位图再转换成OpenCV的cv::Mat。我将从原理到代码从环境配置到避坑指南完整拆解这个过程让你不仅能“抄作业”更能理解背后的“所以然”。2. 核心原理绕开putText自己“画”文字要理解这个解决方案首先要放弃让cv::putText直接支持中文的幻想。我们的核心思路是“借力打力”利用操作系统原生、成熟的文字渲染能力生成一个包含中文文本的位图Bitmap然后将这个位图数据“喂”给OpenCV的cv::Mat对象。这个过程本质上是在OpenCV的图像流水线之外独立完成了一次文字光栅化Rasterization的工作。2.1 技术路线选型为什么是GDIWindows和Cairo/Pango跨平台对于Windows平台最自然的选择是GDI。它是Windows图形设备接口的增强版内置了高质量的TrueType字体渲染支持对中文的显示效果非常好且是系统自带组件无需额外安装。我们将使用GDI创建一个内存中的Bitmap对象在其上使用Graphics对象进行文字绘制最后通过内存拷贝将像素数据转换到cv::Mat。对于Linux或macOS平台常见的方案是使用Cairo2D图形库配合Pango文本布局和渲染库。这套组合是GTK等桌面环境的基础对多语言文本渲染的支持非常成熟。其工作流程与GDI类似创建Cairo图像表面Surface用Pango进行文本布局和渲染最后获取像素数据。注意本文将以Windows GDI的方案作为主要示例进行详解因为Windows是大多数C开发者的主要环境且GDI方案代码相对简洁依赖最少。跨平台的Cairo/Pango方案思路完全一致只是API不同文末会给出关键指引。2.2 核心流程拆解整个解决方案可以抽象为以下几个标准化步骤这个流程具有普适性创建离屏画布在内存中创建一个与目标图像尺寸、颜色通道数相匹配的位图对象如GDI的Bitmap。配置绘图上下文获取该位图的绘图上下文如GDI的Graphics并设置字体、颜色、抗锯齿等属性。绘制文本在绘图上下文上在指定位置绘制目标中文字符串。数据提取与转换锁定位图的数据缓冲区将其像素数据按正确的格式通常是BGR或BGRA读取出来。构建OpenCV矩阵利用读取到的像素数据创建或填充一个cv::Mat对象。合成图像将包含文字的cv::Mat可能带Alpha通道与原始背景图像进行叠加如使用cv::addWeighted或ROI操作。这个流程的关键在于数据格式的匹配。GDI的Bitmap默认像素格式可能是ARGBAlpha, Red, Green, Blue而OpenCV的cv::Mat默认使用BGR顺序。忽略这个差异会导致颜色完全错乱。3. WindowsGDI方案完整实现与代码精讲下面我们一步步实现一个纯C、仅依赖OpenCV和Windows SDK的实用类。我们将这个类命名为OpenCVTextRenderer。3.1 环境准备与项目配置首先确保你的开发环境已就绪编译器Visual Studio 2015或更高版本推荐VS2019/2022。OpenCV已正确安装并配置到你的项目中。可以通过vcpkg、源码编译或官方预编译包安装。确保在项目属性中包含了OpenCV的头文件目录和库文件目录并链接了opencv_worldxxx.lib或具体的opencv_core.lib,opencv_imgproc.lib等。Windows SDKGDI是Windows SDK的一部分现代Visual Studio默认包含。在你的Visual Studio C项目中需要进行以下关键配置在项目属性 - C/C - 预处理器 - 预处理器定义中添加_UNICODE和UNICODE宏定义以确保使用宽字符集。在项目属性 - 链接器 - 输入 - 附加依赖项中添加opencv_world455.lib根据你的OpenCV版本修改数字和gdiplus.lib。3.2 核心工具类OpenCVTextRenderer实现我们将创建一个头文件opencv_text_renderer.h和一个源文件opencv_text_renderer.cpp。opencv_text_renderer.h#pragma once #include opencv2/opencv.hpp #include Windows.h #include gdiplus.h #pragma comment(lib, gdiplus.lib) /** * brief 用于在OpenCV图像上渲染中文及其他GDI支持字体的轻量级工具类。 * details 该类封装了GDI的初始化、文本绘制和像素数据转换逻辑提供简单的API。 */ class OpenCVTextRenderer { public: /** * brief 构造函数初始化GDI。 * throws std::runtime_error 如果GDI初始化失败。 */ OpenCVTextRenderer(); /** * brief 析构函数释放GDI资源。 */ ~OpenCVTextRenderer(); /** * brief 在指定的OpenCV图像上绘制中文文本。 * * param image 输入/输出的图像CV_8UC3或CV_8UC4格式。 * param text 要绘制的UTF-8编码的中文文本。 * param position 文本左下角的起始位置以像素为单位。 * param fontName 字体名称如“微软雅黑”、“SimSun”。 * param fontSize 字体大小单位像素。 * param fontColor 字体颜色BGR格式与OpenCV一致。 * param backgroundColor 文本背景色BGR格式。如果alpha通道为0则绘制透明背景。 * return true 绘制成功false 绘制失败如图像格式不支持。 */ bool putText(cv::Mat image, const std::string text, const cv::Point position, const std::string fontName 微软雅黑, float fontSize 30, const cv::Scalar fontColor cv::Scalar(0, 0, 0), // BGR Black const cv::Scalar backgroundColor cv::Scalar(255, 255, 255)); // BGR White private: ULONG_PTR m_gdiplusToken; /// GDI令牌用于初始化和销毁。 Gdiplus::GdiplusStartupInput m_gdiplusStartupInput; /// GDI启动参数。 /** * brief 将UTF-8字符串转换为Windows平台所需的宽字符串UTF-16。 * param utf8Str UTF-8编码的字符串。 * return 转换后的std::wstring。 */ std::wstring Utf8ToWideString(const std::string utf8Str); /** * brief 将GDI的Color对象转换为OpenCV的ScalarBGR顺序。 */ cv::Scalar GdiColorToCvScalar(const Gdiplus::Color color); /** * brief 将OpenCV的ScalarBGR转换为GDI的Color对象ARGB。 * param cvScalar OpenCV BGR颜色。 * param alpha 透明度0-255。 * return GDI Color对象。 */ Gdiplus::Color CvScalarToGdiColor(const cv::Scalar cvScalar, BYTE alpha 255); };opencv_text_renderer.cpp#include opencv_text_renderer.h #include string #include stdexcept #include codecvt #include locale OpenCVTextRenderer::OpenCVTextRenderer() { // 初始化GDI Gdiplus::GdiplusStartupInput gdiplusStartupInput; Gdiplus::Status status Gdiplus::GdiplusStartup(m_gdiplusToken, gdiplusStartupInput, nullptr); if (status ! Gdiplus::Ok) { throw std::runtime_error(Failed to initialize GDI.); } } OpenCVTextRenderer::~OpenCVTextRenderer() { // 关闭GDI Gdiplus::GdiplusShutdown(m_gdiplusToken); } std::wstring OpenCVTextRenderer::Utf8ToWideString(const std::string utf8Str) { if (utf8Str.empty()) return std::wstring(); // 使用标准库转换C11及以上 std::wstring_convertstd::codecvt_utf8wchar_t converter; return converter.from_bytes(utf8Str); } cv::Scalar OpenCVTextRenderer::GdiColorToCvScalar(const Gdiplus::Color color) { // GDI Color是ARGBOpenCV Scalar是BGR return cv::Scalar(color.GetBlue(), color.GetGreen(), color.GetRed()); } Gdiplus::Color OpenCVTextRenderer::CvScalarToGdiColor(const cv::Scalar cvScalar, BYTE alpha) { // OpenCV Scalar是BGR转换为GDI的ARGB return Gdiplus::Color(alpha, static_castBYTE(cvScalar[2]), // R static_castBYTE(cvScalar[1]), // G static_castBYTE(cvScalar[0])); // B } bool OpenCVTextRenderer::putText(cv::Mat image, const std::string text, const cv::Point position, const std::string fontName, float fontSize, const cv::Scalar fontColor, const cv::Scalar backgroundColor) { // 1. 参数检查 if (image.empty() || text.empty()) { return false; } if (image.type() ! CV_8UC3 image.type() ! CV_8UC4) { // 仅支持3通道(BGR)或4通道(BGRA)图像 return false; } // 2. 将UTF-8文本和字体名转换为宽字符串 std::wstring wtext Utf8ToWideString(text); std::wstring wfontName Utf8ToWideString(fontName); // 3. 估算文本尺寸这是一个保守估算实际绘制可能略大 Gdiplus::FontFamily fontFamily(wfontName.c_str()); Gdiplus::Font font(fontFamily, fontSize, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel); Gdiplus::RectF layoutRect(0.0f, 0.0f, 10000.0f, 10000.0f); // 一个很大的初始矩形 Gdiplus::RectF boundingBox; Gdiplus::StringFormat stringFormat; stringFormat.SetAlignment(Gdiplus::StringAlignmentNear); Gdiplus::Graphics graphics(image.cols 0 ? image.cols : 1, image.rows 0 ? image.rows : 1); graphics.MeasureString(wtext.c_str(), -1, font, layoutRect, stringFormat, boundingBox); int textWidth static_castint(std::ceil(boundingBox.Width)); int textHeight static_castint(std::ceil(boundingBox.Height)); // 确保文本区域在图像范围内 if (position.x 0 || position.y 0 || position.x textWidth image.cols || position.y textHeight image.rows) { // 可以在此处选择裁剪或返回错误这里简单返回false return false; } // 4. 创建与文本区域大小一致的临时位图32位ARGB格式带Alpha通道 Gdiplus::Bitmap textBitmap(textWidth, textHeight, PixelFormat32bppARGB); Gdiplus::Graphics textGraphics(textBitmap); // 设置高质量渲染 textGraphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAliasGridFit); textGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); // 5. 清空位图背景透明或指定颜色 Gdiplus::Color bgColor; if (backgroundColor[3] 0) { // 假设第四个参数是alpha bgColor Gdiplus::Color(0, 0, 0, 0); // 完全透明 } else { bgColor CvScalarToGdiColor(backgroundColor, 255); } textGraphics.Clear(bgColor); // 6. 绘制文本 Gdiplus::SolidBrush textBrush(CvScalarToGdiColor(fontColor)); Gdiplus::PointF drawPoint(0.0f, 0.0f); // 在临时位图左上角开始绘制 textGraphics.DrawString(wtext.c_str(), -1, font, drawPoint, stringFormat, textBrush); // 7. 锁定位图数据并复制到OpenCV Mat Gdiplus::BitmapData bitmapData; Gdiplus::Rect rect(0, 0, textBitmap.GetWidth(), textBitmap.GetHeight()); if (textBitmap.LockBits(rect, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, bitmapData) Gdiplus::Ok) { // 创建一个临时的OpenCV Mat来存放ARGB数据 cv::Mat textMat(textHeight, textWidth, CV_8UC4, bitmapData.Scan0, bitmapData.Stride); // 根据目标图像类型进行混合 cv::Rect roi(position.x, position.y - textHeight, textWidth, textHeight); // 注意GDI绘制原点在左上角putText在左下角 if (roi.y 0) roi.y 0; // 简单边界处理 if (image.type() CV_8UC3) { // 目标为3通道BGR需要将4通道ARGB的Alpha通道混合掉 cv::Mat textBGR; cv::cvtColor(textMat, textBGR, cv::COLOR_BGRA2BGR); // 先忽略Alpha转为BGR textBGR.copyTo(image(roi)); // 直接覆盖无透明效果 } else if (image.type() CV_8UC4) { // 目标为4通道BGRA可以进行Alpha混合 // 这里为了简单直接拷贝Alpha通道更复杂的混合需要逐像素计算 textMat.copyTo(image(roi)); } textBitmap.UnlockBits(bitmapData); return true; } return false; }3.3 使用示例与效果验证创建一个简单的main.cpp来测试我们的工具类#include opencv2/opencv.hpp #include opencv_text_renderer.h int main() { try { // 1. 创建渲染器实例会自动初始化GDI OpenCVTextRenderer renderer; // 2. 创建一张测试图像白色背景 cv::Mat image(500, 800, CV_8UC3, cv::Scalar(255, 255, 255)); // 3. 绘制中文文本 bool success renderer.putText(image, 你好OpenCV这是中文测试。, cv::Point(50, 100), // 位置 微软雅黑, // 字体 40, // 大小 cv::Scalar(0, 0, 255), // 红色 (B,G,R) cv::Scalar(255, 255, 255)); // 白色背景 if (!success) { std::cerr Failed to put text. std::endl; return -1; } // 4. 再绘制一行使用不同字体和颜色 renderer.putText(image, 也可以使用宋体、黑体等。, cv::Point(50, 180), SimSun, // 宋体 30, cv::Scalar(0, 128, 0), // 绿色 cv::Scalar(200, 200, 255)); // 浅蓝色背景 // 5. 显示并保存结果 cv::imshow(OpenCV Chinese Text Demo, image); cv::imwrite(output_chinese.jpg, image); cv::waitKey(0); } catch (const std::exception e) { std::cerr Error: e.what() std::endl; return -1; } return 0; }编译并运行此程序你将看到一张800x500的白色图片上面清晰地显示着两行不同字体、不同颜色的中文效果与系统其他软件显示的中文完全一致。4. 关键细节、避坑指南与性能优化实现本身并不复杂但实际应用中会遇到很多细节问题。下面是我在多个项目中总结出的核心要点和避坑经验。4.1 字符编码从UTF-8到宽字符的“安全通道”这是第一个大坑。我们的C代码文件通常保存为UTF-8无BOM格式字符串字面量你好在内存中也是UTF-8编码。GDI的API如DrawString需要的是宽字符wchar_t*在Windows上即UTF-16。因此必须进行正确的编码转换。错误做法直接使用std::string.c_str()或简单的std::wstring构造。这会导致乱码因为编译器可能按系统默认编码如GBK解释字符串。正确做法使用C11的std::wstring_convert如示例代码所示或Windows APIMultiByteToWideChar并明确指定代码页为CP_UTF8。// 使用Windows API的替代方案 std::wstring Utf8ToWideString_WinAPI(const std::string utf8Str) { if (utf8Str.empty()) return L; int wideLen MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, nullptr, 0); if (wideLen 0) return L; std::wstring wideStr(wideLen, 0); MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, wideStr[0], wideLen); wideStr.pop_back(); // 移除末尾的null字符 return wideStr; }实操心得我强烈建议在项目全局使用UTF-8编码。在Visual Studio中确保源文件保存为“UTF-8 带签名”或“UTF-8 无签名”并在项目属性中设置“字符集”为“使用Unicode字符集”。这能最大程度避免编码混乱。4.2 颜色空间转换ARGB vs. BGR vs. BGRAGDI的Bitmap使用32位ARGB格式字节顺序通常是A, R, G, B但取决于系统而OpenCV的cv::Mat默认使用BGR顺序。如果你直接将Bitmap的缓冲区当作BGR图像读取会发现红色和蓝色通道互换。解决方案在从BitmapData复制数据时必须进行通道顺序的调整。我们的示例代码中CvScalarToGdiColor和GdiColorToCvScalar函数完成了这个转换。更关键的是在LockBits后我们创建了一个CV_8UC4的Mat指向ARGB数据然后使用cv::cvtColor(textMat, textBGR, cv::COLOR_BGRA2BGR)将其转换为OpenCV标准的BGR。注意这里的COLOR_BGRA2BGR常量名可能有点误导它实际上假设输入内存布局是B,G,R,A而GDI的是A,R,G,B。但在Windows上Bitmap的32位格式实际内存布局是BGRA小端序所以COLOR_BGRA2BGR正好适用。这是一个非常重要的平台细节验证方法绘制一个纯红色的文字(cv::Scalar(0,0,255))如果显示为蓝色说明通道顺序错了。4.3 文本尺寸测量与定位cv::putText的origin参数指定的是文本左下角的基线位置。而GDI的DrawString默认从左上角开始绘制。此外不同字体的实际占用的像素高度和宽度需要精确测量否则文本可能错位或被裁剪。MeasureString的局限性Graphics::MeasureString返回的矩形通常比实际绘制的文本区域稍小可能导致文本边缘被裁剪。一个更可靠的方法是使用Graphics::MeasureCharacterRanges或先绘制到一个足够大的临时位图再精确裁剪但这更复杂。我们的策略示例中使用了MeasureString并进行了向上取整(std::ceil)这是一个保守估计能保证文本不被裁剪但可能会在文本右侧和下方留下一点多余空间。对于大多数应用这已经足够。如果你需要像素级精确的定位和边界框就需要研究更精确的文本度量API如GetTextMetrics或GetGlyphOutline。4.4 透明背景与Alpha混合我们的示例代码支持了透明背景的选项通过backgroundColor的alpha分量。当目标图像是4通道BGRA时我们可以将带Alpha通道的文本位图直接复制过去。但当目标是3通道BGR时Alpha通道信息会丢失我们只能选择忽略透明度如示例中直接cvtColor后覆盖。实现真正的Alpha混合如果需要在3通道图像上实现半透明文字效果就需要手动进行Alpha混合计算。这涉及到对ROI区域内的每个像素根据文本位图对应像素的Alpha值与背景色进行加权合成。这会增加计算量但效果更好。伪代码如下for (int y 0; y textHeight; y) { for (int x 0; x textWidth; x) { cv::Vec4b textPixel textMat.atcv::Vec4b(y, x); cv::Vec3b bgPixel image.atcv::Vec3b(roi.y y, roi.x x); float alpha textPixel[3] / 255.0f; bgPixel[0] cv::saturate_castuchar(textPixel[0] * alpha bgPixel[0] * (1 - alpha)); // B bgPixel[1] cv::saturate_castuchar(textPixel[1] * alpha bgPixel[1] * (1 - alpha)); // G bgPixel[2] cv::saturate_castuchar(textPixel[2] * alpha bgPixel[2] * (1 - alpha)); // R } }4.5 性能考量与优化建议在循环中频繁创建OpenCVTextRenderer对象、Bitmap和Graphics对象并进行文本测量和绘制可能会成为性能瓶颈。对象复用将OpenCVTextRenderer设计为单例或全局对象避免重复初始化GDI。对于需要反复绘制相同文本如FPS计数器的场景可以考虑缓存渲染结果。即首次渲染某个文本特定内容、字体、大小、颜色时将生成的cv::Mat缓存起来下次直接使用。批量绘制如果需要绘制多行文本不要每行都调用一次putText。可以修改函数接受一个文本列表和位置列表在一个足够大的临时位图上一次性绘制所有文本最后再一次性合成到目标图像上。这能显著减少GDI对象创建和销毁的开销以及数据拷贝的次数。避免频繁的尺寸测量如果文本内容和样式不变只改变位置那么文本尺寸只需要计算一次。权衡方案选择对于性能极其敏感、且需要大量动态文本覆盖的应用如AR实时标注如果字体样式单一可以考虑预渲染字符集方案。即在初始化阶段将常用汉字或ASCII字符以位图形式渲染并保存到一张大图或纹理图集中使用时通过查表拼接。这完全避免了运行时的字体渲染开销是游戏开发中的常用技巧。5. 跨平台扩展思路与常见问题排查5.1 Linux/macOS 方案Cairo Pango在Linux上你可以使用Cairo和Pango库实现完全相同的功能。思路完全一致使用cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height)创建图像表面。创建Cairo上下文(cairo_create)和Pango布局(pango_cairo_create_layout)。使用pango_layout_set_text设置UTF-8文本并用pango_layout_set_font_description设置字体。调用pango_cairo_show_layout进行绘制。通过cairo_image_surface_get_data获取像素数据。注意Cairo的ARGB数据顺序可能是预乘Alpha并正确转换为OpenCV的BGR/BGRA。在macOS上除了CairoPango还可以使用原生的Core Text和Core Graphics框架其API设计更接近Windows的GDI性能也通常更优。5.2 常见问题排查表问题现象可能原因解决方案编译错误未找到Gdiplus.hWindows SDK未正确安装或包含路径未设置。确保使用Visual Studio Installer安装了对应版本的Windows SDK。链接错误未解析的外部符号没有链接gdiplus.lib。在项目属性 - 链接器 - 输入 - 附加依赖项中添加gdiplus.lib。运行时崩溃在GdiplusStartupGDI初始化失败可能因为资源冲突或重复初始化。确保GdiplusStartup和GdiplusShutdown成对调用且全局只初始化一次。中文显示为乱码或问号字符串编码转换错误。检查Utf8ToWideString函数是否正确将UTF-8转为UTF-16。确保源码文件是UTF-8编码。文字颜色错误红蓝互换颜色通道顺序未转换。检查CvScalarToGdiColor和GdiColorToCvScalar函数确认BGR和ARGB的转换正确。文字位置偏移或裁剪文本尺寸测量不准或坐标原点理解错误。确认MeasureString返回的尺寸并理解GDI绘制原点在左上角而putText在左下角。调整roi的y坐标计算position.y - textHeight。绘制透明背景时边缘有白边GDI Clear时颜色格式或Alpha值设置错误。确保bgColor的Alpha通道设置为0以实现透明。检查Bitmap创建时是否为PixelFormat32bppARGB。程序退出时内存泄漏报告GDI对象未正确释放。确保Bitmap::UnlockBits被调用并且GdiplusShutdown在程序退出前执行。使用RAII管理GDI资源。在多线程中调用崩溃GDI对象不是线程安全的。为每个线程创建独立的OpenCVTextRenderer实例或使用线程锁保护共享的GDI资源。5.3 一个更健壮的封装建议对于生产环境建议对上述类进行增强异常安全使用RAII管理GDI资源如Bitmap,Graphics确保在异常发生时也能正确释放。字体缓存缓存已创建的Gdiplus::Font对象避免重复创建。日志输出在关键步骤添加日志便于调试。提供更丰富的API如支持文本对齐左对齐、居中、右对齐、文本旋转、下划线/删除线等。6. 总结与最终建议经过以上拆解你会发现解决OpenCV C中文显示问题的核心并不在于寻找某个神秘的“开关”而在于理解计算机图形中文本渲染的基本原理它需要一个支持复杂字形和编码的渲染引擎。OpenCV自带的putText不具备这个能力所以我们“借用”了操作系统原生引擎Windows GDI的能力。这套“零依赖”方案的最大优势在于部署简单。你不需要说服团队引入一个新的第三方库不需要处理复杂的跨平台编译脚本尤其适合在已有的大型C项目中快速集成。它的缺点是对Windows平台的强依赖以及GDI本身在极端高性能场景下可能不是最优选。我个人在实际项目中的体会是对于90%的OpenCV C应用场景如算法原型验证、工具软件、离线处理系统这套GDI方案完全够用稳定且效果出色。在决定采用此方案前问自己两个问题1) 我的项目是否必须支持非Windows平台2) 我需要的文本渲染性能是否达到了每秒上千次、且要求极低延迟如果答案都是“否”那么这就是你的最佳选择。最后分享一个小技巧如果你发现渲染的文本在特定尺寸下有点“模糊”可以尝试调整Graphics的SetTextRenderingHint属性。TextRenderingHintAntiAliasGridFit在大多数情况下提供了清晰度和性能的良好平衡而TextRenderingHintClearTypeGridFit能利用Windows的ClearType技术提供更锐利的屏幕显示效果但可能不适合用于最终生成的图片。多试试不同的参数找到最适合你应用场景的那一个。