C#-WPF动态绘图-自定义坐标轴标签



  • 背景

    • 前几天被隔壁项目组喊去帮忙改Bug,关于WPF绘图库DynamicDataDisplay(简称d3)的坐标值显示问题,去网上查了一些WPF相关的教程和案例,但基本都不涉及坐标轴标签的自定义。在Google上翻了半天之后终于找到了方法,本文在记录下修改方法的同时也简述一下C#开发时的一些经验教训,少走弯路。

    环境

    • Parallel Desktop Windows 10 企业版(未激活)
    • Visual Studio 2017 Community

    坐标轴静态修改方式

    • 静态修改适用于坐标轴标签规则不会改变的情况,例如:你要显示一张对数坐标轴的图片,并且没有某个按钮或选项可以将其改为线性坐标轴,便可以这么做

    • 在绘图区域所在的xaml文件(我这里是Page1.xaml)中找到要修改的坐标轴,这里我要改的是四个plotter的水平轴,也就是四个不同地方的<d3:ChartPlotter.HorizontalAxis>,因为四个plotter除了文本显示以外的代码都是一样的,下文以其中一个为例来贴代码

    Page1.xaml

            <d3:ChartPlotter x:Name="plotter2" RenderTransformOrigin="0.5,0.5" Margin="1,20,250,0" Height="200" VerticalAlignment="Top" >
                <d3:ChartPlotter.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform/>
                        <SkewTransform AngleY="0"/>
                        <RotateTransform Angle="0"/>
                        <TranslateTransform X="0" Y="0"/>
                    </TransformGroup>
                </d3:ChartPlotter.RenderTransform>
                <d3:ChartPlotter.VerticalAxis>
                    <d3:VerticalIntegerAxis />
                </d3:ChartPlotter.VerticalAxis>
                <d3:ChartPlotter.HorizontalAxis>
                    <d3:HorizontalIntegerAxis>
                    </d3:HorizontalIntegerAxis>
                </d3:ChartPlotter.HorizontalAxis>
                <d3:Header />
                <d3:VerticalAxisTitle Content="纵轴"/>
                <d3:HorizontalAxisTitle Content="横轴"/>
            </d3:ChartPlotter>
    
    • 找到<d3:HorizontalIntegerAxis>的起始标签,给它加上两个属性:NameLabelProvider

    Page1.xaml

        <d3:HorizontalIntegerAxis x:Name="XAxis1" LabelProvider="{StaticResource YourLabelProvider}">
        </d3:HorizontalIntegerAxis>
    
    • app.xaml的资源字典标签下加入YourLabelProvider这个资源名称

    app.xaml

        <Application x:Class="XXX.App" XXX="XXX...">
            <Application.Resources>
                <ResourceDictionary>
                    <local:YourClassName x:Key="YourLabelProvider" />
                </ResourceDictionary>
            </Application.Resources>
        </Application>
    
    • YourClassName对应.cs文件中的类名,YourLabelProvider对应Page1.xaml中所需要获取的资源名

    • 然后新建一个LabelProvider.cs文件,写YourClass类如下:

    LabelProvider.cs

        using System;
        using Microsoft.Research.DynamicDataDisplay.Charts.Axes;
        using System.Windows.Controls;
        namespace XXX{
            public class YourClassName : GenericLabelProvider<int>
            {
                public override System.Windows.UIElement[] CreateLabels(Microsoft.Research.DynamicDataDisplay.Charts.ITicksInfo<int> ticksInfo)
                {
                    var originLabels = tickticksInfo.Ticks
                    var customElements = new TextBlock[ticksInfo.Ticks.Length];
                    for (int i = 0; i < customElements.Length; i++)
                    {
                        // 此处的YourRules为自定义的新标签与原标签的对应规则,返回数字值即可(如将1,2,3变为10,100,1000则用Math.Pow())
                        var newLabel = YourRules(originLabels[i])
                        customElements[i] = new TextBlock();
                        customElements[i].Text = newLabel.ToString();
                    }
                    return customElements;
                }
            }
        }
    
    • 这样就实现了静态自定义坐标轴

    坐标轴动态修改方式

    • 动态修改使用于你的应用中有其他的按钮或选项,当其值变动时会重新绘图并调整坐标轴的标签规则,例如从条形图变成折线图,从对数坐标变为线性坐标等

    • 一开始试图将上文的YourClass代码直接放在Page1的主Class内部,调用Page1自身的变量来控制,但是发现这么做并不能识别到判断用的变量,于是试图自己修改。

    • 将YourClass重写为两个单独的类,试图根据条件触发这两个不同的LabelProvider,经过一番寻找发现实现这一需求的最好方法就是直接在向app.xaml中添加资源时将这两个LabelProvider分别命名并添加,然后当需要改变坐标轴时,在Page1.xaml.cs的对应函数中增加一行用来添加/删除挂载在对应Axis上的资源的Key值,就实现了根据不同的条件调用不同的自定义标签生成器来生成标签。

    • 实现增加/删除操作的方法是直接对XAxis1LabelProvider属性赋值(此时并不需要如在静态修改中一样在Page1.xaml中为其显性指定这个属性),需要用到System.Windows.Application.Current.Resources["ResourceName"]这个函数,注意其需要强制类型转换,用法如下:

    Page1.xaml.cs

        using Microsoft.Research.DynamicDataDisplay.Charts.Axes;
        using System.Windows.Controls;
        // 这个XAxis1是对应坐标轴的Name属性
        XAxis1.LabelProvider = (LabelProviderBase<int>) System.Windows.Application.Current.Resources["YourFirstLabelProvider"];
    
    • 于是有以下更改:

    LabelProvider.cs

        using System;
        using Microsoft.Research.DynamicDataDisplay.Charts.Axes;
        using System.Windows.Controls;
        namespace XXX{
            public class YourFirstClassName : GenericLabelProvider<int>
            {
                public override System.Windows.UIElement[] CreateLabels(Microsoft.Research.DynamicDataDisplay.Charts.ITicksInfo<int> ticksInfo)
                {
                    var originLabels = tickticksInfo.Ticks
                    var customElements = new TextBlock[ticksInfo.Ticks.Length];
                    for (int i = 0; i < customElements.Length; i++)
                    {
                        // 此处的YourFirstRules为第一套自定义的新标签与原标签的对应规则,返回数字值即可(如将1,2,3变为10,100,1000则用Math.Pow())
                        var newLabel = YourFirstRules(originLabels[i])
                        customElements[i] = new TextBlock();
                        customElements[i].Text = newLabel.ToString();
                    }
                    return customElements;
                }
            }
            public class YourSecondClassName : GenericLabelProvider<int>
            {
                public override System.Windows.UIElement[] CreateLabels(Microsoft.Research.DynamicDataDisplay.Charts.ITicksInfo<int> ticksInfo)
                {
                    var originLabels = tickticksInfo.Ticks
                    var customElements = new TextBlock[ticksInfo.Ticks.Length];
                    for (int i = 0; i < customElements.Length; i++)
                    {
                        // 此处的YourSecondRules为第二套自定义标签生成规则
                        var newLabel = YourSecondRules(originLabels[i])
                        customElements[i] = new TextBlock();
                        customElements[i].Text = newLabel.ToString();
                    }
                    return customElements;
                }
            }
        }
    

    app.xaml

        <Application x:Class="XXX.App" XXX="XXX...">
            <Application.Resources>
                <ResourceDictionary>
                    <local:YourClassName x:Key="YourFirstLabelProvider" />
                    <local:YourClassName x:Key="YourSecondLabelProvider" />
                </ResourceDictionary>
            </Application.Resources>
        </Application>
    
    • 再次运行时,每次绘图时就会根据在Page1.xaml.cs中添加的那行赋值代码的位置和指定的资源名来更改自定义坐标轴的方式了

    初学C#的经验教训

    1. 做大型C系工程一定要用VS,电脑差了就换电脑,系统不对就装虚拟机。VS对工程结构和文件内容的识别速度以及自动补全的智能优先级能减少你好几倍的上网搜索时间。
    2. 越久远的库越应该上Google搜,百度上WPF实例的可用度几乎为零,而且会搜出一堆不相关的中国用户搜索频率高的绘图相关东西。这篇文章讲到的几乎所有问题如果是参考的网络,都是从Google找到答案的。
    3. 当然,时间久远的库的官方文档也很重要,多搜官方文档可以找到不少函数及用法,这些如果你没见过是不可能自己猜出来的(当然也可以用VS的智能补全大致搜一下所需功能的英文单词,看看有没有靠谱的函数,再去进一步搜索)。
    4. xaml与cs的关联如果没做过前端,可能会觉得很难理解。在有这种感觉的情况下,去强迫自己做一个项目,就再也不会因此而害怕这种超文本与代码关联的开发了。
    5. 在开发过程中还学到了很多与这个项目无关的WPF用法,如Binding等,这些虽然对最后的开发没起到作用,但如果能理解,对下一次做C#开发就会有很大帮助。总之,既然遇到了,就变成自己的吧。

 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待