在WPF中使用ItemsControl控件来实现线状图控件(一)

简介:

在前面的文章里面提到了如何使用ItemsControl编写一个直方图控件(还没有写完,今天因为有急用就先写线状图的编写方法了),因此在阅读这篇文章之前,推荐先阅读下面几篇文章:

1.       使用ListBox控件来实现直方图控件(一)

2.       使用ListBox控件来实现直方图控件(二)

3.       使用ListBox控件来实现直方图控件(三)

4.       使用ListBox控件来实现直方图控件(四)

5.       使用ListBox控件来实现直方图控件(五)

按照在()里面介绍过的方法 ,程序是把直方图后面的数据转换成矩形的高度,对于线状图来说,那就是把线状图后面的数据转化成折线上点的高度就可以了。因此我们的Converter可以复用直方图控件里面的Converter

internal class ChartValueToHeightConverter : IValueConverter

{

    #region IValueConverter Members

 

    public object Convert(object value, Type targetType,

        object parameter, CultureInfo culture)

{

    // 复用直方图控件里面的Converter唯一的问题是,当时我在Converter函数里面

    // 硬编码成Histogram类型了,所以这里我只好暂时先将它改成LineChart类型。

    // 如果要做一个通过的Converter的话,问题也不是很大,只要把下面用到的TickLabelHeight

    // Maximum, Minimum属性都放在ChartBase类里面就好了。

        LineChart chart =

            ((ObjectDataProvider)parameter).ObjectInstance as LineChart;

 

        return (chart.ActualHeight - chart.TickLabelHeight) * 0.9 * (double)value

              / (chart.Maximum - chart.Minimum);

    }

 

    public object ConvertBack(object value, Type targetType,

        object parameter, CultureInfo culture)

    {

        throw new NotImplementedException();

    }

 

    #endregion

}

 

ItemsControlItemTemplate换成下面这个DataTemplate就可以把点画出来了:

 

<Grid x:Name="ChartArea" Width="20" VerticalAlignment="Bottom">

    <Grid.RowDefinitions>

        <RowDefinition Height="Auto"/>

        <RowDefinition Height="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type local:LineChart}}, Path=TickLabelHeight}"/>

    </Grid.RowDefinitions>

    <Canvas Grid.Row="0">

        <Rectangle Width="5" Height="5" Fill="{Binding Path=Fill}" Canvas.Left="0"x:Name="ChartPoint"

                   Cursor="Cross"

                   local:ConnectLinker.Linker="{Binding ElementName=connectLinker}"

                   ToolTip="{Binding Path=Value}">

            <Canvas.Bottom>

                <Binding Path="Value"

                         Converter="{StaticResource chartValueToHeightConverter}"

                         ConverterParameter="{StaticResource LineChartReference}">

                </Binding>

            </Canvas.Bottom>

        </Rectangle>

    </Canvas>

    <Label Content="{Binding Path=Category}" Padding="0" Margin="0" Grid.Row="1" />

</Grid>

 

下面是效果图:

 

现在剩下的问题是,如何把这些点连接起来―也就是说如何根据这些点绘制一个折线?WPF提供了Path控件用来绘制折线、曲线等线形,Path控件要求输入一个Geometry实例数组赋值给Data属性,而每一个Geometry(我们只考虑折线的情况)都要求输入一个起始点,和线段的定义组合―Segments。也就是说,Path控件要求一个连续的线段数组定义,不能从多个点直接定义出来,即WPF没有提供方法让我们以类似下面的代码直接创建一个折线:

 

Path path = new Path();

path.Data = new PathGeometry(new Point[] { point1, point2, point3, ..., point 100 });

 

当然啦,通过一些小技巧应该还是可以达到类似上面代码的功能的,例如在Xaml里面,我们就可以用下面的形式来创建一个Path对象――但这篇文章不会去探讨这个技巧。

<Path Stroke="Black" StrokeThickness="1" 

      Data="M 10,50 L 200,70" />

 

