نماذج ويندوز: الشيفرة خلف النموذج
في الدرس العملي 1، نموذجك الأول لسطح المكتب، بنينا نموذجا (form) بسحب عناصر التحكم (controls) وكتابة بضعة معالجات للأحداث (event handlers). ينظر هذا الدرس إلى ما خلف سطح التصميم (design surface): الشيفرة التي يكتبها المصمم (designer)، ولماذا يرفض المصمم أحيانا أن يفتح وكيف تصلحه دون أن تبدأ من جديد، ولماذا النموذج مجرد صنف (class)، والعادات التي تحافظ على نظافة مشروع نماذج ويندوز (Windows Forms) من الدقيقة الأولى. بني كل برنامج في هذا الدرس وشغل على ويندوز، وكل لقطة شاشة ورسالة ورقم أدناه مأخوذ من تلك التشغيلات.
الأهداف
بنهاية هذا الدرس ينبغي أن تكون قادرا على:
- شرح ماهية التطوير السريع للتطبيقات (rapid application development)، ومتى تكون نماذج ويندوز خيارا جيدا أو ضعيفا.
- قراءة الشيفرة التي يكتبها المصمم في
InitializeComponent، وإصلاح مصمم لم يعد يفتح. - حذف معالج حدث بالطريقة الصحيحة، من قائمة الأحداث (Events) في نافذة الخصائص (Properties window).
- شرح أن النموذج صنف يرث من
Form، وتغيير خصائص (properties) عناصر التحكم أثناء تشغيل البرنامج. - اختيار النموذج الأول في
Program.cs، وفتح نموذج ثان، وتمرير البيانات إليه عبر بانيه (constructor). - إضافة TabControl، وMenuStrip باختصارات لوحة المفاتيح، وStatusStrip.
- تحديد الأحداث التي تطلقها نقرة الفأرة وضغطة المفتاح وسطر من الشيفرة.
- بدء المشروع بالطريقة الصحيحة: إعادة تسمية النموذج، وضبط خاصيتيه Text وFont أولا، وتسمية كل عنصر تحكم قبل استخدامه، وفحص ترتيب الانتقال، وإبقاء المنطق خارج النموذج.
الجزء 1: لماذا نماذج ويندوز
التطوير السريع للتطبيقات
نماذج ويندوز (WinForms) أحد أنواع المشاريع الأصلية في .NET. ظهرت مع أول إصدار من .NET في 2001 و2002، وهي بيئة للتطوير السريع للتطبيقات: تسحب عناصر التحكم من صندوق الأدوات (Toolbox)، وتضبط بضع خصائص، وتنقر نقرا مزدوجا لتضيف معالجا، فيصبح لديك برنامج سطح مكتب يعمل.
هذه السرعة هي نقطة قوتها وفخها في آن واحد. فهي تجعل إثبات المفهوم (proof of concept) رخيصا جدا: بضع ساعات، أو يومان، تكفي لمعرفة ما إذا كانت الفكرة تنجح. لكن البرنامج المبني بالسحب والنقر المزدوج وحدهما، دون فهم الشيفرة التي خلفه، يصبح صعب التغيير فيما بعد. وبقية هذا الدرس عن تلك الشيفرة.
نماذج ويندوز وأنواع مشاريع سطح المكتب الأخرى
| نوع المشروع | طريقة تصميم النافذة | الوضع الحالي |
|---|---|---|
| نماذج ويندوز | السحب والإفلات على سطح التصميم | موجودة منذ أول إصدار من .NET؛ وما زالت شائعة جدا في الشركات |
| WPF | لغة XAML، وهي لغة ترميز (markup language) لوصف الواجهة | الخيار المعتاد لتطبيق سطح مكتب جديد |
| UWP | XAML | مرتبطة بأحدث إصدارات ويندوز؛ ومهملة (deprecated) |
| .NET MAUI | XAML | سطح المكتب والهاتف المحمول من مشروع واحد؛ وما زالت حديثة |
تصف WPF وUWP و.NET MAUI كلها النافذة بلغة XAML، وهي أقوى من سحب عناصر التحكم إلى النموذج لكنها أكثر تعقيدا.
متى تستخدمها
- نسخة أولية سريعة (prototype) لاختبار فكرة.
- أداة صغيرة لك أو لفريقك: برنامج صغير يؤدي مهمة واحدة.
- تطبيق بسيط يبنيه شخص واحد ويصونه.
- تطبيقات الأعمال القائمة: بنت شركات كثيرة أدوات سطح المكتب الخاصة بها بنماذج ويندوز وما زالت توسعها، لذا من المرجح أن تقابلها في العمل.
أما لتطبيق سطح مكتب جديد وكبير فإن WPF هو الخيار الأفضل عادة، والمثال التالي يبين أحد الأسباب.
تحذير بشأن النسخ الأولية: النسخة الأولية التي تعمل تبدو مكتملة. فإذا عرضت على عميل نموذجا يستجيب للنقر بالفعل، فقد يطلبه في الغد، مع أنه مجرد عرض تجريبي. انتبه لمن يرى النسخة الأولية ولما تعد به.
النموذج لا يعيد ترتيب نفسه
توقع: صمم النموذج أدناه بحجم 420 في 235 بكسل. ماذا يحدث لعناصر التحكم فيه عند تصغير النافذة، أو تكبيرها إلى أقصى حد؟

شغلنا النموذج نفسه مرتين، مرة بعد ضبط خاصيته Size على 300 في 200، ومرة بعد تكبيره إلى أقصى حد:


