WinFormsで作るシンプルなアプリケーションランチャー2【.NET 9 / C#】

コンピュータ

アプリケーションランチャーを使っていて登録したアプリケーションの数が増えるとスクロールするのが大変になってきました。
対策として、Homeキーでで最初の行に移動する機能と、アプリケーションをダブルクリックするたびに1つ上の行に移動する機能を付与しました。
よく使うアプリが上部に集まるような仕様になりましたので、多量のアプリケーションが登録されていても、目的のアプリケーションを見つけやすくなったと思われます。

ソースコード

using System.Diagnostics;
using System.Text.Json;


namespace ApplicationLauncher01;

public class AppInfo
{
    public string? Name { get; set; }
    public string? FullName { get; set; }

    [System.Text.Json.Serialization.JsonIgnore]
    public Icon? Icon { get; set; }
}

public partial class Form1 : Form
{
    private const string IniPath = "AppLaunch.json";
    private List<AppInfo> apps = new();
    private ImageList imageList = new();

    ListBox listBox = new()
    {
        Dock = DockStyle.Fill,
        DrawMode = DrawMode.OwnerDrawFixed,
        ItemHeight = 50
    };

    public Form1()
    {
        InitializeComponent();
        InitUI();
        LoadApps();
    }
    private void InitUI()
    {
        this.Text = "App Launcher";
        this.Width = 400;
        this.Height = 800;
        this.AllowDrop = true;

        this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);

        // リストボックスのアテムの描画
        listBox.DrawItem += ListBox_DrawItem;

        // リストボックスでダブルクリック
        listBox.MouseDoubleClick += ListBox_MouseDoubleClick;

        // リストボックスでキーダウン
        listBox.KeyDown += ListBox_KeyDown;

        // コントロールの追加
        this.Controls.Add(listBox);

        // ドラックエンター
        this.DragEnter += Form1_DragEnter;

        // ドラックドロップ
        this.DragDrop += Form1_DragDrop;

        // フォームクロージング
        this.FormClosing += Form1_FormClosing;
    }

    // 読み込み
    private void LoadApps()
    {
        if (!File.Exists(IniPath)) return;
        
        var lines = File.ReadAllLines(IniPath);
        foreach (var line in lines)
        {
            try
            {
                var info = JsonSerializer.Deserialize<AppInfo>(line);
                if (info != null && File.Exists(info.FullName))
                {
                    try { info.Icon = Icon.ExtractAssociatedIcon(info.FullName); } catch { }
                    apps.Add(info);
                    (Controls[0] as ListBox)?.Items.Add(info);
                }
            }
            catch { }
        }
    }

    // 保存
    private void SaveApps()
    {
        var options = new JsonSerializerOptions { WriteIndented = false };
        using var writer = new StreamWriter(IniPath, false);
        foreach (var app in apps)
        {
            app.Icon = null;
            writer.WriteLine(JsonSerializer.Serialize(app, options));
        }
    }

    // リストボックスのアイテムの描画
    void ListBox_DrawItem(object? sender, DrawItemEventArgs e)
    {
        e.DrawBackground();
        if (e.Index < 0 || e.Index >= apps.Count) return;

        var app = apps[e.Index];
        var icon = app.Icon ?? SystemIcons.Application;
        var font = e.Font ?? SystemFonts.DefaultFont;
        e.Graphics.DrawIcon(icon, e.Bounds.Left + 4, e.Bounds.Top + 4);
        e.Graphics.DrawString(app.Name, font, Brushes.Black, e.Bounds.Left + 54, e.Bounds.Top + 15);
    }
    // リストボックスでダブルクリック
    void ListBox_MouseDoubleClick(object? sender, MouseEventArgs e)
    {
        if (listBox.SelectedIndices.Count == 0)
            return;

        int index = listBox.SelectedIndices[0];

        if (index > 0)
        {
            // 先頭以外の場合一つ上へ移動

            // 対象アイテムを取得して削除
            var item = listBox.Items[index];
            listBox.Items.RemoveAt(index);

            // 一つ上に再挿入
            listBox.Items.Insert(index - 1, item);

            var app = apps[index];
            apps.RemoveAt(index);
            apps.Insert(index - 1, app);

            // 再選択
            listBox.SelectedIndex = index - 1;
            listBox.Select();

        }

        // アプリケーションの起動
        var path = apps[listBox.SelectedIndex].FullName;
        if (File.Exists(path)) Process.Start(path);
    }
    // リストボックスでキーダウン
    void ListBox_KeyDown(object? sender, KeyEventArgs e)
    {
        if (listBox.SelectedIndex < 0) return;

        switch (e.KeyCode)
        {
            case Keys.Delete:
                // 削除
                apps.RemoveAt(listBox.SelectedIndex);
                listBox.Items.RemoveAt(listBox.SelectedIndex);
                break;
            case Keys.Home:
                // トップへ
                if (listBox.SelectedIndices.Count > 0)
                {
                    int index = listBox.SelectedIndices[0];
                    if (index > 0)
                    {
                        var item = listBox.Items[index];
                        listBox.Items.RemoveAt(index);
                        listBox.Items.Insert(0, item);
                        var app = apps[index];
                        apps.RemoveAt(index);
                        apps.Insert(0, app);
                    }
                }
                break;
        }
    }
    // ドラックエンター
    void Form1_DragEnter(object? sender, DragEventArgs e)
    {
        var data = e.Data;
        if (data is null) return;

        if (data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy;
    }
    // ドラックドロップ
    void Form1_DragDrop(object? sender, DragEventArgs e)
    {
        var data = e.Data;
        if (data is null) return;

        var fileDrop = data.GetData(DataFormats.FileDrop);
        if (fileDrop is null) return;

        string[] files = (string[])fileDrop;
        string exe = files[0];
        if (Path.GetExtension(exe).Equals(".exe", StringComparison.CurrentCultureIgnoreCase))
        {
            string name = Path.GetFileNameWithoutExtension(exe);
            var info = new AppInfo { Name = name, FullName = exe };
            try { info.Icon = Icon.ExtractAssociatedIcon(exe); } catch { }
            apps.Add(info);
            listBox.Items.Add(info);
        }
    }
    // フォームクロージング
    void Form1_FormClosing(object? sender, FormClosingEventArgs e)
    {
        // 保存
        this.SaveApps();
    }
}

