XAMLで始めるWPF入門:シンプルなアプリケーションランチャー

コンピュータ

シンプルなアプリケーションランチャー

プロジェクトの作成

cd (mkdir SimpleLauncher)
dotnet new wpf -f net8.0

ソースコード

ファイル名:SimpleLauncher.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
    <ApplicationIcon>Assets/App.ico</ApplicationIcon>
  </PropertyGroup>
  <ItemGroup>
    <Resource Include="Assets\**\*.*" />
  </ItemGroup>
</Project>

ファイル名:App.xaml.cs

using System.IO;
using System.Windows;

namespace SimpleLauncher;

public partial class App : Application
{
    public static string DataDir =>
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SimpleLauncher");
    public static string PathsFile => Path.Combine(DataDir, "apps.txt");

    protected override void OnStartup(StartupEventArgs e)
    {
        Directory.CreateDirectory(DataDir);
        base.OnStartup(e);
    }
}

ファイル名:MainWindow.xaml.cs

using System.Windows;
using System.Windows.Input;
using NxLib.Helper;

namespace SimpleLauncher;


public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var vm = new MainViewModel();

        this.DataContext = vm;

        Wiring.AcceptFiles(List, files =>
        {
            (DataContext as IMainViewModel)?.SetFile(files[0]);
        }, "exe"); 

        Wiring.Hotkey(this, Key.Delete, ModifierKeys.None,
        () =>
        {
            vm.DeleteSelected();
        });

        Wiring.Hotkey(this, Key.Up, ModifierKeys.Alt,
        () =>
        {
            vm.MoveSelectedUp();
        });

        Wiring.Hotkey(this, Key.Down, ModifierKeys.Alt,
        () =>
        {
            vm.MoveSelectedDown();
        });

    }
    private void ListViewItem_DoubleClick(object sender, MouseButtonEventArgs e)
    {
        if (this.DataContext is not MainViewModel) return;

        var vm = (MainViewModel)this.DataContext;
        vm.LaunchApp();
    }
}

ファイル名:Helpers\IconHelper.cs

using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace NxLib.Helper;
public static class IconHelper
{
    // SHGetFileInfoでHICONを取得してWPFのImageSourceへ変換
    [DllImport("Shell32.dll", CharSet = CharSet.Unicode)]
    private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes,
        out SHFILEINFO psfi, uint cbFileInfo, uint uFlags);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct SHFILEINFO
    {
        public IntPtr hIcon;
        public int iIcon;
        public uint dwAttributes;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string szDisplayName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
        public string szTypeName;
    }

    private const uint SHGFI_ICON = 0x000000100;
    private const uint SHGFI_LARGEICON = 0x000000000; // 32x32
    private const uint SHGFI_SHELLICONSIZE = 0x000000004;

    [DllImport("User32.dll", SetLastError = true)]
    private static extern bool DestroyIcon(IntPtr hIcon);

    public static ImageSource GetIconImageSource(string path)
    {
        var info = new SHFILEINFO();
        IntPtr result = SHGetFileInfo(path, 0, out info, (uint)Marshal.SizeOf(info),
            SHGFI_ICON | SHGFI_LARGEICON | SHGFI_SHELLICONSIZE);

        if (result != IntPtr.Zero && info.hIcon != IntPtr.Zero)
        {
            var img = Imaging.CreateBitmapSourceFromHIcon(
                info.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
            DestroyIcon(info.hIcon);
            img.Freeze();
            return img;
        }

        // 何も取れなければ透明1x1
        var wb = new WriteableBitmap(1, 1, 96, 96, PixelFormats.Bgra32, null);
        wb.Freeze();
        return wb;
    }
}

ファイル名:Helpers\SubProc.cs

using System.Diagnostics;

namespace NxLib.Helper;

public static class SubProc
{
    public static bool Launch(string path)
    {
        bool result = true;
        try
        {
            Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
        }
        catch
        {
            result = false;
        }

        return result;
    }
}

ファイル名:Helpers\TextFile.cs

using System.IO;
using System.Text;

namespace NxLib.Helper;

public static class TextFile
{
    private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

    public static string[] Load(string path)
    {
        try
        {
            if (!File.Exists(path)) return Array.Empty<string>();
            // 空行・空白を除去、重複は大文字小文字無視で除去
            return File.ReadLines(path)
                       .Select(s => s.Trim())
                       .Where(s => !string.IsNullOrWhiteSpace(s))
                       .Distinct(StringComparer.OrdinalIgnoreCase)
                       .ToArray();
        }
        catch
        {
            return Array.Empty<string>();
        }
    }

    public static void Save(string path, IEnumerable<string> rows)
    {
        File.WriteAllLines(
            path,
            rows.Where(p => !string.IsNullOrWhiteSpace(p)).Select(p => p.Trim()),
            Utf8NoBom
        );
    }
}

ファイル名:Helpers\Wiring.cs

using System.Windows;
using System.Windows.Input;

