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

عناصر التحكم المخصصة وإعادة الاستخدام

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

عناصر التحكم المخصصة وإعادة الاستخدام

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

نهجان: التركيب مقابل الوراثة الفرعية

قبل كتابة أي كود تحتاج إلى اتخاذ قرار بشأن استراتيجية التنفيذ. تمنحك JavaFX خيارين نظيفين.

  • التركيب (مُفضَّل): امتدّ من لوحة تخطيط موجودة (HBox أو VBox أو StackPane وغيرها) وأضف عُقدًا فرعية داخل منشئها. والنتيجة هي لوحة يمكن وضعها في أي مكان يُقبل فيه Node.
  • الوراثة الفرعية من Control: امتدّ من Control وأرفقه بتنفيذ Skin. هذا هو المسار الصحيح للعناصر المعقدة القابلة للتخصيص بالسمات (skinnable)، لكنه يتطلب مزيدًا من الكود المعياري.

بالنسبة لغالبية عناصر التحكم المخصصة اليومية، يُعدّ التركيبُ الإجابةَ الصحيحة. تحصل على سلوك التخطيط مجانًا وتركّز كليًا على واجهة برمجة المكون ومنطقه.

بناء مكون LabeledField قابل للإعادة

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

import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.VBox; public class LabeledField extends VBox { private final Label label = new Label(); private final TextField field = new TextField(); private final Label errorLabel = new Label(); // خصائص JavaFX حتى يتمكن المستدعون من الربط بها private final StringProperty labelText = new SimpleStringProperty(this, "labelText", ""); private final StringProperty errorText = new SimpleStringProperty(this, "errorText", ""); public LabeledField(String labelText) { setSpacing(4); getStyleClass().add("labeled-field"); label.textProperty().bind(this.labelText); errorLabel.textProperty().bind(errorText); errorLabel.getStyleClass().add("field-error"); errorLabel.setVisible(false); errorLabel.setManaged(false); // أظهر تسمية الخطأ فقط عندما يكون النص غير فارغ errorText.addListener((obs, old, val) -> { boolean hasError = val != null && !val.isEmpty(); errorLabel.setVisible(hasError); errorLabel.setManaged(hasError); }); getChildren().addAll(label, field, errorLabel); setLabelText(labelText); } // --- موصّلات الخاصية ----------------------------------------------- public StringProperty labelTextProperty() { return labelText; } public String getLabelText() { return labelText.get(); } public void setLabelText(String t) { labelText.set(t); } public StringProperty errorTextProperty() { return errorText; } public String getErrorText() { return errorText.get(); } public void setErrorText(String t) { errorText.set(t); } /** تفويض حتى يتمكن المستدعون من الربط بقيمة TextField الداخلية. */ public StringProperty textProperty() { return field.textProperty(); } public String getText() { return field.getText(); } public void setText(String t) { field.setText(t); } /** منح الحقل الداخلي نصًا توجيهيًا. */ public void setPrompt(String prompt) { field.setPromptText(prompt); } }
أفصح عن خصائص JavaFX، وليس مجرد getters/setters. بتغليف labelText وerrorText كحقول StringProperty وإعادتها من labelTextProperty()، يستطيع المستدعون استخدام واجهة الربط الكاملة — بما فيها تعبيرات Bindings.when(…) والمستمعين — تمامًا كما يفعلون مع أي عنصر تحكم مدمج.

استخدام المكون في المشهد

بمجرد وجود الفئة تستخدمها كأي عُقدة أخرى:

LabeledField emailField = new LabeledField("Email address"); emailField.setPrompt("you@example.com"); emailField.setMaxWidth(300); Button submitBtn = new Button("Submit"); submitBtn.setOnAction(e -> { if (!emailField.getText().contains("@")) { emailField.setErrorText("Please enter a valid email address."); } else { emailField.setErrorText(""); System.out.println("Email: " + emailField.getText()); } }); VBox root = new VBox(12, emailField, submitBtn); root.setPadding(new Insets(20));

تحميل FXML داخل عنصر تحكم مخصص

للعناصر ذات التخطيطات المعقدة، يكون الاحتفاظ بالبنية في ملف FXML أكثر نظافة بكثير من إنشاء شجرة المشهد بالكامل في Java خالص. النمط هو تحميل FXML داخل منشئ المكون ذاته، مع قيام المكون بدور الجذر والمتحكم معًا.

// resources/com/example/controls/StatusCard.fxml // <?xml version="1.0" encoding="UTF-8"?> // <fx:root type="VBox" xmlns:fx="http://javafx.com/fxml"> // <Label fx:id="titleLabel" styleClass="card-title"/> // <Label fx:id="statusLabel" styleClass="card-status"/> // </fx:root> import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Label; import javafx.scene.layout.VBox; import java.io.IOException; public class StatusCard extends VBox { @FXML private Label titleLabel; @FXML private Label statusLabel; public StatusCard() { FXMLLoader loader = new FXMLLoader( getClass().getResource("/com/example/controls/StatusCard.fxml")); loader.setRoot(this); // <fx:root> يُعيَّن لهذه النسخة loader.setController(this); // تُحقن حقول @FXML في هذه النسخة try { loader.load(); } catch (IOException ex) { throw new RuntimeException("Cannot load StatusCard.fxml", ex); } } public void setTitle(String title) { titleLabel.setText(title); } public void setStatus(String status) { statusLabel.setText(status); } }
استخدم <fx:root> — لا <VBox> — كعنصر جذر في ملف FXML. هذه هي الإشارة إلى FXMLLoader بأن كائن الجذر مُقدَّم خارجيًا (عبر setRoot(this)). بدونه يُنشئ المحمّل نسخةً ثانية من VBox ولا تُحقن حقول @FXML أبدًا في المكون.

إتاحة واجهة CSS

يكون عنصر التحكم المُصمَّم جيدًا قابلًا للتخصيص بالـ CSS دون الحاجة إلى تغيير المصدر. هناك مستويان من الدعم يجب أن توفرهما.

  1. فئات الأنماط: خصّص سلاسل فئات نمط ذات معنى حتى يتمكن المستخدمون من استهداف مكونك في ورقة الأنماط.
    getStyleClass().add("labeled-field"); label.getStyleClass().add("labeled-field-label"); field.getStyleClass().add("labeled-field-input"); errorLabel.getStyleClass().add("labeled-field-error");
  2. الفئات الزائفة في CSS: للحالات مثل غير صالح، استخدم PseudoClass حتى يستجيب عنصر التحكم لـ :invalid في CSS.
    import javafx.css.PseudoClass; private static final PseudoClass INVALID = PseudoClass.getPseudoClass("invalid"); // قم بتبديله عند تغيير الخطأ errorText.addListener((obs, old, val) -> { boolean hasError = val != null && !val.isEmpty(); errorLabel.setVisible(hasError); errorLabel.setManaged(hasError); pseudoClassStateChanged(INVALID, hasError); });
    في ورقة أنماط التطبيق:
    .labeled-field:invalid .labeled-field-input { -fx-border-color: #e53935; }

إبقاء عناصر التحكم قابلة للإعادة: إرشادات التصميم

  • غلّف التخطيط، وأفصح عن البيانات. قرارات التخطيط الداخلية (المسافات والحشوات وبنية العُقد) يجب ألا تتسرب. ينبغي أن يرى المستدعون فقط واجهة برمجية نظيفة قائمة على الخصائص.
  • لا تُضمّن الأبعاد بشكل ثابت داخل المكون. اترك لتخطيط الأصل أو CSS التحكم في الحجم. استخدم setMaxWidth(Double.MAX_VALUE) على العُقد الفرعية التي تريد أن تنمو مع الحاوية.
  • فضّل ObjectProperty وStringProperty وBooleanProperty على الحقول البسيطة. هذا يتيح الربط ويجعل المكون يعمل بشكل طبيعي مع باقي JavaFX.
  • وفّر منشئًا بدون وسيطات بالإضافة إلى المنشئات الملائمة. لا يستطيع FXML استدعاء المنشئات التي تأخذ معاملات إلا إذا استخدمت @NamedArg، لذا يبقى المنشئ الافتراضي المكوّن قابلًا للاستخدام من FXML.
لا تصل إلى حقول محقونة بـ @FXML في جسم المنشئ — استخدم @FXML initialize() بدلًا من ذلك. عندما يستدعي محمّل FXML منشئك تكون الحقول المحقونة قيمتها null؛ فهي لا تُملأ إلا بعد تحليل XML. أعدّ الروابط والمستمعين التي تشير إلى تلك الحقول داخل التابع initialize()، الذي يستدعيه المحمّل بمجرد اكتمال الحقن.

تعبئة عناصر التحكم لإعادة الاستخدام عبر المشاريع

بمجرد امتلاكك عدة عناصر تحكم مخصصة يستحق تعبئتها كـ JAR مستقلة (أو وحدة Java). ضع كل فئة تحكم وموردها FXML في نفس الحزمة. في المشروع المعياري، أفصح عن الحزمة في module-info.java:

module com.example.controls { requires javafx.controls; requires javafx.fxml; exports com.example.controls; opens com.example.controls to javafx.fxml; // يسمح لـ FXMLLoader بالوصول إلى الحقول الخاصة }

توجيه opens … to javafx.fxml إلزامي لأن FXMLLoader يستخدم الانعكاس (reflection) لحقن الحقول المُعلَّمة بـ @FXML، ونظام الوحدات يحجب الوصول الانعكاسي افتراضيًا.

الخلاصة

تُبنى عناصر التحكم المخصصة بامتداد لوحة تخطيط (التركيب) أو Control (عند الحاجة إلى تخصيص كامل بالسمات)، مع الإفصاح عن واجهة برمجية قائمة على الخصائص، وتحميل FXML بنمط <fx:root> عندما يكون التخطيط معقدًا، وتوفير خطاطيف CSS عبر فئات الأنماط والفئات الزائفة. والنتيجة عُقدة لا تختلف — من منظور شجرة المشهد — عن أي عنصر تحكم مدمج في JavaFX، ويمكن لأي شاشة في التطبيق إعادة استخدامها دون تكرار.