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

وحدات التحكم والتعليق @FXML

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

وحدات التحكم والتعليق @FXML

في الدرس السابق تعلّمت أن ملف FXML هو وصف تصريحي لرسم بياني للمشهد. غير أن مشهدًا ثابتًا بمفرده لا يُنجز شيئًا — لا نقرات على الأزرار، ولا تحقق من البيانات، ولا تنقل بين الشاشات. هذا السلوك التفاعلي يقطن في فئة وحدة التحكم: وهي فئة Java عادية مرتبطة بملف FXML عبر الـ FXMLLoader. يغطي هذا الدرس كل ما تحتاجه لبناء وحدة تحكم وربطها بأسلوب احترافي في تطبيقات JavaFX.

دور وحدة التحكم

وحدة التحكم هي الحرف "C" في نمط MVC. تحتفظ بمراجع للعناصر المعلنة في FXML، وتستجيب لأحداث المستخدم، وتحدّث النموذج. لا تحتوي على أي كود تخطيط — ذلك عمل ملف FXML (واختياريًا Scene Builder).

هذا الفصل مقصود وذو قيمة: يستطيع المصمم تحرير FXML دون لمس Java، ويستطيع المطور اختبار منطق وحدة التحكم باستقلالية تامة عن الواجهة.

إعلان وحدة التحكم في FXML

تربط وحدة التحكم بملف FXML عبر سمة fx:controller في العنصر الجذري:

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.LoginController" spacing="12" alignment="CENTER" prefWidth="320" prefHeight="240"> <Label text="Username"/> <TextField fx:id="usernameField"/> <Label text="Password"/> <PasswordField fx:id="passwordField"/> <Button text="Log In" onAction="#handleLogin"/> <Label fx:id="errorLabel" style="-fx-text-fill: red;"/> </VBox>

سمتان تتولّيان عملية الربط هنا:

  • fx:id — يمنح عنصر واجهة المستخدم اسمًا سيحقنه FXMLLoader في الحقل المطابق في وحدة التحكم.
  • onAction="#handleLogin" — البادئة # تعني "استدعِ هذا التابع على وحدة التحكم". وهي تعمل مع أي سمة حدث (onKeyPressed وonMouseClicked وغيرها).

كتابة فئة وحدة التحكم

وحدة التحكم فئة Java عادية. الحقول التي تطابق قيم fx:id يجب أن تُوسَم بـ @FXML. يستخدم FXMLLoader الانعكاس (reflection) لحقنها — فيتجاوز محددات الوصول، لذا يمكن أن تكون الحقول private (وينبغي أن تكون كذلك).

