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

MathConverter - 如何在 XAML 中进行数学运算

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (15投票s)

2011年8月10日

Apache

7分钟阅读

viewsIcon

82571

downloadIcon

2593

支持算术表达式的WPF/Silverlight值转换器。

概述

MathConverter 是一个用于WPF/Silverlight的算术转换器。它允许您执行如下操作:

<RotateTransform Angle="{Binding Text, ElementName=Seconds, 
           Converter={ikriv:MathConverter}, ConverterParameter=x*6}" />

甚至可以这样:

<RotateTransform>
    <RotateTransform.Angle>
        <MultiBinding Converter="{ikriv:MathConverter}" 
                 ConverterParameter="x*30 + y/2">
            <Binding Path="Hours" />
            <Binding Path="Minutes" />
        </MultiBinding>
    </RotateTransform.Angle>
</RotateTransform>

换句话说,它可以在XAML中直接执行简单的计算,从而无需混乱您的视图模型或编写一次性的值转换器。

背景

在处理我的XAML文件时,我偶尔会遇到需要在XAML中直接进行一些简单计算的需求。例如,如果我希望某个宽度正好是另一个宽度的三分之一,或者旋转角度是某个特定值的某个倍数怎么办?这个问题可以通过多种方式解决。宽度问题通常通过边距、填充或网格来更好地处理。其他时候,我会将计算放入视图模型中。还有些时候,我会创建一个专门的绑定转换器来为我进行算术运算。

为什么需要另一个转换器?

当然,我不是唯一面临这个问题的人。如何在XAML中进行数学计算这个问题已经被提出(例如在StackOverflow上)并且已经有了答案(在MSDN博客中)。

Lester Lobo在他的博客中提供了不止一个转换器:他自己的ArithmeticConverter和Douglas Stockwell的JScriptConverter。在某些情况下,它们都可以完成工作,但它们并不完美。ArithmeticConverter过于简单。它只能执行一个数学运算,并且只能接受一个参数。另一方面,JScriptConverter过于强大。由于您可以使用完整的JScript语言,因此它几乎是图灵完备的,但它涉及动态程序集编译,而且它不适用于Silverlight。动态程序集编译速度慢,会弄乱用户的磁盘,并可能带来安全隐患。说实话,对于大多数XAML数学计算的需求来说,这似乎有点小题大做。

总的来说,您通常不想在XAML中真正开始编程。一旦有了这些智能的可编程转换器,就很容易开始编写循环和条件,访问文件等等,无所不能。这并不总是明智之举。XAML被设计用于描述GUI控件的视觉表示。作为一种编程语言,它可能不会表现出色:我们有更好的工具可以进行调试、重用、访问控制等。

Teleric有一个名为Telerik.Windows.Controls.Carousel.ArithmeticValueConverter的类。不幸的是,该类的文档非常有限。根据我在网上找到的示例判断,它甚至比Lester的转换器功能更有限。它接受单个值作为参数,并可能将其添加到参数中(或者将其与参数相乘,我不确定)。

所有这些促使我创建了自己的转换器,它能够执行任意算术运算,可以在Silverlight中工作,并且不涉及动态编译。是的,这意味着我不得不自己编写解析器 :) 哦,我的大学时光,那时很有趣,现在也很有趣。

使用MathConverter

实际上,MathConverter有两个版本:一个用于WPF,一个用于Silverlight。它们使用#if !SILVERLIGHT预处理器指令从同一源代码条件编译。原因 twofold

  • Silverlight不支持多重绑定。
  • Silverlight不支持标记扩展。

标记扩展支持

没有标记扩展,您必须在资源中定义转换器(<ikriv:MathConverter x:Key="name" />),然后将其引用为{Binding Converter={StaticResource name} ...}。有了标记扩展支持,您可以简单地写{Binding Converter={ikriv:MathConverter} ...},这将为您创建一个转换器实例。标记扩展仅在WPF中可用。

支持的表达式

ConverterParameter必须包含一个有效的算术表达式。支持四种算术运算、一元加、一元减和括号。遵循常规的优先级规则:2+2*2返回6

对于常规绑定,单个参数可以引用为xa{0}。这些符号可以互换使用。参数不必出现在表达式中。如果不存在,转换器将始终返回相同的值。单参数表达式示例

  • 42.8
  • 2+2*2
  • a+1x+1相同,与{0}+1相同
  • (x-1)/(x+1)
  • -x*(x+9.5)

对于多重绑定,第一个参数可以引用为ax{0}。第二个参数是by{1}。第三个参数是cz{2}之一,第四个是dt{3}。第五个及之后的参数只能通过数字形式{n}引用,其中n是零基参数编号。Silverlight不支持多重绑定。以下是一些多重绑定表达式的示例

  • 42.8
  • a+b*c
  • (x+y)/(z-1)
  • {0}+2*{1}+3*{2}*2.7818*{3}+3.1416*{4}

所有计算均以decimal类型执行。然后将结果转换为目标类型,目标类型可以是stringintdoublelongdecimal。如果计算导致错误(表达式格式错误、溢出、除以零、目标类型未知),则返回值是DependencyPropety.UnsetValue,并且异常文本会写入Visual Studio输出窗口,例如:“MathConverter: error parsing expression 'x*6+'. Unexpected end of text at position 4"

示例项目

包含该转换器、一个WPF示例、一个Silverlight示例和单元测试的示例项目。下面演示了Silverlight示例应用程序。

演示项目包含下面演示的MathConverter示例Silverlight应用程序。时钟指针的旋转角度在XAML中使用MathConverter计算。

Silverlight