追記:20250627
使ってみて、Homeで最上段に移動する機能は良いですが、アプリケーションの起動で1つ上段へ移動は良いアイディアだと思ったのですが、毎回アイコンが入れ替わるのは煩わしく感じるので廃止しようと思います。かわりにキーボード操作で上下に移動する機能を付け加えようと考えています。

using System.Diagnostics;
using System.Text.Json;


namespace ApplicationLauncher01;

public class AppInfo
{
    public string? Name { get; set; }
    public string? FullName { get; set; }

    [System.Text.Json.Serialization.JsonIgnore]
    public Icon? Icon { get; set; }
}

public partial class Form1 : Form
{
    private const string IniPath = "AppLaunch.json";
    private List<AppInfo> apps = new();
    private ImageList imageList = new();

    ListBox listBox = new()
    {
        Dock = DockStyle.Fill,
        DrawMode = DrawMode.OwnerDrawFixed,
        ItemHeight = 50
    };

    public Form1()
    {
        InitializeComponent();
        InitUI();
        LoadApps();
    }
    private void InitUI()
    {
        this.Text = "App Launcher";
        this.Width = 400;
        this.Height = 800;
        this.AllowDrop = true;

        this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);

        // リストボックスのアテムの描画
        listBox.DrawItem += ListBox_DrawItem;

        // リストボックスでダブルクリック
        listBox.MouseDoubleClick += ListBox_MouseDoubleClick;

        // リストボックスでキーダウン
        listBox.KeyDown += ListBox_KeyDown;

        // コントロールの追加
        this.Controls.Add(listBox);

        // ドラックエンター
        this.DragEnter += Form1_DragEnter;

        // ドラックドロップ
        this.DragDrop += Form1_DragDrop;

