Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >图片转Icon工具开发实战 - 从需求分析到代码实现

图片转Icon工具开发实战 - 从需求分析到代码实现

作者头像
郑子铭
发布于 2025-04-15 06:54:50
发布于 2025-04-15 06:54:50
3700
代码可运行
举报
运行总次数:0
代码可运行

一、需求分析与方案设计

在开发工作中,我们经常需要将图片转换为不同尺寸的 Icon 文件。无论是为网站制作 favicon.ico,还是为应用程序设计图标,这都是一个常见的需求。市面上虽然有许多图片转 Icon 的工具,但它们通常存在功能单一、广告多或操作复杂等问题。

本文将介绍如何使用 C#和 Avalonia 开发一个简单高效的图片转 Icon 工具,实现以下功能:

  1. 支持将常见图片格式(如 PNG、JPG 等)转换为 ICO 格式
  2. 支持生成多种尺寸的图标(16x16、32x32、48x48、64x64、128x128、256x256)
  3. 提供两种转换模式:
    • 合并模式:将多个尺寸的图标合并到一个 ICO 文件中
    • 分离模式:为每个尺寸生成单独的 ICO 文件
  4. 支持拖拽操作,提升用户体验

二、核心转换代码

首先,我们来看核心的图片转 Icon 转换逻辑。这部分代码封装在ImageHelper类中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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文件中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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>

对上面的代码进行简短描述,我们的界面主要包括以下几个部分:

  1. 源图片选择区域(支持文本输入和文件选择)
  2. 目标图标尺寸选择区域(通过复选框选择)
  3. 两个操作按钮(合并生成和分离生成)
  4. 备注信息区域(提供使用说明和 HTML 引用示例)

实现效果如下:

界面截图

2. 拖拽功能实现

为了提升用户体验,我们支持两种选择源图片的方式:

  1. 点击"..."按钮从文件选择器选择
  2. 直接将图片文件拖拽到输入框

ImageToIconView.axaml.cs中实现拖拽处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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中实现业务逻辑:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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. 处理异常情况

两种转换模式的效果如下:

多尺寸合并转换

转换成多个尺寸

五、数据模型设计

为了管理图标尺寸选项,我们定义了以下数据模型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 工具,它具有以下特点:

  1. 简洁的用户界面:操作直观,支持拖拽操作
  2. 丰富的转换选项:支持多种尺寸,满足不同应用场景需求
  3. 灵活的转换模式:可以生成单个多尺寸 ICO 文件,也可以生成多个单尺寸 ICO 文件
  4. 良好的代码结构:采用 MVVM 设计模式,代码清晰,易于维护和扩展

这个工具可以应用于以下场景:

  • 网站开发中生成 favicon.ico
  • 应用程序开发中生成应用图标
  • 设计师快速生成不同尺寸的图标文件

此外,本项目还展示了如何在 C#应用中使用强大的图像处理库 Magick.NET,以及如何使用 Avalonia 构建跨平台桌面应用,这些知识点都可以应用到其他类似的开发项目中。

希望本文对你有所帮助,如有问题欢迎在评论区留言讨论!

源码参考

  • 码坊工具箱源码[1]
  • 转换工具包[2]

参考资料

[1]

码坊工具箱源码: https://github.com/dotnet9/CodeWF.Toolbox

[2]

