一、需求分析與方案設計
在開發工作中,我們經常需要將圖片轉換為不同尺寸的 icon 文件。無論是為網站製作 favicon.ico,還是為應用程式設計圖標,這都是一個常見的需求。市面上雖然有許多圖片轉 icon 的工具,但它們通常存在功能單一、廣告多或操作複雜等問題。
本文將居間如何使用 c#和 avalonia 開發一個簡單高效的圖片轉 icon 工具,實現以下功能:
- 支持將常見圖片格式(如 png、jpg 等)轉換為 ico 格式
- 支持生成多種尺寸的圖標(16x16、32x32、48x48、64x64、128x128、256x256)
- 提供兩種轉換模式:
- 合併模式:將多個尺寸的圖標合併到一個 ico 文件中
- 分離模式:為每個尺寸生成單獨的 ico 文件
- 支持拖拽操作,提升用戶體驗
二、核心轉換代碼
首先,我们来看核心的图片转 Icon 转换逻辑。这部分代码封装在ImageHelper类中:
using ImageMagick;
using System.IO;
using System.Threading.Tasks;
// ReSharper disable once CheckNamespace
namespace CodeWF.Tools;
public static class ImageHelper
{
public static async Task MergeGenerateIcon(string sourceImagePath, string destIconPath, uint[] sizes)
{
var baseImage = new MagickImage(sourceImagePath);
var collection = new MagickImageCollection();
foreach (var size in sizes)
{
var resizedImage = baseImage.Clone();
resizedImage.Resize(size, size);
collection.Add(resizedImage);
}
await collection.WriteAsync(destIconPath);
}
public static async Task SeparateGenerateIcon(string sourceImagePath, string destIconFolder, uint[] sizes)
{
var fileName = Path.GetFileNameWithoutExtension(sourceImagePath);
var baseImage = new MagickImage(sourceImagePath);
foreach (var size in sizes)
{
var resizedImage = baseImage.Clone();
resizedImage.Resize(size, size);
var savePath = Path.Combine(destIconFolder, $"{fileName}-{size}x{size}.ico");
await resizedImage.WriteAsync(savePath);
}
}
}
上面代碼使用了 nuget 包 magick.net-q16-anycpu。magick.net 是 imagemagick 的.net 封裝庫,提供了強大的圖像處理功能。q16 表示圖像處理時使用 16 位量化,anycpu 表示支持多種處理器架構。通過這個庫,我們可以輕鬆地調整圖片尺寸並保存為 ico 格式。
核心代碼提供了兩個主要方法:
MergeGenerateIcon:将一张源图片转换为包含多个尺寸的单个 ICO 文件SeparateGenerateIcon:将一张源图片转换为多个不同尺寸的 ICO 文件
三、用戶界面設計
1. 基礎界面布局
使用 Avalonia 框架设计用户界面,界面定义在ImageToIconView.axaml文件中:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:u="https://irihi.tech/ursa"
xmlns:i18n="https://codewf.com"
xmlns:vm="clr-namespace:CodeWF.Modules.Converter.ViewModels"
xmlns:language="clr-namespace:Localization"
xmlns:local="clr-namespace:CodeWF.Modules.Converter.Models"
prism:ViewModelLocator.AutoWireViewModel="True"
x:DataType="vm:ImageToIconViewModel"
x:CompileBindings="True"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CodeWF.Modules.Converter.ImageToIconView">
<StackPanel>
<TextBlock Text="{i18n:I18n {x:Static language:ImageToIconView.ChoiceSourceImageDescription}}" />
<StackPanel Orientation="Horizontal" Margin="0 10">
<TextBox VerticalAlignment="Center" Margin="10 0" Width="400" Classes="Small"
Text="{Binding NeedConvertImagePath}"
DragDrop.AllowDrop="True" DragDrop.Drop="RaiseDropSourceImagePath"/>
<Button Content="..." Classes="Small" Command="{Binding RaiseChoiceNeedConvertImageHandler}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{i18n:I18n {x:Static language:ImageToIconView.DestImageSize}}" />
<ItemsControl ItemsSource="{Binding IconSizes}" Margin="0 10">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}" Content="{Binding Content}"
VerticalAlignment="Center" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button Margin="10" Classes="Small"
Content="{i18n:I18n {x:Static language:ImageToIconView.MergeGenerateButtonContent}}"
Command="{Binding RaiseMergeGenerateIconHandler}" />
<Button Classes="Small"
Content="{i18n:I18n {x:Static language:ImageToIconView.SeparateGenerateButtonContent}}"
Command="{Binding RaiseSeparateGenerateIconHandler}" />
</StackPanel>
<TextBlock Margin="0 40 0 0" Classes="H4" Theme="{StaticResource TitleTextBlock}"
Text="{i18n:I18n {x:Static language:ImageToIconView.MemoTitle}}" />
<TextBlock Margin="0 5 0 3" Text="{i18n:I18n {x:Static language:ImageToIconView.MemoContent1}}" />
<TextBlock Text="{i18n:I18n {x:Static language:ImageToIconView.MemoContent2}}" />
<Border Margin="0,16" Classes="CodeBlock">
<SelectableTextBlock FontFamily="Consolas"
Text="<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />" />
</Border>
</StackPanel>
</UserControl>
對上面的代碼進行簡短描述,我們的界面主要包括以下幾個部分:
- 源圖片選擇區域(支持文本輸入和文件選擇)
- 目標圖標尺寸選擇區域(通過複選框選擇)
- 兩個操作按鈕(合併生成和分離生成)
- 備註信息區域(提供使用說明和 html 引用示例)
實現效果如下:

2. 拖拽功能實現
為了提升用戶體驗,我們支持兩種選擇源圖片的方式:
- 點擊"... "按鈕從文件選擇器選擇
- 直接將圖片文件拖拽到輸入框
在ImageToIconView.axaml.cs中实现拖拽处理:
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using CodeWF.Modules.Converter.ViewModels;
namespace CodeWF.Modules.Converter;
public partial class ImageToIconView : UserControl
{
public ImageToIconView()
{
InitializeComponent();
}
public void RaiseDropSourceImagePath(object? sender, DragEventArgs e)
{
if (this.DataContext is not ImageToIconViewModel vm)
{
return;
}
var files = e.Data.GetFiles();
var file = files?.FirstOrDefault();
if (file == null)
{
return;
}
vm.NeedConvertImagePath = file.TryGetLocalPath();
e.Handled = true;
}
}
通過以上代碼,實現了將文件拖拽到文本框時自動獲取文件路徑的功能:

四、視圖模型實現
在ImageToIconViewModel.cs中实现业务逻辑:
using Avalonia.Platform.Storage;
using AvaloniaXmlTranslator;
using CodeWF.Core.IServices;
using CodeWF.Modules.Converter.Models;
using CodeWF.Tools;
using CodeWF.Tools.FileExtensions;
using ReactiveUI;
using System.Collections.ObjectModel;
using Ursa.Controls;
namespace CodeWF.Modules.Converter.ViewModels;
public class ImageToIconViewModel : ReactiveObject
{
private readonly IFileChooserService _fileChooserService;
private readonly INotificationService _notificationService;
private readonly FilePickerFileType _icoFilePickerFileType =
new("Icon file") { Patterns = ["*.ico"] };
public ImageToIconViewModel(IFileChooserService fileChooserService, INotificationService notificationService)
{
_fileChooserService = fileChooserService;
_notificationService = notificationService;
IconSizes.AddRange(Enum.GetValues<IconSize>()
.Select(size => new IconSizeItem(size)));
}
#region Properties
public ObservableCollection<IconSizeItem> IconSizes { get; } = new();
private string? _needConvertImagePath;
public string? NeedConvertImagePath
{
get => _needConvertImagePath;
set => this.RaiseAndSetIfChanged(ref _needConvertImagePath, value);
}
#endregion
#region Command's handler
public async Task RaiseChoiceNeedConvertImageHandler()
{
var files = await _fileChooserService.OpenFileAsync(
I18nManager.Instance.GetResource(Localization.ImageToIconView.ChoiceSourceImageDescription)!,
true,
[FilePickerFileTypes.All]);
if (!(files?.Count > 0))
{
return;
}
NeedConvertImagePath = files[0];
}
public async Task RaiseMergeGenerateIconHandler()
{
(bool isSuccess, uint[]? sizes) = await GetGenerateInfo();
if (!isSuccess)
{
return;
}
var folder = Path.GetDirectoryName(NeedConvertImagePath);
var fileName = Path.GetFileNameWithoutExtension(NeedConvertImagePath);
var saveIconPath = Path.Combine(folder, $"{fileName}.ico");
try
{
await ImageHelper.MergeGenerateIcon(NeedConvertImagePath, saveIconPath, sizes);
}
catch (Exception ex)
{
await MessageBox.ShowOverlayAsync(ex.Message);
}
FileHelper.OpenFolderAndSelectFile(saveIconPath);
}
public async Task RaiseSeparateGenerateIconHandler()
{
(bool isSuccess, uint[]? sizes) = await GetGenerateInfo();
if (!isSuccess)
{
return;
}
var saveIconFolder = Path.GetDirectoryName(NeedConvertImagePath);
try
{
await ImageHelper.SeparateGenerateIcon(NeedConvertImagePath, saveIconFolder, sizes);
}
catch (Exception ex)
{
await MessageBox.ShowOverlayAsync(ex.Message);
}
FileHelper.OpenFolder(saveIconFolder);
}
private async Task<(bool IsSuccess, uint[]? Sizes)> GetGenerateInfo()
{
if (string.IsNullOrWhiteSpace(NeedConvertImagePath)
|| !File.Exists(NeedConvertImagePath))
{
await MessageBox.ShowOverlayAsync(
I18nManager.Instance.GetResource(Localization.ImageToIconView.ChoiceSourceImageDialogTitle)!);
return (false, null);
}
var selectedSize = IconSizes.Where(item => item.IsSelected).ToList();
if (selectedSize.Count <= 0)
{
await MessageBox.ShowOverlayAsync(
I18nManager.Instance.GetResource(Localization.ImageToIconView.DestImageSize)!);
return (false, null);
}
var destSizes = selectedSize.Select(size => (uint)(size.Size)).ToArray();
return (true, destSizes);
}
#endregion
}
視圖模型遵循 mvvm 設計模式,主要負責:
- 管理 ui 數據和狀態
- 處理用戶操作(選擇文件、執行轉換等)
- 驗證輸入數據
- 調用核心業務邏輯
- 處理異常情況
兩種轉換模式的效果如下:


