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

Bullet Graphs - A Custom Control - WPF vs. Windows Forms

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (47投票s)

2008年10月10日

CPOL

19分钟阅读

viewsIcon

190327

downloadIcon

6428

本文比较了在WPF和Windows Forms中开发一款线业务控件——Bullet Graph。

目录

概述

“我们应该为线业务(LOB)用户界面(UI)代码使用Windows Presentation Foundation (WPF)还是Windows Forms (WinForms)?”

不幸的是,这个问题并不容易回答。WPF不仅仅是改进的Windows Forms;它与其前身是根本性的不同,提供的不仅仅是额外的润色。它是UI开发的一个全新范例!

本文通过两种技术直接比较开发自定义控件,从而突出两者之间的差异。

本文中用作案例研究的控件是Bullet Graph。

本文附带的源代码在WPF和WinForms中都提供了功能齐全的Bullet Graph控件,所以如果您只是想在应用程序中使用其中一个控件,请便。如果您想了解更多关于WPF的知识,请继续阅读……

背景

有很多文章和博客都在宣传WPF的优点,重点介绍了动画、丰富的UI模型和卓越的图形等功能。有关深入概述,请参阅文章:“Windows Presentation Foundation的十大UI开发突破”。虽然WPF对于外观和风格提供竞争优势的桌面应用程序来说似乎是显而易见的最佳选择,但诸如无样式控件、样式设置和可换肤界面等功能使WPF成为几乎理所当然的选择。

然而,对于样式不重要的“更严肃”的业务应用程序呢?对于线业务(LOB)应用程序,可用性、开发成本、稳定性和信息呈现的清晰度比样式更重要。事实上,过度的样式设置可能是有害的,并且通常不鼓励。WPF是否已准备好用于LOB仍然是一个热烈讨论的话题。有趣的是,Tim Sneath在他的近期关于.NET 3.5 SP1发布的公告中,对采用WPF的企业应用程序数量感到惊讶。

Windows Forms和WPF的编程模型存在根本性差异,这使得确定它们在LOB领域内的相对优势变得非常困难。通过直接比较WPF和Windows Forms开发同一个应用程序,可以非常深刻地理解它们之间的差异。几个月前,Josh Smith发表了一篇文章:“用Windows Forms和WPF创建同一个程序”,他就是这样做的。这篇文章以一个简单的应用程序为例,对比了这两种截然不同的技术在开发过程中的差异。虽然表面上这两种应用程序在语法上差异很大,但在使用用户控件、数据绑定和布局技术(WinForms的Panel和WPF的ItemsControl)方面,它们却有很多相似之处。基于这篇文章,我个人很难在这两种技术之间选出赢家,但这绝不是这篇文章的缺点或作者的失败。这仅仅是因为,不出所料,WPF和Windows Forms有很多相似之处。

在这篇文章中,我想做的是进行同样的尝试,用WinForms和WPF创建同一个程序,但要在LOB控件的背景下进行。我特意选择了一个不直接发挥WPF优势的控件,即没有位图效果、动画、换肤等……

当着手这项任务时,我没有一个明确的赢家,并且我一直试图保持公正。我也是一个务实主义者,只要能快速完成工作并取得高质量的最终结果,我乐于使用任何工具,无论是WinForms还是WPF,C#还是Java。

Bullet Graphs

本文选用的LOB控件是Bullet Graph,这是一个用于指示“不超过”目标的控件,由Stephen Few于2005年开发。Stephen是业务仪表盘领域的专家,他善于以简洁明了的方式传达复杂的关键业务数据,其著作“Information Dashboard Design”备受赞誉。他引入Bullet Graph是为了取代主导数字仪表盘的炫酷径向仪表和温度计,提供更清晰的替代方案。我不确定Stephen会对WPF的宣传功能集有什么看法,他甚至连饼图都不喜欢……我可不敢让他看我以前的CodeProject文章

下图显示了一些仪表控件(来自CodeProject文章)旁边的一些Bullet Graph。

gauge_and_bullet.jpg

毫不意外,您已经可以找到WPF的仪表控件,但却找不到它们更清晰的替代品——Bullet Graph。