تبقى عناصر التحكم حيث وضعت. فالنافذة الأصغر تقطعها دون أشرطة تمرير (scroll bars)، والنافذة الأكبر تترك مساحة فارغة. نموذج ويندوز "ما تراه هو ما تحصل عليه". وجعله يتكيف مع حجم النافذة يتطلب عملا إضافيا، وهذا أحد أسباب تفضيل WPF عادة لتطبيق جديد.
الجزء 2: عرض سريع، والشيفرة التي خلفه
ابنه في دقيقتين
هكذا تكون سرعة النسخة الأولية. ابدأ مشروعا جديدا من نوع Windows Forms App، ودون أن تعيد تسمية أي شيء:
- اسحب Label وTextBox إلى النموذج. واضبط الخاصية Text للتسمية على
First Name. - اختر الاثنين (انقر أحدهما، ثم Ctrl+نقرة على الآخر)، واضغط Ctrl+C ثم Ctrl+V مرتين: تحصل على زوجين آخرين من التسميات ومربعات النص. وتظهر خطوط المحاذاة (snaplines) أثناء السحب لتساعدك على ترتيبها في صف واحد.
- اسحب Button وCheckBox.
- اسحب ProgressBar واضبط خاصيته Value على
45. يعرض الشريط 45 من 100.
شغله: يمكنك الكتابة، والضغط على Tab للتنقل بين الحقول، ووضع علامة في مربع الاختيار. لا يفعل البرنامج شيئا بعد، لكنه يبدو برنامجا حقيقيا.
اربط الزر
انقر نقرا مزدوجا على الزر. ينشئ Visual Studio المعالج button1_Click ويفتحه. أضف سطرا واحدا:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show($"Hello {textBox1.Text} {textBox2.Text}");
}textBox1هو مربع النص الأول؛ ويظهر اسمه الافتراضي في أعلى نافذة الخصائص.- مربع النص كائن (object)، و
Textخاصية لذلك الكائن: النص الذي كتبه المستخدم. $"..."هو استيفاء السلاسل النصية (string interpolation): يستبدل كل تعبير بين قوسين معقوصين بقيمته.
توقع: يكتب المستخدم Lab في المربع الأول وStudent في المربع الثاني، ثم ينقر الزر. ماذا يقول مربع الرسالة؟