由于我们是通过ItemsControl.ItemTemplate一个一个地把点画出来的,而绘制线状图的折线段只需要获取定义折线的所有点的坐标就可以了,现在的问题是,如何将ItemsControl里面的一个个独立的点通过一种方式联系起来?一种方案是重写ItemsControlOnRender函数,然后遍历ItemsControl的每一个Item,通过某种复杂的办法查询出Item里面的点,然后在OnRender函数里面自己写代码绘制出折线段。这种办法有一些缺点:

 

1.       OnRender函数重写可能比较困难,因为我们只能获取到每一个点(实际是一个Rectangle实例)相对于Canvas的高度,因此我们在画线的时候需要考虑到线状图里面的X轴坐标的高度。

2.       对于ItemsControl,你要找到里面使用DataBinding技术生成的控件,那是相当的麻烦,要使用ItemsControl.ItemContainerGenerator里面的好几个不同的函数,并且还要和VisualTreeHelper.GetChildren等几个函数组合使用才能找到DataBinding技术生成的Rectangle实例――而且你还不一定能够拿得到(请参见麻烦三)。

3.       ItemsControl会有一个优化,实际上VirtualizeStackPanel的一个优化,就是它不会显示不在可见区域(具体一点说是,用户还没有看到过)的数据,就是说如果这个数据用户还没有看到的话――例如这个数据是显示在屏幕外的,那么DataBinding不会为这个数据计算DataTemplate而生成相应的控件,这样就会导致你使用麻烦二里面描述的方法对一些数据会获取到null引用。 

 

看起来通过现有控件或者通过ItemsControl本身暂时很难做到,于是我们只好自己写一个控件来帮LineChart控件将屏幕上的点连起来,这个我们可以通过自定义一个内容控件并且定义一个附加属性(Attached Property)来实现这个功能。至于为什么可以这样做,我会在后面的文章里面讲。下面是解决方案:

 

<UserControl x:Class="TestLineChart.LineChart"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:TestLineChart">

    ...

    <Grid>

        <local:ConnectLinker x:Name="connectLinker"                            

                             ... >

            <ItemsControl ...>

                ...

                <ItemsControl.ItemTemplate>

                    <DataTemplate>

                        <Grid x:Name="ChartAreaWidth="20"VerticalAlignment="Bottom">

                            ...

                                <Rectangle ...

                                           local:ConnectLinker.Linker="{Binding ElementName=connectLinker}">

                            ...

                        </Grid>

                    </DataTemplate>

                </ItemsControl.ItemTemplate>

            </ItemsControl>

        </local:ConnectLinker>

    </Grid>

</UserControl>

 

下面是这个控件的源代码:

 

using System.Collections.Generic;

using System.Windows.Controls;

using System.Windows;

using System.Windows.Media;

using System.Windows.Shapes;

 

namespace TestLineChart

{

    public class ConnectLinker : ContentControl

    {

        public static ConnectLinker GetLinker(DependencyObject obj)

        {

            return (ConnectLinker)obj.GetValue(LinkerProperty);

        }

 

        public static void SetLinker(DependencyObject obj, ConnectLinker value)

        {

            obj.SetValue(LinkerProperty, value);

        }

 

        public static readonly DependencyProperty LinkerProperty =

            DependencyProperty.RegisterAttached("Linker",

                typeof(ConnectLinker), typeof(ConnectLinker),

                new UIPropertyMetadata(nullnewPropertyChangedCallback(LinkChangedCallBack)));

       

        private List<UIElement> Points { getset; }

 

        public ConnectLinker()

        {

            Points = new List<UIElement>();

        }

 

        private static void LinkChangedCallBack(DependencyObject sender,DependencyPropertyChangedEventArgs e)

        {

            ConnectLinker linker = e.NewValue as ConnectLinker;

            linker.Points.Add(sender as UIElement);

            linker.InvalidateVisual();

        }

 

        protected override void OnRender(DrawingContext drawingContext)

