可视化 WPF 中的二进制规则系统






4.42/5 (15投票s)
一次使用WPF可视化简单数字规则系统的有趣探索。
引言
本文展示了一个WPF应用程序,它接受一组简单规则、一些要由这些规则处理的输入数据,然后允许我们可视化将这些规则应用于输入数据的结果。规则和输入数据存储在XML文件中,允许您使用各种配置进行实验。
背景
前几天在工作中,我和我的好朋友 Grant Hinkson 聊天。Grant提到他一直在阅读 Stephen Wolfram 的书《一种新科学》。他向我解释了 元胞自动机 的思想,这引起了我的兴趣并激发了我的想象力。拥有应用于基本构建块的简单规则集,然后通过重复应用这些规则来观察可以产生的宏观现象的想法,对我来说非常引人入胜。
现在,在进一步说明之前,我必须明确一点。我没有研究过Wolfram的作品,我也不声称理解他的思想,我的程序也绝不是试图演示他的概念。他的 Mathematica 产品在这方面做得非常出色。我只是受了一次关于元胞自动机的讨论的启发,并在此灵感的基础上构建了一个简单的程序。
二进制规则系统的思想
这个想法很简单:取一串数字,对这些数字应用一些规则,然后得到一串新数字。输入序列中的每个数字都会产生输出序列中的一个数字。规则可以包含你想要的任何逻辑。最简单的规则是返回输入值。在我的程序中,对于给定的输入值,每个规则都返回相同的输出值,但该输出值是可配置的。
那么,我为什么称之为“二进制”规则系统呢?我的规则系统只处理零到七的值。这八个数字可以用前三个二进制值表示(即二进制中的000到111)。当我们稍后渲染数字时,我们将把每个数字显示为其二进制表示的可视化。
视觉解释
让我们从最简单的例子开始。在演示项目中,*simple1.xml* 文件包含以下配置
<?xml version="1.0" encoding="utf-8" ?>
<config iterations="8">
<rules>
<rule input="0" output="1" />
<rule input="1" output="2" />
<rule input="2" output="3" />
<rule input="3" output="4" />
<rule input="4" output="5" />
<rule input="5" output="6" />
<rule input="6" output="7" />
<rule input="7" output="0" />
</rules>
<numbers>
<number value="0" />
</numbers>
</config>
上面显示的配置展示了基本原理。`<rules>` 部分指定了如何将输入值映射到输出值。此示例中的映射只是按升序遍历数字行。`<numbers>` 部分只有一个数字,零。这意味着应用程序将只显示一个数字并开始对该数字应用规则。规则将应用多少次?正如您在`<config>`元素中看到的那样,规则将应用八次,由“`iterations`”属性指定。
使用上述配置运行程序将创建这些数字的十进制可视化
0
1
2
3
4
5
6
7
你也可以说这是相同数字的二进制可视化
000
001
010
011
100
101
110
111
这是程序运行时应用此配置的屏幕截图,其中黑色单元格代表 0,白色单元格代表 1。图像中的每个单元格“行”代表一个数字,并且数字随着您从一行向下移动到另一行而增加。
也可以指定数字中的每个位应如何渲染。如上所示,默认情况下,如果一个位的值为0,则为黑色;如果其值为1,则为白色。演示项目中的 *simple2.xml* 文件展示了如何指定每个位在为1时的颜色。这是该文件
<?xml version="1.0" encoding="utf-8" ?>
<config
color1="White"
color2="LightGray"
color3="Gray"
iterations="8"
>
<rules>
<rule input="0" output="1" />
<rule input="1" output="2" />
<rule input="2" output="3" />
<rule input="3" output="4" />
<rule input="4" output="5" />
<rule input="5" output="6" />
<rule input="6" output="7" />
<rule input="7" output="0" />
</rules>
<numbers>
<number value="0" />
</numbers>
</config>
加载该配置文件运行程序,效果如下:
到目前为止,我们只看到了 `<numbers>` 部分中包含一个数字的示例。随着我们添加更多输入数字,可视化效果变得更加有趣。下一个配置在 *simple3.xml* 文件中,包含八个输入数字。这些数字在同一行中并排显示。这是下一个配置
<?xml version="1.0" encoding="utf-8" ?>
<config
color1="White"
color2="LightGray"
color3="Gray"
iterations="8"
>
<rules>
<rule input="0" output="1" />
<rule input="1" output="2" />
<rule input="2" output="3" />
<rule input="3" output="4" />
<rule input="4" output="5" />
<rule input="5" output="6" />
<rule input="6" output="7" />
<rule input="7" output="0" />
</rules>
<numbers>
<number value="0" />
<number value="1" />
<number value="2" />
<number value="3" />
<number value="4" />
<number value="5" />
<number value="6" />
<number value="7" />
</numbers>
</config>
以上配置渲染效果如下:
本节的最后一个示例是一个配置,它采用之前的输入数字并创建它们的镜像对称,使用更令人兴奋的颜色,并增加规则应用的次数。由此产生的可视化效果具有视觉吸引力,特别是如果您以更大的尺寸查看它。 *simple4.xml* 配置文件如下所示
<?xml version="1.0" encoding="utf-8" ?>
<config
color1="DodgerBlue"
color2="Orange"
color3="Lime"
iterations="200"
>
<rules>
<rule input="0" output="1" />
<rule input="1" output="2" />
<rule input="2" output="3" />
<rule input="3" output="4" />
<rule input="4" output="5" />
<rule input="5" output="6" />
<rule input="6" output="7" />
<rule input="7" output="0" />
</rules>
<numbers>
<number value="0" />
<number value="1" />
<number value="2" />
<number value="3" />
<number value="4" />
<number value="5" />
<number value="6" />
<number value="7" />
<number value="6" />
<number value="5" />
<number value="4" />
<number value="3" />
<number value="2" />
<number value="1" />
<number value="0" />
</numbers>
</config>
上面看到的配置看起来像这样
规则系统的工作原理
这个规则系统非常简单。它由一个表示数字的类和一个表示规则的类组成。这两个类都有一个相应的集合类。这是表示数字的 `BinaryNumber` 类
public class BinaryNumber
{
byte _value;
public BinaryNumber(byte value)
{
if (value < 0 || 7 < value)
throw new ArgumentOutOfRangeException("value");
_value = value;
}
public bool Bit1
{
get { return _value % 2 == 1; }
}
public bool Bit2
{
get { return (_value >> 1) % 2 == 1; }
}
public bool Bit4
{
get { return (_value >> 2) % 2 == 1; }
}
public byte Value
{
get { return _value; }
}
}
这个类是对一个 byte
值的封装。`Bit1`、`Bit2` 和 `Bit4` 属性稍后在为这个类创建可视化时会发挥作用。如果它们所代表的位对于 `BinaryNumber` 的值是“开启”的,则这些属性都会返回 true
。例如,如果值为五(二进制为101),`Bit4` 返回 true
,`Bit2` 返回 false
,`Bit1` 返回 true
。
我们通过将 `BinaryNumber` 添加到 `BinaryNumberCollection` 来创建 `BinaryNumber` 序列。该类还提供了通过对值序列应用规则来获取输出值的逻辑。该类如下所示
public class BinaryNumberCollection : ReadOnlyCollection<BinaryNumber>
{
readonly BinaryRuleCollection _rules;
public BinaryNumberCollection(IList<BinaryNumber> list, BinaryRuleCollection rules)
: base(list)
{
_rules = rules;
}
public BinaryNumberCollection OutputNumbers
{
get
{
var outputQuery =
from number in base.Items
select _rules.ApplyRule(number);
return new BinaryNumberCollection(outputQuery.ToList(), _rules);
}
}
}
`BinaryRule` 类代表一个规则,它非常简单
public class BinaryRule
{
public BinaryRule(byte input, byte output)
{
this.Input = new BinaryNumber(input);
this.Output = new BinaryNumber(output);
}
public BinaryNumber Input { get; private set; }
public BinaryNumber Output { get; private set; }
}
规则被添加到 `BinaryRuleCollection` 中,该类的实例由所有 `BinaryNumberCollection` 对象共享。规则集合提供了一种方法,可以获取规则针对输入值创建的输出值。该类列出如下
public class BinaryRuleCollection : ReadOnlyCollection<BinaryRule>
{
public BinaryRuleCollection(IList<BinaryRule> list)
: base(list)
{
}
public BinaryNumber ApplyRule(BinaryNumber input)
{
BinaryRule rule =
base.Items.FirstOrDefault(r => r.Input.Value == input.Value);
if (rule == null)
{
Debug.Fail("Missing rule for input value " + input.Value);
return null;
}
return rule.Output;
}
}
可视化如何工作
到目前为止,我们还没有讨论我是如何创建可视化的。我们只关注了二进制规则系统是什么以及它是如何工作的。现在,是时候将注意力转向这个程序的渲染方面了。
我本可以有很多种方法来实现渲染。我想保持简单,尽可能多地使用数据绑定和数据模板。我知道还有其他方法可以稍微提高性能,但性能对我来说不是优先考虑的问题。我宁愿保持简单,易于在XAML中配置。
该程序有两个数据模板。一个模板渲染一个 `BinaryNumber` 对象。它在 `StackPanel` 中显示三个 `Rectangle`,每个 `Rectangle` 代表数字中的一个二进制位。该模板如下所示
<DataTemplate DataType="{x:Type model:BinaryNumber}">
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="Rectangle">
<Setter Property="Height" Value="1" />
<Setter Property="Width" Value="1" />
</Style>
</StackPanel.Resources>
<Rectangle x:Name="bit4" />
<Rectangle x:Name="bit2" />
<Rectangle x:Name="bit1" />
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Bit1}" Value="True">
<Setter
TargetName="bit1"
Property="Fill"
Value="{DynamicResource color3}"
/>
</DataTrigger>
<DataTrigger Binding="{Binding Bit2}" Value="True">
<Setter
TargetName="bit2"
Property="Fill"
Value="{DynamicResource color2}"
/>
</DataTrigger>
<DataTrigger Binding="{Binding Bit4}" Value="True">
<Setter
TargetName="bit4"
Property="Fill"
Value="{DynamicResource color1}"
/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
用于绘制每个 `Rectangle` 的画刷来自一个动态资源引用。这些画刷被放置在 `Application.Resources` 集合中。该逻辑存在于 `Configuration` 类的构造函数中,如下所示
XDocument xdoc = XDocument.Load(configFilePath);
XElement configElem = xdoc.Element("config");
BrushConverter converter = new BrushConverter();
for (int i = 1; i <= 3; ++i)
{
string colorName = "color" + i;
XAttribute attr = configElem.Attribute(colorName);
Brush brush;
if (attr == null)
brush = Brushes.White;
else
brush = converter.ConvertFromString(attr.Value) as Brush;
App.Current.Resources[colorName] = brush;
}
`BinaryNumberCollection` 类也有一个数据模板。该模板在 `ItemsControl` 中显示集合的所有 `BinaryNumber` 对象。每个 `ItemsControl` 都可以被认为是可视化中的一个“行”。该模板声明如下
<DataTemplate DataType="{x:Type model:BinaryNumberCollection}">
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
所有 `BinaryNumberCollection` 的容器是一个 `ItemsControl`,其 `ItemsSource` 属性在代码背后设置为 `List<BinaryNumberCollection>`。该 `ItemsControl` 被包装在一个 `Viewbox` 中,以便无论 `Window` 的大小如何,都能很好地渲染。这些元素如下所示
<Border
Background="Black"
BorderBrush="Black"
BorderThickness="3"
Margin="6"
Padding="1"
>
<Viewbox Stretch="Fill">
<ItemsControl x:Name="itemList" />
</Viewbox>
</Border>
结论
这可能不是最有用或最有趣的文章,但我认为它很有趣。WPF 以简单、声明式的方式为这些数据创建引人入胜的可视化,这本身就很有吸引力。我希望你喜欢这篇文章,并且可能在此过程中学到了一两件事。祝你编码愉快!