ちょっとしたテキストを一時的に書き留めておくために、
WPFでシンプルなスクラッチパッドを作りました。
用途はかなり限定的で、
主に PowerShell のワンライナーやコマンド断片の記録用です。
メモ帳やノートアプリのように、
整理・分類・検索といった機能は持たせていません。
「とりあえず書く」「あとで消してもいい」
そんな 作業中の思考置き場として使うことを目的にしています。
実装は コードビハインド中心で、
MVVM を厳密に適用することはせず、
View / State / Helper を最低限に分けるだけの構成にしました。
保存先はシンプルに テキストファイルで、
タイトルは本文の先頭行から自動生成されます。
名前を考える手間を省き、
「書くこと」に集中できる作りです。
ソースコード
ファイル名:App.xaml
<Application x:Class="ScratchPad.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ScratchPad"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
ファイル名:App.xaml.cs
using System.Configuration;
using System.Data;
using System.Windows;
namespace ScratchPad;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
ファイル名:Helpers\AppPathHelper.cs
// アプリケーション固有の保存パスを一元管理する Helper。
using System.Reflection;
namespace Maywork.WPF.Helpers;
public static class AppPathHelper
{
/// アプリケーション名。
public static string AppName { get; } = GetDefaultAppName();
// ------------------------------------------------------------
// static ctor
// ------------------------------------------------------------
static AppPathHelper()
{
EnsureDirectories();
}
// ------------------------------------------------------------
// public paths
// ------------------------------------------------------------
/// 設定ファイル・ユーザー設定用(APPDATA)
public static string Roaming =>
System.IO.Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData),
AppName);
/// キャッシュ・ログ・一時データ用(LOCALAPPDATA)
public static string Local =>
System.IO.Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData),
AppName);
public static string SettingsFile =>
System.IO.Path.Combine(Roaming, "settings.json");
public static string CacheDir =>
System.IO.Path.Combine(Local, "cache");
public static string LogDir =>
System.IO.Path.Combine(Local, "log");
public static string TempDir =>
System.IO.Path.Combine(Local, "temp");
// ------------------------------------------------------------
// helpers
// ------------------------------------------------------------
private static string GetDefaultAppName()
{
var asm = Assembly.GetEntryAssembly();
return asm?.GetName().Name ?? "Application";
}
/// <summary>
/// 必要なディレクトリをすべて作成する。
/// </summary>
public static void EnsureDirectories()
{
System.IO.Directory.CreateDirectory(Roaming);
System.IO.Directory.CreateDirectory(Local);
System.IO.Directory.CreateDirectory(CacheDir);
System.IO.Directory.CreateDirectory(LogDir);
System.IO.Directory.CreateDirectory(TempDir);
}
}
/*
【APPDATA (Roaming)】
・ユーザー設定ファイル
・UIレイアウト
・履歴情報
・ユーザー辞書など
※PC移行・ドメイン環境ではローミング対象。
【LOCALAPPDATA (Local)】
・キャッシュデータ
・ログファイル
・一時ファイル
・画像サムネイル
※削除されても再生成可能なデータを保存する。
*/
ファイル名:Helpers\ViewModelBase.cs
// 汎用的な ViewModel 基底クラス実装。
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Maywork.WPF.Helpers;
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
ファイル名:MainWindow.xaml
<Window x:Class="ScratchPad.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:ScratchPad"
mc:Ignorable="d"
Title="スクラッチパッド" Height="800" Width="400">
<TabControl
x:Name="TabHost"
TabStripPlacement="Bottom"/>
</Window>
ファイル名:MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace ScratchPad;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// タブのアイテムを動的に追加するループ
TabItem? firstTab = null;
foreach (var factory in TabViewRegistry.Tabs)
{
var view = factory();
var tab = new TabItem
{
Header = view.Title,
Content = view // ← UserControl
};
if (firstTab is null)
{
firstTab = tab;
}
TabHost.Items.Add(tab);
}
TabHost.SelectedItem = firstTab;
}
}
ファイル名:ScratchPad.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
ファイル名:TabViewRegistry.cs
using ScratchPad.Views;
namespace ScratchPad;
public static class TabViewRegistry
{
public static IReadOnlyList<Func<ITabView>> Tabs { get; } =
[
() => new ScratchPadView(),
];
}
ファイル名:Views\ITabView.cs
namespace ScratchPad.Views;
public interface ITabView
{
string Title { get; }
}
ファイル名:Views\ScratchPadItem.cs
using System.IO;
using Maywork.WPF.Helpers;
public class ScratchPadItem : ViewModelBase
{
string _title = "";
string _content = "";
public string Title
{
get => _title;
set
{
if (_title == value) return;
_title = value;
OnPropertyChanged(nameof(Title));
}
}
public string Content
{
get => _content;
set
{
if (_content == value) return;
_content = value;
OnPropertyChanged(nameof(Content));
}
}
private static string SanitizeFileName(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
return new string(name
.Select(c => invalidChars.Contains(c) ? '_' : c)
.ToArray());
}
public ScratchPadItem()
{
}
public static ScratchPadItem
Create(string content)
{
string title = content.Split("\r\n")[0];
title = SanitizeFileName(title);
if (String.IsNullOrEmpty(title))
{
title = DateTime.Now.ToString("yyyyMMdd_HHmmss");
}
var obj = new ScratchPadItem
{
Title = title,
Content = content,
};
return obj;
}
}
ファイル名:Views\ScratchPadState.cs
using System.Collections.ObjectModel;
using Maywork.WPF.Helpers;
namespace ScratchPad.Views;
public class ScratchPadState : ViewModelBase
{
public ObservableCollection<ScratchPadItem> Items = [];
public ScratchPadItem? SelectedItem {get; set;}
}
ファイル名:Views\ScratchPadView.xaml
<UserControl x:Class="ScratchPad.Views.ScratchPadView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FontSize="14">
<Grid>
<Grid.RowDefinitions>
<!-- テキストエリア -->
<RowDefinition Height="*"/>
<!-- リストビュー -->
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- テキストエリア -->
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<!-- テキストボックス -->
<RowDefinition Height="*"/>
<!-- 操作ボタン -->
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBox x:Name="TextBox1"
AcceptsReturn="True"
TextWrapping="Wrap"/>
</Grid>
<Grid Grid.Row="1">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<Button x:Name="NewButton"
Margin="4"
Padding="4"
Content="新規" />
<Button x:Name="UpdateButton"
Margin="4"
Padding="4"
Content="更新" />
<Button x:Name="DeleteButton"
IsEnabled="False"
Margin="4"
Padding="4"
Content="削除" />
<Button x:Name="CopyButton"
IsEnabled="False"
Margin="4"
Padding="4"
Content="コピー" />
</StackPanel>
</Grid>
</Grid>
<!-- リストビュー -->
<Grid Grid.Row="1">
<ListView x:Name="List"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter
Property="FontSize"
Value="14"/>
<Setter
Property="Padding"
Value="4"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView AllowsColumnReorder="False">
<GridViewColumn
Header="名前"
Width="300"
DisplayMemberBinding="{Binding Title}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Grid>
</UserControl>
ファイル名:Views\ScratchPadView.xaml.cs
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using Maywork.WPF.Helpers;
namespace ScratchPad.Views;
public partial class ScratchPadView : UserControl, ITabView
{
public string Title => "スクラッチパッド";
public ScratchPadState State;
static string GetTextPath(string title)
{
var dir = AppPathHelper.Roaming;
var txtDir = Path.Combine(dir, "text");
var path = Path.Combine(txtDir, $"{title}.txt");
return path;
}
public ScratchPadView()
{
InitializeComponent();
State = new();
Loaded += (_, __) => Init();
NewButton.Click += (_, __) => NewText();
UpdateButton.Click += (_, __) => UpdateText();
DeleteButton.Click += (_, __) => DeleteText();
CopyButton.Click += (_, __) => CopyText();
List.SelectionChanged += (_, __) => SelectionChanged();
TextBox1.TextChanged += (_, __) => TextChanged();
List.ItemsSource = State.Items;
this.DataContext = State;
}
// 初期化
void Init()
{
var dir = AppPathHelper.Roaming;
var txtDir = Path.Combine(dir, "text");
if (!Directory.Exists(txtDir))
{
Directory.CreateDirectory(txtDir);
return;
}
var files = Directory.GetFiles(txtDir, "*.txt");
State.Items.Clear();
foreach (var file in files.OrderBy(f => f))
{
string content = File.ReadAllText(file);
var item = ScratchPadItem.Create(content);
State.Items.Add(item);
}
}
// 新規
void NewText()
{
TextBox1.Text = String.Empty;
List.SelectedItem = null;
}
//
void SelectionChanged()
{
if (State.SelectedItem is null)
{
DeleteButton.IsEnabled = false;
return;
}
DeleteButton.IsEnabled = true;
TextBox1.Text = State.SelectedItem.Content;
}
// 更新
void UpdateText()
{
string content = TextBox1.Text;
if (string.IsNullOrEmpty(content)) return;
var item = ScratchPadItem.Create(content);
var existing = State.Items.FirstOrDefault(x => x.Title == item.Title);
string txtFile = GetTextPath(item.Title);
File.WriteAllText(txtFile, item.Content);
if (existing != null)
{
// 既存 → 更新
existing.Content = item.Content;
}
else
{
// 無ければ追加
State.Items.Add(item);
if (List.Items.Count > 0)
{
List.SelectedIndex = List.Items.Count - 1;
}
}
UpdateButton.IsEnabled = false;
}
// 削除
void DeleteText()
{
if (State.SelectedItem is null) return;
var item = State.SelectedItem;
var existing = State.Items.FirstOrDefault(x => x.Title == item.Title);
if (existing is null) return;
string txtFile = GetTextPath(existing.Title);
if (File.Exists(txtFile))
File.Delete(txtFile);
State.Items.Remove(existing);
State.SelectedItem = null;
NewText();
}
// 入力変更
void TextChanged()
{
string content = TextBox1.Text;
if (string.IsNullOrEmpty(content))
{
NewButton.IsEnabled = false;
UpdateButton.IsEnabled = false;
CopyButton.IsEnabled = false;
}
else
{
NewButton.IsEnabled = true;
UpdateButton.IsEnabled = true;
CopyButton.IsEnabled = true;
}
}
// コピー
void CopyText()
{
string content = TextBox1.Text;
if (string.IsNullOrEmpty(content)) return;
// クリップボードへコピー
Clipboard.SetText(content);
}
}

コメント