        {

            PathGeometry geometry = new PathGeometry();

            Point? previousPoint = null;

            int index = 0;

            Brush brush = null;

            for (int i = 0; i <= Points.Count - 1; ++i)

            {

                Rectangle e = Points[i] as Rectangle;

                if (brush == null)

                {

                    brush = e.Fill;

                }

 

                if (previousPoint == null)

                {

                    previousPoint = new Point(Canvas.GetLeft(e), Canvas.GetBottom(e));

                }

                else

                {

                    PathFigure figure = new PathFigure();

 

                    LineSegment segment = new LineSegment();

                    // Canvas parent = LogicalTreeHelper.GetParent(e) as Canvas;

                    Grid parent = e.FindAncestor<Grid>();

                    Point point = new Point(parent.ActualWidth * index,Canvas.GetBottom(e));

                    segment.Point = point;

                    figure.StartPoint = previousPoint.Value;

 

                    PathSegmentCollection segments = new PathSegmentCollection();

                    segments.Add(segment);

                    figure.Segments = segments;

 

                    previousPoint = point;

                    geometry.Figures.Add(figure);

                }

 

                index++;

            }

 

            if (brush != null)

            {

                drawingContext.DrawGeometry(brush, new Pen(brush, 2), geometry);

            }

        }

    }

 

    public static class Extension

    {

        public static T FindAncestor<T>(this DependencyObject element) where T :DependencyObject

        {

            DependencyObject parent = VisualTreeHelper.GetParent(element) asDependencyObject;

            while (parent != null)

            {

                if (parent is T)

                    return parent as T;

                else

                    parent = VisualTreeHelper.GetParent(parent) as DependencyObject;

            }

 

            return null;

        }

    }

}

 

这样我们就得到类似下图的效果,但是……有一些问题,这个线怎么没有经过点呢?


 


仔细分析,原来线是画反了,因为默认
Window的坐标原点是屏幕的最上角,也就是说在折线的时候,是以屏幕的左上角为原点绘制的,而画点的时候,却是以Canvas的左下角为原点绘制的――因为<Gridx:Name="ChartArea" Width="20" VerticalAlignment="Bottom">这一行已经明确告诉我们这个事实了。没关系,难不倒我们,只要把最终的线段在中心位置反转一下就可以了-即把最后输出线段的代码修改一下下就好了:

            if (brush != null)

            {

                drawingContext.PushTransform(new ScaleTransform(1, -1, 0, ActualHeight / 2));

                drawingContext.DrawGeometry(brush, new Pen(brush, 2), geometry);

            }

呃,还是有一点小问题,线段的走向是正确了,但是怎么跟点还是有那么一点点距离呢?

 

 

这是因为我们在反转线段的时候,没有考虑到X轴还是有一定的高度的,而刚才反转得时候,我们是根据整个线状图高度的一半进行反转的,没有考虑X轴高度的问题,这个问题好解决,只要在ConnectLinker类里面加一个属性TolerranceHeight,并且在Xaml里面把值赋上就可以了:

 

 

        internal double TranslateTolorrence

        {

            get { return (double)GetValue(TranslateTolorrenceProperty); }

            set { SetValue(TranslateTolorrenceProperty, value); }

        }

 

        internal static readonly DependencyProperty TranslateTolorrenceProperty =

            DependencyProperty.Register("TranslateTolorrence"typeof(double),typeof(ConnectLinker), new UIPropertyMetadata(0.0d));

 

        ...

 

        protected override void OnRender(DrawingContext drawingContext)

        {

            ...

 

            if (brush != null)

            {

                drawingContext.PushTransform(new ScaleTransform(1, -1, 0, ActualHeight / 2 - TranslateTolorrence / 2));

                // 把线段稍微上移一些(根据点的高度),这样使得线段看起来是从点的中心穿过一样。

                drawingContext.PushTransform(new TranslateTransform(3, 0));

                drawingContext.DrawGeometry(brush, new Pen(brush, 2), geometry);

            }

        }

}

 