يقول Hello Lab Student. وما زالت التسميات تحمل label2 وlabel3، والزر button1: هذا عرض سريع، لا برنامج مكتمل.
أين توجد تلك الشيفرة
انقر بالزر الأيمن على Form1.cs واختر View Code: الصنف معرف بالشكل public partial class Form1 : Form. وكلمة partial تعني أن الصنف مكتوب في ملفين، ويدمجهما المترجم (compiler) في صنف واحد. والنصف الآخر هو Form1.Designer.cs، تحت Form1.cs في مستكشف الحلول (Solution Explorer). وتحتوي دالته (method) InitializeComponent على كل ما فعله المصمم لك. هذا جزء من ملفنا:
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.textBox1 = new System.Windows.Forms.TextBox();
// ... one line like these for every control ...
this.button1 = new System.Windows.Forms.Button();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(120, 140);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(100, 30);
this.button1.TabIndex = 6;
this.button1.Text = "button1";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
// ...
this.progressBar1.Value = 45;
// ...
this.Controls.Add(this.button1);
this.Controls.Add(this.progressBar1);
// ...
}اقرأها والدرس العملي 1 في ذهنك:
- ينشئ
new System.Windows.Forms.Button()كائنا من الصنفButton. فكل عنصر تحكم على النموذج كائن مخزن في حقل (field) من حقول Form1. - كل سطر
this.button1.Something = ...قيمة واحدة من نافذة الخصائص. - السطر
this.button1.Click += new System.EventHandler(this.button1_Click);يربط الحدث Click للزر بالدالةbutton1_Clickالموجودة في النصف الآخر من الصنف، أي Form1.cs. - يضع
this.Controls.Add(...)عنصر التحكم على النموذج.
يقول رأس الملف "لا تعدل محتويات هذه الدالة بمحرر الشيفرة". وهذا صحيح في 99 مرة من 100. والمثال التالي هو الاستثناء.
المصمم مجرد شيفرة تعرض
سطح التصميم ليس صورة يخزنها Visual Studio في مكان ما. فلكي يعرض المصمم النموذج يقرأ هذه الشيفرة وينفذها. فإذا كانت الشيفرة معطوبة لم يبق شيء يعرض، ويتوقف المصمم برسالة خطأ بدلا من النموذج.
مثال محلول: المصمم يتعطل
حادث تقليدي:
- تنقر نقرا مزدوجا على جزء فارغ من النموذج بالخطأ. فينشئ Visual Studio المعالج الخاص بالحدث الافتراضي للنموذج، وهو Load: دالة
Form1_Loadفي Form1.cs، وسطر ربط واحدا في Form1.Designer.cs. - لم تكن تريده، فتحذف الدالة
Form1_Loadمن Form1.cs.
توقع: ما زال سطر الربط موجودا في ملف المصمم. ماذا يحدث بعد ذلك؟
لم يعد البرنامج يبنى. هذه رسالة المترجم من عملية البناء التي أجريناها (بعد اختصار مسار الملف):
Form1.Designer.cs(129,55): error CS1061: 'Form1' does not contain a definition for 'Form1_Load' and no accessible extension method 'Form1_Load' accepting a first argument of type 'Form1' could be found (are you missing a using directive or an assembly reference?)ويرفض المصمم فتح النموذج. وفي فيديو المعمل عرض:
The designer cannot process unknown name 'Form1_Load' at line 153. The code within the method 'InitializeComponent' is generated by the designer and should not be manually modified. Please remove any changes and try opening the designer again.يعيد كثير من المبتدئين تشغيل Visual Studio، ثم يحذفون المشروع ويبدؤون من جديد. ولا حاجة إلى ذلك: اقرأ الرسالة. فهي تسمي المشكلة (Form1_Load) ورقم السطر. والسطر 129 من ملف Form1.Designer.cs لدينا هو:
this.Load += new System.EventHandler(this.Form1_Load);حذف المعالج من مكان واحد (الدالة في Form1.cs) ولم يحذف من المكان الآخر (سطر الربط في Form1.Designer.cs)، لذا يشير هذا السطر إلى دالة لم تعد موجودة.
الإصلاح: انقر Go to code في صفحة الخطأ التي يعرضها المصمم (أو افتح Form1.Designer.cs)، واحذف ذلك السطر وحده، واحفظ، وافتح المصمم مرة أخرى. يعود النموذج. وهذه هي المرة الوحيدة التي تعدل فيها الشيفرة المولدة يدويا: تحذف السطر الذي لا يستطيع المصمم نفسه إصلاحه.
احذف المعالج بالطريقة الصحيحة
لا تحذف المعالج بحذف دالته. احذفه من حيث أنشئ:
- افتح النموذج في المصمم واختر عنصر التحكم (أو النموذج نفسه).
- في نافذة الخصائص انقر الزر الذي يحمل رمز البرق: وهو قائمة الأحداث لعنصر التحكم ذلك.
- ابحث عن الحدث (وهو هنا Load)، واختر اسم المعالج المقابل له، واحذفه.
- يختفي سطر الربط. ويحذف Visual Studio الدالة الفارغة أيضا في العادة؛ فإذا بقيت في Form1.cs فاحذفها: لم يعد شيء يشير إليها.
وقائمة الأحداث هي أيضا المكان الذي ترى فيه كل حدث لعنصر التحكم، لا حدثه الافتراضي وحده: فللزر Click وMouseClick وMouseHover وDragDrop وغيرها كثير. والنقر المزدوج على عنصر التحكم لا ينشئ إلا معالج حدثه الافتراضي (Click للزر، وLoad للنموذج).
تفاعلي: عطل المصمم وأصلحه
تعرض الأداة التفاعلية أدناه نموذجا في المصمم بجوار ملفي الشيفرة الخاصين به. انقر نقرا مزدوجا على النموذج، واحذف الدالة بالطريقة الخاطئة، وابن المشروع، وشاهد المصمم يتعطل؛ ثم أصلحه من رسالة الخطأ، أو احذف المعالج من قائمة الأحداث. وتبين الأداة أيضا لماذا ينبغي أن تسمي عنصر التحكم قبل أن تنقر عليه نقرا مزدوجا. ويعرض تشغيل العرض القصة كلها خطوة خطوة.
الجزء 3: النموذج صنف
الوراثة: ما يعطيك إياه Form
تعني Form1 : Form أن Form1 يرث (inherits) من الصنف Form. ضع المؤشر على Form واضغط F12 لترى تعريفه؛ وافعل الشيء نفسه مع صنفه الأساسي (base class)، ثم مرة أخرى. طبعنا السلسلة كاملة من برنامج قيد التشغيل:
WinFormsDemo.Form1
System.Windows.Forms.Form
System.Windows.Forms.ContainerControl
System.Windows.Forms.ScrollableControl
System.Windows.Forms.Control
System.ComponentModel.Component
System.MarshalByRefObject
System.Objectيضيف كل مستوى شيئا. ولهذا يملك النموذج الفارغ بالفعل شريط عنوان، وأيقونة، وأزرار التصغير والتكبير والإغلاق، وخلفية، وقابلية تغيير الحجم، والقدرة على رسم عناصر التحكم التي تضيفها. ولا يعيد أحد كتابة تلك الشيفرة لكل برنامج: بل ترثها. أما بناء نافذة من الصفر دون هذا الصنف الأساسي فيعني أن تكتب ذلك كله بنفسك: شريط العنوان، وزر الإغلاق، والخلفية، وقائمة النقر بالزر الأيمن.
الخصائص في المصمم وفي وقت التشغيل
كل ما في نافذة الخصائص خاصية عادية في C# لكائن ما. والقيمة التي تضبطها في المصمم ليست إلا القيمة الابتدائية: فشيفرتك تستطيع قراءتها وتغييرها أثناء تشغيل البرنامج.
في باني النموذج، بعد InitializeComponent()، غيرنا تسميتين:
public Form1()
{
InitializeComponent();
label2.Text = "Last Name";
label3.Text = "Full Name";
}وأصبح الزر يملأ مربع النص الثالث ويحرك شريط التقدم:
private void button1_Click(object sender, EventArgs e)
{
textBox3.Text = $"{textBox1.Text} {textBox2.Text}";
progressBar1.Value += 10;
}
ما زال المصمم يعرض label2 وlabel3: فالنصان الجديدان لا يوجدان إلا أثناء تشغيل البرنامج. وللنص الثابت يكون ضبطه في المصمم أبسط؛ أما الشيفرة فللقيم التي تتغير.
توقع: يكتب المستخدم Lab وStudent وينقر الزر مرة واحدة. ماذا يوجد في المربع الثالث، وأين الشريط؟

أصبحت textBox3.Text تساوي Lab Student، وانتقلت progressBar1.Value من 45 إلى 55.
مثال محلول: خاصية لها قواعد
لشريط التقدم ProgressBar ثلاث خصائص مترابطة. قرأنا قيمها الافتراضية على ويندوز: Minimum تساوي 0، وMaximum تساوي 100، ويجب أن تبقى Value بينهما.
توقع: يبدأ الشريط عند 45 وتضيف كل نقرة 10. ماذا يحدث في النقرة السادسة؟
| النقرة | القيمة قبلها | القيمة بعدها |
|---|---|---|
| 1 | 45 | 55 |
| 2 | 55 | 65 |
| 3 | 65 | 75 |
| 4 | 75 | 85 |
| 5 | 85 | 95 |
| 6 | 95 | 105، غير مسموح |
تحاول النقرة السادسة ضبط القيمة على 105، وهي أكبر من Maximum. ويعرض البرنامج عند تشغيله خارج Visual Studio ما يلي:

Value of '105' is not valid for 'Value'. 'Value' should be between 'minimum' and 'maximum'. (Parameter 'value')
Actual value was 105.قد ترفض الخاصية قيمة ما. والشيفرة التي تغير خاصية في وقت التشغيل يجب أن تحترم قواعد تلك الخاصية.
النموذج كائن: نوافذ كثيرة
إذا كان Form1 صنفا، فيمكنك أن تنشئ منه ما تشاء من الكائنات. وShow دالة يرثها Form1 من Form. غيرنا معالج الزر إلى:
private void button1_Click(object sender, EventArgs e)
{
Form1 frm = new Form1();
frm.Show();
frm.Left = this.Left + 430;
}السطر الثالث لا يفعل إلا تحريك النافذة الجديدة إلى يمين الأولى، لكي تظهر النافذتان معا. وLeft خاصية موروثة أخرى.
توقع: يكتب المستخدم Lab وStudent في النافذة الأولى، ثم ينقر button1 مرتين. ماذا تعرض النافذتان الجديدتان؟