Stephen Few发布了Bullet Graph的详细设计规范。WPF和WinForms控件尽可能地遵守此规范。我添加的任何附加功能,例如工具提示,都是合理的,因为它们传达了有用信息并提高了清晰度。他的规范如下所示。

bullet_spec.png

Windows Forms 控件

我将WinForms Bullet Graph实现为一个用户控件;这为您提供了一个空白画布,您可以在上面放置其他控件。然而,Bullet Graph的许多内容都依赖于显示的数据,因此无法直接在Visual Studio Designer中进行组装。替代方法是处理控件的Paint事件,使用提供的Graphics对象以编程方式绘制控件。这使得控件的构建成为一个纯手动过程。

我将不详细介绍Bullet Graph的每个组件如何在WinForms控件中组装,这部分非常直接明了,只需要一些简单的算法和绘图代码。这里是渲染刻度条、特征度量和对比度量部分的示例代码片段,仅供参考(当然,完整的源代码随本文提供)。

private void BulletGraph_Paint(object sender, PaintEventArgs e)
{
    Rectangle rec = bulletBackground.ClientRectangle;
    rec.Offset(bulletBackground.Location);
    float ratio = (float)rec.Width / (float)graphRange;

    // determine the tick spacing
    float tickSpacing = graphRange>0 ? 1 : -1;
    float[] multipliers = new float[] { 2.5f, 2, 2 };
    int multiplierIndex = 0;
    while (Math.Abs((float)graphRange / tickSpacing) > 7)
    {
        tickSpacing *= multipliers[multiplierIndex % multipliers.Length];
        multiplierIndex++;
    }

    // plot the axes
    float tickPosition = 0;
    while (Math.Abs(tickPosition) <= Math.Abs((float)graphRange))
    {
        float xPos = rec.Left + tickPosition * ratio;

        // bottom tick marks
        PointF p1 = new PointF(xPos, rec.Bottom);
        PointF p2 = new PointF(xPos, rec.Bottom + 3);
        e.Graphics.DrawLine(Pens.Black, transform(p1, rec), transform(p2, rec));

        // upper tick marks
        p1 = new PointF(xPos, rec.Top);
        p2 = new PointF(xPos, rec.Top - 3);
        e.Graphics.DrawLine(Pens.Black, transform(p1, rec), transform(p2, rec));
        
        // labels
        Rectangle labelRec = transform(new Rectangle((int)xPos - 15,
                                       rec.Bottom + 4, 30, 15), rec);
        StringFormat drawFormat = new StringFormat();
        drawFormat.Alignment = StringAlignment.Center;
        e.Graphics.DrawString(Convert.ToString(tickPosition),
                  this.Font, Brushes.Black, labelRec, drawFormat);

        tickPosition += tickSpacing;
    }

    // plot the featured measure
    float thirdOfHeight = (float)rec.Height / 3;
    int x = rec.Left;
    int y = (int) ((float)rec.Top + thirdOfHeight);
    int width = (int)((float)GetFeaturedMeasure() * ratio);
    int height = (int)thirdOfHeight;

    Rectangle controlRec = transform(new Rectangle(x, y, width, height), rec);

    featuredMeasureRect.Location = controlRec.Location;
    featuredMeasureRect.Size = controlRec.Size;

    // plot the comparative measure
    float sixthOfHeight = (float)rec.Height / 6;
    x = (int)((float)comparativeMeasure * ratio + rec.Left);
    y = (int)((float)rec.Top + sixthOfHeight);
    width = rec.Width / 80;
    height = (int)(sixthOfHeight*4);

    controlRec = transform(new Rectangle(x, y, width, height), rec);

    comparativeMeasureRect.Location = controlRec.Location;
    comparativeMeasureRect.Size = controlRec.Size;
}

正如您所见,这相当直接,尽管从代码的检查中很难确切地看出渲染和布局是如何实际执行的。值得注意的一个特性是,X轴上绘制的任何点都会通过一个转换函数,如下所示。

PointF transform(PointF point, Rectangle container)
{
    if (flowDirection == FlowDirection.LeftToRight)
        return point;
    else
    {
        float x = container.X * 2 + container.Width - point.X ;
        return new PointF(x, point.Y);
    }
}

