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

عناصر التحكم الشائعة

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

عناصر التحكم الشائعة

يقوم JavaFX على مفهوم الـ Scene Graph (رسم بياني للمشهد): كل عنصر مرئي هو Node، وتُرتَّب هذه العقد في شجرة تنتهي عند Stage (نافذة نظام التشغيل). قبل أن تصمّم أي شيء معقد عليك أن تكون متمكّنًا من الأدوات الأربع الأساسية في كل نموذج: Button وLabel وTextField وCheckBox. يستعرض هذا الدرس كلًّا منها — واجهة برمجته وآلية الأحداث فيه والأنماط التي يستخدمها المطورون المحترفون عند ربطها معًا.

هيكل التطبيق الأساسي

يمتد كل برنامج JavaFX من javafx.application.Application ويتجاوز التابع start(Stage primaryStage). هذا التابع هو نقطة دخول كود الواجهة؛ تستدعيه المنصة على خيط JavaFX Application Thread بعد تشغيل البيئة.

import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class ControlsDemo extends Application { @Override public void start(Stage stage) { VBox root = new VBox(12); // مسافة 12 بكسل بين العناصر root.setPadding(new javafx.geometry.Insets(16)); // تُضاف عناصر التحكم أدناه ... Scene scene = new Scene(root, 400, 300); stage.setTitle("Controls Demo"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }
خيط JavaFX Application Thread: يجب أن يعمل كل كود الواجهة — إنشاء العقد وقراءة حالتها وتعديلها — على خيط JavaFX Application Thread. إذا أنشأت خيطًا خلفيًا وأردت تحديث الواجهة منه، فاحتِ تحديثك داخل Platform.runLater(() -> { ... }).

Label

يُعدّ Label عقدة نصية غير تفاعلية. مهمته الرئيسية عرض معلومات للقراءة فقط أو كتسمية لعنصر تحكم آخر. يمكنك إنشاؤه بسلسلة نصية حرفية أو ربطه بخاصية ديناميكية.

import javafx.scene.control.Label; Label greeting = new Label("مرحبًا بك في التطبيق"); greeting.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;"); // ربط التسمية بحقل إدخال لدعم إمكانية الوصول Label nameLabel = new Label("الاسم الكامل:"); nameLabel.setLabelFor(nameField); // nameField معرَّف أدناه

للنصوص الديناميكية يُفضَّل استخدام الربط بالخاصية (property binding) بدلًا من استدعاء setText() مرارًا. على سبيل المثال، لعرض عدد الأحرف آنيًا:

Label countLabel = new Label(); // ربط نص التسمية بطول نص الخاصية في TextField countLabel.textProperty().bind( nameField.textProperty().length().asString("%d حرف") );

TextField

يقبل TextField إدخال النص على سطر واحد. أهم عضو فيه هو textProperty()، وهي StringProperty يمكنك مراقبتها أو ربطها أو قراءتها في أي وقت.

import javafx.scene.control.TextField; TextField nameField = new TextField(); nameField.setPromptText("أدخل اسمك الكامل"); // نص توضيحي nameField.setPrefWidth(280); // قراءة القيمة عند الطلب String current = nameField.getText(); // الاستجابة الفورية لكل ضغطة مفتاح nameField.textProperty().addListener((obs, oldVal, newVal) -> { System.out.println("تغيّر من: " + oldVal + " إلى: " + newVal); }); // الاستجابة عند ضغط المستخدم على Enter nameField.setOnAction(e -> System.out.println("تم الإرسال: " + nameField.getText()));
استخدم الخاصية النصية بدلًا من الاستطلاع. استدعاء getText() داخل معالج زر مناسب للنماذج الصغيرة، لكن إرفاق ChangeListener بـtextProperty() يجعل منطقك تفاعليًا ويبسّط التحقق من الصحة — تتحقق أثناء كتابة المستخدم لا فقط عند الإرسال.

إذا أردت تقييد الإدخال على الأرقام يمكنك تصفية التغيير قبل تطبيقه باستخدام TextFormatter:

import javafx.scene.control.TextFormatter; import javafx.util.converter.IntegerStringConverter; TextFormatter<Integer> formatter = new TextFormatter<>( new IntegerStringConverter(), 0, change -> change.getControlNewText().matches("-?\\d*") ? change : null ); ageField.setTextFormatter(formatter);

Button

يُطلق Button حدثًا من النوع ActionEvent عند النقر عليه. ترفق معالجًا بـsetOnAction(EventHandler<ActionEvent>) أو بتعبير lambda منذ Java 8.

import javafx.scene.control.Button; Button submitBtn = new Button("إرسال"); // معالج lambda — موجز وأسلوبي submitBtn.setOnAction(e -> handleSubmit()); // زر افتراضي: يُفعَّل بالضغط على Enter في أي مكان في المشهد submitBtn.setDefaultButton(true); // زر إلغاء: يُفعَّل بالضغط على Escape Button cancelBtn = new Button("إلغاء"); cancelBtn.setCancelButton(true); cancelBtn.setOnAction(e -> stage.close());

يمكن تعطيل الأزرار برمجيًا، وهو نمط شائع لمنع الإرسال قبل ملء جميع الحقول المطلوبة:

// تعطيل الزر كلما كان حقل الاسم فارغًا submitBtn.disableProperty().bind( nameField.textProperty().isEmpty() );
لماذا الربط أفضل من جمل if: حين تربط disableProperty() بتعبير مشتق من حالة النموذج، يُعيد الزر تفعيل نفسه تلقائيًا فور تغيّر الشرط — دون الحاجة للتعامل يدويًا مع المستمعين.

CheckBox

يُمثّل CheckBox خيارًا بولياني. خاصيته الأساسية هي selectedProperty()، وهي BooleanProperty. افتراضيًا، يملك مربع الاختيار حالتين: محدد وغير محدد. إذا استدعيت setAllowIndeterminate(true) اكتسب حالة ثالثة وهي الحالة غير المحددة (الشرطة التي تُعرض عند تحديد "بعض العناصر" في أشجار الملفات).

import javafx.scene.control.CheckBox; CheckBox agreeBox = new CheckBox("أوافق على الشروط والأحكام"); // قراءة الحالة عند الطلب boolean agreed = agreeBox.isSelected(); // الاستجابة للتغييرات agreeBox.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> { System.out.println("الموافقة: " + isNowSelected); }); // ربط الزر ليشترط الموافقة submitBtn.disableProperty().bind( nameField.textProperty().isEmpty().or(agreeBox.selectedProperty().not()) );

تجميع الكل: نموذج تسجيل بسيط

تابع start() الكامل التالي يربط الأدوات الأربع في نموذج صغير فعّال:

@Override public void start(Stage stage) { Label nameLabel = new Label("الاسم الكامل:"); TextField nameField = new TextField(); nameField.setPromptText("أدخل اسمك"); Label statusLabel = new Label(); statusLabel.textProperty().bind( nameField.textProperty().length().asString("الأحرف: %d") ); CheckBox agreeBox = new CheckBox("قبول الشروط"); Button submitBtn = new Button("تسجيل"); submitBtn.setDefaultButton(true); submitBtn.disableProperty().bind( nameField.textProperty().isEmpty().or(agreeBox.selectedProperty().not()) ); submitBtn.setOnAction(e -> { System.out.println("تم التسجيل: " + nameField.getText()); nameField.clear(); agreeBox.setSelected(false); }); VBox root = new VBox(10, nameLabel, nameField, statusLabel, agreeBox, submitBtn); root.setPadding(new javafx.geometry.Insets(20)); stage.setScene(new Scene(root, 360, 240)); stage.setTitle("التسجيل"); stage.show(); }

الأخطاء الشائعة

لا تنشئ عقدًا أو تعدّلها خارج خيط JavaFX Application Thread. بناء Label داخل new Thread(() -> { ... }).start() سيتصرف بصمت بشكل خاطئ أو سيطرح IllegalStateException. خصّص الخيط الخلفي للحوسبة فقط وادفع تحديث الواجهة برجوعه عبر Platform.runLater().
  • لا تستدع getText() داخل مستمع الخاصية لقراءة القيمة الجديدة — استخدم المعامل newVal المُوفَّر مباشرةً للمستمع.
  • تجنّب CSS المدمج للأنماط المشتركة — ضع الأنماط المتكررة في ملف .css خارجي وحمّله عبر scene.getStylesheets().add(...).
  • يُفضَّل setPromptText() على تسميات العناصر النائبة — نص التلميح مدمج في TextField ويختفي بنظافة عندما يبدأ المستخدم بالكتابة.

الخلاصة

تشكّل الأدوات الأربع التي تناولها هذا الدرس العمود الفقري لكل نموذج JavaFX تقريبًا. يعرض Label النص ويرتبط بخصائص أخرى. يلتقط TextField مدخلات المستخدم ويكشف عن textProperty() تفاعلية. يُطلق Button الإجراءات ويمكن تفعيله أو تعطيله عبر الربط. يُنمذج CheckBox خيارًا بوليانيًا مع حالة غير محددة اختيارية. في الدرس القادم ستتعامل مع بيانات أكثر ثراءً باستخدام ListView وTableView وComboBox.