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






4.89/5 (15投票s)
支持算术表达式的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
。
对于常规绑定,单个参数可以引用为x
、a
或{0}
。这些符号可以互换使用。参数不必出现在表达式中。如果不存在,转换器将始终返回相同的值。单参数表达式示例
42.8
2+2*2
a+1
与x+1
相同,与{0}+1
相同(x-1)/(x+1)
-x*(x+9.5)
对于多重绑定,第一个参数可以引用为a
、x
或{0}
。第二个参数是b
、y
或{1}
。第三个参数是c
、z
或{2}
之一,第四个是d
、t
、{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
类型执行。然后将结果转换为目标类型,目标类型可以是string
、int
、double
、long
或decimal
。如果计算导致错误(表达式格式错误、溢出、除以零、目标类型未知),则返回值是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
接口有几个具体的实现:
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
,解析器将生成以下表达式树:
之后,转换器只需调用顶级表达式的Eval()
并提供参数即可。
类型安全
转换器使用decimal
类型执行所有计算。字面量常量使用decimal.TryParse()
转换为decimal
。变量(源属性)值使用System.Convert.ToDecimal()
转换。然后可以将输出值转换为decimal
、string
、long
、int
或double
。不支持其他目标类型。
标记扩展
在WPF下,转换器派生自MarkupExtension
类,该类允许使用{ikriv:MathConverter}
语法在XAML中创建转换器实例,其中ikriv
是映射到CLR命名空间Ikriv.Wpf
的XAML命名空间。Silverlight目前不支持标记扩展,因此您需要在资源中定义转换器实例。
结论
我试图使代码尽可能自解释和简单。注释不多,但方法都很短,名称也很具描述性。我解析的表达式语法非常小,如果不是微不足道的话。您不必了解解析理论即可理解解析器的工作原理。您可能想扩展语法以支持函数调用、数组参数等,但这远远超出了我需要的范围。希望您喜欢这个转换器,并发现它很有价值。