这样做是为了支持Bullet Graph可以以从右到左的 LTR 布局方向呈现的要求,以突出对业务有负面影响的度量(例如,缺陷计数)。

我确实使用了一些技巧来开发这个控件,第一个是关于控件布局——理想情况下,控件应该是可调整大小的;第二个是关于工具提示的需求。Windows Forms有一些简单的技术可以通过控件的AnchorDock属性来管理布局。我真的想使用Anchor属性,该属性在容器调整大小时在控件周围保持恒定的边距,以便在保持刻度条尺寸固定的同时调整 Bullet Graph 的中心条。此外,WinForms中的工具提示绑定到控件;因此,为自定义渲染的图形显示工具提示并不容易。

为了解决这些问题,我在设计界面上创建了一些“虚拟”控件,如下图所示。

winforms_layout.png

两个黑色的矩形是特征度量和对比度量,虚线矩形是用于绘制定量刻度条的区域。这三个都是PictureBox类型的控件,并非因为我想渲染图片,仅仅是因为我想利用所有控件的一些通用功能。定量刻度条使用Anchor属性进行定位,而通常的红橙绿条则在代码隐藏文件中Paint事件处理程序中绘制。特征度量和对比度量PictureBox根据后台代码中可视化的数据进行定位,并相应地设置工具提示。

下图显示了几个不同大小的Bullet Graph,展示了控件如何缩放。

winforms_tooltip.png

Windows Forms提供了一些非常好的设计器支持功能,允许您为每个控件实例的运行时属性提供设计时界面。对于Bullet Graph,我使用了最基本的功能。控件属性作为公共语言运行时(CLR)属性公开,并指定了CategoryDescription属性。我特别喜欢的一个功能是ExpandableObjectConverter;当此属性存在时,它允许在属性网格中进行内联编辑,如下图所示。

[TypeConverter(typeof(ExpandableObjectConverter))]
public class QualitativeRange
{
    public int? Maximum {get; set;}

    public Color Color { get; set; }

    public override string ToString()
    {
        return "Maximum: " + 
       (Maximum.HasValue ? Maximum.ToString() : "") + ", Color: " + Color;
    }
}

private QualitativeRange[] qualitativeRanges = new QualitativeRange[] { };

[Description("The qualitative ranges to display")]
[Category("Bullet Graph")]
public QualitativeRange[] QualitativeRanges
{
    get { return qualitativeRanges; }
    set { qualitativeRanges = value; }
}

winforms_expandableobject.jpg

向 Windows Forms 自定义控件添加简单数据绑定再容易不过了,只需将DefaultBindingProperty属性添加到自定义控件即可。有了这个属性,您的控件的客户端只需导航到属性网格中的“Data”类别,然后就可以将DataSource连接到控件,如下图所示。

winforms_databinding.jpg

简单数据绑定非常直接;但是,您只能指定一个DefaultBindingProperty。复杂绑定和列表绑定的替代方法要复杂得多,如这篇优秀的CodeProject文章所示。

在Bullet Graph控件实现并添加数据绑定支持后,添加标准的图例格式将是一个有用的功能。与其将此添加到Bullet Graph控件本身,不如添加一个包含Bullet Graph实例和标准图例的单独用户控件更有意义。这种方法提供了更大的灵活性,允许用户选择是否使用带图例或不带图例的图表。自定义控件BulletGraphWithLegend如下图所示。

winforms_legend.png

此控件的代码隐藏非常普通,它只是定义了与Bullet Graph相同的属性,并将它们委托给它所包含的Bullet Graph。

[Category("Bullet Graph")]
public int ComparativeMeasure
{
    get { return bulletGraph1.ComparativeMeasure; }
    set { bulletGraph1.ComparativeMeasure = value; }
}

[Category("Bullet Graph")]
public int FeaturedMeasure
{
    get { return bulletGraph1.FeaturedMeasure; }
    set { bulletGraph1.FeaturedMeasure = value; }
}

这是必需的,因为Windows Forms没有WPF那样的属性值继承概念。

总而言之,下面的示例展示了已绑定到.NET对象的多个Bullet Graph。每个控件的配置通过Visual Studio Designer是一个简单的过程。

winforms_complete.png

WPF 控件

