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

الخاصيات والارتباطات في جافا


رضوى العربي

تعتمد برمجة واجهات المُستخدِم الرسومية GUI على اِستخدَام الأحداث events استخدامًا كبيرًا، متضمنةً الأحداث منخفضة المستوى، مثل أحداث لوحة المفاتيح والفأرة، والأحداث عالية المستوى، مثل تلك الناتجة عن اختيار قائمة أو ضبط قيمة مزلاج. تَنتُج أحداث المزلاج -كما رأينا في في مقال التخطيط الأساسي لواجهة المستخدم في مكتبة جافا إف إكس JavaFX - من خاصية قابلة للمراقبة observable مُعرَّفة بذلك المزلاج، وبالتالي إذا أردنا الإستجابة إلى تغيُّر قيمة مزلاج sldr، فعلينا أن نُسجِّل مُستمِعًا listener بالخاصية valueProperty المُعرَّفة به على النحو التالي:

sldr.valueProperty().addListener( . . . );

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

في الواقع، قيمة valueProperty للمزلاج هي كائنٌ object من النوع DoubleProperty، إذ تُغلِّف كائنات هذا النوع قيمةً من النوع double، وتحتوي على التابعين get()‎ و set()‎ لاسترجاع تلك القيمة وتعديلها. علاوةً على ذلك، تُعدّ تلك القيمة قيمةً قابلةً للمراقبة، مما يَعنِي أنها تُولِّد أحداثًا عندما تتغيَّر، كما أنها تُعَد أيضًا خاصيةً قابلةً للارتباط bindable؛ إذ يستطيع هذا النوع من الخاصيات أن يرتبط بخاصياتٍ أخرى من نفس النوع، وتُجبَر الخاصيات المُرتبطِة بتلك الطريقة على أن يكون لها نفس القيمة.

تَستخدِم واجهة تطوير تطبيقات مكتبة JavaFX الخاصيات القابلة للارتباط بكثرة، وسنتعلّم بهذا المقال كيفية استخدامها والدور الذي تلعبه.

الأصناف التي سنناقشها بهذا المقال مُعرَّفةٌ بحزمة javafx.beans وحزمها الفرعية، ومع ذلك لن نضطّر لاستيراد import تلك الأصناف من الحزم الموجودة بها إلى برامج JavaFX؛ لأننا غالبًا ما نعتمد على كائناتٍ موجودة بالفعل.

القيم القابلة للمراقبة Observable

تُعدّ الكثير من متغيرات النسخ instance variables المُعرَّفة بكائنات مكتبة JavaFX، قيمًا قابلةً للمراقبة، أي أنها تُولِّد أحداثًا، حتى أن أغلبها قابلٌ للارتباط أيضًا؛ إذ يُعدّ كلٌ من عرض حاوية canvas وارتفاعها مثلًا، قيمًا قابلةً للمراقبة من النوع DoubleProperty؛ كما يُعدّ النص الموجود بحقل نصي أو عنوان label قيمةً قابلةً للمراقبة من النوع StringProperty؛ إلى جانب أن قائمة أبناء حاوية من النوع Pane تُعَد قيمةً قابلةً للمراقبة من النوع ObservableList<Node>‎. ويتكَّون مربع الاختيار checkbox من خاصية قابلة للمراقبة من النوع BooleanProperty، والتي تشير إلى اختيار المربع؛ وفي مثال آخر، نجد لون النص المعروض بعنوان، فهو أيضًا قيمةٌ قابلة للمراقبة من النوع ObjectProperty<Paint>‎.

تُولِّد القيم القابلة للمراقبة نوعين من الأحداث هما: الحدث الأول، وهو حدث تغيُّر القيمة، وفي تلك الحالة لا بُدّ أن يُنفِّذ implement معالج الحدث واجهة نوع الدالة ChangeListener<T>‎ ذات المعاملات غير مُحدّدة النوع parameterized، والتي تحتوي على التابع changed(target,oldValue,newValue)‎؛ إذ يُمثِّل المعامل الأول الكائن القابل للمراقبة الذي تغيَّرت قيمته، ويحتوي المعامل الثاني على القيمة السابقة؛ في حين يحتوي المعامل الثالث على القيمة الجديدة. على سبيل المثال، إذا أدرنا عرض القيمة الحالية لمزلاج مثل نص عنوان، فعندها ينبغي أن نُغيّر النص عندما تتغير قيمة المزلاج، بحيث تتوافق قيمتهما دائمًا. ولهذا، علينا أن نُسجِّل مُستمعًا لحدث تغيُّر القيمة لكي يَضبُط نص العنوان ليتوافق مع قيمة المزلاج، على النحو التالي:

slider.valueProperty().addListener( 
            (t,oldVal,newVal) -> label.setText("Slider Value: " + newVal) );

سنرى لاحقًا أن هناك طرائقٌ أفضل لإنجاز نفس المهمة.

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

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

بالنسبة لمكتبة JavaFX، يُفضَّل عادةً تهيئة مُستمِع إلى حدث انعدام الصلاحية بدلًا من حدث تغيُّر القيمة.

يُعرِّف مستمعو الأحداث من النوع InvalidationListener تابعًا وحيدًا، هو التابع invalidated(obv)‎، إذ يُشير معامل ذلك التابع إلى الكائن القابل للمراقبة الذي أصبحت قيمته غير صالحة. ألقِ نظرةً على الشيفرة التالية على سبيل المثال، مع الملاحظة أن sayHello هو مربع اختيار:

sayHello.selectedProperty().addListener( e -> {
    if (sayHello.isSelected())
        label.settext("Hello");
    else
        label.setText("Goodbye");
});

تُسجِّل الشيفرة بالمثال السابق مستمع حدث من النوع InvalidationListener. ويُمكِننا تنفيذ نفس الأمر باستدعاء sayHello.setOnAction()‎، ولكن هنالك اختلاف: يُستدعَى مُستمعِو الأحداث من النوع ActionListener، فقط إذا كانت حالة مربع الاختيار قد تغيَّرت بواسطة المُستخدِم؛ بينما يُستدعَى مُستمعِي أحداث انعدام الصلاحية متى تغيّرت القيمة، بما في ذلك التغييرات المُجراة نتيجة لاستدعاء sayHello.setSelected()‎.

لاحِظ أننا كنا قد اِستخدَمنا نفس التابع addListener()‎ لتسجيل مستمعي الأحداث من النوع ChangeListener، إذ يَستطيع المُصرِّف compiler أن يُفرِّق بينهما حتى لو مرَّرنا المُستمِع بهيئة تعبير لامدا lambda expression؛ لأن تعبير لامدا لحدث من النوع ChangeListener يَستقبِل ثلاثة معاملات؛ في حين أن تعبير لامدا لحدث من النوع InvalidationListener، يَستقبِل معاملًا واحدًا.

يَطرَح ذلك السؤال التالي: ماذا سيحدث لو كانت قيمة الخاصية selectedProperty بمربع الاختيار sayHello قد أصبحت غير صالحةٍ فقط دون أن تتغير؟ هل سيعيد الاستدعاء sayHello.isSelected()‎ القيمة الحالية غير الصالحة أم القيمة الجديدة؟ في الحقيقة، سيؤدي استدعاء sayHello.isSelected()‎ إلى تحصيل القيمة الجديدة وإعادتها. لا يُمكِن عمومًا قراءة القيم غير الصالحة نهائيًا؛ إذ تؤدي أي محاولة لقراءتها إلى إعادة تحصيلها ما يَنتُج عنه حساب القيمة الجديدة.

بالنسبة لمكتبة JavaFX، يُمكِن الوصول إلى الخاصيات القابلة للمراقبة ضمن كائنٍ ما باستدعاء توابع نسخ تنتهي أسماؤها بكلمة "Property"، إذ يُستخدَم الاستدعاء slider.valueProperty()‎ مثلًا، للوصول إلى خاصية القيمة بمزلاج؛ بينما يُستخدَم الاستدعاء label.textProperty()‎ للوصول إلى خاصية النص بعنوان. ليست جميع القيم القابلة للقراءة بكائنات مكتبة JavaFx خاصيات؛ في حين تُعدّ الخاصيات القابلة للمراقبة قابلةً للارتباط أيضًا -كما سنرى بالمقال التالي-؛ كما لا تُعدّ القيم البسيطة القابلة للمراقبة قابلة للارتباط، مع أنها ما تزال تُولِّد حدثي تغيُّر القيمة وانعدام الصلاحية، بل ويُمكِن تسجيل مستمعي أحداث للاستجابة إلى التغيُّرات التي تحدث بقيمتها.

الخاصيات القابلة للارتباط Bindable

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

Label message = new Label("Never Seen");
TextField input = new TextField("Type Here!");
message.textProperty().bind( input.textProperty() );

