عناصر JavaFX والتخطيطات وFXML

القوائم والجداول وصناديق الاختيار

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

القوائم والجداول وصناديق الاختيار

ثلاثة عناصر تهيمن على عرض البيانات في كل تطبيق JavaFX حقيقي تقريبًا: ListView وTableView وComboBox. يتّبع كل منها نفس الفلسفة المعمارية — الفصل بين النموذج والعرض (Model-View) حيث يعرض العنصر أيَّ بيانات تضعها في ObservableList، ويحدّث نفسه تلقائيًا عند تغيّر تلك القائمة. إن فهمت هذا النمط مرة واحدة فهمتَ العناصر الثلاثة كلّها.

أساس ObservableList

قبل لمس أي عنصر، استوعب هذا جيّدًا: عناصر البيانات في JavaFX لا تحتفظ بنسخ من بياناتك. إنها تحتفظ بمرجع إلى ObservableList<T> وتراقبه بحثًا عن أي تغيير. أضف عنصرًا إلى القائمة فيُعيد العنصر رسم نفسه. احذف واحدًا فيختفي الصف. لن تحتاج أبدًا إلى استدعاء دالة "تحديث".

import javafx.collections.FXCollections; import javafx.collections.ObservableList; ObservableList<String> names = FXCollections.observableArrayList( "Alice", "Bob", "Carol" ); names.add("Dave"); // العنصر يحدّث نفسه — لا حاجة لكود إضافي

ListView

يعرض ListView<T> قائمة قابلة للتمرير من العناصر، واحد في كل صف. يمكن أن يكون النوع العام أي شيء — String أو فئة نموذج أو نوع مُعدَّد (enum).

import javafx.scene.control.ListView; import javafx.collections.FXCollections; ListView<String> listView = new ListView<>( FXCollections.observableArrayList("Alpha", "Beta", "Gamma", "Delta") ); listView.setPrefHeight(200); // قراءة العنصر المحدد listView.getSelectionModel().selectedItemProperty().addListener( (obs, oldVal, newVal) -> System.out.println("Selected: " + newVal) ); // وضع الاختيار المتعدد listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); ObservableList<String> selected = listView.getSelectionModel().getSelectedItems();
SelectionModel هو الجسر إلى اختيار المستخدم. تُعيد getSelectionModel() كائن MultipleSelectionModel يكشف عن selectedItemProperty() (ربط مفرد) وgetSelectedItems() (مجموعة قابلة للمراقبة للاختيار المتعدد). اقرأ الاختيار دائمًا عبر هذه الواجهة البرمجية — لا تتجوّل في شجرة الخلايا بنفسك أبدًا.

تخصيص عرض الخلايا بمصانع الخلايا

بشكل افتراضي يستدعي ListView الدالة toString() على كل عنصر. للحصول على صفوف أغنى — أيقونات، أزرار، نصوص منسّقة — أمدّ بـمصنع خلايا (cell factory): وهو Callback تستدعيه القائمة مرة واحدة لكل خلية مرئية لإنشاء ListCell<T>. تقوم JavaFX بعدها بإعادة تدوير تلك الخلايا أثناء التمرير (نفس مبدأ RecyclerView في Android).

