UIの「一貫性」を設計せよ:WPFのStyles, Triggers, Templatesで構築する堅牢な業務アプリケーション
業務アプリケーションの開発において、多くのエンジニアが陥る「罠」がある。それは、「コントロールごとに個別にプロパティを設定してしまうこと」だ。
ボタンの色を一つずつ設定し、マウスホバー時の挙動を一つずつイベントハンドラで書き込む。一見、動いているように見える。しかし、その設計は「技術的負債」の塊だ。要件変更で「ボタンの色をすべてコーポレートカラーの紺色に変更してくれ」と言われた瞬間、あなたは数百箇所のコードを修正する羽目になり、その過程で必ずどこかのボタンの挙動を壊す。
プロフェッショナルなアーキテクトは、UIを「個別の部品の集合」ではなく、「一貫したルールに基づいたシステムの表現」として捉える。
本稿では、WPF(Windows Presentation Foundation)におけるStyles, Triggers, Templatesを駆使し、保守性が高く、変更に強いモダンなUIをVB.NETで構築するための極限の知見を伝授する。
1. Single Source of Truth:App.xamlによるグローバルな統制
デザインの統一感を出すための第一歩は、デザインの定義を各画面(WindowやUserControl)に散らさないことだ。すべての共通ルールは `App.xaml`、あるいはプロジェクト内の `ResourceDictionary` に集約しなければならない。
Implicit Style(暗黙的スタイル)の活用
`x:Key` を指定しないスタイルを「暗黙的スタイル」と呼ぶ。これを使うことで、プロジェクト内のすべての `Button` に対して、個別にコードを書くことなく一括でデザインを適用できる。
<!-- App.xaml または ResourceDictionary 内 -->
<Application.Resources>
<!--
カラーパレットをリソースとして定義する。
これにより、ブランドカラーの変更が一行で完了する。
-->
<SolidColorBrush x:Key="PrimaryColor" Color="#0056b3" />
<SolidColorBrush x:Key="HoverColor" Color="#004494" />
<SolidColorBrush x:Key="DisabledColor" Color="#cccccc" />
<SolidColorBrush x:Key="TextColor" Color="White" />
<!--
すべてのButtonに適用される暗黙的スタイル
-->
<Style TargetType="Button">
<Setter Property="Background" Value="{StaticResource PrimaryColor}" />
<Setter Property="Foreground" Value="{StaticResource TextColor}" />
<Setter Property="Padding" Value="10,5" />
<Setter Property="FontSize" Value="14" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
</Application.Resources>
アーキテクトの視点:
「色を直接 `Background=”#0056b3″` と書くのは素人の仕事だ。必ず `SolidColorBrush` としてリソース化せよ。これが『変更への耐性』を生む。」
2. Triggers:ロジックと表現の分離
業務システムにおいて、ボタンの「無効化(IsEnabled=False)」や「マウスホバー」の表現は極めて重要だ。ここで初心者は `MouseEnter` や `MouseLeave` イベントをVB.NETのコードビハインドに書こうとする。それは絶対にやめてくれ。
UIの状態変化(Visual State)は、XAMLの `Trigger` で完結させるべきだ。コードビハインドにUIの見た目のロジックを混ぜると、ユニットテストが不可能になり、保守性が著しく低下する。
Property Triggerによる状態制御
前述のスタイルに、トリガーを追加して「マウスホバー時」と「無効時」の挙動を定義する。
<Style TargetType="Button">
<!-- (前述のSetterは省略) -->
<Setter Property="Background" Value="{StaticResource PrimaryColor}" />
<Style.Triggers>
<!-- マウスホバー時の挙動 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource HoverColor}" />
</Trigger>
<!-- コントロールが無効化されている時の挙動 -->
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{StaticResource DisabledColor}" />
<Setter Property="Foreground" Value="Gray" />
</Trigger>
</Style.Triggers>
</Style>
アーキテクトの視点:
「`IsMouseOver` をトリガーで制御することで、VB.NET側のコードは『ビジネスロジック』だけに集中できる。UIの色の変化という『副作用』をロジックから切り離せ。」
3. ControlTemplate:コントロールの「骨格」を再定義する
`Style` は既存のプロパティ(BackgroundやFontSizeなど)を書き換えるものだが、`ControlTemplate` はコントロールの「見た目の構造そのもの」を作り直すものだ。
例えば、「角丸のモダンなボタン」を作りたい場合、単に `CornerRadius` プロパティを変えるだけでは不十分な場合がある(Buttonには本来CornerRadiusプロパティがない)。その場合は、`Template` を書き換える必要がある。
プロダクションレベルのテンプレート実装例
<Style x:Key="ModernButtonStyle" TargetType="Button">
<!-- 基本プロパティの設定 -->
<Setter Property="Background" Value="{StaticResource PrimaryColor}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<!--
Borderを使ってボタンの形状を定義する。
ここでのポイントは ContentPresenter を忘れないことだ。
これが、ボタン内に配置されたテキストやアイコンを表示する「窓」になる。
-->
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
SnapsToDevicePixels="true">
<ContentPresenter x:Name="contentPresenter"
Focusable="False"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<!-- テンプレート内でのトリガー定義 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="{StaticResource HoverColor}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.7" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
アーキテクトの視点:
「`TemplateBinding` を使え。これにより、`Style` で設定した `Background` の値が、テンプレート内の `Border` に動的に伝搬する。これを怠ると、スタイルを変更しても見た目が変わらないという、デバッグの地獄に陥ることになる。」
4. VB.NET 実装:堅牢なデータバインディングとの連携
UIが完成したら、次はそれをどう制御するかだ。業務アプリでは、ボタンの有効/無効を「データの状態」に基づいて制御する必要がある。ここでは、MVVMパターンを意識した堅牢な実装例を示す。
ViewModel (VB.NET)
UIの状態(ボタンが押せるかどうか)は、ViewではなくViewModelが決定する。
Imports System.ComponentModel
''' <summary>
''' 業務ロジックを司るViewModel
''' </summary>
Public Class MainViewModel
Implements INotifyPropertyChanged
Private _isProcessing As Boolean = False
Public Property IsProcessing As Boolean
Get
Return _isProcessing
End Get
Set(value As Boolean)
If _isProcessing <> value Then
_isProcessing = value
OnPropertyChanged(NameOf(IsProcessing))
' 処理中フラグが変わったら、ボタンのIsEnabledも自動的に変わるように通知
OnPropertyChanged(NameOf(CanExecuteProcess))
End If
End Set
End Property
''' <summary>
''' ボタンの有効状態を判定するプロパティ
''' </summary>
Public ReadOnly Property CanExecuteProcess As Boolean
Get
' 処理中でなければボタンを押せる
Return Not _isProcessing
End Get
End Property
Public Event PropertyPropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(propertyName, New PropertyChangedEventArgs(propertyName))
End Sub
''' <summary>
''' 実行ボタンが押された時の処理
''' </summary>
Public Async Function ExecuteProcessAsync() As Task
If Not CanExecuteProcess Then Return
IsProcessing = True
Try
' データベース連携やファイル操作などの重い処理をシミュレート
Await Task.Delay(3000)
' ここに実際の業務ロジックを記述
Finally
IsProcessing = False
End Try
End Function
End Class
View (XAML)
ViewModelのプロパティと、先ほど定義したスタイルを紐付ける。
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="業務自動化ツール" Height="300" Width="400">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<!--
Styleに定義したModernButtonStyleを適用。
IsEnabledプロパティは、ViewModelのCanExecuteProcessにバインドされる。
-->
<Button Content="データ処理実行"
Style="{StaticResource ModernButtonStyle}"
Width="150" Height="40"
IsEnabled="{Binding CanExecuteProcess}"
Command="{Binding ExecuteCommand}" />
<!--
注意:Commandパターンを使うのがベストだが、
シンプルにするために今回はコードビハインドからの呼び出し例とする
-->
<TextBlock Text="処理中..."
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}"
HorizontalAlignment="Center" Margin="10"/>
</StackPanel>
</Window>
5. まとめ:プロフェッショナルのためのチェックリスト
優れた業務アプリケーションを構築するために、以下の原則を脳に刻んでおけ。
1. Magic Numbers/Colorsを排除せよ: 色、フォントサイズ、余白はすべて `ResourceDictionary` で定義し、名前を付けて管理すること。
2. コードビハインドを汚すな: `MouseEnter` や `Click` で見た目を変えるな。それは `Trigger` の仕事だ。
3. TemplateBindingをマスターせよ: `Style` の値を `ControlTemplate` 内の要素に伝播させる仕組みを理解せよ。
4. 状態はViewModelが持つ: 「ボタンが押せるかどうか」はUIの属性ではなく、ビジネスロジックの結果である。
これらの設計原則を守ることで、あなたの作るアプリケーションは、単なる「動くツール」から、「変化に強く、メンテナンスが容易な、プロフェッショナルな製品」へと昇華する。
現場へ戻り、その設計思想をコードに反映させろ。期待している。