package com.example; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; public class LoginController { @FXML private TextField usernameField; @FXML private PasswordField passwordField; @FXML private Label errorLabel; /** * تُستدعى تلقائيًا بواسطة FXMLLoader بعد حقن جميع حقول @FXML. * استخدمها بدلًا من المنشئ لأي تهيئة تحتاج إلى عناصر الواجهة. */ @FXML private void initialize() { errorLabel.setVisible(false); usernameField.requestFocus(); } @FXML private void handleLogin() { String user = usernameField.getText().trim(); String pass = passwordField.getText(); if (user.isEmpty() || pass.isEmpty()) { errorLabel.setText("Please fill in both fields."); errorLabel.setVisible(true); return; } // تفويض الأمر إلى خدمة — أبقِ وحدة التحكم خفيفة boolean ok = AuthService.authenticate(user, pass); if (!ok) { errorLabel.setText("Invalid credentials. Try again."); errorLabel.setVisible(true); passwordField.clear(); } else { SceneRouter.goTo("dashboard"); } } }
لماذا initialize() وليس المنشئ؟ عندما ينفذ المنشئ، لم يحقن FXMLLoader أي حقول بعد. تُستدعى initialize() بعد ملء جميع حقول @FXML، لذا فهي المكان الصحيح لضبط الحالة الابتدائية وربط الخصائص أو جلب البيانات الأولية.

تحميل FXML من Java

نقطة دخول التطبيق (أو مساعد التبديل بين المشاهد) تستخدم FXMLLoader لتحليل الملف والحصول على العقدة الجذرية:

import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class App extends Application { @Override public void start(Stage stage) throws Exception { FXMLLoader loader = new FXMLLoader( getClass().getResource("/fxml/login.fxml") ); Parent root = loader.load(); // يحلل FXML وينشئ وحدة التحكم ويحقن الحقول stage.setScene(new Scene(root)); stage.setTitle("My App"); stage.show(); } }

بعد عودة loader.load() يمكنك استدعاء loader.getController() للحصول على نسخة وحدة التحكم. هذا مفيد عندما تحتاج إلى تمرير بيانات إلى وحدة التحكم قبل عرض المشهد — وهو نمط شائع لتمرير كائن نموذج إلى شاشة التفاصيل.

FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/detail.fxml")); Parent root = loader.load(); DetailController ctrl = loader.getController(); ctrl.setItem(selectedItem); // تمرير البيانات قبل عرض المشهد stage.setScene(new Scene(root)); stage.show();

تمرير البيانات بين وحدات التحكم

لا يوجد "موجّه" مدمج في JavaFX. الأنماط الشائعة لنقل البيانات عند التنقل بين الشاشات:

  1. حقن عبر الضبط (Setter injection) (كما هو موضح أعلاه) — يستدعي الكود المُستدعي ضابطًا عامًا على وحدة التحكم التالية بعد load() وقبل show().
  2. نموذج مشترك / خدمة singleton — تحتفظ وحدتا التحكم بمرجع لنفس الخدمة أو كائن النموذج القابل للمراقبة؛ تقرأه وحدة التحكم الثانية في initialize().
  3. حقن عبر المنشئ باستخدام مصنع وحدات التحكم — تمرير تعبير lambda كمعامل ثانٍ لمنشئ FXMLLoader لبناء وحدات التحكم بنفسك، مما يُتيح حقن التبعيات الحقيقي.
// مصنع وحدات التحكم — يتيح حقن التبعيات عبر المنشئ FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/orders.fxml")); loader.setControllerFactory(type -> new OrdersController(orderRepository)); Parent root = loader.load();
أبقِ وحدات التحكم خفيفة. ينبغي لوحدة التحكم أن تترجم أحداث الواجهة إلى استدعاءات خدمة أو نموذج، وأن تترجم حالة النموذج مجددًا إلى حالة عناصر الواجهة. منطق الأعمال لا مكان له هنا. وحدة التحكم التي يصعب اختبارها بشكل وحدوي هي على الأرجح وحدة تحكم تقوم بالكثير.

معالجات الأحداث: توابع @FXML مقابل التسجيل باستخدام Lambda

صياغة onAction="#methodName" في FXML واستدعاء button.setOnAction(e -> ...) في الكود متكافئتان في النتيجة. استخدم مراجع أحداث FXML للمعالجات التي تُشكّل جزءًا طبيعيًا من التخطيط المُعلن. استخدم تسجيل lambda في initialize() عندما تحتاج إلى التقاط متغير محلي أو عندما يُربط المعالج ديناميكيًا.

@FXML private void initialize() { // معالج ديناميكي يلتقط متغير حلقة — يجب تنفيذه في الكود for (Button btn : colorButtons) { final String color = btn.getId(); btn.setOnAction(e -> applyColor(color)); } }

وحدات التحكم المتداخلة مع fx:include

كثيرًا ما تُقسَّم واجهات المستخدم الكبيرة إلى أجزاء FXML قابلة لإعادة الاستخدام يتم تحميلها بـ fx:include. يمكن لكل FXML مُضمَّن أن يملك وحدة تحكمه الخاصة. تستطيع وحدة التحكم الأصلية الوصول إلى وحدة التحكم الفرعية بالإعلان عن حقل @FXML يحمل الاسم <fx:id of the include>Controller:

<!-- parent.fxml --> <BorderPane fx:controller="com.example.MainController" ...> <top> <fx:include fx:id="toolbar" source="toolbar.fxml"/> </top> </BorderPane>
// MainController.java @FXML private ToolbarController toolbarController; // تُحقن تلقائيًا
لا تُشر إلى حقول @FXML من المنشئ. تكون قيمتها null وقت بناء الكائن. أي كود يلمس عنصر واجهة يجب أن يكون في initialize() أو في معالج حدث — وليس أبدًا في المنشئ أو في مُهيِّئ ثابت.

الخلاصة

التعليق @FXML هو الغراء الذي يربط ملف FXML التصريحي بوحدة التحكم Java الأمرية. تذكّر ثلاث نقاط محورية: أعلن عن fx:controller في العنصر الجذري لـ FXML؛ وسِّم الحقول المطابقة بـ @FXML (يمكن أن تكون خاصة)؛ وضع جميع منطق عناصر الواجهة الأولي في initialize() لا في المنشئ. في الدرس القادم ستستخدم Scene Builder لتوليد ملفات FXML بصريًا وصيانتها، وسترى كيف يُبقي سمات fx:id وfx:controller متزامنة تلقائيًا.