أساسيات JavaFX ورسم المشهد

نموذج الخيوط في JavaFX

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

نموذج الخيوط في JavaFX

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

لماذا خيط واحد للواجهة الرسومية؟

الشجرة المشهدية هي شجرة من كائنات Node. عندما تجتاز خط التصيير تلك الشجرة لإنتاج إطار، يجب أن يرى لقطة متسقة. لو عدّل خيطان في وقت واحد خصائص عقد مختلفة، فقد يصادف المصيّر تغييرات نُفِّذت جزئيًا: زر في منتصف إعادة تحجيمه بينما يُستبدَل تسميته. سيكون الفساد البصري الناتج، أو ما هو أسوأ من ذلك — ConcurrentModificationException — بالغ الصعوبة في إعادة إنتاجه وتصحيحه.

بدلًا من إجبار كل وصول للعقد على المرور بقفل (lock) — الذي سيُتسلسل واجهة المستخدم بالكامل ويجعل أخطاء الخيوط صعبة الاكتشاف في الوقت المناسب — يتبع JavaFX النهج الأبسط والمُثبَت: تعيين خيط واحد يملك الشجرة المشهدية، وجعل الإطار يكتشف الكتابات من أي خيط آخر ويرفضها في وقت التشغيل.

يُنشئ مُطلق الإطار خيط JavaFX. عند استدعاء Application.launch()، يبدأ JavaFX خيط JAT ويستدعي start(Stage) عليه. كل ما تفعله داخل start()، وجميع معالجات الأحداث المربوطة بعناصر التحكم، تعمل تلقائيًا على JAT. لن تحتاج للتفكير في الخيوط إلا عند إدخال عمل في الخلفية.

ما يحدث عند انتهاك القاعدة

إذا حاولت تعديل عقدة في شجرة المشهد الحية من خيط خلفية، يرمي JavaFX استثناء IllegalStateException برسالة "Not on FX application thread". في إصدارات وقت التشغيل القديمة قد يُفسد الانتهاك الحالة بصمت بدلًا من الرمي — وهذا أسوأ بكثير. في كلتا الحالتين الإصلاح واحد: أعد نقل التحديث إلى JAT.

// خطأ — تعديل Label من خيط خلفية new Thread(() -> { String result = doHeavyWork(); // يعمل على خيط الخلفية — لا بأس statusLabel.setText(result); // تعطل: IllegalStateException }).start();

Platform.runLater — الجسر بين الخيوط

Platform.runLater(Runnable) هي الآلية الأساسية لإعادة نشر العمل على JAT. تستدعيها من أي خيط وتمرر Runnable؛ يضع وقت تشغيل JavaFX التشغيلي في قائمة الانتظار وينفّذه على JAT في دورة الإطار (النبضة) التالية. الاستدعاء غير مُعيق: يستمر خيط الخلفية فورًا بعد أن تعود runLater().

