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

خصائص JavaFX

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

خصائص JavaFX

إذا سبق لك بناء تطبيق Swing أو AWT الخام فأنت تعرف الإحباط جيدًا: تُحدّث حقلًا في النموذج ثم تستدعي label.setText(...) يدويًا وتُعيد الرسم وتأمل أنك لم تفوّت أي مسار. تحل JavaFX هذه المشكلة على مستوى اللغة من خلال نظام الخصائص — مجموعة من الأغلفة القابلة للملاحظة حول قيم Java العادية. عندما تتغير القيمة تُخطَر كل الأطراف المهتمة تلقائيًا. يتناول هذا الدرس ما هي الخصائص وكيف تعمل من الداخل والأسلوب الاصطلاحي لكتابة فئة نموذج تستخدمها.

ما هي الخاصية القابلة للملاحظة؟

خاصية JavaFX هي كائن يُغلّف قيمة ويوفر ثلاث قدرات:

  1. وصول للقراءة والكتابة إلى القيمة الأساسية عبر get() و set().
  2. إشعار بالتغيير — تُستدعى المستمعات المسجّلة على الخاصية في كل مرة تتغير فيها القيمة.
  3. دعم الربط — يمكن ربط الخاصية بخاصية أخرى لتبقى متزامنتان دون الحاجة إلى كود مزامنة يدوي.

تأتي JavaFX بتطبيقات جاهزة لكل نوع بدائي ولـ Object:

  • StringProperty / SimpleStringProperty
  • IntegerProperty / SimpleIntegerProperty
  • DoubleProperty / SimpleDoubleProperty
  • BooleanProperty / SimpleBooleanProperty
  • ObjectProperty<T> / SimpleObjectProperty<T>

نمط التسمية ثابت: الواجهة مثل StringProperty تصف العقد، والتطبيق مثل SimpleStringProperty هو ما تُنشئه في فئات النموذج الخاصة بك.

التسلسل الهرمي لخصائص JavaFX

من المفيد فهم التسلسل الهرمي الكامل للأنواع حتى تتمكن من قراءة وثائق JavaFX API دون التباس:

  • Observable — الواجهة الأساسية؛ تستطيع إطلاق أحداث الإبطال.
  • ObservableValue<T> — تضيف getValue() ومستمعات التغيير.
  • ReadOnlyProperty<T> — تضيف getBean() و getName() (بيانات وصفية عن الكائن المالك واسم الحقل).
  • Property<T> — تضيف setValue() وطرق الربط.
  • WritableValue<T> — تضيف setValue() باستقلالية.

الواجهات المتخصصة مثل StringProperty تمتد من كل من Property<String> و WritableStringValue، مضيفةً get() و set(String) المحددين بالنوع لتجنب عمليات الملاكمة.

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

كتابة فئة نموذج بالخصائص

الاتفاقية في JavaFX لفئة النموذج (تُسمى أحيانًا JavaFX Bean) تعكس اتفاقية Java Beans لكنها تضيف محوّلًا ثالثًا — محوّل الخاصية — الذي يُرجع كائن الخاصية نفسه حتى يتمكن المُستدعون من الربط به.

