ایجاد نمودار خطی در WPF

پنجشنبه 10 دی 1394

این مقاله یک آموزش کامل و مرحله به مرحله برای ایجاد نمودار خطی در WPF را ارائه می دهد، همچنین نشان می دهیم که چگونه می توان آن را به یک کامپوننت تبدیل کرد که در اپلیکیشن های دیگر WPF با الگوی MVVM نیز قابل استفاده مجدد باشد.

ایجاد نمودار خطی در WPF

WPF یک پلت فرم گرافیکی واحد را فراهم می کند، که به شما اجازه می دهد به راحتی انواع رابط های کاربری(user interface) و اشیاء گرافیکی را در اپلیکیشن دات نت خود ایجاد کنید. در اینجا می خواهیم به صورت مرحله به مرحله ایجاد کنترل(کامپوننت) نمودار خطی در WPF را آموزش دهیم.

لازم است این کنترل نمودار نیز رفتاری شبیه المان های ساخته شده در WPF داشته باشد: این یعنی بتواند در XAML ایجاد شود؛ محدوده  داده ها برای Property ها در ViewModel و MVVM سازگار تعریف شده باشد؛ سازگار با الگوی MVVM باشد. در اینجا می خواهیم با توضیح سیستم مختصاتی یک نمودار دو بعدی که در WPF  استفاده شده شروع کنیم. و بعد نشان دهیم که چگونه می توان یک نمودار خطی ساده در این سیستم ها ساخت، در نهایت نشان می دهیم که چگونه می توان یک کنترل نمودار خطی ایجاد کرد و چگونه می توان این کنترل نمودار را در اپلیکیشن WPF با الگوی MVVM استفاده مجدد کرد.

 

سیستم مختصاتی نمودارهای دو بعدی

یک سیستم مختصاتی سفارشی شده در اپلیکیشن های نمودار دو بعدی باید شرایط زیر را برآورده کند:

این اپلیکیشن همانطور که در اکثر اپلیکیشن های نمودار انجام شده، باید مستقل از واحد اشیاء گرافیکی در دنیای واقعی باشد و محور Y آن باید از پایین به بالا اشاره کند، . شکل 1 این سیستم مختصاتی سفارشی را نشان می دهد:

 

همانطور که مشاهده می کنید ما همان سیستم مختصات X-Y که در دنیای واقعی در یک ناحیه ارائه شده است را تعریف کرده ایم. شما نیز می توانید یک سیستم مختصاتی مشابه را با استفاده از کنترل پنل سفارشی و باز نویسی متدهای MeasureOverride و ArrangeOverride ایجاد کنید. هر متد اندازه داده مورد نیاز نسبت به موقعیت و المان های فرزند ارائه شده، بر می گرداند. این یک متد استاندارد برای ایجاد سیستم های مختصاتی سفارشی می باشد. ولی ما به جای ایجاد یک کنترل پنل سفارشی در اینجا می خواهیم این سیستم مختصاتی را با استفاده از یک روش متفاوت و بر اساس کد نویسی مستقیم بسازیم. در بخش بعدی یک نمودار خطی ساده ایجاد می کنیم که نشان می دهد چگونه می توان سیستم مختصاتی نمودار دو بعدی سفارشی ساخت.

نمودارهای خطی ساده

نمودار خطی X-Y از دو مقدار برای نمایش هر نقطه داده استفاده می کند. این نوع از نمودار برای توضیح روابط بین داده ها بسیار مفید است و اغلب در تجزیه و تحلیل های آماری داده ها در اپلیکیشن های بزرگ در جوامع علمی، ریاضیات، مهندسی و امور مالی و همچنین در زندگی روزمره مورد استفاده قرار می گیرد.

Visual Studio 2013 را باز کرده ، یک پروژه WPF جدید ایجاد کنید و نام آن را WpfChart قرار دهید. ما از Caliburn.Micro به عنوان فریم ورک MVVM استفاده خواهیم کرد ولی وارد جزئیات چگونه استفاده کردن از آن نمی شویم. یک UserControl به پروژه اضافه کنید و نام آن را SimpleChartView قرار دهید. قطعه کد زیر در XAML نوشته شده و برای ایجاد یک نموار خطی می باشد:

<Grid ClipToBounds="True" cal:Message.Attach="[Event SizeChanged]=
      [Action AddChart($this.ActualWidth, $this.ActualHeight)];
      [Event Loaded]=[Action AddChart($this.ActualWidth, $this.ActualHeight)]">
      <Polyline Points="{Binding SolidLinePoints}" Stroke="Black" StrokeThickness="2"/>
      <Polyline Points="{Binding DashLinePoints}" Stroke="Black" 
      StrokeThickness="2" StrokeDashArray="4,3"/>
</Grid>

در اینجا ما دو Polylines به Grid اضافه می کنیم که به دو شیئ PointCollection محدود شده است و به ترتیب SolidLinePoints و DashLinePoints می باشند. ممکن است متوجه شده باشید که ما از مکانیسم Caliburn.Micro برای اتصال رویدادهای UI در View به متدهای تعریف شده در ViewModel استفاده کرده ایم. در اینجا ما از Property با نام Message.Attach برای اتصال به رویدادهای SizeChanged و Grid’s Loaded استفاده کرده ایم تا متد AddChart را در ViewModel داشته باشیم و Property های ActualHeight و ActualWidth را به متد AddChart ارسال کنیم. این اکشن بارگذاری یا تغییر اندازه Grid را کنترل می کند و نتیجه آن در دوبار ایجاد شدن مجموعه ای از نقطه ها و ترسیم مجدد نمودار در صفحه نشان داده می شود.

 یک کلاس جدید به پروژه اضافه کرده و نام آن را SimpleChartViewModel قرار دهید. کد زیر برای این کلاس در نظر گرفته می شود:

using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;

