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

مشروع: تطبيق JavaFX قائم على نموذج مع FXML

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

مشروع: تطبيق JavaFX قائم على نموذج مع FXML

يجمع هذا الدرس الختامي كل ما تناوله البرنامج التعليمي في مشروع واحد مكتفٍ بذاته: نموذج تسجيل جهات الاتصال. يجمع التطبيق الاسم الأول والاسم الأخير والبريد الإلكتروني والجنس ونبذة قصيرة، ويتحقق من صحة المدخلات عند نقر المستخدم على إرسال، ثم يعرض السجل المُقدَّم في لوحة ملخص للقراءة فقط. تُعرَّف واجهة المستخدم بالكامل في FXML وتُربط بمتحكم Java، ولا يوجد أي كود تخطيط في Java.

لماذا هذا المشروع؟ النماذج هي أكثر أنماط واجهة المستخدم الرسومية شيوعًا. بناؤها من البداية إلى النهاية يُجبرك على الجمع بين كل المهارات المُغطَّاة في هذا البرنامج: أجزاء التخطيط والعناصر وFXML والمتحكمات ومعالجة الأحداث والتحقق من الصحة.

هيكل المشروع

حافظ على نظافة شجرة المصدر. يبدو تخطيط Maven النموذجي لمشروع JavaFX كالتالي:

src/ main/ java/ com/example/contactapp/ MainApp.java ContactController.java Contact.java resources/ com/example/contactapp/ contact-form.fxml styles.css

يقع ملف FXML وملف CSS داخل resources في نفس مسار الحزمة كفئات Java. تحلّها JavaFX عبر getClass().getResource("contact-form.fxml") عند تحميل المشهد.

النموذج: Contact.java

نموذج Java بسيط يحمل بيانات النموذج بين المتحكم وبقية التطبيق. لا توجد أنواع JavaFX هنا — إبقاء النموذج خاليًا من تبعيات واجهة المستخدم يعني إمكانية اختباره دون تشغيل مجموعة الأدوات.

