在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   ,如需转载请自行联系原作者

相关文章
|
19天前
|
C# 开发者 Windows
基于Material Design风格开源、易用、强大的WPF UI控件库
基于Material Design风格开源、易用、强大的WPF UI控件库
|
2月前
|
C# 容器
浅谈WPF之UniformGrid和ItemsControl
在日常开发中,有些布局非常具有规律性,比如相同的列宽,行高,均匀的排列等,为了简化开发,WPF提供了UniformGrid布局和ItemsControl容器,本文以一个简单的小例子,简述,如何在WPF开发中应用UniformGrid和ItemsControl实现均匀的布局,仅供学习分享使用,如有不足之处,还请指正。
67 0
|
4月前
|
C#
浅谈WPF之装饰器实现控件锚点
使用过visio的都知道,在绘制流程图时,当选择或鼠标移动到控件时,都会在控件的四周出现锚点,以便于修改大小,移动位置,或连接线等,那此功能是如何实现的呢?在WPF开发中,想要在控件四周实现锚点,可以通过装饰器来实现,今天通过一个简单的小例子,简述如何在WPF开发中,应用装饰器,仅供学习分享使用,如有不足之处,还请指正。
65 1
|
8月前
|
C# Windows
WPF技术之图形系列Polygon控件
WPF Polygon是Windows Presentation Foundation (WPF)框架中的一个标记元素,用于绘制多边形形状。它可以通过设置多个点的坐标来定义多边形的形状,可以绘制任意复杂度的多边形。
460 0
|
8月前
|
C# Windows
WPF技术之RichTextBox控件
WPF RichTextBox是Windows Presentation Foundation (WPF)中提供的一个强大的文本编辑控件,它可以显示富文本格式的文本,支持多种文本处理操作。
347 0
|
4月前
|
前端开发 C# 容器
浅谈WPF之控件拖拽与拖动
使用过office的visio软件画图的小伙伴都知道,画图软件分为两部分,左侧图形库,存放各种图标,右侧是一个画布,将左侧图形库的图标控件拖拽到右侧画布,就会生成一个新的控件,并且可以自由拖动。那如何在WPF程序中,实现类似的功能呢?今天就以一个简单的小例子,简述如何在WPF中实现控件的拖拽和拖动,仅供学习分享使用,如有不足之处,还请指正。
108 2
|
8月前
|
数据挖掘 数据处理 C#
WPF技术之DataGrid控件
WPF DataGrid是一种可以显示和编辑数据的界面控件。它可以作为表格形式展示数据,支持添加、删除、修改、排序和分组操作。
184 0
|
20天前
|
C# 开发者 C++
一套开源、强大且美观的WPF UI控件库
一套开源、强大且美观的WPF UI控件库
135 0
|
5月前
|
算法 C# UED
浅谈WPF之控件模板和数据模板
WPF不仅支持传统的Windows Forms编程的用户界面和用户体验设计,同时还推出了以模板为核心的新一代设计理念。在WPF中,通过引入模板,将数据和算法的“内容”和“形式”进行解耦。模板主要分为两大类:数据模板【Data Template】和控件模板【Control Template】。
96 8
|
8月前
|
定位技术 C# UED
WPF技术之ScrollViewer控件
WPF ScrollViewer是WPF中常用的一个控件,它提供了滚动视图的功能,可用于显示超出容器可视区域的内容。ScrollViewer通常用于容纳大量内容的控件,以在有限的空间内显示这些内容,并允许用户通过滚动来查看隐藏的部分。
714 0