namespace WpfChart
{
    [Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
    public class SimpleChartViewModel : Screen
    {
        [ImportingConstructor]
        public SimpleChartViewModel()
        {
            DisplayName = "01. Simple Line";
        }

        private double chartWidth = 300;
        private double chartHeight = 300;
        private double xmin = 0;
        private double xmax = 6.5;
        private double ymin = -1.1;
        private double ymax = 1.1;

        private PointCollection solidLinePoints;
        public PointCollection SolidLinePoints
        {
            get { return solidLinePoints; }
            set
            {
                solidLinePoints = value;
                NotifyOfPropertyChange(() => SolidLinePoints);
            }
        }


        private PointCollection dashLinePoints;
        public PointCollection DashLinePoints
        {
            get { return dashLinePoints; }
            set
            {
                dashLinePoints = value;
                NotifyOfPropertyChange(() => DashLinePoints);
            }
        }

        public void AddChart(double width, double height)
        {
            chartWidth = width;
            chartHeight = height;

            SolidLinePoints = new PointCollection();
            DashLinePoints = new PointCollection();
            double x = 0;
            double y = 0;
            double z = 0;
            for (int i = 0; i < 70; i++)
            {
                x = i / 5.0;
                y = Math.Sin(x);
                z = Math.Cos(x);

                DashLinePoints.Add(NormalizePoint(new Point(x, z)));
                SolidLinePoints.Add(NormalizePoint(new Point(x, y)));
            }
        }

        public Point NormalizePoint(Point pt)
        {
            var res = new Point();
            res.X = (pt.X - xmin) * chartWidth / (xmax - xmin);
            res.Y = chartHeight - (pt.Y - ymin) * chartHeight / (ymax - ymin);
            return res;
        }
    }
}

نمودار خطی با Chart Style

مثال قبل نشان داد که چگونه می توان به راحتی یک نمودار خطی دو بعدی در WPF با استفاده از الگوی MVVM استاندارد و با یک جدایی کامل بین View و ViewModel ایجاد کرد.

به همین منظور برای برنامه نمودار به شکل object-oriented (شیئ گرا) بیشتر و گسترش ساده آن برای اضافه کردن ویژگی های جدید، باید دو کلاس جدید ایجاد کنیم: که ChartStyle و LineSeries می باشند. کلاس LineSeries داده ها و استایل های خطی شامل رنگ خط، ضخامت، dash و غیره را دارد. کلاس ChartStyle تمام اطلاعات مرتبط با layout شامل gridlines (خطوط راهنما)، عنوان ،  علائم و نشانه ها و برچسب ها را برای محورها را دارد. به همین منظور نیاز داریم که به صورت داینامیک تعدادی کنترل روی View ایجاد کنیم اما ایجاد این کنترلها در XAML سخت است. بنابراین ما یکViewModel  می خواهیم که قادر به دستیابی به View بوده و View را به صورت داینامیک کنترل کند. این ممکن است قوانین MVVM را نقض کند ولی ما در ادامه  نشان می دهیم که با این روش هنوز هم می توان اپلیکیشن های نمودار MVVM سازگار در زمان تبدیل نمودار به user control های نمودار ایجاد کرد.

کلاس LineSeries

یک پوشه جدید به پروژه اضافه کرده و نام آن را ChartModel قرار دهید.یک کلاس جدید به فولدر ChartModel اضافه کرده و نام آن را LineSeries قرار دهید. کد زیر مربوط به این کلاس است:

using System;
using System.Windows.Media;
using System.Windows.Shapes;
using Caliburn.Micro;
using System.Windows;

namespace WpfChart.ChartModel
{
    public class LineSeries : PropertyChangedBase
    {
        public LineSeries()
        {
            LinePoints = new BindableCollection<Point>();
        }

        public BindableCollection<Point> LinePoints { get; set; }
        private Brush lineColor = Brushes.Black;
        public Brush LineColor
        {
            get { return lineColor; }
            set { lineColor = value; }
        }
        private double lineThickness = 1;
        public double LineThickness
        {
            get { return lineThickness; }
            set { lineThickness = value; }
        }

        public LinePatternEnum LinePattern { get; set; }

        private string seriesName = "Default";
        public string SeriesName
        {
            get { return seriesName; }
            set { seriesName = value; }
        }

        private DoubleCollection lineDashPattern;
        public DoubleCollection LineDashPattern
        {
            get { return lineDashPattern; }
            set
            {
                lineDashPattern = value;
                NotifyOfPropertyChange(() => LineDashPattern);
            }
        }

        public void SetLinePattern()
        {
            switch (LinePattern)
            {
                case LinePatternEnum.Dash:
                    LineDashPattern = new DoubleCollection() { 4, 3 };
                    break;
                case LinePatternEnum.Dot:
                    LineDashPattern = new DoubleCollection() { 1, 2 };
                    break;
                case LinePatternEnum.DashDot:
                    LineDashPattern = new DoubleCollection() { 4, 2, 1, 2 };
                    break;
            }
        }
    }

