اذهب إلى المحتوى

مثال تطبيقي متكامل حول استخدام تقنيات جافا


رضوى العربي

سنتناول خلال هذا المقال برنامجًا معقدًا بعض الشئ عما رأيناه مسبقًا، فقد كانت غالبية الأمثلة التي تعرَّضنا إليها مجرد أمثلة بسيطة هدفها توضيح تقنية برمجية أو اثنتين على الأكثر؛ أما الآن، فقد ان الوقت لتوظيف كل تلك الأفكار والتقنيات معًا ضمن برنامجٍ واحدٍ حقيقي. سنناقش أولًا البرنامج وتصميمه بصورةٍ مُبسطة، وبينما نفعل ذلك، سنتحدث سريعًا عن بعض خاصيات جافا التي لم نتمكَّن من الحديث عنها أثناء المقالات السابقة؛ إذ تتضمَّن تلك الخاصيات أمورًا يُمكِن تطبيقها على جميع البرامج وليس فقط على برامج واجهات المُستخدم الرسومية GUI.

سنتحدث تحديدًا عن برنامج عرض مجموعة ماندلبرو Mandelbrot، والذي يَسمَح للمُستخدِم باكتشاف تلك المجموعة الشهيرة، إذ سنبدأ أولًا بشرح ما يعنيه ذلك. تتوفَّر نسخةٌ أقوى من هذا البرنامج؛ ونسخةٌ أخرى منه مكتوبةٌ بلغة JavaScript، وقابلةٌ للتشغيل بمتصفحات الإنترنت.

مجموعة ماندلبرو Mandelbrot

مجموعة ماندلبرو هي مجموعةٌ من النقاط الواقعة على سطح مستوى xy، التي تُحسَب مواضعها بواسطة عمليةٍ حسابية؛ وكل ما تحتاج إلى معرفته لكي تتمكَّن من تشغيل البرنامج هو أن تَعرِف إمكانية استخدام هذه المجموعة لصناعة مجموعةٍ من الصور الرائعة. سننتقل الآن إلى التفاصيل الحسابية: لنفترض لدينا نقطة (a,b)، وكان الإحداثي الأفقي والرأسي لتلك النقطة مكونين من أعدادٍ حقيقية، عندها يُمكِننا إذًا تطبيق العمليات التالية عليها:

Let x = a
Let y = b
Repeat:
   Let newX = x*x - y*y + a
   Let newY = 2*x*y + b
   Let x = newX
   Let y = newY

تتغير إحداثيات النقطة (x,y) أثناء تنفيذ حلقة التكرار loop بالأعلى، ويَنقلنا ذلك إلى السؤال التالي: هل تزداد قيم إحداثيات النقطة (x,y) دون أي قيد أم أنها تقتصر على منطقة نهائية ضمن المستوى؟ إذا كانت (x,y) تذهب إلى اللانهاية (أي تزداد بدون قيد)، فإن نقطة البداية (a,b) لا تنتمي إلى مجموعة ماندلبرو؛ أما إذا كانت النقطة (x,y) مقتصرةً على منطقة نهائية، فإن نقطة البداية (a,b) تنتمي إلى المجموعة.

من المعروف أنه لو أصبح 'x2 + y2' أكبر من '4' ضمن أي لحظة، فإن النقطة (x,y) تذهب إلى اللانهاية، وبالتالي لو أصبح 'x2 + y2' أكبر من '4' بالحلقة المُعرَّفة بالأعلى، فيُمكِننا أن نُنهِي الحلقة، ونستنتج أن النقطة (a,b) ليست ضمن مجموعة ماندلبرو بلا شك. في المقابل، بالنسبة لنقطة (a,b) تنتمي إلى تلك المجموعة، فإن الحلقة لن تنتهي أبدًا. إذا شغَّلنا ذلك على حاسوب، فإننا بالطبع لا نريد أن نحصل على حلقة لا نهائية تَعمَل إلى الأبد، ولذلك سنضع حدًا أقصى على عدد مرات تنفيذ الحلقة، وسيُمثِّل maxIterations ذلك الحد. ألقِ نظرةً إلى ما يلي:

x = a;
y = b;
count = 0;
while ( x*x + y*y < 4.1 ) {
   count++;
   if (count > maxIterations)
      break;
   double newX = x*x - y*y + a;
   double newY = 2*x*y + b;
   x = newY;
   y = newY;
}

بعد انتهاء الحلقة، وإذا كانت قيمة count أقل من أو تُساوِي maxIterations؛ فعندها يُمكِننا أن نستنتج أن النقطة (a,b) لا تنتمي إلى مجموعة ماندلبرو؛ أما إذا كانت قيمة count أكبر من maxIterations، فقد تنتمي النقطة (a,b) إلى المجموعة أو لا؛ وفي العموم كلما كانت قيمة maxIterations أكبر، كلما زادت احتمالية انتماء النقطة (a,b) إلى المجموعة.

سنُنشِئ صورةً باستخدام العملية الحسابية السابقة على النحو التالي: سنَستخدِم شبكةً مستطيلةً من البكسلات لتمثيل مستطيلٍ واقعٍ على سطح المستوى، بحيث يتوافق كل بكسل مع إحداثيات قيمها حقيقية (a,b)، إذ تُشير قيم الإحداثيات إلى مركز البكسل. سنُشغِّل حلقة التكرار بالأعلى لكل بكسل؛ فإذا تعدَّت قيمة count قيمة الحد الأقصى maxIterations، فسنُلّون البكسل باللون الأسود، مما يعني أن تلك النقطة قد تنتمي إلى مجموعة ماندلبرو؛ أما إذا لم يتعداه، فسيعتمد لون البكسل على قيمة count بعد انتهاء الحلقة، مما يَعنِي أننا سنَستخدِم ألوانًا مختلفةً للقيم المختلفة من count.

كلما ازدادت قيمة count، كانت النقطة أقرب إلى مجموعة ماندلبرو؛ وبالتالي تُعطِي الألوان بعض المعلومات عن النقاط الواقعة خارج المجموعة وعن شكل المجموعة، ولكن من المهم أن تدرك أن تلك الألوان عشوائية وأن النقاط الملونة لا تنتمي إلى المجموعة.

تَعرِض الصورة التالية لقطة شاشة من برنامج عرض مجموعة ماندلبرو، الذي يَستخدِم نفس تلك العملية الحسابية؛ إذ تُمثِّل المنطقة السوداء مجموعة ماندلبرو، باستثناء أن بعض النقاط السوداء قد لا تكون ضمن المجموعة فعليًا.

001Mandelbrot.png

إذا شغَّلت البرنامج فبإمكانك تكبير الصورة حول أي منطقة صغيرة ضمن سطح المستوى، وذلك بنقر زر الفأرة على الصورة مع السحب؛ إذ يؤدي ذلك إلى رسم "صندوق تكبير" مستطيل الشكل حول جزء الصورة ذاك كما هو مُوضَّح بالأعلى. وعندما تُحرِّر زر الفأرة، سيزداد حجم جزء الصورة الموجود داخل صندوق التكبير لكي يملأ الشاشة بالكامل؛ أما إذا نقرت على أي نقطة ضمن الصورة، فسيزداد حجم الصورة أيضًا عند النقطة التي نقرت عليها بمعدل تكبير يساوي الضعف. انقر على "Shift" أو اِستخدِم زر الفأرة الأيمن لكي تُصغرّ حجم الصورة.

تُعدّ النقاط الموجودة عند الحدود الفاصلة لمجموعة ماندلبرو هي النقاط الأكثر تشويقًا، وفي الحقيقة، يُعَد ذلك الفاصل معقدًا تعقيدًا لا نهائيًا؛ فإذا كبَّرت الصورة إلى حدٍ بعيد، فلن يمنعك البرنامج من فعل ذلك، ولكنك ستتعدّى إمكانيات نوع البيانات double، وستبدأ بكسلات الصورة في الظهور وستصبح الصورة بلا معنى.

يُمكِنك استخدام قائمة "MaxIterations" لزيادة الحد الأقصى لعدد مرات تكرار الحلقة، وتذكّر أن البكسلات السوداء قد تقع أو لا ضمن المجموعة؛ فإذا أزدت قيمة MaxIterations، فقد تجد أن بعض المناطق السوداء قد أصبحت ملونةً هي الأخرى.

تُحدِّد قائمة "Palette" مجموعة الألوان المُستخدَمة، ويؤدي استخدام لوحات ألوان مختلفة إلى إنتاج صورٍ مختلفة، ولكنها مُجرّد مجموعة ألوان عشوائية فقط. وتحدِّد قائمة "PaletteLength" عدد الألوان المختلفة المُستخدَمة؛ فإذا اِستخدَمت الإعدادات الافتراضية، فإن كل قيمة ممكنة للمتغير count يُقابلها قيمة لونٍ مختلفة.