package com.example.contactapp; public class Contact { private final String firstName; private final String lastName; private final String email; private final String gender; private final String bio; public Contact(String firstName, String lastName, String email, String gender, String bio) { this.firstName = firstName; this.lastName = lastName; this.email = email; this.gender = gender; this.bio = bio; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getEmail() { return email; } public String getGender() { return gender; } public String getBio() { return bio; } }

ملف FXML: contact-form.fxml

العنصر الجذر هو BorderPane. يحتوي center على GridPane بحقول النموذج؛ ويحتوي bottom على أزرار الإجراءات؛ ويحتوي right على لوحة الملخص التي تظهر بعد الإرسال.

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <?import javafx.geometry.Insets?> <BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.contactapp.ContactController" prefWidth="720" prefHeight="480" stylesheets="@styles.css"> <!-- ===== الوسط: نموذج الإدخال ===== --> <center> <GridPane hgap="12" vgap="14"> <padding><Insets top="24" right="24" bottom="24" left="24"/></padding> <columnConstraints> <ColumnConstraints minWidth="110" halignment="RIGHT"/> <ColumnConstraints hgrow="ALWAYS"/> </columnConstraints> <Label text="الاسم الأول *" GridPane.columnIndex="0" GridPane.rowIndex="0"/> <TextField fx:id="firstNameField" promptText="أدخل الاسم الأول" GridPane.columnIndex="1" GridPane.rowIndex="0"/> <Label text="الاسم الأخير *" GridPane.columnIndex="0" GridPane.rowIndex="1"/> <TextField fx:id="lastNameField" promptText="أدخل الاسم الأخير" GridPane.columnIndex="1" GridPane.rowIndex="1"/> <Label text="البريد الإلكتروني *" GridPane.columnIndex="0" GridPane.rowIndex="2"/> <TextField fx:id="emailField" promptText="user@example.com" GridPane.columnIndex="1" GridPane.rowIndex="2"/> <Label text="الجنس" GridPane.columnIndex="0" GridPane.rowIndex="3"/> <ComboBox fx:id="genderCombo" promptText="اختر…" GridPane.columnIndex="1" GridPane.rowIndex="3"/> <Label text="نبذة" GridPane.columnIndex="0" GridPane.rowIndex="4" GridPane.valignment="TOP" style="-fx-padding: 4 0 0 0;"/> <TextArea fx:id="bioArea" promptText="مقدمة قصيرة…" prefRowCount="4" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="4"/> <Label fx:id="errorLabel" styleClass="error-label" GridPane.columnIndex="1" GridPane.rowIndex="5"/> </GridPane> </center> <!-- ===== الأسفل: أزرار الإجراءات ===== --> <bottom> <HBox spacing="12" alignment="CENTER_RIGHT"> <padding><Insets bottom="16" right="24"/></padding> <Button text="مسح" onAction="#handleClear"/> <Button text="إرسال" defaultButton="true" styleClass="primary-btn" onAction="#handleSubmit"/> </HBox> </bottom> <!-- ===== اليمين: لوحة الملخص (مخفية حتى الإرسال) ===== --> <right> <VBox fx:id="summaryPane" spacing="8" visible="false" managed="false" styleClass="summary-pane"> <padding><Insets top="24" right="20" bottom="24" left="20"/></padding> <Label text="السجل المُرسَل" styleClass="summary-title"/> <Label fx:id="summaryName"/> <Label fx:id="summaryEmail"/> <Label fx:id="summaryGender"/> <Label fx:id="summaryBio" wrapText="true" maxWidth="200"/> </VBox> </right> </BorderPane>
اضبط managed="false" مع visible="false" على أي لوحة تريد إخفاءها في البداية. visible="false" وحده يجعل اللوحة غير مرئية لكنه يحجز مساحتها، مما يخلف فجوة فارغة مزعجة. ضبط الخاصيتين معًا يُزيلها تمامًا من تدفق التخطيط حتى تحتاجها.

المتحكم: ContactController.java

المتحكم هو المكان الذي يعيش فيه كل سلوك التطبيق. يُنشئه FXMLLoader — لا كودك أنت — لذا يُحقَن كل حقل مُعلَّم بـ @FXML تلقائيًا قبل تشغيل initialize().

package com.example.contactapp; import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.scene.layout.VBox; import java.net.URL; import java.util.ResourceBundle; import java.util.regex.Pattern; public class ContactController implements Initializable { // ---- العناصر المُحقَنة ---- @FXML private TextField firstNameField; @FXML private TextField lastNameField; @FXML private TextField emailField; @FXML private ComboBox<String> genderCombo; @FXML private TextArea bioArea; @FXML private Label errorLabel; // ---- لوحة الملخص ---- @FXML private VBox summaryPane; @FXML private Label summaryName; @FXML private Label summaryEmail; @FXML private Label summaryGender; @FXML private Label summaryBio; private static final Pattern EMAIL_RE = Pattern.compile("^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}$"); @Override public void initialize(URL location, ResourceBundle resources) { genderCombo.setItems( FXCollections.observableArrayList("ذكر", "أنثى", "أفضل عدم الذكر") ); } @FXML private void handleSubmit() { String firstName = firstNameField.getText().trim(); String lastName = lastNameField.getText().trim(); String email = emailField.getText().trim(); String gender = genderCombo.getValue(); String bio = bioArea.getText().trim(); // ---- التحقق من الصحة ---- if (firstName.isEmpty() || lastName.isEmpty()) { showError("الاسم الأول والاسم الأخير مطلوبان."); return; } if (!EMAIL_RE.matcher(email).matches()) { showError("يرجى إدخال عنوان بريد إلكتروني صحيح."); return; } clearError(); Contact c = new Contact(firstName, lastName, email, gender != null ? gender : "—", bio); // ---- ملء الملخص ---- summaryName.setText(c.getFirstName() + " " + c.getLastName()); summaryEmail.setText(c.getEmail()); summaryGender.setText("الجنس: " + c.getGender()); summaryBio.setText(bio.isEmpty() ? "(لا توجد نبذة)" : bio); summaryPane.setVisible(true); summaryPane.setManaged(true); } @FXML private void handleClear() { firstNameField.clear(); lastNameField.clear(); emailField.clear(); genderCombo.setValue(null); bioArea.clear(); clearError(); summaryPane.setVisible(false); summaryPane.setManaged(false); } private void showError(String message) { errorLabel.setText(message); } private void clearError() { errorLabel.setText(""); } }

نقطة الدخول: MainApp.java

فئة التطبيق بسيطة ومختصرة. مهمتها الوحيدة هي تحميل FXML وإرفاق المشهد وعرض المرحلة.

package com.example.contactapp; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class MainApp extends Application { @Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader = new FXMLLoader( getClass().getResource("contact-form.fxml") ); Parent root = loader.load(); primaryStage.setTitle("تسجيل جهة الاتصال"); primaryStage.setScene(new Scene(root)); primaryStage.setResizable(true); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
يجب أن يتطابق مسار المورد مع الحزمة. إذا كان ملف FXML في resources/com/example/contactapp/contact-form.fxml، فإن getClass().getResource("contact-form.fxml") يحلّ بشكل صحيح لأن MainApp تعيش في نفس الحزمة. استخدام مسار مطلق مثل "/contact-form.fxml" سيفشل في وقت التشغيل ما لم يكن الملف في جذر مسار الفئات.

CSS مبسّط: styles.css

CSS في JavaFX هو مجموعة فرعية من CSS 2 مع خصائص خاصة بـ JavaFX مسبوقة بـ -fx-. تجعل ورقة الأنماط الصغيرة النموذج يبدو أنيقًا دون إرباك FXML أو المتحكم.

.primary-btn { -fx-background-color: #2563eb; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 6 18 6 18; } .primary-btn:hover { -fx-background-color: #1d4ed8; } .error-label { -fx-text-fill: #dc2626; -fx-font-size: 12px; } .summary-pane { -fx-background-color: #f0f9ff; -fx-border-color: #bae6fd; -fx-border-width: 1; } .summary-title { -fx-font-size: 14px; -fx-font-weight: bold; }

قرارات التصميم الرئيسية

  • الفصل بين المسؤوليات: التخطيط في FXML والسلوك في المتحكم والبيانات في النموذج. لا تعرف أي من هذه الملفات الثلاثة التفاصيل الداخلية للأخرى.
  • التحقق قبل بناء النموذج: يُبنى كائن Contact فقط بعد اجتياز جميع الفحوصات. هذا يُبقي النموذج صحيحًا دائمًا — مبدأ يتسع بشكل جيد عند إضافة الحفظ في قاعدة البيانات لاحقًا.
  • تبديل managed و visible: تستخدم لوحة الملخص كلا العلَمَين لكي لا تهدر النافذة مساحة قبل الإرسال، وتتوسع بشكل طبيعي لتكشف اللوحة بعده.
  • تجميع التعبير النمطي مرة واحدة: نمط البريد الإلكتروني ثابت static final، ولا يُعاد تجميعه عند كل نقرة إرسال.

الخلاصة

لقد بنيت الآن تطبيق JavaFX كامل قائم على نموذج: تخطيط FXML نظيف يجمع BorderPane وGridPane وHBox وVBox؛ ومتحكم يتعامل مع التهيئة والتحقق والإرسال والمسح؛ ونموذج Java بسيط؛ وورقة أنماط CSS تُطبّق العلامة التجارية دون لمس كود Java. هذه البنية — FXML للهيكل، والمتحكم للسلوك، والنموذج للبيانات — هي النمط المعياري الذي ستستخدمه وتراه في كل قاعدة كود JavaFX احترافية.