Xaml里面的修改

 

<local:ConnectLinker x:Name="connectLinker"

                     TranslateTolorrence="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:LineChart}}, Path=TickLabelHeight}">

 ...

</local:ConnectLinker>

 

最后终于得到想要的线状图效果了:

 



未完待续……

这里是完整的源代码:/Files/killmyday/TestLineChart.zip



本文转自 donjuan 博客园博客,原文链接:http://www.cnblogs.com/killmyday/archive/2009/07/27/1532230.html   ,如需转载请自行联系原作者

相关文章
|
6月前
|
C# 开发者 Windows
基于Material Design风格开源、易用、强大的WPF UI控件库
基于Material Design风格开源、易用、强大的WPF UI控件库
385 0
|
6月前
|
C# 容器
浅谈WPF之UniformGrid和ItemsControl
在日常开发中,有些布局非常具有规律性,比如相同的列宽,行高,均匀的排列等,为了简化开发,WPF提供了UniformGrid布局和ItemsControl容器,本文以一个简单的小例子,简述,如何在WPF开发中应用UniformGrid和ItemsControl实现均匀的布局,仅供学习分享使用,如有不足之处,还请指正。
238 0
|
6月前
|
C#
浅谈WPF之装饰器实现控件锚点
使用过visio的都知道,在绘制流程图时,当选择或鼠标移动到控件时,都会在控件的四周出现锚点,以便于修改大小,移动位置,或连接线等,那此功能是如何实现的呢?在WPF开发中,想要在控件四周实现锚点,可以通过装饰器来实现,今天通过一个简单的小例子,简述如何在WPF开发中,应用装饰器,仅供学习分享使用,如有不足之处,还请指正。
138 1
|
C# Windows
WPF技术之RichTextBox控件
WPF RichTextBox是Windows Presentation Foundation (WPF)中提供的一个强大的文本编辑控件,它可以显示富文本格式的文本,支持多种文本处理操作。
595 0
|
3月前
|
开发框架 缓存 前端开发
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(11) -- 下拉列表的数据绑定以及自定义系统字典列表控件
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(11) -- 下拉列表的数据绑定以及自定义系统字典列表控件
|
3月前
|
C# 开发者 Windows
一款基于Fluent设计风格、现代化的WPF UI控件库
一款基于Fluent设计风格、现代化的WPF UI控件库
|
3月前
|
C# Windows
WPF中如何使用HandyCotrol控件库
WPF中如何使用HandyCotrol控件库
190 1
|
3月前
|
C# 前端开发 UED
WPF数据验证实战:内置控件与自定义规则,带你玩转前端数据验证,让你的应用程序更上一层楼!
【8月更文挑战第31天】在WPF应用开发中,数据验证是确保输入正确性的关键环节。前端验证能及时发现错误,提升用户体验和程序可靠性。本文对比了几种常用的WPF数据验证方法,并通过示例展示了如何使用内置验证控件(如`TextBox`)及自定义验证规则实现有效验证。内置控件结合`Validation`类可快速实现简单验证;自定义规则则提供了更灵活的复杂逻辑支持。希望本文能帮助开发者更好地进行WPF数据验证。
110 0
|
3月前
|
C# UED 定位技术
WPF控件大全:初学者必读,掌握控件使用技巧,让你的应用程序更上一层楼!
【8月更文挑战第31天】在WPF应用程序开发中,控件是实现用户界面交互的关键元素。WPF提供了丰富的控件库,包括基础控件(如`Button`、`TextBox`)、布局控件(如`StackPanel`、`Grid`)、数据绑定控件(如`ListBox`、`DataGrid`)等。本文将介绍这些控件的基本分类及使用技巧,并通过示例代码展示如何在项目中应用。合理选择控件并利用布局控件和数据绑定功能,可以提升用户体验和程序性能。
67 0
|
3月前
|
开发框架 前端开发 JavaScript
WPF应用开发之控件动态内容展示
WPF应用开发之控件动态内容展示