    public enum LinePatternEnum
    {
        Solid = 1,
        Dash = 2,
        Dot = 3,
        DashDot = 4,
    }
}

این کلاس یک شیئ point collection (مجموعه نقطه) ایجاد می کند که یک LinePoints برای LineSeries معین شده نامیده می شود. سپس این کلاس یک استایل خطی برای شیئ خطی تعریف می کند که شامل رنگ خط ، ضخامت،  الگوی خط و نام سری می باشد. خصوصیت SeriesName زمان ایجاد یک legend برای نمودار مورد استفاده قرار می گیرد. الگوی خط توسط یک enumeration با نام LinePatternEnum تعریف می شود که 4 الگوی خطی تعریف شده است که شامل Solid، Dash، و Dot و DashDot می باشد.

ما یک الگوی خطی از طریق متد SetLinePattern ایجاد می کنیم. در اینجا نیازی به ایجاد الگوی خطی از طریق متد SetLinePattern نیست. نیازی به ایجاد الگوی خطی solid هم نیست زیرا این تنظیمات به طور پیش فرض در شیئ Polyline وجود دارند. همچنین الگوی خطی dashed یا dotted را با استفاده از خصوصیت StrokeDashArray در Polyline ایجاد می کنیم.

کلاس ChartStyle

یک کلاس جدید دیگر با نام ChartStyle به فولدر ChartModel اضافه می کنیم. کد زیر مربوط به این کلاس است:

using Caliburn.Micro;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace WpfChart.ChartModel
{
    public class ChartStyle
    {
        private double xmin = 0;
        private double xmax = 6.5;
        private double ymin = -1.1;
        private double ymax = 1.1;
        private string title = "Title";
        private string xLabel = "X Axis";
        private string yLabel = "Y Axis";
        private bool isXGrid = true;
        private bool isYGrid = true;
        private Brush gridlineColor = Brushes.LightGray;
        private double xTick = 1;
        private double yTick = 0.5;
        private LinePatternEnum gridlinePattern;
        private double leftOffset = 20;
        private double bottomOffset = 15;
        private double rightOffset = 10;
        private Line gridline = new Line();
        public Canvas TextCanvas { get; set; }
        public Canvas ChartCanvas { get; set; }

        public double Xmin
        {
            get { return xmin; }
            set { xmin = value; }
        }


        ... ...
        ... ...        


        public void AddChartStyle(TextBlock tbTitle, TextBlock tbXLabel, TextBlock tbYLabel)
        {
            Point pt = new Point();
            Line tick = new Line();
            double offset = 0;
            double dx, dy;
            TextBlock tb = new TextBlock();

            //  determine right offset:
            tb.Text = Xmax.ToString();
            tb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
            Size size = tb.DesiredSize;
            rightOffset = 5;

            // Determine left offset:
            for (dy = Ymin; dy <= Ymax; dy += YTick)
            {
                pt = NormalizePoint(new Point(Xmin, dy));
                tb = new TextBlock();
                tb.Text = dy.ToString();
                tb.TextAlignment = TextAlignment.Right;
                tb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
                size = tb.DesiredSize;
                if (offset < size.Width)
                    offset = size.Width;
            }
            leftOffset = offset + 5;
            Canvas.SetLeft(ChartCanvas, leftOffset);
            Canvas.SetBottom(ChartCanvas, bottomOffset);
            ChartCanvas.Width = Math.Abs(TextCanvas.Width - leftOffset - rightOffset);
            ChartCanvas.Height = Math.Abs(TextCanvas.Height - bottomOffset - size.Height / 2);

             ... ...
             ... ...
        }

        public Point NormalizePoint(Point pt)
        {
            if (Double.IsNaN(ChartCanvas.Width) || ChartCanvas.Width <= 0)
                ChartCanvas.Width = 270;
            if (Double.IsNaN(ChartCanvas.Height) || ChartCanvas.Height <= 0)
                ChartCanvas.Height = 250;
            Point result = new Point();
            result.X = (pt.X - Xmin) * ChartCanvas.Width / (Xmax - Xmin);
            result.Y = ChartCanvas.Height - (pt.Y - Ymin) * ChartCanvas.Height / (Ymax - Ymin);
            return result;
        }

        public void SetLines(BindableCollection<LineSeries> dc)
        {
            if (dc.Count <= 0)
                return;

            int i = 0;
            foreach (var ds in dc)
            {
                PointCollection pts = new PointCollection();

                if (ds.SeriesName == "Default")
                    ds.SeriesName = "LineSeries" + i.ToString();
                ds.SetLinePattern();
                for (int j = 0; j < ds.LinePoints.Count; j++)
                {
                    var pt = NormalizePoint(ds.LinePoints[j]);
                    pts.Add(pt);
                }

                Polyline line = new Polyline();
                line.Points = pts;
                line.Stroke = ds.LineColor;
                line.StrokeThickness = ds.LineThickness;
                line.StrokeDashArray = ds.LineDashPattern;
                ChartCanvas.Children.Add(line);
                i++;
            }
        }
    }
}

در اینجا، ما فقط  بخشی از کد را در این کلاس قرار داده ایم. شما می توانید کد کامل شده listing را در فایل ضمیمه شده ببینید. توجه داشته باشید که ما Canvas control, ChartCanvas, را به عنوان خصوصیت عمومی تعریف کرده ایم.

معمولا فریم ورک MVVM اجازه نمی دهد که کنترل Canvas در مدل یا ViewModel ظاهر شود. همانطور که قبلا توضیح داده شد، هدف ما از انجام این کار ایجاد امکان اضافه کردن کنترلها به صورت داینامیک است.

در اینجا ما فیلدهای عضو بیشتری اضافه می کنیم که برای دستکاری طرح(layout) نمودار و و ظاهر آن استفاده می کنیم. شما می توانید به راحتی می توانید معنای هر فیلد و Property از این نام(name) را درک کنید. توجه داشته باشید که ما خصوصیت Canvas و TextCanvas دیگری اضافه می کنیم.که TextCanvas  برای نگهداری tick mark labels (برچسب نشانه ها) استفاده می شود، در حالی که ChartCanvas نمودار خود را نگه میدارد.

علاوه بر این ما فیلدهای عضو(member) زیر را برای تعریف gridlines (خطوط راهنما) درنمودار استفاده می کنیم:

private bool isXGrid = true;
private bool isYGrid = true;
private Brush gridlineColor = Brushes.LightGray;
private LinePatternEnum gridlinePattern;

 

این فیلدها و Property های مربوط به آنها انعطاف بیشتری در ظاهر gridline ها ایجاد می کند. خصوصیت GridlinePattern  به شما اجازه می دهد استایل های متنوعی برای dash انتخاب کنید که شامل solid، dash، dot و dash-dot می باشد. شما می توانید رنگ gridline ها را با استفاده از خصوصیت GridlineColor تغییر دهید. علاوه بر این ما دو خصوصیت bool به صورت IsXGrid و IsYGrid تعریف می کنیم که به شما اجازه می دهد gridlines افقی یا عمودی را فعال یا غیر فعال کنید.

 

 

ایجاد نمودارهای خطی با Chart Style

حالا می توانیم از دو کلاس LineSeries و ChartStyle بالا برای ایجاد یک نمودار خطی با خطوط راهنما(gridlines)، برچسب های محورها(axis labels)، عنوان(title) و علائم تیک (tick marks) استفاده کنیم. یک UserControl جدید به پروژه اضافه کرده و نام آن را ChartView قرار دهید. در اینجا کد مربوط به این قطعه کد در XAML برای View نشان داده شده است:

<Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

 

در اینجا، یک عنوان  قرار داده ایم و برچسب ها را برای محورهای X و Y در سلولهای مختلف از از کنترل Grid در نظر گرفته ایم و دو کنترل بومی نیز تعریف کرده ایم: که textCanvas و chartCanvas می باشند. textCanvas یک کنترل بومی برای قابلیت اندازه بندی مجدد می باشد زیرا که خصوصیات عرض و ارتفاع آن به خصوصیات ActualHeight و chartGrid’s ActualWidth محدود شده است. ما از کنترل textCanvas به عنوان والد chartCanvas برای نگهداری برچسب های علامت تیک مدیریت می کند؛ کنترل chartCanvas نمودار خود را مدیریت می کند.

یک کلاس جدید به پروژه اضافه کرده و نام آن را ChartViewModel قرار می دهیم که کد آن به صورت زیر است:

using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Windows.Documents;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using WpfChart.ChartModel;

namespace WpfChart
{
    [Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
    public class ChartViewModel :Screen
    {
        [ImportingConstructor]
         public ChartViewModel()
        {
            DisplayName = "02. Chart";       
        }       

        private ChartView view;
        private ChartStyle cs;       

        private void SetChartStyle()
        {
            view = this.GetView() as ChartView;
            view.chartCanvas.Children.Clear();
            view.textCanvas.Children.RemoveRange(1, view.textCanvas.Children.Count - 1);
            cs = new ChartStyle();
            cs.ChartCanvas = view.chartCanvas;
            cs.TextCanvas = view.textCanvas;
            cs.Title = "Sine and Cosine Chart";
            cs.Xmin = 0;
            cs.Xmax = 7;
            cs.Ymin = -1.5;
            cs.Ymax = 1.5;
            cs.YTick = 0.5;
            cs.GridlinePattern = LinePatternEnum.Dot;
            cs.GridlineColor = Brushes.Green;
            cs.AddChartStyle(view.tbTitle, view.tbXLabel, view.tbYLabel);
        }

        public void AddChart()
        {
            SetChartStyle();

            BindableCollection<LineSeries> dc = new BindableCollection<LineSeries>();
            var ds = new LineSeries();
            ds.LineColor = Brushes.Blue;
            ds.LineThickness = 2;
            ds.LinePattern = LinePatternEnum.Solid;
            for (int i = 0; i < 50; i++)
            {
                double x = i / 5.0;
                double y = Math.Sin(x);
                ds.LinePoints.Add(new Point(x, y));
            }
            dc.Add(ds);

            ds = new LineSeries();
            ds.LineColor = Brushes.Red;
            ds.LineThickness = 2;
            ds.LinePattern = LinePatternEnum.Dash;
            ds.SetLinePattern();
            for (int i = 0; i < 50; i++)
            {
                double x = i / 5.0;
                double y = Math.Cos(x);
                ds.LinePoints.Add(new Point(x, y));
            }
            dc.Add(ds);
            cs.SetLines(dc);
        }
    }
}

توجه ویژه ای نسبت به اینکه چگونه  از طریق View model به Viewدسترسی داریم داشته باشید. Caliburn.Micro یک متد با نام GetView را ارائه می کند که به ما اجازه می دهد به راحتی به View دسترسی داشته باشیم. همانطور که قبلا بحث شد، کلاس ChartStyle نیاز به دسترسی به کنترل های textCanvas و chartCanvas دارد که در View ایجاد شده است. بنابراین در متد SetChartStyle به View با استفاده از کد زیر دسترسی داریم:

            view = this.GetView() as ChartView;
            view.chartCanvas.Children.Clear();
            view.textCanvas.Children.RemoveRange(1, view.textCanvas.Children.Count - 1);
            cs = new ChartStyle();
            cs.ChartCanvas = view.chartCanvas;
            cs.TextCanvas = view.textCanvas;

ابتدا شیئ View را از متد GetView می گیریم.  به منظور ترسیم دوباره چارت در زمانی که پنجره اپلیکیشن resize شده است،  باید تمام المان های فرزند textCanvas را فراخوانی کنیم.اما chartCanvas تمام کنترل های فرزند chartCanvas را پاک می کند که  توسط  کد بولد(پررنگ شده) شده در کد بالا بدست می آید. سپس یک شیئ ChartStyle ایجاد کرده و Property های TextCanvas و ChartCanvas خود را برای کنترل های View مربوطه تنظیم می کنیم.

در متد AddChart ،  ابتدا متد SetChartStyle را فراخوانی می کنیم  که gridline ها، title،  tick mark ها و برچسب های محورها را تنظیم می کند. بقیه کد شبیه به کدی است در مثال قبلی استفاده کردیم. شکل زیر نتایج اجرای این اپلیکیشن را نشان می دهد. شما می توانید مشاهده کنید که نمودار امکانات title، labels، خطوط راهنما، علائم و تیک را دارد. تا اینجا شما با موفقیت یک نمودار خطی در WPF ایجاد کرده اید.

 

کنترل های نمودار خطی

در بخش های قبلی، ما کد همه کلاس هایی که در برنامه نمودار ما به طور مستقیم وجود داشت پیاده سازی کردیم. برای یک اپلیکیشن ساده این روش به خوبی کار می کند. اما اگر بخواهید همین کد را برای دیگر برنامه های دات نت استفاده کنید این روش بی اثر و بی فایده است. فریم ورک دات نت و wpf  ابزار قدرتمند،user control را برای حل این مشکل ارائه داده است. user control های سفارشی در WPF فقط شبیه دکمه های ساده یا text box هایی هستند که از قبل با WOP و دات نت فراهم شده بوند.

به طور معمول کنترل ها یا کامپوننت هایی که طراحی می کنید در پنجره های(windows) متعدد یا کدهای ماژولار مورد استفاده قرار می گیرد. این user control های سفارشی شده می تواند مقداری از کد را که باید  برای تغییر پیاده سازی در برنامه ایجاد کنید، کاهش می دهد. هیچ دلیلی برای کپی کردن کد در اپلیکشن خودتان نیست چرا که با این کار bug ها را از دست می دهید. و متوجه اشتباهاتتان در کدنویسی نمی شوید، بنابراین این کار یک تمرین برنامه نویسی خوب جهت ایجاد قابلیت های خاص برای user control در کدهای منبع کنترل ها می باشد که می تواند کپی کردن کد را کاهش داده و کد را ماژولار کند.

در این بخش می خواهیم به شما نشان دهیم که چگونه می توان نمودارهای خطی را در user control سفارشی قرار دهیم و چگونه می توان  یک کنترل را در اپلیکیشن WPF خود با استایل MVVM استفاده کرد. ما تلاش خواهیم کرد که کنترل نمودار را در first-class مربوط به WPF ایجاد کرده و آن را برای XAML در دسترس قرار دهیم. این بدین معناست که باید خواص وابستگی را تعریف کنیم و رویدادهای Rout شده برای کنترل نمودار مانند data binding، استایل ها، و انیمیشن را به منظور گرفتن پشتیبانی از سرویس های WPF ضروری تعریف کنیم.

این برای ایجاد یک کنترل نمودار خطی بر اساس نمودارهای خطی که ما از قبل آنها را نوشته ایم، آسان است. مدل توسعه یافته برای  user control نمودار بسیار شبیه مدل استفاده شده برای اپلیکیشن توسعه یافته در WPF است.

بر روی Solution  با نام  user control راست کلیک کرده و  Add | New Project را انتخاب کنید. شما می توانید یک کتابخانه user control برای WPF جدید از template ها انتخاب کرده و نام آن را ChartControl قرار دهید. زمانی که این کار را انجام دهید، Visual Studio 2013 یک فایل XAML ایجاد کرده و یک کلاس سفارشی مربوط برای مدیریت مقداردهی اولیه و مدیریت رویدادهای کد ایجاد می کند.این کار یک user control پیش فرض با نام UserControl1 ایجاد می کند. بر روی UserControl1.xaml در solution explorer راست کلیک کرده و Rename را انتخاب کرده و نام آن را به LineChart تغییر دهید. همچنین شما باید نام UserControl1 را به LineChart در هر دو فایل code behind برای این کنترل تغییر دهید. یک فولدر جدید در کنترل اضافه کرده و نام آن را ChartModel قرار دهید . دو کلاس موجود از فولدر ChartModel از پروژه ChartModel از پروژه جاری را اضافه کرده و فضای نام آن را به ChartControl برای تمام کلاس ها در فولدر ChartModel از فولدر جاری تغییر دهید. برای جلوگیری از سردرگمی، باید نام های این دو کلاس را به LineSeriesControl و ChartStyleControl تغییر دهید:

<UserControl x:Class="ChartControl.LineChart"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">   