转换工具包: https://github.com/dotnet9/CodeWF.Tools

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-04-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
HDU 4348 To the moon(可持久化线段树)
To the moon Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Total Submission(s): 4287    Accepted Submission(s): 923 Problem Description Background To The Moon is a independent game released in November 2011, it is
ShenduCC
2018/04/27
6970
HDU 4605 Magic Ball Game(可持续化线段树,树状数组,离散化)
Magic Ball Game Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Total Submission(s): 2489    Accepted Submission(s): 759 Problem Description When the magic ball game turns up, Kimi immediately falls in it. The inter
ShenduCC
2018/04/27
6680
HDU 5634 Rikka with Phi (线段树)
Problem Description Rikka and Yuta are interested in Phi function (which is known as Euler's totient function). Yuta gives Rikka an array  of positive integers, then Yuta makes  queries.   There are three types of queries:  Change  into ,
ShenduCC
2018/04/27
5770
给球上色(线段树+离散化) - HDU 1199
假定待离散化的序列为a[n],b[n]是序列a[n]的一个副本,则对应以上三步为:
ACM算法日常
2019/01/02
1.3K0
HDU 4417 Super Mario(线段树)
Super Mario Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 5370    Accepted Submission(s): 2461 Problem Description Mario is world-famous plumber. His “burly” figure and amazing jumping ability
ShenduCC
2018/04/27
6320
HDU 5877 2016大连网络赛 Weak Pair(树状数组,线段树,动态开点,启发式合并,可持久化线段树)
Weak Pair Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 262144/262144 K (Java/Others) Total Submission(s): 1468    Accepted Submission(s): 472 Problem Description You are given a  tree of  nodes, labeled from 1 to . To the th node a non-n
ShenduCC
2018/04/27
7210
HDU 3450 Counting Sequences(线段树)
Counting Sequences Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/65536 K (Java/Others) Total Submission(s): 2335    Accepted Submission(s): 820 Problem Description For a set of sequences of integers{a1,a2,a3,...an}, we define a sequence
ShenduCC
2018/04/27
5210
hdu 1394
求逆序数的时候,算出以每一个数为逆序数对的第二个数的情况之和即为序列的逆序数,这样能够防止反复
全栈程序员站长
2022/07/10
1850
[OI题库]八月提高模拟题解
具体地,将该点插入单调栈时,只会改变栈顶位置和插入点的值。记录原栈顶位置、插入点更改前的值,即可在回溯时撤销。
Clouder0
2022/09/23
2850
P2023 [AHOI2009]维护序列
老师交给小可可一个维护数列的任务,现在小可可希望你来帮他完成。 有长为N的数列,不妨设为a1,a2,…,aN 。有如下三种操作形式: (1)把数列中的一段数全部乘一个值; (2)把数列中的一段数全部加一个值; (3)询问数列中的一段数的和,由于答案可能很大,你只需输出这个数模P的值。 输入输出格式 输入格式: 第一行两个整数N和P( )。第二行含有N个非负整数,从左到右依次为a1,a2,…,aN, ( )。第三行有一个整数M,表示操作总数。从第四行开始每行描述一个操作,输入的操作有以下三种形式: 操作1
attack
2018/04/13
7610
BZOJ2752: [HAOI2012]高速公路(road)(线段树 期望)
Time Limit: 20 Sec  Memory Limit: 128 MB Submit: 1820  Solved: 736 [Submit][Status][Discuss] Description Y901高速公路是一条重要的交通纽带,政府部门建设初期的投入以及使用期间的养护费用都不低,因此政府在这条高速公路上设立了许多收费站。 Y901高速公路是一条由N-1段路以及N个收费站组成的东西向的链,我们按照由西向东的顺序将收费站依次编号为1~N,从收费站i行驶到i+1(或从i+1行驶到i)需要收取V
attack
2018/07/27
2630
CodeForces 19D Points (线段树+set)
D. Points time limit per test 2 seconds memory limit per test 256 megabytes input standard input output standard output Pete and Bob invented a new interesting game. Bob takes a sheet of paper and locates a Cartesian coordinate system on it as
ShenduCC
2018/04/27
6640
HDU 3333 Turing Tree (线段树)
Turing Tree Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 4768    Accepted Submission(s): 1686 Problem Description After inventing Turing Tree, 3xian always felt boring when solving problems ab
ShenduCC
2018/04/27
5410
[Atcoder][CF]简单题选做练习笔记 2
第一个式子,要求 p_i \equiv 0 \pmod 3,第二个式子要求 p_i \equiv 1 \pmod 3 且 p_j \equiv 2 \pmod 3 或者反过来。
Clouder0
2022/09/23
3890
BZOJ3083: 遥远的国度(树链剖分)
以下图片来自(https://blog.csdn.net/lcomyn/article/details/45718295)
attack
2018/07/27
3230
BZOJ3083: 遥远的国度(树链剖分)
HDU6315 Naive Operations(线段树 复杂度分析)
设\(d_i\)表示\(i\)号节点还需要加\(d_i\)次才能产生\(1\)的贡献
attack
2018/10/08
4670
HDU6315 Naive Operations(线段树 复杂度分析)
SDUT算法分析与设计机测
将分解问题看成,以某个数字开头的分解有多少种,比如12可以看成以2开头的式子有几个,以3开头的有几个,4开头的有几个,6开头的有几个… 其中以2开头的分解式为例,可以看成12 = 2 * 6,即分析6有多少种分解方式,就是有多少个2开头的式子。
Here_SDUT
2022/09/19
5070
洛谷P3722 [AH2017/HNOI2017]影魔(线段树 set spaly)
题意 题目链接 Sol 这题好毒瘤啊。。 首先要观察到几个性质: 将最小值旋转到根相当于把右子树变为祖先的左子树,然后将原来的根变为当前最小值 上述操作对深度的影响相当于右子树不变,其他的位置-1 然后就可以做了,把询问离线之后离散化一下,建一棵权值线段树表示每个值对应的深度 同时用set维护出已经加入的值 每次先找到后继,看一下有没有左孩子,如果有的话说明前驱一定没有右孩子。 注意随时更新信息 复杂度\(O(nlogn)\) #include<bits/stdc++.h> #define Pair pa
attack
2019/03/08
3530
NOI 系列真题练习笔记
NOIP 前开始做做真题,虽然每年都风格迥异,不过感受一下 OI 风格的题目还是有一定意义的。
Clouder0
2022/09/23
8860
Day1下午解题报告
预计分数:0+30+30=60 实际分数:0+30+40=70 T1水题(water) 贪心,按长度排序, 对于第一幅牌里面的,在第二个里面,找一个长度小于,高度最接近的牌 进行覆盖。 考场上的我离正解只差一个小于号之遥。。。。。。。 1 #include <stdio.h> 2 #include <string.h> 3 #include <algorithm> 4 #include <iostream> 5 #include <set> 6 using namespace std; 7 i
attack
2018/04/11
7750
推荐阅读
相关推荐
HDU 4348 To the moon(可持久化线段树)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验