نمودار پای(PieChart) در WPF

برای برنامه‌نویسی در حوزه اطلاعات در بیشتر زمان‌ها نیاز به تهیه انواع نمودارها از جمله نمودارپای(PieChart)، میله‌ای(BarChart)، خطی(LineChart) و ... می‌باشد، زیبای یک برنامه به وجود همین نمودارها وابسته می‌باشد چراکه فقط با یک نگاه می‌تواند اطلاعات بسیاری را از یک نمودار دریافت کرد. ابزارهای مختلفی برای تولید این نمودارها موجود است برای نمونه اکسل(Excel)، پاوربی‌آی(PowerBI)، کلیک‌ویو(Qlikview) و ... اما همه این ابزارها خارج از محیط برنامه‌نویسی‌مان هستند و استفاده از نمودارهای تولیدشده توسط این نرم‌افزارها درون برنامه خودمان امکان‌پذیر نیست. در محیط برنامه‌نویسی ویژوال‌استودیو نیز می‌توان از ابزارهای تولید نمودار استفاده کرد مانند wpfToolkit، LiveChart، ScottPlot، oxyplot، Telerik، DevExpress، Syncfusion، SciChart WPF و بسیاری دیگر از این نوع ابزارها موجود است که برخی رایگان و برخی مانند تلریک را باید خریداری نمود. هرکدام از این ابزارها دارای قابلیت‌ها و محدودیت‌های خودشان می‌باشند. در این مقاله هدف ساخت یک نمودارپای بصورت مستقل از این ابزارها و فقط با کمک WPF می‌باشد. قابلیت‌هایی چون لیبل(lable)، دونات‌شکل(Doughnut)، خروج‌ازمرکز(Indent) و ... در آن درنظر گرفته‌شده که شما نیز می‌توانید قابلیت‌های مدنظر خودتان را به آن بیفزایید.


یک برنامه جدید از نوع WPF ایجاد کنید(نام آن را می‌توانید prjPieChart بگذارید)

یک کلاس جدید ایجاد کنید با نام PieSliceShape که از کلاس Shape ارث می‌برد بسازید، درون کلاس و در بخش using کد زیر را وارد کنید(درصورت موجودنبودن):

using System.Windows.Shapes;
using System.Windows;
using System.Windows.Media;
using System.Globalization;

دو متغیر محلی زیر را درون کلاس تعریف کنید:

private FormattedText _lable = new FormattedText("", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Tahoma"), 16, Brushes.Black);// Text of Pie value to show
        private Point _tp = new Point(0, 0);// Text point

حال ویژگی‌های(Properties) مورد نیاز برای نمودار پای را تعریف می‌کنیم(برای اطلاعات بیشتر درباره نحوه تعریف ویژگی در WPF به این‌جا مراجعه نمایید)، برای فهم بیشتر نخست یکی از آنها را تعریف می‌کنیم و بقیه مشابه آن می‌باشد(البته کد مربوطه را اینجا می‌آورم فقط توضیح داده‌نخواهدشد)

public bool Doughnut
        {
            get { return (bool)GetValue(DoughnutProperty); }
            set { SetValue(DoughnutProperty, value); }
        }
        public static readonly DependencyProperty DoughnutProperty = DependencyProperty.Register(
            "Doughnut",
            typeof(bool),
            typeof(PieSliceShape),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)
            );

در خط شماره 10 یک PropertyMetadata ایجاد شده که مقدار اولیه برای ویژگی Doughnut را false قرارداده و مقدار بعدی یعنی AffectsRender به ما این امکان را می‌دهد تا درصورت تغییر مقدار ویژگی Doughnut در محیط طراحی(DesignMode) بلافاصله تغییر نمایش داده شود. ویژگی Doughnut این امکان را می‌دهید که شکل نمودارپای دایره‌ای یا دونات شکل باشد،

بقیه ویژگی‌ها به ترتیب در زیر آمده‌اند:

public string Lable
        {
            get { return (string)GetValue(LableProperty); }
            set { SetValue(LableProperty, value);  }
        }
        public static readonly DependencyProperty LableProperty = DependencyProperty.Register(
            "Lable",
            typeof(string),
            typeof(PieSliceShape),
            new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender)
            );

        public bool ShowLable
        {
            get { return (bool)GetValue(ShowLableProperty); }
            set { SetValue(ShowLableProperty, value); }
        }
        public static readonly DependencyProperty ShowLableProperty = DependencyProperty.Register(
            "ShowLable",
            typeof(bool),
            typeof(PieSliceShape),
            new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender)
            );

        public Brush Foreground
        {
            get { return (Brush)GetValue(ForegroundProperty); }
            set { SetValue(ForegroundProperty, value); }
        }
        public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register(
            "Foreground",
            typeof(Brush),
            typeof(PieSliceShape),
            new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender)
            );

        public double Indent
        {
            get { return (double)GetValue(IndentProperty); }
            set { SetValue(IndentProperty, value); }
        }
        public static readonly DependencyProperty IndentProperty = DependencyProperty.Register(
            "Indent",
            typeof(double),
            typeof(PieSliceShape),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender)
            );

        public double StartAngle
        {
            get { return (double)GetValue(StartAngleProperty); }
            set { SetValue(StartAngleProperty, value); }
        }
        public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register(
            "StartAngle",
            typeof(double),
            typeof(PieSliceShape),
            new FrameworkPropertyMetadata(
                0.0,
                FrameworkPropertyMetadataOptions.AffectsRender,
                null,
                new CoerceValueCallback(AngleLimit))
            );

        public double Angle
        {
            get { return (double)GetValue(AngleProperty); }
            set { SetValue(AngleProperty, value); }
        }
        public static readonly DependencyProperty AngleProperty = DependencyProperty.Register(
            "Angle",
            typeof(double),
            typeof(PieSliceShape),
            new FrameworkPropertyMetadata(
                90.0,
                FrameworkPropertyMetadataOptions.AffectsRender,
                null,
                new CoerceValueCallback(AngleLimit))
            );

        private static object AngleLimit(DependencyObject depObj, object baseVal)
        {
            return Math.Max(Math.Min((double)baseVal, 359.99), 0.0);
        }

ویژگی‌های زیر تعریف شده‌اند:

  • Label: مقداری که می‌خواهیم بصورت متنی نمایش دهیم
  • ShowLabel: آیا Label نمایش‌داده‌شود یا نه
  • Foreground: رنگ موردنیاز برای نمایش Label
  • Indent: مقدار خروج از مرکز یا برجسته‌کردن نمودار
  • StartAngle: زاویه آغاز نمودار
  • Angle: زوایه نمودارپای

شاید بپرسید رنگ نمودار، ضخامت، رنگ حاشیه و ... چگونه تعریف‌می‌گردد، از آنجایی که کلاس ما از کلاس Shape ارث‌برده لذا نیازی به تعریف این ویژگی‌ها نیست، و ویژگی‌های فوق درون کلاس Shape تعریف شده‌اند.

توجه کنید تابع AngleLimit مقدارداده ورودی(زاویه) را کنترل می‌کند تا از محدوده مجاز(از صفر تا 360 درجه) تجاوز نکند.

در کلاس Shape یک ویژگی وجود دارد که پس از ارث‌بری باید حتما آن را بازنویسی نمود، این ویژگی به نام DefiningGeometry هندسه(Geometry) شکل‌مان را مشخص می‌کند و بدون آن ما شکل‌مان هیچ هندسه‌ای ندارد(حتما موقع کامپایل به ایراد بر‌می‌خوریم وحتی در زمان طراحی نیز خطا داریم)، در زیر کد مربوطه برای تولید هندسه نمودارپای قراردارد:

protected override Geometry DefiningGeometry
        {
            get
            {
                double _maxw = Math.Max(0.0, RenderSize.Width + 2 * StrokeThickness);//Max width of shape
                double _maxh = Math.Max(0.0, RenderSize.Height + 2 * StrokeThickness);// max height of shape
                double _halfw = _maxw / 2;
                double _halfh = _maxh / 2;
                double _dw = _maxw / 6; // Doughnut width
                double _dh = _maxh / 6; //Doughnut height
                double _sa = StartAngle * Math.PI / 180.0;//Rad Start Angle
                double _se = (StartAngle + Angle) * Math.PI / 180.0;//Rad End Angle
                double _xs = _halfw * Math.Cos(_sa);//Pie Slice Start X
                double _ys = _halfh * Math.Sin(_sa);//Pie Slice Start Y
                double _xe = _halfw * Math.Cos(_se);//Pie Slice End X
                double _ye = _halfh * Math.Sin(_se);//Pie Slice End Y

                double _xsd = _dw * Math.Cos(_sa);//Pie Slice Doughnut Start X
                double _ysd = _dh * Math.Sin(_sa);//Pie Slice Doughnut Start Y
                double _xed = _dw * Math.Cos(_se);//Pie Slice Doughnut End X
                double _yed = _dh * Math.Sin(_se);//Pie Slice Doughnut End Y

                double _atd = (StartAngle + Angle / 2.0);//digree middle Angle
                double _sm = _atd * Math.PI / 180.0;//Rad middle Angle
                double _maxwh = Math.Max(_maxw, _maxh); 
                double _xi = Indent * _maxw/_maxwh * Math.Cos(_sm);//Indent X
                double _yi = Indent * _maxh/_maxwh * Math.Sin(_sm);//Indent Y
                _lable = new FormattedText(Lable,
                            CultureInfo.CurrentCulture,
                            FlowDirection.LeftToRight,
                            new Typeface("Tahoma"),
                            16,
                            Foreground);// Create Lable string
                double _xoffset = 0;
                double _yoffset = 0;
                if (_atd >= 0 && _atd < 90)
                { _yoffset = -_lable.Height; }
                else if (_atd >= 90 && _atd < 180)
                { _xoffset = -_lable.Width; _yoffset = -_lable.Height; }
                else if (_atd >= 180 && _atd < 270)
                { _xoffset = -_lable.Width;}
                _tp = new Point(_halfw + _halfw * Math.Cos(_sm) + _xoffset + _xi, _halfh - _halfh * Math.Sin(_sm) + _yoffset-_yi);// Text point

                Point _sp = new Point(_halfw + _xs + _xi, _halfh - _ys - _yi);//Big Arc Start Point
                Point _ep = new Point(_halfw + _xe + _xi, _halfh - _ye - _yi);//Big Arc End Point
                Point _center = new Point(_halfw + _xi, _halfh - _yi);//PieSlice Center
                Size _size = new Size(_halfw, _halfh);//PieSliceShape big arc Size
                Size _sized = new Size(_dw, _dh);//PieSliceShape small arc size
                StreamGeometry _gm = new StreamGeometry();
                using (StreamGeometryContext _sgc = _gm.Open())
                {
                    _sgc.BeginFigure(_sp, true, true);
                    _sgc.ArcTo(_ep, _size, 0.0, (Angle > 180), SweepDirection.Counterclockwise, true, false);
                    if (Doughnut)
                    {
                        Point _spd = new Point(_halfw + _xsd + _xi, _halfh - _ysd - _yi);//Start Point of small arc
                        Point _epd = new Point(_halfw + _xed + _xi, _halfh - _yed - _yi);//End Point of small arc
                        _sgc.LineTo(_epd, true, false);
                        _sgc.ArcTo(_spd, _sized, 0.0, (Angle > 180), SweepDirection.Clockwise, true, false);// small arc
                        _sgc.LineTo(_sp, true, false);
                    }
                    else
                    {
                        _sgc.LineTo(_center, true, false);
                    }
                }
                return _gm;
            }
        }

طول و عرض دونات را در اینجا یک‌ششم اندازه شکل گرفته‌ایم که شما می‌توانید این را نیز با تعریف یک ویژگی جدید تبدیل به متغیر کنید. تمام توضیحات درون خود کد بصورت کامنت وجود دارد.

اما با کمی دقت متوجه می‌شوید که در کد بالا خروجی نمایش برای تکست وجود ندارد برای این که متن یا همان Label نمایش داده‌شود کارهای مختلفی می‌شود انجام‌داد مثلا متن را تبدیل به یک شکل هندسی(Geometry) کرد و با شکل اصلی ترکیب‌نمود(Union) این کار یک عیب بزرگ دارد و آن این است که این تبدیل زمان‌بر بوده و از نظر منطقی روش مناسبی نیست، روش دیگر آن است که موقع رندر(Render) شدن شکل، متن را بنویسیم یعنی قطعه کد زیر:

protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
            if (ShowLable)
            {
                drawingContext.DrawText(_lable, _tp);// drow text at point of _tp
            }
        }

پس از افزودن قطعه کد بالا به کلاس، برنامه را ذخیره و کمپایل نمایید و سپس در پنجره MainWindow در قسمت تعریف Window کد زیر را قراردهید:

xmlns:local="clr-namespace:prjPieChart"

توجه‌کنید که در این‌جا باید نام پروژه خودتان را قراردهید. در میان دو دستور Grid نیز کد زیر را اضافه کنید:

<local:PieSliceShape StartAngle="0" Angle="25" Lable="Qliksaaz1" Fill="Blue" Width="200" Height="200" Stroke="White" StrokeThickness="0" Indent="0" Doughnut="False"/>
        <local:PieSliceShape StartAngle="25" Angle="20" Lable="Qliksaaz2" Fill="Red" Width="200" Height="200" Stroke="White" StrokeThickness="0" Indent="30" Doughnut="True"/>
        <local:PieSliceShape StartAngle="45" Angle="80" Lable="Qliksaaz4" Fill="Aqua" Width="200" Height="200" Stroke="White" StrokeThickness="0" Indent="0" Doughnut="False"/>
        <local:PieSliceShape StartAngle="125" Angle="235" Lable="Qliksaaz4" Fill="Bisque" Width="200" Height="200" Stroke="White" StrokeThickness="0" Indent="10" Doughnut="True"/>

برنامه را مجدد ذخیره و اجرا نمایید تا شکلی مشابه زیر ظاهر شود.

 


فایلهای مطلب

کپی
لینک اشتراک گذاری

  • 563
  • 0