    <Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

 

شما ممکن است توجه کرده باشید که ما داریم از کد code-behind برای پیاده سازی کنترل نمودار استفاده می کنیم به خاطر اینکه ما از متد Message.Attach مربوط به Caliburn.Micro برای مدیریت رویدادهایی مانند SizeChanged استفاده نمی کنیم. در واقع ما دو گزینه برای ایجاد یک کتابخانه user control داریم. اگر کنترل نموداری که طراحی کرده ایم تنها در اپلیکیشن جاری مورد استفاده قرار گرفته باشد، می توانیم یک user control با استفاده از روش MVVM ایجاد کنیم. در این مورد ما باید View model های جداگانه برای کنترل و نمونه ای از آن در  اپلیکیشن والد ایجاد کنیم که  از این کنترل استفاده خواهد کرد. بنابراین View والد این کنترل را در آن خواهد داشت و  کنترل های View model  را در این کنترل از طریق ParentVM.UserControlVM متصل خواهد کرد و UserControlVM و user control ما از دیگر اتصالات مراقبت خواهند کرد.

از سوی دیگر، اگر کنترل ما توسط دیگر اپلیکیشن ها یا توسط دیگر توسعه دهندگان مورد استفاده قرار گیرد، باید user control خود را با استفاده از پیاده سازی control template بر اساس خواص وابستگی ایجاد کرد. در اینجا باید به یک نکته مهم اشاره کنیم که  تصمیم گرفته ایم از MVVM یا خواص وابستگی code-behind برای توسعه کنترل نمودار خود استفاده کنیم. این کار نقض قوانین MVVM برای مصرف کنندگان user control ما نیست.

در اینجا می خواهیم نشان دهیم که چگونه می توان یک کنترل نمودار بر اساس خواص وابستگی(dependency properties) پیاده سازی شده در کد code-behind ایجاد کنیم. این خواص وابستگی یک روش ساده برای اتصال داده ها در زمانی که شیئ منبع المانی از WPF است ارائه می دهد و Property منبع یک خاصیت وابستگی است. زیرا خواص وابستگی پشتبانی داخلی برای اعلام خواص تغییر یافته دارد. به عنوان یک نتیجه تغییر مقدار خواص وابستگی ، Property  در شیئ مورد نظر را بلافاصله به روز رسانی می کند. این دقیقا همان چیزی است که ما می خواهیم و این بدون نیاز به ساخت هر نوع زیرساخت اضافی اتفاق میافتد، مانند یک اینترفیس INotifyPropertyChanged.

تعریف ویژگی های وابستگی

در ادامه ما یک رابط عمومی(public interface) پیاده سازی می کنیم که کنترل نمودار خطی را در معرض جهان بیرون قرار می دهد. به عبارت دیگر زمان آن است که Property ها، متدها و رویدادهایی که مصرف کننده کامپوننت نمودار ما  احتیاج دارد را ایجاد کنیم.

ممکن است  بخواهیم بیشتر Property ها  در کلاس ChartStyleControl را  در معرض جهان خارج قرار دهیم به عنوان مثال محدودیت محورها، عنوان  و برچسب ها. در همان زمان تلاش می کنیم  در صورت امکان تغییرات کوچکی در پروژه نمودار خطی اصلی ایجاد کنیم.

مرحله اول برای ایجاد خواص وابستگی، یک فیلد استاتیک برای آن تعریف می کنیم، که با کلمه Property اضافه شده به انتهای نام خواص تعریف می شود. کد زیر را به فایل code-behind اضافه کنید:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using Caliburn.Micro;
using System.Collections.Specialized;

namespace ChartControl
{
    public partial class LineChart : UserControl
    {
        private ChartStyleControl cs;
        public LineChart()
        {
            InitializeComponent();
            this.cs = new ChartStyleControl();
            this.cs.TextCanvas = textCanvas;
            this.cs.ChartCanvas = chartCanvas;
        }