تفتح نافذتان جديدتان، ومربعاتهما فارغة. فكل نافذة كائن مستقل من الصنف Form1، له مربعات النص الخاصة به: والكتابة في إحداها لا تغير الأخرى.
الجزء 4: أكثر من نموذج واحد
Program.cs يختار النموذج الأول
عندما تضغط Start، كيف يعرف البرنامج أي نموذج يفتح؟ Program.cs:
namespace WinFormsDemo
{
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
}ينشئ Application.Run(new MainForm()) النموذج الأول. ضع هناك صنف نموذج آخر، فيفتح ذلك النموذج أولا بدلا منه. والنموذج الأول هو النموذج الرئيسي (main form): يعيش التطبيق ما دام هذا النموذج مفتوحا.
مرر البيانات عبر الباني
أضف نموذجا ثانيا من Add, Form في مستكشف الحلول: Form2، وفيه تسمية واحدة كبيرة، messageLabel. وباني النموذج باني عادي، لذا يمكن أن يأخذ معاملات (parameters):
public partial class Form2 : Form
{
private string formMessage;
public Form2(string message)
{
InitializeComponent();
formMessage = message;
messageLabel.Text = formMessage;
}
}بعد أن أصبح الباني يحتاج إلى رسالة، لم يعد new Form2() يترجم: فالمستدعي يجب أن يمرر قيمة. وزر النموذج الرئيسي يفعل ذلك:
private void openForm2Button_Click(object sender, EventArgs e)
{
Form2 frm = new Form2("Hello from Form 1");
frm.Show();
}توقع: ماذا يظهر عندما ينقر المستخدم Open Form 2؟

انتقل النص من نموذج إلى آخر عبر الباني. ويمكنك أن تمرر بهذه الطريقة أي قيمة، حتى مرجعا (reference) إلى النموذج الأول، لكي يستطيع Form2 أن يستدعيه.
مثال محلول: أغلق النموذج الرئيسي
توقع: Form2 مفتوح. يختار المستخدم File, Exit في النموذج الرئيسي، فينفذ this.Close(). هل يبقى Form2 مفتوحا؟
لا. في تشغيلنا انتهى البرنامج كله برمز الخروج (exit code) 0 مباشرة بعد النقر على Exit (استمر التشغيل 5.3 ثانية وتوقف عند تلك الخطوة)، مع أن Form2 كان ما زال مفتوحا. فعمر النموذج الرئيسي هو عمر التطبيق: عندما يغلق يغلق التطبيق، ويغلق معه كل نموذج آخر.
الجزء 5: الحاويات والقوائم وشريط الحالة
في صندوق الأدوات أكثر من مجموعة Common Controls. ومجموعتان منه تستحقان المعرفة الآن.
الحاويات: GroupBox وTabControl
يجمع GroupBox (الدرس العملي 1) عناصر التحكم المترابطة تحت عنوان. ويحتوي TabControl على عدة صفحات تبويب (tab pages): النقر على تبويب يعرض صفحة مختلفة، كأنها نموذج مختلف، لكنه نموذج واحد. وكل صفحة حاوية (container): اسحب عناصر التحكم إليها. ونموذجنا الرئيسي فيه صفحة Main بزرين، وصفحة Options فيها مربع اختيار:

StatusStrip: شريط معلومات في الأسفل
من مجموعة Menus & Toolbars اسحب StatusStrip إلى النموذج: يلتصق بالحافة السفلية، مثل شريط الحالة (status bar) في Visual Studio. واستخدم قائمته المنسدلة لتضيف StatusLabel وProgressBar. يسميهما المصمم toolStripStatusLabel1 وtoolStripProgressBar1؛ ويستخدمهما الزر Work:
private void workButton_Click(object sender, EventArgs e)
{
toolStripProgressBar1.Value += 5;
toolStripStatusLabel1.Text = "Working...";
}توقع: تبدأ تسمية الحالة بالنص Ready، والشريط عند 0. ماذا يعرض الشريط بعد ثلاث نقرات على Work؟

Working...، وشريط عند 15 من 100. والبرنامج الحقيقي يستخدم هذا الشريط ليبلغ عن تقدمه أثناء العمل، ثم يعرض Ready مرة أخرى في النهاية.
MenuStrip والمفتاح &
اسحب MenuStrip إلى النموذج واكتب عناصر القائمة (menu items) عليه مباشرة. واكتب & قبل الحرف الذي يفتح العنصر من لوحة المفاتيح:
| ما تكتبه | ما تعرضه القائمة | لوحة المفاتيح |
|---|---|---|
&File | File، وتحت F خط | Alt+F تفتحها |
E&xit | Exit، وتحت x خط | X، داخل القائمة File |
&Help | Help، وتحت H خط | Alt+H تفتحها |
&About | About، وتحت A خط | A، داخل القائمة Help |
تظهر الخطوط تحت الحروف عندما يضغط المستخدم Alt. فالضغط على Alt ثم F ثم X يغلق البرنامج دون لمس الفأرة: وهكذا تصبح برامج ويندوز متاحة (accessible) للمستخدمين الذين لا يستطيعون استخدام الفأرة. التزم بالأسماء المعيارية: قائمة File فيها Exit على الحرف X، وقائمة Help فيها About. ولا يجوز أن يشترك عنصران في القائمة نفسها في حرف واحد؛ أما العناصر في قوائم مختلفة فيجوز.
يخزن المصمم الرمز & في الخاصية Text:
this.fileToolStripMenuItem.Text = "&File";
this.exitToolStripMenuItem.Text = "E&xit";
this.helpToolStripMenuItem.Text = "&Help";
this.aboutToolStripMenuItem.Text = "&About";عنصر القائمة مثل الزر: انقر عليه نقرا مزدوجا لتحصل على معالج الحدث Click الخاص به. وإذا نقرت أحدها نقرا مزدوجا بالخطأ، فاحذف المعالج من قائمة الأحداث (الجزء 2)، لا بحذف الدالة.
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
this.Close();
}
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
{
MessageBox.Show("Created in the OS1 lab");
}this هو الكائن الحالي، أي النموذج الرئيسي، وClose دالة يرثها من Form.