        // フォームクロージング
        this.FormClosing += Form1_FormClosing;
    }

    // 読み込み
    private void LoadApps()
    {
        if (!File.Exists(IniPath)) return;
        
        var lines = File.ReadAllLines(IniPath);
        foreach (var line in lines)
        {
            try
            {
                var info = JsonSerializer.Deserialize<AppInfo>(line);
                if (info != null && File.Exists(info.FullName))
                {
                    try { info.Icon = Icon.ExtractAssociatedIcon(info.FullName); } catch { }
                    apps.Add(info);
                    (Controls[0] as ListBox)?.Items.Add(info);
                }
            }
            catch { }
        }
    }

    // 保存
    private void SaveApps()
    {
        var options = new JsonSerializerOptions { WriteIndented = false };
        using var writer = new StreamWriter(IniPath, false);
        foreach (var app in apps)
        {
            app.Icon = null;
            writer.WriteLine(JsonSerializer.Serialize(app, options));
        }
    }

    // リストボックスのアイテムの描画
    void ListBox_DrawItem(object? sender, DrawItemEventArgs e)
    {
        e.DrawBackground();
        if (e.Index < 0 || e.Index >= apps.Count) return;

        var app = apps[e.Index];
        var icon = app.Icon ?? SystemIcons.Application;
        var font = e.Font ?? SystemFonts.DefaultFont;
        e.Graphics.DrawIcon(icon, e.Bounds.Left + 4, e.Bounds.Top + 4);
        e.Graphics.DrawString(app.Name, font, Brushes.Black, e.Bounds.Left + 54, e.Bounds.Top + 15);
    }
    // リストボックスでダブルクリック
    void ListBox_MouseDoubleClick(object? sender, MouseEventArgs e)
    {
        if (listBox.SelectedIndices.Count == 0)
            return;

        /*
        int index = listBox.SelectedIndices[0];

        if (index > 0)
        {
            // 先頭以外の場合一つ上へ移動

            // 対象アイテムを取得して削除
            var item = listBox.Items[index];
            listBox.Items.RemoveAt(index);

            // 一つ上に再挿入
            listBox.Items.Insert(index - 1, item);

            var app = apps[index];
            apps.RemoveAt(index);
            apps.Insert(index - 1, app);

            // 再選択
            listBox.SelectedIndex = index - 1;
            listBox.Select();

        }
        */

        // アプリケーションの起動
        var path = apps[listBox.SelectedIndex].FullName;
        if (File.Exists(path)) Process.Start(path);
    }
    // リストボックスでキーダウン
    void ListBox_KeyDown(object? sender, KeyEventArgs e)
    {
        if (listBox.SelectedIndex < 0) return;

        if (e.KeyCode == Keys.Delete)
        {
            // 削除
            apps.RemoveAt(listBox.SelectedIndex);
            listBox.Items.RemoveAt(listBox.SelectedIndex);
        }

        if (e.Alt && e.KeyCode == Keys.Up)
        {
            // 上へ
            if (listBox.SelectedIndices.Count > 0)
            {
                int index = listBox.SelectedIndices[0];
                if (index > 0)
                {
                    var item = listBox.Items[index];
                    listBox.Items.RemoveAt(index);
                    var app = apps[index];
                    apps.RemoveAt(index);

                    index -= 1;
                    listBox.Items.Insert(index, item);
                    apps.Insert(index, app);

                    // 再選択
                    listBox.SelectedIndex = index;
                    listBox.Select();
                }
            }
            e.Handled = true;
        }
        if (e.Alt && e.KeyCode == Keys.Down)
        {
            // 下へ
            if (listBox.SelectedIndices.Count > 0)
            {

                int index = listBox.SelectedIndices[0];
                if (index < (apps.Count - 1))
                {
                    var item = listBox.Items[index];
                    listBox.Items.RemoveAt(index);
                    var app = apps[index];
                    apps.RemoveAt(index);

                    index += 1;
                    listBox.Items.Insert(index, item);
                    apps.Insert(index, app);

                    // 再選択
                    listBox.SelectedIndex = index;
                    listBox.Select();
                }
            }
            e.Handled = true;
        }
        if (e.Alt && e.KeyCode == Keys.Home)
        {
            // トップへ
            if (listBox.SelectedIndices.Count > 0)
            {
                int index = listBox.SelectedIndices[0];
                if (index > 0)
                {
                    var item = listBox.Items[index];
                    listBox.Items.RemoveAt(index);
                    listBox.Items.Insert(0, item);
                    var app = apps[index];
                    apps.RemoveAt(index);
                    apps.Insert(0, app);

                    // 再選択
                    listBox.SelectedIndex = 0;
                    listBox.Select();
                }
            }
            e.Handled = true;
        }
        if (e.Alt && e.KeyCode == Keys.End)
        {
            // ボトムへ
            if (listBox.SelectedIndices.Count > 0)
            {
                int index = listBox.SelectedIndices[0];
                if (index < (apps.Count - 1))
                {
                    var item = listBox.Items[index];
                    listBox.Items.RemoveAt(index);
                    listBox.Items.Add(item);
                    var app = apps[index];
                    apps.RemoveAt(index);
                    apps.Add(app);

                    // 再選択
                    listBox.SelectedIndex = apps.Count-1;
                    listBox.Select();
                }
            }
            e.Handled = true;
        }
    }
    // ドラックエンター
    void Form1_DragEnter(object? sender, DragEventArgs e)
    {
        var data = e.Data;
        if (data is null) return;

        if (data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy;
    }
    // ドラックドロップ
    void Form1_DragDrop(object? sender, DragEventArgs e)
    {
        var data = e.Data;
        if (data is null) return;

        var fileDrop = data.GetData(DataFormats.FileDrop);
        if (fileDrop is null) return;

        string[] files = (string[])fileDrop;
        string exe = files[0];
        if (Path.GetExtension(exe).Equals(".exe", StringComparison.CurrentCultureIgnoreCase))
        {
            string name = Path.GetFileNameWithoutExtension(exe);
            var info = new AppInfo { Name = name, FullName = exe };
            try { info.Icon = Icon.ExtractAssociatedIcon(exe); } catch { }
            apps.Add(info);
            listBox.Items.Add(info);
        }
    }
    // フォームクロージング
    void Form1_FormClosing(object? sender, FormClosingEventArgs e)
    {
        // 保存
        this.SaveApps();
    }
}

以下のディレクトリにインストールするPowerShellスクリプト
"C:\Users\karet\Tools\ApplicationLauncher01"

dotnet build -c Release -o output
Get-ChildItem ./output/* | Where-Object { $_.Name -ne "AppLaunch.json" } | Copy-Item -Destination "C:\Users\karet\Tools\ApplicationLauncher01" -Force

コメント