        private void chartGrid_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            ResizeLineChart();
        }

        private void SetLineChart()
        {
            cs.Xmin = this.Xmin;
            cs.Xmax = this.Xmax;
            cs.Ymin = this.Ymin;
            cs.Ymax = this.Ymax;
            cs.XTick = this.XTick;
            cs.YTick = this.YTick;
            cs.XLabel = this.XLabel;
            cs.YLabel = this.YLabel;
            cs.Title = this.Title;
            cs.IsXGrid = this.IsXGrid;
            cs.IsYGrid = this.IsYGrid;
            cs.GridlineColor = this.GridlineColor;
            cs.GridlinePattern = this.GridlinePattern;          

            ResizeLineChart();
        }

        private void ResizeLineChart()
        {
            chartCanvas.Children.Clear();
            textCanvas.Children.RemoveRange(1, textCanvas.Children.Count - 1);
            cs.AddChartStyle(tbTitle, tbXLabel, tbYLabel);

            if (DataCollection != null)
            {
                if (DataCollection.Count > 0)
                {
                    cs.SetLines(DataCollection);
                }
            }
        }

        public static DependencyProperty XminProperty =
             DependencyProperty.Register("Xmin", typeof(double), typeof(LineChart),
             new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public double Xmin
        {
            get { return (double)GetValue(XminProperty); }
            set { SetValue(XminProperty, value); }
        }

        ......
        ......

        public static readonly DependencyProperty DataCollectionProperty = 
             DependencyProperty.Register("DataCollection",
             typeof(BindableCollection<LineSeriesControl>), typeof(LineChart),
             new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
             OnDataChanged));

        public BindableCollection<LineSeriesControl> DataCollection
        {
            get { return (BindableCollection<LineSeriesControl>)GetValue(DataCollectionProperty); }
            set { SetValue(DataCollectionProperty, value); }
        }

        private static void OnDataChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var lc = sender as LineChart;
            var dc = e.NewValue as BindableCollection<LineSeriesControl>;
            if (dc != null)
                dc.CollectionChanged += lc.dc_CollectionChanged;
        }

        private void dc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (DataCollection != null)
            {
                CheckCount = 0;
                if (DataCollection.Count > 0)
                    CheckCount = DataCollection.Count;
            }
        }

        public static DependencyProperty CheckCountProperty =
            DependencyProperty.Register("CheckCount", typeof(int), typeof(LineChart),
            new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            new PropertyChangedCallback(OnStartChart)));

        public int CheckCount
        {
            get { return (int)GetValue(CheckCountProperty); }
            set { SetValue(CheckCountProperty, value); }
        }

        private static void OnStartChart(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            (sender as LineChart).SetLineChart();
        }
    }
}

در اینجا فقط می خواهیم به شما نشان دهیم که چگونه خواص وابستگی را ایجاد کنید و کدهای تکراری را برای تعریف خواص وابستگی حذف کنید. شما می توانید لیست کد کامل را در نمونه برنامه ضمیمه شده بررسی کنید. توجه کنید که ما چگونه خواص وابستگی برای Xmin را تعریف می کنیم. WPF با تکنیک های جدیدی از تعریف خواص برای یک کنترل را ارائه می دهد. هسته اصلی سیتم Property جدید خواص وابستگی است و کلاس wrapper یک DepedencyObject را فراخوانی می کند. در اینجا ما از کلاس wrapper برای  رجیستر کردن خواص وابستگی Xamin در سیستم خواص استفاده می کنیم تا شیئ شامل Property داخل آن باشد و می توانیم به راحتی مقدار Property را get یا set کنیم. معمولا  property wrapper نباید شامل هرنوع logic ای باشد زیرا ممکن است به طور مستقیم و با استفاده از متدهای SetValue و GetValue از کلاس پایه DependencyObject تنظیم و بازیابی شود.