五、數據模型設計
為了管理圖標尺寸選項,我們定義了以下數據模型:
using CodeWF.Tools.Extensions;
using ReactiveUI;
using System.ComponentModel;
namespace CodeWF.Modules.Converter.Models;
public enum IconSize
{
[Description("16x16")] Size16 = 16,
[Description("24x24")] Size24 = 24,
[Description("32x32")] Size32 = 32,
[Description("48x48")] Size48 = 48,
[Description("64x64")] Size64 = 64,
[Description("128x128")] Size128 = 128,
[Description("256x256")] Size256 = 256
}
public class IconSizeItem(IconSize size) : ReactiveObject
{
private bool _isSelected = true;
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
public string Content { get; set; } = size.GetDescription();
public IconSize Size { get; set; } = size;
}
六、在線icon轉換功能
除了桌面應用版本外,我還開發了一個基於blazor的在線icon轉換工具,方便用戶無需安裝軟體即可實現圖片到icon的轉換。
1. 在線轉換器特點
在線訪問地址:https://dotnet9.com/tool/ico
與桌面版相比,在線版本有以下特點:
- 無需安裝:直接通過瀏覽器訪問使用
- 跨平台兼容:支持任何現代瀏覽器,包括行動裝置
- 臨時文件存儲:轉換後的文件會在伺服器上臨時保存,用戶需要及時下載
- 簡化界面:針對網頁使用體驗優化,操作更加簡潔

2. 轉換流程
在線轉換工具的工作流程簡單直觀:
- 選擇一個圖片文件(支持png、jpg、jpeg、webp格式)
- 選擇需要轉換的圖標尺寸
- 選擇轉換模式(合併生成或分別生成)
- 點擊按鈕後,系統將圖片上傳至伺服器進行轉換
- 轉換完成後,點擊"下載"按鈕獲取生成的文件
在線版本同樣使用了magick.net進行圖像處理,核心轉換邏輯與桌面版相同,但增加了文件上傳處理、臨時存儲和清理等功能。有興趣深入了解具體實現的讀者,可以直接查看原始碼:
七、總結與應用場景
通過本文,我們實現了桌面版和在線版兩種圖片轉icon工具,滿足了不同用戶的需求。它們具有以下特點:
- 簡潔的用戶界面:操作直觀,支持拖拽操作
- 豐富的轉換選項:支持多種尺寸,滿足不同應用場景需求
- 靈活的轉換模式:可以生成單個多尺寸 ico 文件,也可以生成多個單尺寸 ico 文件
- 良好的代碼結構:採用 mvvm 設計模式,代碼清晰,易於維護和擴展
這個工具可以應用於以下場景:
- 網站開發中生成 favicon.ico
- 應用程式開發中生成應用圖標
- 設計師快速生成不同尺寸的圖標文件
此外,本項目還展示了如何在 c#應用中使用強大的圖像處理庫 magick.net,以及如何使用 avalonia 構建跨平台桌面應用,這些知識點都可以應用到其他類似的開發項目中。
希望本文對你有所幫助,如有問題歡迎在評論區留言討論!
源碼參考