الرسوميات والحركة بلغة C#
بنينا نماذجنا حتى الآن من عناصر تحكم جاهزة: التسميات ومربعات النص والأزرار. أما في هذا الدرس فنرسم على النموذج مباشرة. يستخدم الجزء 1 نظام GDI+، وهو نظام الرسم في نماذج ويندوز (Windows Forms)، لرسم الخطوط والمستطيلات والأشكال البيضاوية (ellipses) والأقواس (arcs) والمضلعات (polygons) والنصوص. ويشرح الجزء 2 متى يبقى الرسم على الشاشة ومتى يختفي. ويبني الجزء 3 تطبيقا صغيرا يرسم أنماطا من الخطوط انطلاقا من أربعة أعداد. ويجعل الجزآن 4 و5 الأشياء تتحرك: مؤقت (Timer) يغير موضعا مرات كثيرة في الثانية، ويعاد رسم النموذج بعد كل تغيير. بني كل برنامج في هذا الدرس وشغل على ويندوز، وكل لقطة شاشة ورقم وسطر سجل فيما يلي مأخوذ من تلك التشغيلات.
الأهداف
بنهاية هذا الدرس ينبغي أن تكون قادرا على:
- الحصول على كائن
Graphicsمن الحدثPaint، وتخطيط الرسم باستخدام إحداثيات (coordinates) النموذج بالبكسل. - رسم الحدود الخارجية باستخدام قلم
Penوتلوين الأشكال باستخدام فرشاةSolidBrush:DrawLineوDrawRectangleوDrawEllipseوDrawArcوDrawPolygonوDrawStringودوالFill. - شرح سبب اختفاء الرسم الذي يتم باستخدام
CreateGraphicsوبقاء الرسم الذي يتم فيPaint، واستخدامInvalidateوRefresh. - حساب نهاية خط من زاوية وطول باستخدام
Math.CosوMath.Sin، وبناء نمط من خطوط كثيرة. - التحريك باستخدام
Timer: ضبطIntervalوEnabled، وتغيير موضع فيTick، واستدعاءInvalidate. - جعل الكائنات ترتد (bounce) عن حواف النموذج، وتحريك عدة كائنات بمؤقت واحد، وتوجيه أحدها بمفاتيح الأسهم، وتشغيل حركة إطارا بإطار (frame by frame).
الجزء 1: الرسم باستخدام GDI+
الكائن Graphics
GDI+ هو نظام الرسم الذي تقوم عليه نماذج ويندوز. ويوجد في C# في فضاء الأسماء (namespace) System.Drawing، ومحوره الصنف (class) Graphics: يمثل كائن Graphics سطح رسم (drawing surface)، وكل دالة (method) رسم (DrawLine وFillEllipse وDrawString وغيرها) تنتمي إليه. وللعمل مع الرسوميات خطوتان دائما: احصل أولا على كائن Graphics، ثم استخدمه للرسم.
توجد ثلاث طرق للحصول عليه:
| الطريقة | الشيفرة | استخدمها عندما |
|---|---|---|
الحدث Paint لنموذج أو عنصر تحكم | Graphics g = e.Graphics; | ترسم صورة النموذج (الطريقة المعتادة) |
الدالة CreateGraphics | Graphics g = this.CreateGraphics(); | تريد الرسم على نموذج أو عنصر تحكم موجود بالفعل |
| من صورة | Graphics g = Graphics.FromImage(myBitmap); | تريد تعديل صورة موجودة |
وإلى جانب Graphics ستستخدم أربعة أصناف أخرى: القلم (Pen) يرسم الخطوط والحدود الخارجية، والفرشاة (Brush) تلون المساحات (والنص)، وكائن الخط (Font) يصف حروف النص، والبنية (structure) Color تعطي الألوان.
أنشئ معالج الحدث Paint
نستخدم النموذج كأنه لوحة رسم. وحدثه Paint هو المكان الذي توضع فيه شيفرة الرسم:
- انقر أي مكان على النموذج في عرض التصميم (Design view).
- افتح نافذة الخصائص (Alt+Enter، أو انقر بالزر الأيمن واختر Properties).
- انقر أيقونة البرق للانتقال من قائمة الخصائص إلى قائمة الأحداث (Events). وأيقونة A to Z ترتب القائمة أبجديا، فيسهل العثور على
Paint. - انقر نقرا مزدوجا على المربع المجاور للحدث Paint.
ينشئ Visual Studio دالة فارغة، أي هيكل دالة (method stub)، ويربطها بالحدث. ونضيف سطرا واحدا يعطي سطح الرسم اسما قصيرا:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
// draw here
}e هو الكائن PaintEventArgs الممرر إلى المعالج، وe.Graphics هو سطح الرسم في إعادة الرسم هذه.
الإحداثيات على النموذج
تخيل النموذج شبكة من البكسلات:
(0, 0)هي الركن العلوي الأيسر من منطقة العميل (client area)، أي الجزء الواقع داخل شريط العنوان والحواف.- يزيد
xإلى اليمين. - يزيد
yإلى الأسفل. وهذا عكس المحور y في درس الهندسة. - الوحدة هي البكسل (pixel). والخط من
(2, 2)إلى(4, 1)لا يتجاوز طوله بضعة بكسلات ويكاد لا يرى، لذا تستخدم الرسوم الحقيقية أعدادا بالمئات.
ولنعطي أنفسنا مساحة كافية، يضبط الباني (constructor) حجم النافذة:
public Form1()
{
InitializeComponent();
this.Width = 500;
this.Height = 500;
}this هو كائن النموذج الذي يجري بناؤه. وWidth وHeight هما حجم النافذة كاملة، بما فيها شريط العنوان والحواف. وقد طبع برنامجنا حجم منطقة العميل على ويندوز: 484 في 461 بكسل. فالمساحة التي ترسم عليها أضيق من النافذة بمقدار 16 بكسل وأقصر منها بمقدار 39 بكسل.
قلم أم فرشاة؟
القلم يرسم الحد الخارجي (outline) للشكل، والفرشاة تلون داخله. وهذا هو الفرق الأساسي بينهما.
Pen myPen = new Pen(Color.Black); // width 1
Pen bluePen = new Pen(Color.Blue, 3); // width 3
SolidBrush myBrush = new SolidBrush(Color.Red);
SolidBrush orangeBrush = new SolidBrush(Color.FromArgb(255, 255, 140, 0));الوسيط (argument) الثاني للباني Pen هو عرض الخط. وتبني Color.FromArgb لونا من أربعة أعداد من 0 إلى 255: ألفا (alpha، و255 تعني معتما تماما)، والأحمر، والأخضر، والأزرق.
| الكائن | يلون | تستخدمه |
|---|---|---|
Pen | الحد الخارجي | DrawLine وDrawRectangle وDrawEllipse وDrawArc وDrawPolygon |
SolidBrush | الداخل | FillRectangle وFillEllipse وFillPolygon وDrawString |
تحجز كائنات Pen وBrush وFont وGraphics موارد من النظام. استدع Dispose() على ما تنشئه أنت منها عندما تنتهي منه. ولا تتخلص أبدا من e.Graphics: فالحدث Paint هو الذي أنشأه، وهو الذي يتخلص منه بنفسه.
مثال محلول: خط ومستطيل
داخل Form1_Paint:
g.DrawLine(myPen, 20, 20, 400, 420); // x1, y1, x2, y2
g.DrawRectangle(myPen, 100, 100, 200, 150); // x, y, width, heightتوقع: ما النقطتان اللتان يصل بينهما الخط، وأين الأركان الأربعة للمستطيل؟
تصل DrawLine بين نقطتين معطاتين في صورة أزواج إحداثيات، فالخط يمتد من (20, 20) إلى (400, 420).
أما DrawRectangle فتأخذ ركنا واحدا فقط، هو الركن العلوي الأيسر (x, y)، مع عرض w وارتفاع h؛ ويحسب الحاسوب الباقي. فإذا كان (x, y) الركن العلوي الأيسر و(x2, y2) الركن السفلي الأيمن:
- x2 = x + w = 100 + 200 = 300
- y2 = y + h = 100 + 150 = 250
فالركنان العلويان هما (100, 100) و(300, 100)، والركنان السفليان هما (100, 250) و(300, 250).
مثال محلول: شكل بيضاوي داخل مستطيل محيط
لا توجد دالة DrawCircle. فالشكل البيضاوي يعرف بواسطة مستطيله المحيط (bounding rectangle): ترسم DrawEllipse الشكل البيضاوي الذي يتسع تماما داخل المستطيل الذي تعطيه لها. فالمستطيل الذي طوله أكبر من ارتفاعه يعطي شكلا بيضاويا مسطحا، والمربع يعطي دائرة.
g.DrawEllipse(myPen, 300, 200, 50, 50); // the bounding rectangle: x, y, width, heightتوقع: ما هذا الشكل، وأين مركزه، وهل يلمس المستطيل من المثال السابق؟
- العرض والارتفاع متساويان، فهو دائرة نصف قطرها 50 / 2 = 25.
- مركزها هو مركز المستطيل المحيط:
(300 + 25, 200 + 25)=(325, 225). - أقصى نقطة فيها يسارا هي
(300, 225). والضلع الأيمن للمستطيل هو الخطx = 300منy = 100إلىy = 250، والقيمة 225 تقع داخل هذا المدى. فالدائرة تلمس المستطيل من الخارج. ويمكنك رؤية ذلك في لقطة الشاشة بعد قليل.
DrawArc: جزء من الشكل البيضاوي
القوس جزء من شكل بيضاوي. وتأخذ DrawArc المستطيل المحيط نفسه الذي تأخذه DrawEllipse، مع زاويتين بالدرجات:
g.DrawArc(myPen, x, y, width, height, startAngle, sweepAngle);- تقاس
startAngle(زاوية البداية) من المحور x المار بمنتصف المستطيل المحيط. و0هي النقطة اليمنى. - و
sweepAngle(زاوية المسح) هي المسافة التي يمتد إليها القوس من هناك، في اتجاه عقارب الساعة (clockwise). ويكون الاتجاه مع عقارب الساعة لأنyيزيد إلى الأسفل: فالنقطة التي تبعد 90 درجة عن النقطة اليمنى هي النقطة السفلى. - وزاوية المسح السالبة ترسم في الاتجاه الآخر، أي عكس اتجاه عقارب الساعة.
مثال محلول: أربعة أقواس
رسمنا أربعة أقواس في أربعة مربعات مقاس كل منها 100 في 100 (المربعات الرمادية)، بقلم أسود عرضه 3:
int[,] angles = { { 0, 90 }, { 0, 180 }, { 0, -90 }, { 90, 180 } };
for (int i = 0; i < 4; i++)
{
int x = 30 + i * 130;
g.DrawRectangle(boxPen, x, 30, 100, 100);
g.DrawArc(arcPen, x, 30, 100, 100, angles[i, 0], angles[i, 1]);
g.DrawString(angles[i, 0] + ", " + angles[i, 1], font, Brushes.Black, x + 25, 145);
}توقع جزء الدائرة الذي يرسمه كل زوج من الزوايا قبل أن تنظر.