namespace NxLib.Helper;
public static class Wiring
{
    public static void AcceptFiles(FrameworkElement el, Action<string[]> onFiles, params string[] exts)
    {
        el.AllowDrop = true;
        el.Drop += (_, e) =>
        {
            if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;

            var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;

            if (exts is { Length: > 0 })
                files = files
                    .Where(f => exts.Any(x => f.EndsWith(x, StringComparison.OrdinalIgnoreCase)))
                    .ToArray();

            if (files.Length > 0)
                onFiles(files);
        };
    }

    public static void Hotkey(Window w, Key key, ModifierKeys mods, Action action, Func<bool>? canExecute = null)
    {
        var cmd = new RoutedUICommand();
        ExecutedRoutedEventHandler exec = (_, __) => action();
        CanExecuteRoutedEventHandler can = (_, e) => e.CanExecute = canExecute?.Invoke() ?? true;

        var cb = new CommandBinding(cmd, exec, can);
        var kb = new KeyBinding(cmd, key, mods);

        w.CommandBindings.Add(cb);
        w.InputBindings.Add(kb);
    }
}

ファイル名:Models\AppItem.cs

using System.ComponentModel;
using System.Windows.Media;

using NxLib.Helper;

namespace SimpleLauncher;
public class AppItem
{
    public string Path { get; }
    public string DisplayName { get; }
    public ImageSource Icon { get; }

    public AppItem(string path, string displayName, ImageSource icon)
    {
        Path = path;
        DisplayName = displayName;
        Icon = icon;
    }

    public static AppItem FromPath(string path)
    {
        var name = System.IO.Path.GetFileNameWithoutExtension(path);
        var icon = IconHelper.GetIconImageSource(path);
        return new AppItem(path, name, icon);
    }
}

ファイル名:ViewModels\IMainViewModel.cs

namespace SimpleLauncher;

public interface IMainViewModel
{
    void SetFile(string file);
    void DeleteSelected();
    void MoveSelectedUp();
    void MoveSelectedDown();
    void LaunchApp();
}

ファイル名:ViewModels\MainViewModel.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

using NxLib.Helper;

namespace SimpleLauncher;

public class MainViewModel : INotifyPropertyChanged, IMainViewModel
{
    public ObservableCollection<AppItem> Apps { get; private set; } = [];
    public AppItem? SelectedApp { get; set; }
    public MainViewModel()
    {
        // 起動時復元
        foreach (var p in TextFile.Load(App.PathsFile)) SetFile(p);

        // 終了時保存
        System.Windows.Application.Current.Exit += (_, __) =>
        {
            var paths = Apps.Select(a => a.Path).ToArray();
            TextFile.Save(App.PathsFile, paths);
        };
    }
    public void SetFile(string file)
    {
        Apps.Add(AppItem.FromPath(file));
    }

    public void DeleteSelected()
    {
        if (SelectedApp is null) return;
        var idx = Apps.IndexOf(SelectedApp);
        if (idx < 0) return;
        Apps.RemoveAt(idx);
        if (Apps.Count > 0)
        {
            var newIdx = Math.Min(idx, Apps.Count - 1);
            SelectedApp = Apps[newIdx];
        }
    }

    public void MoveSelectedUp()
    {
        MoveSelected(-1);
    }

    public void MoveSelectedDown()
    {
        MoveSelected(+1);
    }

    private void MoveSelected(int delta)
    {
        var item = SelectedApp;
        if (item is null) return;

        var oldIndex = Apps.IndexOf(item);
        var newIndex = oldIndex + delta;
        if (newIndex < 0 || newIndex >= Apps.Count) return;

        Apps.Move(oldIndex, newIndex);
    }
    public void LaunchApp()
    {
        if (SelectedApp is null) return;
        SubProc.Launch(SelectedApp.Path);
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

ファイル名:MainWindow.xaml

<Window x:Class="SimpleLauncher.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        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:local="clr-namespace:SimpleLauncher"
        mc:Ignorable="d"
        Title="SimpleLauncher" Height="800" Width="400">
    <Grid>
        <ListView
            x:Name="List"
            Margin="8"
            ItemsSource="{Binding Apps}"
            SelectedItem="{Binding SelectedApp, Mode=TwoWay}"
            AllowDrop="True"
            SelectionMode="Single">
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter
                        Property="FontSize"
                        Value="14"/>
                    <Setter
                        Property="Padding"
                        Value="4"/>
                    <EventSetter
                        Event="MouseDoubleClick"
                        Handler="ListViewItem_DoubleClick"/>
                </Style>
            </ListView.ItemContainerStyle>
            <ListView.View>
                <GridView AllowsColumnReorder="False">
                    <GridViewColumn
                        Header="アイコン"
                        Width="48">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Image
                                    Source="{Binding Icon}"
                                    Width="24" Height="24"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn
                        Header="名前"
                        Width="300"
                        DisplayMemberBinding="{Binding DisplayName}"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

ビルド

dotnet build -c Release -o "出力先ディレクトリ"

使い方

  • exeファイルをドラッグ&ドロップでアプリケーションを登録
  • Deleteキーでアプリの削除
  • Alt+↑キーでアプリを一段上へ移動
  • Alt+↓キーでアプリを一段下へ移動
  • ダブルクリックでアプリケーションを起動

実行イメージ

起動時のスクリーンショット

コメント