圖片轉Icon工具開發實戰 - 從需求分析到程式碼實現

圖片轉Icon工具開發實戰 - 從需求分析到程式碼實現

本文介紹了如何使用C#和Avalonia開發一個圖片轉Icon的工具,包括需求分析、核心程式碼實現、UI設計和MVVM模式的應用。

最後更新 2025/3/10 上午6:14
沙漠尽头的狼
預計閱讀 10 分鐘
分類
.NET
標籤
.NET C# Avalonia UI MVVM UI設計

一、需求分析與方案設計

在開發工作中,我們經常需要將圖片轉換為不同尺寸的 Icon 檔案。無論是為網站製作 favicon.ico,還是為應用程式設計圖示,這都是一個常見的需求。市面上雖然有許多圖片轉 Icon 的工具,但它們通常存在功能單一、廣告多或操作複雜等問題。

本文將介紹如何使用 C# 和 Avalonia 開發一個簡單高效的圖片轉 Icon 工具,實現以下功能:

  1. 支援將常見圖片格式(如 PNG、JPG 等)轉換為 ICO 格式
  2. 支援生成多種尺寸的圖示(16x16、32x32、48x48、64x64、128x128、256x256)
  3. 提供兩種轉換模式:
    • 合併模式:將多個尺寸的圖示合併到一個 ICO 檔案中
    • 分離模式:為每個尺寸生成單獨的 ICO 檔案
  4. 支援拖放操作,提升使用者體驗

二、核心轉換程式碼

首先,我們來看核心的圖片轉 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="&lt;link rel=&quot;shortcut icon&quot; href=&quot;/favicon.ico&quot; type=&quot;image/x-icon&quot; /&gt;" />
        </Border>
    </StackPanel>
</UserControl>

對上面的程式碼進行簡短描述,我們的介面主要包括以下幾個部分:

  1. 來源圖片選擇區域(支援文字輸入和檔案選擇)
  2. 目標圖示尺寸選擇區域(透過核取方塊選擇)
  3. 兩個操作按鈕(合併生成和分離生成)
  4. 備註資訊區域(提供使用說明和 HTML 引用範例)

實現效果如下:

介面截圖

2. 拖放功能實現

為了提升使用者體驗,我們支援兩種選擇來源圖片的方式:

  1. 點擊「...」按鈕從檔案選擇器選擇
  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 設計模式,主要負責:

  1. 管理 UI 資料和狀態
  2. 處理使用者操作(選擇檔案、執行轉換等)
  3. 驗證輸入資料
  4. 呼叫核心業務邏輯
  5. 處理例外情況

兩種轉換模式的效果如下:

多尺寸合併轉換

轉換成多個尺寸

五、資料模型設計

為了管理圖示尺寸選項,我們定義了以下資料模型:

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

與桌面版相比,線上版本有以下特點:

  1. 無需安裝:直接透過瀏覽器訪問使用
  2. 跨平台相容:支援任何現代瀏覽器,包括行動裝置
  3. 臨時檔案儲存:轉換後的檔案會在伺服器上臨時儲存,使用者需要及時下載
  4. 簡化介面:針對網頁使用體驗最佳化,操作更加簡潔

線上轉換介面

2. 轉換流程

線上轉換工具的工作流程簡單直觀:

  1. 選擇一個圖片檔案(支援 PNG、JPG、JPEG、WEBP 格式)
  2. 選擇需要轉換的圖示尺寸
  3. 選擇轉換模式(合併生成或分別生成)
  4. 點擊按鈕後,系統將圖片上傳至伺服器進行轉換
  5. 轉換完成後,點擊「下載」按鈕獲取生成的檔案

線上版本同樣使用了 Magick.NET 進行影像處理,核心轉換邏輯與桌面版相同,但增加了檔案上傳處理、臨時儲存和清理等功能。有興趣深入了解具體實現的讀者,可以直接查看原始碼:

七、總結與應用場景

透過本文,我們實現了桌面版和線上版兩種圖片轉 Icon 工具,滿足了不同使用者的需求。它們具有以下特點:

  1. 簡潔的使用者介面:操作直觀,支援拖放操作
  2. 豐富的轉換選項:支援多種尺寸,滿足不同應用場景需求
  3. 靈活的轉換模式:可以生成單個多尺寸 ICO 檔案,也可以生成多個單尺寸 ICO 檔案
  4. 良好的程式碼結構:採用 MVVM 設計模式,程式碼清晰,易於維護和擴展

這個工具可以應用於以下場景:

  • 網站開發中生成 favicon.ico
  • 應用程式開發中生成應用圖示
  • 設計師快速生成不同尺寸的圖示檔案

此外,本專案還展示了如何在 C# 應用中使用強大的影像處理函式庫 Magick.NET,以及如何使用 Avalonia 構建跨平台桌面應用,這些知識點都可以應用到其他類似的開發專案中。

希望本文對你有所幫助,如有問題歡迎在評論區留言討論!

原始碼參考

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2025/3/10

挪車二維碼生成工具開發實戰

本文介紹如何開發一個挪車二維碼生成工具,包括C#和Avalonia實現的桌面版以及Blazor前端和.NET Web API實現的線上版,涵蓋需求分析、核心程式碼實作、UI設計和MVVM模式的應用。

繼續閱讀
同分類 / 同標籤 2025/2/25

.NET 10 Preview 1 發佈

今天 .NET 10 Preview 1 發佈了,我第一時間下載,升級了 Avalonia UI 專案和部落格網站,前者功能測試及 AOT 發佈正常,後者偵錯正常,Docker 暫時未成功

繼續閱讀