import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; public class Product { // 1. تعريف حقول الخاصية الخاصة private final StringProperty name = new SimpleStringProperty(this, "name", ""); private final IntegerProperty stock = new SimpleIntegerProperty(this, "stock", 0); // 2. المحوّل العادي — يُرجع قيمة النوع البدائي أو String public String getName() { return name.get(); } public int getStock() { return stock.get(); } // 3. المحوّل العادي للكتابة — يكتب عبر الخاصية public void setName(String value) { name.set(value); } public void setStock(int value) { stock.set(value); } // 4. محوّل الخاصية — يعرض الخاصية للربط public StringProperty nameProperty() { return name; } public IntegerProperty stockProperty() { return stock; } }

يُمرر المُنشئ ذو الثلاثة حجج (new SimpleStringProperty(bean, name, initialValue)) بيانات وصفية: this هو الكائن المالك و "name" هو اسم الحقل. هذه البيانات اختيارية لكنها قيّمة عند تصحيح الأخطاء — تظهر في إخراج toString() للخاصية وفي تشخيصات Scene Builder.

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

إرفاق مستمعات التغيير

بمجرد امتلاكك كائن الخاصية يمكنك تسجيل ChangeListener عليه. يتلقى المستمع الـ observable نفسه، والقيمة القديمة، والقيمة الجديدة:

Product p = new Product(); p.setName("Laptop"); p.setStock(42); // الاستماع لتغييرات المخزون p.stockProperty().addListener((observable, oldValue, newValue) -> { System.out.printf("تغيّر المخزون: %d -> %d%n", oldValue.intValue(), newValue.intValue()); if (newValue.intValue() < 5) { System.out.println("تحذير: مخزون منخفض!"); } }); p.setStock(3); // يطبع: تغيّر المخزون: 42 -> 3 / تحذير: مخزون منخفض! p.setStock(100); // يطبع: تغيّر المخزون: 3 -> 100

لاحظ توقيع اللامدا لـ IntegerProperty: على الرغم من أن القيمة الأساسية من نوع int، يتلقى المستمع Number (النوع الأب الملاكَم). استدع .intValue() لإلغاء الملاكمة.

الخصائص للقراءة فقط

أحيانًا تريد خاصية قابلة للقراءة من الخارج لكن لا يُمكن الكتابة إليها إلا من داخل الفئة — مثل قيمة محسوبة كإجمالي السعر. توفر JavaFX ReadOnlyIntegerWrapper (والمكافئ للأنواع الأخرى) لهذا النمط:

import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerWrapper; public class Cart { // الغلاف الداخلي القابل للكتابة — هذه الفئة وحدها تستطيع الكتابة private final ReadOnlyIntegerWrapper itemCount = new ReadOnlyIntegerWrapper(this, "itemCount", 0); // العرض العام للقراءة فقط المعروض للعالم الخارجي public ReadOnlyIntegerProperty itemCountProperty() { return itemCount.getReadOnlyProperty(); } public int getItemCount() { return itemCount.get(); } public void addItem() { itemCount.set(itemCount.get() + 1); // الكتابة من داخل الفئة } }

يحمل الغلاف IntegerProperty الحقيقي. يُرجع getReadOnlyProperty() عرضًا يشارك نفس القيمة لكنه لا يعرض أي دالة set(). يمكن للمُستدعين الربط به والاستماع للتغييرات لكن لا يستطيعون تعديله.

لا تُرجع الغلاف نفسه. إذا أرجع محوّل xxxProperty() الخاص بك الـ ReadOnlyIntegerWrapper بدلًا من استدعاء getReadOnlyProperty()، يستطيع الكود الخارجي تحويله مجددًا إلى IntegerProperty قابل للكتابة وكسر التغليف. أرجع دائمًا العرض للقراءة فقط من المحوّل العام.

الخصائص وخيط تطبيق JavaFX

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

// خطأ — تحديث خاصية نموذج من خيط خلفي new Thread(() -> { String result = fetchFromNetwork(); product.setName(result); // قد يُسبب تناقضات في العرض }).start(); // صحيح — النقل إلى خيط FX new Thread(() -> { String result = fetchFromNetwork(); javafx.application.Platform.runLater(() -> product.setName(result)); }).start();

يضع Platform.runLater() الـ Runnable في طابور نبضات JavaFX. للعمل الخلفي الأثقل، يُقدّم الدرس التالي Task و Service اللذين يتعاملان مع ذلك تلقائيًا.

الخلاصة

خصائص JavaFX هي أغلفة قابلة للملاحظة تفصل منتج القيمة عن كل مستهلك يهتم بها. نمط Bean ذو المحوّلات الثلاثة — محوّل قراءة عادي، ومحوّل كتابة عادي، ودالة xxxProperty() — هو العقد المعياري الذي ينبغي أن تتبعه فئات النموذج. في الدرس التالي سترى كيف تربط خاصيتين معًا باستخدام الربط (Binding)، مما يُزيل كليًا نمط مستمع التغيير لأكثر احتياجات المزامنة شيوعًا.