پیاده سازی یک کنترل Chart در #C
یکشنبه 13 دی 1394در این مقاله قصد داریم چارتی را پیاده سازی کنیم که قابلیت نمایش چند نمودار را همزمان داشته باشد. قابلیت انعطاف پذیر ی داشته باشد و نمودارها بدون در نظر گرفتن محدوده چارت با آن تناسب داشته باشند. از کنترل چارت مایکروسافت استفاده خواهیم کرد و با توجه به نیازهای خود، قابلیت ها و امکانات آن را گسترش خواهیم داد.
در این مقاله قصد داریم چارتی را پیاده سازی کنیم که قابلیت نمایش چند نمودار را همزمان داشته باشد. قابلیت انعطاف پذیر ی داشته باشد و نمودارها بدون در نظر گرفتن محدوده چارت با آن تناسب داشته باشند. از کنترل چارت مایکروسافت استفاده خواهیم کرد و با توجه به نیازهای خود، قابلیت ها و امکانات آن را گسترش خواهیم داد.
مشکل با نمودارهای دیگر این است که به محدوده های بسیار متفاوتی تمایل دارند، که ترکیب آنها را در یک چارت مشکل می سازد. سایز کنترل نمودار قابل متناسب با همه نمودار ها تغییر میکند ، یک گراف که محدوده ای بین 1 تا 100 دارد می تواند گراف دیگری که محدوده ای بین 0.001 تا 0.01 دارد به صورت مجازی ایجاد کند. اما اگر بخواهیم دو گراف را بدون درنظر گرفتن محدوده شان مقایسه کنیم چه می شود؟
از این رو به یک محدوده چارت برای هر گراف یا گراف هایی که می توانند دسته بندی شوند نیاز داریم. این محدوده های چارت انباشته می شوند و هر کدام محور ها و سایز خود را بر اساس گرافی که نمایش داده می شود دارند. نتیجه یک چارت است که گراف های خود را دارد که متناسب با محدوده چارت قرار میگیرند.
چارت مجموعه ای از محدوده های نموداری مختلف است که کنار هم انباشته شده اند. برای هر گراف تک سه محدوده چارت مختلف نیاز داریم. یکی برای نمایش گراف ، یکی برای محور X و دیگری برای محور Y . این محدوده های چارت با یک شماره GUID به یکدیگر مقید می شوند.
از آنجا که چارت اصلی NET. از این قابلیت پشتیبانی نمیکند، هر محدوده چارت را با استفاده از کلاس ChartAreaTagger برچسب زده ایم.
/// <summary> /// We use this class to tag our chart areas. /// </summary> private class ChartAreaTagger { public ChartAreaTagger(Guid _guid, ChartAreaType _chartAreaType, Color _chartColor) { this.guid = _guid; this.chartAreaType = _chartAreaType; this.chartColor = _chartColor; } private Guid guid; /// <summary> /// This is the unique value that binds all three chart areas together (X, Y and Chart). /// </summary> public Guid @Guid { get { return guid; } } private ChartAreaType chartAreaType; /// <summary> /// This defines which type of chart area are we dealing with. /// </summary> public ChartAreaType @ChartAreaType { get { return chartAreaType; } } private Color chartColor; public Color ChartColor { get { return chartColor; } } } /// <summary> /// This enumerates the three possible chart area types. /// </summary> private enum ChartAreaType { /// <summary> /// This is for X axis labels. /// </summary> AXIS_X = 2, /// <summary> /// This is for Y axis labels. /// </summary> AXIS_Y = 4, /// <summary> /// This is for displaying our chart. /// </summary> CHART = 8 }
کلاس ChartAreaTagger همه اطلاعات لازم برای قرار دادن گراف های مختلف را بر روی چارت و اجرای Layout فراهم میکند.
عنصر GUI
چارت یک UserControl را ارث بری میکند که یه شیء دارد : کنترل چارتNET. . کنترل یک محدوده چارت و یک نقش اضافه شده در طراحی دارد. این دو شیء هیچگاه تغییر نمیکنند.
برای حذف و اضافه کردن محدوده های چارت در صورت نیاز ،باید فرمت مشخصی در طراحی داشته باشیم، برای انجام این کار نیاز به یک متد در کد خود داریم که همه چارت ها فرمت مشخصی داشته باشند:
/// The routine to format our chart areas (all are formatted the same). /// </summary> private void FormatChartArea(ChartArea _chartArea) { ... }
محدوده چارت اصلی که برای نمایش grid و نگهداری گراف ها استفاده می شود ، به بعضی استثناها نیاز دارد که به صورت زیر متد Load را برای آن مینویسم :
this.chart1.ChartAreas[0].AxisX.LabelStyle.Enabled = false; this.chart1.ChartAreas[0].AxisX.MajorTickMark.Enabled = false; this.chart1.ChartAreas[0].AxisX.LineColor = Color.Transparent; this.chart1.ChartAreas[0].AxisY.LabelStyle.Enabled = false; this.chart1.ChartAreas[0].AxisY.MajorTickMark.Enabled = false; this.chart1.ChartAreas[0].AxisY.LineColor = Color.Transparent;
اضافه کردن یک گراف
اضافه کردن یک گراف با چک کردن اینکه آیا یک محدوده مناسب در چارت موجود است یا نه شروع می شود. این کار را با استفاده از بدست آوردن حداقل و حداکثر چارت جدید و مقایسه آن با چارت موجود انجام می دهیم .
//obtain minimums and maximums... float _minX = this.GetMinX(_points); float _maxX = this.GetMaxX(_points); float _minY = this.GetMinY(_points); float _maxY = this.GetMaxY(_points); //...so we can obtain the right chart areas ChartArea _chartAreaAxisX = null; ChartArea _chartAreaAxisY = null; ChartArea _chartAreaChart = this.GetBestSuitedChartArea( _minX, _maxX, _minY, _maxY, out _chartAreaAxisX, out _chartAreaAxisY);
ما تفاوت دو چارت را به صورت درصدی حساب میکنیم ، ممکن است تفاوت حساب شده مقدار بزرگی شود در صورتی که با مقادیر کمی این تفاوت حساب شده باشد. به طور مثال دو گراف را در نظر بگیرید که یکی 0 تا 100 و دیگری 1 تا 100 باشد . گراف در منطقه چارت مشابه قرار خواهد گرفت. اما تفاوت درصدی آنها برای 0 تا 1 ، 100 % خواهد بود. از آنجایی که این مقدار بیشتر از MAX_PERCENTAL_DIFFERENCE است که برای 10% تنظیم می شود ، گراف در محدوده چارتی خود قرار خواهد گرفت.
برای مقابله با این موضوع ، تفاوت درصدی در گراف ها را بررسی میکنیم. اگر مینیمم ، ماکسیمم و تناسب آنها خارج از MAX_PRECENTRAL_DIFFERENCE بود ، آنگاه محدوده چارت مناسب نخواهد بود.
/// <summary> /// Routine for obtaining the best suited chart area from the already present one, /// based on the given chart proportions. /// </summary> private ChartArea GetBestSuitedChartArea( float _minX, float _maxX, float _minY, float _maxY, out ChartArea _suitableAxisX, out ChartArea _suitableAxisY) { List<ChartArea> _suitableAxisXtmp = new List<ChartArea>(); List<ChartArea> _suitableAxisYtmp = new List<ChartArea>(); ChartArea _suitableChartArea = null; foreach (ChartArea _chartArea in this.chart1.ChartAreas) { //just a quick check if this chart area is even worth considering if (double.IsNaN(_chartArea.AxisX.Minimum)) { continue; } if (double.IsInfinity(_chartArea.AxisX.Maximum)) { continue; } if (double.IsNaN(_chartArea.AxisY.Minimum)) { continue; } if (double.IsInfinity(_chartArea.AxisY.Maximum)) { continue; } if (_chartArea.Tag is ChartAreaTagger) { ChartAreaTagger _chartAreaTagger = (ChartAreaTagger)_chartArea.Tag; #region "for X axis" if (this.GetPercentageDifference (_chartArea.AxisX.Minimum, _minX) > MAX_PERCENTAGE_DIFFERENCE) { if ((this.GetPercentageDifference(_chartArea.AxisX.Maximum - _chartArea.AxisX.Minimum, _maxX - _minX) > MAX_PERCENTAGE_DIFFERENCE) || (this.GetPercentageDifference(_chartArea.AxisX.Maximum, _maxX) > MAX_PERCENTAGE_DIFFERENCE)) { continue; } } if (this.GetPercentageDifference (_chartArea.AxisX.Maximum, _maxX) > MAX_PERCENTAGE_DIFFERENCE) { if ((this.GetPercentageDifference(_chartArea.AxisX.Maximum - _chartArea.AxisX.Minimum, _maxX - _minX) > MAX_PERCENTAGE_DIFFERENCE) || (this.GetPercentageDifference (_chartArea.AxisX.Minimum, _minX) > MAX_PERCENTAGE_DIFFERENCE)) { continue; } } if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_X) { _suitableAxisXtmp.Add(_chartArea); } #endregion "for X axis" #region "for Y axis" if (this.GetPercentageDifference(_chartArea.AxisY.Minimum, _minY) > MAX_PERCENTAGE_DIFFERENCE) { if ((this.GetPercentageDifference(_chartArea.AxisY.Maximum - _chartArea.AxisY.Minimum, _maxY - _minY) > MAX_PERCENTAGE_DIFFERENCE) || (this.GetPercentageDifference (_chartArea.AxisY.Maximum, _maxY) > MAX_PERCENTAGE_DIFFERENCE)) { continue; } } if (this.GetPercentageDifference (_chartArea.AxisY.Maximum, _maxY) > MAX_PERCENTAGE_DIFFERENCE) { if ((this.GetPercentageDifference(_chartArea.AxisY.Maximum - _chartArea.AxisY.Minimum, _maxY - _minY) > MAX_PERCENTAGE_DIFFERENCE) || (this.GetPercentageDifference (_chartArea.AxisY.Minimum, _minY) > MAX_PERCENTAGE_DIFFERENCE)) { continue; } } if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_Y) { _suitableAxisYtmp.Add(_chartArea); } #endregion "for Y axis" //if we are here, that means that both X and Y axes are suitable, //so the chart area is suitable as well. if (_chartAreaTagger.ChartAreaType == ChartAreaType.CHART) { _suitableChartArea = _chartArea; } } } //when there are many suitable areas, we take the first ones. if (_suitableAxisXtmp.Count > 0) { _suitableAxisX = _suitableAxisXtmp[0]; } else { _suitableAxisX = null; } if (_suitableAxisYtmp.Count > 0) { _suitableAxisY = _suitableAxisYtmp[0]; } else { _suitableAxisY = null; } return _suitableChartArea; }
اگر هیچ محدوده چارت مناسبی نبود ، آنگاه :
. یک جدید ایجاد میکنیم.
. آن را فرمت دهی میکنیم.
. اندازه ابعاد آن را با توجه به گراف تنظیم میکنیم.
Guid _guid = Guid.NewGuid(); if ((_chartAreaAxisX == null) || (_chartAreaAxisY == null)) { #region "one for showing the chart" _chartAreaChart = new ChartArea(Guid.NewGuid().ToString()); this.FormatChartArea(_chartAreaChart); _chartAreaChart.Tag = new ChartAreaTagger(_guid, ChartAreaType.CHART, _color); _chartAreaChart.BackColor = Color.Transparent; _chartAreaChart.IsSameFontSizeForAllAxes = this.chart1.ChartAreas[0].IsSameFontSizeForAllAxes; this.chart1.ChartAreas.Add(_chartAreaChart); //format for chart (no labels) this.FormatAxis(_chartAreaChart.AxisX, _minX, _maxX, false, true, false); this.FormatAxis(_chartAreaChart.AxisY, _minY, _maxY, false, true, false); #endregion "one for showing the chart" } if (_chartAreaAxisX == null) { #region "one for X axis" _chartAreaAxisX = new ChartArea(Guid.NewGuid().ToString()); this.FormatChartArea(_chartAreaAxisX); _chartAreaAxisX.BackColor = Color.Transparent; _chartAreaAxisX.IsSameFontSizeForAllAxes = this.chart1.ChartAreas[0].IsSameFontSizeForAllAxes; _chartAreaAxisX.Tag = new ChartAreaTagger(_guid, ChartAreaType.AXIS_X, _color); _chartAreaAxisX.AxisX.LabelStyle.ForeColor = _color; this.chart1.ChartAreas.Add(_chartAreaAxisX); //format for X axis this.FormatAxis(_chartAreaAxisX.AxisX, _minX, _maxX, true, false, true); //this is dummy this.FormatAxis(_chartAreaAxisX.AxisY, _minY, _maxY, false, false, false); #endregion "one for X axis" } if (_chartAreaAxisY == null) { #region "one for Y axis" _chartAreaAxisY = new ChartArea(Guid.NewGuid().ToString()); this.FormatChartArea(_chartAreaAxisY); _chartAreaAxisY.BackColor = Color.Transparent; _chartAreaAxisY.IsSameFontSizeForAllAxes = this.chart1.ChartAreas[0].IsSameFontSizeForAllAxes; _chartAreaAxisY.Tag = new ChartAreaTagger(_guid, ChartAreaType.AXIS_Y, _color); _chartAreaAxisY.AxisY.LabelStyle.ForeColor = _color; this.chart1.ChartAreas.Add(_chartAreaAxisY); //this is dummy this.FormatAxis(_chartAreaAxisY.AxisX, _minX, _maxX, false, false, false); //format for Y axis this.FormatAxis(_chartAreaAxisY.AxisY, _minY, _maxY, true, false, true); #endregion "one for Y axis" }
اگربتوانیم یک محدوده چارت متناسب که با مینیمم و ماکسیمم ابعاد همخوانی داشته باشد پیدا کنیم، آنگاه :
. چارت جدید خود را در آن قرار می دهیم .
. تغییر اندازه محدوده چارت که پس از آن می توانید هر دو نمودار را در آن داشته باشید.
#region "check minimums and maximums of the eventual existing charts" IEnumerable<PointF>[] _functionsOnChartArea = this.GetFunctionsOnChartArea(_chartAreaChart); foreach (IEnumerable<PointF> _functionTmp in _functionsOnChartArea) { float _functionMinX = this.GetMinX(_functionTmp); if (_functionMinX < _minX) { _minX = _functionMinX; } float _functionMaxX = this.GetMaxX(_functionTmp); if (_functionMaxX > _maxX) { _maxX = _functionMaxX; } float _functionMinY = this.GetMinY(_functionTmp); if (_functionMinY < _minY) { _minY = _functionMinY; } float _functionMaxY = this.GetMaxY(_functionTmp); if (_functionMaxY > _maxY) { _maxY = _functionMaxY; } } #endregion "check minimums and maximums of the eventual existing charts" #region "and adjust the axes accordingly, so they can take the old chart plus the newly added" this.FormatAxis(_chartAreaAxisX.AxisX, _minX, _maxX, true, false, true); this.FormatAxis(_chartAreaAxisX.AxisY, _minY, _maxY, false, false, false); this.FormatAxis(_chartAreaAxisY.AxisX, _minX, _maxX, false, false, false); this.FormatAxis(_chartAreaAxisY.AxisY, _minY, _maxY, true, false, true); this.FormatAxis(_chartAreaChart.AxisX, _minX, _maxX, false, true, false); this.FormatAxis(_chartAreaChart.AxisY, _minY, _maxY, false, true, false); #endregion "and adjust the axes accordingly, so they can take the old chart plus the newly added" #region "then draw our chart" //set the color to transparent, because we don't want chart to be shown on this axis this.DrawGraph(_chartAreaAxisX, _points, null, Color.Transparent); //set the color to transparent, because we don't want chart to be shown on this axis this.DrawGraph(_chartAreaAxisY, _points, null, Color.Transparent); //finally, add the chart and draw it this.DrawGraph(_chartAreaChart, _points, _legendTitle, _color); #endregion "then draw our chart"
روش ذکر شده باید برای محور X و Y و چارت انجام شود.
چارت در نهایت کشیده شده است و تنها به قدم آخر که Layout است نیاز داریم.
تنها چیزی مهم این است که با توجه به گراف ها می توان محدوده های بسیار متفاوتی وجود داشته داشته باشد و باید همه چیز را بر اساس در صد در نظر بگیریم.
برای درستی وضعیت محور های اضافی برچسبی را برای آنها در نظر میگیریم. برای محور X برچسب Height و برای محور Y برچسب Width را در نظر گرفتیم. بدیت ترتیب طرح وضعیت چارت اضافی را محاسبه کرده ایم که هر کدام توسط سایز برچسب تعیین می شوند.
private void PerformChartLayout() { if (this.chart1.Series.Count == 0) { //hide the base chart area this.chart1.ChartAreas[0].Visible = false; } else { //make the base chart area visible this.chart1.ChartAreas[0].Visible = true; //we must have a simple graph on our base series, otherwise the grid //would be hidden because of the MS chart's logic (but we make it transparent) if (!this.chart1.Series.Any(delegate (Series _series) { if (_series.ChartArea == this.chart1.ChartAreas[0].Name) { return true; } else { return false; } })) { //as we set our grid in percentages, the graph must be of suitable range this.DrawGraph( this.chart1.ChartAreas[0], new PointF[] { new PointF(1f, 1f), new PointF(100f, 100f) }, null, Color.Transparent); } float _offsetX = 0; float _offsetY = 0; #region "position the chart plot positions with respect to label sizes" //we do this reversed, so the most recently added charts expand to outside foreach (ChartArea _chartArea in this.chart1.ChartAreas.Reverse()) { //set the whole chart area position _chartArea.Position.FromRectangleF(this.ChartPosition); if (_chartArea.Tag is ChartAreaTagger) { ChartAreaTagger _chartAreaTagger = (ChartAreaTagger)_chartArea.Tag; if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_X) { //set the size of the tick marks _chartArea.AxisX.MajorTickMark.Size = this.TickMarkSizePercentageY; //then measure the height of the labels float _axisXLabelHeight = this.GetAxisXLabelHeightPercentage (_chartArea) + this.TickMarkSizePercentageY; //and compute the position of the chart plot position //based on the size of the label and the offset, //which increases for every chart area RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition(); _chartInnerPlotPosition.Y -= (_axisXLabelHeight + _offsetY); _chartInnerPlotPosition.Height -= (_axisXLabelHeight + _offsetY); _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition); //increase the offset! _offsetY += _axisXLabelHeight; } else if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_Y) { //set the size of the tick marks _chartArea.AxisY.MajorTickMark.Size = this.TickMarkSizePercentageX; //then measure the width of the labels float _axisYLabelWidth = this.GetAxisYLabelWidthPercentage (_chartArea) + this.TickMarkSizePercentageX; //and compute the position of the chart plot position //based on the size of the label and the offset, //which increases for every chart area RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition(); _chartInnerPlotPosition.X += (_axisYLabelWidth + _offsetX); _chartInnerPlotPosition.Width -= (_axisYLabelWidth + _offsetX); _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition); //increase the offset! _offsetX += _axisYLabelWidth; } } } #endregion "position the chart plot positions with respect to label sizes" //the chart areas are now positioned accordingly to the label sizes, //but this is not enough; we must position them also by the full offsets //(which we computed while positioning them), //so we just iterate through chart areas once again and use the computed offsets. //The areas will thus start from offset and not from zero. #region "position the areas with respect to the offset" foreach (ChartArea _chartArea in this.chart1.ChartAreas) { //set the whole chart area position _chartArea.Position.FromRectangleF(this.ChartPosition); if (_chartArea.Tag is ChartAreaTagger) { ChartAreaTagger _chartAreaTagger = (ChartAreaTagger)_chartArea.Tag; if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_X) { //this moves to the right of the screen and decreases in width RectangleF _chartInnerPlotPosition = _chartArea.InnerPlotPosition.ToRectangleF(); _chartInnerPlotPosition.X += _offsetX; _chartInnerPlotPosition.Width -= _offsetX; _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition); } else if (_chartAreaTagger.ChartAreaType == ChartAreaType.AXIS_Y) { //this doesn't move, but decreases in height RectangleF _chartInnerPlotPosition = _chartArea.InnerPlotPosition.ToRectangleF(); _chartInnerPlotPosition.Height -= (_offsetY * 2); _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition); } else if (_chartAreaTagger.ChartAreaType == ChartAreaType.CHART) { //this moves to the right of the screen //(while not moving by Y), decreases in width and in height RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition(); _chartInnerPlotPosition.X += _offsetX; _chartInnerPlotPosition.Width -= _offsetX; _chartInnerPlotPosition.Height -= (_offsetY * 2); _chartArea.InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition); } } else if (_chartArea == this.chart1.ChartAreas[0]) //don't forget the base chart area, //which also must take the offset into account { //this does the same as the other CHART chart areas RectangleF _chartInnerPlotPosition = this.GetChartInnerPlotPosition(); _chartInnerPlotPosition.X += _offsetX; _chartInnerPlotPosition.Width -= _offsetX; _chartInnerPlotPosition.Height -= (_offsetY * 2); this.chart1.ChartAreas[0].InnerPlotPosition.FromRectangleF(_chartInnerPlotPosition); } } #endregion "position the areas with respect to the offset" } }
از متد PrrformLayout() جهت اطمینان از آنکه همیشه یک گراف در محدوده چارت پایه وجود دارد ، به طوری که grid نمایش داده خواهد شد.
- C#.net
- 2k بازدید
- 1 تشکر