در برخی شرایط، ممکن است بخواهیم برخی منطق ها و متدها را بعد از تنظیم مقدار برای یک خاصیت وابستگی محاسبه کند. می توانیم این کارها را با پیاده سازی یک متد callback انجام دهیم که زمانی که یک Property از طریق wrapper یا SetValue مستقیم فراخوانی می شود راه اندازی می شود. برای مثال، بعد از ایجاد DataCollection که شامل شیئ LineSeriesControl می باشد، می خواهیم کنترل نمودار را برای نمودار خطی مربوط به طور اتوماتیک برای LineSeriesControl ایجاد کنیم. کد پررنگ شده در فایل code-behind قبلی نشان می دهد که چگونه می توان یک متد callback را پیاده سازی کرد. DataCollectionProperty شامل یک متد callback با نام OnDataChanged می باشد. در داخل این متد callback یک رویداد handler برای خصوصیت CollectionChanged اضافه می کنیم و این زمانی که DataCollection تغییر کند راه اندازی می شود. در CollectionChanged handler یک خواص وابستگی دیگر تنظیم می کنیم و CheckCount را در DataCollection فراخوانی می کنیم. اگر CheckCount > 0 باشد می فهمیم که DataCollection شامل اشیاء LineSeries می باشد و به همین خاطر متد callback دیگری با نام OnStartChart پیاده سازی می کنیم که برای خصوصیت CheckCount در نمودار خطی با فراخوانی متد SetLineChart می باشد.

توجه داشته باشید که داخل متد SetLineChart ما property های عمومی را در کلاس ChartStyleControl برای خواص وابستگی مربوط به کنترل نمودار تنظیم کرده ایم. به این ترتیب هر زمان که خواص وابستگی در کلاس LineChart تغییر کند خواص کلاس های Legend و ChartStyle نیز متعاقبا تغییر خواهد کرد.

در اینجا ما همچنین یک chartGrid_SizeChanged event handler به کنترل نمودار خطی اضافه می کنیم این handler تنظیم می کند که هر زمان کنترل نمودار resize شود به روز رسانی می شود. حالا می توانیم کتابخانه کنترل را توسط راست کلیک بر روی نام پروژه و انتخاب Build ،بسازیم(Build کنیم).

استفاده از کنترل نمودار

تا اینجای کار ما یک کنترل نمودار خطی ایجاد شده داریم که می توانیم به راحتی از آن در پروژه WpfChart خود استفاده کنیم. برای استفاده از این کنترل در اپلیکیشن WPF نیاز داریم فضای نام دات نت را map  کنیم و فضای نام XML را به صورت شکل زیر مونتاژ کنیم:

xmlns:local="clr-namespace:ChartControl;assembly=ChartControl"

 

اگر کنترل نمودار در همان اپلیکیشن ما قرا گرفته باشد فقط نیاز داریم که فضای نام را map کنیم:

xmlns:local="clr-namespace:ChartControl

با استفاده از فضای نام XML و نام کلاس user control می توانید یک کاربر را به طور دقیق اضافه کنید همانطور که دیگر انواع شیئ فایل XAML را اضافه کرده اید.

ایجاد یک نمودار خطی ساده

حالا نشان خواهیم داد که چگونه از کنترل نمودار برای ایجاد یک نمودار خطی در WPF استفاده کنیم. در پروژه WpfChart، بر روی References راست کلیک کرده و Add References را انتخاب کنید تا پنجره Reference Manager ظاهر شود. بر روی Solution کلیک کرده و سپس Projects را از پنل سمت چپ این پنجره انتخاب کرده و ChartControl را highlight کنید. بر روی OK کلیک کنید تا ChartControl به پروژه جاری اضافه شود. به این ترتیب شما می توانید از این کنترل در اپلیکیشن WPF شبیه به المان های ساخته شده در آن استفاده کنید. یک UserControl جدید به پروژه WpfChart اضافه کرده و نام آن را ChartControlView قرار دهید. در اینجا فایل XAML برای این View به صورت زیر است:

<UserControl x:Class="WpfChart.ChartControlView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:ChartControl;assembly=ChartControl"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="500">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="150"/>
        </Grid.ColumnDefinitions>

 

در اینجا به سادگی یک کنترل نمودار خطی دقیقا مثل آنچه که برای دیگر انواع المان WPF ایجاد کرده بودید، ایجاد می کنید. توجه کنید که چگونه ما Property GridlinePattern را  مشخص می کنیم و به سادگی از Solid, Dash, Dot, DashDot که در LinePatternEnum تعریف شده استفاده می کنیم. این بسیار ساده تر از استفاده code-behind می باشد که شما نیاز به تعیین نوع مسیر کامل به منظور تعریف الگوی خطی gridlines دارید. همچنین می توانید دیگر خصوصیت های استاندارد برای المان های WPF جهت کنترل نمودار را مشخص کنید که مانند عرض، ارتفاع، CanvasLeft ، CanvasTop و BackGround می باشد. این Property های استاندارد اجازه می دهند کنترل را قرار داده و اندازه کنترل را تنظیم کنید یا رنگ پس زمینه کنترل را انتخاب کنید.

حالا یک کلاس جدید به پروژه اضافه می کنیم و نام آن را ChartControlViewModel قرا ر می دهیم. در اینجا کد کلاس به صورت زیر نشان داده شده است:

using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Media;
using ChartControl;

