
简介面向C# Winform开发者的常用窗体图标合集覆盖窗口图标、菜单项图标、按钮图标、对话框图标及状态栏图标等常见场景可帮助开发者在设计UI时快速选用风格统一的视觉元素避免自行绘制或零散收集图标的麻烦。压缩包共包含2000个文件约62.45MB以PNG、ICO、GIF等图标格式为主并包含少量BMP、CSS、XML、TXT等辅助文件基本满足不同分辨率与使用场景的需求。图标内容涵盖保存、打开、关闭、最小化/最大化、警告、错误等高频功能图形也适用于工具栏和右键快捷菜单等控件。目前已有3231人浏览学习这一图标集支持通过Properties.Resources直接引用、资源管理器集成或运行时动态加载等方式嵌入窗体、按钮、菜单和状态栏对于希望提升桌面应用专业感的开发者而言能显著缩短素材准备时间并保持整个应用的视觉一致性。1. 一个窗体图标为什么值得单开一个合集来管很多 C# WinForm 项目把窗体图标当成最后才做的事编译前随手丢一个 .ico 进去。我在维护上位机和内部管理系统时图标却是代码评审里被点最多的位置有的窗体标题栏正常任务栏图标模糊成一团托盘图标透明底变成黑块按钮上的小图标明显是从 32 像素硬拉出来的。问题不在设计师不给图而在“常用窗体图标”没有被当成工程资源来管理。我想讲的正是这件事C# WinForm 应用里窗体图标的来源、格式、内嵌方式、控件接法以及那些让人头疼的显示坑。适合刚入门 WinForm 的新手也适合想统一内部系统界面美化的团队。2. 把图标当成资源而不是图片格式、来源与内嵌方式2.1 常用窗体图标合集的格式选型.ico 和 .png 各有各的命一套可复用的 C# WinForm 窗体图标合集不是把各种图标丢进一个文件夹。真正能落地的合集应该是同一主题、多尺寸、同时提供 .ico 和 .png 两套格式的资源目录。很多人把“图标”和“ico”划等号实际在 WinForm 中只有 Form.Icon、NotifyIcon.Icon 强依赖 ico按钮、菜单、ToolStrip、ListView、TreeView 的图标都走 Image 对象用带透明通道的 png 更稳。这个区别直接决定你建合集时要做两套输出否则做到一半就会在控件上翻车。.ico 本质是一个容器可以把多张位图打包进一个文件系统读到后按目标尺寸挑选最接近的一帧。如果容器里只有 256x256 一帧Windows 在任务栏就不得不把它硬缩到 16x16结果就是边缘发灰、细线消失。反过来如果只有 16x16 一帧放大到 32 或 48 也一样糊。所以每个 .ico 文件至少要有 16、24、32、48 和 256 这五档。我一般还会把 64 也加进去AltTab 这类场景会取到更合适的帧。使用位置典型控件推荐尺寸首选格式透明要求窗体标题栏 / 任务栏Form.Icon16、24、32.ico 多帧必须支持 Alpha通知区域NotifyIcon16、20、24.ico 多帧必须支持 Alpha按钮 / 菜单 / 工具条Button / ToolStripItem16、24.png必须列表 / 树ListView / TreeView16、32、48.png必须程序图标资源管理器.exe 文件48、256.ico 多帧必须来源方面常见做法是到老牌图标站下载同一主题的 256x256 png再用工具导出 .ico。免费图标不是不能用但必须先确认透明通道没有被压坏也要注意授权能不能放进商业系统。我的习惯是先定一套配色主色、次色、禁用色再用同一套源图导出所有尺寸。东拼西凑的图标单独看都行放在同一个窗体上就觉得乱这也是很多 WinForm 界面美化看起来廉价的原因。如果你有 ImageMagick一行就够magick icon-256.png -define icon:auto-resize16,24,32,48,64,256 app.icoauto-resize会按 ico 规范把 png 生成多个尺寸帧Windows 需要 256 作为最大帧。若没有 magickGIMP 的 ico 导出插件也能选多尺寸。2.2 把图标装进 WinForm 工程Resources.resx 与嵌入资源两条路径把图标放到工程里最常见的做法是在项目属性的“资源”页面里添加现有文件Visual Studio 会把它写进 Resources.resx并生成一个强类型访问类。这样代码里可以直接写Properties.Resources.app_ico编辑器里也能预览。问题是 resx 文件在多人合作时容易产生无意义的冲突稍不注意 Designer.cs 就会重新生成对纯资源项目来说操作成本并不低。我一般会走嵌入资源这条路。做法是在项目下新建 Icons 目录把图标文件加进去然后把每个文件的 Build Action 设为 Embedded Resource。对于 SDK 风格的 csproj这样一段就够ItemGroup EmbeddedResource IncludeIcons\**\*.ico;Icons\**\*.png / /ItemGroup嵌入资源会随着程序集一起编译发布时不需要单独携带图标文件。资源名默认是“命名空间.目录.文件名”比如WindowsApp.Icons.app.ico。这个前缀是最大的坑所以我后面的加载代码用文件名后缀反查而不是硬写全名。当同一套图标要在多个 WinForm 项目里复用时我不建议各自复制一份。更稳的做法是单独建一个类库项目 IconLibrary把 Icons 目录和加载器都放进去其他项目引用这个类库。这样图标只维护一份换图标主题时只需重新编译 IconLibrary。这个模式会在最后一章给出完整结构。2.3 基础代码从嵌入资源加载 Icon 和 Image下面这个 IconLoader 是 WinForm 项目里通用的基础。它按文件名后缀匹配嵌入资源避免手写完整资源名using System; using System.Drawing; using System.IO; using System.Linq; using System.Reflection; public static class IconLoader { public static Icon Load(string fileName) { using var stream FindAndOpen(fileName); return new Icon(stream); // Icon 构造会同步读入数据流可安全关闭 } public static Image LoadImage(string fileName) { using var input FindAndOpen(fileName); var buffer new MemoryStream(); input.CopyTo(buffer); buffer.Position 0; return new Bitmap(buffer); // Bitmap 需要流存活转成内存副本再返回 } private static Stream FindAndOpen(string fileName) { var assembly Assembly.GetExecutingAssembly(); string fullName assembly.GetManifestResourceNames() .FirstOrDefault(n n.EndsWith(. fileName, StringComparison.OrdinalIgnoreCase)); if (fullName null) throw new FileNotFoundException(嵌入资源里没有找到: fileName); return assembly.GetManifestResourceStream(fullName) ?? throw new InvalidOperationException(资源流为空: fullName); } }FirstOrDefault(n n.EndsWith(. fileName, ...))加了个点是为了避免app.ico匹配到myapp.ico.bak之类的名字。默认命名空间改动后只要文件名不变资源名字不用同步改这是我踩过好几次坑后的习惯。Load用于窗体图标和托盘图标LoadImage用于按钮、菜单、ListView。注意Icon和Bitmap的流生命周期不同Icon构造时会把像素数据读走using没有问题Bitmap构造后仍可能引用流所以这里先复制到MemoryStream。如果你把Bitmap的using也写进去会出现“资源已关闭”之类的玄学报错。3. 从 Form.Icon 到 ListView 图像列表在 WinForm 里挂图标的正确姿势3.1 先做一个带缓存的 IconManager别让图标裸奔Icon和Image都实现了 IDisposable如果每个窗体构造时都从流里 new 一个图标窗体关闭后这些 GDI 句柄不一定立刻释放长时间运行就会出现句柄数只涨不降。所以我会在 IconLoader 上面再加一层带缓存的 IconManagerusing System; using System.Collections.Generic; using System.Drawing; public static class IconManager { private static readonly Dictionarystring, Icon IconCache new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionarystring, Image ImageCache new(StringComparer.OrdinalIgnoreCase); public static Icon GetIcon(string key) { if (IconCache.TryGetValue(key, out var icon)) return icon; icon IconLoader.Load(key); IconCache[key] icon; return icon; } public static Image GetImage(string key) { if (ImageCache.TryGetValue(key, out var image)) return image; image IconLoader.LoadImage(key); ImageCache[key] image; return image; } }这里的逻辑很简单第一次加载某个图标后放进静态字典之后所有窗体拿到的都是同一个实例。Key 用StringComparer.OrdinalIgnoreCase避免App.ico和app.ico被当成两个资源重复加载。图标合集一般就是几十个文件缓存不会膨胀但如果你在业务代码里动态生成大量图标就要考虑按模块清理缓存否则反而成了另一种泄漏。3.2 把图标挂到窗体、按钮、工具栏和托盘的代码写法实际用起来是一段很直白的代码private void ApplyIcons() { // 窗体标题栏、任务栏、AltTab 共用 Form.Icon this.Icon IconManager.GetIcon(app.ico); // 按钮Image 是 Image 类型取 png不拿 ico btnSave.Image IconManager.GetImage(save_24.png); btnDelete.Image IconManager.GetImage(delete_24.png); // 工具栏按钮 toolStripNew.Image IconManager.GetImage(new_24.png); // 托盘 notifyIcon1.Icon IconManager.GetIcon(tray.ico); notifyIcon1.Text 数据采集服务; }Button.Image 和 ToolStripItem.Image 需要 Image用IconManager.GetIcon(save.ico).ToBitmap()不是不行但Icon.ToBitmap()只会返回当前帧通常是 32x3216x16 的按钮会拿 32 的图硬压边缘全是锯齿。所以图标合集里必须有一份专门给控件用的 png尺寸按控件实际显示大小走。NotifyIcon 正好相反它接收Icon而且系统会向它请求小图标。如果tray.ico里只有 32x32 一帧托盘区域就可能显示成被裁剪过的样子把 16、20、24 三个尺寸都放进去高 DPI 下才不容易翻车。3.3 列表控件配图ListView 的 SmallImageList 和 LargeImageList 必须分开ListView 在 WinForm 里有五种视图Details 和 SmallIcon 用 SmallImageListLargeIcon 和 Tile 用 LargeImageListTreeView 则只有一个 ImageList通常用 16 或 20 的 png。很多项目直接用一个 ImageList 同时赋给两个属性大图标模式就会拿 16 的图硬拉界面一下子掉档次。private void InitImageLists() { var small new ImageList { ColorDepth ColorDepth.Depth32Bit, ImageSize new Size(16, 16) }; small.Images.Add(folder, IconManager.GetImage(folder_16.png)); small.Images.Add(file, IconManager.GetImage(file_16.png)); var large new ImageList { ColorDepth ColorDepth.Depth32Bit, ImageSize new Size(48, 48) }; large.Images.Add(folder, IconManager.GetImage(folder_48.png)); large.Images.Add(file, IconManager.GetImage(file_48.png)); listView1.SmallImageList small; listView1.LargeImageList large; }加入列表项时第二个参数是 ImageKeyvar item new ListViewItem(数据表.xlsx, file); listView1.Items.Add(item);这里有两个必须记住的细节。第一ImageSize必须在 Add 图片之前设好设置之后如果再改WinForms 会把已加的图片全部清掉这是控件属性里最容易被忽略的坑。第二SmallImageList 和 LargeImageList 不能共用同一个 ImageList 实例因为ImageSize是全局的16 和 48 只能二选一。TreeView 只有一个 ImageList一般接管small即可treeView1.ImageList small; treeView1.ImageKey folder;4. 窗体图标最常见的 4 个翻车现场现象、原因与排查4.1 图标透明底变成黑块问题多半出在转换工具或 GetHicon现象窗体图标或按钮小图标透明区域显示成黑色或灰白色块边缘还带锯齿。原因普通看图工具导出的 ico 不一定保留 32 位透明通道老式 ico 用 1 位掩码做透明边缘会沿着掩码锯齿。运行时如果贪省事用Bitmap.GetHicon()加Icon.FromHandle临时造 Icon透明通道也容易丢还会把 Bitmap 的生命周期和 GDI 句柄绑在一起。解决统一用多帧 ico并确认每帧都是 32bpp 带 Alpha。ImageMagick 的auto-resize默认写 32 位一般没问题若仍变黑用 GIMP 导出 ico 时勾选压缩选项并把色彩深度选为 32bpp。代码里不要用GetHicon()做透明图标直接从资源文件加载 .ico 是最不容易出错的路线。4.2 任务栏、标题栏、资源管理器显示不一致.ico 里缺了对应尺寸的帧现象同一个 exe 在资源管理器里显示的高清图标很好看编译后任务栏或标题栏却糊成一团AltTab 里的图标也不正常。原因.ico 里没有系统请求的 16 或 24 帧Windows 只能拿 48 或 256 的那一帧硬缩。缩小的过程中细线会消失透明区边缘会发灰。解决检查 .ico 实际包含的帧用 ImageMagick 一条命令就能看magick identify app.ico输出里应该列出 16x16、24x24、32x32、48x48、64x64、256x256。如果缺帧重新用-define icon:auto-resize16,24,32,48,64,256生成。还有一个 Windows 玄学替换 exe 图标后资源管理器可能还显示旧图标不是没生成而是 icon cache 没刷新换个 exe 文件名或重启资源管理器就能看到新效果。4.3 高 DPI 下图标发虚只给一档尺寸WinForms 不会自动换图现象系统缩放 125% 或 150% 时按钮图标、ListView 大图标明显发虚标题栏图标也像被拉伸过。原因ImageList 的ImageSize是以物理像素写死的WinForms 会缩放字体和布局但不会自动把 24 的 png 换成 32 的 png。如果程序没有声明 PerMonitorV2Windows 还会对整个窗口做位图拉伸图标自然跟着糊。解决先保证程序是 DPI aware.NET Core 3.0 以上在 Main 开头加Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);然后在窗体里处理 DPI 变化protected override void OnDpiChanged(DpiChangedEventArgs e) { base.OnDpiChanged(e); float scale e.DeviceDpiNew / 96f; btnSave.Image scale 1.5f ? IconManager.GetImage(save_32.png) : IconManager.GetImage(save_24.png); largeImages.ImageSize new Size((int)Math.Round(48 * scale), (int)Math.Round(48 * scale)); listView1.LargeImageList largeImages; }这里PickSaveIcon的逻辑就是“按缩放比例换一档图标”。如果图标合集里只有 24 一档高 DPI 下再怎么处理都只能拉伸所以一开始建合集时就要把 32、48 这些档位备齐。.NET Framework 4.7 以下没有完美的 PerMonitorV2最实际的办法是把资源图做大一倍靠双倍分辨率抵消缩放。4.4 句柄只涨不降最后图标全变空白没有统一管理 Icon 生命周期现象程序运行一整天任务管理器里 GDI 对象数持续上涨超过一万后标题栏和图标开始变空白。原因每次打开窗体都new Icon(stream)窗体关闭后旧 Icon 没有被及时释放替换 Button.Image 时旧 Image 也没 Dispose。更常见的是有人写了notifyIcon1.Icon Icon.FromHandle(bmp.GetHicon())这个 Icon 不拥有 HICON 句柄不调用DestroyIcon就永远是泄漏。解决用 IconManager 静态缓存之后所有窗体共享同一个 Icon 实例重复创建的问题自然消失。替换控件 Image 时如果是临时构造的 Image记得先 Dispose 旧对象如果是 IconManager 里缓存的 Image不要 Dispose它还被其他控件引用着。至于GetHicon那条路我在生产代码里一律不用除非你明确知道句柄由谁在什么时候销毁否则就是给自己挖坑。5. 值得复制的一招把图标合集做成独立类库用常量表分发当你有多个 WinForm 项目共用图标时最值得做的一件事是把图标收进独立类库并用常量类管理键名。这样既不会每个项目复制一份图标也不会在代码里散落save_24.png这种魔法字符串。类的写法很简单namespace IconLibrary { public static class IconKeys { public const string MainWindow app.ico; public const string Save save_24.png; public const string Delete delete_24.png; public const string FolderSmall folder_16.png; public const string FolderLarge folder_48.png; } }IconLoader 和 IconManager 也放进这个类库。WinForm 项目里引用后代码变成this.Icon IconManager.GetIcon(IconKeys.MainWindow); btnSave.Image IconManager.GetImage(IconKeys.Save);这样做的好处是换图标主题时只改 IconLibrary 一个项目重新编译后所有上层项目跟着更新新增图标时也只加一个常量评审时扫一眼就知道哪些资源被引用了。发布前我还会在 IconLibrary 里放一个资源自检方法防止嵌入资源漏加using System; using System.Linq; class IconChecker { static void Main() { var assembly typeof(IconKeys).Assembly; var names assembly.GetManifestResourceNames(); string[] keys { IconKeys.MainWindow, IconKeys.Save, IconKeys.Delete, IconKeys.FolderSmall, IconKeys.FolderLarge }; foreach (var key in keys) { bool ok names.Any(n n.EndsWith(. key, StringComparison.OrdinalIgnoreCase)); Console.WriteLine(ok ? OK key : 缺失 key); if (!ok) Environment.ExitCode 1; } } }自检脚本的EndsWith(. key)和加载器保持一致避免匹配到相似前缀。我用这套方案把三个独立项目的图标统一收进了同一个类库每次替换主题只需要重新编译一次。没有常量表的图标引用我一律在评审里打回。任务栏不再是黑匣子图标也终于不再是窗体里最随意的部分。希望帮到你。本文还有配套的精品资源点击获取