ابزار خطکش در WPF
برنامهنویسان بخصوص کسانی که برنامه تهیه گزارشهای قابل چاپ ایجاد میکنند حتما نیاز به یک ابزاری برای تعیین محل دقیق قرارگیری اطلاعات دارند مثلا فرضکنید میخواهید درون یک فرم کاغذی چاپشده و آماده که سربرگ شرکت را نیز دارد اطلاعات فاکتور فروش را چاپکرده و تحویلدهید. اینکار درظاهر ساده میباشد اما قرار دادن اطلاعات دقیقا در محلیخاص بسیار سخت است برای نمونه قراردادن نام مشتری دقیقا در محل مورد نظر(یعنی در جلوی کلمه ناممشتری که روی کاغذ چاپ شدهاست) نیاز به چندین بار سعیوخطا دارد تا کار بدرستی انجامشود، اما با سعیوخطا هم دقیقا نمیتوان اطلاعات را درست ارنج(Arrange) نمود. اما ابزاری مانندخطکش که ابعاد کاغذ درآن مشخصباشد کمکی بزرگ در این راه به برنامهنویس میکند. با این ابزار دقت قراردادن اشیا در محلی خاص افزایش یافته و امکان همتراز نمودن افزایش مییابد. البته این ابزار از نظر امکانات بسیار ابتدایی میباشد، یک خطکش باید دارای واحداندازهگیری، قابلیت زوم(Zoom)شدن و یا اسکیلینگ(Scaling)، قابلیت اسکرول(Scroll)شدن و چندین امکان دیگری باشد تا به توان از آن استفاده نمود. در این مقاله سعی میگردد در چند مرحله تکتک قابلیتهای موردنیاز را به آن اضافهنمود.
یک پروژه جدید از نوع WPF و با نام BaseRuler ایجادکنید، یک کلاس جدید داخل آن با نام RulerBase که از FrameworkElement (از نیماسپیس System.Windows) ارثمیبرد ایجادکنید. در نیماسپیس برنامه کد زیر را قراردهید:
public enum MeasureUnits
{
mm,
cm,
inch,
px,
point
}
#region Static class for convert units
public static class MeasureUnitConverter
{
/// <summary>
/// convert value from unitFrom to unitTo
/// </summary>
/// <param name="unitFrom"></param>
/// <param name="value"></param>
/// <param name="unitTo"></param>
/// <returns></returns>
public static double UnitConverter(MeasureUnits unitFrom, MeasureUnits unitTo = MeasureUnits.px, double value = 1d)
{
double _div = 1d;
double _mul = 1d;
switch (unitTo)
{
case MeasureUnits.cm: _mul = 37.79527559055d; break;
case MeasureUnits.mm: _mul = 3.779527559055d; break;
case MeasureUnits.inch: _mul = 96d; break;
case MeasureUnits.point: _mul = 1.333333333333d; break;
}
switch (unitFrom)
{
case MeasureUnits.cm: _div = 37.79527559055d; break;
case MeasureUnits.mm: _div = 3.779527559055d; break;
case MeasureUnits.inch: _div = 96d; break;
case MeasureUnits.point: _div = 1.333333333333d; break;
}
return value * _mul / _div;
}
}
#endregionMeasureUnits واحدهای اندازهگیری را مشخص میکند و تابع استاتیک UnitConverter مقدار یک واحداندازهگیری را از یک واحدخاص به واحدی دیگر تبدیل میکند.(اعدادی که در کد بالا مشاهده میکنید در اینترنت قابل یافتن است، در برنامه بالا واحد پایه برای تبدیل Pixel میباشد و تمامی واحدها نخست بهPixel تبدیلشده و سپس به هم تبدیلمیشوند).
در بخش using اطلاعات زیر را (در صورت عدم وجود) قراردهید:
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;سپس درون کلاس نخست کد زیر را کپی کنید:
#region MeasureUnit
private double _smallTick = 10d;
private double _mediumTick = 50d;
private double _largeTick = 100d;
private void SetMeasureUnitTick()
{
switch (this.MeasureUnit)
{
case MeasureUnits.cm:
case MeasureUnits.inch:
_smallTick = 0.1d;
_mediumTick = 0.5d;
_largeTick = 1d;
break;
case MeasureUnits.mm:
_smallTick = 1d;
_mediumTick = 5d;
_largeTick = 10d;
break;
case MeasureUnits.point:
case MeasureUnits.px:
_smallTick = 10d;
_mediumTick = 50d;
_largeTick = 100d;
break;
}
}
public static readonly DependencyProperty MeasureUnitProperty = DependencyProperty.Register("MeasureUnit",
typeof(MeasureUnits),
typeof(RulerBase),
new FrameworkPropertyMetadata(MeasureUnits.px,
FrameworkPropertyMetadataOptions.AffectsRender,
MeasureUnitPropertyChangedCallback)
);
private static void MeasureUnitPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != e.OldValue) ((RulerBase)d).SetMeasureUnitTick();
}
public MeasureUnits MeasureUnit
{
get { return (MeasureUnits)GetValue(MeasureUnitProperty); }
set { SetValue(MeasureUnitProperty, value); }
}
#endregionخط 30 تا خط 45 ویژگی(Property) واحداندازهگیری(MeasureUnit) را تعریف میکند و خط 2 تا 28 با تغییر ویژگی MeasureUnit مقدارهای تیک(نشانههای روی خطکش که اندازه طول را مشخصمیکند) را تنظیم میکند.
حال تعریف بقیه ویژگیها: کد زیر را در کلاس کپی نمایید.
#region LabelShow
public static readonly DependencyProperty LabelShowProperty = DependencyProperty.Register(
"LabelShow",
typeof(bool),
typeof(RulerBase),
new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender));
public bool LabelShow
{
get { return (bool)GetValue(LabelShowProperty); }
set { SetValue(LabelShowProperty, value); }
}
#endregion
#region Foreground
public static readonly DependencyProperty ForegroundProperty =
DependencyProperty.Register(
"Foreground",
typeof(Brush),
typeof(RulerBase),
new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender));
public Brush Foreground
{
get { return (Brush)GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); }
}
#endregion
#region FontFamily
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register(
"FontFamily",
typeof(FontFamily),
typeof(RulerBase),
new FrameworkPropertyMetadata(new FontFamily("Arial (Body CS)"), FrameworkPropertyMetadataOptions.AffectsRender));
public FontFamily FontFamily
{
get { return (FontFamily)GetValue(FontFamilyProperty); }
set { SetValue(FontFamilyProperty, value); }
}
#endregion
#region FontSize
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.Register(
"FontSize",
typeof(double),
typeof(RulerBase),
new FrameworkPropertyMetadata(11d, FrameworkPropertyMetadataOptions.AffectsRender));
public double FontSize
{
get { return (double)GetValue(FontSizeProperty); }
set { SetValue(FontSizeProperty, value); }
}
#endregion
#region Background
public static readonly DependencyProperty BackgroundProperty =
DependencyProperty.Register(
"Background",
typeof(Brush),
typeof(RulerBase),
new FrameworkPropertyMetadata(Brushes.Gray, FrameworkPropertyMetadataOptions.AffectsRender));
public Brush Background
{
get { return (Brush)GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
}
#endregion
#region Orientation
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register(
"Orientation",
typeof(Orientation),
typeof(RulerBase),
new FrameworkPropertyMetadata(Orientation.Horizontal,
FrameworkPropertyMetadataOptions.AffectsRender));
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
#endregionویژگیهای تعریفشده به شرح زیر است:
- LabelShow: آیا اعداد نشان دادهشود یا نه.
- Foreground: رنگ نوشتهها(اعداد) را مشخصمیکند.
- FontFamily: نوع فونت نوشتهها(اعداد).
- FontSize: اندازه فونت اعداد.
- Background: رنگ زمینه خطکش
- Orientation: مشخص میکند خطکش افقی یا عمودی است.
تاکنون ویژگیها همگی مشخصشدهاند حال لازم است خطکش را براساس این ویژگیها نمایشدهیم برای این منظور تابع OnRender را بازنویسی میکنیم.
/// <summary>
/// render ruler
/// </summary>
/// <param name="drawingContext"></param>
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
System.Windows.Media.Pen _pen = new System.Windows.Media.Pen(this.Foreground, 0.5);//make pen
drawingContext.DrawRectangle(this.Background, _pen, new Rect(this.RenderSize));//draw frame
double _factor = MeasureUnitConverter.UnitConverter(this.MeasureUnit);
if (this.Orientation == System.Windows.Controls.Orientation.Horizontal)
{
double _Sstart = this.RenderSize.Height * 0.80;
double _Mstart = this.RenderSize.Height * 0.65;
double _Lstart = this.RenderSize.Height * 0.50; //max height of tick.
//draw small ticks
for (double _d = _smallTick / _factor; _d <= this.RenderSize.Width; _d += _smallTick / _factor)
drawingContext.DrawLine(_pen, new Point(_d, _Sstart), new Point(_d, this.RenderSize.Height));
//draw medium ticks.
for (double _d = _mediumTick / _factor; _d <= this.RenderSize.Width; _d += _mediumTick / _factor)
drawingContext.DrawLine(_pen, new Point(_d, _Mstart), new Point(_d, _Sstart));
double _lastLabelPos = 0;
double _labelPos = 0;
//draw large ticks and labels
for (double _d = _largeTick / _factor; _d <= this.RenderSize.Width; _d += _largeTick / _factor)
{
drawingContext.DrawLine(_pen, new Point(_d, _Lstart), new Point(_d, _Mstart));
if (this.LabelShow)
{
FormattedText _format = new FormattedText(
(Math.Round(_d * _factor, 0)).ToString(System.Globalization.CultureInfo.CurrentCulture),
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(this.FontFamily,
FontStyles.Normal,
FontWeights.Normal,
FontStretches.Normal),
this.FontSize,
this.Foreground,
null,
TextFormattingMode.Ideal);
_labelPos = _d - _format.Width / 2;
// if labels too near do not show it.
if (_lastLabelPos <= _labelPos)
{
drawingContext.DrawText(_format, new Point(_labelPos, 0));
_lastLabelPos = _d + _format.Width / 2;
}
}
}
}
else
{
double _Sstart = this.RenderSize.Width * 0.80;
double _Mstart = this.RenderSize.Width * 0.65;
double _Lstart = this.RenderSize.Width * 0.50;//max tick width
//Draw small ticks.
for (double _d = _smallTick / _factor; _d <= this.RenderSize.Height; _d += _smallTick / _factor)
drawingContext.DrawLine(_pen, new Point(_Sstart, _d), new Point(this.RenderSize.Width, _d));
//draw medium ticks.
for (double _d = _mediumTick / _factor; _d <= this.RenderSize.Height; _d += _mediumTick / _factor)
drawingContext.DrawLine(_pen, new Point(_Mstart, _d), new Point(_Sstart, _d));
double _lastLabelPos = 0;
double _labelPos = 0;
//draw large tick and label
for (double _d = _largeTick / _factor; _d <= this.RenderSize.Height; _d += _largeTick / _factor)
{
drawingContext.DrawLine(_pen, new Point(_Lstart, _d), new Point(_Mstart, _d));
if (this.LabelShow)
{
FormattedText _format = new FormattedText(
(Math.Round(_d * _factor, 0)).ToString(System.Globalization.CultureInfo.CurrentCulture),
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(this.FontFamily,
FontStyles.Normal,
FontWeights.Normal,
FontStretches.Normal),
this.FontSize,
this.Foreground,
null,
TextFormattingMode.Ideal);
// if labels are too near then do not show label
_labelPos = _d - _format.Width / 2;
if (_lastLabelPos <= _labelPos)
{
RotateTransform _rotatedText = new RotateTransform();
_rotatedText.Angle = -90;// vertical draw tex for vertical ruler.
drawingContext.PushTransform(_rotatedText);
_rotatedText.CenterX = 0;
_rotatedText.CenterY = _d;
drawingContext.DrawText(_format, new Point(-_format.Width / 2, _d));
drawingContext.Pop();
_lastLabelPos = _d + _format.Width / 2;
}
}
}
}
}این کد براساس عمودی یا افقی بودن خطکش علامتها و اعداد را نمایش میدهد.(فقط در خطهای 44 و 85 اگر نمایش اعداد باعث فرو رفتن آنها در همدیگر شود برخی از اعداد نمایش داده میشود) برنامه را ذخیره و کامپایل نمایید، سپس در پنجره MainWindow نخست نیماسپیس برنامه را تعریف کنید(به نام local) وسپس میان دستورات Grid کد زمل زیر را اضافه کنید:
<local:RulerBase Margin="30 0 0 0" HorizontalAlignment="Stretch" Height="30" VerticalAlignment="Top" Background="Aqua"/>
<local:RulerBase Margin="0 30 0 0" Width="30" HorizontalAlignment="Left" VerticalAlignment="Stretch" Orientation="Vertical" Background="AliceBlue" MeasureUnit="cm"/>برنامه را مجدد ذخیرهکنید و اجرا نمایید تا شکلی مانند زیر ظاهر شود.
حال میخواهیم اسکیل(Scale) را به برنامه بیفزاییم، چه باید انجام شود، یعنی قابلیتهای زوماین(ZoomIn) و زومآوت(ZoomOut) به آن افزود. نخست ویژگی زیر را به کلاس اضافه کنید:
#region Scale Property
public double Scale
{
get { return (double)GetValue(ScaleProperty); }
set { SetValue(ScaleProperty, value); }
}
public static readonly DependencyProperty ScaleProperty =
DependencyProperty.Register("Scale",
typeof(double),
typeof(RulerBase),
new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender));
#endregionسپس خط شماره 10 در کد مربوط به تابع OnRender که در بالا توضیح داده شد(خط مربوط به تعریف متغیر_factor) به صورت زیر تغییر دهید:
double _factor = MeasureUnitConverter.UnitConverter(this.MeasureUnit) / (this.Scale == 0 ? 1 : this.Scale);کار تمام است، برنامه را ذخیره کرده و با استفاده از ویژگی Scale در کد زمل مربوط به هر کدام از خطکشها عملیات Zoom انجام دهید.
بروزرسانی:سه شنبه 15 شهريور ماه 1401
این خطکش به خوبی میتواند محل دقیق اشیا را نشان دهد اما چند اشکال یا ویژگی نیاز دارد تا کارایی آن بهتر گردد، دو ویژگی مهم عبارتند از:
- نشانهگر، وقتی بر روی کانواس حرکت میکنیم باید بتوانیم محل موس را روی خطکشهای افقی و عمودی ببینیم.
- ارتباط با کانواس، باید بتوان لینکی میان کانواسی که این خطکش متعلق به آن است برقرار کرد در ضمن درصورت عدم وجود کانواس نباید ایرادی یا error اتفاق بیافتد.
کد زیر را به کلاس BaseRuler تعریف شده در بالا اضافهکنید(توجه کنید بخش تعریف اسکیل باید جایگزین شود):
#region Scale Property
public double Scale
{
get { return (double)GetValue(ScaleProperty); }
set { SetValue(ScaleProperty, value); }
}
public static readonly DependencyProperty ScaleProperty =
DependencyProperty.Register("Scale",
typeof(double),
typeof(RulerAdvance),
new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender, ScalePropertyChangedCallback));
private static void ScalePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RulerAdvance _advRuler = d as RulerAdvance;
_advRuler.ScalePropertyChangedCallback(e);
}
private void ScalePropertyChangedCallback(DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
SetScale(this.Diagram, (double)e.NewValue);
}
}
#endregion
#region Indicator Property
public double Indicator
{
get { return (double)GetValue(IndicatorProperty); }
set { SetValue(IndicatorProperty, value); }
}
public static readonly DependencyProperty IndicatorProperty =
DependencyProperty.Register("Indicator",
typeof(double),
typeof(RulerAdvance),
new FrameworkPropertyMetadata(0d,FrameworkPropertyMetadataOptions.AffectsRender));
#endregion
#region IndicatorBrush Property
public Brush IndicatorBrush
{
get { return (Brush)GetValue(IndicatorBrushProperty); }
set { SetValue(IndicatorBrushProperty, value); }
}
public static readonly DependencyProperty IndicatorBrushProperty =
DependencyProperty.Register("IndicatorBrush",
typeof(Brush),
typeof(RulerAdvance),
new FrameworkPropertyMetadata(Brushes.Red));
#endregion
#region Diagram
public Panel Diagram
{
get { return (Panel)GetValue(DiagramProperty); }
set { SetValue(DiagramProperty, value); }
}
public static readonly DependencyProperty DiagramProperty =
DependencyProperty.Register("Diagram",
typeof(Panel),
typeof(RulerAdvance),
new FrameworkPropertyMetadata(null, DiagramPropertyChangedCallback));
ScrollViewer _rulerScrollViewer;
private static void DiagramPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RulerAdvance _advRuler = d as RulerAdvance;
_advRuler.DiagramPropertyChangedCallback(e);
}
private void DiagramPropertyChangedCallback(DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != null)
{
((Panel)e.OldValue).MouseMove -= this.DiagramMouseMove;
}
if (e.NewValue != null)
{
Panel _p = e.NewValue as Panel;
if (_p != null)
{
_p.MouseMove += this.DiagramMouseMove;
this.SetScale(_p,this.Scale);
}
}
}
private void SetScale(Panel panel, double scale)
{
if (panel != null)
{
ScaleTransform _scaleTransform = panel.LayoutTransform as ScaleTransform;
if (_scaleTransform == null)
{
panel.LayoutTransform = new ScaleTransform();
_scaleTransform = panel.LayoutTransform as ScaleTransform;
}
_scaleTransform.ScaleX = scale;
_scaleTransform.ScaleY = scale;
}
}
private void DiagramMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
Point _p = e.GetPosition((Panel)sender);
if (Orientation == Orientation.Horizontal)
Indicator = _p.X;
else
Indicator = _p.Y;
}
#endregionویژگی Indicator یک نشانهگر تعریفمیکند تا محل حرکت موس را درون کانواس یا پانلمان را نشان دهد. ویژگی Diagram این امکان را میدهد تا مشخص کنیم که حرکت موس روی کدام پانل را میخواهیم رصد کنیم(و در صورت تغییر اسکیل آن پانل نیز تغییر اسکیل میدهد).
تابع DiagramPropertyChangedCallback یک موسمووو ایونت(MouseMove Event) برای Diagram تعریف میکند تا با حرکت موس بتوان محل آن را در خطکش نمایش داد. تابع SetScale اسکیل پانلمان را تغییرمیدهد. توجه کنید تمامی توابع انتقال(Transform Functions) مانند دوران، تغییرات اندازهای و... همگی با استفاده از توابع فوق ScaleTransform، SkewTransform، RotateTransform، TranslateTransform قابلانجاماست.
برنامه را ذخیره و کامپایل نمایید.
در پنجره زمل MainWindow کد زیر را درون دستور گرید(Grid) واردنمایید:
<local:RulerAdvance HorizontalAlignment="Left" Height="30" VerticalAlignment="Top" Width="517" Diagram="{Binding ElementName=mycanvas}" Scale="1"/>
<Canvas x:Name="mycanvas" HorizontalAlignment="Left" Height="250" Margin="0,60,0,0" VerticalAlignment="Top" Width="507">
<Ellipse Fill="#FFF4F4F5" Height="100" Canvas.Left="35" Stroke="Black" Canvas.Top="73" Width="221"/>
</Canvas>در کد بالا به کانواس نام mycanvas دادهایم و برای ویژگی Diagram خطکشمان آن را با استفاده از دستور Binding و ElementName به کانواس مرتبط کردهایم. برنامه را ذخیره کرده و با استفاده از ویژگی اسکیل مربوط به خطکش اسکیل کانواس و خطکشتان را تغییردهید. سپس با اجرای برنامه و حرکت دادن موس روی شکل بیضیگون نشانهگر را در خطکش خواهید دید(دقت کنید ایونت موسمووو فقط روی اشکال و اشیای درون کانواس کار میکند). اگر نیاز به خطکش عمودی دارید آن را به برنامه بیفزایید.
نکته: اگر بخواهیم ایونت موسمووو برای تمامی کانواس کارکند فقط کافی است رنگ پس زمینه برای کانواس تنظیم کنیم(حتیBackground="Transparent" نیز کارمیکند).
بروزرسانی:شنبه 19 شهريور ماه 1401
به پیوست فایل خطکش با استفاده از یک کانواس قابل اسکرولشدن اضافهگردیده، توجه کنید که با تغییر ویژگی اسکیل در کانواس ویژگی اسکیل در هر دو خطکش افقی و عمودی تغییر میکند و نیز با اسکرول کردن کانواس بهصورت عمودی خطکش عمودی اسکرول میگردد و با اسکرول افقی، خطکش افقی اسکرول میشود. همچنین برخی از بایندها(Binding) درون فایل MainRuler.xaml انجام شده و برخی دیگر درون MainRuler.cs. دلیل این کار محدودیتها و بیشتر برای آموزش نحوه بایندیگ توسط برنامه انجام شده. این برنامه بخشی از یک برنامه رسم گراف مربوط به درس گراف تئوری(Graph Theory) در رشته کامپیوتر میباشد(شکل اصلی برنامه مشابه همانیاست که به عنوان عکس اصلی مقاله ثبتشده است، اصل برنامه قابلیتهایی چون رسم گراف، الگوریتم یافتن کوتاهترین مسیر، آسیبپذیری شبکهای، پیمایش عمقترتیب(DFS) و سطحترتیب(BFS) و ... و نیز برای ایجاد خروجی تصویری(Image) و پی دی اف(PDF) و قابلیتهای دیگر ایجادگردیدهاست. بقیه بخش های برنامه به صورت مجزا از هم در سایت قرارخواهد گرفت.
فایلهای مطلب
ابزار خطکش در WPF (33.08 کیلو بایت)
ابزار خطکش در WPF(با نشانگر) (38.1 کیلو بایت)
ابزار خطکش در WPF(با استفاده از تمپلیت) (88.21 کیلو بایت)