الاسمان fileToolStripMenuItem وtoolStripProgressBar1 هما الاسمان الافتراضيان من المصمم. وهما يعملان، لكنهما طويلان ولا يقولان الكثير. والجزء 6 عن التسمية.
الحدث Click أكثر من نقرة فأرة
الحدث Click للزر يعني "جرى تفعيل الزر"، لا "ضغط زر الفأرة". ولكي نرى بالضبط ما يطلقه كل إجراء، ربطنا معالج تسجيل بعدة أحداث لنموذج صغير وشغلناه على ويندوز بلوحة المفاتيح والفأرة الحقيقيتين. كل صف إجراء واحد؛ والأحداث مرتبة حسب ترتيب إطلاقها:
| ما يفعله المستخدم أو الشيفرة | الأحداث المطلقة، بالترتيب |
|---|---|
يكتب حرفا واحدا في firstNameText | KeyPress، TextChanged |
يضغط Backspace في firstNameText | KeyPress، TextChanged |
ينقر في firstNameText بالفأرة | Click، MouseClick |
يضغط Space على sayHelloButton وعليه التركيز | Click |
يضغط Enter على sayHelloButton وعليه التركيز | Click |
ينقر sayHelloButton بالفأرة | MouseDown، Click، MouseClick، MouseUp |
ينقر agreeCheck بالفأرة | CheckedChanged، Click، MouseClick |
يضغط Space على agreeCheck وعليه التركيز | CheckedChanged، Click |
ينقر عنصرا من cityList بالفأرة | Click، MouseClick، SelectedIndexChanged |
| ينقر العنصر المختار بالفعل | Click، MouseClick، SelectedIndexChanged |
يضغط السهم Down في cityList | SelectedIndexChanged |
| يضغط Alt+H ثم A | Click الخاص بـaboutToolStripMenuItem |
تنفذ الشيفرة firstNameText.Text = "Code"; | TextChanged |
| تضبط الشيفرة النص نفسه مرة أخرى | لا شيء |
تنفذ الشيفرة sayHelloButton.PerformClick(); | Click |
سجلنا KeyPress وTextChanged وClick وMouseClick على مربع النص؛ وClick وMouseClick وMouseDown وMouseUp على الزر؛ وCheckedChanged وClick وMouseClick على مربع الاختيار؛ وSelectedIndexChanged وClick وMouseClick على ListBox؛ وClick على عنصر القائمة About.
ثلاثة أمور تستحق الملاحظة:
- الضغط على Space أو Enter على زر عليه التركيز (focus) يطلق Click ولا يطلق MouseClick. ضع عمل الزر في Click فيعمل مع الفأرة ولمسة الإصبع ولوحة المفاتيح.
- تغيير قيمة من الشيفرة يطلق الحدث نفسه الذي يطلقه المستخدم: ضبط
Textأطلق TextChanged. وضبط النص نفسه مرة أخرى لم يطلق شيئا. - يطلق كل عنصر تحكم عدة أحداث لإجراء واحد؛ وأنت تعالج ما تحتاج إليه منها فقط، والبقية لا تفعل شيئا.
تفاعلي: ساحة الأحداث
استخدم النموذج في الأداة التفاعلية بالفأرة ولوحة المفاتيح، وشاهد كل حدث يظهر في السجل، مع تظليل المعالج الذي يعمل له في Form1.cs. ويعرض تشغيل العرض الإجراءات الموجودة في الجدول أعلاه.
الجزء 6: بدء المشروع بالطريقة الصحيحة
العرض السريع يعمل، لكنه مليء بأسماء مثل label2 وbutton1. فعندما تبدأ مشروعا حقيقيا افعل هذه الأمور، بهذا الترتيب، قبل أن تكتب أي شيفرة.
الخطوة 1: أعد تسمية Form1
في مستكشف الحلول اختر Form1.cs واضغط F2 (أو انقر بالزر الأيمن واختر Rename). وسمه MainForm.cs. يسألك Visual Studio هل يعيد تسمية كل الإشارات إلى Form1 في الشيفرة أيضا: اختر Yes. أصبح الصنف الآن MainForm، وأصبح Program.cs يقول Application.Run(new MainForm());.
الخطوة 2: اضبط الخاصية Text للنموذج
ما زال شريط العنوان يقول Form1. فقد تغير اسم الملف واسم الصنف، لكن العنوان خاصية مستقلة، هي Text، ولها قيمتها الخاصة. اضبطها على قيمة ذات معنى، مثل Main Form.
الخطوة 3: اضبط الخط أولا
اختر النموذج واضبط خاصيته Font، مثلا على Segoe UI 14 (القيمة الافتراضية Segoe UI 9). وافعل ذلك قبل إضافة عناصر التحكم، لسببين.
توقع: نموذجنا بحجم 360 في 150 بكسل وفيه زر بحجم 100 في 30، وقد صمم بالخط الافتراضي ذي 9 نقاط. غيرنا خط النموذج إلى 18 نقطة. ماذا يحدث للأحجام؟


