GUIのサンプルコードを書いていると、ウィンドウやコントロールの初期化で幅や高さなどを省略できると、コードがコンパクトになるのでは無いかと思い試してみました。
目的
UIを構成するコードの定型的な部分を排除し、コード量を最小化を試みる。
方法
ウィンドウやコントロールの初期化やデフォルトパラメタを自分好みにしたファクトリーメソッドで構成された、staticなクラスライブラリを作成する。その他、画像の読み込み、D&D、キー入力、ダイアログなど、GUIアプリを構成する機能も提供する。
手の込んだ振る舞いで無ければ、staticなクラスを他のアプリに使いまわすことが出来るはず。
実行イメージ
メインルーチン
アプリケーションごとに異なる部分。
ファイル名:NoXAML41Simpls.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
ファイル名:AppEntry.cs
using System;
using System.Windows;
namespace NoXAML41Simples;
using System.Windows;
public class AppEntry : Application
{
[STAThread] public static void Main() => new AppEntry().Run(new MainWindow());
}
ファイル名:MainWindow.cs
// メインウィンドウ
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using static UI;
public sealed class MainWindow : Window
{
public MainWindow()
{
var tabs = new TabControl();
this.Init("Simple NoXAML Samples").Content(tabs);
// ボタンのサンプル
var tab1 = UI.Tab("Button",
Col(
Btn("Hello", () => MessageBox.Show("Clicked!"))
)
).AddTo(tabs);
// スライダーのサンプル
var sliderValue = UI.Lbl("50%");
var sliderBar = UI.Bar();
var tab2 = UI.Tab("Slider",
Col(
Row(Lbl("Value:"), sliderValue),
sliderBar,
// 0~100 のスライダー。目盛 10、スナップ有り。
Sld(0, 100, 50, v => {
sliderValue.Text = $"{(int)v}%";
sliderBar.Width = v * 3; // 値に応じてバーの長さを変える(簡易プレビュー)
}, tick:10, snap:true)
)
).AddTo(tabs);
// グリッドのサンプル
var grid = Grd("*", "*,Auto");
var tab3 = Tab("Grid", grid).AddTo(tabs);
// 左
var left = Col(
Btn("Open", () =>
{
var p = Dialogs.Open();
if (p is not null) this.Title = p;
}),
Btn("Save", () => { Dialogs.SaveAs("untitled.txt"); })
).PlaceIn(grid, 0, 0);
// 右
var righ = Row(
Btn("Prompt", () =>
{
var p = Dialogs.Prompt("タイトル", "メッセージ", "デフォルト");
if (p is not null) this.Title = p;
})
).PlaceIn(grid, 0, 1);
// メニューバーとステータスバーのサンプル
var statusText = Lbl();
var panel = new DockPanel();
var tab4 = Tab("Menu&StatusBar", panel).AddTo(tabs);
// メニュー
UI.MAddToTop(panel,
UI.MiRoot("_File",
UI.MItem(this, "_Open", Key.O, ModifierKeys.Control, () => { statusText.Text = "Open selected"; }),
UI.MItem(this, "_Save", Key.S, ModifierKeys.Control, () => { statusText.Text = "Save selected"; }),
UI.MSep(),
UI.MItem("E_xit", Close)
),
UI.MiRoot("_Help",
UI.MItem("_About", () => { statusText.Text = "About selected"; })
)
);
// ステータスバー
// ここで生成して追加、TextBlockを保持
var (_, st) = UI.SBAddToBottom(panel, "Ready");
statusText = st;
// ダミーテキストブロック
var tb = Lbl("ダミー").AddTo(panel);
// イメージのサンプル
var img = UI.Img();
var tab5 = Tab("Image", img).AddTo(tabs);
// D&D
AllowDrop = true;
// 画像だけ受け付けて最初の1枚を表示
Wiring.AcceptFiles(this, files =>
{
img.Source = BmpSrc.FromFile(files[0]);
},
".png",".jpg",".jpeg",".bmp",".gif",".webp");
// Ctrl+C = Copy
Wiring.Hotkey(this, Key.C, ModifierKeys.Control,
() =>
{
if (img.Source is BitmapSource bmp)
Clipb.SetImageFromSourceOrThrow(img.Source);
},
() => img.Source is not null);
}
}
共通ライブラリ
さまざまアプリケーションで共通につかえる再利用可能なヘルパークラス。
アタッチ拡張
ファイル名:Kit\Attach.cs
// 汎用アタッチ拡張
using System.Windows;
using System.Windows.Controls;
static class Attach
{
// 親要素のChildrenに追加
public static T
AddTo<T>(this T child, Panel parent) where T : UIElement
{ parent.Children.Add(child); return child; }
// Grid(親要素)にrow,colを指定して登録
public static T
PlaceIn<T>(this T child, Grid parent, int row = 0, int col = 0, int rowSpan = 1, int colSpan = 1) where T : UIElement
{
Grid.SetRow(child, row); Grid.SetColumn(child, col);
if (rowSpan != 1) Grid.SetRowSpan(child, rowSpan);
if (colSpan != 1) Grid.SetColumnSpan(child, colSpan);
parent.Children.Add(child); return child;
}
// 親要素のContentにセット
public static T
SetTo<T>(this T child, ContentControl parent) where T : UIElement
{ parent.Content = child; return child; }
// 親要素のItemsに追加
public static T AddTo<T>(this T child, ItemsControl parent) where T : UIElement
{ parent.Items.Add(child); return child; }
}
ビットマップソース
ファイル名:Kit\BmpSrc.cs
// ビットマップソース
using System.IO;
using System.Windows.Media.Imaging;
public static class BmpSrc
{
// ファイルパスから読み込み(ロックしない/Freeze 済み)
public static BitmapSource FromFile(string path, int? decodeW = null, int? decodeH = null)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
return FromStream(stream, decodeW, decodeH);
}
// ストリームから読み込み(必要なら)
public static BitmapSource FromStream(Stream stream, int? decodeW = null, int? decodeH = null)
{
var bi = new BitmapImage();
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad; // EndInit後にstreamを閉じられる
bi.StreamSource = stream;
if (decodeW.HasValue) bi.DecodePixelWidth = decodeW.Value;
if (decodeH.HasValue) bi.DecodePixelHeight = decodeH.Value;
bi.EndInit();
bi.Freeze();
return bi;
}
// 例外を投げたくない場合用
public static bool TryFromFile(string path, out BitmapSource? bmp, int? decodeW = null, int? decodeH = null)
{
try { bmp = FromFile(path, decodeW, decodeH); return true; }
catch { bmp = null; return false; }
}
}
クリップボード
ファイル名:Kit\Clipb.cs
// クリップボード
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
public static class Clipb
{
/// クリップボードへ画像を設定します。失敗時は例外をそのままスローします。
/// STA スレッドで呼び出してください(WPFのUIスレッドならOK)。
public static void SetImageOrThrow(BitmapSource bmp)
{
if (bmp is null) throw new ArgumentNullException(nameof(bmp));
Clipboard.SetImage(bmp); // 失敗時は例外が呼び出し元へ
}
/// ImageSource が BitmapSource の場合のみコピーします。そうでなければ例外。
public static void SetImageFromSourceOrThrow(ImageSource src)
{
if (src is not BitmapSource bmp)
throw new ArgumentException("ImageSource は BitmapSource である必要があります。", nameof(src));
Clipboard.SetImage(bmp);
}
}
ダイアログ
ファイル名:Kit\Dialogs.cs
// ダイアログ
using System.Windows;
using System.Windows.Controls;
using Microsoft.Win32;
static class Dialogs
{
public static string? Open(string filter = "Text|*.txt;*.log;*.md|All|*.*")
{ var d = new OpenFileDialog { Filter = filter }; return d.ShowDialog() == true ? d.FileName : null; }
public static string? SaveAs(string suggest = "untitled.txt", string filter = "Text|*.txt|All|*.*")
{ var d = new SaveFileDialog { FileName = suggest, Filter = filter }; return d.ShowDialog() == true ? d.FileName : null; }
public static string? Prompt(string title, string message, string defaultText = "")
{
var owner = Application.Current?.Windows.OfType<Window>().FirstOrDefault(w => w.IsActive);
var w = new Window {
Title = title, Width = 420, Height = 160,
ResizeMode = ResizeMode.NoResize,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = owner
};
var label = new TextBlock { Text = message, Margin = UiDefaults.Margin };
var tb = new TextBox { Text = defaultText, Margin = UiDefaults.Margin };
var ok = new Button { Content="OK", IsDefault = true, MinWidth = 80, Margin = UiDefaults.Margin };
var cancel= new Button { Content="Cancel",IsCancel = true, MinWidth = 80, Margin = UiDefaults.Margin };
ok.Click += (_, __) => w.DialogResult = true; // これで閉じる
w.Content = UI.Col(label, tb, UI.Row(ok, cancel));
var result = w.ShowDialog();
return result == true ? tb.Text : null;
}
}
ボーダー
ファイル名:Kit\UI.Border.cs
// ボーダー
using System.Windows;
using System.Windows.Controls;
public static partial class UI // コントロールの初期化
{
public static Border Bar(double height=16)
=> new Border{ Height=height, Margin=new Thickness(6),
Background=SystemColors.HighlightBrush };
}
ボタン
ファイル名:Kit\UI.Button.cs
// ボタン
using System.Windows;
using System.Windows.Controls;
public static partial class UI // よく使うビルダー:Row/Col/Btn/Txt
{
// ボタン
public static Button
Btn(
string text, // 表示文字列
Action? onClick = null // クリック時実行するコード
)
{
var b = new Button
{
Content = text,
Margin = new Thickness(6),
MinWidth = 96
};
if (onClick != null)
b.Click += (_, __) => onClick();
return b;
}
}
グリッド
ファイル名:Kit\UI.Grid.cs
// グリッド
using System.Windows;
using System.Windows.Controls;
public static partial class UI // コントロールの初期化
{
// グリッド
public static Grid
Grd(string rows = "*", string cols = "*")
{
var g = new Grid();
foreach (var r in rows.Split(',')) g.RowDefinitions.Add(new RowDefinition { Height = GrdParse(r) });
foreach (var c in cols.Split(',')) g.ColumnDefinitions.Add(new ColumnDefinition { Width = GrdParse(c) });
return g;
}
// グリッド引数文字列=>オプション値
static GridLength GrdParse(string s)
{
s = s.Trim();
if (s.Equals("Auto", System.StringComparison.OrdinalIgnoreCase)) return GridLength.Auto;
if (s.EndsWith("*")) return new GridLength(s.Length == 1 ? 1 : double.Parse(s[..^1]), GridUnitType.Star);
return new GridLength(double.Parse(s), GridUnitType.Pixel);
}
}
イメージ
ファイル名:Kit\UI.Image.cs
// イメージ
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
public static partial class UI
{
// Imageオブジェクトの生成
public static Image Img(
Stretch stretch = Stretch.Uniform,
double? width = null,
double? height = null,
Thickness? margin = null,
BitmapScalingMode scaling = BitmapScalingMode.HighQuality)
{
var img = new Image { Stretch = stretch };
if (width.HasValue) img.Width = width.Value;
if (height.HasValue) img.Height = height.Value;
if (margin.HasValue) img.Margin = margin.Value;
RenderOptions.SetBitmapScalingMode(img, scaling);
return img;
}
}
メニュー
ファイル名:Kit\UI.Menu.cs
// メニュー
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
public static partial class UI // よく使うビルダー
{
// メニューバーを作成
public static Menu MBar(params MenuItem[] roots)
{
var m = new Menu();
foreach (var r in roots) m.Items.Add(r);
return m;
}
// DockPanel の上部に配置して返す
public static Menu MAddToTop(DockPanel root, params MenuItem[] roots)
{
var m = MBar(roots);
DockPanel.SetDock(m, Dock.Top);
root.Children.Add(m);
return m;
}
// ルート(サブメニューを持つ)MenuItem
public static MenuItem MiRoot(string header, params object[] items)
{
var mi = new MenuItem { Header = header };
foreach (var it in items)
{
switch (it)
{
case MenuItem m: mi.Items.Add(m); break;
case Separator s: mi.Items.Add(s); break;
case null: mi.Items.Add(new Separator()); break;
default: throw new ArgumentException("Root() の items は MenuItem / Separator / null のみ可");
}
}
return mi;
}
// クリックだけの項目
public static MenuItem MItem(string header, Action onClick)
{
var mi = new MenuItem { Header = header };
mi.Click += (_, __) => onClick();
return mi;
}
// ショートカット付き項目(Window にコマンド/キーをバインドしつつ表示も揃える)
public static MenuItem MItem(Window w, string header, Key key, ModifierKeys mods, Action onInvoke)
{
var cmd = new RoutedUICommand();
w.CommandBindings.Add(new CommandBinding(cmd, (_, __) => onInvoke(), (_, e) => e.CanExecute = true));
w.InputBindings.Add(new KeyBinding(cmd, key, mods));
return new MenuItem
{
Header = header,
Command = cmd,
InputGestureText = MFormatGesture(key, mods) // 表示用
};
}
public static Separator MSep() => new Separator();
static string MFormatGesture(Key key, ModifierKeys mods)
{
var parts = new List<string>();
if (mods.HasFlag(ModifierKeys.Control)) parts.Add("Ctrl");
if (mods.HasFlag(ModifierKeys.Shift)) parts.Add("Shift");
if (mods.HasFlag(ModifierKeys.Alt)) parts.Add("Alt");
if (mods.HasFlag(ModifierKeys.Windows)) parts.Add("Win");
parts.Add(key.ToString());
return string.Join("+", parts);
}
}
スライダー
ファイル名:Kit\UI.Slider.cs
// スライダー
using System.Windows;
using System.Windows.Controls;
public static partial class UI
{
// onChanged を渡すと初期値でも一回呼び出します
public static Slider Sld(double min = 0, double max = 100, double value = 50,
Action<double>? onChanged = null, double? tick = null, bool snap = false)
{
var s = new Slider
{
Minimum = min,
Maximum = max,
Value = value,
Margin = new Thickness(6),
IsMoveToPointEnabled = true,
IsSnapToTickEnabled = snap
};
if (tick is double t) s.TickFrequency = t;
if (onChanged != null)
{
s.ValueChanged += (_, __) => onChanged(s.Value);
onChanged(value); // 初期反映
}
return s;
}
}
スタックパネル
ファイル名:Kit\UI.StackPanel.cs
// スタックパネル
using System.Windows;
using System.Windows.Controls;
public static partial class UI
{
// スタックパネル(横積み)
public static StackPanel
Row(params UIElement[] kids)
{
var p=new StackPanel
{
Orientation=Orientation.Horizontal, // 水平
Margin=new Thickness(6)
};
foreach(var k in kids)
p.Children.Add(k);
return p;
}
// スタックパネル(縦積み)
public static StackPanel
Col(params UIElement[] kids)
{
var p=new StackPanel
{
Orientation=Orientation.Vertical, // 垂直
Margin=new Thickness(6)
};
foreach(var k in kids)
p.Children.Add(k);
return p;
}
}
ステータスバー
ファイル名:Kit\UI.StatusBar.cs
// ステータスバー
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
public static partial class UI // よく使うビルダー
{
/// ステータスバーを作成し、DockPanelの下部に追加します。
/// 表示用の TextBlock も同時に作成して返します。
public static (StatusBar bar, TextBlock text) SBAddToBottom(DockPanel root, string initial = "Ready")
{
var bar = new StatusBar();
DockPanel.SetDock(bar, Dock.Bottom);
var text = new TextBlock { Text = initial, Margin = new Thickness(6, 0, 6, 0) };
bar.Items.Add(text);
root.Children.Add(bar);
return (bar, text);
}
}
タブコントロール
ファイル名:Kit\UI.TabControl.cs
// タブコントロール
using System.Windows;
using System.Windows.Controls;
public static partial class UI
{
public static TabItem Tab(string header, UIElement content)
=> new() { Header = header, Content = content };
}
テキストブロック
ファイル名:Kit\UI.TextBlock.cs
// テキストブロック
using System.Windows;
using System.Windows.Controls;
public static partial class UI
{
public static TextBlock Lbl(string text="", double font=14)
=> new TextBlock{ Text=text, Margin=new Thickness(6),
VerticalAlignment=VerticalAlignment.Center, FontSize=font };
}
テキストボックス
ファイル名:Kit\UI.TextBox.cs
// テキストボックス
using System.Windows;
using System.Windows.Controls;
public static partial class UI // よく使うビルダー:Row/Col/Btn/Txt
{
// テキストボックス
public static TextBox
Txt(
bool multi=true, // 複数行
bool wrap=false // テキストの折り返し
)
{
return new TextBox
{
AcceptsReturn=multi,
AcceptsTab=multi,
TextWrapping=wrap ? TextWrapping.Wrap : TextWrapping.NoWrap,
Margin=new Thickness(6)
};
}
}
UIのデフォルト値
ファイル名:Kit\UiDefaults.cs
// デフォルト値
using System.Windows;
static class UiDefaults
{
public static double Scale = 1.0;
public static string FontFamily = "Yu Gothic UI";
public static double FontSize = 14;
public static Thickness Margin = new(6);
public static double ButtonW = 96, ButtonH = 32;
public static double TextBoxW = 480, TextBoxH = 260;
public static void UseCompact() { Scale = 0.9; Margin = new(4); ButtonH = 28; }
public static void UseTouch() { Scale = 1.3; Margin = new(10); ButtonH = 44; FontSize = 16; }
}
ウィンドウ
ファイル名:Kit\Win.cs
// Window関連
using System.Windows;
public static class Win
{
// 初期化
public static T
Init<T>(
this T w,
string title="Demo", // タイトル文字列
double width=640, // ウィンドウ幅
double height=420, // ウィンドウ高さ
WindowStartupLocation loc = WindowStartupLocation.CenterScreen
) where T : Window
{
w.Title=title;
w.Width=width;
w.Height=height;
w.WindowStartupLocation=loc;
return w;
}
// コンテンツをセット
public static T
Content<T>(
this T w,
UIElement content
) where T : Window
{
w.Content = content;
return w;
}
}
イベント連携
ファイル名:Kit\Wiring.cs
using System.Windows;
using System.Windows.Input;
public static class Wiring
{
// D&D: 指定拡張子だけ受け付ける。Unloadedで自動解除。
public static void AcceptFiles(FrameworkElement el, Action<string[]> onFiles, params string[] exts)
{
el.AllowDrop = true;
DragEventHandler? drop = null;
drop = (_, e) =>
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
if (exts?.Length > 0)
files = files.Where(f => exts.Any(x => f.EndsWith(x, StringComparison.OrdinalIgnoreCase))).ToArray();
if (files.Length > 0) onFiles(files);
};
el.Drop += drop;
el.Unloaded += (_, __) => el.Drop -= drop;
}
// Hotkey: Actionベースで最短。Unloadedで自動解除。
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);
RoutedEventHandler? unload = null;
unload = (_, __) => { w.Unloaded -= unload!; w.CommandBindings.Remove(cb); w.InputBindings.Remove(kb); };
w.Unloaded += unload;
}
}
コメント