يُجبِر التابع bind()‎ قيمة message.textProperty()‎ بأن تكون هي نفسها قيمة input.textProperty()‎. وبمجرد تنفيذ التابع bind()‎، يُنسَخ النص الموجود بالحقل النصي إلى العنوان؛ لكي لا يرى المُستخدِم نص العنوان المبدئي (وهو "Never Seen" بالمثال السابق). عند حدوث أي تغيير على النص الموجود بالحقل النصي أثناء تشغيل البرنامج، يُطبَّق ذلك التغيير على العنوان تلقائيًا، سواءً كان ذلك التغيير نتيجةً لكتابة المُستخدِم بالحقل النصي، أو نتيجةً لاستدعائنا للتابع input.setText()‎ برمجيًا. لاحِظ أن مفهوم الارتباط مُنفَّذ داخليًا باستخدام الأحداث ومستمعي الأحداث، فالهدف منه هو ألا نضطّر لتهيئة مستمعي الأحداث يدويًا، وإنما نَستدعِي فقط التابع bind()‎ لتهيئة كل شيء أوتوماتيكيًا.

عندما نَستخدِم bind()‎ لربط خاصية بخاصيةٍ أخرى، لا يُمكِننا عندها تعديل قيمة الخاصية المُرتَبطِة تعديلًا مباشرًا، ولذلك يؤدي أي استدعاء للتابع message.setText()‎ بالمثال السابق إلى حدوث استثناء exception. وبالطبع، بإمكان أي خاصيةٍ الارتباط بخاصيةٍ واحدةٍ أخرى فقط بأي لحظة. يُمكِننا استخدام التابع unbind()‎ الذي لا يَستقبِل أي معاملات، وذلك لإلغاء ارتباطٍ معين على النحو التالي:

message.textProperty().unbind();

يحتوي البرنامج التوضيحي BoundPropertyDemo.java على أمثلةٍ متعددة عن الخاصيات المرتبطة. ستَجِد كما في المثال السابق، بأن خاصية النص الموجودة بعنوان مرتبطةً بخاصية النص الموجودة بحقلٍ نصي، بحيث تؤدي الكتابة في الحقل النصي إلى تعديل نص العنوان. تعرِض الصورة التالية لقطة شاشة من البرنامج:

001Bound_Property_Demo.png

يُمثِّل العنوان الموجود على يمين أسفل النافذة مثالًا آخرًا عن الارتباط، إذ يَعرِض العنوان قيمة المزلاج، وسيتغير نص العنوان بمجرد أن يَضبُط المُستخدِم قيمة المزلاج. كما ناقشنا بالأعلى، يُمكِننا تنفيذ ذلك بتسجيل مستمعٍ إلى خاصية المزلاج valueProperty، ولكنها مُنفَّذةٌ بهذا المثال باستخدام الارتباط. ومع ذلك توجد مشكلة، إذ أن خاصية textProprety الموجودة بالعنوان من النوع StringProperty، بينما خاصية valueProperty الموجودة بالمزلاج من النوع DoubleProperty، ولذلك لا يُمكِن ربطهما مباشرةً؛ لأن الإرتباط يَعمَل بنجاح فقط إذا كانت كلتاهما من نفس النوع. لحسن الحظ، يحتوي النوع DoubleProperty على التابع asString()‎ الذي يُحوِّل الخاصية إلى النوع string، أي لو كان slider مزلاجٌ من النوع Slider، فسيُمثِّل مايلي خاصيةً من النوع string تحتوي على التمثيل النصي لقيمة المزلاج العددية التي هي بالأساس من النوع double:

slider.valueProperty().asString()

وبالتالي يُمكِننا الآن أن نربط خاصية textProprety الموجودة بالعنوان بتلك الخاصية النصية، كما يمكن للتابع asString()‎ أن يَستقبِل صيغة سلسلة تنسيق format string، مثل تلك التي يَستخدِمها التابع System.out.printf، لتنسيق قيمة المزلاج العددية. وقد اِستخدَمنا المتغير sliderVal لتمثيل العنوان بهذا البرنامج، وربطنا خاصيته النصية بكتابة ما يلي:

sliderVal.textProperty().bind( 
                   slider.valueProperty().asString("Slider Value: %1.2f") );

إذا شئنا الدقة، يُعدّ التعبير slider.valueProperty().asString()‎ من النوع StringBinding وليس StringProperty، ولكننا سنتجاهل التمييز بينهما هنا لأنه ليس مهمًا.