الأحجام المقيسة قبل التغيير وبعده:
| العنصر | خط 9 نقاط | خط 18 نقطة |
|---|---|---|
| الحجم الداخلي للنموذج (ClientSize) | 360 x 150 | 669 x 320 |
| الزر | 100 x 30 عند (120, 60) | 186 x 64 عند (223, 128) |
| مربع النص | 180 x 23 | 331 x 39 |
| تسمية تستخدم خط النموذج | 9 pt | 18 pt |
| تسمية لها خطها الخاص بحجم 10 pt | 10 pt | 10 pt |
يخزن المصمم حجم الخط الذي صمم به النموذج، AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F): أي أن متوسط الحرف 7 في 15 بكسل عند 9 نقاط. وعند 18 نقطة يصبح المقياس نفسه 13 في 32، لذا يضرب كل عرض وكل موضع أفقي في 13/7، وكل ارتفاع في 32/15:
360 x 13 / 7 = 668.6, shown as 669 150 x 32 / 15 = 320
100 x 13 / 7 = 185.7, shown as 186 30 x 32 / 15 = 64(ارتفاع مربع النص 39 لا 49، لأن مربع النص يحدد ارتفاعه بنفسه من خطه.)
إذن السبب الأول: تغيير خط نموذج مكتمل يغير حجم كل شيء، فيضاعفه تقريبا، ثم يجب عليك أن تسحب النموذج ليعود إلى حجم معقول (أو تكتب قيمة جديدة للخاصية Size في نافذة الخصائص). أما إذا ضبطت الخط على نموذج فارغ فلا يحتاج شيء إلى إصلاح.
والسبب الثاني في الصفين الأخيرين. فعنصر التحكم الذي ليس له خط خاص به يستخدم خط النموذج: وكل عنصر تحكم تسحبه إلى النموذج بعد ضبط الخط يأتي بالحجم الصحيح من البداية. لكن عنصر التحكم الذي غيرت خطه يحتفظ بقيمته الخاصة، ولا تصل إليه التغييرات اللاحقة على خط النموذج. وتغيير القيم الافتراضية متأخرا يترك النموذج بأحجام مختلطة.
الخطوة 4: سم كل عنصر تحكم، أولا
الخاصية Name لعنصر التحكم هي اسم المتغير الخاص به في الصنف. لذا سمه كما تسمي متغيرا في C#: بصيغة camelCase، وبكلمات تقول ما هو.
| الأسلوب القديم (الترميز المجري، Hungarian notation) | أسلوب هذا الدرس |
|---|---|
lblFirstName | firstNameLabel |
txtFirstName | firstNameText |
btnSayHello | sayHelloButton |
lstCity | cityList |
استخدم الدرس العملي 1 بادئات قصيرة مثل txtName، وهو أسلوب كان شائعا في السنوات الأولى من .NET وأوصت به Microsoft في وقت ما. أما الآن فتوصي Microsoft بعدم استخدامه: فالشيفرة يقرؤها البشر، و"تسمية الاسم الأول" تقرأ بشكل طبيعي، بينما "L B L first name" لا تقرأ كذلك. واجعل الأزواج متسقة أيضا: firstNameLabel مع firstNameText، وlastNameLabel مع lastNameText.
سم عنصر التحكم قبل أن تنقر عليه نقرا مزدوجا. فاسم المعالج يؤخذ من اسم عنصر التحكم لحظة النقر المزدوج:
- انقر نقرا مزدوجا على زر ما زال اسمه
button1: يصبح المعالجbutton1_Click. - أعد تسمية الزر إلى
sayHelloButtonبعد ذلك: يبقى المعالجbutton1_Clickكما هو. فإعادة تسمية عنصر التحكم لا تعيد تسمية الدالة. - بعد أشهر يحتوي Form1.cs على
button1_Clickوbutton2_Clickوbutton3_Click، ولا أحد يعرف أيها لأي زر.
لكي تتأكد من أنه لم يبق شيء بلا اسم، افتح القائمة المنسدلة في أعلى نافذة الخصائص: فهي تعرض كل كائن على النموذج. وأي label1 أو button2 في تلك القائمة ما زال يحتاج إلى اسم.
وهناك اختصاران في المصمم يساعدان هنا. اسحب مستطيلا حول عدة عناصر تحكم (أو Ctrl+نقرة عليها) لتحركها معا، أو لتضبط خاصية مشتركة بينها: فمع اختيار عنصري تحكم اكتب في Name مباشرة. وعندما تضع مربع نص بجوار تسمية، اسحبه حتى يمر خط المحاذاة عبر نص كل منهما: فيصبح النص داخل المربع على استقامة نص التسمية.
الخطوة 5: افحص ترتيب الانتقال
المستخدم الذي لا يستطيع استخدام الفأرة، أو الذي يكتب بسرعة فحسب، يتنقل في النموذج بالمفتاح Tab. ويتبع الترتيب الخاصية TabIndex لكل عنصر تحكم، من الأصغر إلى الأكبر، متجاوزا عناصر التحكم التي لا يمكن أن تأخذ التركيز. قرأنا القيم الافتراضية على ويندوز: الخاصية TabStop تساوي True لكل من TextBox وButton وFalse للتسمية Label، لذا تتجاوز التسميات.
يعطي المصمم كل عنصر تحكم جديد قيمة TabIndex التالية، لذا يكون ترتيب الانتقال هو الترتيب الذي أضفت به عناصر التحكم. وفي نموذجنا أضيف الزر قبل الاسم الأخير:
| عنصر التحكم | TabIndex |
|---|---|
| firstNameLabel | 0 |
| firstNameText | 1 |
| sayHelloButton | 2 |
| lastNameLabel | 3 |
| lastNameText | 4 |
توقع: المؤشر في مربع الاسم الأول. إلى أين تنقل كل ضغطة على Tab؟
ضغطنا Tab على ويندوز وسجلنا التركيز: firstNameText، ثم sayHelloButton، ثم lastNameText، ثم العودة إلى firstNameText. يأتي الزر قبل الاسم الأخير، وبعد آخر عنصر تحكم يعود التركيز إلى الأول. اضبط TabIndex للعنصر lastNameText على 2 وللعنصر sayHelloButton على 3، فيصبح الترتيب: الاسم الأول، ثم الاسم الأخير، ثم الزر.
ولا يلزم أن تكون الأرقام متتالية. فالمهم ترتيبها وحده: ينتقل ويندوز إلى عنصر التحكم التالي، حسب ترتيب TabIndex، الذي يمكن أن يأخذ التركيز. ومع firstNameText عند 10، وlastNameText عند 45، وsayHelloButton عند 50، أعطى التشغيل: الاسم الأول، ثم الاسم الأخير، ثم الزر، تماما كما مع 1 و2 و3. وترك فجوات (10 و50) يتيح لك أن تدخل عنصر تحكم جديدا بينها لاحقا (45) دون إعادة الترقيم.
فخ: الكائن بدلا من خاصيته Text
مع الأسماء الجيدة تصبح التحية سهلة القراءة والكتابة. توقع ما يعرضه هذا الإصدار عندما يكتب المستخدم Lab وStudent:
private void sayHelloButton_Click(object sender, EventArgs e)
{
MessageBox.Show($"Hello {firstNameText} {lastNameText}");
}
Hello System.Windows.Forms.TextBox, Text: Lab System.Windows.Forms.TextBox, Text: Studentتبنى الشيفرة وتعمل، لكن firstNameText هو كائن TextBox كاملا، وتحويل كائن إلى سلسلة نصية يعطي اسم صنفه مع وصف. وما أردته هو خاصية واحدة من ذلك الكائن: firstNameText.Text.
الخطوة 6: أبق المنطق خارج النموذج
من المغري أن تضع الشيفرة كلها في MainForm.cs، حتى الشيفرة التي تقرأ من قاعدة بيانات. والمشاريع التي تفعل ذلك تنتهي بآلاف الأسطر خلف نموذج واحد، وعندما يحين وقت الانتقال إلى WPF، أو إلى إصدار أحدث من .NET، يجب أن تعاد كتابة كل شيء لأن المنطق متشابك مع النافذة.
بدلا من ذلك أضف إلى الحل (solution) مشروعا من نوع مكتبة أصناف (Class Library) وضع فيه كل جزء من المنطق. ولا يحتفظ النموذج إلا بالشيفرة التي تتعامل مع المستخدم: تقرأ مربعات النص، وتستدعي المكتبة، وتعرض النتيجة. حتى التحية مكانها المكتبة:
namespace GreetingLibrary;
public static class Greeter
{
public static string Greet(string firstName, string lastName)
{
return $"Hello {firstName} {lastName}";
}
}المكتبة لا تعرف شيئا عن النماذج، لذا يمكن لأي واجهة أن تستخدمها. هذا برنامج وحدة تحكم (console) يشير إلى المكتبة نفسها:
using GreetingLibrary;
Console.WriteLine(Greeter.Greet("Lab", "Student"));
Console.WriteLine(Greeter.Greet("Operating", "Systems"));ويطبع:
Hello Lab Student
Hello Operating Systemsويتقلص معالج النموذج إلى استدعاء واحد:
using GreetingLibrary;
namespace BetterWinFormsDemo;
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void sayHelloButton_Click(object sender, EventArgs e)
{
string message = Greeter.Greet(firstNameText.Text, lastNameText.Text);
MessageBox.Show(message);
}
}وتبين الأسطر الأولى تعديلا صغيرا آخر: namespace BetterWinFormsDemo; المنتهي بفاصلة منقوطة هو نطاق على مستوى الملف (file-scoped namespace). ويعمل في نماذج ويندوز أيضا، ويوفر مستوى واحدا من الإزاحة.
المشروع الأفضل
كل ما في هذا الجزء معا: نموذج أعيدت تسميته إلى MainForm وعنوانه Main Form، وخط بحجم 14 نقطة ضبط قبل إضافة أي عنصر تحكم، وأسماء بصيغة camelCase، وترتيب انتقال مفحوص، والمنطق في GreetingLibrary.

