الربط والأحداث والتنسيق في JavaFX

الارتباطات المحسوبة والطلاقة

18 دقيقة الدرس 3 من 12

الارتباطات المحسوبة والطلاقة

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

يوفّر JavaFX واجهتَين برمجيتَين تتكاملان: فئة الأداة المساعدة Bindings وواجهة الطلاقة (method-chaining) المضمَّنة مباشرةً في أنواع الخصائص. إتقان كلتيهما — ومعرفة متى تستخدم أيًّا منهما — هو المهارة الأساسية في هذا الدرس.

فئة Bindings المساعدة

javafx.beans.binding.Bindings مصنعٌ من ميثودات ثابتة تبني كائنات الارتباط. تُعيد كل ميثود كائن Binding<T> (أو نوعًا فرعيًا محددًا مثل DoubleBinding أو StringBinding أو BooleanBinding) يحتفظ بالنتيجة المحسوبة ويُبطلها تلقائيًا.

تخيّل نموذج عربة تسوق حيث سعر الوحدة والكمية هما DoubleProperty وIntegerProperty على التوالي، ويجب أن يساوي الإجمالي دائمًا حاصل ضربهما:

import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; public class Cart { private final DoubleProperty unitPrice = new SimpleDoubleProperty(9.99); private final IntegerProperty quantity = new SimpleIntegerProperty(1); // ارتباط محسوب — يتحدث عند تغيّر unitPrice أو quantity private final DoubleBinding total = Bindings.multiply(unitPrice, quantity); public static void main(String[] args) { Cart cart = new Cart(); System.out.println(cart.total.get()); // 9.99 cart.quantity.set(3); System.out.println(cart.total.get()); // 29.97 cart.unitPrice.set(5.00); System.out.println(cart.total.get()); // 15.0 } }

لاحظ أنك لم تأمر total بإعادة الحساب قط — فهو يتتبع اعتماداته (unitPrice وquantity) تلقائيًا. الارتباط كسول: يُعيد الحساب فقط عند استدعاء get() بعد تغيّر أحد الاعتمادات.

تسلسل الارتباطات المحسوبة

تبرز القوة الحقيقية عند تسلسل العمليات. افترض أن التسمية يجب أن تعرض "Total: $29.97":

import javafx.beans.binding.StringBinding; StringBinding label = Bindings.concat( "Total: $", Bindings.format("%.2f", total) // تنسيق DoubleBinding كسلسلة نصية ); // ربط textProperty لتسمية مباشرةً Label totalLabel = new Label(); totalLabel.textProperty().bind(label);

في كل مرة تتغيّر فيها unitPrice أو quantity، يتجدد نص التسمية تلقائيًا — بلا معالجات أحداث ولا تحديثات يدوية.

واجهة الطلاقة

مصنع Bindings صريح ومقروء، لكن للعمليات الحسابية البسيطة والمنطق البولياني تكون واجهة الطلاقة أكثر إيجازًا. تكشف كل خاصية رقمية وارتباط عن ميثودات مثل add() وsubtract() وmultiply() وdivide() وnegate()، وعوامل المقارنة (greaterThan() وisEqualTo()، إلخ).

إعادة كتابة مثال الإجمالي بواجهة الطلاقة:

// أسلوب الطلاقة — يُقرأ كعمليات حسابية تقريبًا DoubleBinding total = unitPrice.multiply(quantity); // تسلسل إضافي: تطبيق خصم 10% إذا كانت الكمية >= 5 DoubleBinding discounted = Bindings.when(quantity.greaterThanOrEqualTo(5)) .then(total.multiply(0.90)) .otherwise(total);

Bindings.when(...).then(...).otherwise(...) هو المعادل الارتباطي لعامل الثلاثي. الفروع الثلاثة كلها عناصر قابلة للمراقبة، لذا يكون التعبير بأكمله تفاعليًا بالكامل.

الطلاقة مقابل مصنع Bindings: كلاهما ينتج كائنات ارتباط متطابقة داخليًا. استخدم أسلوب الطلاقة للعمليات الحسابية والشروط البسيطة؛ واستخدم مصنع Bindings لتنسيق السلاسل النصية وconcat وselect (سلاسل الخصائص المتداخلة) وكل ما يُقرأ بوضوح أكبر كعملية مسمّاة.

الارتباطات المخصصة عبر createDoubleBinding

أحيانًا لا تكفي العمليات المدمجة — تحتاج إلى تشغيل منطقك الخاص. استخدم Bindings.createDoubleBinding (وبدائلها createStringBinding وcreateBooleanBinding وcreateObjectBinding) لتوفير Callable والإعلان الصريح عن اعتماداته:

import javafx.beans.binding.Bindings; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; DoubleProperty radius = new SimpleDoubleProperty(5.0); // المساحة = π * r² (معادلة مخصصة) DoubleBinding circleArea = Bindings.createDoubleBinding( () -> Math.PI * radius.get() * radius.get(), radius // الاعتماد: أبطل عند تغيّر radius ); System.out.println(circleArea.get()); // 78.539... radius.set(10.0); System.out.println(circleArea.get()); // 314.159...

الوسيط الثاني (وأي وسطاء إضافية varargs) يُدرج كل Observable تقرأ منه اللامبدا. إن نسيت إدراج أحد الاعتمادات، لن يُبطَل الارتباط عند تغيّر تلك القيمة — خطأ خفي صعب التشخيص.

أعلن دائمًا عن جميع الاعتمادات. إذا كانت Callable تقرأ a.get() وb.get()، مرّر كلًّا من a وb وسيطَين للاعتماد. إغفال أحدهما يعني أن الارتباط يُعيد قيمة قديمة بصمت بعد تغيّر تلك الخاصية.

الربط بحالة تعطيل الزر

نمط شائع في الواجهات: زر إرسال يُعطَّل عندما يكون حقل نصي مطلوب فارغًا. باستخدام ارتباط بولياني يصبح هذا سطرًا واحدًا:

TextField nameField = new TextField(); Button submitBtn = new Button("Submit"); // ربط disable بالشرط "النص فارغ" submitBtn.disableProperty().bind(nameField.textProperty().isEmpty());

تُعيد isEmpty() على StringExpression كائن BooleanBinding قيمته true حين تكون السلسلة بلا محارف. تتتبع الآن حالة تمكين الزر الحقلَ تلقائيًا — بلا KeyListener ولا معالج تغيير.

فضّل الارتباط على المستمعين للحالة المحسوبة. الارتباط يجعل العلاقة بين النموذج والعرض تصريحية: تُحدد القاعدة مرة واحدة وتُطبّقها الإطار إلى الأبد. المستمعون يجعلون العلاقة إجرائية — يجب أن تتذكر تحديث الهدف في كل مسار كود يُغيّر المصدر.

التخلص من الارتباطات

يحتفظ الارتباط بمرجع إلى خصائص مصدره. إذا عاش الارتباط أطول من نطاقه المقصود (مثلًا نموذج طويل الأمد يحتفظ بارتباط لعقدة عرض قصيرة الأمد)، لديك تسريب في الذاكرة. استدع unbind() على الخاصية الهدف قبل التخلص من عقدة المشهد، أو استخدم Bindings.bindBidirectional / unbindBidirectional للارتباطات ثنائية الاتجاه.

// عند إغلاق مربع الحوار، حرّر الارتباط submitBtn.disableProperty().unbind();

مثال عملي: عداد الكلمات المباشر

جمع كل ما سبق: TextArea يُعرض عدد كلماته في تسمية، وزر إرسال يُعطَّل عند وجود أقل من 10 كلمات.

TextArea textArea = new TextArea(); Label wordLabel = new Label(); Button submitBtn = new Button("Submit"); // عدّ الكلمات: تقسيم على المسافات وتصفية الرموز الفارغة StringBinding wordCountStr = Bindings.createStringBinding(() -> { String text = textArea.textProperty().get().trim(); long count = text.isEmpty() ? 0 : Arrays.stream(text.split("\\s+")).filter(w -> !w.isEmpty()).count(); return "Words: " + count; }, textArea.textProperty()); IntegerBinding wordCount = Bindings.createIntegerBinding(() -> { String text = textArea.textProperty().get().trim(); return text.isEmpty() ? 0 : (int) Arrays.stream(text.split("\\s+")).filter(w -> !w.isEmpty()).count(); }, textArea.textProperty()); wordLabel.textProperty().bind(wordCountStr); submitBtn.disableProperty().bind(wordCount.lessThan(10));

السلسلة التفاعلية بأكملها — من ضغطة مفتاح إلى تحديث التسمية إلى حالة الزر — مُعبَّر عنها كتصريحات بلا كود ترابط إجرائي. هذا هو الأسلوب الذي صُمِّمت ارتباطات JavaFX لتمكينه.

الخلاصة

يتيح لك مصنع Bindings وواجهة الطلاقة تأليف تعبيرات قابلة للمراقبة من تعبيرات أبسط دون كتابة مستمع تغيير واحد. استخدم Bindings.createXxxBinding للمنطق المخصص، وأعلن دائمًا عن كل اعتماد تقرأ منه اللامبدا. اربط حالة الواجهة (disabled، visible، text) مباشرةً بخصائص النموذج للإبقاء على وحدة التحكم مختصرة ومنطقك قابلًا للاختبار. في الدرس القادم ستُطبّق هذه التقنيات على المجموعات القابلة للمراقبة — القوائم والخرائط التي تنتشر تعديلاتها أيضًا عبر رسم الارتباط.