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

العقد وبنية الرسم البياني للمشهد

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

العقد وبنية الرسم البياني للمشهد

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

نوعان أساسيان من العقد

كل عنصر في مشهد JavaFX يرث من الفئة المجردة javafx.scene.Node. تنقسم هذه الفئة بعد ذلك إلى فرعين:

  • عقد الأب (Parent nodes) — فئات فرعية من javafx.scene.Parent. تمتلك قائمة أبناء قابلة للرصد وتتولى تخطيط أبنائها. لا تُنشئ Parent مباشرةً أبدًا؛ بل تعمل مع فئات فرعية محددة كـ Group وRegion وPane وVBox وHBox.
  • عقد الأوراق (Leaf nodes) — فئات فرعية مباشرة من Node لا يمكنها امتلاك أبناء. الأشكال (Rectangle، Circle، Line) وImageView وCanvas وMediaView كلها عقد أوراق.
عناصر التحكم تقع في المنتصف: عناصر واجهة المستخدم كـ Button وTextField وListView ترث من Control، التي ترث من Region، التي ترث بدورها من Parent. لذا فهي عقد أب داخليًا — تحتوي على عقد مظهرها الخاص — لكنك تتعامل معها كعقد أوراق من منظور الرسم البياني لمشهدك لأنك لا تضيف إليها أبناءً مباشرةً.

قاعدة ملكية العقدة

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

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

بناء الرسم البياني في الكود

أبسط حاوية هي Group. لا تُطبّق أي منطق تخطيط — يُحدَّد موضع الأبناء عبر خصائص layoutX/layoutY أو التحويلات الخاصة بهم. تكدّس StackPane أبناءها فوق بعضهم، بتوسيط افتراضي. تُرتّب HBox أبناءها في صف أفقي، بينما تُرتّبهم VBox رأسيًا.

فيما يلي رسم بياني صغير للمشهد مبني بالكامل في Java:

import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.shape.Circle; import javafx.stage.Stage; public class SceneGraphDemo extends Application { @Override public void start(Stage primaryStage) { // --- عقد الأوراق --- Label heading = new Label("Scene Graph Demo"); Circle dot = new Circle(20); // نصف قطر 20 بكسل Button okBtn = new Button("OK"); Button cancelBtn = new Button("Cancel"); // --- أب وسيط: زرّان جنبًا إلى جنب --- HBox buttonRow = new HBox(10, okBtn, cancelBtn); // مسافة 10 بكسل // --- الأب الجذر: كل شيء رأسيًا --- VBox root = new VBox(12, heading, dot, buttonRow); // مسافة 12 بكسل root.setPadding(new javafx.geometry.Insets(16)); Scene scene = new Scene(root, 300, 200); primaryStage.setTitle("Graph Demo"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }

تبدو الشجرة الناتجة على النحو التالي:

  • VBox (جذر / جذر المشهد)
    • Label — "Scene Graph Demo"
    • Circle — نصف قطر 20
    • HBox
      • Button — "OK"
      • Button — "Cancel"

قائمة الأبناء

يعرض كل Parent أبناءه عبر getChildren()، التي تُعيد ObservableList<Node>. يمكنك التعامل مع هذه القائمة في أي وقت على خيط تطبيق JavaFX ويتحدّث الرسم البياني للمشهد فورًا:

// إضافة عقدة ديناميكيًا Label status = new Label("Saved."); root.getChildren().add(status); // إزالة عقدة محددة root.getChildren().remove(dot); // استبدال جميع الأبناء دفعةً واحدة root.getChildren().setAll(heading, buttonRow);

نظرًا لأن القائمة قابلة للرصد، يستمع نظام التخطيط إلى التغييرات ويجدول تمريرة تخطيط تلقائيًا. لن تحتاج أبدًا إلى استدعاء طريقة "تحديث" يدوية.

كيف تُوجّه البنية التصيير والتخطيط

الرسم البياني للمشهد ليس مجرد بنية بيانات — بل هو المحرك الذي يُشغّل ثلاثة أنظمة فرعية في JavaFX:

  1. التصيير (ترتيب الرسم): تُرسم العقد باجتياز عمق أولًا بترتيب مسبق — الأب قبل أبنائه، والإخوة بترتيب القائمة. العقدة المرسومة لاحقًا تظهر فوق الأخرى. تغيير موضع العقدة في قائمة الأبناء يُغيّر تراصها البصري.
  2. التخطيط: تحسب كل Parent حجم أبنائها وموضعهم خلال تمريرة تخطيط. تبدأ التمريرة من الجذر وتتدرج للأسفل. تُبلّغ عقد الأوراق عن حجمها المفضّل؛ تُخصّص الآباء المساحة وفقًا لقواعدها الخاصة.
  3. انتشار الأحداث: تجتاز أحداث الفأرة ولوحة المفاتيح الشجرة في مرحلتين — الالتقاط (من الجذر إلى الهدف) والفقاعة (bubble) (من الهدف إلى الجذر). فهم شكل الشجرة يُخبرك بالضبط أي العقد ستستقبل حدثًا وبأي ترتيب.
ابقِ شجرتك ضحلة. كل مستوى من مستويات التداخل يُضيف تمريرة تخطيط. خطأ أداء شائع هو تغليف عقدة واحدة في حاويات StackPane أو Group زائدة. إذا كانت الحاوية لا تُقدّم أي غرض تخطيطي أو بصري، فأزلها.

Group مقابل Region — اختيار الأب الصحيح

Group وRegion هما فئتا الأب الأساسيتان اللتان ستختار بينهما في معظم الأوقات:

  • Group — لا تخطيط، لا خلفية، لا حشو. حجمها يساوي الحدود المجمّعة لأبنائها. استخدمها لأسطح الرسم أو أهداف الحركة أو عندما تريد تطبيق تحويل واحد على مجموعة أشكال.
  • Region (وفئاتها الفرعية Pane، VBox، HBox، BorderPane، GridPane، إلخ) — تمتلك خلفية وحشوًا وحدًا وتنسيقًا CSS. استخدمها لجميع أعمال تخطيط واجهة المستخدم.

أنظمة الإحداثيات والتحويلات

لكل عقدة نظام إحداثيات محلي خاص بها. نظام إحداثيات الأب هو الفضاء الذي يُوضع فيه أبناؤه. عندما تضبط node.setLayoutX(50)، فأنت تضع العقدة على بُعد 50 بكسل من يسار أصل الأب، لا من أصل المشهد. التحويلات (Translate، Rotate، Scale) المطبّقة على الأب تتتالى على جميع أبنائه، ولهذا يُؤدي تدوير Group تدوير جميع الأشكال المجمّعة فيه معًا.

الخلاصة

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