WPF控件也是一个用户控件,但这几乎是WPF与其Windows Forms对应物之间相似性的终点。因为本文位于CodeProject的WPF类别下,并且我预计其大多数读者对WPF而非Windows Forms感兴趣,所以我会更详细地介绍WPF Bullet Graph的开发。

我特意做了设计目标,尽量减少代码隐藏的使用,优先使用XAML提供的声明式方法、数据绑定以及在需要时使用ValueConverter

布局

BulletGraph用户控件的布局定义为一个由三行组成的Grid,如下所示。

<Grid.RowDefinitions>
    <!-- houses the upper scale bar -->
    <RowDefinition Height="*"/>
    <!-- houses the qualitative range bar, plus featured & 
                comparative measures -->
    <RowDefinition Height="4*"/>
    <!-- houses the lower scale bar with axis labels -->
    <RowDefinition Height="3*"/>
</Grid.RowDefinitions>

这会将UserControl的“画布”分成三个部分,比例为1:4:3。在接下来的部分,我们将看到各个组件是如何构建的。

定性范围

定性范围定义为我们UserControl的Collection Dependency Property,QualitativeRange类与WinForms控件中使用的相同(见上文)。它们使用以下XAML进行渲染。

<UserControl ... x:Name="bulletGraph" >
   ...
  <UserControl.Resources>    
    <ItemsPanelTemplate x:Key="gridItemControl">
      <Grid/>
    </ItemsPanelTemplate>
  </UserControl.Resources>
  ...
  <!-- qualitative range bar -->
  <ItemsControl x:Name="qualitativeRangeBar" Grid.Row="1"
                ItemsSource="{Binding Path=
            (c:BulletGraphWithLegend.QualitativeRanges),
                    ElementName=bulletGraph}"
                ItemsPanel="{StaticResource gridItemControl}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Rectangle HorizontalAlignment="Left">
          <Rectangle.Fill>
            <SolidColorBrush Color="{Binding Path=Color}"/>
          </Rectangle.Fill>
          <Rectangle.Width>
            <MultiBinding Converter="{StaticResource ScalingMultiConverter}">
              <!-- maximum value -->
              <Binding Path="(c:BulletGraphWithLegend.GraphRange)" 
                ElementName="bulletGraph"/>
              <!-- value to scale -->
              <Binding Path="Maximum"/>
              <!-- width to scale to -->
              <Binding Path="ActualWidth" ElementName="bulletGraph"/>
            </MultiBinding>
          </Rectangle.Width>
        </Rectangle>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
  ...
</UserControl>

上面的XAML定义了一个ItemsControl,其ItemsSource绑定到QualitativeRanges Dependency Property。ItemsPanel模板被指定,以便项目放置在Grid上,这仅仅是为了Grid在排列元素方面提供的灵活性。

每个QualitativeRange都渲染为一个矩形,其Fill颜色绑定到Color属性。此布局的有趣之处在于如何确定每个矩形的Width。矩形Width属性通过MultiBinding进行绑定,该绑定使用ScalingMultiConverter值转换器。该转换器接受三个参数:最大值、要缩放的值以及要缩放的值的范围;它执行简单的线性缩放。值转换器代码如下。这个简单的转换器在用户控件(以及我参与过的许多其他WPF项目)中广泛使用。

class ScalingMultiConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, 
            object parameter, CultureInfo culture)
    {
        if (!ValuesPopulated(values))
            return 0.0;

        double containerWidth = (double)values[2];
        double valueToScale = (double)values[1];
        double maximum = (double)values[0] ;

        return valueToScale * containerWidth / maximum;
    }

    private bool ValuesPopulated(object[] values)
    {
        foreach (object value in values)
        {
            if (value==null || value.Equals(DependencyProperty.UnsetValue))
                return false;
        }
        return true;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, 
                object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

上述XAML和值转换器的最终效果是,每个定量范围都渲染为一个矩形,其长度(即宽度)由其最大值指定。这种方法的唯一限制是必须按降序将范围提供给ItemsControl,否则其中一些范围将被隐藏。这对用户控件的客户端来说是一个不合理的要求,因此,我们在代码隐藏中对范围进行了适当的排序。这是Bullet Graph控件唯一的代码隐藏。

定性度量渲染如下图所示,并为托管的Grid启用了ShowGridLines

wpf_qualitativeranges.png

特征度量

用于渲染特征度量的XAML与定性范围的XAML非常相似,只有一个矩形如下渲染。

<!-- featured measure -->
<Rectangle x:Name="featuredMeasure" Grid.Row="1" Fill="Black" 
                HorizontalAlignment="Left"
           DataContext="{Binding ElementName=bulletGraph}"
           Height="{Binding Path=ActualHeight,
                    ElementName=qualitativeRangeBar,
                    Converter={StaticResource ScalingConverter},
                    ConverterParameter=3}">       
    <!-- the rectangle width defines the length of the featured measure bar -->
    <Rectangle.Width>
        <MultiBinding Converter="{StaticResource ScalingMultiConverter}">
            <Binding Path="(c:BulletGraphWithLegend.GraphRange)"/>
            <Binding Path="(c:BulletGraphWithLegend.FeaturedMeasure)"/>
            <Binding Path="ActualWidth"/>
        </MultiBinding>
    </Rectangle.Width>
    <Rectangle.ToolTip>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Featured Measure: "/>    
            <TextBlock Text="{Binding Path=
            (c:BulletGraphWithLegend.FeaturedMeasure)}"/>
        </StackPanel>
    </Rectangle.ToolTip>
</Rectangle>

矩形的高度绑定到定性范围的ActualHeightFrameworkElementActualHeightActualWidth属性在元素渲染期间的各种布局通道中得到更新。此绑定使用了更简单的缩放值转换器,该转换器具有固定的缩放因子。在这种情况下,可以看到特征度量将是定性范围条高度的三分之一。

特征度量条的长度(即宽度)再次使用ScalingMultiConverter,其中要缩放的值是Bullet Graph控件的FeaturedMeasure;还要注意Rectangle DataContext绑定到UserControl,避免了在Rectangle的作用域内重复使用ElementName绑定。

下图显示了添加了特征度量的Bullet Graph。

wpf_featuredmeasure.png

为特征度量添加工具提示是一项非常容易的任务,所有FrameworkElement子类都支持一个具有丰富内容模型的工具提示。

刻度条

<UserControl ... x:Name="bulletGraph" >
   ...
  <UserControl.Resources>
    <!-- for each item within the scale bar compute 
        the Canvas.Left property value -->
    <Style TargetType="ContentPresenter" x:Key="scaleBarItem">
      <Setter Property="Canvas.Left">
        <Setter.Value>
          <MultiBinding ConverterParameter="-0.5" 
        Converter="{StaticResource ScalingMultiConverter}">
            <!-- maximum value -->
            <Binding Path="(c:BulletGraphWithLegend.GraphRange)" 
        ElementName="bulletGraph"/>
            <!-- value to scale -->
            <Binding Path="."/>
            <!-- range to scale over -->
            <Binding Path="ActualWidth" ElementName="bulletGraph"/>
          </MultiBinding>
        </Setter.Value>
      </Setter>
    </Style>
  </UserControl.Resources>
  ...
  <!-- top scale bar -->
  <ItemsControl x:Name="scaleBarTop" Grid.Row="0"
                ItemContainerStyle="{StaticResource scaleBarItem}" 
            ItemsPanel="{StaticResource canvasItemControl}"
                ItemsSource="{Binding Path=(c:BulletGraphWithLegend.GraphRange),
                                      ElementName=bulletGraph,
                                      Converter={StaticResource ScaleBarConverter}}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Canvas>
          <Line Y2="{Binding Path=ActualHeight, ElementName=scaleBarTop}" 
                Stroke="Black" X1="1" X2="1" Y1="0"/>
        </Canvas>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
...
</UserControl>

同样,ItemsControl被用来渲染刻度条,其中每个Item都是一个单独的刻度线。刻度线本身是一个简单的线条,通过绑定来确保其高度占据包含它的行的整个高度。有趣的部分在于如何定位每个刻度线的X位置。

ItemsControl绑定到Bullet Graph的GraphRange属性,这是一个标量值。在此绑定中使用的ScaleBarConverter将此标量转换为一个数组,该数组用于定位每个刻度线。例如,GraphRange为300将被转换为以下数组:{0, 50, 100, 150, 200, 250, 300}。

ItemContainerStyle属性设置ItemContainer使用的样式,它是控件中每个Item的主机容器。给定的样式设置了容器的Canvas.Left附加属性,结果是每个画布在刻度条中都正确放置。定位项目的责任已委托给ItemsControl;因此,我们所要做的就是绘制一条垂直线来渲染刻度线。

较低的刻度条也使用相同的ItemContainerStyle,其中包含刻度线和标签。同样,定位刻度线/标签的责任被分解开来,因此项目模板只需绘制刻度和标签。有关详细信息,请参阅文章附带的源代码。

下图显示了带有两个刻度条的Bullet Graph。刻度条标签的边框显示了ItemsControl如何用于定位每个项目。

wpf_scalebar.png

对于可能感兴趣的读者,将刻度条最大值转换为刻度线数组的算法如下。

class ScaleBarConverter : IValueConverter 
{
    // the maximum number of ticks which should be rendered
    private static int MaximumNumberOfTicks = 7;

    // multipliers which, when applied is sequence, 
    //provide an aesthetic scalebar
    private static double[] Multipliers = new double[] { 2.5f, 2, 2 };

    public object Convert(object value, Type targetType,
                          object parameter, CultureInfo culture)
    {
        if (value == null || value.Equals(0.0))
            return null;

        double range = (double)value;
        
        // determine the tick spacing
        double tickSpacing = range > 0 ? 1 : -1;            
        int multiplierIndex = 0;
        while (Math.Abs(range / tickSpacing) > MaximumNumberOfTicks)
        {
            tickSpacing *= Multipliers[multiplierIndex % Multipliers.Length];
            multiplierIndex++;
        }

        // determine the total number of scalebar ticks
        int tickCount = (int)(range / tickSpacing) + 1;

        // construct the scale
        double[] scale = new double[tickCount];
        for (int i = 0; i < tickCount; i++)
            scale[i] = i * tickSpacing;

        return scale;
    }

