シンプルなアプリケーションランチャー
プロジェクトの作成
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+↓キーでアプリを一段下へ移動
- ダブルクリックでアプリケーションを起動
実行イメージ
起動時のスクリーンショット
コメント