0, 90: من النقطة اليمنى ربع دورة في اتجاه عقارب الساعة نزولا إلى النقطة السفلى: الربع السفلي الأيمن.0, 180: من النقطة اليمنى نصف دورة في اتجاه عقارب الساعة مرورا بالنقطة السفلى إلى النقطة اليسرى: النصف السفلي.0, -90: من النقطة اليمنى ربع دورة عكس اتجاه عقارب الساعة صعودا إلى النقطة العليا: الربع العلوي الأيمن.90, 180: من النقطة السفلى نصف دورة في اتجاه عقارب الساعة مرورا باليسار إلى الأعلى: النصف الأيسر.
مثال محلول: أربعة أقواس أخرى
المربعات نفسها مع أربعة أزواج أخرى من الزوايا:
int[,] angles = { { 90, 180 }, { 180, 90 }, { 270, -90 }, { -90, 180 } };توقع: اثنان من هذه الاستدعاءات يرسمان الربع نفسه تماما. فأيهما؟

90, 180: النصف الأيسر، كما سبق.180, 90: من النقطة اليسرى ربع دورة في اتجاه عقارب الساعة صعودا إلى النقطة العليا: الربع العلوي الأيسر.270, -90: الزاوية270هي النقطة العليا؛ وربع دورة عكس اتجاه عقارب الساعة ينتهي إلى النقطة اليسرى: الربع العلوي الأيسر نفسه، مرسوما في الاتجاه المعاكس.-90, 180: الزاوية-90هي النقطة العليا أيضا (270 و-90 اتجاه واحد)؛ ونصف دورة في اتجاه عقارب الساعة مرورا باليمين إلى الأسفل: النصف الأيمن.
النص يرسم أيضا: DrawString
g.DrawString("Hello World", new Font("Arial", 16), myBrush, 150, 400);ترسم DrawString النص في موضع معين باستخدام فرشاة وخط (Font). ووسطاؤها هي: النص، وكائن Font (اسم عائلة الخط وحجمه بالنقاط)، وفرشاة (فالحروف أشكال ملونة، لذا تحتاج إلى فرشاة لا إلى قلم)، و(x, y) للركن العلوي الأيسر من النص.
مثال محلول: الصورة كاملة
ضع الاستدعاءات الخمسة كلها في معالج Paint واحد، بقلم أسود وفرشاة حمراء، على النموذج الذي مقاسه 500 في 500:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Pen myPen = new Pen(Color.Black);
SolidBrush myBrush = new SolidBrush(Color.Red);
g.DrawLine(myPen, 20, 20, 400, 420);
g.DrawRectangle(myPen, 100, 100, 200, 150);
g.DrawEllipse(myPen, 300, 200, 50, 50);
g.DrawArc(myPen, 400, 100, 50, 50, 0, 90);
g.DrawString("Hello World", new Font("Arial", 16), myBrush, 150, 400);
myPen.Dispose();
myBrush.Dispose();
}توقع: ارسم النافذة على الورق، ثم قارنها بالتشغيل الحقيقي.

