ایجاد یک کنترل Guage در WPF (قسمت آخر)
در کد ارائه شده مثال ، به منظور توصیف (لطفا توجه داشته باشید که عمدا از عبارت ترسیم استفاده نشده است، زیرا که این کلاس اساسا برای توصیف (Describe) ویژگیهای ظاهری یک شکل دو بعدی کاربرد دارد و عملیات ترسیم (Drawing) و استفاده از یک میزبان بصری (Visual Container) و قابل دید مراحل دیگری وجود دارد که در ادامه توضیح داده خواهد شد) ویژگیهای مورد نیاز جهت ترسیم یک شکل ساده دو بعدی از Geometry استفاده شده است. کلاس Geometry و همه مشتقات آن از جمله ElipseGeomtry، PathGeometry و .... صرفا برای توصیف یک یا مجموعه ای از موجودیت های گرافیکی و رصد نقطه تلاقی اشاره گر Mouse با این اشکال (Hit Testing) و تعیین دریچه های برش نما (Clip Regions) استفاده میشوند. در بحث تواناییهای WPF برای ایجاد یک انیمیشن نیز، که اتفاقا یکی از نقاط قوت WPF نیز محسوب میگردد از Geometry به عنوان مسیر (Path) یک یا چند سکانس انیمیشن استفاده میشود.
در یک جمع بندی کلی، به نظر میرسد که کلاسهای Geometry و Shape در ظاهر با تواناییهایی کمابیش یکسان برای توصیف اشکال دو بعدی استفاده میگردند. به عنوان مثال در یک نگاه کلی کلاسهای EllipseGeometry و Ellipse که اولی از Geometry و دومی از Shape منشعب میشوند برای توصیف/ترسیم یک دایره/بیضی کاملا یکسان به نظر میرسند اما حقیقتا تفاوتهای مهمی وجود دارد که باید به آن توجه داشت.
در ابتدا کلاس Shape انشعابی (Derived) از کلاس بسیار مهم و کلیدی FrameworkElement می باشد. در واقع یک موجودیت گرافیکی ایجاد شده توسط Shape علاوه بر خواص گرافیکی از دیدگاه WPF یک Element با تواناییهای بسیار خاص تلقی میگردد. از مهمترین این تواناییها میتوان به مواردی مانند قابلیت ذاتی ترسیم و مشارکت در مکانیسم های بعضا پیچیده چیدمان یا Layout اشاره کرد که Geometry و مشتقات آن فاقد این تواناییها می باشد.
دقیقا به همین دلیل است که در اغلب منابع معتبر هنگام معرفی Geometry یا برخی دیگر از کلاسهای سطوح پایین تری که در گرافیک دخیل هستند از اصطلاح Lightweight استفاده میشود که اشاره به بار بسیار اندکیست که در مقایسه با Shape در هنگام استفاده از آنها به سیستم تحمیل میگردد.
به نظرم تا حدودی از اصل موضوع فاصله گرفته ایم و بنابراین قبل از بازگشت به موضوع اصلی به نظرم توجه به این نکته خالی از لطف نیست که یکی از کلاسهای منشعب از Shape یعنی کلاس Path در واقع از Geometry برای توصیف و ترسیم اشکال گرافیکی استفاده میکند که اتفاقا در مبحث رسم اشکال گرافیکی از تنوع قابلیت شگفت انگیزی برخوردار است.
در کد مثالی که در قسمت اول ارائه شد، ما به سادگی با استفاده از ترکیب سه Geometry ساده (دو قطعه خط و یک کمان) شکل ساده مورد نظر خودمان را توصیف نموده ایم. (مجددا تکرار میکنم که عملیات فیزیکی ترسیم هنوز انجام نشده است) . مجموعه دستورات این توصیف ساده در یک Stream خاص منظوره که کلاس StreamGeometry می باشد قرار میگیرد. با محاصره کردن این توصیف در داخل یک بلاک using تلاش میکنیم که مدیریت استفاده از منابع تخصیص یافته برای انجام این کار را تا حد امکان به عهده گرفته و آن را بهینه نماییم.
اینک برای ترسیم اطلاعاتی که قبلا توصیف شده نیاز به یک DrawingContext ضروریست.
از آنجاییکه کلاس ایجاد شده ما یعنی SectorVisual کلاسی منشعب از DrawingVisual است توصیه میشود برای به دست آوردن یک DrawingContext از متد RenderOpen که برای همین منظور در DrawingVisual پیش بینی شده است استفاده نماییم. چنانچه در مثال مشاهده میشود مجددا برای مدیریت بهتر منابع سیستم استفاده از DrawingContext در یک بلاک using محصور شده است که بلافاصله بعد از فراخوانی DrawGeometry و خروج از بلاک using ، اجبار به آزادسازی حافظه انجام میشود. نام متد DrawGeometry به روشنی نشان میدهد که این متد، توصیف مرتبط با یک موجودیت گرافیکی که در قالب یک Geometry ذخیره شده است را دریافت نموده و گرافیک مذکور با قلم (Pen) و Brush دلخواه را ترسیم میکند.
اما متاسفانه هنوز کل پروسه به اتمام نرسیده است. برای رویت و چیدمان این موجودیت گرافیکی نیاز به یکی از کلاسهای آگاه به پروسه Layout الزامیست. به عبارت ساده تر این کلاسها، کلاسهایی هستند که در ساختار سلسله مرتبی کلاسهای WPF از UIElement منشعب شده اند. برای رفع این مشکل دو راه حل اصلی وجود دارد . یکی از آنها Override کردن متد OnRender و یا اساسا ایجاد یک کلاس سفارشی منشعب از UIElement است . چنانچه در انتهای همین قسمت مشاهده خواهیم نمود، برای ایجاد یک کنترل Guage ، که هدف نهایی ما از کل این مباحث دو قسمتی بوده، از روش اول استفاده شده است. در کد زیر روش استفاده از راه دوم نیز نشان داده شده است:
public class VisualContainer : UIElement
{
private VisualCollection _children;
private SectorVisual _visual = new SectorVisual();
protected override Visual GetVisualChild(int index)
{
return _visual;
}
protected override int VisualChildrenCount
{
get { return 1; }
}
}
در ساده ترین شکل ممکن فقط کافیست دو متد GetVisualChild و VisualChildCount را به ترتیبی که در مثال نشان داده شده است Override نماییم.
اگر در Visual Studio این کار را انجام بدهیم، بعد از یک بار Compile کردن کد مذکور، یک نمونه قابل استفاده از VisualContainer را در جعبه ابزارها (Toolbox) مشاهده خواهیم کرد که به سادگی با Drag نمودن آن در داخل پنجره یا Page میزبان ، یک نمونه از این کلاس ایجاد می شود که نمایش تصویر مورد نظر ما میسر خواهد شد. به جای این روش بدیهیست که با نمونه سازی این کلاس در کد یا در XAML به نتیجه مشابهی دست خواهیم یافت.
این مثال بهانه ای بود برای یک بررسی اجمالی گرافیک در WPF که همانطور که در قسمت اول این بحث مطرح شد هدف اصلی من ایجاد یک کنترل گرافیکی سفارشی (Guage) است که کد آن را در زیر مشاهده میکنید:
public class Guage : FrameworkElement
{
public int Ticks { get; set; }
public Size TickSize { get; set; }
public Brush TickBrush { get; set; }
protected override void OnRender(DrawingContext dc)
{
double radius = Math.Min((RenderSize.Width – TickSize.Width) / 2,
(RenderSize.Height – TickSize.Height) / 2);
Point center = new Point((RenderSize.Width – TickSize.Width) / 2,
(RenderSize.Height – TickSize.Height) / 2);
Point center2 = new Point(RenderSize.Width / 2,
RenderSize.Height / 2);
if (Ticks > 50)
{
dc.DrawEllipse(Brushes.Transparent, new Pen(Brushes.Black, 2), center2, 15, 15);
}
for (int i = 0; i <= Ticks; i++)
{
double ratio = (double)I / Ticks;
double x = center.X + radius * Math.Cos(Math.PI * ratio);
double y = center.Y – radius * Math.Sin(Math.PI * ratio);
Typeface tf = new Typeface(“Tahoma”);
CultureInfo ci = new CultureInfo(“fa-IR”);
int adjust = 100 / Ticks;
int result = Math.Abs(100 – (i * adjust));
FormattedText ft = new FormattedText((result).ToString(), ci, FlowDirection.LeftToRight, tf, 8.5, new SolidColorBrush(Colors.Black));
Rect currRect = new Rect(TickSize);
currRect.Offset(x, y);
Point strokeCenter = new Point(currRect.X + 0.5 * currRect.Width,
currRect.Y + 0.5 * currRect.Height);
RotateTransform rotation = new RotateTransform(90 – 180 * ratio,
strokeCenter.X, strokeCenter.Y);
dc.PushTransform(rotation);
dc.DrawRectangle(TickBrush, null, currRect);
if (Ticks < 50)
{
dc.DrawText(ft, new Point(x-3, y+(TickSize.Height)));
}
dc.Pop();
}
}
}
برای دستیابی به نتیجه بهتر پس از ایجاد یک پروژه WPF در یک Name Space اختصاصی (مثلا MyControls) کد کلاس Guage را قرار داده و در آخرین مرحله تمهیدات لازم در XAML برای میزبانی این کنترل به همراه یک کنترل Slider جهت آزمایش عملکرد Guage قرار میدهیم. کد XAML مورد نیاز برای انجام این کار به شرح زیر میباشد:
<Window
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:Controls=”clr-namespace: UsingGuage.MyControls” x:Class=”UsingGuage.MainWindow”
Title=”MainWindow” Height=”450” Width=”825”>
<Window.Resources>
<DrawingBrush x:Key=”DrawingBrush1”
Viewbox=”0,0,21,121”
ViewboxUnits=”Absolute”>
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<RadialGradientBrush>
<GradientStop Color=”#FFF51212”
Offset=”0” />
<GradientStop Color=”#FFE45808”
Offset=”1” />
</RadialGradientBrush>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<PathGeometry Figures=”M149.50001,79.499991 C149.50001,79.499991 139.49997,189.49994 139.49997,189.49994 139.49997,189.49994 149.50001,199.49994 149.50001,199.49994 L159.50005,189.49994 z”>
<PathGeometry.Transform>
<MatrixTransform Matrix=”0.999996185317286,0,0,1.00000044504821,-138.9994373343,-79.000027751935” />
</PathGeometry.Transform>
</PathGeometry>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Brush=”#FF000000”
DashCap=”Flat”
EndLineCap=”Flat”
LineJoin=”Miter”
MiterLimit=”10”
StartLineCap=”Flat”
Thickness=”1” />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
<ControlTemplate x:Key=”SliderTemplate”
TargetType=”Slider”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Controls:Guage Ticks=”100”
TickSize=”1,25”
Grid.Row=”0”
Grid.RowSpan=”2”
TickBrush=”Gray” />
<Controls:Guage Ticks=”20”
TickSize=”2,50”
Grid.Row=”0”
Grid.RowSpan=”2”
TickBrush=”IndianRed”
x:Name=”PART_Track” />
<TextBlock Text=” آسان است WPF”
FontSize=”14”
Foreground=”Black”
HorizontalAlignment=”Center”
VerticalAlignment=”Center” />
<Rectangle Grid.Row=”0”
VerticalAlignment=”Stretch”
RenderTransformOrigin=”0.5,1”
Width=”50”
Opacity=”0.75”
Fill=”{StaticResource DrawingBrush1}”>
<Rectangle.RenderTransform>
<RotateTransform Angle=”{Binding Value, RelativeSource={RelativeSource TemplatedParent}}” />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</ControlTemplate>
</Window.Resources>
<DockPanel>
<TextBlock Text=”{Binding Value, ElementName=_slider}”
DockPanel.Dock=”Top” />
<Slider x:Name=”_moveSlider”
Minimum=”-90”
Maximum=”90”
Value=”-45”
Orientation=”Vertical”
Width=”50”
Margin=”20,0,0,0”
VerticalAlignment=”Top”
Height=”200”
DockPanel.Dock=”Left” />
<Slider x:Name=”_slider”
Minimum=”-100”
Maximum=”100”
Template=”{StaticResource SliderTemplate}”
Value=”{Binding Value, ElementName=_moveSlider}” />
</DockPanel>
</Window>
ما در کلاس Guage برای تامین هر سه مرحله مورد نیاز (یعنی اولا توصیف شکل گرافیکی، ثانیا ترسیم و قرار دادن آن در یک محتوای مناسب و در نهایت درج در یک میزبان قابل نمایش در Visual Tree) کلاس FrameworkElement را هدف گرفته و کلاس خود را از آن منشعب (Derived) میکنیم.
همانطور که قبلا هم اشاره شد کلاس FrameworkElement در WPF از اهمیت بسیار بالایی برخوردار است.این کلاس در واقع حلقه اتصال کلاسهای المانهای WPF و امکانات بالقوه هسته (Core-level set) سطوح پاین تر از کلاس UIElement تلقی میشود. این جایگاه خاص و کلیدی این امکان را به این کلاس میدهد که علاوه بر قابلیتهایی که از کلاس UIElement به ارث میبرد، قابلیت های کلیدی استفاده از DataBinding استفاده از Style و .... را نیز برای کلیه کلاسهایی که مستقیم و غیر مستقیم وارث این کلاس باشند فراهم نماید. کلاس Control نمونه بارزی از این کلاسها میباشد.
کلاس Gusge با ایجاد سه عضو Ticks و TickSize و TickBrush که به ترتیب از نوع int و Size و Brush میباشند امکان سفارشی کردن صفحه مدرج زمینه را در ساده ترین شکل ممکن فراهم میکند. Ticks تعداد درجات در زاویه 180 در جه (نیم دایره) است و TickSize اندازه طول و قطر هر نشانگر درجه را تامین میکند. بدیهیست که TickBrush نوع Brush مورد نظر برای ترسیم این درجه بندی را مشخص میکند.
کلاس Gusge با Override کردن متد OnRender عملا با یک تیر چند نشان زده و مهمتر از هر چیزی یک DrawingContext آماده دریافت میکند که چنانچه قبلا توضیح داده شد استفاده از متدهای آن برای رسم اشکال ساده گرافیکی پایه، ضروری میباشد. چنانچه در ادامه کد کاملا مشخص شده است ما از متد DrawEllipse این کلاس برای ترسیم دایره مرکزی، از متد DrawRectangle برای ترسیم خطوط مدرج (که در دو ردیف رسم میشوند) و از DrawText برای اعداد مشخص کننده بر روی نوار مدرج استفاده کرده ایم. همچنین دو متد Push و Pop به صورت مکمل و در داخل یک حلقه برای جابجایی (از نوع Transform) خطوط مدرج در بازه یک زاویه 180 درجه نقش کلیدی بازی میکنند .
به نظر میرسد آنچه در XAML مشاهده میکنید به اندازه کافی گویا میباشد و بدیهیست که نمونه سازی کلاس Guage در XAML انجام شده و با استفاده از تکنیک Element Binding یکی دیگر از قابلیتهای بسیار استثنایی WPF که امکان Binding دو کنترل UI به یکدیگر میباشد به نمایش گذاشته شده است.
همچنین در قسمت Resourceها مشخصات ترسیم عقربه با استفاده از دستورات Geometry String Commands ترسیم شده است. این شبه زبان بسیار ساده ولی قدرتمند با استفاده از دستورات ساده ای نظیر M و Z و F و L و A و H و... می توانند سگمنتهای گرافیکی مورد نیاز در کل Geometry ما را که فقط یک عقربه ساده میباشد تامین نمایند. در ادامه این عقربه با استفاده از یک ماتریس و ویژگی Transform هر آنچه ما به عنوان عقزبه یک کنترل Guage نیاز داریم را انجام خواهد داد.
دانلود سورس کد پروژه در ویژوال استودیو 2012
در پایان ضمن پوزش به دلیل وقفه نسبتا طولانی در بخش دوم این مطلب امیدوارم که مورد توجه همکاران محترم قرار گرفته باشد.