背景
我有一个风险游戏学习项目的 MVVM 框架。我已决定尝试以编程方式创建 UI 的一部分,以“方便”/优雅并作为学习机会。有问题的部分是自定义按钮,代表游戏板的领土,最终应该接受大部分用户的输入。
关键的选择
复杂的根源:我尝试利用全局枚举 (TerrID_Enum.cs),这似乎是合乎逻辑的举动,因为游戏板是静态的。每个 Territory 都会收到一个 0 - 41 的 int ID 号。这样我就可以在任何框架(模型、VM、视图)中使用数组和 for 循环,其中索引与适当的 Territory 直接相关。 (正如我所说,一个 global 枚举)。我在这里和其他地方的网上看到了不同的看法,所以如果你认为关于这个选择有一些你认为我需要知道的事情,请随时插话。
从 For-Loop 索引/区域枚举关系中构建绑定路径的自定义方法
正如您将看到的,按钮的编程初始化程序依赖于自定义方法来编写绑定路径 (Board.MakeStringPath())。它似乎在某种程度上起作用——自定义按钮的内容网格内的 Path.Data 属性首先获得正确的几何形状——但在 IntializeComponent() 之后它似乎变为空?除此之外,由于某种原因,绑定路径和自定义按钮的 ActualHeight 和 ActualWidth 属性都为 0,我不知道这是如何相关的,但它似乎是。
最相关的代码片段:
这是我的 MainWindow.xaml 和 MainWindow.cs 包含的内容。为了清楚起见,我省略了一些触发器和杂项:
<Window>
<Window.Resources>
<ControlTemplate x:Key="TerrButtonCT" TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" BorderBrush="{x:Null}" BorderThickness="0">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Window.Resources>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
Board VM = new(NewGame);
this.DataContext = VM;
Canvas mainCanvas = new Canvas();
this.AddChild(mainCanvas);
Button[] TerrButtons = new Button[42];
// Programatic creation and detail of the Territory Buttons
for (int index = 0; index < 42; index++)
{
// "Content" Grid and Shape, with associated Bindings, are created
Grid defaultGrid = new Grid();
Binding terrButtonPathData = new(VM.MakePathString(index, nameof(VM.Shapes)));
terrButtonPathData.Source = VM;
Binding terrButtonFill = new(VM.MakePathString(index, nameof(VM.Color)));
terrButtonFill.Source = VM;
Path terrPath = new Path() { Stretch = Stretch.Fill };
BindingOperations.SetBinding(terrPath, Path.DataProperty, terrButtonPathData);
BindingOperations.SetBinding(terrPath, Path.FillProperty, terrButtonFill);
defaultGrid.Children.Add(terrPath);
// Control Template brought in from Window Resources
ControlTemplate buttonCT = (ControlTemplate)this.Resources["TerrButtonCT"];
// Triggers
// Multi-Trigger for Mouse "Hover" behavior
MultiTrigger hoverTrigger = new MultiTrigger();
hoverTrigger.Conditions.Add(new() { Property = IsMouseOverProperty, Value = true} );
hoverTrigger.Conditions.Add(new() { Property = Button.IsPressedProperty, Value = false }); ;
// Setters for Trigger details. Shape and Fill need to be bound just like "Content"
hoverTrigger.Setters.Add(new Setter() { Property = BackgroundProperty, Value = null });
hoverTrigger.Setters.Add(new Setter() { Property = BorderBrushProperty, Value = null });
Path hoverPath = new() { Stretch = Stretch.Fill };
Binding hoverColor = new Binding(VM.MakePathString(index, nameof(VM.HoverColor)));
hoverColor.Source = VM;
BindingOperations.SetBinding(hoverPath, Path.DataProperty, terrButtonPathData);
BindingOperations.SetBinding(hoverPath, Path.FillProperty, hoverColor);
Grid hoverGrid = new Grid();
hoverGrid.Children.Add(hoverPath);
hoverTrigger.Setters.Add(new Setter() { Property = ContentProperty, Value = hoverGrid });
// Wrap everything into an individualized Style for the Button
Style buttonStyle = new Style();
buttonStyle.Triggers.Add(hoverTrigger);
buttonStyle.Setters.Add(new Setter() { Property = ContentProperty, Value = defaultGrid });
buttonStyle.Setters.Add(new Setter() { Property = TemplateProperty, Value = buttonCT });
// Feed custom details to the Button
TerrButtons[index] = new Button
{
Name = ((TerrID)index).ToString(),
Style = buttonStyle
};
// Add each Button to the MainWindow Child Canvas
mainCanvas.Children.Add((TerrButtons[index]));
}
InitializeComponent();
}
}
}
到目前为止,这是 ViewModel:
internal class Board : ViewModelBase
{
private ObservableCollection<SolidColorBrush> _color;
private ObservableCollection<SolidColorBrush> _hoverColor;
public Board(Game newGame)
{
CurrentGame = newGame;
Shapes = new List<Geometry>();
Color = new ObservableCollection<SolidColorBrush>();
HoverColor = new ObservableCollection<SolidColorBrush>();
for (int index = 0; index < BaseArmies.Count; index++)
{
if (Application.Current.FindResource(((TerrID)index).ToString() + "Geometry") != null)
Shapes.Add(Application.Current.FindResource(((TerrID)index).ToString() + "Geometry") as Geometry);
Color.Add(Brushes.Black);
HoverColor.Add(Brushes.AntiqueWhite);
}
}
public List<Geometry> Shapes { get; init; }
public ObservableCollection<SolidColorBrush> Color
{
get { return _color; }
set
{
_color = value;
OnPropertyChanged(nameof(Color));
}
}
public ObservableCollection<SolidColorBrush> HoverColor
{
get { return _hoverColor; }
set
{
_hoverColor = value;
OnPropertyChanged(nameof(HoverColor));
}
}
...
internal string MakePathString(int index, string propertyName)
{
if (index >= 0 && index < 42 && propertyName != null)
{
string indexStr = index.ToString();
switch (propertyName)
{
case nameof(Color):
return "Color[" + indexStr + "]";
case nameof(HoverColor):
return "HoverColor[" + indexStr + "]";
case nameof(PressedColor):
return "PressedColor[" + indexStr + "]";
case nameof(DisabledColor):
return "DisabledColor[" + indexStr + "]";
case nameof(Shapes):
return "Shapes[" + indexStr + "]";
}
return "Property not found!";
}
else return "MakePathString() requires 0 < int index < 42 and string propertyName.";
}
更多代码/注释
除了全局 TerrID 枚举之外,还有一个巨大的 App.xaml,其中包含每个领土的几何图形。我认为没有必要在这里包含,但你可以在 Github 上找到它。
自定义带有路径的按钮的基本设计是从这里改编/提升的。
如果需要查看更多:Full Project @ Github
Nuget 包包括:MS MVVM 社区工具包 8.2.0
谢谢!
我知道要经历很多事情,所以提前致谢!
过去的尝试和挫折
我尝试在 XAML 中设置自定义按钮样式,但我想创建一个自定义路径字符串以启用 for 循环初始化程序,这将需要嵌套绑定(Binding.Path 上的绑定,我发现它不需要工作,因为它不是 DependencyProperty),或者与 DataObjectProvider 进行的一些精心设计的工作,我还没有全神贯注。 (我也放弃了一系列不可行的转换器,但这可能是一个解决方案,我只是没有看到它)。