import javafx.application.Platform; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; public class BackgroundTask { public static void runWithProgress(Label statusLabel, ProgressBar bar) { new Thread(() -> { // المرحلة 1 — عمل في الخلفية (آمن، بدون لمس واجهة المستخدم) Platform.runLater(() -> { statusLabel.setText("جارٍ البدء..."); bar.setProgress(-1); // غير محدد }); String result = performExpensiveOperation(); // يعلق هنا، خارج JAT // المرحلة 2 — تحديث واجهة المستخدم عند الانتهاء (عودة إلى JAT) Platform.runLater(() -> { statusLabel.setText("تم: " + result); bar.setProgress(1.0); }); }).start(); } private static String performExpensiveOperation() { try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "تمت معالجة 42 سجلًا"; } }

لاحظ الفصل بين المسؤوليات: تحتوي دالة lambda للـ Thread على الحساب فقط، ويحتوي كلٌّ من كتلتَي runLater على تحديثات واجهة المستخدم فقط. هذا النمط هو العمود الفقري لكل تطبيق JavaFX متجاوب.

Platform.runLater مقابل Platform.runAndWait

يوفر JavaFX أيضًا Platform.runAndWait(Runnable). على خلاف runLater، فهو يعلّق الخيط المُستدعي حتى ينتهي التشغيلي من التنفيذ على JAT. نادرًا ما يكون هذا هو الاختيار الصحيح:

  • runLater: أطلق وانسَ؛ يستمر خيط الخلفية فورًا. استخدمه في كل الحالات تقريبًا.
  • runAndWait: يعلّق خيط الخلفية حتى تتم المعالجة. استخدمه فقط حين تحتاج فعلًا قيمة إرجاع محسوبة على JAT قبل المتابعة، ومن خيط غير JAT فقط. استدعاء runAndWait من JAT نفسه يرمي استثناءً.
إغراق JAT بنداءات runLater مشكلة أداء حقيقية. إذا استدعى خيط خلفية Platform.runLater آلاف المرات في الثانية (مثلًا من حلقة محاكاة مشددة)، تمتلئ قائمة أحداث JAT أسرع مما تستطيع الإفراغ. ستتجمد واجهة المستخدم. الحل هو تقليل التحديثات: اجمع النتائج في متغير مشترك وانشر تحديثًا واحدًا لكل إطار رسوم متحركة، أو استخدم javafx.concurrent.Task الذي يتعامل مع هذا تلقائيًا.

التحقق مما إذا كنت بالفعل على JAT

أحيانًا يمكن استدعاء دالة بشكل مشروع إما من JAT أو من خيط خلفية. استخدم Platform.isFxApplicationThread() للتفرع بأمان:

public void safeSetText(Label label, String text) { if (Platform.isFxApplicationThread()) { label.setText(text); // بالفعل على JAT، استدع مباشرة } else { Platform.runLater(() -> label.setText(text)); // أرسل إلى JAT } }

javafx.concurrent.Task — التجريد رفيع المستوى

لمعظم المهام الخلفية الحقيقية، Task<V> (في حزمة javafx.concurrent) أنظف من الخيوط الخام مع نداءات runLater يدوية. تلفّ Task عقد الخيوط لك: تتجاوز call() لعمل الخلفية، وتربط عناصر واجهة المستخدم بخصائصها القابلة للملاحظة (messageProperty، progressProperty، valueProperty). يضمن الإطار تحديث الخصائص على JAT.

import javafx.concurrent.Task; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; public class LoadDataTask extends Task<String> { @Override protected String call() throws Exception { updateMessage("جارٍ تحميل البيانات..."); // آمن للخيوط: ينشر على JAT تلقائيًا updateProgress(0, 100); // محاكاة العمل for (int i = 1; i <= 100; i++) { Thread.sleep(20); updateProgress(i, 100); } return "تم تحميل مجموعة البيانات: 100 صف"; } // ربطها في متحكمك: public static void start(Label statusLabel, ProgressBar bar) { LoadDataTask task = new LoadDataTask(); statusLabel.textProperty().bind(task.messageProperty()); bar.progressProperty().bind(task.progressProperty()); task.setOnSucceeded(e -> { statusLabel.textProperty().unbind(); statusLabel.setText(task.getValue()); }); new Thread(task).start(); } }
فضّل Task على الخيوط الخام للعمل الخلفي. يمنحك دعمًا للإلغاء (task.cancel())، ومعالجة الاستثناءات (setOnFailed)، وتتبع التقدم، وتوجيه JAT الصحيح — كل هذا دون نداء واحد يدوي لـ runLater.

نظام النبضات: متى تتحدث واجهة المستخدم فعلًا؟

لا يُعيد JavaFX تصيير المشهد بعد كل تغيير في الخاصية. بدلًا من ذلك يُدمج التغييرات ويُعيد الرسم على نبضة منتظمة — عادةً بمعدل 60 هيرتز، متزامنة مع معدل تحديث الشاشة عبر ردود نداء AnimationTimer. عند استدعاء runLater، يعمل تشغيليك خلال مرحلة معالجة أحداث النبضة التالية، وتنعكس التغييرات التي تجريها في مرحلة التصيير لنفس الإطار. لهذا السبب تبدو نداءات runLater المتعددة المنشورة بسرعة من خيط خلفية أنها "تُدمَج" بصريًا — فقد تُنفَّذ جميعها في نبضة واحدة.

الخلاصة

يُبنى نموذج خيوط JavaFX على قاعدة واحدة: الشجرة المشهدية تُلمَس فقط على JAT. Platform.runLater(Runnable) هي صمام الأمان — يُدرج تحديث واجهة مستخدم من أي خيط. للعمل الخلفي غير التافه، تُطبّق Task<V> فوق هذه الأداة البدائية وتمنحك تقدمًا قابلًا للملاحظة وإلغاءً ومعالجة أخطاء. احترام هذه الحدود هو ما يُبقي تطبيق JavaFX سريعًا وصحيحًا وخاليًا من الأعطال. في الدرس الأخير التالي ستطبّق كل ما تعلمته من هذا البرنامج التعليمي ببناء تطبيق JavaFX صغير كامل.