1. 项目概述为什么选择C Builder处理图片水印在桌面应用开发领域尤其是需要处理大量本地文件、追求高效执行效率的场景下C Builder一直是一个被低估的利器。它结合了C的高性能与RAD快速应用开发框架的便捷性特别适合开发图形界面工具。今天要聊的“图片透明水印添加”就是一个典型的应用场景。你可能用过很多在线工具或者Photoshop但当需要批量处理成百上千张产品图、内部资料图或者需要将水印功能集成到自己的管理软件中时一个轻量、快速、无需联网且可定制的本地工具就显得尤为重要。C Builder的VCLVisual Component Library框架里对图像处理有不错的原生支持尤其是通过TBitmap、TPngImage等类我们可以直接操作像素数据。实现透明水印的核心说白了就是图像融合将一张作为水印的图片通常是带Alpha通道的PNG以一定的透明度叠加到另一张作为背景的目标图片的指定位置上。这个过程涉及到图像加载、像素遍历、颜色混合计算以及最终的图像保存。用C Builder来做不仅能保证处理速度特别是处理大图或批量时还能轻松做出一个带有文件拖放、进度显示、参数调节的友好GUI工具这是单纯写控制台程序或者依赖复杂第三方库所难以比拟的。2. 核心原理与设计思路拆解在动手写代码之前我们必须把“透明水印”这个需求背后的图像学原理和程序逻辑想清楚。这决定了我们代码的效率和最终效果的优劣。2.1 透明混合的数学基础Alpha混合所谓透明在计算机图形学里通常指Alpha混合。每张图片的每个像素点除了红(R)、绿(G)、蓝(B)三个颜色通道外还可以有一个Alpha(A)通道代表不透明度。A255表示完全不透明A0表示完全透明。当我们想把一张前景图水印叠加到背景图原图上时对于重叠区域的每个像素需要进行混合计算。最常用的混合公式是Alpha合成结果颜色 (前景颜色 * 前景Alpha / 255) (背景颜色 * (255 - 前景Alpha) / 255)这个公式是线性的。但实际操作中我们拿到的不一定是带Alpha通道的图片。更常见的情况是水印图是一张背景为纯色比如白色或黑色的图片我们希望将这个纯色背景视为“透明”。这就引出了第二种方案颜色键控Color Keying。即指定一种颜色如RGB(255,255,255)纯白作为透明色在混合时遇到这个颜色的像素就直接跳过不进行绘制。对于我们的项目更通用和专业的做法是支持带Alpha通道的PNG水印。这样水印边缘可以有平滑的半透明过渡如阴影、柔光效果更好。C Builder的TPngImage组件可以很好地支持PNG的Alpha通道。2.2 整体流程设计基于上述原理一个健壮的图片水印工具流程应该如下加载阶段分别加载目标图片和水印图片。需要支持常见格式JPG, PNG, BMP。特别注意水印图片的格式优先使用PNG。预处理阶段检查水印图片尺寸。如果水印比原图还大需要提供缩放选项等比例缩放至合适大小或强制拉伸。根据用户选择的位置如左上、右下、居中、平铺等计算水印图片在原图上的起始绘制坐标。核心混合阶段遍历水印图片每一个像素或需要绘制的矩形区域根据其Alpha值按照混合公式与原图对应位置的像素进行颜色计算并将结果写回原图。输出阶段将处理后的内存图像保存为文件。这里要注意保存格式如果原图是JPG保存为JPG时会丢失Alpha信息但混合后的结果已经是位图所以没问题。如果需要保留透明背景则应保存为PNG。2.3 技术选型为什么是GDI和CanvasC Builder自带的TBitmap虽然可以直接操作像素但其对PNG Alpha通道的支持在旧版本中并不完善。更现代、更强大的选择是使用GDI。GDI是Windows系统自带的图形设备接口提供了更丰富的图像处理功能包括对PNG、TIFF等格式的完美支持以及内置的图像绘制、缩放、旋转和Alpha混合功能。在C Builder中我们可以通过引入Gdiplus.h头文件和链接gdiplus.lib库来使用GDI。它的核心类是Gdiplus::Graphics我们可以把它想象成一个画布Canvas而Gdiplus::Image类则用于加载图像文件。使用GDI实现水印叠加会异常简单用Gdiplus::Image加载原图和水印图。创建一个与原图同样尺寸的Gdiplus::Bitmap作为绘图表面。创建一个Gdiplus::Graphics对象关联到这个Bitmap。用Graphics.DrawImage将原图绘制到Bitmap上。设置Graphics的合成模式然后用Graphics.DrawImage将水印图绘制到指定位置。GDI会自动处理Alpha混合将最终的Bitmap保存到文件。这种方法避免了手动遍历像素和计算混合公式代码更简洁执行效率也高因为底层是高度优化的C和汇编代码。因此本项目将采用GDI作为核心图像处理引擎。3. 开发环境搭建与项目创建3.1 安装与配置C Builder这里以较新的C Builder 10.4 Sydney或更新版本为例。确保安装时勾选了相关组件。使用GDI需要配置项目新建一个VCL Forms Application。这是标准的带窗体的桌面应用程序。在项目设置中添加库路径和包含路径。通常GDI的头文件和库文件在Windows SDK中。对于现代C Builder更简单的方法是直接使用它自带的System.Win.Gdiplus单元对应Delphi的Vcl.Imaging.GIFImg,Vcl.Imaging.pngimage等可能需要但GDI是核心。 实际上对于C我们需要手动链接。在项目管理器Project Manager中右键点击你的项目选择“Options”。在“C Linker”的“Additional Options”里添加-lgdiplus在代码中包含头文件。在你的主窗体头文件如Unit1.h或预编译头文件stdafx.h如果有中添加#include windows.h #include gdiplus.h #pragma comment(lib, gdiplus.lib)注意gdiplus.h和gdiplus.lib通常位于Windows SDK目录下如C:\Program Files (x86)\Windows Kits\10\Include\...和Lib\...。现代C Builder的编译器可能已经自动包含了这些路径。3.2 设计主窗体界面一个友好的水印工具界面应该包含以下控件TButton用于选择原图文件夹、选择水印图片、开始处理。TImage用于预览原图和水印图。TComboBox或TRadioGroup用于选择水印位置左上、右上、左下、右下、居中、平铺、拉伸。TTrackBar或TEdit用于调节透明度0-100%。TCheckBox选项如“平铺水印”、“缩放水印至合适大小”、“覆盖原文件”或“输出到新目录”。TListBox或TListView显示待处理的文件列表。TProgressBar显示批量处理的进度。TSaveDialog/TOpenDialog文件对话框。将这些控件拖放到窗体上并给它们起一个有意义的名称如btnSelectSource、imgPreview、cmbPosition、tbTransparency等。4. 核心代码实现与分步解析接下来是重头戏我们将分模块实现核心功能。4.1 初始化与终止GDIGDI在使用前需要初始化程序退出前需要关闭。最佳位置是在主窗体的OnCreate和OnDestroy事件中。在Unit1.h的类定义中添加私有成员变量private: ULONG_PTR m_gdiplusToken; // GDI初始化令牌在Unit1.cpp的窗体构造函数TForm1::TForm1中添加初始化代码// 初始化GDI Gdiplus::GdiplusStartupInput gdiplusStartupInput; Gdiplus::GdiplusStartup(m_gdiplusToken, gdiplusStartupInput, NULL);在窗体的析构函数或OnDestroy事件处理程序中添加清理代码// 关闭GDI Gdiplus::GdiplusShutdown(m_gdiplusToken);4.2 加载与预览图片为“选择水印图片”按钮编写事件处理程序。这里使用TOpenDialog。void __fastcall TForm1::btnLoadWatermarkClick(TObject *Sender) { OpenDialog1-Filter 图片文件|*.png;*.jpg;*.jpeg;*.bmp; OpenDialog1-Title 选择水印图片; if (OpenDialog1-Execute()) { UnicodeString watermarkPath OpenDialog1-FileName; // 使用GDI加载图片到Image控件预览 std::unique_ptrGdiplus::Image pImage(Gdiplus::Image::FromFile(watermarkPath.c_str())); if (pImage pImage-GetLastStatus() Gdiplus::Ok) { // 将GDI Image转换为VCL的TBitmap用于预览简单演示非最优 Graphics::TBitmap* bmp new Graphics::TBitmap(); bmp-Width pImage-GetWidth(); bmp-Height pImage-GetHeight(); // 更完善的转换需要遍历像素这里为简化仅示意。 // 实际项目中可以考虑用TGPBitmap等封装类或直接用一个TImageList保存GDI Image。 imgWatermarkPreview-Picture-Assign(bmp); delete bmp; // 保存水印文件路径供后续使用 m_watermarkPath watermarkPath; } else { ShowMessage(加载水印图片失败); } } }注意上述将GDIImage直接转为TBitmap的代码是示意性的会丢失Alpha通道。仅用于简单预览。在实际混合操作中我们会直接使用GDI的Image对象。4.3 单张图片添加水印的函数这是最核心的函数。我们创建一个函数输入原图路径、水印图路径、位置、透明度输出处理后的内存图像或直接保存文件。bool AddWatermarkToImage(const UnicodeString sourcePath, const UnicodeString watermarkPath, WatermarkPosition pos, // 枚举类型表示位置 int transparency, // 0-100 const UnicodeString outputPath) { // 1. 加载原图和水印图 std::unique_ptrGdiplus::Image srcImage(Gdiplus::Image::FromFile(sourcePath.c_str())); std::unique_ptrGdiplus::Image wmImage(Gdiplus::Image::FromFile(watermarkPath.c_str())); if (!srcImage || srcImage-GetLastStatus() ! Gdiplus::Ok || !wmImage || wmImage-GetLastStatus() ! Gdiplus::Ok) { return false; } // 2. 创建一个和原图同样大小的位图作为画布 Gdiplus::Bitmap canvas(srcImage-GetWidth(), srcImage-GetHeight(), PixelFormat32bppARGB); Gdiplus::Graphics graphics(canvas); // 3. 将原图绘制到画布上 graphics.DrawImage(srcImage.get(), 0, 0, srcImage-GetWidth(), srcImage-GetHeight()); // 4. 计算水印绘制位置和尺寸 int wmWidth wmImage-GetWidth(); int wmHeight wmImage-GetHeight(); int srcWidth srcImage-GetWidth(); int srcHeight srcImage-GetHeight(); // 处理缩放选项假设有缩放逻辑这里以“适应”为例保持宽高比缩放至原图宽高的1/4 // float scale 0.25f; // wmWidth static_castint(wmWidth * scale); // wmHeight static_castint(wmHeight * scale); int x 0, y 0; switch (pos) { case wpTopLeft: break; // (0,0) case wpTopRight: x srcWidth - wmWidth; break; case wpBottomLeft: y srcHeight - wmHeight; break; case wpBottomRight: x srcWidth - wmWidth; y srcHeight - wmHeight; break; case wpCenter: x (srcWidth - wmWidth) / 2; y (srcHeight - wmHeight) / 2; break; // 平铺和拉伸的情况需要更复杂的逻辑这里省略 } // 5. 设置透明度Alpha混合参数 // GDI中透明度通过ColorMatrix或ImageAttributes设置更灵活。 // 这里使用一个简单的办法创建一个带有透明度参数的ImageAttributes Gdiplus::ImageAttributes imgAttr; // 透明度从0-100转换为0.0f-1.0f的Alpha乘数 float alpha static_castfloat(transparency) / 100.0f; // 创建一个颜色矩阵只修改Alpha分量 Gdiplus::ColorMatrix clrMatrix { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, alpha, 0.0f, // 这一行第4列控制Alpha 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; imgAttr.SetColorMatrix(clrMatrix, Gdiplus::ColorMatrixFlagsDefault, Gdiplus::ColorAdjustTypeBitmap); // 6. 绘制水印使用ImageAttributes应用透明度 Gdiplus::Rect destRect(x, y, wmWidth, wmHeight); graphics.DrawImage(wmImage.get(), destRect, 0, 0, wmImage-GetWidth(), wmImage-GetHeight(), Gdiplus::UnitPixel, imgAttr); // 7. 保存结果 // 根据输出文件扩展名选择编码器 CLSID encoderClsid; UnicodeString ext ExtractFileExt(outputPath).LowerCase(); if (ext .png) GetEncoderClsid(Limage/png, encoderClsid); else if (ext .jpg || ext .jpeg) GetEncoderClsid(Limage/jpeg, encoderClsid); else if (ext .bmp) GetEncoderClsid(Limage/bmp, encoderClsid); else return false; // 不支持的格式 Gdiplus::Status status canvas.Save(outputPath.c_str(), encoderClsid, NULL); return (status Gdiplus::Ok); }上面代码中用到的GetEncoderClsid函数需要自己实现用于根据MIME类型获取GDI编码器的CLSID。这是一个通用辅助函数int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) { UINT num 0; UINT size 0; Gdiplus::GetImageEncodersSize(num, size); if (size 0) return -1; Gdiplus::ImageCodecInfo* pImageCodecInfo (Gdiplus::ImageCodecInfo*)(malloc(size)); if (pImageCodecInfo NULL) return -1; Gdiplus::GetImageEncoders(num, size, pImageCodecInfo); for (UINT j 0; j num; j) { if (wcscmp(pImageCodecInfo[j].MimeType, format) 0) { *pClsid pImageCodecInfo[j].Clsid; free(pImageCodecInfo); return j; } } free(pImageCodecInfo); return -1; }4.4 批量处理与进度反馈批量处理的核心是遍历文件夹对每个支持的图片文件调用上面的AddWatermarkToImage函数。为了不阻塞UI我们应该在后台线程中进行批量处理。C Builder可以使用TThread类。创建后台线程类。在项目中新建一个单元File - New - Unit定义一个从TThread派生的类例如TWatermarkThread。在线程的Execute方法中执行批量任务。通过Synchronize或Queue方法安全地更新UI上的进度条。在主窗体中启动和监控线程。处理开始按钮的点击事件创建并启动线程。在线程的OnTerminate事件中处理完成后的清理工作如启用按钮、显示完成提示。由于篇幅限制这里不展开完整的线程代码但结构如下// WatermarkThread.h class TWatermarkThread : public TThread { private: TStringList* m_fileList; UnicodeString m_watermarkPath; // ... 其他参数 (位置透明度等) void __fastcall UpdateProgress(); protected: virtual void __fastcall Execute(); public: __fastcall TWatermarkThread(bool CreateSuspended, TStringList* fileList, const UnicodeString watermarkPath, // ... 其他参数 ); };// WatermarkThread.cpp void __fastcall TWatermarkThread::Execute() { for (int i 0; i m_fileList-Count !Terminated; i) { // 处理单张图片 bool success AddWatermarkToImage(...); // 更新进度 Synchronize(UpdateProgress); } }在主窗体中void __fastcall TForm1::btnBatchProcessClick(TObject *Sender) { // 禁用按钮防止重复点击 btnBatchProcess-Enabled false; // 创建并启动线程 m_watermarkThread new TWatermarkThread(false, m_selectedFiles, // 文件列表 m_watermarkPath, // ... 传递其他参数 ); m_watermarkThread-OnTerminate OnWatermarkThreadTerminated; // m_watermarkThread-Resume(); // 如果创建时挂起 } void __fastcall TForm1::OnWatermarkThreadTerminated(TObject *Sender) { // 处理完成启用按钮提示用户 btnBatchProcess-Enabled true; ShowMessage(批量处理完成); delete m_watermarkThread; m_watermarkThread nullptr; }5. 进阶功能与性能优化基础功能实现后我们可以考虑一些增强功能和优化点让工具更专业、更好用。5.1 支持平铺与拉伸水印在位置枚举中增加wpTile平铺和wpStretch拉伸。在核心绘制函数中需要特殊处理平铺使用双重循环在X和Y方向上重复绘制水印图片直到覆盖整个原图背景。case wpTile: for (int tileY 0; tileY srcHeight; tileY wmHeight) { for (int tileX 0; tileX srcWidth; tileX wmWidth) { Gdiplus::Rect tileRect(tileX, tileY, wmWidth, wmHeight); graphics.DrawImage(wmImage.get(), tileRect, ...); } } break;拉伸将水印图片的绘制目标矩形设置为整个原图大小。case wpStretch: destRect Gdiplus::Rect(0, 0, srcWidth, srcHeight); break;5.2 水印缩放与旋转用户可能希望调整水印大小或角度。这可以通过在绘制前对水印图像进行变换来实现。GDI的Graphics对象支持旋转和缩放。// 在绘制水印前保存当前绘图状态 Gdiplus::GraphicsState savedState graphics.Save(); // 平移坐标系到水印中心点用于旋转 graphics.TranslateTransform(x wmWidth/2.0f, y wmHeight/2.0f); // 旋转例如30度 graphics.RotateTransform(30.0f); // 缩放例如0.5倍 graphics.ScaleTransform(0.5f, 0.5f); // 绘制水印此时绘制原点在(0,0)即水印中心 Gdiplus::Rect rotatedRect(-wmWidth/2, -wmHeight/2, wmWidth, wmHeight); graphics.DrawImage(wmImage.get(), rotatedRect, ...); // 恢复绘图状态 graphics.Restore(savedState);5.3 性能优化要点避免在循环中重复加载水印图片在批量处理前将水印图片一次性加载到内存Gdiplus::Bitmap并在所有图片处理中复用。使用内存流处理网络或内存中的图片如果图片源不是文件可以使用IStream接口与GDI的Image类结合。图片编码器参数设置保存JPG时可以设置编码质量参数。Gdiplus::EncoderParameters encoderParams; encoderParams.Count 1; encoderParams.Parameter[0].Guid Gdiplus::EncoderQuality; encoderParams.Parameter[0].Type Gdiplus::EncoderParameterValueTypeLong; encoderParams.Parameter[0].NumberOfValues 1; ULONG quality 95; // 质量 1-100 encoderParams.Parameter[0].Value quality; canvas.Save(outputPath.c_str(), encoderClsid, encoderParams);多线程处理对于超大批量图片可以创建多个工作线程将文件列表分片并行处理充分利用多核CPU。但要注意线程安全和文件写入冲突每个线程应输出到独立文件或使用锁。6. 常见问题与调试技巧实录在实际开发和使用过程中你肯定会遇到一些“坑”。以下是我总结的几个典型问题及其解决方案。6.1 水印图片背景不透明出现白色/黑色边框问题描述使用了一张背景为纯色的PNG或JPG作为水印混合后水印的矩形边框清晰可见。原因分析代码使用了简单的DrawImage没有处理水印图片自身的透明信息。JPG格式没有透明通道。即便是PNG如果保存时背景不是透明的它也不透明。解决方案确保水印源文件是背景透明的PNG。使用Photoshop、GIMP等工具制作时删除背景层。使用颜色键控。如果水印背景是纯色可以在绘制前设置ImageAttributes的SetColorKey方法将该颜色设为透明。Gdiplus::ImageAttributes imgAttr; // 将纯白色(RGB255,255,255)设为透明 imgAttr.SetColorKey(Gdiplus::Color(255,255,255), Gdiplus::Color(255,255,255));使用带Alpha通道的PNG并采用本文4.3节中设置ColorMatrix的方法这是最规范的做法。6.2 处理后的图片颜色变深或变浅问题描述添加水印后整张图片的色调看起来不对劲。原因分析颜色通道处理错误。可能是混合公式用错或者在使用GDI时原图和水印图的像素格式如24位RGB, 32位ARGB不匹配导致颜色信息错位。解决方案确保用于最终绘制的Bitmap画布的像素格式是PixelFormat32bppARGB以支持Alpha通道。检查ColorMatrix设置是否正确。确保只修改了Alpha行第4行其他行保持单位矩阵。如果手动操作像素务必确认RGBA分量的顺序。GDI中内存布局通常是BGRA蓝、绿、红、Alpha而不是常见的RGBA。6.3 批量处理大图片时程序卡顿或无响应问题描述处理大量高分辨率图片时界面“卡死”进度条不动。原因分析所有耗时操作都在主UI线程中执行阻塞了消息循环。解决方案必须使用后台线程。如4.4节所述将批量处理逻辑放在TThread中。**在后台线程中适时调用TThread::Synchronize或TThread::Queue来更新UI控件如进度条、状态文本。切勿在后台线程中直接访问VCL控件。考虑分块处理。对于单张极大的图片甚至可以将其分块在子线程中处理不同区域但这复杂度较高一般GDI的DrawImage效率已经足够。6.4 保存为JPG后图片质量下降严重问题描述原图是高清JPG添加水印保存后图片变得模糊或有明显压缩痕迹。原因分析JPG是一种有损压缩格式。保存时如果没有指定质量参数GDI会使用默认质量通常较低如75。解决方案在调用Save方法时传入EncoderParameters并设置EncoderQuality如5.3节所示。将质量值设为90或95可以在文件大小和视觉质量间取得较好平衡。6.5 程序在部分电脑上无法运行提示找不到gdiplus.dll问题描述在自己电脑上编译的程序拷贝到其他电脑运行时报错。原因分析目标计算机可能没有安装相应版本的GDI运行库。GDI虽然是系统组件但在一些精简版系统或非常旧的系统上可能缺失。解决方案静态链接GDI比较困难因为GDI库通常动态链接。分发运行时库将gdiplus.dll随程序一起分发并放在程序同级目录。但要注意版权和系统版本兼容性。添加运行时检测在程序启动时尝试动态加载gdiplus.dll如果失败则给用户友好提示并引导其安装必要的系统更新或Visual C Redistributable某些版本SDK会包含。这是最稳妥的做法。HMODULE hGdiPlus LoadLibrary(Lgdiplus.dll); if (!hGdiPlus) { ShowMessage(运行此程序需要GDI支持请确保系统已安装相关组件。); return; } // ... 之后再用GdiplusStartup7. 项目扩展与实用技巧掌握了核心功能后你可以根据实际需求将这个工具扩展得更加强大。7.1 添加文字水印功能除了图片水印文字水印也非常常见。使用GDI的Gdiplus::Font和Gdiplus::StringFormat类可以轻松实现。Gdiplus::Font font(LArial, 24, Gdiplus::FontStyleBold, Gdiplus::UnitPixel); Gdiplus::SolidBrush brush(Gdiplus::Color(128, 255, 255, 255)); // 半透明白色 Gdiplus::StringFormat format; format.SetAlignment(Gdiplus::StringAlignmentCenter); // 水平居中 // 计算文字区域如果需要背景框可以画一个矩形 graphics.DrawString(L我的水印, -1, font, Gdiplus::PointF(x, y), format, brush);可以结合GraphicsPath为文字添加轮廓或者使用TextureBrush用图片填充文字实现更炫酷的效果。7.2 实现拖拽文件与文件夹功能提升用户体验允许用户直接将图片或文件夹拖拽到程序窗口。在窗体上放置一个TPanel作为接收区域并设置其DragMode和OnDragOver、OnDragDrop事件。void __fastcall TForm1::PanelDropAreaDragOver(TObject *Sender, TObject *Source, int X, int Y, TDragState State, bool Accept) { // 检查拖拽的数据中是否有文件 if (dynamic_castTDragObjectEx*(Source)-GetFileList()) Accept true; } void __fastcall TForm1::PanelDropAreaDragDrop(TObject *Sender, TObject *Source, int X, int Y) { TDragObjectEx* dragObj dynamic_castTDragObjectEx*(Source); if (dragObj dragObj-GetFileList()) { TStrings* droppedFiles dragObj-GetFileList(); // 将文件路径添加到你的处理列表中 for (int i 0; i droppedFiles-Count; i) { AddFileToList(droppedFiles-Strings[i]); } } }7.3 配置文件与预设保存用户可能经常使用同一组设置如水印图片、位置、透明度。可以将这些设置保存到INI文件或注册表中。// 保存设置 TIniFile* ini new TIniFile(ChangeFileExt(Application-ExeName, .ini)); ini-WriteString(Settings, WatermarkPath, m_watermarkPath); ini-WriteInteger(Settings, Position, cmbPosition-ItemIndex); ini-WriteInteger(Settings, Transparency, tbTransparency-Position); delete ini; // 加载设置 TIniFile* ini new TIniFile(ChangeFileExt(Application-ExeName, .ini)); m_watermarkPath ini-ReadString(Settings, WatermarkPath, ); cmbPosition-ItemIndex ini-ReadInteger(Settings, Position, 3); // 默认右下角 tbTransparency-Position ini-ReadInteger(Settings, Transparency, 50); delete ini;7.4 处理内存与资源泄漏这是一个C项目的永恒话题。务必确保所有new出来的Gdiplus::Image、Gdiplus::Bitmap等对象在不用时用delete释放。强烈建议使用std::unique_ptr等智能指针进行管理如示例代码所示。GDI对象Graphics,Font,Brush,Pen等在作用域结束后会自动释放但也要注意不要在循环中重复创建应尽量复用。在后台线程结束时确保所有动态分配的内存和资源都已正确释放。最后一个经验之谈在开发这类图像处理工具时务必准备一组测试图片包括不同格式JPG, PNG, BMP、不同尺寸小图标、4K大图、不同颜色深度24位、32位的图片。每实现一个功能或修复一个Bug后都用这组图片跑一遍确保没有引入回归问题。特别是边缘情况比如水印图比原图大、透明度为0或100、保存格式转换等这些地方最容易出问题。