يضبط ملف المصمم لهذا النموذج الخط مرة واحدة، على النموذج:
this.AutoScaleDimensions = new System.Drawing.SizeF(11F, 25F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(500, 200);
this.Font = new System.Drawing.Font("Segoe UI", 14F);
this.Name = "MainForm";
this.Text = "Main Form";القيمة 11F, 25F هي حجم الحرف الذي قاسه ويندوز للخط Segoe UI 14 في تشغيلنا. وعندما تطابق القيمة المخزنة القيمة المقيسة يحتفظ النموذج بالحجم الذي صمم به: فقد عمل هذا النموذج بحجمه المصمم 500 في 200. (خزنت محاولتنا الأولى قيمة مخمنة هي 10 في 25، فوسع ويندوز النموذج إلى 550، بنسبة 11/10.)
اختبار سريع
- أي ملف تغيره لكي يفتح نموذج مختلف أولا؟
- نقرت نقرا مزدوجا على زر اسمه
button1، ثم أعدت تسميته إلىsaveButton. ما اسم المعالج؟ - يضغط المستخدم Space على زر عليه التركيز. أيهما يعمل: معالج Click، أم معالج MouseClick، أم كلاهما؟
- يسمي مشروع قديم مربع نص
txtEmail. ما اسمه بأسلوب هذا الدرس؟ - حذفت
button2_Clickمن Form1.cs، والآن لا يفتح المصمم. أي سطر تحذف، ومن أي ملف؟
التدريب
نحو 30 دقيقة. أجب على الورق أولا، ثم ابن البرنامج وشغله لتتحقق. الإجابات في النهاية.
التمرين 1: توقع ترتيب الانتقال (نحو 6 دقائق)
في نموذج تسجيل عناصر التحكم التالية (مع استبعاد التسميات):
| عنصر التحكم | TabIndex | TabStop |
|---|---|---|
| nameText | 3 | True |
| emailText | 7 | True |
| phoneText | 5 | True |
| newsCheck | 1 | False |
| saveButton | 12 | True |
وتظهر على النموذج من الأعلى إلى الأسفل بالترتيب: Name، وEmail، وPhone، ومربع الاختيار الخاص بالأخبار، وSave.
- أي عنصر تحكم يكون فيه المؤشر عند فتح النموذج؟
- اكتب موضع التركيز بعد كل ضغطة من أربع ضغطات على Tab.
- لماذا لا يصل Tab أبدا إلى
newsCheck، مع أن قيمة TabIndex له هي الأصغر؟
التمرين 2: المصمم لا يفتح (نحو 6 دقائق)
نقر طالب نقرا مزدوجا على checkBox1، ثم حذف الدالة الجديدة من Form1.cs. وما زال Form1.Designer.cs يحتوي على:
this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged);- لم يعد المشروع يبنى. ما رمز الخطأ الذي يبلغ عنه المترجم، وما المفقود بحسب الرسالة؟
- اذكر طريقتين لاستعادة مشروع يعمل.
- كيف كان ينبغي أن يحذف الطالب المعالج؟
التمرين 3: شريط بقيمة عظمى أصغر (نحو 6 دقائق)
في نموذج شريط تقدم ProgressBar خاصيتاه Maximum = 50 وValue = 0، وفيه زر:
private void button1_Click(object sender, EventArgs e)
{
progressBar1.Value += 15;
}- ما قيمة Value بعد ثلاث نقرات؟
- ماذا يحدث في النقرة الرابعة؟ اكتب الرقم الذي ستذكره رسالة الخطأ.
التمرين 4: نموذج ترحيب (نحو 12 دقيقة)
ابن مشروعا فيه نموذجان، مجهزا بالطريقة الصحيحة (عناصر تحكم مسماة، وعنوان ذو معنى).
- MainForm، وعنوانه
Main Form: تسميةnameLabelخاصيتها Text هيName، ومربع نصnameText، وزرwelcomeButtonخاصيته Text هيWelcome. - WelcomeForm، وعنوانه
Welcome: تسمية واحدةwelcomeLabelبخط كبير. يأخذ بانيه اسم المستخدم سلسلة نصية ويعرضWelcome,يليه الاسم ثم!.
النقر على Welcome يفتح WelcomeForm بالاسم المأخوذ من nameText.
- اكتب كل ما تحتاج إليه من بانيات أو معالجات.
- ماذا يعرض نموذج الترحيب عندما يكتب المستخدم
Student؟ - ماذا يحدث إذا حاول MainForm تنفيذ
new WelcomeForm()، دون وسيط (argument)؟
الإجابات
اختبار سريع
Program.cs: غير النموذج فيApplication.Run(new MainForm());.- يبقى
button1_Click. فاسم المعالج يأتي من الاسم الذي كان لعنصر التحكم عند النقر المزدوج عليه؛ وإعادة تسمية عنصر التحكم لا تعيد تسمية الدالة. ولهذا تسمي عناصر التحكم أولا. - معالج Click وحده. فالحدث MouseClick لا يطلقه إلا الفأرة؛ أما Space وEnter فيطلقان Click.
emailText: اسم بصيغة camelCase يقول ما هو، دون بادئة للنوع.- سطر الربط
this.button2.Click += new System.EventHandler(this.button2_Click);في Form1.Designer.cs.
التمرين 1
تشغيلنا لهذا النموذج على ويندوز، مع الضغط على Tab أربع مرات:
- يكون المؤشر في
nameTextعند فتح النموذج: فقيمة TabIndex له هي الأصغر (3) بين عناصر التحكم التي تأخذ التركيز بالمفتاح Tab. phoneText(5)، ثمemailText(7)، ثمsaveButton(12)، ثم العودة إلىnameText(3). فالترتيب يتبع TabIndex، لا الموضع على الشاشة: يأتي Phone قبل Email مع أنه أسفل منه على النموذج.- الخاصية
TabStopللعنصرnewsCheckمضبوطة على False، لذا يتجاوزه Tab. ويظل المستخدم قادرا على نقره بالفأرة.
التمرين 2
- أبلغت عملية بناء أجريناها لهذه الحالة بالضبط عن:
Form1.Designer.cs(104,75): error CS1061: 'Form1' does not contain a definition for 'checkBox1_CheckedChanged' and no accessible extension method 'checkBox1_CheckedChanged' accepting a first argument of type 'Form1' could be found (are you missing a using directive or an assembly reference?)الدالة checkBox1_CheckedChanged مفقودة، والسطر 104 من Form1.Designer.cs هو سطر الربط الذي ما زال يشير إليها.
- إما أن تحذف سطر الربط ذلك من Form1.Designer.cs (إذا لم يكن المعالج مطلوبا)، وإما أن تعيد الدالة إلى Form1.cs (إذا كان المعالج مطلوبا):
private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
}- من قائمة الأحداث: اختر
checkBox1في المصمم، وانقر رمز البرق في نافذة الخصائص، واحذف المعالج المقابل للحدث CheckedChanged. فهذا يحذف سطر الربط والدالة معا.
التمرين 3
- 0، ثم 15، ثم 30، ثم 45.
- تحاول النقرة الرابعة ضبط القيمة على 60، وهي أكبر من القيمة العظمى 50. وعرض تشغيلنا نافذة الاستثناء غير المعالج (unhandled exception):

Value of '60' is not valid for 'Value'. 'Value' should be between 'minimum' and 'maximum'. (Parameter 'value')
Actual value was 60.التمرين 4
WelcomeForm.cs:
namespace WinFormsDemo
{
public partial class WelcomeForm : Form
{
public WelcomeForm(string userName)
{
InitializeComponent();
welcomeLabel.Text = $"Welcome, {userName}!";
}
}
}المعالج في MainForm.cs:
private void welcomeButton_Click(object sender, EventArgs e)
{
WelcomeForm frm = new WelcomeForm(nameText.Text);
frm.Show();
}- الشيفرة أعلاه، بنيت وشغلت على ويندوز.
Welcome, Student!:

- لا تترجم الشيفرة. فالباني الوحيد يحتاج إلى سلسلة نصية، وقالت عملية البناء التي أجريناها:
MainForm.cs(12,35): error CS7036: There is no argument given that corresponds to the required parameter 'userName' of 'WelcomeForm.WelcomeForm(string)'