تلمس الدائرة المستطيل عند (300, 225)، والقوس هو الربع السفلي الأيمن من مستطيله المحيط الذي مقاسه 50 في 50، والنص أحمر لأن DrawString تستخدم الفرشاة.
مثال محلول: رسم أم تلوين أم كلاهما؟
لكل دالة Draw تأخذ قلما دالة توأم Fill تأخذ فرشاة. رسمنا المستطيل والشكل البيضاوي والمثلث أنفسها ثلاث مرات لكل منها، بقلم أزرق عرضه 3 وفرشاة برتقالية. العمود 1 يرسم فقط، والعمود 2 يلون فقط، والعمود 3 يلون ثم يرسم:
Pen bluePen = new Pen(Color.Blue, 3);
SolidBrush orangeBrush = new SolidBrush(Color.FromArgb(255, 255, 140, 0));
g.DrawRectangle(bluePen, 20, 20, 120, 80);
g.FillRectangle(orangeBrush, 170, 20, 120, 80);
g.FillRectangle(orangeBrush, 320, 20, 120, 80);
g.DrawRectangle(bluePen, 320, 20, 120, 80);
g.DrawEllipse(bluePen, 20, 120, 120, 80);
g.FillEllipse(orangeBrush, 170, 120, 120, 80);
g.FillEllipse(orangeBrush, 320, 120, 120, 80);
g.DrawEllipse(bluePen, 320, 120, 120, 80);
Point[] triangle = { new Point(80, 220), new Point(140, 300), new Point(20, 300) };
g.DrawPolygon(bluePen, triangle);
Point[] triangle2 = { new Point(230, 220), new Point(290, 300), new Point(170, 300) };
g.FillPolygon(orangeBrush, triangle2);
Point[] triangle3 = { new Point(380, 220), new Point(440, 300), new Point(320, 300) };
g.FillPolygon(orangeBrush, triangle3);
g.DrawPolygon(bluePen, triangle3);يعطى المضلع في صورة مصفوفة من قيم Point؛ وتصل DrawPolygon بينها بالترتيب ثم تغلق الشكل بالعودة إلى النقطة الأولى.
توقع الأشكال التسعة، ثم انظر:

تعطي Draw الحد الخارجي فقط، وتعطي Fill الداخل فقط. وللحصول على الاثنين لون أولا ثم ارسم ثانيا: ففي الترتيب الآخر يغطي التلوين النصف الداخلي من الحد الخارجي الذي عرضه 3 بكسلات.
تفاعلي: ملعب الرسم باستخدام GDI+
اختر دالة رسم، واسحب على النموذج لتحديد مكان الشكل (أو اكتب الأعداد)، واقرأ استدعاء C# الذي يرسمه. وفعل إظهار المستطيل المحيط للأشكال البيضاوية والأقواس، وجرب زوايا مسح سالبة.
الجزء 2: متى ترسم الصورة؟
الحدث Paint يأتي مرة بعد مرة
لا يحتفظ ويندوز بنسخة مما رسمته. فكلما وجب عرض جزء من النموذج من جديد، طلب ويندوز من النموذج أن يرسم نفسه بإطلاق الحدث Paint. ويحدث ذلك:
- عند عرض النموذج أول مرة؛
- عند كشف جزء مغطى من النموذج، أو تغيير حجم النموذج؛
- عند استدعاء الشيفرة للدالة
Invalidate()، ومعناها "هذه المنطقة لم تعد محدثة، فأعد رسمها عندما تستطيع"؛ - عند استدعاء الشيفرة للدالة
Refresh()، التي تمسح المنطقة وتعيد رسمها فورا.
لذا فمكان شيفرة الرسم هو Paint: فهي تنفذ من جديد في كل مرة تحتاج فيها الصورة.
مثال محلول: Paint وLoad وزر
يرسم هذا النموذج ثلاثة أشياء في ثلاثة أماكن:
// Paint: a blue rectangle
private void Form1_Paint(object sender, PaintEventArgs e)
{
SolidBrush blueBrush = new SolidBrush(Color.RoyalBlue);
e.Graphics.FillRectangle(blueBrush, 20, 70, 150, 100);
blueBrush.Dispose();
}
// Load: a green circle, drawn with CreateGraphics
private void Form1_Load(object sender, EventArgs e)
{
SolidBrush greenBrush = new SolidBrush(Color.Green);
Graphics formGraphics = this.CreateGraphics();
formGraphics.FillEllipse(greenBrush, 200, 70, 100, 100);
greenBrush.Dispose();
formGraphics.Dispose();
}
// Draw circle button: a red circle, drawn with CreateGraphics
private void drawButton_Click(object sender, EventArgs e)
{
SolidBrush redBrush = new SolidBrush(Color.Red);
Graphics formGraphics = this.CreateGraphics();
formGraphics.FillEllipse(redBrush, 330, 70, 100, 100);
redBrush.Dispose();
formGraphics.Dispose();
}
// Refresh button
private void refreshButton_Click(object sender, EventArgs e)
{
this.Refresh();
}توقع: ماذا تعرض النافذة عند فتحها؟ ثم بعد النقر على Draw circle؟ ثم بعد النقر على Refresh؟
عند فتحها:

