画像をアイコンに変換するツール開発実践 - 要件分析からコード実装まで

画像をアイコンに変換するツール開発実践 - 要件分析からコード実装まで

この記事では、C#とAvaloniaを使用して画像をアイコンに変換するツールの開発方法を紹介します。要件分析、コアコードの実装、UIデザイン、MVVMパターンの適用を含みます。

最終更新 2025/03/10 6:14
沙漠尽头的狼
読了目安 8 分
カテゴリ
.NET
タグ
.NET C# Avalonia UI MVVM UIデザイン

一、要件分析と設計

開発業務において、画像を異なるサイズのアイコンファイルに変換する必要はよくあります。ウェブサイト用のfavicon.icoの作成や、アプリケーション用のアイコン設計など、これは一般的な要件です。市販の画像→アイコン変換ツールは多数存在しますが、通常は機能が限定的であったり、広告が多かったり、操作が複雑であるなどの問題があります。

本記事では、C# と Avalonia を使用して、シンプルで効率的な画像→アイコン変換ツールを開発する方法を紹介し、以下の機能を実現します:

  1. 一般的な画像形式(PNG、JPG など)を ICO 形式に変換可能
  2. 複数のサイズのアイコン生成に対応(16x16、32x32、48x48、64x64、128x128、256x256)
  3. 2 つの変換モードを提供:
    • マージモード:複数サイズのアイコンを 1 つの ICO ファイルに結合
    • 分離モード:各サイズごとに個別の ICO ファイルを生成
  4. ドラッグ&ドロップ操作に対応し、ユーザーエクスペリエンスを向上

二、コア変換コード

まず、画像からアイコンへの変換ロジックの中核部分を見ていきます。このコードは 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 形式で保存できます。

コアコードでは 2 つの主要メソッドを提供します:

  • MergeGenerateIcon:1 つの元画像を複数サイズを含む単一の ICO ファイルに変換
  • SeparateGenerateIcon:1 つの元画像を複数の異なるサイズの ICO ファイルに変換

三、ユーザーインターフェース設計

1. 基本UIレイアウト

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. 2 つの操作ボタン(マージ生成と分離生成)
  4. メモ情報エリア(使用説明と HTML 参照例を提供)

実装イメージは以下の通りです:

インターフェーススクリーンショット

2. ドラッグ&ドロップ機能の実装

ユーザーエクスペリエンス向上のため、元画像の選択方法として以下の 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. 例外処理を担当

2 つの変換モードの効果は以下の通りです:

複数サイズのマージ変換

複数サイズに分離変換

五、データモデル設計

アイコンサイズオプションを管理するために、以下のデータモデルを定義します:

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;
}

六、オンラインアイコン変換機能

デスクトップアプリ版に加えて、Blazor ベースのオンラインアイコン変換ツールも開発しました。ユーザーはソフトウェアをインストールせずに画像からアイコンへの変換が可能です。

1. オンライン変換ツールの特長

オンラインアクセス URL:https://dotnet9.com/tool/ico

デスクトップ版と比較して、オンライン版には以下の特長があります:

  1. インストール不要:ブラウザから直接アクセスして利用
  2. クロスプラットフォーム対応:モバイルデバイスを含む最新のブラウザをすべてサポート
  3. 一時ファイル保存:変換後のファイルはサーバー上に一時的に保存され、ユーザーは速やかにダウンロードする必要があります
  4. 簡素化されたインターフェース:Web での使用体験に最適化し、操作がよりシンプルに

オンライン変換インターフェース

2. 変換フロー

オンライン変換ツールのワークフローはシンプルで直感的です:

  1. 画像ファイルを選択(PNG、JPG、JPEG、WEBP 形式に対応)
  2. 変換したいアイコンサイズを選択
  3. 変換モードを選択(マージ生成または個別生成)
  4. ボタンをクリックすると、画像がサーバーにアップロードされて変換が実行
  5. 変換完了後、「ダウンロード」ボタンをクリックして生成されたファイルを取得

オンライン版でも Magick.NET を使用した画像処理を行っており、コア変換ロジックはデスクトップ版と同じです。ただし、ファイルアップロード処理、一時保存、クリーンアップなどの機能が追加されています。具体的な実装に興味がある読者は、ソースコードを直接参照してください:

七、まとめと適用シナリオ

本記事では、デスクトップ版とオンライン版の 2 種類の画像→アイコン変換ツールを実現し、さまざまなユーザーニーズに対応しました。これらには以下の特長があります:

  1. シンプルなユーザーインターフェース:直感的な操作、ドラッグ&ドロップ対応
  2. 豊富な変換オプション:複数のサイズに対応し、さまざまなアプリケーションシナリオのニーズを満たす
  3. 柔軟な変換モード:1 つのマルチサイズ ICO ファイル、または複数の単一サイズ ICO ファイルを生成可能
  4. 良好なコード構造:MVVM デザインパターンを採用し、コードが明確で、保守・拡張が容易

このツールは以下のシナリオに適用できます:

  • ウェブサイト開発における favicon.ico の生成
  • アプリケーション開発におけるアプリアイコンの生成
  • デザイナーがさまざまなサイズのアイコンファイルをすばやく生成

さらに、本プロジェクトでは C# アプリケーションで強力な画像処理ライブラリ Magick.NET を使用する方法や、Avalonia を使用してクロスプラットフォームデスクトップアプリケーションを構築する方法を示しています。これらの知識は他の類似開発プロジェクトにも応用できます。

本記事がお役に立てば幸いです。ご質問があれば、コメント欄でお気軽にご議論ください!

ソースコード参考

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2025/03/10

駐車連絡用QRコード生成ツールの開発実践

本記事では、C#とAvaloniaを使用したデスクトップ版、およびBlazorフロントエンドと.NET Web APIを使用したオンライン版の駐車連絡用QRコード生成ツールの開発方法について、要件分析、コアコード実装、UI設計、MVVMパターンの適用を含めて紹介します。

続きを読む
同じカテゴリ / 同じタグ 2026/01/11

AvaloniaのクリップボードとDataGridの問題

最近のAvaloniaデスクトップソフトウェア開発で解決した2つの問題を記録:クリップボードコピーのクラッシュ、タブ切り替え時のDataGridの遅延。根本原因を分析し、解決策を提供する

続きを読む
同じカテゴリ / 同じタグ 2025/02/25

.NET 10 Preview 1 リリース

本日.NET 10 Preview 1がリリースされました。私はすぐにダウンロードして、Avalonia UIプロジェクトとブログサイトをアップグレードしました。前者は機能テストとAOT公開が正常に動作し、後者はデバッグが正常に行えます。Dockerは今のところ成功していません。

続きを読む