10. [TD]: تنفيذ الطبقة [ui] باستخدام واجهة Swing
الكلمات المفتاحية: بنية متعددة الطبقات، Spring، حقن التبعيات، مكتبة مكونات Swing.
![]() |
10.1. Support
![]() |
في الطبقة [UI]، نريد إنشاء واجهة رسومية Swing. يحتوي Netbeans على أداة [Matisse] لإنشاء واجهات Swing هذه، وهي أفضل مما يقدمه Eclipse. تميل واجهات Swing إلى أن يتم استبدالها بواجهات JavaFx. يستخدم كل من Netbeans و Eclipse نفس الأداة لإنشاء هذه الأخيرة. لذا، إذا قمنا بإنشاء واجهات JavaFx، يمكننا الاحتفاظ بـ Eclipse في جميع مراحل بنية الطبقات.
يمكن لـ Netbeans فتح أي مشروع Maven. لذلك سنستخدم مشروع Maven السابق ونضيف إليه واجهة Swing. في [2]، نقوم بتحميل (File / Open project) مشاريع Maven الخاصة بالطبقات الثلاث التي قمنا بإنشائها باستخدام Eclipse. ثم نقوم بإنشاء ملفاتها الثنائية [3]. تقوم الخيارات [Build] و [Clean and Build] بإنشاء الملف الثنائي للمشروع الذي يتم تطبيقهما عليه. يتم وضع هذه الملفات الثنائية في مجلد [target] [4-5] الخاص بالمشروع:
![]() |
تقوم الخيار [Clean] بحذف هذا المجلد [target]. أما الخيار [Build] فيعيد إنشاؤه. تُظهر التجربة أنه عند مواجهة مشاكل غير متوقعة، فإن أول ما يجب فعله هو تنفيذ [Clean and Build] على المشروع للتأكد من العمل مع أحدث إصدار منه. وهذا ضروري بشكل خاص عند وجود ملفات تكوين لا تؤدي تعديلاتها إلى إعادة تجميع تلقائية عند تنفيذ المشروع. لذلك، يجب فرض إعادة التجميع هذه باستخدام [Clean and Build] حتى يتم تثبيت الإصدارات الجديدة في المجلد [target].
10.2. طريقة عمل التطبيق
لنعد إلى البنية العامة لتطبيق [Elections]:
![]() |
ننتقل الآن إلى تطبيق جديد للطبقة [ui]. التطبيق الوحيد الذي تم تنفيذه حتى الآن هو واجهة وحدة التحكم. سنقوم الآن بإنشاء واجهة رسومية.
سيتوفر للمستخدم الواجهة التالية للتفاعل مع التطبيق [Elections]:
![]() |
![]() |
توجد واجهة المستخدم الرسومية في الطبقة [ui]. وهي التي تتفاعل مع المستخدم.
- عند بدء التشغيل، يقوم تطبيق وحدة التحكم [main] بإنشاء مثيلات للطبقات الثلاث للتطبيق باستخدام Spring. ويتم ذلك قبل أن تصبح الواجهة الرسومية مرئية. وفي هذه المرحلة من التهيئة أيضًا، تُطلب المعلومات التي تميز الانتخابات (عدد المقاعد الشاغرة، والحد الأدنى للأصوات، والقوائم المتنافسة) من الطبقة [dao]. إذا فشلت مرحلة التهيئة هذه (على سبيل المثال، تعذر الوصول إلى البيانات)، يتم عرض رسالة خطأ على وحدة التحكم ولا يتم عرض الواجهة الرسومية.
- إذا سارت عملية قراءة البيانات على ما يرام، يتم عرض الواجهة الرسومية مع المعلومات التالية (انظر لقطة الشاشة أعلاه):
- عدد المقاعد الشاغرة في (2)
- الحد الأدنى للأصوات في (3)
- معرفات وأسماء القوائم المرشحة في (4)
- ثم يقوم المستخدم بتخصيص عدد الأصوات لكل قائمة مرشحة باستخدام الحقول 4 (المعرف - الاسم)، 5 (الأصوات)، 6 (لإضافة).

- يمكن بعد ذلك استخدام الرابط (10) لحساب المقاعد:

- يتيح الرابط [Enregistrer] (12) تسجيل النتائج في مصدر البيانات.
10.3. فئة [ElectionsSwing] لتنفيذ الطبقة [ui]
10.3.1. مشروع Netbeans
ملاحظة: توضح الفقرة 22.4 كيفية الحصول على Netbeans.
سيكون شكل مشروع Netbeans النهائي للتطبيق كما يلي: [1]. قم بإنشائه باتباع الخطوات التالية: [2-5]:
![]() |
![]() |
تأكد من أن المشروع مهيأ ليتم تجميعه بواسطة JDK 1.8 [1-6]:
![]() |
10.3.2. تكوين Maven
سيعتمد المشروع الجديد [elections-swing-metier-dao-jdbc] على المشروع السابق [elections-console-metier-dao-jdbc]. ولذلك، نضيف تبعية Maven بالطريقة التالية [1-3]:
![]() |
![]() |
10.3.3. بناء واجهة المستخدم الرسومية
لإنشاء الواجهة الرسومية، يمكننا اتباع الخطوات التالية:
![]() |
![]() |
- [1]: إضافة كائن إلى الحزمة [elections.ui.service]
- [2]: اختيار الخيار [JFrame Form] في الفئة [Swing GUI Forms]
![]() |
- [4]: تسمية الفئة
- [5]: حزمة الفئة.
- إنهاء المعالج.
- [6]: الفئة التي تم إنشاؤها
![]() |
- [7]: الفئة [AbstractElectionsSwing] في وضع [Design]
- [8]: علامة التبويب [Navigator] التي تعرض شجرة [9] لمكونات النافذة
- [10]: علامة التبويب [Properties] التي تعرض خصائص المكون [jFrame] المحدد في [9]
![]() |
- [11]: [JFrame] عبارة عن حاوية للمكونات. يمكن ترتيب هذه المكونات داخل الحاوية وفقًا لقواعد تحديد المواقع المختلفة المسماة layouts. هنا، نختار layout [Free Design] [14] الذي يسمح بوضع المكونات بحرية داخل الحاوية.
نجد المكونات في شريط الأدوات المسمى Palette:
![]() |
- [1]: لوحة
- [2]: مكون JLabel يتم وضعه في حاوية المكونات
- بالنقر بزر الماوس الأيمن عليه، يمكن الوصول إلى خصائص متنوعة: اسمه [4]، نصه [3] أو حتى مديري الأحداث [5]. نستخدم [3] لتعيين النص [6].
![]() |
- [1]: تتيح علامة التبويب [Properties] للمكون [JLabel] الوصول إلى خصائصه: موضعه الأفقي [2]، والرأسي [3]، وخط النص [4]، والنص [5].
عندما يتم وضع مكون وتهيئته على واجهة المستخدم الرسومية وحفظ (Ctrl-S) العمل المنجز، يتم إنشاء كود في الفئة [AbstractElectionsSwing]:
![]() |
![]() |
لا يجب تعديل هذا الكود المظلل باللون الرمادي لأنه يتم حذفه وإعادة إنشاؤه عند الحفظ التالي. وبالتالي، ستفقد التعديلات التي تم إجراؤها.
يمكن العثور على دليل تعليمي لإنشاء واجهة رسومية باستخدام Netbeans في URL [https://netbeans.org/kb/docs/java/quickstart-gui.html?print=yes#design] (نوفمبر 2015).
نقوم الآن بإنشاء الواجهة التالية:
![]() |
مكونات الواجهة هي كما يلي:
رقم | النوع | الاسم | الدور |
JMenuBar | jMenuBar1 | قائمة | |
JLabel | jLabelSAP | عدد المقاعد الشاغرة | |
JLabel | jLabelSE | الحد الأدنى للأصوات | |
JComboBox | jComboBoxNomsListes | قائمة بأسماء القوائم المتنافسة | |
JTextField | jTextFieldVoixListe | عدد الأصوات في قائمة | |
JLabel | jLabelAjouter | لإضافة قائمة إلى (8) | |
(JScrollPane، JList) | jListNomsVoix | أسماء وأصوات القوائم | |
JLabel | jLabelSupprimer | لحذف من (8) القائمة المحددة في (8) | |
JLabel | jLabelCalculer | لحساب نتائج الانتخابات | |
JLabel | jLabelEffacer | لمسح نتائج الانتخابات | |
JLabel | jLabelEnregistrer | لتسجيل نتائج الانتخابات | |
(JScrollPane، JList) | jListResultats | لعرض نتائج الانتخابات | |
(JScrollPane، JTextPane) | jTextPaneMessages | لعرض رسائل المتابعة |
التعليق التوضيحي (JScrollPane، JList) [13-14] موجود للإشارة إلى أنه عند وضع مكون [JList] في النافذة، يتم إدراجه تلقائيًا في مكون [JScrollPane] الذي يسمح بالتمرير عبر القائمة. المكون [JScrollPane] هو الذي يسمح برؤية جميع عناصر القائمة في حين أن عددًا محدودًا منها فقط يكون مرئيًا في وقت معين. وينطبق الأمر نفسه على المكون [JTextPane] و [15-16].
يمكن إنشاء القائمة بالطريقة التالية:
![]() |
- [1, 2]: يتم وضع مكون [Menu Bar] في النافذة
- [3]: القائمة التي تم إنشاؤها افتراضيًا كما هو موضح في علامة التبويب [Navigator]
- [4,5,6]: بالنقر بزر الماوس الأيمن على خيار من القائمة، يمكن:
- تغيير نصها [4]، واسمها [5]
- إدارة أحداثها [6]
- [7]: القائمة المطلوبة
القائمة المطلوبة هي التالية:
المستوى 1 | المستوى 2 |
الانتخابات | |
Quitter | |
القوائم | |
Ajouter | |
Supprimer | |
النتائج | |
Calculer | |
Effacer | |
Enregistrer | |
حول |
يمكن اختبار الواجهة الرسومية في أي وقت:
![]() |
أثناء إنشاء الواجهة، يجب ربط مدير الأحداث ببعض العلامات والقوائم [Ajouter, Effacer, ...]. وإليك كيفية القيام بذلك:
![]() |
- [1]: انقر بزر الماوس الأيمن على المكون الذي تريد إدارة حدث له
- [2]: اختر الخيار [Events]
- [3]: اختر فئة الأحداث
- [4]: اختر الحدث الذي تريد إدارته
رمز Java الذي تم إنشاؤه بواسطة هذه العملية هو التالي:
jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
jLabelCalculerMouseClicked(evt);
}
});
...
private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
// TODO أضف رمز المعالجة الخاص بك هنا:
}
- الأسطر 1-5: تمت إضافة مدير أحداث إلى المكون jLabelCalculer. تتوقع الطريقة addMouseListener كمعلمة فئة تنفذ الواجهة MouseListener التالية:
![]() |
يتم تنفيذ واجهة MouseListener بواسطة فئات مختلفة، بما في ذلك الفئة MouseAdapter. تنفذ هذه الفئة الطرق الخمس لواجهة MouseListener، لكن هذه الطرق لا تقوم بأي شيء. لذلك، يجب اشتقاق هذه الفئة لتنفيذ الطريقة أو الطرق التي تختارها. وهذا ما تم في الكود أعلاه والموضح أدناه:
jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
jLabelCalculerMouseClicked(evt);
}
});
يستخدم الكود أعلاه تقنية الفئة المجهولة الموضحة في الفقرة 2.5 من الدورة التدريبية [ref1].
في السطر 1، المعلمة الخاصة بالطريقة addMouseListener هي فئة مجهولة، يتم تعريفها على الفور. وهي مثيل لفئة مشتقة من الفئة MouseAdapter (السطر 1) التي يتم إعادة تعريف طريقة mouseClicked (الأسطر 2-4) فيها بحيث تقوم بعمل ما.
يتم تعريف الأسلوب jLabelCalculerMouseClicked المسمى «السطر 3» على النحو التالي:
private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
// TODO أضف كود المعالجة الخاص بك هنا:
}
يقوم المطور بإدارة الحدث "MouseClicked" عن طريق وضع كود في هذه الطريقة.
يتم إنشاء جميع معالجات الأحداث بواسطة Netbeans بهذه الطريقة. يمكن للمطور تجاهل أسطر الكود التي تم إنشاؤها بواسطة Netbeans لربط طريقة بحدث أحد المكونات. يمكنه الاكتفاء بوضع كوده في السطر 2 أعلاه. فيما يلي مثال:
private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
System.out.println("Mouse Clicked");
}
إذا قمنا بتشغيل واجهة المستخدم الرسومية ونقرنا على الرابط [Calculer]، فسنحصل على رسالة على وحدة التحكم:
![]() |
- [1]: نضغط مرتين على التسمية [Calculer]
- [2]: تم تشغيل معالج هذا الحدث وأنتج الرسائل mouseClicked في وحدة التحكم في Netbeans.
يتم الإعلان عن مكونات [jComboBoxNomsListes, jListNomsVoix, jListResultats] بالطريقة التالية:
protected javax.swing.JComboBox jComboBoxNomsListes;
protected javax.swing.JList jListNomsVoix;
protected javax.swing.JList jListResultats;
هذه المكونات عبارة عن قوائم يتم تعيين معلماتها عادةً بواسطة نوع T: نوع عناصر النموذج المعروض بواسطة المكونات. يمكن أن يكون هذا النوع T أي نوع. القيمة المعروضة في مكون القائمة هي من النوع [String]. بشكل افتراضي، يتم استخدام الطريقة [T.toString()] للعرض. من أجل التحكم بشكل أفضل في ما سيتم عرضه، سيكون النوع T هنا هو النوع String. وبالتالي، فإن الإعلان الصحيح لقوائمنا هو كما يلي:
protected javax.swing.JComboBox<String> jComboBoxNomsListes;
protected javax.swing.JList<String> jListNomsVoix;
protected javax.swing.JList<String> jListResultats;
نحصل على هذه النتيجة عن طريق تعديل إحدى خصائص المكون:
![]() |
10.3.4. فصل الكود
لنعد إلى بنية تطبيقنا:
![]() |
يجب أن تنفذ الفئة [AbstractElectionsSwing] الطبقة [ui] المذكورة أعلاه. لا يحتوي الكود الذي تم إنشاؤه بواسطة Netbeans في الوقت الحالي سوى على كود لإدارة النافذة ومديري الأحداث الذين لا يقومون بأي شيء في الوقت الحالي. فيما سبق، نرى أن الفئة [AbstractElectionsSwing] سيتعين عليها إدارة التبادلات مع الطبقة [métier]. وستتم هذه الإدارة في معالجات الأحداث. لتوضيح بنية الكود، قررنا وضعه في فئتين:
- [AbstractElectionsSwing] التي ستبقى كما تم إنشاؤها بواسطة Netbeans مع بعض التفاصيل البسيطة. لن تدير هذه الفئة أي حدث بنفسها. ستكون معالجات الأحداث فارغة ومُعلنة على أنها مجردة. سيتم تنفيذها بواسطة فئة مشتقة من [AbstractElectionsSwing].
- [ElectionsSwing]، وهي فئة مشتقة من [AbstractElectionsSwing] ستقوم بتنفيذ جميع معالجات الأحداث.
هذا النوع من الفصل ليس غير معتاد. يمكن العثور عليه على سبيل المثال في صفحات الويب ASP.NET (الإصدار غير MVC). يتطور مشروع Netbeans على النحو التالي:
![]() |
أما كود فئة [AbstractElectionsSwing]، فيتطور على النحو التالي:
public abstract class AbstractElectionsSwing {
....
private void jMenuItemCalculerActionPerformed(java.awt.event.ActionEvent evt) {
doCalculer();
}
...
private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
if (jLabelCalculer.isEnabled()) {
doCalculer();
}
}
....
// مديرو الأحداث
abstract protected void doSupprimer();
abstract protected void doCalculer();
abstract protected void doQuitter();
abstract protected void doEffacer();
abstract protected void doEnregistrer();
abstract protected void doAjouter();
abstract protected void doInformer();
abstract protected void doMajLabelAjouter();
abstract protected void doMajLabelSupprimer();
...
}
- السطر 1: تم إعلان الفئة على أنها مجردة
- الأسطر 3-5: إدارة النقر على خيار القائمة [jMenuItemCalculer]. نلاحظ أن معالجة الحدث يتم تحويلها إلى الطريقة doCalculer في السطر 19. هذه الطريقة غير مطبقة ومُعلنة على أنها مجردة. وستقوم الفئة المشتقة [ElectionsSwing] بتنفيذها؛
- الأسطر 9-13: معالج الحدث clic على التسمية [jLabelCalculer]. يؤدي النقر دائمًا إلى حدوث حدث، سواء كان المكون [jLabel] نشطًا (enabled=true) أو غير نشط (enabled=false). نتأكد هنا من أنه نشط بالفعل لمعالجة الحدث؛
- السطر 15 وما بعده: يتم تطبيق هذه التقنية لتفويض معالجة الأحداث إلى طريقة مجردة على جميع معالجات الأحداث.
تقوم الفئة [ElectionsSwing] المشتقة من [AbstractElectionsSwing] بتنفيذ جميع معالجات الأحداث التي لم يتم تنفيذها بواسطة [AbstractElectionsSwing]:
package elections.ui.service;;
...
public class ElectionsSwing extends AbstractElectionsSwing {
// مديرو الأحداث
@Override
protected void doInformer() {
...
}
@Override
protected void doAjouter() {
...
}
@Override
protected void doCalculer() {
...
}
@Override
protected void doEffacer() {
...
}
@Override
protected void doEnregistrer() {
...
}
@Override
protected void doQuitter() {
System.exit(0);
}
@Override
protected void doSupprimer() {
...
}
@Override
protected void doMajLabelAjouter() {
...
}
@Override
protected void doMajLabelSupprimer() {
...
}
}
- السطر 3: [ElectionsSwing] مشتقة من [AbstractElectionsSwing]
- الأسطر 7-50: معالجات أحداث النافذة الرسومية
ستقوم أساليب الفئة المشتقة [ElectionsSwing] بمعالجة مكونات الفئة الأصلية [AbstractElectionsSwing]. حاليًا، هذه المكونات لها نطاق private، مما يمنع الفئة الفرعية [ElectionsSwing] من الوصول إليها:
private JMenuItem jMenuItemAPropos = null;
private JLabel jLabelAjouter = null;
لحل هذه المشكلة، سنحرص على أن يكون نطاق مكونات واجهة المستخدم الرسومية هو [protected]:
![]() |
- وضع السمة [protected] في [3]؛
10.3.5. تنفيذ الواجهة [IElectionsUI]
لنعد إلى بنية تطبيقنا:
![]() |
فيما سبق، يجب أن تقدم الطبقة [ui] الواجهة [IElectionsUI] إلى الكائن [main]:
package elections.ui.service;
public interface IElectionsUI {
/**
* lance le dialogue avec l'utilisateur
*/
public void run();
}
تم تعريف هذه الواجهة في المشروع [elections-console-metier-dao-jdbc] ووصفها في الفقرة 9.4. ونظرًا لأن هذا المشروع تابع للمشروع [swing]، فإن هذه الواجهة معروفة.
نظرًا لأن الفئة [AbstractElectionsSwing] أصبحت مجردة، لم يعد بإمكان Spring إنشاء مثيل لها. يجب الآن إنشاء مثيل للفئة [ElectionsSwing]. يجب أن تنفذ الفئة [ElectionsSwing] الواجهة [IElectionsUI]. وبالتالي، يتطور كودها على النحو التالي:
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
// طريقة التشغيل لواجهة [ElectionsUI]
public void run() {
...
}
- السطر 1: الفئة [ElectionsSwing] تنفذ الواجهة [IElectionsUI]
- الأسطر 4-6: الطريقة [run] لهذه الواجهة
ما الذي يجب أن تفعله الطريقة run؟ عرض النافذة الرسومية. كيف نفعل ذلك؟ يمكننا أن نسترشد بالطريقة [main] التي أنشأتها Netbeans في الفئة [AbstractElectionsSwing] والتي تقوم بما هو مطلوب:
public static void main(String args[]) {
/* تعيين شكل ومظهر Nimbus */
//<editor-fold defaultstate="collapsed" desc=" رمز إعداد الشكل والمظهر (اختياري) ">
/* إذا لم يكن Nimbus (المقدم في Java SE 6) متاحًا، فاستمر في استخدام الشكل والمظهر الافتراضي.
* For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
/* إنشاء النموذج وعرضه */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new NewJFrame().setVisible(true);
}
});
}
المنشئ [AbstractElectionsSwing] المستخدم في السطر 28 هو التالي:
public AbstractElectionsSwing() {
initComponents();
}
- السطر 2: الطريقة [initComponents] هي طريقة خاصة تم إنشاؤها بواسطة منشئ واجهة المستخدم الرسومية. لا يمكن تغيير كودها.
قد تكون الطريقة [run] للفئة [ElectionsSwing] كما يلي:
@Override
public void run() {
// عرض واجهة المستخدم الرسومية
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
- السطر 6: يتم تهيئة واجهة المستخدم الرسومية باستخدام طريقة [init]. هنا، نرغب في استدعاء الطريقة [initComponents] من الفئة الأصلية، ولكن هذه الطريقة خاصة. لذلك نضيف في الفئة الأصلية [AbstractElectionsSwing] الطريقة التالية [init]:
protected void init(){
initComponents();
}
- (تابع)
- نظرًا لوجودها في الفئة [AbstractElectionsSwing]، فإن الطريقة [init] يمكنها الوصول إلى الطريقة الخاصة [initComponents] في نفس الفئة؛
- نظرًا لأنها تحتوي على السمة [protected]، فهي مرئية في الفئة الفرعية [ElectionsSwing]؛
- السطر 7: يتم إظهار واجهة المستخدم الرسومية؛
ملاحظة: عندما تمت كتابة الطريقة [run] في الفئة [ElectionsSwing]، يمكن حذف الطريقة [main] من الفئة المجردة [AbstractElectionsSwing].
10.3.6. الفئة القابلة للتنفيذ
لنعد إلى بنية تطبيقنا:
![]() |
نود أن يقوم Spring بإنشاء مثيل للطبقة [ui] كما تم فعله عندما تم تنفيذها بواسطة تطبيق console. للقيام بذلك، يجب أن تحتوي فئة التنفيذ [ElectionsSwing] على مرجع إلى الطبقة [métier]:
@Component
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI{
// مرجع على الطبقة [métier]
@Autowired
private IElectionsMetier metier;
...
- السطر 1: فئة [ElectionsSwing] هي مكون Spring؛
- السطران 5-6: حقن مرجع على الطبقة [métier] بواسطة Spring؛
يتم تشغيل واجهة المستخدم الرسومية عن طريق تنفيذ الفئة التالية [BootElectionsSwing]:
![]() |
package elections.ui.boot;
import elections.ui.service.IElectionsUI;
public class BootElectionsSwing extends AbstractBootElections {
public static void main(String[] arguments) {
new BootElectionsSwing().run();
}
@Override
protected IElectionsUI getUI() {
return ctx.getBean("electionsSwing", IElectionsUI.class);
}
}
لقد شرحنا كودًا مشابهًا في الفقرة 9.5، عندما شرحنا كود الفئتين [AbstractBootElections] و [BootElectionsConsole]. في السطر 12، نسترد الفاصوليا المسماة [electionsSwing] والتي تتوافق مع اسم Spring القياسي للفئة [ElectionsSwing].
10.3.7. تهيئة واجهة المستخدم الرسومية
عند عرض واجهة المستخدم الرسومية، تم تهيئة بعض مكوناتها:

نرى أعلاه:
- أن القائمة المنسدلة قد تم ملؤها بأسماء القوائم؛
- أن عدد المقاعد الشاغرة والحد الأدنى للأصوات المطلوب قد تم تحديدهما؛
- أن بعض الروابط قد تم تعطيلها؛
- أن رسالة نجاح معروضة في أسفل النافذة؛
متى ستتم عمليات التهيئة هذه؟ لا يمكن إجراؤها إلا بعد تهيئة الحقل [electionsMetier] من الفئة [ElectionsSwing]. في الواقع، سيتم طلب أسماء القوائم من الطبقة [metier]. سيتم تهيئة هذا الحقل بواسطة Spring بالترتيب التالي:
- استخدام منشئ الفئة [ElectionsSwing] بدون معلمات؛
- حقن التبعيات، وهنا مرجع الطبقة [métier]؛
- تنفيذ الطريقة [run] للفئة [ElectionsSwing]:
@Override
public void run() {
// يتم عرض واجهة المستخدم الرسومية
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
- السطر 12، ذكرنا أننا نستدعي الطريقة [init] من الفئة الأصلية التي ستقوم برسم مكونات واجهة المستخدم الرسومية. سنقوم بإعادة تعريف هذه الطريقة، محليًا، في الفئة [ElectionsSwing]. في هذه الطريقة سنقوم بتهيئة مكونات النافذة (القائمة المنسدلة، التسميات) بالبيانات هذه المرة:
قد يكون للطريقة المحلية [init] الهيكل التالي:
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
...
// مرجع على الطبقة [métier]
@Autowired
private IElectionsMetier metier;
// عمليات التهيئة
public void init() {
// إنشاء المكونات بواسطة الفئة الأم
super.init();
// يتم طلب القوائم من الطبقة [metier]
...
// يتم ربط أسماء القوائم بقائمة الاختيار jComboBoxNomsListes
...
// وكذلك معلمات الانتخابات
...
// يتم تهيئة التسميات المرتبطة بهاتين المعلومتين
...
// رسالة نجاح
...
// تهيئة حالة بعض مكونات النموذج
...
}
يجب ملاحظة استدعاء الطريقة [init] من الفئة الأم في السطر 11.
10.3.8. الفئة [Utilitaires]
تم تجميع عدد من الطرق المساعدة الثابتة في الفئة [Utilitaires]:
![]() |
الفئة [Utilitaires] هي كما يلي:
package istia.st.elections.ui;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
//فئة الأداة المساعدة
class Utilitaires {
// إدارة حالة مصفوفة التسميات
public static void setEnabled(JLabel[] labels, boolean value) {
for (int i = 0; i < labels.length; i++) {
labels[i].setEnabled(value);
}
}
// إدارة حالة مصفوفة خيارات القائمة
public static void setEnabled(JMenuItem[] menuItems, boolean value) {
...
}
}
- السطر 9: تحدد الطريقة setEnabled حالة مكونات JLabel المحددة في مصفوفة. تسمح الطريقة setEnabled لمكون JLabel بتنشيط أو تعطيل JLabel.
المهمة المطلوبة: باتباع مثال الطريقة setEnabled في السطر 9,، اكتب الطريقة setEnabled في السطر 16 التي تقوم بنفس الشيء مع مكونات JMenuItem.
10.3.9. كود فئة [ElectionsSwing]
لنتذكر الهيكل العام لفئة [ElectionsSwing]:
package istia.st.elections.ui;
...
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
// مرجع على الطبقة [métier]
@Autowired
private IElectionsMetier metier;
// عمليات التهيئة
public void init() {
...
}
// معالجات الأحداث
@Override
protected void doInformer() {
...
}
@Override
protected void doAjouter() {
...
}
@Override
protected void doCalculer() {
...
}
@Override
protected void doEffacer() {
...
}
@Override
protected void doEnregistrer() {
...
}
@Override
protected void doQuitter() {
System.exit(0);
}
@Override
protected void doSupprimer() {
...
}
@Override
protected void doMajLabelAjouter() {
...
}
@Override
protected void doMajLabelSupprimer() {
...
}
}
سنقوم بدراسة أساليب الفئة واحدة تلو الأخرى.
10.3.9.1. طريقة [init]
لنعد إلى الواجهة الرسومية:
![]() |
تهدف الطريقة [init] إلى:
- ملء الحقل المركب [4] بمعرفات وأسماء القوائم بالصيغة [id - nom]
- عرض رسالة نجاح في [15]
- تهيئة العلامات [2] و [3]
- تعطيل بعض الروابط
قد يكون الهيكل الأساسي للطريقة [init] كما يلي:
@Override
protected void init() {
// إنشاء المكونات بواسطة الفئة الأصلية
super.init();
// التهيئات المحلية
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
String info;
try {
// يتم طلب القوائم من الطبقة [métier]
listes = ...
// ربط أسماء القوائم بالمنسدل jComboBoxNomsListes
...
// وكذلك معلمات الانتخابات
int nbSiegesAPourvoir = ...
double seuilElectoral = ...
// يتم تهيئة التسميات المرتبطة بهاتين المعلومتين
...
// رسالة نجاح
info = "Source de données lue avec succès";
} catch (ElectionsException ex1) {
// يتم تسجيل الخطأ
info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
} catch (RuntimeException ex2) {
// يتم تسجيل الخطأ
info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
}
// يتم عرض المعلومات
jTextPaneMessages.setText(info);
jTextPaneMessages.setCaretPosition(0);
// حالة النموذج
Utilitaires.setEnabled(new JLabel[] { jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer }, false);
Utilitaires.setEnabled(
new JMenuItem[] { jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer }, false);
// توسيط النافذة
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Dimension frameSize = getSize();
if (frameSize.height > screenSize.height) {
frameSize.height = screenSize.height;
}
if (frameSize.width > screenSize.width) {
frameSize.width = screenSize.width;
}
setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
}
private String getInfoForException(String message, ElectionsException ex) {
// عرض الرسالة
StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
info.append(String.format("Code erreur : %d\n", ex.getCode()));
// يتم عرض الأخطاء
for (String erreur : ex.getErreurs()) {
info.append(String.format("-- %s\n", erreur));
}
return info.toString();
}
private String getInfoForException(String message, RuntimeException ex) {
// عرض الرسالة
StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
// عرض مكدس الاستثناءات
Throwable cause = ex;
while (cause != null) {
info.append(String.format("-- %s\n", cause.getMessage()));
cause = cause.getCause();
}
return info.toString();
}
المهمة المطلوبة: أكمل كود الطريقة [init].
للقراءة في الدورة التدريبية: المكونات JTextField، JLabel
معلومات مفيدة:
يعرض مكون JList البيانات الموجودة في نموذج. بشكل افتراضي، يكون هذا النموذج من النوع DefaultListModel (السطران 2 و3). يعمل كائن DefaultListModel بشكل مشابه لنوع ArrayList:
- لإضافة كائن o إلى النموذج:
[DefaultListModel].addElement(Object o);
في هذا التطبيق، سيكون الكائن o دائمًا من النوع String.
- لإزالة العنصر رقم i من النموذج:
[DefaultListModel].remove(int i);
- للحصول على العنصر رقم i من النموذج:
[DefaultListModel].elementAt(int i);
لإضافة عنصر إلى المجموعة [jComboBoxNomsListes]، سنستخدم الطريقة [addItem]:
jComboBoxNomsListes.addItem(chaîne de caractères)
يحتوي المكون JTextPane على الطريقتين getText() و setText() لقراءة/كتابة النص المعروض.
10.3.9.2. إدارة حالة الرابط [Ajouter]
لا يكون الرابط [Ajouter] [6] نشطًا إلا عندما يكون الحقل [5] الخاص بالأصوات غير فارغ. في الفئة [AbstractElectionsSwing]، يكون المدير الذي يتتبع تحركات المؤشر في الحقل [5] هو التالي:
private void jTextFieldVoixListeCaretUpdate(javax.swing.event.CaretEvent evt) {
doMajLabelAjouter()
}
تستدعي السطر 2 الطريقة [doMajLabelAjouter] من الفئة [ElectionsSwing].
protected void doMajLabelAjouter() {
// تحديد حالة التسمية [jLabelAjouter]
...
// يتم تعيين حالة القائمة [jMenuItemAjouter]
...
}
المهمة المطلوبة: أكمل كود الأسلوب [doMajLabelAjouter].
10.3.9.3. تخصيص الأصوات لكل قائمة
بالنسبة لكل قائمة مرشحة في (4)، يتم اتباع الخطوات التالية:
- اختيار قائمة من (4)
- إدخال عدد الأصوات في (5)
- التأكيد بالنقر على الرابط [Ajouter]
يتم الإبلاغ عن أخطاء الإدخال كما هو موضح في المثال التالي:

إذا كان عدد الأصوات صحيحًا، تتم إضافة القائمة إلى المكون (8)، ويتم مسح عدد الأصوات وإغلاق الرابط [Ajouter]:
![]() | ![]() |
في الفئة [AbstractElectionsSwing]، يكون المدير الذي يدير النقر على الرابط [Ajouter] هو التالي:
private void jLabelAjouterMouseClicked(java.awt.event.MouseEvent evt) {
if (jLabelAjouter.isEnabled()) {
doAjouter();
}
}
تستدعي السطر 3 الطريقة [doAjouter] من الفئة [ElectionsSwing]:
// نماذج القوائم JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// القوائم المتنافسة
private ListeElectorale[] listes;
// القوائم التي أدخلها المستخدم
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
...
@Override
protected void doAjouter() {
// هل عدد الأصوات صحيح؟
...
// إذا كان هناك خطأ، يتم الإبلاغ عنه
if (erreur) {
JOptionPane.showMessageDialog(null, "Nombre de voix incorrect", "Elections : erreur",
JOptionPane.INFORMATION_MESSAGE);
jTextFieldVoixListe.requestFocus();
// العودة إلى واجهة المستخدم الرسومية
return;
}
// لا يوجد خطأ - يتم تسجيل القائمة
listesSaisies.add(...);
modèleNomsVoix.addElement(...);
// نقوم بتنظيف عدد الأصوات
jTextFieldVoixListe.setText("");
// حالة النموذج (القوائم، التسميات)
...
}
- السطر 25: في كل مرة يضيف فيها المستخدم أصواتًا إلى قائمة ويؤكد اختياره، يتم وضع هذه القائمة في الحقل [listesSaisies] في السطر 9. سيتم تسجيل القائمة هناك مع المعلومات [id, version, nom, voix]. تأتي المعلومات الثلاثة الأولى من القوائم المسجلة مسبقًا في الجدول في السطر 6. تسمح طريقة [getSelectedIndex] الخاصة بقائمة الاختيار بمعرفة مؤشر القائمة المحددة؛
المهمة المطلوبة: أكمل كود الطريقة [doAjouter].
10.3.9.4. إدارة حالة الرابط [Supprimer]
لا يكون الرابط [Supprimer] [9] نشطًا إلا عند تحديد عنصر في [8].
في الفئة [AbstractElectionsSwing]، يكون المدير الذي يستجيب للنقر على عنصر في القائمة [8] هو التالي:
private void jListNomsVoixValueChanged(javax.swing.event.ListSelectionEvent evt) {
doMajLabelSupprimer();
}
يستدعي السطر 2 الطريقة [doMajLabelSupprimer] من الفئة [ElectionsSwing].
@Override
protected void doMajLabelSupprimer() {
// تشغيل التسمية [jLabelSupprimer] وخيار القائمة المقابل
...
}
المهمة المطلوبة: أكمل كود الأسلوب [doMajLabelSupprimer].
10.3.9.5. حذف قائمة مرشحة
يتيح الرابط [Supprimer] [9] حذف الزوج (الاسم، الصوت) المحدد في (8). بمجرد إتمام الحذف، يتم إيقاف الرابط [Supprimer]. ولن يتم إعادة تشغيله إلا عند تحديد قائمة جديدة في (8).
![]() | ![]() |
في الفئة [AbstractElectionsSwing]، المدير الذي يستجيب للنقر على الرابط [Supprimer] هو التالي:
private void jLabelSupprimerMouseClicked(java.awt.event.MouseEvent evt) {
if(jLabelSupprimer.isEnabled()){
doSupprimer();
}
}
تستدعي السطر 3 الطريقة [doSupprimer] من الفئة [ElectionsSwing].
@Override
protected void doSupprimer() {
// حذف القائمة المحددة، والقالب modèleNomsVoix والقوائم التي تم إدخالها
...
// تحديث حالة العلامات وخيارات قائمة النموذج
Utilitaires.setEnabled(...);
...
}
المهمة المطلوبة: أكمل كود الأسلوب [doSupprimer].
10.3.9.6. إدارة حالة الرابط [Calculer]
الرابط [Calculer] [10] لا يكون نشطًا إلا عندما يوجد عنصر واحد على الأقل في [8].
المهمة المطلوبة: أضف الكود اللازم لإدارة هذا الرابط في الطرق [doAjouter] و [doSupprimer] المكتوبة سابقًا. سيتم أيضًا إدارة خيار القائمة المقابل.
ملاحظة: يتم الحصول على عدد عناصر عنصر من النوع DefaultListModel باستخدام الطريقة size().
10.3.9.7. حساب المقاعد
يتيح الرابط [Calculer] [10] بدء حساب المقاعد وعرض النتائج في (14). في حالة الفشل (تم استبعاد جميع القوائم)، يتم عرض رسالة خطأ في [15]. في جميع الأحوال، بعد الحساب، يتم تعطيل الرابط [Calculer] [10].
في الفئة [AbstractElectionsSwing]، يكون المدير الذي يستجيب للنقر على الرابط [Calculer] هو التالي:
private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
if(jLabelCalculer.isEnabled()){
doCalculer();
}
}
السطر 3 يستدعي الطريقة [doCalculer] من الفئة [ElectionsSwing].
// القوائم التي أدخلها المستخدم
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
...
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// حساب المقاعد
try {
...
} catch (ElectionsException ex) {
// يتم عرض الاستثناء
...
return;
}
// عرض النتائج
...
// تحديث حالة النموذج
Utilitaires.setEnabled(...);
}
المهمة المطلوبة: أكمل كود الأسلوب [doCalculer].
10.3.9.8. تسجيل النتائج في مصدر البيانات
يتيح الرابط [Enregistrer] (12) تسجيل نتائج حساب المقاعد في مصدر البيانات. بمجرد إتمام التسجيل بنجاح، يتم تعطيل الرابط [Enregistrer]. في حالة الفشل، يتم عرض رسالة خطأ في [15]. في جميع الأحوال، يتم بعد ذلك تعطيل الرابط [Enregistrer].
في الفئة [AbstractElectionsSwing]، يكون المدير الذي يدير النقر على التسمية [Enregistrer] هو التالي:
private void jLabelEnregistrerMouseClicked(java.awt.event.MouseEvent evt) {
if(jLabelEnregistrer.isEnabled()){
doEnregistrer();
}
}
تستدعي السطر 3 الطريقة [doEnregistrer] من الفئة [ElectionsSwing]:
@Override
protected void doEnregistrer() {
// طلب التسجيل في طبقة الأعمال
try {
...
} catch (ElectionsException ex) {
// عرض الاستثناء
...
// العودة إلى واجهة المستخدم الرسومية
return;
}
// تحديث النموذج
Utilitaires.setEnabled(...);
...
}
المهمة المطلوبة: أكمل كود الطريقة [doEnregistrer].
10.3.9.9. مسح النتائج
يتيح الرابط [Effacer] (11) مسح النتائج المعروضة في (14).
في الفئة [AbstractElectionsSwing]، المدير الذي يدير النقر على التسمية [Effacer] هو التالي:
private void jLabelEffacerMouseClicked(java.awt.event.MouseEvent evt) {
if(jLabelEffacer.isEnabled()){
doEffacer();
}
}
تستدعي السطر 3 الطريقة [doEffacer] من الفئة [ElectionsSwing]:
@Override
protected void doEffacer() {
// إفراغ قائمة النتائج
....
// تحديث النموذج
Utilitaires.setEnabled(...);
}
المهمة المطلوبة: أكمل كود الطريقة [doEffacer].
ملاحظة: تحتوي الفئة DefaultListModel على طريقة clear() التي تحذف جميع عناصرها.
10.3.10. التحسينات
يمكن تحسين واجهة المستخدم الرسومية السابقة بعدة طرق: فقد ينسى المستخدم إدخال الأصوات من جميع القوائم الموجودة في القائمة المنسدلة، كما قد يدخل عن طريق الخطأ الأصوات من نفس القائمة عدة مرات.
المهمة المطلوبة: قم بتحسين الخوارزمية بحيث لا يمكن أن يحدث هذان الحالتان. أحد الحلول البسيطة هو إدارة قاموس بالقوائم التي تم إدخالها، بحيث تكون مفاتيح القاموس هي عناصر القائمة المنسدلة. كما سنحرص على ألا يتم تنشيط الرابط [Calculer] إلا بعد إدخال جميع القوائم.
اقرأ في الدورة التدريبية [ref1]: الفصل HashTable في الفقرة 3.8.






