الدائرة الخضراء لا تظهر أبدا. ففي Load لا يكون النموذج على الشاشة بعد، فلا يوجد ما يرسم عليه، ثم يرسم أول Paint الصورة الحقيقية للنموذج.
بعد Draw circle:

بعد Refresh:

مسحت Refresh() النموذج وأطلقت Paint. فيرسم Paint المستطيل الأزرق من جديد، لكن لا أحد يرسم الدائرة الحمراء من جديد، فتختفي. ويحدث الشيء نفسه عند تغيير حجم النافذة أو تغطيتها: فالرسم الذي يتم باستخدام CreateGraphics لا يعاد رسمه.
| مكان الرسم | بعد إعادة الرسم التالية |
|---|---|
Paint، باستخدام e.Graphics | يرسم من جديد: يبقى |
معالج نقرة، باستخدام CreateGraphics | يمحى: لا أحد يرسمه من جديد |
Load، باستخدام CreateGraphics | لا يرى أبدا |
ما زالت CreateGraphics مفيدة لرسم سريع مؤقت على نموذج ظاهر بالفعل. لكن كل ما يجب أن يبقى على الشاشة مكانه Paint، وعلى الشيفرة التي تغيره أن تستدعي Invalidate() أو Refresh() بدلا من الرسم مباشرة.
الجزء 3: أنماط من الأعداد
تطبيق أنماط الخطوط
يطلب التطبيق التالي من المستخدم أربعة أعداد، ويرسم منها نمطا من الخطوط:
- Number of lines: عدد الخطوط التي ترسم.
- Angle: بعد كل خط يدور الاتجاه بهذا العدد من الدرجات.
- Length: طول الخط الأول بالبكسل.
- Increment: بعد كل خط يزيد الطول بهذا العدد من البكسلات.
يحتوي النموذج على لوحتين (panels). العليا، controlPanel، خاصيتها Dock مضبوطة على Top: فتلتصق بالحافة العليا وتحمل التسميات الأربع، ومربعات النص الأربعة (numLines وangle وlength وincrement، والخاصية TextAlign فيها مضبوطة على Right لأنها تحمل أعدادا) والزر Go. والثانية، canvas، خاصيتها Dock مضبوطة على Fill: فتأخذ بقية النموذج، ولون خلفيتها BackColor أبيض. واللوحات المثبتة بالخاصية Dock تبقى في مكانها عند تغيير حجم النافذة. ونعالج الحدث Paint للوحة الرسم canvas (حدد اللوحة، ثم انقر زر البرق)، لا حدث النموذج.

لا يفعل الزر Go إلا أن يطلب إعادة الرسم:
private void goButton_Click(object sender, EventArgs e)
{
canvas.Refresh(); // clear the canvas and raise its Paint event
}خط واحد من زاوية وطول
يبدأ كل خط عند (x1, y1)، وهي نهاية الخط السابق، وله طول L واتجاه θ بالدرجات. ونهايته (x2, y2) هي:
x2 = x1 + L · cos(θ · π / 180)
y2 = y1 + L · sin(θ · π / 180)وبعد كل خط يدور الاتجاه بمقدار α (المربع Angle) ويزيد الطول بمقدار d (المربع Increment):
θ(next) = θ + α
L(next) = L + dتأخذ Math.Cos وMath.Sin الزاوية بالراديان (radians) لا بالدرجات، لذا تضرب الدرجات أولا في π / 180 (Math.PI / 180). ولأن y يزيد إلى الأسفل على النموذج، فالزاوية 90 درجة تشير إلى الأسفل.
معالج الحدث Paint
private void canvas_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Pen myPen = new Pen(Color.Black, 1);
int lines = int.Parse(numLines.Text);
int turn = int.Parse(angle.Text);
int myLength = int.Parse(length.Text);
int step = int.Parse(increment.Text);
int startX = canvas.Width / 2; // start at the centre of the canvas
int startY = canvas.Height / 2;
int myAngle = 0; // the first line points right
for (int i = 0; i < lines; i++)
{
int endX = (int)(startX + Math.Cos(myAngle * Math.PI / 180) * myLength);
int endY = (int)(startY + Math.Sin(myAngle * Math.PI / 180) * myLength);
Point[] points = { new Point(startX, startY), new Point(endX, endY) };
g.DrawLines(myPen, points);
startX = endX; // the next line starts where this one ended
startY = endY;
myAngle = myAngle + turn;
myLength = myLength + step;
}
myPen.Dispose();
}الخاصية Text سلسلة نصية (string)، لذا تمر كل قيمة أولا عبر int.Parse. وتصل DrawLines بين نقاط مصفوفة بالترتيب؛ ومع نقطتين ترسم خطا واحدا. ويحول التحويل (cast) (int) الناتج double من Math.Cos وMath.Sin إلى عدد صحيح من البكسلات، ويفعل ذلك بحذف الكسر، لا بالتقريب. ولأن القيم تقرأ داخل Paint، ترسم لوحة الرسم النمط الافتراضي أيضا عند فتح النموذج أول مرة.
مثال محلول: 4 خطوط، والزاوية 90، والطول 100، والزيادة 0
لوحة الرسم لدينا 560 في 400 بكسل، فمركزها (280, 200). توقع نقاط النهاية الأربع.
| الخط | θ | من | إلى |
|---|---|---|---|
| 1 | 0 | (280, 200) | (380, 200) |
| 2 | 90 | (380, 200) | (380, 300) |
| 3 | 180 | (380, 300) | (280, 300) |
| 4 | 270 | (280, 300) | (280, 200) |
الجدول هو مخرجات البرنامج نفسه (فهو يطبع الخطوط الأولى من كل نمط). يمين، ثم أسفل، ثم يسار، ثم أعلى: مربع، مرسوم في اتجاه عقارب الساعة لأن y يزيد إلى الأسفل. وهو المربع الظاهر في لقطة الشاشة أعلاه.
مثال محلول: 100 خط، والزاوية 74، والطول 5، والزيادة 1
للخط 1 القيمتان θ = 0 وL = 5، فهو يمتد من (280, 200) إلى (285, 200). توقع نهاية الخط 2، حيث θ = 74 وL = 6.
x2 = 285 + 6 · cos(74°) = 285 + 1.65 = 286.65 → 286
y2 = 200 + 6 · sin(74°) = 200 + 5.77 = 205.77 → 205طبع البرنامج line 2: angle 74, length 6: (285, 200) -> (286, 205). كل خط أطول من سابقه ببكسل واحد ويدور 74 درجة، فتلتف الخطوط إلى الخارج:

ومع 500 خط بدلا من 100 تستمر القاعدة نفسها حتى تخرج الخطوط من لوحة الرسم:

و100 خط بزاوية 90 درجة، تبدأ بطول 5 وزيادة 3، تعطي حلزونا مربعا:

مثال محلول: 3 خطوط، والزاوية 120، والطول 150، والزيادة 0
توقع: هل ينتهي الخط الثالث تماما حيث بدأ الخط الأول؟
| الخط | من | إلى |
|---|---|---|
| 1 | (280, 200) | (430, 200) |
| 2 | (430, 200) | (355, 329) |
| 3 | (355, 329) | (279, 199) |

إنه مثلث، لكنه ينتهي عند (279, 199)، أي على بعد بكسل واحد من (280, 200) في كل اتجاه. فالنهاية الدقيقة للخط 2 هي (355.0, 329.9)، وبعد حذف الكسر تصبح (355, 329)، وكل عملية حذف بالتحويل (int) تضيف خطأ صغيرا. وفي نمط من مئات الخطوط تصبح هذه الأخطاء الصغيرة جزءا مما تراه.
تفاعلي: الأنماط خطا بعد خط
اكتب الأعداد الأربعة (أو اختر إعدادا جاهزا)، ثم شغل النمط خطا واحدا في كل مرة. تعرض الأداة حساب الخط التالي بالصيغة نفسها وبعملية الحذف (int) نفسها التي في برنامج C#؛ وتطابق خطوطها الأولى مخرجات البرنامج في كل الأنماط السابقة.
الجزء 4: الحركة باستخدام مؤقت
عنصر التحكم Timer
الحركة (animation) سلسلة من الصور الثابتة، تختلف كل منها قليلا عن سابقتها، تعرض بسرعة واحدة تلو الأخرى. وفي نماذج ويندوز الأداة التي تعني "افعل شيئا مرة أخرى على فترات قصيرة تقاس بالملي ثانية" هي عنصر التحكم Timer (المؤقت). اسحبه من صندوق الأدوات (Toolbox) إلى النموذج: فهو لا يظهر على النموذج نفسه، بل في شريط أسفل سطح التصميم.
| العضو | المعنى |
|---|---|
Interval | الزمن بين حدثي Tick متتاليين، بالملي ثانية (ms) |
Enabled | true: المؤقت يعمل؛ false: المؤقت متوقف |
Tick | الحدث الذي يطلق كل Interval ملي ثانية؛ انقر المؤقت نقرا مزدوجا لإنشاء معالجه |
مربع يتحرك
int x = 10;
int y = 70;
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
SolidBrush redBrush = new SolidBrush(Color.Red);
g.FillRectangle(redBrush, x, y, 50, 50);
g.DrawString("x = " + x, this.Font, Brushes.Black, 10, 10);
redBrush.Dispose();
}
private void timer1_Tick(object sender, EventArgs e) // Interval = 50, Enabled = True
{
x += 5;
this.Invalidate(); // ask Windows to repaint the form
}يحفظ موضع المربع في حقلين (fields)، x وy، لكي يستخدمهما المعالجان كلاهما: يقرؤهما Paint ويغيرهما Tick. وكل Tick يحرك x مسافة 5 بكسلات إلى اليمين ويستدعي Invalidate()، ثم يرسم Paint الذي يليه المربع عند قيمة x الجديدة. وBrushes.Black فرشاة سوداء جاهزة.
مثال محلول: ثلاث لحظات
توقع: يبدأ x من 10. فما قيمة x بعد 20 نبضة (tick)؟
x = 10 + 5 · 20 = 110. يسجل برنامجنا كل نبضة عاشرة، وقد طبع tick 20 at 1263 ms: x = 110. وهذه ثلاث لقطات شاشة للنموذج أثناء تشغيله، بفاصل نحو ثانية بين كل لقطة والتي تليها:

مثال محلول: احذف Invalidate()
صار معالج Tick الآن هذا فقط:
private void timer1_Tick(object sender, EventArgs e)
{
x += 5;
}توقع: ماذا تعرض النافذة؟