    public object ConvertBack(object value, Type targetType,
                              object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

该算法通过迭代方法确定刻度间距。从间距为一开始,在每次迭代中,它确定如果使用该间距将有多少个刻度,并将其与最大允许数量(在本例中为硬编码的七个)进行比较。如果测试失败,则刻度间距会增加一定的量。正是这些由乘数因子数组确定的量,产生了美观的刻度条。

下面的示例显示了如何计算最大值为300的刻度条的刻度间距,其中“m”表示乘数数组。

1 => gives 300 tick marks 

1 * (m[0]=2.5)   = 2.5 =>  120 tick marks

2.5 * (m[1]=2)   = 5   =>  60 tick marks

5 * (m[2]=2)     = 10  =>  30 tick marks

10 *(m[0]=2.5)   = 25 =>  12 tick marks

25 * (m[1]=2)    = 50  =>  6 tick marks

间距为50单位是第一个导致少于7个刻度标记的结果;因此,选择它作为刻度间距。

收尾工作

WPF Bullet Graph控件也嵌套在另一个UserControlBulletGraphWithLegend中,方式与WinForms控件非常相似。然而,WPF具有属性继承的概念,这意味着不需要在容器中定义相同的属性来形成“包装器”。

Bullet Graph可以以从右到左的 LTR 布局方向渲染的要求,通过在控件的父Grid上应用RenderTransform可以轻松实现。然而,这会导致所有子元素都被镜像,包括标签文本!一个简单的解决方法是第二次将相同的变换应用于刻度标签,从而导致文本被镜像两次,因此方向正确。有关详细信息,请参阅附带的源代码。

WPF还包括设计器支持,具有属性描述和类别。

最后,WPF的数据绑定支持是必然的。Bullet Graph的所有属性都公开为依赖属性,允许客户端绑定到其中任何一个。

完成的WPF Bullet Graph如下图所示,WinForms实现直接显示在下方进行比较。

wpf_complete.png

winforms_complete.png

摘要

从上图可以看出,WPF和WinForms的实现看起来非常相似,但我希望从上面的代码片段中可以看出,它们的实现差异很大。在本节中,我将简要介绍其中一些差异。

WPF和WinForms之间一个显著的差异是,WPF提供了更多的控件和内容布局机制。Windows Forms和WPF都共享一些常见的布局机制,通过它们各自的Panel类允许表格、流式(及更多)布局控件。然而,在WPF中,布局可以通过数据绑定进一步增强,这允许您表达复杂的​​关系,如“使元素A的高度是元素B的三分之二,并将其定位在其左下角”。这是WPF新手经常忽略的一点;数据绑定不限于业务数据(例如,姓名、地址、订单数量),它可以用于将任何一个依赖属性绑定到另一个,这对于布局非常有用(顺便说一句,我经常希望CSS也能实现这一点;如果例如您可以让一个DIV和另一个DIV一样高,那么许多被用来实现这一点的hack就可以消失了)。这种优越的布局机制的一个直接结果是,WPF Bullet Graph不包含其WinForms对应控件中的任何算法代码,使其更简洁、更清晰、更易于理解。

WPF通过高度灵活的ItemsControl增加了另一个维度。Dr. WPF的ItemsControl Intelligence Quotient清楚地展示了这种超灵活控件的各种用法。

在清晰度方面,WPF并非总是赢家。XAML语法可能非常冗长,部分原因在于它是用XML表示的。此外,与作为运行时一部分的CLR属性不同,依赖属性纯粹是实现性的。这在一定程度上解释了为什么创建依赖属性及其相应的CLR包装器(严格来说并非强制;但是,Visual Studio编辑器不会识别没有CLR包装器的属性)如此冗长。这几乎让我怀念宏!

就个人而言,我认为要使XAML清晰易读,需要比C#等效项更多的关注。在C#中,问题被分解为变量和方法,如果写得好,它们是自文档化的。然而,XAML表达了更复杂的结构,其中属性是继承的,并且绑定用于表达复杂的内部关系。很容易陷入混乱的境地。例如,需要非常小心处理继承属性,如DataContext,对逻辑树根部附近这个重要属性的更改可能会对元素的后代的属性绑定产生重大但并非显而易见的影响。

最后,WPF和WinForms控件之间还有一个不同之处,我认为这并不利于WPF版本。请比较下面的两个控件的特写。

zoomed.png

我们可以看到WPF控件正在对文本进行抗锯齿处理。许多人认为用于文本抗锯齿的算法效果不佳,当文本动画时效果最差,会产生一种奇怪的“沉降”效果。此外,无法禁用抗锯齿。您可以在MSDN WPF论坛一个最激烈辩论的帖子中关注这场辩论。

在上面的图像中,您可以看到刻度线也被以一种相当模糊的方式进行了抗锯齿处理,而特征度量和定量范围则相当清晰。这是因为所有UIElement子类共有的SnapsToDevicePixels属性可以用来确保WPF使用的设备无关单位“捕捉”到设备像素。然而,SnapsToDevicePixels似乎对ItemsControls无效。这个问题是MSDN WPF论坛上一个尚未得到回答的帖子的主题,请回复明信片……

结论

本文旨在就Bullet Graph控件的开发对WPF和WinForms进行比较。我希望强调的差异能够帮助其他人决定他们是否希望在下一个项目中选择WPF。

这篇文章比我最初设想的要长得多,部分原因是我个人对WPF的兴趣和喜悦。我确定我提到了要保持公正?现在,我把这一点搞砸了!

WPF无疑为开发人员提供了比其前身强大得多的框架,并且社区仍在不断探索如何充分发挥这种力量。

回到早些时候的问题:“WPF是否已准备好用于LOB?”我将给出唯一合理的答案:“这取决于”。对我来说,可以肯定的是,抛开图形效果不谈,WPF为LOB应用程序提供了许多优势,包括本文中看到的灵活数据绑定和高级布局技术,以及一些本文未提及的功能,例如数据模板。这些架构模式肯定会对任何应用程序(LOB或其他)带来好处。

现在,请原谅我,我要去看看我是否能完全在数据绑定中实现Conway的生命游戏……

(感谢我的同事David Pentney建议以Bullet Graph作为本文的主题。)

历史

  • 2008年10月13日 - 对WinForms布局机制的细微修正。
  • 2008年10月10日 - 首次发布。
© . All rights reserved.