import javafx.scene.control.ListCell; listView.setCellFactory(lv -> new ListCell<String>() { @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); // استدع super أولًا دائمًا if (empty || item == null) { setText(null); setGraphic(null); } else { setText(item.toUpperCase()); } } });
استدع super.updateItem() أولًا دائمًا وتعامل مع حالة empty دائمًا. تجاهل أيٍّ منهما يُسبّب ظهور محتوى وهمي في الصفوف الفارغة — وهو خطأ شائع جدًا عند مبتدئي JavaFX. الخلايا تُعاد استخدامها؛ إذا لم تمسحها عندما تكون empty == true فستنزّ البيانات القديمة إلى الفتحات الفارغة.

TableView

يعرض TableView<T> مجموعة من الكائنات في شبكة من الأعمدة. كل TableColumn<T, CellValue> يعرف كيف يستخرج حقلًا واحدًا من كائن من النوع T.

import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; // --- فئة النموذج بخصائص JavaFX --- public class Product { private final SimpleStringProperty name; private final SimpleIntegerProperty price; public Product(String name, int price) { this.name = new SimpleStringProperty(name); this.price = new SimpleIntegerProperty(price); } public String getName() { return name.get(); } public int getPrice() { return price.get(); } public SimpleStringProperty nameProperty() { return name; } public SimpleIntegerProperty priceProperty() { return price; } } // --- بناء الجدول --- TableView<Product> table = new TableView<>(); TableColumn<Product, String> nameCol = new TableColumn<>("Name"); nameCol.setCellValueFactory(new PropertyValueFactory<>("name")); TableColumn<Product, Integer> priceCol = new TableColumn<>("Price ($)"); priceCol.setCellValueFactory(new PropertyValueFactory<>("price")); table.getColumns().addAll(nameCol, priceCol); ObservableList<Product> products = FXCollections.observableArrayList( new Product("Keyboard", 49), new Product("Mouse", 29), new Product("Monitor", 299) ); table.setItems(products);

تستخدم PropertyValueFactory الانعكاس (reflection) لاستدعاء دالة getter باسم يطابق السلسلة التي تمرّرها. البديل الأنظف والآمن للإعادة الهيكلية (refactoring) هو مصنع قيم خلايا lambda يقرأ خاصية JavaFX مباشرةً:

nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

الخلايا القابلة للتحرير

جعل خلية الجدول قابلة للتحرير يتطلب خطوتين: إخبار الجدول بأنه قابل للتحرير، ثم إعطاء العمود مصنع خلايا قابلًا للتحرير ومعالج setOnEditCommit يكتب القيمة الجديدة مجددًا إلى النموذج.

import javafx.scene.control.cell.TextFieldTableCell; table.setEditable(true); nameCol.setEditable(true); nameCol.setCellFactory(TextFieldTableCell.forTableColumn()); nameCol.setOnEditCommit(event -> { event.getRowValue().nameProperty().set(event.getNewValue()); });
اكشف عن كائنات Property من نموذجك. تتّصل عناصر مثل TableView وListView بكفاءة أعلى بكائنات Property (SimpleStringProperty وSimpleIntegerProperty وغيرها). إذا تغيّرت الخاصية برمجيًا، تحدّث خلية الجدول تلقائيًا — لا حاجة لاستدعاءات تحديث يدوية.

ComboBox

يجمع ComboBox<T> بين زر يعرض الاختيار الحالي وقائمة منسدلة. كما يدعم حقل نص اختياريًا قابلًا للتحرير عند استدعاء setEditable(true).

import javafx.scene.control.ComboBox; ObservableList<String> options = FXCollections.observableArrayList( "Small", "Medium", "Large", "X-Large" ); ComboBox<String> sizeBox = new ComboBox<>(options); sizeBox.setValue("Medium"); // تعيين اختيار افتراضي // الاستجابة لتغيّرات الاختيار sizeBox.valueProperty().addListener( (obs, oldVal, newVal) -> System.out.println("Size: " + newVal) ); // أو القراءة عند الطلب String chosen = sizeBox.getValue();

ComboBox مع كائنات مخصصة

عندما يكون النوع العام ليس String، أمدّ بـStringConverter حتى يعرف العنصر كيف يعرض العناصر في وجه الزر وفي القائمة المنسدلة على حدٍّ سواء.

import javafx.util.StringConverter; ComboBox<Product> productBox = new ComboBox<>(products); productBox.setConverter(new StringConverter<Product>() { @Override public String toString(Product p) { return p == null ? "" : p.getName() + " ($" + p.getPrice() + ")"; } @Override public Product fromString(String s) { return null; } // غير قابل للتحرير });

مقارنة العناصر الثلاثة

  • ListView: قائمة بعمود واحد من العناصر. استخدمها للوحات التنقل وقوائم الخيارات أو أي مجموعة قابلة للتمرير حيث تكفي قطعة بيانات واحدة في كل صف.
  • TableView: شبكة متعددة الأعمدة. استخدمه عندما يكون لكل عنصر عدة حقول تحتاج إلى عرضها جنبًا إلى جنب مع الفرز الاختياري والتحرير.
  • ComboBox: عنصر اختيار مدمج. استخدمه داخل النماذج حيث المساحة محدودة والمستخدم يختار قيمة واحدة من مجموعة محددة.

الخلاصة

تشترك العناصر الثلاثة في نموذج بيانات ObservableList — غيّر القائمة فتتحدّث الواجهة تلقائيًا. يستخدم ListView وTableView مصانع الخلايا للعرض المخصص ويتّبعان مخطط إعادة تدوير الخلايا الذي يتعامل مع آلاف الصفوف بكفاءة. يربط TableView الأعمدة بخصائص النموذج إما عبر PropertyValueFactory أو مصانع lambda، ويدعم التحرير في الموضع عبر setOnEditCommit. يندمج ComboBox بسلاسة في النماذج ويكتسب تخصيص العرض من خلال StringConverter. في الدرس القادم ستُرتّب هذه العناصر على الشاشة باستخدام حاويات التخطيط HBox وVBox.