tick 10 at 642 ms: x = 60
tick 30 at 1909 ms: x = 160
tick 50 at 3177 ms: x = 260يبين سجل التشغيل نفسه أن المؤقت يعمل وأن x يستمر في الزيادة. لكن لا شيء يطلب حدث Paint جديدا، فتبقى الشاشة تعرض الصورة القديمة، بما فيها النص القديم x = 10. فتغيير متغير ليس تغييرا على الشاشة حتى يعيد شيء ما الرسم.
ما سرعة الحركة؟
لو جاءت كل نبضة في وقتها تماما لكانت السرعة:
v = s · 1000 / Tحيث v السرعة بالبكسل في الثانية، وs الخطوة في كل نبضة بالبكسل، وT قيمة Interval بالملي ثانية (في الثانية 1000 ملي ثانية، فيأتي في الثانية 1000 / T نبضة). وبالنسبة إلى مربعنا: v = 5 · 1000 / 50 = 100 بكسل في الثانية.
قسنا الزمن الحقيقي بين النبضات في برامجنا، على جهاز ويندوز الذي شغلها:
| Interval | الزمن المقيس بين النبضات | عدد النبضات المقيسة | البرنامج |
|---|---|---|---|
| 10 ms | 16.4 ms | 250 | اللوحة المتنامية |
| 25 ms | 31.7 ms | 180 | التسميات الثلاث |
| 50 ms | 62.6 ms | 40 | المربع المتحرك |
| 50 ms | 62.8 ms | 59 | الشخص الراكض |
| 100 ms | 110.8 ms | 28 | الشخص الراكض |
جاءت كل نبضة متأخرة عن Interval. فالمربع تحرك فعلا 5 بكسلات كل 62.6 ms، أي نحو 80 بكسل في الثانية، لا 100. وInterval هو الزمن الذي تطلبه بين النبضات؛ ولا ينبغي أن يعتمد البرنامج على الحصول عليه بالضبط.
حركة أنعم: DoubleBuffered
كل Paint يمسح النموذج أولا ثم يرسم المربع من جديد. وعلى بعض الشاشات تلتقط العين اللحظة الفاصلة بينهما، فيحدث وميض (flicker) للكائن المتحرك. وضبط الخاصية DoubleBuffered للنموذج على True (في نافذة الخصائص، أو this.DoubleBuffered = true; في الباني) يجعل النموذج يرسم الصورة الجديدة كاملة خارج الشاشة أولا ثم يعرضها في خطوة واحدة. وكل برامجنا المتحركة تضبطها.
تفاعلي: Interval والخطوة وInvalidate
غير Interval والخطوة أثناء حركة المربع، وراقب النبضات على مسطرة الثانية الواحدة، وأوقف Invalidate() لترى x يتغير بينما الشاشة لا تتغير. وزر الخطوة ينفذ حدث Tick واحدا في كل مرة.
الجزء 5: الارتداد والمفاتيح والإطارات
حرك عنصر تحكم واجعله يرتد
لا نحتاج دائما إلى Paint للتحريك. فلعنصر التحكم موضع خاص به، هو خاصيتاه Left وTop، وتغييرهما يحرك عنصر التحكم. يحتوي هذا النموذج على ثلاث تسميات كبيرة (بخط عريض، وحجم 20) ومؤقت قيمة Interval فيه 25 وEnabled فيه True. وتحصل كل تسمية متحركة على زوج خاص بها من حقول السرعة:
int L1x = 5; // label1: speed on x
int L1y = 5; // label1: speed on y
private void timer1_Tick(object sender, EventArgs e)
{
label1.Left -= L1x; // this will move it horizontally
label1.Top -= L1y; // this will move it vertically
if (label1.Left + label1.Width > ClientSize.Width || label1.Left < 0)
{
L1x = -L1x; // bounce from the left or right border
}
if (label1.Top + label1.Height > ClientSize.Height || label1.Top < 0)
{
L1y = -L1y; // bounce from the top or bottom border
}
}الحقول معرفة داخل الصنف لكن خارج كل الدوال، لذا تستطيع كل دوال النموذج استخدامها. يسأل الشرط if الأول: هل تجاوزت الحافة اليمنى للتسمية (Left + Width) الجانب الأيمن من منطقة العميل، أو (||) تجاوزت حافتها اليسرى الجانب الأيسر؟ فإذا كان كذلك تغيرت إشارة السرعة الأفقية، فتحرك النبضات التالية التسمية في الاتجاه الآخر. ويفعل الشرط if الثاني الشيء نفسه للأعلى والأسفل.
مثال محلول: تتبع الارتداد
نموذج ضيق فيه ClientSize.Width = 300. تبدأ labelA عند Left = 230، وعرضها 50 بكسل، وتتحرك بالسرعة Ax = 10، باستخدام الشرط if نفسه السابق:
labelA.Left += Ax;
if (labelA.Left + labelA.Width > ClientSize.Width || labelA.Left < 0)
{
Ax = -Ax;
}توقع قيمة Left والسرعة بعد كل نبضة من النبضات 1 إلى 5.
| النبضة | Left بعد الحركة | Left + Width | أكبر من 300؟ | السرعة بعد النبضة |
|---|---|---|---|---|
| 1 | 240 | 290 | لا | 10 |
| 2 | 250 | 300 | لا (المساوي ليس أكبر) | 10 |
| 3 | 260 | 310 | نعم | -10 |
| 4 | 250 | 300 | لا | -10 |
| 5 | 240 | 290 | لا | -10 |
سجل البرنامج هذه القيم بالضبط، ومنها مثلا tick 3: labelA.Left = 260, Ax = -10. لاحظ الترتيب: تتحرك التسمية أولا، ثم يجري الفحص. ففي النبضة 3 تكون الحافة اليمنى للتسمية عند 310، أي بعد الحافة بمقدار 10 بكسلات، لمدة نبضة واحدة قبل أن تعود. والكائن الأسرع يبرز مسافة أكبر.
كائنات كثيرة، ومؤقت واحد
يستطيع معالج Tick نفسه أن يحرك كائنات كثيرة. تحصل label2 على زوج سرعة خاص بها وعلى الشرطين if نفسيهما:
int L2x = 5;
int L2y = 5;
// in timer1_Tick, after label1
label2.Left += L2x;
label2.Top += L2y;
if (label2.Left + label2.Width > ClientSize.Width || label2.Left < 0)
{
L2x = -L2x;
}
if (label2.Top + label2.Height > ClientSize.Height || label2.Top < 0)
{
L2y = -L2y;
}تطرح label1 سرعتيها وتجمعهما label2، لذا تبدآن في اتجاهين متعاكسين: label1 إلى الأعلى واليسار، وlabel2 إلى الأسفل واليمين. أما label3 فلا شيفرة لها بعد، فتبقى في مكانها. وهذه أربع لقطات شاشة للنموذج أثناء تشغيله:

يحتاج كل كائن إلى متغيرات سرعة خاصة به: فلو استخدمت label2 المتغير L1x لأدى ارتداد label1 إلى عكس اتجاه label2 أيضا. أبعاد التسميات 92 في 32 بكسل، ومنطقة العميل 420 في 300، كما طبع البرنامج عند بدئه.
التوجيه بمفاتيح الأسهم
بدلا من الارتداد، يمكن أن يختار المستخدم الاتجاه. يسرد تعداد (enum) الاتجاهات الأربعة، ويتذكر حقل الاتجاه الحالي، ويغيره الحدث KeyDown للنموذج (قائمة الأحداث، زر البرق):
enum Position
{
Left, Right, Up, Down
}
Position objectPosition = Position.Right;
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left) objectPosition = Position.Left;
else if (e.KeyCode == Keys.Right) objectPosition = Position.Right;
else if (e.KeyCode == Keys.Up) objectPosition = Position.Up;
else if (e.KeyCode == Keys.Down) objectPosition = Position.Down;
}
private void timer1_Tick(object sender, EventArgs e)
{
if (objectPosition == Position.Right) x += 5;
else if (objectPosition == Position.Left) x -= 5;
else if (objectPosition == Position.Up) y -= 5;
else if (objectPosition == Position.Down) y += 5;
this.Invalidate();
}يكتفي KeyDown باختيار الاتجاه؛ ويبقى Tick هو الذي يحرك، بسرعة ثابتة، سواء كان المفتاح مضغوطا أم لا.
صورة بدلا من شكل
بدلا من مربع أحمر يمكننا رسم صورة من ملف باستخدام DrawImage:
Bitmap picture = new Bitmap("mushroom.png"); // loaded once, drawn on every Paint
private void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawImage(picture, x, y, 64, 64); // the image, where, and its size
}- أضف الصورة إلى المشروع (انقر المشروع بالزر الأيمن، ثم Add، ثم Existing Item، واختر "All Files" لترى ملفات الصور).
- في خصائص الصورة اضبط Copy to Output Directory على Copy if newer. فبدون ذلك لا ينسخ الملف بجوار ملف
.exe، ولا يستطيع البرنامج العثور عليه.
نحمل الكائن Bitmap مرة واحدة، في حقل، بدلا من تحميله داخل Paint: فالحدث Paint ينفذ مرات كثيرة في الثانية، وتحميل الملف في كل مرة يكرر العمل نفسه مرة بعد مرة.
شغلنا هذا البرنامج وضغطنا Down ثم Left ثم Up بفاصل نحو 1.5 ثانية بين كل ضغطة والتي تليها. (لا توجد لوحة مفاتيح في جهاز المختبر، فأرسل برنامج مساعد صغير رسائل المفاتيح نفسها التي يرسلها ويندوز عند الضغط الحقيقي على المفاتيح.) وطبع البرنامج KeyDown Down at x = 170, y = 20 وKeyDown Left at x = 170, y = 140 وKeyDown Up at x = 50, y = 140:

الحركة إطارا بإطار
نوع ثان من الحركة يغير الصورة بدلا من الموضع. فالشخص الراكض ثماني صور، من run0.png إلى run7.png، تعرض واحدة تلو الأخرى في حلقة. ويحتوي النموذج على عنصر PictureBox اسمه frameImage (الخاصية SizeMode فيه مضبوطة على StretchImage)، وعنصر TrackBar اسمه speedBar، ومؤقت اسمه animationTimer قيمة Interval فيه 50 وEnabled فيه True.
private string[] frames = { "frames/run0.png", "frames/run1.png", "frames/run2.png", "frames/run3.png",
"frames/run4.png", "frames/run5.png", "frames/run6.png", "frames/run7.png" };
private Bitmap[] images;
private int currentFrame = 0;
public Form1()
{
InitializeComponent();
images = new Bitmap[frames.Length];
for (int i = 0; i < frames.Length; i++)
images[i] = new Bitmap(frames[i]);
frameImage.Image = images[0];
}
private void animationTimer_Tick(object sender, EventArgs e)
{
currentFrame = (currentFrame + 1) % frames.Length; // 0, 1, ..., 7, 0, 1, ...
frameImage.Image = images[currentFrame];
infoLabel.Text = "frame " + currentFrame + ", interval " + animationTimer.Interval + " ms";
}يعطي المعامل % باقي القسمة. و(7 + 1) % 8 يساوي 0، لذا يعود العداد بعد الإطار الأخير إلى الأول دون أي if. وملفات الإطارات الثمانية مضبوطة على Copy if newer، مثل صورة الفطر.
![]()
مثال محلول: حرك TrackBar
لعنصر TrackBar الخاصية Minimum وقيمتها 50 والخاصية Maximum وقيمتها 100. ومعالجه ينسخ قيمته إلى فترة المؤقت:
private void speedBar_ValueChanged(object sender, EventArgs e)
{
animationTimer.Interval = speedBar.Value;
}(ينفذ ValueChanged كلما تغيرت القيمة. والحدث Scroll، الذي ينفذ فقط عندما يحرك المستخدم الشريط بالفأرة أو لوحة المفاتيح، يعمل جيدا هنا أيضا.)
توقع: حرك المستخدم الشريط إلى الطرف الأيمن، 100. هل يركض الشخص أسرع أم أبطأ؟
![]()
أبطأ. فالفترة الأكبر تعني انتظارا أطول بين النبضات، أي إطارات أقل في الثانية. وفي تشغيلنا ارتفع الزمن بين الإطارات من 62.8 ms عند 50 إلى 110.8 ms عند 100. ففي هذا الشريط الطرف الأيمن هو الإعداد الأبطأ.
مثال محلول: حلقة while بدلا من مؤقت
طريقتان لتكبير لوحة زرقاء، pnlMain، من 0 في 0 إلى 300 في 200 بكسل، بكسلا واحدا في كل مرة. الأولى تستخدم مؤقتا قيمة Interval فيه 10، ويبدؤه الزر Timer:
private void btnAnimation1_Click(object sender, EventArgs e)
{
tmrMain.Enabled = true;
}
private void tmrMain_Tick(object sender, EventArgs e)
{
if (pnlMain.Width < targetWidth) pnlMain.Width++;
if (pnlMain.Height < targetHeight) pnlMain.Height++;
}والثانية تستخدم حلقة while داخل معالج النقر للزر While loop:
private void btnAnimation2_Click(object sender, EventArgs e)
{
pnlMain.Height = targetHeight;
while (pnlMain.Width < targetWidth)
{
pnlMain.Width++;
}
}توقع: كلتاهما تغير Width بكسلا واحدا في كل مرة. فهل تبدو كلتاهما حركة؟