تحتوي الكائنات المُمثِّلة لخاصيات Property على توابع كثيرة للتحويل بين الأنواع بالإضافة إلى توابع لإجراء بعض العمليات الأخرى، إذ يُعرِّف النوع DoubleProperty التابع lessThan(number)‎، الذي يعيد خاصيةً من النوع boolean قيمتها تساوي true عندما تكون قيمة النوعDoubleProperty أصغر من عددٍ معين. فمثلًا، إذا كان btn من النوع Button، فإنه يحتوي على خاصية btn.disableProperty()‎ من النوع BooleanProperty للإشارة إلى ما إذا كان الزر مُعطَّلًا أم لا؛ فإذا أردنا أن نُعطِّل الزر عندما تصبح قيمة مزلاج معين أقل من 20، يُمكِننا ربط خاصية تعطيل الزر على النحو التالي:

btn.disableProperty().bind( slider.valueProperty().lessThan(20) );

تتوفَّر توابع أخرى مشابهة، مثل greaterThan()‎ و lessThanOrEqual()‎ و isNotEqualTo()‎ وغيرها، كما تتوفَّر توابع أخرى لإجراء بعض العمليات الحسابية. على سبيل المثال:

slider.valueProperty().multiply(2)

يُمثِّل التعبير السابق خاصيةً من النوع double قيمتها تساوي ضعف قيمة المزلاج.

يُمكِننا استخدام الصنف When المُعرَّف بحزمة javafx.beans.binding لكي نُطبِّق شيئًا مثل المعامل الثلاثي ":?" على خاصيات من النوع boolean، ولكن بقواعد صيغة مختلفة بعض الشيء -ألقِ نظرةً على مقال التعبيرات expressions في جافا-. على سبيل المثال، إذا كان boolProp يُمثِّل خاصيةً من النوع boolean، وكان trueVal و falseVal متغيرين يحتويان على أي قيم بشرط أن تكون من نفس النوع، فإن:

new When(boolProp).then(trueVal).otherwise(falseVal)

يُمثِّل خاصيةً من نفس نوع المتغيرين trueVal و falseVal، وتكون قيمتها مساويةً لقيمة trueVal إذا كان boolProp يحتوي على القيمة true، أو القيمة falseVal إذا كان boolProp يحتوي على القيمة false.

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

label.textProperty().bind( 
     new When(sayHello.selectedProperty()).then("Hello").otherwise("Goodbye")
);

يُمثِل المعامل المُمرَّر بالتعليمة When(sayHello.selectedProperty())‎ خاصيةً من النوع boolean. نظرًا لأن كُلًا من "Hello" و "Goodbye" قيمٌ من النوع String، فستكون الخاصية الناتجة من التعبير بالكامل سلسلةً نصيةً من النوع String، وهو ما يتوافق مع نوع الخاصية label.textProperty()‎.

بنفس الأسلوب، يَستخدِم البرنامج BoundPropertyDemo.java مربع اختيار للتحكُّم بلون الخلفية الخاص بعنوان كبير باستخدام الارتباط مع كائنٍ من النوع When، ويُمكِنك الاطلاع على الشيفرة كاملة لمزيدٍ من التفاصيل.

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

كنا قد ضبطنا حجم النافذة في الأمثلة السابقة التي اِستخدَمنا بها حاوية، ليكون غير قابل للتعديل؛ وذلك لأن حجم الحاوية لا يتغير أوتوماتيكيًا، ومع ذلك، بإمكان البرامج أن تُعدِّل حجم الحاوية من خلال ضبط الخاصيات الممثلة لعرض الحاوية وارتفاعها، وذلك باستدعاء canvas.setWidth(w)‎ و canvas.setHeight(h)‎. هناك حلٌ آخر لضبط حجم الحاوية، إذ يُمكِننا ربط هاتين الخاصيتين بمصدرٍ مناسب؛ لأنهما خاصيتان قابلتان للارتباط من النوع DoubleProperty.

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

canvas.widthProperty().bind( root.widthProperty() ); 
canvas.heightProperty().bind( root.heightProperty() );

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

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

الارتباط ثنائي الاتجاه

يُستخدَم التابع bind()‎ لإنشاء ارتباطاتٍ أحادية الاتجاه؛ بمعنى أنها تَعمَل من اتجاهٍ واحدٍ فقط، وهو ما قد لا يكون مناسبًا في بعض الأحيان. لنفترض مثلًا أنه لدينا مربعي اختيار cb1 و cb2 من النوع CheckBox، ونريدهما أن يكونا متطابقين دائمًا. في الواقع، لا يُمكِننا تحقيق ذلك باستخدام الارتباطات أحادية الاتجاه؛ لأننا لو كتبنا ما يلي مثلًا:

cb2.selectedProperty().bind( cb1.selectedProperty() );

فستصبح حالة مربع الاختيار cb2 مطابقةً لحالة مربع الاختيار cb1، ولكن لن يؤثر تغيير حالة cb2 على حالة cb1، وإنما يؤدي إلى حدوث استثناء؛ لأنه لا يَصِح تعديل خاصيةٍ قد رُبِطَت فعليًا باستخدام bind()‎. يَعنِي ذلك، أنه لو نقر المُستخدِم على مربع الاختيار cb2، فسيحدث استثناء، وذلك لأن مربع الاختيار سيحاول أن يُغيّر حالته.

يَكْمُن الحل في ما يُعرَف باسم الارتباط ثنائي الاتجاه bidirectional binding؛ فعند ارتباط خاصيتين ارتباطًا ثنائي الاتجاه، عندها يُمكِن لقيمة أيٍّ منهما أن تتغير، وعندها تتغيّر قيمة الأخرى أوتوماتيكيًا إلى نفس القيمة. يُستخدَم التابع bindBidirectional()‎ لإنشاء هذا النوع من الارتباط، فيُمكِننا مثلًا كتابة ما يَلِي لربط مربعي الاختيار:

cb2.selectedProperty().bindBidirectional( cb1.selectedProperty() );

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

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

Menu colorMenu = new Menu("Color");

Color[] colors = { Color.BLACK, Color.RED, Color.GREEN, Color.BLUE };
String[] colorNames = { "Black", "Red", "Green", "Blue" };

ToggleGroup colorGroup = new ToggleGroup();

for (int i = 0; i < colors.length; i++) {
    // أنشِئ عنصر قائمة وزر انتقاء مقابل

    RadioButton button = new RadioButton(colorNames[i]);
    RadioMenuItem menuItem = new RadioMenuItem(colorNames[i]);

    button.selectedProperty().bindBidirectional( menuItem.selectedProperty() );

    menuItem.setToggleGroup(colorGroup);

        // 1 
    menuItem.setUserData(colors[i]);

    right.getChildren().add(button);    // أضف زرًا إلى الحاوية
    colorMenu.getItems().add(menuItem); // أضف عنصر قائمة إلى القائمة
    if (i == 0)
        menuItem.setSelected(true);
}

colorGroup.selectedToggleProperty().addListener( e -> {
           // 2
    Toggle t = colorGroup.getSelectedToggle();
    if (t != null) {
            // 3
        Color c = (Color)t.getUserData();
        message.setTextFill(c);
    }
});

حيث أن:

  • [1] تعني: لاحِظ استخدام UserData لتخزين الكائن المُمثِّل للون عنصر القائمة لحاجتنا إليه لاحقًا.
  • [2] تعني: استمع إلى التغييرات الواقعة بالخاصية selectedToggleProperty المُعرَّفة بالصنف ToggleGroup؛ لكي تتمكَّن من ضبط لون العنوان بما يتوافق مع عنصر القائمة الواقع عليه الاختيار.
  • [3] بمعنى: يُمثِّل t العنصر المُختار من النوع RadioMenuItem، إذ يمكنك اِسترجاع اللون من UserData واستخدامه لضبط لون النص. ربما تُصبِح قيمة selectedToggleProperty فارغةً بلحظة معينة، لأن أحدهما وقع عليه الاختيار والآخر غير مختار.

تُضاف عناصر القائمة إلى كائنٍ من النوع ToggleGroup -ألقِ نظرةً على مقال مكونات التحكم البسيطة في واجهة المستخدم في مكتبة جافا إف إكس JavaFX-، بينما لا تُضاف أزرار الانتقاء إليه. والآن، إذا نقر المُستخدِم على إحدى أزرار الانتقاء التي لم تكن قيد الاختيار، فستتغير حالة عنصر القائمة المكافئ إلى وضع الاختيار، ولكن قبل أن يحدث ذلك، سيُغير كائن النوع ToggleGroup حالة عنصر القائمة المختار حاليًا إلى وضع عدم الاختيار، وهو ما يؤدي إلى تغيير حالة زر الانتقاء المربوط بعنصر القائمة ذاك إلى وضع عدم الاختيار أيضًا، وبالنهاية، ستتغير حالة كلٍ من زري الانتقاء وعنصري القائمة.

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

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

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

اقرأ أيضًا


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

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

لا توجد أية تعليقات بعد



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...