قد تحصل أحيانًا على صور أفضل بكثير باستخدام عددٍ مختلف من الألوان؛ فإذا كان عدد الألوان الموجودة بلوحة الألوان المُستخدَمة أقل من قيمة maxIterations، فسيتكرَّر نفس اللون لكي يشمل جميع القيم المحتملة للمتغير count؛ أما إذا كان عددها أكبر من قيمة maxIterations، فسيُستخدَم فقط جزءٌ من الألوان المُتاحة. لذلك، إذا كانت غالبية بكسلات الصورة من خارج المجموعة مُكوَّنة من درجات مختلفة من لون واحد تقريبًا، فقللّ عدد ألوان لوحة الألوان؛ إذ يؤدي ذلك إلى اختلاف الألوان بصورةٍ أسرع مع تغيُّر قيمة count؛ أما إذا وجدتها مُكوَّنةً من ألوان عشوائية تمامًا بدون أي انسيابية بين الألوان، زِد عدد ألوان لوحة الألون.

يحتوي البرنامج على قائمة "File" يُمكِن استخدامها لحفظ الصورة مثل ملف صورة بامتداد PNG؛ ويُمكِنك أيضًا أن تحفظ ملف "param" لحفظ إعدادات البرنامج التي أنتجت الصورة الحالية؛ وتستطيع لاحقًا قراءة هذا الملف إلى البرنامج باستخدام الأمر "Open".

يعود اسم مجموعة ماندلبرو إلى Benoit Mandelbrot، وهو أول من لاحظ ذلك التعقيد المذهل لتلك المجموعة، إذ من الرائع الحصول على هذا التعقيد والجمال من تلك الخوارزمية البسيطة.

تصميم تطبيق مجموعة ماندلبرو

نجد معظم الأصناف بلغة جافا مُعرَّفةً ضمن حزم packages؛ فعلى الرغم من أننا استخدمنا بعض الحزم القياسية، مثل javafx.scene.control و java.io بكثرة، إلا أن معظم الأمثلة التي تعرَّضنا إليها كانت تَستخدِم الحزمة الافتراضية، ويَعنِي ذلك أننا لم نُصرِّح بانتمائها إلى أي حزمةٍ مسماة. وفي المقابل، عند إنجاز أي برمجة جدية، فمن الأفضل دومًا أن نُنشِئ حزمةً لاحتواء الأصناف المُستخدَمة بالبرنامج.

تُوصِي مؤسسة Oracle بأن تكون أسماء الحزم مبنيةً على اسم نطاق domain name الإنترنت للمؤسسة المُنتِجة للحزمة؛ فبالنسبة لحاسوب المؤلف، فإن اسم النطاق الخاص به هو eck.hws.edu، ولا يُفترَض لأي حاسوب آخر بالعالم عمومًا أن يكون له نفس الاسم؛ ووفقًا لمؤسسة Oracle، ينبغي أن يكون اسم الحزمة في تلك الحالة هو edu.hws.eck، أي سيكون ترتيب عناصر اسم النطاق معكوسًا. علاوةً على ذلك، ينبغي أن تُسمَى الحزم الفرعية ضمن تلك الحزمة بأسماءٍ، مثل edu.hws.eck.mdbfx، وهو في الواقع الاسم الذي اختاره المؤلف لتطبيق عرض مجموعة ماندلبرو. ويَضمَن ذلك ألا يَستخدِم أي شخصٍ آخر -شرط أن يتبِع نفس نمط التسمية- نفس اسم الحزمة، وبناءً على ذلك، يُمكِن استخدام ذلك الاسم الفريد لتحديد هوية التطبيق.

ناقشنا باختصار طريقة استخدام الحزم بمقال بيئات البرمجة programming environment في جافا، وكذلك أثناء شرح بعض الأمثلة البرمجية بمقال أمثلة برمجية على الشبكات في جافا: إطار عمل لتطوير الألعاب عبر الشبكة. باختصار، يُمثِل التالي كل ما ينبغي أن تعرفه بخصوص تطبيق عرض مجموعة ماندلبرو، فالبرنامج مُعرَّفٌ ضمن 7 ملفات شيفرة مصدرية تجدها بالمجلد edu/hws/eck/mdbfx الموجود داخل مجلد source بالموقع؛ أي أن الملفات موجودة بمجلدٍ اسمه mdbfx، والموجود بدوره بمجلدٍ اسمه eck، المتواجد بمجلد hws، ضمن مجلد edu. لا بُدّ أن تتبِع المجلدات اسم الحزمة بتلك الطريقة.

يحتوي نفس ذلك المجلد على ملفٍ اسمه strings.properties المُستخدَم بالبرنامج والذي سنناقشه بالأسفل. ويحتوي مجلد examples على ملفات الموارد التي تَستخدِمها قائمة "Examples". ونظرًا لاعتماد البرنامج على مكتبة JavaFX، فينبغي أن تتأكّد من توفُّر المكتبة عند تصريف البرنامج أو تشغيله كما ناقشنا بمقال بيئات البرمجة (programming environment) في جافا المشار إليه في الأعلى؛ وإذا كنت تَستخدِم بيئة تطوير متكاملة Integrated Development Environment مثل Eclipse، فكل ما عليك فعله هو إضافة مجلد "edu" إلى المشروع، مع ضبطه لكي يَستخدِم مكتبة JavaFX؛ وإذا أردت استخدام سطر الأوامر، فينبغي أن يشير مجلد العمل working directory إلى المجلد المُتضمِّن لمجلد edu؛ وإذا لم تكن تَستخدِم إصدارًا قديمًا من JDK والذي تكون فيه مكتبة JavaFX مبنيةً مسبقًا، فستحتاج إلى إضافة خيارات مكتبة JavaFX إلى أمري javac و java.

إذا كنت قد عرَّفت الأمرين jfxc و jfx المكافئين للأمرين javac و java مع خيارات مكتبة JavaFX، فيُمكِنك ببساطة أن تُصرِّف الشيفرة المصدرية باستخدام الأمر التالي:

jfxc  edu/hws/eck/mdbfx/*.java

أو الأمر التالي إذا كنت تَستخدِم نظام التشغيل Windows:

jfxc  edu\hws\eck\mdbfx\*.java

ستَجِد صنف التطبيق الرئيسي مُعرَّفًا بالصنف Main. اِستخدِم الأمر التالي لتشغيل البرنامج:

jfx  edu.hws.eck.mdbfx.Main

يجب أن يُنفِّذ هذا الأمر بالمجلد المُتضمِّن لمجلد edu؛ وإذا كان إصدار JDK المُستخدِم يتضمَّن مكتبة JavaFX مُسبَقًا، فيُمكِنك ببساطة أن تَستخدِم الأمرين javac و java بدلًا من jfxc و jfx.

يتضمَّن الملف MandelbrotCanvas.java غالبية العمليات المطلوبة لحساب صور مجموعة ماندلبرو وعرضها؛ إذ يُعدّ الصنف MandelbrotCanvas صنفًا فرعيًا من الصنف Canvas، ويُمكِنه حساب صور مجموعة ماندلبرو وعرضها كما ناقشنا بالأعلى. تعتمد الصورة الناتجة على النطاق الظاهر من قيم x و y، وعلى الحد الأقصى لعدد مرات تكرار الخوارزمية، وعلى لوحة الألوان المُستخدَمة لتلوين البكسلات خارج المجموعة. وتأتي جميع تلك المُدْخَلات من مكان آخر ضمن البرنامج، ويقتصر دور الصنف MandelbrotCanvas على حساب الصورة وعرضها بناءً على قيم المْدخَلات المُعطاة له.

بالإضافة إلى تلوين بكسلات الصورة، يَستخدِم الصنف MandelbrotCanvas مصفوفةً ثنائية الأبعاد لتخزين قيمة count لكل بكسلٍ في الصورة؛ إذ تُحدِّد قيمة count لبكسل معين مع لوحة الألوان المُستخدَمة، اللون المستعمل لتلوين ذلك البكسل كما ناقشنا بالأعلى. وفي حالة تغيير لوحة الألوان المُستخدَمة، سيَستخدِم البرنامج قيمة المتغير count لكل بكسل لإعادة ضبط الألوان، دون أن يعيد حساب مجموعة ماندلبرو مرةً أخرى. في المقابل، إذا تغير نطاق قيم x و y، أو إذا تغير حجم النافذة، فسيضطّر البرنامج لإعادة حساب قيم count لجميع البكسلات.

قد تستغرِق عملية حساب تلك القيم وقتًا طويلًا، ولأنه من غير المُفترَض أن نُعطِّل block واجهة المُستخدِم أثناء إجراء تلك الحسابات، سيُجرِي البرنامج تلك العمليات بخيطٍ عاملٍ worker thread منفصل كما ناقشنا بمقال البرمجة باستخدام الخيوط threads في جافا، إذ يَستخدِم البرنامج تحديدًأ خيطًا عاملًا وحيدًا لكل معالج. عند بدء عملية حساب تلك القيم، ستكون الصورة شفافة، أي أنه يُمكِنك رؤية الخلفية الرمادية للنافذة.

تُقسَّم العملية إجمالًا إلى مجموعةٍ من المهام tasks، وتتكوَّن كل مهمة من عملية حساب صف واحد من الصورة. وبعد انتهاء أي مهمة، تُطبِّق الألوان الناتجة على البكسلات الموجودة بالصف الخاص بتلك المهمة. نظرًا لأنه من الممكن تعديل الحاوية فقط من خلال خيط تطبيق JavaFX، فستَستدعِي كل مهمة التابع Platform.runLater()‎ لإجراء التغييرات المطلوبة، وهذا يُمكِّن المُستخدِم من الاستمرار باستخدام القوائم وحتى الفأرة أثناء عملية حساب الصورة.

يحتوي الملف MandelbrotPane.java على كامل محتوى نافذة تطبيق عرض مجموعة ماندلبرو، إذ يُعدّ الصنف MandelbrotPane صنفًا فرعيًا من الصنف BorderPane. يحتوي منتصف كائن الحاوية المنتمي للصنف BorderPane على كائنٍ من النوع MandelbrotCanvas. وفي الحقيقة، يَستخدِم البرنامج حاويةً ثانيةً شفافةً فوق الحاوية المتضمِّنة للصورة؛ فعندما يَرسِم المُستخدِم "صندوق تكبير" باستخدام الفأرة، فإن ذلك الصندوق يُرسَم فعليًا بالحاوية العلوية لكي لا يشوه الصورة (ألقِ نظرةً على مقال أمثلة عن رسوميات فاخرة باستعمال جافا). في المقابل، تحتوي المنطقة السفلية من الحاوية على عنوان من النوع Label، يَعمَل مثل شريطٍ لعرض حالة البرنامج، إذ يُستخدَم لعرض بعض المعلومات التي قد تَهِم المُستخدِم. أخيرًا، يحتوي البرنامج على شريط قوائم أعلى الحاوية.

يُعرِّف الصنف Menus.java -المُشتَق من الصنف MenuBar- شريط القوائم الخاص بالتطبيق. (ألقِ نظرةً على مقال بناء تطبيقات كاملة باستعمال مكتبة جافا إف إكس JavaFX لمزيدٍ من المعلومات عن القوائم وعناصر القوائم، كما يُعرِّف الصنف Menus مجموعةً من التوابع والأصناف الفرعية المتداخلة nested subclasses لتمثيل جميع عناصر القائمة، وكذلك الأوامر التي تُمثِّلها تلك العناصر؛ إذ تشتمل تلك الأوامر على أوامرٍ مُتعلِّقة بمعالجة الملفات وتَستخدِم التقنيات التي تَعرَّضنا لها بمقالات مدخل إلى التعامل مع الملفات في جافا ومقدمة مختصرة للغة XML واستعمالها في تطبيقات جافا وأمثلة عن رسوميات فاخرة باستعمال جافا المشار إليه بالأعلى.

تحتوي القوائم "MaxIterations" و "Palette" و"PaletteLength" على مجموعةٍ من الكائنات المنتمية إلى النوع RadioMenuItems. يُعرِّف البرنامج صنفًا متداخلًا داخل الصنف Menus لتمثيل كل مجموعة؛ فعلى سبيل المثال، يُعرِّف الصنف PaletteManager عناصر قائمة "Palette" على هيئة متغيرات نسخة instance variables، ويُسجِّل معالج حدث لكل عنصر، كما يُعرِّف القليل من البرامج المفيدة لمعالجة القائمة. تتشابه الأصناف الخاصة بالقوائم الثلاثة، جتى أنه من الأفضل تعريفها على أنها أصنافٌ فرعيةٌ مشتقةٌ من صنفٍ أكثر عمومية. ويحتوي البرنامج أيضًا على قائمة "Examples" التي تتضمَّن الإعدادات الخاصة ببعض العينات لقطعٍ من مجموعة ماندلبرو.

يُنفِّذ الصنف MandelbrotPane كثيرًا من العمل الذي يتطلّبه البرنامج؛ فهو يُهيئ معالجات لأحداث الفأرة MousePressed و MouseDragged و MouseReleased بالحاوية العلوية، ليُمكِّن المُستخدِم من تكبير الصورة وتصغيرها؛ كما يُهيئ معالجًا للحدث MouseMoved، الذي يُحدِّث شريط الحالة ويجعله يَعرِض إحداثيات النقطة المقابلة للمكان الحالي لمؤشر الفأرة على الصورة. يُولَّد الحدث MouseMoved عندما يُحرِّك المُستخدِم مؤشر الفأرة دون أن يضغط باستمرار على زرها. ويُستخدم كذلك الحدث MouseExited لإعادة ضبط شريط الحالة إلى كلمة "Idle" عندما يقع مؤشر الفأرة خارج الحاوية.

بالإضافة إلى ما سبق، يُنفِّذ البرنامج أوامر قوائم أخرى كثيرة باستدعاء توابع مُعرَّفةٍ بالصنف MandebrotPane. ويحتوي الصنف Menus على متغير نسخة اسمه owner، يشير إلى الحاوية -من النوع MandelbrotPane- المُتضمِّنة لشريط القوائم، وبالتالي يُمكِنه استخدام ذلك المتغير لاستدعاء أي توابع مُعرَّفة بالصنف MandelbrotPane؛ إذ يضبُط التابع setLimits()‎ مثلًا، نطاق قيم x و y الظاهرة بالصورة؛ كما أن هناك توابعٌ أخرى لضبط كُلٍ من لوحة الألوان المُستخدَمة وعدد الألوان الموجودة بلوحة الألوان، والحد الأقصى لعدد مرات تكرار الخوارزمية.

بمجرد تغيُّر أي من تلك الخاصيات، لا بُدّ من تعديل الصورة المعروضة لمجموعة ماندلبرو؛ فعلى سبيل المثال، عندما تتغير لوحة الألوان المُستخدَمة أو عدد الألوان الموجودة باللوحة، يَحسِب الصنف MandelbrotPane لوحةً جديدةً من الألوان ويَستدعِي تابعًا مُعرَّفًا بالصنف MandelbrotCanvas ليُبلِّغه بأن عليه استخدام تلك اللوحة الجديدة. وفي المقابل، عندما يتغير الحد الأقصى لعدد مرات تكرار الخوارزمية، تكون إعادة حساب الصورة بالكامل ضرورية، ولهذا يَستدعِي الصنف MandelbrotPane التابع startJob()‎ المُعرَّف بالصنف MandelbrotCanvas ليُبلِّغه بأن عليه أن يبدأ وظيفةً جديدة، ويتولى الصنف MandelbrotCanvas كل العمل اللازم لتهيئة تلك الوظيفة وإدارتها.

يُمرَّر كائن الصنف MandelbrotPane المُستخدَم بالبرنامج مثل معاملٍ إلى باني الصنف Menus، ويُخزِّن بدوره كائن الصنف Menus المُمثِّل للقوائم نسخةً من ذلك الكائن بهيئة متغير نسخة، اسمه owner. في الواقع، يُعالِج الصنفان MandelbrotPane و MandelbrotCanvas غالبية أوامر القوائم، ولكي يتمكَّن الكائن المُمثِّل للقوائم من تنفيذ تلك الأوامر، فإنه يحتاج إلى مرجع reference إلى كائن الصنف MandelbrotPane. وبالمثل من الصنف MandelbrotCanvas، يُعرِّف كائن الصنف MandelbrotPane التابع getDisplay()‎ الذي يعيد مرجعًا إلى الحاوية التي يحتويها، وبالتالي يستطيع الكائن المُمثِّل للقوائم الحصول على مرجعٍ إلى الحاوية باستدعاء owner.getDisplay()‎.

كنا نضع شيفرة البرنامج بالكامل بالأمثلة السابقة من هذه السلسلة بملفٍ واحدٍ كبير، وبالتالي كانت جميع الكائنات متاحةً لكل أجزاء الشيفرة مباشرةً. وفي المقابل، عند تقسيم البرنامج إلى مجموعة من الملفات، لا يكون الوصول إلى الكائنات الضرورية بهذه السهولة.

تُعدّ الأصناف MandelbrotPane و MandelbrotCanvas و Menus أكثر الأصناف أهميةً بتطبيق عرض مجموعة ماندلبرو؛ إذ يُعرِّف الصنف Main.java الصنف الفرعي المُشتَق من الصنف Application، والذي ينبغي تشغيله عند تنفيذ البرنامج؛ ويضع تابعه start()‎ كائنًا من النوع MandelbrotPane داخل المرحلة stage الرئيسية للبرنامج.

يحتوي البرنامج على ثلاثة أصناف أخرى، إذ يُعرِّف الصنفان SetImageSizeDialog.java و SetLimitsDialog.java صناديق نوافذ مخصَّصة، والتي لن نناقشها هنا أكثر من ذلك؛ أما الصنف الأخير، فهو I18n، والذي سنناقشه بالأسفل.

أظهرت هذه المناقشة القصيرة لتصميم تطبيق عرض مجموعة ماندلبرو أنه يَستخدِم تشكيلةً واسعةً من التقنيات التي تعرَّضنا لها مُسبقًا خلال مقالات هذه السلسلة، وسنفحص بالجزء المُتبقِي من هذا المقال القليل من الخاصيات الجديدة المُستخدَمة ضمن البرنامج.

الأحداث ومستمعي الأحداث والارتباطات

تعاملنا مع الأحداث events ومستمعي الأحداث بكثرة، وكذلك مع ارتباط binding الخاصيات القابلة للمراقبة observable ببعض الأمثلة، وسيكون من الرائع لو رأينا طريقة استخدام تلك التقنيات ضمن تطبيق عرض مجموعة ماندلبرو، إذ سنَستخدِم مجموعةً من الأصناف.

لنبدأ الآن من الحقيقة التالية: لا يَعرِف الصنف MandelbrotCanvas أي شيء عن الصنف Menus مع أن شريط القوائم يحتوي على عناصر يبدو وكأنها تَعرِف ما يحدث بصنف الحاوية MandelbrotCanvas. بالتحديد، تُعطَّل بعض عناصر القائمة عندما تكون عملية حساب الصورة قيد التنفيذ.

بما أن الحاوية لا تستدعِي أي توابع أو تَستخدِم أيًا من متغيرات صنف القوائم Menus، فكيف تمكَّنت القوائم من معرفة ما إذا كانت هناك عمليةً حسابيةً قيد التنفيذ بالحاوية؟ الإجابة بالطبع هي من خلال استخدام الأحداث أو على نحوٍ أكثر دقة، وذلك من خلال استخدام الارتباط (ألقِ نظرةً على مقال البواني وتهيئة الكائنات Object Initialization في جافا).

يحتوي الصنف MandelbrotCanvas على خاصية قابلة للمراقبة من النوع boolean اسمها working، إذ تحتوي تلك الخاصية على القيمة true عندما يكون هناك عملية معالجة قيد التنفيذ. وينبغي أن تكون عناصر القائمة مُعطَّلةً عندما تكون قيمة تلك الخاصية مساويةً للقيمة true، وهو ما يُمكِننا إجراؤه بسطر شيفرةٍ واحد يربُط خاصية عنصر قائمة disableProperty بخاصية الحاوية workingProperty. على سبيل المثال، يُمكِننا تطبيق ذلك على عنصر القائمة "saveImage" بكتابة ما يَلي داخل باني الصنف Menus:

saveImage.disableProperty().bind(owner.getDisplay().workingProperty());

إذ يشير owner هنا إلى كائن الصنف MandelbrotPane؛ بينما تشير القيمة المُعادة من التابع owner.getDisplay()‎ إلى كائن الصنف MandelbrotCanvas الموجود به.

وبالمثل، يُعيد عنصر القائمة "Restore Previous Limits" ضبط نطاق قيم x و y الظاهرة إلى قيمها السابقة قبل آخر تحديث؛ إذ يَستخدِم الصنف Menus متغير النسخة previousLimits من النوع double[]‎ ليتذكر نطاق القيم السابق، ولكن السؤال هو: كيف يحصل على تلك المعلومات؟ عندما يُكبّر المُستخدِم الصورة أو يُصغرّها، يحدث ذلك التغيير بالصنف MandelbrotPane؛ بالتالي لا بُدّ إذًا من وجود طريقة تُمكِّن القوائم من ملاحظة ذلك التغيير، ويكْمُن الحل طبعًا في استخدام خاصيةٍ قابلةٍ للمراقبة، وتكون تلك الخاصية من النوع ObjectProperty<double[]>‎ في هذه الحالة.

يُضيف باني الصنف Menus مستمع حدث من النوع ChangeListener إلى الخاصية limitsProperty المُعرَّفة بالصنف MandelbrotPane على النحو التالي:

owner.limitsProperty().addListener( (o,oldVal,newVal) -> {
       // خزِّن القيمة القديمة للمتغير‫ limitsProperty لاستخدامها بالأمر "Restore Previous Limits"
    previousLimits = oldVal;
    undoChangeOfLimits.setDisable( previousLimits == null );
});

نظرًا لأننا نَستخدِم الأحداث هنا للتواصل، فإن الصنفين MandelbrotCanvas و MandelbrotPane خفيفا الترابط loosely coupled مع الصنف Menus. في الحقيقة، يُمكِننا استخدامهما دون أي تعديل ببرامج أخرى لا تحتوي على نفس الصنف Menus من الأساس؛ وبدلًا من استخدام الأحداث والارتباط، كان من الممكن جعل صنفي الحاوية والعرض يَستدعِيان توابعًا، مثل limitsChanged()‎ و computationStarted()‎ مُعرَّفين بالصنف Menus. يكون الترابط بين الأصناف في تلك الحالة قويًا strong coupling، وبالتالي سيضطّر أي مبرمج يرغب باستخدام الصنف MandelbrotCanvas إلى استخدام الصنف Menus أيضًا، أو إلى تعديل الصنف MandelbrotCanvas كي لا يُشير إلى الصنف Menus.

لا يُمكِننا طبعًا معالجة جميع المشاكل باستخدام الأحداث، كما أنه ليس من الضروري أن يكون أي ترابط قوي شيئًا سيئًا، إذ يشير الصنف MandelbrotPane مثلًا إلى الصنف MandelbrotCanvas مباشرةً ولا يُمكِننا استخدامه بدونه، ولكن بما أن الغرض من كائن الصنف MandelbrotPane هو حمل كائنٍ آخر من الصنف MandelbrotCanvas، فلا يُمثِّل هذا الترابط مشكلةً هنا. وفي المقابل، يُمكِننا استخدام الصنف MandelbrotCanvas على نحوٍ مستقل عن الصنف MandelbrotPane.

يُوظِّف الصنف MandelbrotPane الأحداث لغرضٍ آخر؛ إذ تقع حاوية الصورة والحاوية الشفافة ضمن الكائن displayHolder من النوع StackPane، وعندما يُغيّر المُستخدِم حجم النافذة، سيتغير حجم الكائن displayHolder ليتناسب مع الحجم الجديد، وينبغي عندها أن يُضبَط حجم الحاوية لكي يتناسب مع حجم العرض الجديد؛ أي لا بُدّ من بدء عملية المعالجة لحساب صورةٍ جديدة. ولذلك، يُهيئ الصنف MandelbrotPane مستمعين إلى الخاصيات height و width المُعرَّفة بالكائن displayHolder؛ وذلك حتى يتمكَّن من الاستجابة للتغييرات بالحجم.

ولكن، عندما يُغيّر المُستخدِم حجم النافذة ديناميكيًا، قد يتغير حجم displayHolder عدة مرات بكل ثانية. ونظرًا لأن بدء عملية حساب صورة جديدة يتطلَّب كثيرًا من الوقت، فإننا بالتأكيد لا نرغب في فعل ذلك عدة مراتٍ بالثانية الواحدة.

في الواقع، سيبدأ البرنامج عملية حساب صورة جديدة فقط بعد مرور حوالي ثلث ثانية من توقُّف التغيير بحجم النافذة؛ وبالتالي إذا جرَّبت تغيير حجم نافذة البرنامج، فستلاحظ أن الحاوية لا تُغيِّر حجمها تلقائيًا بتُغيُّر حجم النافذة، ويَعرِض البرنامج نفس الصورة طالما كان الحجم هو نفسه. تُوضِح الشيفرة التالية طريقة فعل ذلك.

يُهيئ البرنامج مستمعي أحداث تغيُّر الحجم displayHolder، ليستدعوا التابع startDelayedJob()‎ على النحو التالي:

displayHolder.widthProperty().addListener( e -> startDelayedJob(300,true) );
displayHolder.heightProperty().addListener( e -> startDelayedJob(300,true) );

إذ يُمثِّل المعامل الأول للتابع startDelayedJob()‎ الزمن بوحدة الميلي ثانية، الذي لا بُدّ من انتظاره قبل إعادة ضبط حجم الحاوية وبدء عملية حساب جديدة؛ بينما يشير المعامل الثاني إلى أن ضبط حجم الحاوية ليس ضروريًا قبل بدء عملية المعالجة.

يَستخدِم البرنامج كائنًا من النوع Timer المُعرَّف بحزمة java.util لكي يتمكَّن من تأجيل تنفيذ العملية؛ إذ يُمكِننا ببساطة أن نُمرِّر كائنًا من النوع TimerTask يُمثِّل مهمةً مؤجَلّةً إلى كائنٍ من النوع Timer، وذلك لكي تُنفَّذ المهمة بعد زمنٍ معين؛ كما يُمكِننا أيضًا إلغاء المهمة إذا لم يكن ذلك الزمن قد مرّ بعد، إذ يَستخدمِ البرنامج التابع startDelayedJob()‎ لإضافة مهمة تغيير حجم الحاوية إلى المؤقت، لتُنفَّذ بعد 300 ميلي ثانية. وفي حالة استدعاء التابع startDelayedJob()‎ مرةً أخرى قبل مرور 300 ميلي ثانية، فستُلغَى المهمة السابقة تلقائيًا وتُضاف المهمة الجديدة بدلًا منها إلى المؤقت. وبذلك، تكون مُحصلة ما سبق هو عدم تنفيذ أي مهمة إلى أن تَمرّ 300 ميلي ثانية دون أي استدعاء جديدٍ للتابع ()startDelayedJob.

يتيح البرنامج خيار ضبط الصورة لتكون ثابتة الحجم؛ إذ لا ينبغي تلك الحالة أن يتغير حجم displayHolder نهائيًا. وبناءً على ذلك، قد تكون الصورة صغيرةً، ولا تتمكَّن من ملئ النافذة بالكامل، وستَظهَر عندها أجزاءٌ من الخلفية الرمادية الموجودة وراءها. في المقابل، قد تكون الصورة كبيرةً جدًا على النافذة، وفي تلك الحالة، ينبغي أن تظهر أشرطة تمرير يُمكِن اِستخدامها للمرور عبر كامل الصورة؛ ويُنفِّذ البرنامج ذلك باستخدَام الصنف ScrollPane الذي يُمثِّل حاويةً تحتوي على مكوِّن واجهة معين، ويُوفِّر أشرطة تمرير إذا اقتضت الضرورة؛ أما عندما يكون حجم الصورة ثابتًا، فسيُحذَف displayHolder من الصنف MandelbrotPane، ويُوضَع بكائن الصنف ScrollPane المُمثِّل للحاوية، ثم يُوضَع كائن الحاوية ذلك بمنتصف كائن الصنف MandelbrotPane.

المزيد عن واجهات المستخدم الرسومية

أخيرًا، سنذكر هنا بعض التفاصيل المُتعلّقة ببرمجة واجهات المُستخدِم الرسومية التي لم نتمكَّن من عرضها بالمقالات السابقة.

ذكرنا من قبل أن ملفات الموارد تُمثِّل جزءًا من البرنامج ولكنها لا تتضمَّن أي شيفرة، وقد رأينا على سبيل المثال في مقال التعرف على بعض أصناف مكتبة جافا إف إكس JavaFX البسيطة مدى سهولة اِستخدَام الصور مثل ملفات موارد مع الصنف Image؛ ولكن يُمكِننا عمومًا استخدام أي نوعٍ من البيانات مثل ملفات موارد.

يحتوي تطبيق عرض مجموعة ماندلبرو مثلًا، على قائمة "Examples"؛ وعندما يختار المُستخدِم أمرًا من تلك القائمة، تُحمَّل الإعدادات الخاصة بعرضٍ معيَّن لمجموعة ماندلبرو إلى البرنامج؛ وتكون هذه الإعدادات مُخزَّنةً في الواقع بهيئة ملفات موارد بصيغة XML.

كيف يَصِل البرنامج إلى تلك الملفات؟ لسوء الحظ، ليس الأمر بنفس سهولة استخدام صورة على أنها ملف مورد لإنشاء كائنٍ من النوع Image.

تُخزَّن الموارد بملفات تقع إلى جانب ملفات الأصناف المُصرَّفة الخاصة بالبرنامج؛ إذ يُحدِّد بلغة جافا كائنٌ من النوع ClassLoader -ويُعرَف باسم مُحمِّل أصناف class loader- مكان ملفات الأصناف ويُحمِّلها للبرنامج عند الحاجة. يملُك مُحمِّل الأصناف قائمةً بالمسارات التي ينبغي عليه البحث فيها عن ملفات الأصناف؛ إذ تُعرَف تلك القائمة باسم مسارات الأصناف class path، والتي تتضمَّن موضع تخزين أصناف جافا القياسية، كما تتضمَّن المجلد الحالي.

إذا كان البرنامج مُخزَّنًا داخل ملف jar، فسيكون ذلك الملف أيضًا ضمن مسارات الأصناف. وبالإضافة إلى ملفات الأصناف، تستطيع كائنات الصنف ClassLoader العثور على ملفات الموارد الواقعة بمسارات الأصناف أو بمجلداتٍ فرعية داخل مسارات الأصناف.

علينا أولًا أن نحصل على كائنٍ من النوع ClassLoader لكي نتمكَّن من استخدام ملفات الموارد؛ إذ يمكننا باستخدام ذلك الكائن أن نُحدِّد موضع ملف موردٍ معين. في العموم، يحتوي أي كائن على تابع النسخة getClass()‎، الذي يعيد كائنًا يُمثِّل الصنف الذي ينتمي إليه الكائن؛ ويحتوي الكائن المُمثِّل للصنف بدوره على التابع getClassLoader()‎، الذي يعيد الكائن -من النوع ClassLoader- الذي حمَّل الصنف المَعنِي. وبالتالي، يُمكِننا كتابة ما يلي بأي تابع نسخة لأي كائن لكي نحصل على مُحمِّل الأصناف الذي نريده.

ClassLoader classLoader = getClass().getClassLoader();

يُمكِننا بدلًا من ذلك استخدام ClassName.class، إذ يشير ClassName إلى اسم الصنف الذي نريده، لكي نحصل على مرجع reference إلى الكائن المُمثِّل لذلك الصنف. على سبيل المثال، كان بإمكاننا استخدام Menus.class.getClassLoader()‎ بتطبيق عرض مجموعة ماندلبرو لكي نسترجع مُحمِّل الأصناف.

بمجرد حصولنا على مُحمِّل الأصناف، يُمكِننا اِستخدَامه للعثور على أي ملف مورد؛ إذ يعيد مُحمِّل الأصناف مُحدِّد الموارد الموحَّد URL الخاص بالملف، وهو ما يُمكِّننا من قراءة البيانات الموجودة بالملف. وكما هو الحال مع ملفات الصور، يحتاج مُحمِّل الأصناف إلى مسار الملف لكي يتمكَّن من إيجاده؛ إذ يتضمَّن المسار اسم الملف، بالإضافة إلى أي مجلدات ينبغي التنقُل عبرها للوصول إلى الملف.

تقع ملفات الموارد المُتضمِّنة للأمثلة بالنسبة لتطبيق عرض مجموعة ماندلبرو داخل سلسلة المجلدات edu/hws/eck/mdbfx/examples، وبالتالي يكون مسار أحد تلك الملفات "settings1.mdb" هو edu/hws/eck/mdbfx/examples/settings1.mdb.

يُعيد الأمر التالي مُحدِّد الموارد المُوحد الخاص بذلك الملف:

URL resourceURL = 
         classLoader.getResource("edu/hws/eck/mdbfx/examples/settings1.mdb");

والآن، بعد أن حصلنا على مُحدِّد الموارد الموحد الخاص بالملف، يُمكِننا ببساطة استخدام كائن مجرى من النوع InputStream لفتح الملف وقراءة بياناته على النحو التالي:

InputStream stream = resourceURL.openStream();

وهذا بالضبط هو ما يفعله تطبيق عرض مجموعة ماندلبرو لكي يُنفِّذ قائمة "Examples". إذًا، باستخدامنا لمجرى دخل، يُمكِننا قراءة أي نوعٍ من البيانات الموجودة بملف مورد، وأن نفعل بها أي شيء نريده، ولكن تذكّر أن جافا تُوفِّر أساليبًا أفضل لتحميل بعض أنواع البيانات، مثل الصور إلى البرنامج.

سنناقش الآن موضوعًا آخر عن استخدام المُسرِّعات accelerators لعناصر القائمة؛ إذ أن المُسرِّع ببساطة هو مفتاح، أو عدة مفاتيح بلوحة المفاتيح يُمكِن اِستخدَامها لاستدعاء عنصر قائمة معين بدلًا من اختيار عنصر القائمة عن طريق الفأرة.

يشيع استخدام المُسرِّع "Control-S" مثلًا لحفظ ملف؛ كما يُستخدَم الصنف KeyCombination المُعرَّف بحزمة javafx.scene.input لتمثيل اتحاد مجموعةٍ من المفاتيح المُمكِن اِستخدَامها مثل مُسرِّع؛ ويُمكِننا إنشاء كائنٍ من هذا الصنف من سلسلةٍ نصية، مثل "ctrl+S".

ينبغي أن تحتوي السلسلة النصية على مجموعة عناصرٍ تفصل بينها إشارة الجمع، بحيث يُمثِّل كل عنصر منها -باستثناء الأخير- مفتاح مُعدِّل، مثل "ctrl"، أو "alt"، أو "meta"، أو "shift"، أو "shortcut"؛ ويُمكِن كتابة تلك المفاتيح باستخدام حروفٍ كبيرة أو صغيرة.

يُمثِّل المفتاح "shortcut" استثناءً، إذ يُكافئ المفتاح "meta" بنظام التشغيل Mac؛ بينما يُكافئ المفتاح "ctrl" بأنظمة Linux و Windows، وبالتالي نحصل على المُعدِّل المناسب لأوامر القائمة بحسب النظام المُشغَّل عليه البرنامج.

في المقابل، لا بُدّ أن يكون العنصر الأخير بالسلسلة النصية المُمثِّلة لاتحاد مجموعة المفاتيح من نوع التعداد KeyCode؛ إذ يُمثِّل ذلك النوع في العموم حرفًا أبجديًا مكتوبًا بالحالة الكبيرة، ويُمثِّل مفتاح ذلك الحرف، ولكنه قد يكون أيضًا مفتاح دالة مثل "F9" (لا يَعمَل جميعها بالمناسبة). على سبيل المثال، تُمثِّل السلسلة النصية "ctrl+shift+N" الضغط باستمرار على مفتاحي "control" و "shift" مع الضغط على مفتاح "N"؛ بينما تُمثِّل السلسلة النصية "shortcut+S" الضغط باستمرار على المُعدِّل المناسب للحاسوب الذي يَعمَل عليه البرنامج مع الضغط على مفتاح "S".

نستطيع تمرير تلك السلاسل النصية إلى التابع الساكن KeyCombination.valueOf()‎ لنُنشِئ منها كائنًا من النوع KeyCombination، وذلك لنَستخدِمه لتهيئة مُسرِّعٍ لأي عنصرٍ من عناصر القائمة، إذ تضيف الشيفرة التالية مثلًا مُسرِّعًا لعنصر القائمة "Save Image" الموجود بتطبيق عرض مجموعة ماندلبرو:

saveImage.setAccelerator( KeyCombination.valueOf("shortcut+shift+S") );

يُمكِننا استخدام المُسرِّعات مع أي نوع من أنواع عناصر القائمة، بما في ذلك RadioMenuItem و CheckMenuItem. وفي جميع الحالات، عندما ينقر المُستخدِم على مجموعة المفاتيح المُمثِّلة لمُسرِّع معين، ينبغي أن يكون لذلك نفس تأثير النقر بالفأرة على عنصر القائمة المقابل.

يكون المُسرِّع الخاص بعنصر قائمة معين مكتوبًا عادةً إلى جانب نص عنصر القائمة؛ وبالنسبة لتطبيق عرض مجموعة ماندلبرو، تملُك جميع الأوامر الموجودة بقائمتي "File" و "Control" مُسرِّعات خاصة بها.

التدويل Internationalization

سنناقش خلال ما هو متبقي من هذا المقال موضوعين يُمكِن تطبيقهما على جميع البرامج وليس على برامج واجهات المُستخدِم الرسومية فقط؛ إذ لا نُطبِّقهما عادةً بالبرامج الصغيرة، ولكنهما مهمان جدًا للتطبيقات الضخمة.

يُقصَد بالتدويل كتابة البرنامج كتابةً يَسهُل معها تهيئته ليَعمَل بمختلف أنحاء العالم. تُستخدَم كلمة "I18n" عادةً للإشارة إلى التدويل، إذ يمثّل "18" عدد الأحرف بين الحرف الأول "I" والحرف الأخير "n" من كلمة "Internationalization". في المقابل، يُطلَق على مهمة تهيئة البرنامج ليَعمَل بمنطقة معينة اسم التوطين localization؛ في حين يُطلَق اسم المحليات locales على تلك المناطق. تختلف المحليات عن بعضها بجوانب كثيرة، مثل نوع العملة المُستخدَمة، والصيغة المُستخدَمة لكتابة الأعداد والتواريخ، ولكن الاختلاف الأبرز والأكثر وضوحًا هو اللغة. سنناقش هنا طريقة كتابة البرامج لنتمكَّن من ترجمتها إلى لغاتٍ أخرى بسهولة.

تتمحور الفكرة الأساسية في عدم كتابة السلاسل النصية التي تَظهَر للمُستخدِم ضمن الشيفرة المصدرية للبرنامج؛ لأننا لو فعلنا ذلك، فسنحتاج إلى مترجم قادر على البحث داخل الشيفرة بأكملها، ويَستبدِل كل سلسلةٍ نصية بترجمتها، وسنضطّر بعدها إلى إعادة تصريف البرنامج. في المقابل، إذا أردنا كتابة برنامج يدعم خاصية التدويل، فلا بُدّ أن نُخزِّن كل السلاسل النصية معًا ضمن ملفٍ واحد أو أكثر على نحوٍ منفصلٍ تمامًا عن ملفات الشيفرة المكتوبة بلغة جافا، وبالتالي نستطيع أن نعثر عليهم بسهولة ونترجمهم. ونظرًا لأننا لم نُعدِّل ملفات الشيفرة، فإننا لا نحتاج حتى إلى إعادة تصريف البرنامج.

والآن لكي نُنفِّذ تلك الفكرة، علينا أن نُخزِّن السلاسل النصية داخل ملف خاصيات properties file واحدٍ أو أكثر؛ وهو ملفٌ بسيط يحتوي على قائمة أزواج مفتاح / قيمة، ولأن الغرض منها هنا هو الترجمة، فستُشير القيم إلى السلاسل النصية المعروضة للمُستخدِم، وهي ببساطة السلاسل النصية التي ينبغي أن نترجمها؛ أما المفاتيح فهي أيضًا سلاسلٌ نصية، ولكن لا حاجة لترجمتها، لأنها لن تَظهَر أبدًا للمُستخدِم.

نظرًا لأننا لن نُعدِّل المفاتيح، يُمكِننا استخدامها ضمن الشيفرة المصدرية. وينبغي عمومًا أن يُقابِل كلُّ سلسلةٍ نصيةٍ مفتاحًا فريدًا يُعرِّف هويتها، وبناءً على ذلك، يستطيع البرنامج استخدام المفاتيح للعثور على ما يقابلها من سلاسل نصية من ملف الخاصيات؛ أي يحتاج البرنامج إلى معرفة المفاتيح فقط، بينما يرى المُستخدِم القيم المقابلة لتلك المفاتيح. وعند ترجمة ملف الخاصيات، سيتمكَّن المُستخدِم من رؤية قيمٍ مختلفة لنفس المفاتيح.

تُكتَب أزواج مفتاح / قيمة بملفات الخاصيات على النحو التالي:

key.string=value string

لا ينبغي أن تحتوي السلسلة النصية المُمثِّلة للمفتاح قبل إشارة التساوي على أيّ فراغات، إذ تُستخدَم عادةً النقاط للفصل بين الكلمات المُكوِّنة لها. في المقابل، بإمكان السلسلة النصية المُمثِّلة للقيمة أن تحتوي على مسافات أو أيّ محارف أخرى. إذا انتهى السطر بمحرف "\"، ستستمر القيمة إلى السطر التالي، وتُهمَل الفراغات الموجودة ببداية ذلك السطر في تلك الحالة. لسوء الحظ، يمكن أن يحتوي ملف الخاصيات على محارف من مجموعة محارف ASCII فقط؛ إذ تدعم تلك المجموعة الأحرف الأبجدية الإنجليزية فقط. ومع ذلك، بإمكان القيمة أن تتضمَّن محارف UNICODE عشوائية، مما يَعنِي ضرورة تشفير المحارف من خارج مجموعة محارف ASCII.

يتضمَّن JDK البرنامج native2ascii، الذي يَستطيع تحويل الملفات التي تَستخدِم محارف من خارج مجموعة محارف ASCII إلى ملف خاصيات بصيغةٍ مناسبة.

لنفترض أننا نريد عرض سلسلة نصية للمُستخدِم، مثل اسم أمر ضمن قائمة ببرنامج معين. عندها، سيحتوي إذًا ملف الخاصيات على زوج مفتاح/قيمة على النحو التالي:

menu.saveimage=Save PNG Image...

إذ أن "Save PNG Image…‎" هي السلسلة النصية التي ينبغي أن تَظهَر بالقائمة. والآن، يستطيع البرنامج استخدام المفتاح "menu.saveimage" للعثور على قيمة المفتاح، ومن ثُمَّ يَستخدِمها مثل نص عنصر القائمة. تُجرَى عملية العثور تلك بواسطة الصنف ResourceBundle، إذ يمكنه استرجاع ملفات الخاصيات واستخدامها.

قد تحتوي السلسلة النصية المعروضة للمُستخدِم -في بعض الأحيان- على سلاسل نصية فرعية لا يُمكِن تحديدها قبل تشغيل البرنامج، مثل اسم ملف معين؛ فقد يرغب البرنامج بإبلاغ المُستخدِم مثلًابالرسالة التالية "Sorry, the file, filename, cannot be loaded"، علمًا أن filename هو اسم ملفٍ اختاره المُستخدِم أثناء تشغيل البرنامج. لمعالجة تلك الحالة، بإمكان القيم -بملفات الخاصيات- أن تتضمَّن عنصرًا زائفًا placeholder؛ إذ يُستبدَل ذلك العنصر بسلسلةٍ نصيةٍ يُحدِّدها البرنامج بعد تشغيله، ويُكْتَب ذلك العنصر الزائف على النحو التالي "{0}"، أو "{1}"، أو "{2}".

بالنسبة لمثال خطأ الملف، قد يحتوي ملف الخاصيات على القيمة التالية:

error.cantLoad=Sorry, the file, {0}, cannot be loaded

يسترجِع البرنامج القيمة المقابلة للمفتاح error.cantLoad، ثم يستبدل اسم الملف الفعلي بالعنصر الزائف "{0}". قد يختلف ترتيب الكلمات عند ترجمة السلسلة النصية، ولكن نظرًا لأننا نَستخدِم عنصرًا زائفًا لتمثيل اسم الملف، سيتمكَّن المترجم من وضع اسم الملف بالمكان النحوي الصحيح بالنسبة للغة المُستخدَمة. في الواقع، لا يُعالِج الصنف ResourceBundle عملية الاستبدال تلك، وإنما يتولَّى الصنف MessageFormat تلك المهمة.

يَستخدِم تطبيق عرض مجموعة ماندلبرو ملف خاصيات، اسمه strings.properties؛ إذ لا بُدّ أن ينتهي اسم أي ملف خاصيات بكلمة ".properties". يقرأ البرنامج أي سلسلة نصية تراها عند تشغيل التطبيق من ذلك الملف.

بَرمَج الكاتب الصنف I18n.java لقراءة قيم المفاتيح، ويحتوي ذلك الصنف على التابع الساكن static التالي:

public static tr( String key, Object... args )

يُعالِج التابع السابق العملية بالكامل؛ إذ يَستقبِل التابع المعامل key الذي يُمثِّل المفتاح الذي ينبغي أن يبحث عنه التابع ضمن ملف الخاصيات strings.properties؛ بينما تُمثِل المعاملات الإضافية القيم التي ينبغي أن تحلّ محل العناصر المزيفة -إن وجدت- بالقيمة المقابلة للمفتاح. تذكّر أن التصريح عن المعامل باستخدام "Object…‎"، وهذا يَعنِي احتمالية تمرير أيّ عددٍ من المعاملات الفعلية بعد المعامل key. ألقِ نظرةً على مقال تعرف على المصفوفات (Arrays) في جافا. تشمل الاستخدامات النموذجية ما يلي:

String saveImageCommandText = I18n.tr( "menu.saveimage" );

String errMess = I18n.tr( "error.cantLoad" , selectedFile.getName() );

في الواقع، سترى استدعاءاتٍ كثيرةً مشابهة ضمن شيفرة تطبيق عرض مجموعة ماندلبرو؛ إذ كُتِبَ الصنف I18n بطريقة عامة لتتمكَّن من استخدامه بأي برنامجٍ آخر، وكل ما تحتاج إليه هو توفير ملف خاصيات على أنه ملف مورد، وأن تُخصِّص اسمه بالملف I18n.java، وأخيرًا أن تضع الصنف ضمن الحزمة الخاصة بك.

يُمكِننا أيضًا استخدام أكثر من ملف خاصيات ضمن نفس البرنامج. فعلى سبيل المثال، قد نُضمِّن نسخةً فرنسية وأخرى يابانية من ملف الخاصيات إلى جانب النسخة الإنجليزية، فإذا كان اسم الملف بالنسخة الإنجليزية هو strings.properties، فينبغي أن تكون أسماء الملفات بالنسختين الفرنسية واليابانية strings_fr.properties و strings_ja.properties؛ إذ تملُك كل لغة ترميزًا مكوَّنًا من حرفين، مثل "fr" و "ja"، ويُستخدَم هذا الترميز باسم ملف الخاصيات الخاص بتلك اللغة.

بدايةً، يَستخدِم البرنامج الاسم البسيط لملف الخاصيات "strings"؛ فإذا كان البرنامج مُشغَّلًا بنظام جافا وكانت اللغة المُفضلة هي الفرنسية، فسيحاول البرنامج أن يُحمِّل ملف خاصيات باسم "strings_fr.properties"؛ وإذا فشل، فسيحاول أن يُحمِّل ملف خاصيات باسم "strings.properties". يَعنِي ذلك، أن البرنامج سيستَخدِم ملف الخاصيات الخاص باللغة الفرنسية في الموضع الفرنسي، وسيستخدِم ملف الخاصيات الخاص باللغة اليابانية في موضع اللغة اليابانية، وأخيرًا، سيَستخدِم ملف الخاصيات الافتراضي في الحالات الأخرى.

الإعدادات المفضلة

تَسمَح غالبية البرامج للمُستخدِم بضبط إعداداته المفضلة؛ إذ تُمثِّل تلك الإعدادات جزءًا من حالة البرنامج، التي ينبغي أن يتذكرها عند تشغيله مرةً أخرى، وليتمكَّن من ذلك، عليه أن يُخزِّنها بملفٍ ضمن المجلد الرئيسي للمُستخدِم، كما أن عليه أن يتمكَّن من تحديد موقعها بعد ذلك.

ينبغي إذًا تسمية الملف بطريقة تُجنِّبنا أيّ تعارضٍ مع أسماء الملفات المُستخدَمة بواسطة البرامج الأخرى. هناك مشكلةٌ أخرى، وهي أننا بتلك الطريقة سنملأ المجلد الرئيسي للمُستخدِم بملفاتٍ لا ينبغي أن يُعرَف بوجودها أساسًا.

تتعامل جافا مع تلك المشاكل بتوفير طريقةٍ قياسية لمعالجة الإعدادات المفضلة؛ إذ تُعرِّف جافا الصنف Preferences ضمن حزمة java.util.prefs، وهذا الصنف هو كلُّ ما تحتاج إليه.

يحتوي ملف تطبيق عرض مجموعة ماندلبرو Main.java على مثالٍ لاستخدام الصنف Preferences. يضبُط المُستخدِم عادةً إعداداته المفضلة بمعظم البرامج عبر صندوق نافذة مُخصَّص، ولكن لا يحتوي تطبيق ماندلبرو على إعدادات يتناسب معها هذا النوع من المعالجة. وبدلًا من ذلك، يُخزِّن البرنامج بعضًا من جوانب حالة البرنامج أوتوماتيكيًا على أنها إعداداتٌ مفضلة؛ وبكل مرة يُشغِّل المُستخدِم بها البرنامج، فإنه يقرأ تلك الإعدادات إن وُجدت؛ وبكل مرة يُغلِق المُستخدِم بها البرنامج، فإنه يُخزِّن تلك الإعدادات. تؤدي الحاجة إلى حفظ الإعدادات المفضلة إلى مشكلةٍ شيقةٍ نوعًا ما.

ينتهي البرنامج بمجرد غلق النافذة، ولكننا نحتاج إلى طريقةٍ لحفظ الإعدادات عند حدوث ذلك، ويَكْمُن الحل عادةً في استخدام الأحداث: يُسجِّل التابع start()‎ المُعرَّف بالصنف Main مستمعًا إلى حدث غلق النافذة، وعند وقوع ذلك الحدث، يُعالِجه معالج الحدث بحفظ الإعدادات المفضلة.

تُخزَّن الإعدادات المفضلة ببرامج جافا بصيغةٍ تعتمد على المنصة المُستخدَمة، وبمكانٍ يعتمد أيضًا على تلك المنصة، ولكن بصفتك مبرمج جافا، فلا حاجة للقلق بشأن ذلك، إذ يعرف نظام التفضيلات بجافا تمامًا مكان تخزين البيانات، ولكن ما يزال هناك مشكلة فصل الإعدادات المفضلة لبرنامج معين عن إعدادات بقية برامج جافا التي قد تكون مُشغَّلة على نفس الحاسوب.

تحلّ جافا تلك المشكلة بنفس الطريقة التي حلّت بها مشكلة تسمية الحزم؛ إذ تُعرَّف ببساطة الإعدادات المفضلة لبرنامج معين من خلال اسم الحزمة الخاصة بالبرنامج مع اختلافٍ بسيط بالترميز. على سبيل المثال، تطبيق عرض مجموعة ماندلبرو مُعرَّف بحزمة edu.hws.eck.mdbfx، وإعداداته مُعرَّفة عبر السلسلة النصية "‎/edu/hws/eck/mdbfx". حلَّ المحرف "/" محلَّ النقاط مع إضافة "/" إلى البداية.

تُخزَّن الإعدادات المفضلة لأي برنامج داخل عقدة node، ويُمكِن استرجاع العقدة المقابلة للسلسلة النصية المُعرِّفة لإعدادات برنامج معين على النحو التالي:

Preferences root = Preferences.userRoot();
Preferences node = root.node(pathName);

يشير المعامل pathname إلى السلسلة النصية المُعرِّفة للعقدة، مثل "‎/edu/hws/eck/mdbfx". تتكوَّن العقدة نفسها من قائمةٍ بسيطة من أزواج مفتاح / قيمة، ويتكوَّن كلٌ من المفتاح والقيمة من سلاسل نصية. في الحقيقة، يستطيع البرنامج أن يُخزِّن أي سلاسل نصية ضمن تلك العقد، فهي تُمثِّل فقط طريقةً للاحتفاظ بالبيانات من عملية تشغيل برنامج لأخرى، ولكن يُحدِّد المفتاح عمومًأ هوية عنصرٍ معيّنٍ من الإعدادات المفضلة، وتكون قيمته المقابلة هي القيمة المفضلة. إذا كان prefnode كائنًا من النوع Preferences، فإنه يحتوي على التابع prefnode.get(key)‎ لاسترجاع القيمة المرتبطة بمفتاح معين، والتابع prefnode.put(key,value)‎ لضبط القيمة الخاصة بمفتاح معين.

يَستخدِم البرنامج "Main.java" الإعدادات المفضلة لتخزين شكل نافذة البرنامج وموضعها، إذ يَسمَح ذلك بالإبقاء على حجم النافذة وشكلها بين عمليات التشغيل المتتالية لنفس البرنامج. وعندما يُشغِّل المُستخدِم البرنامج، سيجد النافذة بنفس المكان الذي تركها فيه آخر مرة شغَّل خلالها البرنامج. يُخزِّن البرنامج أيضًا اسم المجلد الذي كان يحتوي على آخر ملف فتحه المُستخدِم أو حفظه، وهذا في الواقع أمر مهم؛ لأن السلوك الافتراضي لصندوق نافذة فتح ملف هو عرض مجلد العمل الحالي، وهو نادرًا ما سيكون المكان الذي يرغب المُستخدِم بحفظ ملفات البرنامج به.

بفضل خاصية الإعدادات المفضلة، سيتمكَّن المُستخدِم من الانتقال إلى المجلد الصحيح عند أول تعاملٍ له مع البرنامج، وبعد ذلك سيجد المُستخدِم نفسه بالمجلد الصحيح أوتوماتيكيًا عند استخدامه للبرنامج مرةً أخرى. تستطيع الإطلاع على الشيفرة المصدرية بالملف Main.java لمزيدٍ من التفاصيل.

يُمكِننا بالتأكيد قول المزيد عن لغة جافا وعن البرمجة عمومًأ، ولكن هذه السلسلة لا تمثِّل سوى مقدمةً إلى البرمجة باستخدام لغة جافا، وقد حان الوقت لانتهاء رحلتنا. التي نتمنى أنها كانت رحلةً مُبهِجةً لك وأنك قد استفدت منها ببناء أساسٍ قوي تستطيع استخدامه قاعدةً لاكتشافاتك التالية.

ترجمة -بتصرّف- للقسم Section 5: Finishing Touches من فصل Chapter 13: GUI Programming Continued من كتاب Introduction to Programming Using Java.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

الحمد لله الأن أنهيت آخر تمرين بالفصل 13 وبهذا أنهي كامل الكتاب.

كانت رحلة طويلة ولكن ممتعة و عظيمة , لقد خَتم الكاتب كتابه بسطرين ويا ليته أطال.

هذا الكتاب واحد من أفضل الكتب التعليمية التي قرأتها , كتاب متدرج غني بالأمثة ولغة خطابه مناسبة لمستوى المتلقي , وأما تمارين آخر كل فصل هذه لوحدها حكاية , تمارين بغاية الفائدة وأنا أرى أنها شكلت نصف فائدة الكتاب.

أما الكاتب فما شاء الله مخلص جداً في عمله , الكتاب يحوي عدد كبير من الأمثلة هذا غير المكتبات التي كتبها للكتاب فقط , وحلول التمارين المفصلة , والاجتهاد الكبير في توضيح كل صغيرة وكبيرة , هذا غير أنه ما زال قائماً على تحديث الكتاب و تحسينه وهذا المجهود كله مجاني و تحت رخصة المشاع الابداعي.

عمل يشكر عليه وأسأل الله له الهداية.

وأشكركم على هذه الترجمة الممتازة , لقد قرأت جزء من الكتاب بلغته و جزء من هنا واستفدت من هذه الترجمة جداً.

أتمنى لو تجمع هذه المقالات في ملف واحد وتنشر ككتاب لكي لا تضيع وتبقى مغمورة قد لا ينتبه لها إلا النذر اليسير.

أخيراً أنبه كل من يريد قراءة الكتاب إلى العودة للموقع الرسمي للكتاب و حل التمارين لأنها في غاية الأهمية.

تم التعديل في بواسطة اسماعيل Ismail
رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ On 08/12/2022 at 11:19 قال اسماعيل Ismail:

الحمد لله الأن أنهيت آخر تمرين بالفصل 13 وبهذا أنهي كامل الكتاب.

كانت رحلة طويلة ولكن ممتعة و عظيمة , لقد خَتم الكاتب كتابه بسطرين ويا ليته أطال.

هذا الكتاب واحد من أفضل الكتب التعليمية التي قرأتها , كتاب متدرج غني بالأمثة ولغة خطابه مناسبة لمستوى المتلقي , وأما تمارين آخر كل فصل هذه لوحدها حكاية , تمارين بغاية الفائدة وأنا أرى أنها شكلت نصف فائدة الكتاب.

أما الكاتب فما شاء الله مخلص جداً في عمله , الكتاب يحوي عدد كبير من الأمثلة هذا غير المكتبات التي كتبها للكتاب فقط , وحلول التمارين المفصلة , والاجتهاد الكبير في توضيح كل صغيرة وكبيرة , هذا غير أنه ما زال قائماً على تحديث الكتاب و تحسينه وهذا المجهود كله مجاني و تحت رخصة المشاع الابداعي.

عمل يشكر عليه وأسأل الله له الهداية.

وأشكركم على هذه الترجمة الممتازة , لقد قرأت جزء من الكتاب بلغته و جزء من هنا واستفدت من هذه الترجمة جداً.

أتمنى لو تجمع هذه المقالات في ملف واحد وتنشر ككتاب لكي لا تضيع وتبقى مغمورة قد لا ينتبه لها إلا النذر اليسير.

أخيراً أنبه كل من يريد قراءة الكتاب إلى العودة للموقع الرسمي للكتاب و حل التمارين لأنها في غاية الأهمية.

 مرحبًا @اسماعيل Ismailيسعدنا جدًا أن الكتاب قد نال إعجابك وأنك قد حصلت على الفائدة المرجوة منه.

بخصوص جمع المقالات في ملف واحد على شكل كتاب، ففي الواقع هذا مخطط له منذ فترة وسيكون متاحًا قريبًا جدًا؛ ليس فقط المقالات، وإنما محتوى المقالات + ترجمة التمارين بتفاصيلها من أجل الحصول على استفادة أوسع بلغتنا الأم "العربية".

 

مشكور على اهتمامك بتحسين تجربة الوصول إلى محتوى السلسلة للقارئ العربي، ونتمنى لك تجربة قراءة واستفادة ممتعة دائمًا على أكاديمية حسوب.

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...