لا. نسخة المؤقت تكبر بشكل مرئي: يبين سجلها الحجم 50 في 50 بعد 818 ms، والحجم الكامل 300 في 200 بعد 4912 ms. أما حلقة while فانتهت في 67.8 ms وظهرت اللوحة مكتملة دفعة واحدة. فالنموذج لا يستطيع إعادة الرسم إلا بعد عودة معالج النقر، وعندها تكون الحلقة قد وصلت إلى 300 بالفعل. أما المؤقت فيعطي النموذج فرصة لإعادة الرسم بين كل خطوتين، وهذا ما يجعل الخطوات مرئية.
فحص سريع
g.DrawRectangle(p, 50, 40, 100, 60): أين الركن السفلي الأيمن؟- أي الاستدعاءين يرسم النصف العلوي من دائرة في المستطيل المحيط
r:g.DrawArc(p, r, 0, 180)أمg.DrawArc(p, r, 180, 180)؟ - يرسم نص في معالج الحدث Click لزر باستخدام
CreateGraphics. ماذا يمكن أن يحدث له عند تغيير حجم النموذج؟ Interval = 40والخطوة 4 بكسلات. ما السرعة إذا جاءت كل نبضة في وقتها؟- معالج Tick يغير
x، لكن المربع لا يتحرك على الشاشة. ما السطر الناقص؟
تمارين
نحو 30 دقيقة. أجب على الورق أولا، ثم ابن البرنامج وشغله للتحقق.
التمرين 1: ارسم بيتا (نحو 7 دقائق)
Pen myPen = new Pen(Color.Black, 2);
SolidBrush doorBrush = new SolidBrush(Color.SaddleBrown);
SolidBrush windowBrush = new SolidBrush(Color.Gold);
g.DrawRectangle(myPen, 60, 120, 200, 140); // walls
Point[] roof = { new Point(60, 120), new Point(160, 40), new Point(260, 120) };
g.DrawPolygon(myPen, roof); // roof
g.FillRectangle(doorBrush, 140, 190, 40, 70); // door
g.FillEllipse(windowBrush, 90, 150, 30, 30); // round window
g.DrawString("Home", new Font("Arial", 14), Brushes.Black, 135, 8);- أعط الأركان الأربعة للجدران وقمة السقف.
- هل يلمس الباب الجدار السفلي؟ اكتب عملية الجمع.
- أعط مركز النافذة ونصف قطرها.
التمرين 2: الارتداد إلى اليسار (نحو 7 دقائق)
في النموذج الضيق نفسه الذي في تتبع الارتداد (ClientSize.Width = 300)، تبدأ labelB عند Left = 20، وعرضها 40 بكسل، وتتحرك بالسرعة Bx = -8، بالشرط if نفسه الذي في label1.
- اكتب قيمة
Leftوالسرعة بعد كل نبضة من النبضات 1 إلى 5. - في أي نبضة تتغير إشارة السرعة، وأي نصفي الشرط
||يكون صحيحا عندئذ؟ - ما أصغر قيمة تأخذها
Leftللتسمية؟
التمرين 3: توقع النمط (نحو 6 دقائق)
في تطبيق أنماط الخطوط اكتب 5 خطوط، وAngle 144، وLength 150، وIncrement 0.
- احسب نهاية الخط 1 ونهاية الخط 2. المركز
(280, 200). - سم الشكل.
- هل ينتهي الخط 5 عند
(280, 200)تماما؟
التمرين 4: حرك label3 (نحو 10 دقائق)
ابدأ من برنامج التسميات الثلاث، حيث ترتد label1 وlabel2 بالفعل.
- أضف حقلين،
L3x = 3وL3y = 7، واجعل label3 تتحرك باستخدام+=وترتد عن الحواف الأربع كلها. - تبدأ label3 عند
Left = 150وTop = 160، وحجمها 92 في 32 بكسل، ومنطقة العميل 420 في 300. توقع: في أي نبضة ترتد label3 أول مرة، وعن أي حافة؟
الإجابات
فحص سريع
(150, 100): 50 + 100 و40 + 60.g.DrawArc(p, r, 180, 180): يبدأ عند النقطة اليسرى ويمسح في اتجاه عقارب الساعة فوق القمة إلى النقطة اليمنى. أما0, 180فيرسم النصف السفلي.- قد يمحى: فتغيير الحجم يطلق Paint، وPaint لا يرسمه من جديد.
- 4 · 1000 / 40 = 100 بكسل في الثانية.
this.Invalidate();في نهاية معالج Tick.
التمرين 1
- الجدران:
(60, 120)و(260, 120)في الأعلى، و(60, 260)و(260, 260)في الأسفل (60 + 200 = 260، و120 + 140 = 260). وقمة السقف هي(160, 40)، النقطة الوسطى في المضلع. - الحافة السفلية للباب عند 190 + 70 = 260، وهو مستوى أسفل الجدران نفسه، لذا فهو يقف على الجدار السفلي.
- المستطيل المحيط للنافذة 30 في 30 عند
(90, 150): دائرة مركزها(105, 165)ونصف قطرها 15.

التمرين 2
| النبضة | Left | السرعة بعد النبضة |
|---|---|---|
| 1 | 12 | -8 |
| 2 | 4 | -8 |
| 3 | -4 | 8 |
| 4 | 4 | 8 |
| 5 | 12 | 8 |
في النبضة 3 تصبح قيمة Left هي -4، فيصبح النصف الثاني من الشرط (Left أقل من 0) صحيحا، وتتغير السرعة إلى 8. وأصغر قيمة تأخذها Left هي -4: فلمدة نبضة واحدة تكون 4 بكسلات من التسمية خارج النموذج. وهذه هي القيم التي سجلها برنامجنا، ومنها مثلا tick 3: labelB.Left = -4, Bx = 8.
التمرين 3
- الخط 1:
θ = 0، من(280, 200)إلى(430, 200). - الخط 2:
θ = 144. x2 = 430 + 150 · cos(144°) = 430 - 121.35 = 308.65، وبعد حذف الكسر 308؛ وy2 = 200 + 150 · sin(144°) = 200 + 88.17 = 288.17، وبعد حذف الكسر 288. فالخط 2 يمتد من(430, 200)إلى(308, 288). - إنه نجمة خماسية (five-pointed star). ومجموع الانعطافات الخمسة 5 · 144 = 720 درجة، أي دورتان كاملتان، لذا يقطع القلم مساره.
- لا. ينتهي الخط 5 عند
(278, 198)، كما طبع البرنامج: فكل عملية حذف بالتحويل(int)تفقد قليلا.

التمرين 4
int L3x = 3;
int L3y = 7;
// in timer1_Tick, after label2
label3.Left += L3x;
label3.Top += L3y;
if (label3.Left + label3.Width > ClientSize.Width || label3.Left < 0)
{
L3x = -L3x;
}
if (label3.Top + label3.Height > ClientSize.Height || label3.Top < 0)
{
L3y = -L3y;
}بعد k من النبضات يكون أعلى التسمية عند 160 + 7k، وحافتها السفلية أدنى منه بمقدار 32 بكسل. ويصبح فحص الأسفل صحيحا عندما يكون 160 + 7k + 32 أكبر من 300، أي عندما يكون k أكبر من 15.4. لذا ترتد label3 في النبضة 16، عن الحافة السفلية: Top = 272، و272 + 32 = 304 أكبر من 300. والحافة اليمنى ما زالت بعيدة: فقيمة Left عندئذ 198 فقط، وفحص اليمين يتطلب أن يكون 150 + 3k + 92 أكبر من 420، أي أن يكون k أكبر من 59.3. ويتفق سجل برنامجنا مع ذلك: tick 16 at 811 ms: label3 (198, 272) L3x 3 L3y -7، وفي النبضة 17 يعود أعلى التسمية إلى 265.
