المجموعات القابلة للملاحظة
في الدروس السابقة رأيت كيف تُخطر خصائص JavaFX الفردية المستمعين عند تغيّر قيمتها. ينطبق المبدأ ذاته على المجموعات: تأتي مع JavaFX مجموعة موازية من الأغلفة القابلة للملاحظة لـ List وMap وSet، تُطلق أحداث تغيير دقيقة في كل مرة تُضاف فيها عناصر أو تُحذف أو تُستبدل. هذا هو السباك الداخلي لكل ListView وTableView وComboBox في التطبيق الحقيقي — فهي ترصد ObservableList وتتحدّث تلقائيًا، دون الحاجة إلى إخبار واجهة المستخدم يدويًا بإعادة الرسم.
إنشاء ObservableList
فئة المصنع FXCollections هي نقطة انطلاقك لجميع المجموعات القابلة للملاحظة. نادرًا ما تُنشئ الفئات الملموسة مباشرةً؛ بل تذهب إلى المصنع حتى يتطوّر التنفيذ دون كسر كودك.
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
// يغلّف ArrayList داخليًا
ObservableList<String> names = FXCollections.observableArrayList();
names.addAll("Alice", "Bob", "Carol");
// من قائمة موجودة
List<String> raw = List.of("X", "Y", "Z");
ObservableList<String> copy = FXCollections.observableArrayList(raw);
تمتدّ ObservableList<E> من java.util.List<E>، لذا تعمل كل عملية مجموعة قياسية — add، وremove، وsort، وsubList — تمامًا كما تتوقع. الفارق الوحيد أن الطفرات تُطلق أحداثًا أيضًا.
الاستماع إلى التغييرات باستخدام ListChangeListener
أرفق ListChangeListener لتستقبل وصفًا تفصيليًا لكل طفرة. يستقبل المستمع كائن Change يجب أن تُكرّر عبره، لأن عملية منطقية واحدة (مثل setAll) قد تُنتج عدة تغييرات فرعية متقطعة.
import javafx.collections.ListChangeListener;
ObservableList<String> items = FXCollections.observableArrayList("A", "B", "C");
items.addListener((ListChangeListener<String>) change -> {
while (change.next()) {
if (change.wasAdded()) {
System.out.println("Added: " + change.getAddedSubList());
}
if (change.wasRemoved()) {
System.out.println("Removed: " + change.getRemoved());
}
if (change.wasReplaced()) {
System.out.println("Replaced at index " + change.getFrom());
}
if (change.wasPermutated()) {
// أُعيد ترتيب العناصر، مثلًا بعد الفرز
for (int i = change.getFrom(); i < change.getTo(); i++) {
System.out.println("Index " + i + " moved to " + change.getPermutation(i));
}
}
}
});
items.add("D"); // يُطلق: Added [D]
items.remove("B"); // يُطلق: Removed [B]
items.set(0, "Alpha"); // يُطلق: Replaced at index 0
FXCollections.sort(items); // يُطلق: Permutated
استدع change.next() دائمًا في حلقة. كائن Change هو مؤشر قد يصف عدة نطاقات فرعية من القائمة. قراءة النطاق الأول فحسب — أو إغفال الحلقة تمامًا — يُسقط صامتًا معلومات عمليات متعددة العناصر كـ addAll أو removeIf.
ربط ObservableList بعنصر واجهة مستخدم
تقبل عناصر تحكم JavaFX التي تعرض مجموعات كائن ObservableList نموذجًا لها. يشترك عنصر التحكم داخليًا؛ ولست بحاجة إلى كتابة مستمع بنفسك لتحديث العرض.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TaskListApp extends Application {
private final ObservableList<String> tasks =
FXCollections.observableArrayList("Write tests", "Fix bug #42", "Deploy");
@Override
public void start(Stage stage) {
ListView<String> listView = new ListView<>(tasks); // ربط النموذج هنا
Button addBtn = new Button("Add Task");
addBtn.setOnAction(e -> tasks.add("New task " + (tasks.size() + 1)));
Button removeBtn = new Button("Remove Last");
removeBtn.setOnAction(e -> {
if (!tasks.isEmpty()) tasks.remove(tasks.size() - 1);
});
VBox root = new VBox(10, listView, addBtn, removeBtn);
stage.setScene(new Scene(root, 300, 250));
stage.setTitle("Observable Task List");
stage.show();
}
public static void main(String[] args) { launch(args); }
}
لاحظ ما غائب: لا استدعاء لـ listView.refresh() ولا listView.getItems().clear() متبوعةً بإعادة إضافة العناصر. أنت تُعدّل tasks وتتفاعل ListView من تلقاء نفسها. هكذا يُؤتي النمط القابل للملاحظة ثماره عمليًا.
احتفظ بـ ObservableList حقلًا لا متغيرًا محليًا. تحتفظ عناصر التحكم بمرجع إلى كائن القائمة ذاته. إن أعدت تعيين الحقل إلى قائمة جديدة يظل عنصر التحكم يراقب القديمة ويصبح متقادمًا. طوّع القائمة الموجودة في موضعها، أو استدع listView.setItems(newList) صراحةً لتبديل النموذج.
ObservableMap و ObservableSet
يوجد النمط ذاته للخرائط والمجموعات:
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.collections.MapChangeListener;
ObservableMap<String, Integer> scores = FXCollections.observableHashMap();
scores.addListener((MapChangeListener<String, Integer>) change -> {
if (change.wasAdded()) {
System.out.printf("PUT %s = %d%n", change.getKey(), change.getValueAdded());
}
if (change.wasRemoved()) {
System.out.printf("REMOVED %s (was %d)%n", change.getKey(), change.getValueRemoved());
}
});
scores.put("Alice", 95); // PUT Alice = 95
scores.put("Alice", 97); // REMOVED Alice (was 95) + PUT Alice = 97
scores.remove("Alice"); // REMOVED Alice (was 97)
في الخريطة، استدعاء put الذي يستبدل مفتاحًا موجودًا يُطلق حدثًا للإزالة (القيمة القديمة) وحدثًا للإضافة (القيمة الجديدة) في استدعاء مستمع واحد — يحمل كائن Change ذاته wasAdded() وwasRemoved() كليهما بقيمة true.
طرق العرض المصفّاة والمرتّبة
بدلًا من إدارة مجموعة ثانية متزامنة مع الأولى، تمنحك JavaFX أغلفة عرض مباشرة تظل متسقة تلقائيًا:
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.scene.control.TextField;
ObservableList<String> allItems = FXCollections.observableArrayList(
"apple", "banana", "apricot", "cherry", "avocado");
// FilteredList يغلّف allItems؛ يمكن تغيير المسند في وقت التشغيل
FilteredList<String> filtered = new FilteredList<>(allItems, s -> true);
// SortedList يغلّف FilteredList لخط أنابيب كامل
SortedList<String> sorted = new SortedList<>(filtered,
Comparator.naturalOrder());
ListView<String> listView = new ListView<>(sorted);
// ربط حقل البحث بمسند الفلتر
TextField search = new TextField();
search.textProperty().addListener((obs, old, text) ->
filtered.setPredicate(s -> s.startsWith(text.toLowerCase())));
خط الأنابيب هو allItems ← FilteredList ← SortedList ← ListView. تطوّع allItems (إضافات، حذف) يتكاسك تلقائيًا عبر السلسلة. تغيير المسند أو المقارِن يُعيد التقييم فورًا — دون أي حلقة معيارية.
اربط مقارِن SortedList بترتيب فرز TableView عند إقرانهما: sorted.comparatorProperty().bind(tableView.comparatorProperty()). بدون هذا الربط، يُرتّب النقر على رأس العمود الترتيب المرئي لكنه لا يُرتّب SortedList الخلفية، ويبدو الجدول كأنه يُرتّب ثم يعود إلى الوضع السابق بشكل غير منتظم.
المستخرج — التفاعل مع تغييرات خصائص العناصر
افتراضيًا، لا تُطلق ObservableList أحداثًا إلا عند تغيّر عضوية المجموعة (إضافة/حذف/استبدال). إن كانت عناصرك وحدات JavaFX beans وتحتاج القائمة إلى إطلاق أحداث عند تغيّر خاصية داخل عنصر أيضًا، استخدم مستخرجًا:
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Employee {
private final javafx.beans.property.StringProperty name =
new javafx.beans.property.SimpleStringProperty(this, "name");
public Employee(String n) { name.set(n); }
public javafx.beans.property.StringProperty nameProperty() { return name; }
public String getName() { return name.get(); }
}
// يخبر المستخرج القائمة بأي خصائص تراقبها لكل عنصر
ObservableList<Employee> employees = FXCollections.observableArrayList(
emp -> new Observable[]{ emp.nameProperty() }
);
employees.add(new Employee("Alice"));
employees.addListener((ListChangeListener<Employee>) change -> {
while (change.next()) {
if (change.wasUpdated()) {
System.out.println("Employee updated in range ["
+ change.getFrom() + ", " + change.getTo() + ")");
}
}
});
// هذا الآن يُطلق حدث wasUpdated()
employees.get(0).nameProperty().set("Alicia");
بدون المستخرج، إعادة تسمية موظف تُحدّث الكائن في الذاكرة لكن القائمة تظل صامتة — لا تُعيد ListView رسم الصف أبدًا. بوجود المستخرج، يُطلق حدث wasUpdated() ويُعيد أي عنصر تحكم مرتبط العرض تلقائيًا.
الخلاصة
كل من ObservableList وObservableMap وObservableSet مجموعات Java قياسية معزّزة بإشعار التغيير. تُنشئها عبر FXCollections، وترفق ListChangeListener أو MapChangeListener للتفاعل مع الطفرات، وتربطها مباشرةً بعناصر تحكم كـ ListView وTableView. تمنحك FilteredList وSortedList طرق عرض خط أنابيب مباشرة دون أي مزامنة يدوية. عند استخدام عناصر JavaFX beans، اعتمد على المستخرج لرصد تغييرات الخصائص الداخلية للعناصر أيضًا. تجعل هذه الأدوات مجتمعةً طبقة البيانات في واجهة المستخدم التفاعلية تصريحية وموثوقة في آنٍ واحد.