65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.42/5 (15投票s)

2008年6月1日

CPOL

6分钟阅读

viewsIcon

55891

downloadIcon

163

一次使用WPF可视化简单数字规则系统的有趣探索。

BinaryRuleVisualization

引言

本文展示了一个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。图像中的每个单元格“行”代表一个数字,并且数字随着您从一行向下移动到另一行而增加。

simple1.png

也可以指定数字中的每个位应如何渲染。如上所示,默认情况下,如果一个位的值为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>

加载该配置文件运行程序,效果如下:

simple2.png

到目前为止,我们只看到了 `<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>

以上配置渲染效果如下:

simple3.png

本节的最后一个示例是一个配置,它采用之前的输入数字并创建它们的镜像对称,使用更令人兴奋的颜色,并增加规则应用的次数。由此产生的可视化效果具有视觉吸引力,特别是如果您以更大的尺寸查看它。 *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>

上面看到的配置看起来像这样

simple4.png

规则系统的工作原理

这个规则系统非常简单。它由一个表示数字的类和一个表示规则的类组成。这两个类都有一个相应的集合类。这是表示数字的 `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 以简单、声明式的方式为这些数据创建引人入胜的可视化,这本身就很有吸引力。我希望你喜欢这篇文章,并且可能在此过程中学到了一两件事。祝你编码愉快!

© . All rights reserved.