<!-- Silverlight -->
<UserControl ...>
    <UserControl.Resources>
        <ikriv:MathConverter x:Key="MathConverter" />
    </UserControl.Resources>
    <Grid ...>
        <TextBox Name="Hours" ... />
        <TextBox Name="Minutes" ... />
        <TextBox Name="Seconds" ... />
 
        <!-- small hand (hours) -->
        <Line X1="0" Y1="0" X2="0" Y2="-35" 
                  Stroke="Black" StrokeThickness="4">
            <Line.RenderTransform>
                <RotateTransform Angle="{Binding Text, ElementName=Hours,
                   Converter={StaticResource MathConverter}, ConverterParameter=x*30}" />
            </Line.RenderTransform>
        </Line>
 
        <!-- big hand (minutes) -->
        <Line X1="0" Y1="0" X2="0" Y2="-40" 
                Stroke="Black" StrokeThickness="3">
            <Line.RenderTransform>
                <RotateTransform Angle="{Binding Text, ElementName=Minutes,
                  Converter={StaticResource MathConverter}, ConverterParameter=x*6}" />
            </Line.RenderTransform>
        </Line>
 
        <!-- seconds hand -->
        <Line X1="0" Y1="0" X2="0" Y2="-40" 
                   Stroke="Black" StrokeThickness="1">
            <Line.RenderTransform>
                <RotateTransform Angle="{Binding Text, ElementName=Seconds,
                  Converter={StaticResource MathConverter}, ConverterParameter=x*6}" />
            </Line.RenderTransform>
        </Line>
    </Grid>
</UserControl>

WPF

Silverlight不支持多重绑定,因此小时指针的位置仅取决于整点小时,而不取决于分钟。也就是说,在10:59:59时,它仍然会精确地指向10点。在WPF版本中,我们可以通过结合小时和分钟来做得更好,因此在10:59:59时,小时指针会接近指向11点。

<!-- WPF -->

<!-- small hand (hours) -->
<Line X1="0" Y1="0" X2="0" Y2="-35" 
            Stroke="Black" StrokeThickness="4">
    <Line.RenderTransform>
        <RotateTransform>
            <RotateTransform.Angle>
                <MultiBinding Converter="{ikriv:MathConverter}" 
                              ConverterParameter="x*30 + y/2">
                    <Binding Path="Text" ElementName="Hours" />
                    <Binding Path="Text" ElementName="Minutes" />
                </MultiBinding>
            </RotateTransform.Angle>
        </RotateTransform>
    </Line.RenderTransform>
</Line>

实时Silverlight演示

一个实时Silverlight演示可在此处获得。

转换器代码内部

解析器

转换器的核心是解析器。它的任务是解析表达式文本,例如x*30 + y/2,并将其转换为IExpression类型的对象,该对象代表计算树(见下文)。因此,解析器的公共接口只有一个方法

class Parser
{
    public IExpression Parse(string text);
}

Parser是一个经典的递归下降解析器。它使用简单的算术表达式语法,其变体可以在几乎所有的编译器书中找到(例如,Aho等人,第77页)。

Expression ::= Expression + Term
Expression ::= Experssion - Term
Term ::= Term * Factor
Term ::= Term / Factor
Factor ::= constant
Factor ::= variable
Factor ::= -Factor
Factor ::= +Factor
Factor ::= (Expression)

语法的每个非终结符符号都由一个相应的解析方法处理:Parser.ParseExpression()Parser.ParseTerm()Parser.ParseFactor()。解析器通过跳过空格和使用RegEx解析十进制数来充当原始的词法分析器。

表达式树

解析器产生一个实现IExpression接口的单一对象,该接口定义如下:

interface IExpression
{
    decimal Eval(object[] args);
}

传递给Eval方法的数组包含表达式中涉及的变量的具体值。例如,对于表达式x*30 + y/2,我们期望在args[0]中找到x的值,在args[1]中找到y的值。对于常规绑定,将只有一个参数,即源属性的值。对于多重绑定,可能有多个参数,它们是源属性的值。

IExpression接口有几个具体的实现:

Parser classes

  • Constant.Eval(args)始终返回相同的值,例如30.0
  • Variable.Eval(args)返回args[index],其中index是变量编号(x=0,y=1,依此类推)。
  • Negate类持有另一个表达式。Negate.Eval()评估该表达式并否定结果。
  • BinaryOperation持有两个表达式和一个操作函数(乘法、除法、加法等)。BinaryOperation.Eval()评估内部表达式并将函数应用于结果。

对于表达式文本x*30+y/2,解析器将生成以下表达式树:

Expression tree

之后,转换器只需调用顶级表达式的Eval()并提供参数即可。

类型安全

转换器使用decimal类型执行所有计算。字面量常量使用decimal.TryParse()转换为decimal。变量(源属性)值使用System.Convert.ToDecimal()转换。然后可以将输出值转换为decimalstringlongintdouble。不支持其他目标类型。

标记扩展

在WPF下,转换器派生自MarkupExtension类,该类允许使用{ikriv:MathConverter}语法在XAML中创建转换器实例,其中ikriv是映射到CLR命名空间Ikriv.Wpf的XAML命名空间。Silverlight目前不支持标记扩展,因此您需要在资源中定义转换器实例。

结论

我试图使代码尽可能自解释和简单。注释不多,但方法都很短,名称也很具描述性。我解析的表达式语法非常小,如果不是微不足道的话。您不必了解解析理论即可理解解析器的工作原理。您可能想扩展语法以支持函数调用、数组参数等,但这远远超出了我需要的范围。希望您喜欢这个转换器,并发现它很有价值。

© . All rights reserved.