namespace WpfChart
{
    [Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
    public class ChartControlViewModel : Screen
    {
        [ImportingConstructor]
        public ChartControlViewModel()
        {
            DisplayName = "03. Chart Control";
            DataCollection = new BindableCollection<LineSeriesControl>();
        }

        public BindableCollection<LineSeriesControl> DataCollection{get;set;}

        public void AddChart()
        {
            DataCollection.Clear();

            LineSeriesControl ds = new LineSeriesControl();
            ds.LineColor = Brushes.Blue;
            ds.LineThickness = 2;
            ds.SeriesName = "Sine";
            ds.LinePattern = LinePatternEnum.Solid;
            for (int i = 0; i < 50; i++)
            {
                double x = i / 5.0;
                double y = Math.Sin(x);
                ds.LinePoints.Add(new Point(x, y));
            }
            DataCollection.Add(ds);

            ds = new LineSeriesControl();
            ds.LineColor = Brushes.Red;
            ds.LineThickness = 2;
            ds.SeriesName = "Cosine";
            ds.LinePattern = LinePatternEnum.Dash;
            for (int i = 0; i < 50; i++)
            {
                double x = i / 5.0;
                double y = Math.Cos(x);
                ds.LinePoints.Add(new Point(x, y));
            }
            DataCollection.Add(ds);
        }
    }
}

درست مثل زمانی که ما نمودارهای قبلی را از پیش ایجاد کردیم، نیاز داریم که سری های خطی را ایجاد کرده و سپس آنها را به نمودار DataCollection کنترل اضافه کنیم. زیبایی استفاده از کنترل نمودار این است که ما می توانید از الگوی MVVM استاندارد در اپلیکیشن استفاده کنیم، حتی از طریق ایجاد کنترل نمودار با استفاده از خواص وابستگی و روش Code behind . در اینجا ما فقط خصوصیت DataCollection را در View Model تعریف کردیم( نیازی به پیاده سازی رابط INotifyCollectionChanged برای این collection نداریم زیرا به صورت ساخته شده در این interface وجود دارد) و این را به کنترل نمودار متصل می کند. متد AddChart در View Model نیز به دکمه در View با نامگذاری Caliburn.Micro محدود است. به این ترتیب ما یک جداسازی کامل بین View و View Model  ایجاد کردیم که مطابق با نیازمندی های الگوی MVVM می باشد.

اجرای این مثال و کلیک بر روی دکمه  Chart نتیجه ای که در شکل زیر نشان داده شده را می دهد. شما می توانید نمودار را تغییر اندازه دهید تا ببینید که چگونه نمودار به طور خودکار آپدیت می شود.

 

ایجاد چندین نمودار خطی

با استفاده از کنترل نمودار خطی می توانید به آسانی نمودارهای متعددی در یک پنجره WPF ایجاد کنید. اجازه دهید یک UserControl جدید به پروژه اضافه کنیم ونام آن را MultiChartView قرار دهیم.یک کنترل گرید دو به دو ایجاد کرده و یک کنترل نمودار برای هر 4 سلول گرید اضافه کنید که می تواند با استفاده از فایل XAML زیر انجام شود:

<UserControl x:Class="WpfChart.MultiChartView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:cal="http://www.caliburnproject.org"
             xmlns:local="clr-namespace:ChartControl;assembly=ChartControl"
             mc:Ignorable="d"
             d:DesignHeight="600" d:DesignWidth="600">
    <Grid cal:Message.Attach="[Event Loaded]=[Action AddChart]">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

 

در اینجا ما 4 کنترل نمودرا خطی ایجاد می کنیم که Chart1, Chart2, Chart3, Chart4 نام دارد. برای سادگی کار در این مثال توابع داده ای مشابه روی نمودار رسم می کنیم. اما برای هر نمودار یک خصوصیت GridlineColor متفاوت تنظیم می کنیم. در عمل می توانیم توابع ریاضی متفاوتی را روی هر نمودار با توجه به نیازمندی های اپلیکیشن خود رسم کنیم.

یک کلاس جدید به فولدر ViewModels  اضافه کرده و نام آن را MultiChartViewModel قرار دهید. کد این کلاس به صورت زیر است:

using System;
using Caliburn.Micro;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using ChartControl;

namespace WpfChart
{
    [Export(typeof(IScreen)), PartCreationPolicy(CreationPolicy.NonShared)]
    public class MultiChartViewModel : Screen
    {
        private readonly IEventAggregator _events;
        [ImportingConstructor]
        public MultiChartViewModel()
        {
            this._events = events;
            DisplayName = "02. Multiple Charts";
            DataCollection = new BindableCollection<LineSeriesControl>();
        }

        public BindableCollection<LineSeriesControl> DataCollection { get; set; }

        public void AddChart()
        {
            DataCollection.Clear();

            LineSeriesControl ds = new LineSeriesControl();
            ds.LineColor = Brushes.Blue;
            ds.LineThickness = 2;
            ds.SeriesName = "Sine";
            ds.LinePattern = LinePatternEnum.Solid;
            for (int i = 0; i < 50; i++)
            {
                double x = i / 5.0;
                double y = Math.Sin(x);
                ds.LinePoints.Add(new Point(x, y));
            }
            DataCollection.Add(ds);

            ds = new LineSeriesControl();
            ds.LineColor = Brushes.Red;
            ds.LineThickness = 2;
            ds.SeriesName = "Cosine";
            ds.LinePattern = LinePatternEnum.Dash;
            for (int i = 0; i < 50; i++)
            {
                double x = i / 5.0;
                double y = Math.Cos(x);
                ds.LinePoints.Add(new Point(x, y));
            }
            DataCollection.Add(ds);
        }
    }
}

این View Model  مشابه مثال قبلی است که در آن یک نمودار خطی تکی ایجاد کرده بودید. در اینجا 4 کنترل های نمودار خطی به شیئ DataCollection  مشابه متصل می شود. مانند بقیه المانهای ساخته شده در WPF می توانید بسیاری از کنترل های نمودار را بر اساس نیاز، در یک اپلیکیشن WPF تکی قرار دهید.

شکل زیر نتایج اجرای این مثال را نشان می دهد.

فایل های ضمیمه

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان
  • WPF
  • 2k بازدید
  • 1 تشکر

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید