الربط والأحداث والتنسيق في JavaFX

معالجة الأحداث بعمق

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

معالجة الأحداث بعمق

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

تسلسل أنواع الأحداث

كل حدث في JavaFX هو نسخة من javafx.event.Event، غير أن النظام يستخدم تسلسلًا هرميًا غنيًا من الأنواع لتصنيفها:

  • InputEvent — جذر أحداث مدخلات المستخدم.
  • MouseEvent — يغطي MOUSE_PRESSED وMOUSE_RELEASED وMOUSE_CLICKED وMOUSE_MOVED وMOUSE_DRAGGED وMOUSE_ENTERED وMOUSE_EXITED وغيرها.
  • KeyEventKEY_PRESSED وKEY_RELEASED وKEY_TYPED. استخدم KEY_TYPED لإدخال الأحرف وKEY_PRESSED لمفاتيح التحكم والاختصارات.
  • ScrollEvent — حركات التمرير بعجلة الفأرة أو لوحة التتبع.
  • DragEvent — دورة حياة السحب والإفلات: DRAG_DETECTED وDRAG_OVER وDRAG_DROPPED وDRAG_DONE.
  • ActionEvent — تطلقه عناصر التحكم مثل Button وMenuItem وCheckBox عند تفعيلها (نقر أو Enter أو مسافة).
  • WindowEventWINDOW_SHOWING وWINDOW_HIDDEN وWINDOW_CLOSE_REQUEST.

تُشكّل أنواع الأحداث بدورها تسلسلًا هرميًا. فـMouseEvent.MOUSE_CLICKED ابن لـMouseEvent.ANY، الذي هو ابن لـInputEvent.ANY، الذي هو ابن لـEvent.ANY. وتسجيل معالج لنوع أب يعني أنه سيستقبل جميع أحداث الأنواع الفرعية أيضًا.

دورة إيفاء الحدث: الالتقاط والفقاعة

حين يُطلَق حدث على عنصر ما، لا يُسلّمه JavaFX مباشرةً. بل يسير عبر سلسلة إيفاء الحدث — قائمة عناصر تبدأ من جذر شجرة المشهد (Stage → Scene → العنصر الجذر) نزولًا حتى العنصر المستهدف، ثم صعودًا مجددًا:

  1. مرحلة الالتقاط (من الأعلى إلى الهدف): يسير الحدث نزولًا في السلسلة. بإمكان أي مرشّح أحداث مسجَّل في الطريق فحص الحدث أو استهلاكه قبل أن يصل إلى هدفه.
  2. مرحلة الفقاعة (من الهدف إلى الأعلى): يرتفع الحدث مجددًا عبر السلسلة. يستقبله أي معالج أحداث مسجَّل في الطريق بترتيب عكسي.
التمييز الجوهري: تعمل المرشّحات في مرحلة الالتقاط (من الأعلى إلى الأسفل). وتعمل المعالجات في مرحلة الفقاعة (من الأسفل إلى الأعلى). ترى المرشّحات الحدث أولًا، ما يجعلها المكان الأنسب لاعتراض الحدث أو منعه قبل أن تعالجه أي عنصر فرعي.

تسجيل معالجات الأحداث

أكثر طريقة شائعة للاستجابة للحدث هي استخدام addEventHandler(EventType, EventHandler). تستقبل دالة Lambda كائن الحدث المكتوب بنوعه.

import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; public class HandlerDemo extends Application { @Override public void start(Stage stage) { Button btn = new Button("Click me"); // معالج على الزر — يُطلَق أثناء مرحلة الفقاعة btn.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { System.out.println("معالج الزر — الزر: " + event.getButton() + " x=" + event.getX() + " y=" + event.getY()); }); // معالج على المشهد — يُطلَق أيضًا أثناء الفقاعة، بعد معالج الزر StackPane root = new StackPane(btn); Scene scene = new Scene(root, 300, 200); scene.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { System.out.println("معالج المشهد — الهدف كان: " + event.getTarget()); }); stage.setScene(scene); stage.setTitle("معالجات الأحداث"); stage.show(); } public static void main(String[] args) { launch(args); } }

حين تنقر على الزر ستجد أن معالجه يطبع أولًا ثم يطبع معالج المشهد. هذه هي مرحلة الفقاعة في العمل.

خصائص الاختصار مقابل addEventHandler: تعرض عناصر التحكم دوال ضبط مختصرة مثل btn.setOnAction(...) وbtn.setOnMouseClicked(...). هذه أغلفة تسجّل معالجًا واحدًا. تُجزئ للحالات البسيطة، لكن addEventHandler يتيح لك إرفاق معالجات مستقلة متعددة لنوع الحدث نفسه — مفيد حين تحتاج أجزاء مختلفة من التطبيق الاستجابة للعنصر ذاته.

تسجيل مرشّحات الأحداث

تُسجَّل المرشّحات بـaddEventFilter(EventType, EventHandler) على عنصر سلفي (أب أو جد). تُطلَق أثناء مرحلة الالتقاط، قبل أن يصل الحدث إلى هدفه. ما يجعلها مثالية لـ:

  • التحقق من المدخلات (حظر المفاتيح غير الرقمية في حقل نصي).
  • تسجيل أو تدقيق جميع التفاعلات من نوع معين داخل شجرة فرعية.
  • تعطيل منطقة كاملة من واجهة المستخدم مؤقتًا دون تعديل كل عنصر على حدة.
import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.VBox; // السماح بالأرقام فقط في حقل النص عبر مرشّح على المستوى الأب VBox form = new VBox(10); TextField amountField = new TextField(); form.getChildren().add(amountField); // مرشّح مسجَّل على العنصر الأب — يُطلَق أثناء الالتقاط (قبل أن يرى amountField المفتاح) form.addEventFilter(KeyEvent.KEY_TYPED, event -> { String ch = event.getCharacter(); if (!ch.matches("[0-9]")) { event.consume(); // يوقف الحدث — لن يصل إلى حقل النص ولن يرتفع بالفقاعة } });

استهلاك الأحداث

استدعاء event.consume() يوقف رحلة الحدث عبر سلسلة الإيفاء — فلن تستقبله أي مرشّحات أو معالجات إضافية على العناصر الأب أو الفرعية. هذه هي آلية حظر السلوك الافتراضي.

الاستهلاك المبكر يُعطّل الأشياء. إن استهلكت MouseEvent في مرشّح على اللوحة الجذرية، لن تُطلق أي زر بداخلها نداءات onAction الخاصة به أبدًا. اجعل نطاق مرشّحاتك ضيّقًا قدر الإمكان، ولا تستهلك إلا حين يكون لديك سبب ملموس.

KeyEvent في التطبيق العملي

تحمل أحداث لوحة المفاتيح كائن KeyCode (المفتاح الفعلي) وسلسلة حرفية. للاختصارات، استخدم KEY_PRESSED وتحقق من الكود وأعلام المُعدِّلات معًا:

scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { if (event.isControlDown() && event.getCode() == KeyCode.S) { System.out.println("Ctrl+S مضغوط — جاري الحفظ..."); event.consume(); // تمنع نظام التشغيل أو معالجات أخرى من التصرف } });

لإدخال النصوص الحرة، استمع إلى KEY_TYPED. تُعيد دالة getCharacter() الخاصة به الحرف القابل للطباعة بعد معالجة منهجية إدخال المنصة، فتتعامل بشكل صحيح مع لوحات المفاتيح الدولية.

إزالة المعالجات والمرشّحات

لكل استدعاء addEventHandler وaddEventFilter نظير متماثل removeEventHandler / removeEventFilter. لإزالة مستمع يجب الاحتفاظ بمرجع لكائن EventHandler الأصلي — دالة Lambda مجهولة مُمرَّرة إلى add لا يمكن تمريرها لاحقًا إلى remove.

EventHandler<MouseEvent> highlight = event -> node.setStyle("-fx-opacity: 0.7;"); node.addEventHandler(MouseEvent.MOUSE_ENTERED, highlight); node.addEventHandler(MouseEvent.MOUSE_EXITED, event -> node.setStyle("")); // لاحقًا، لإيقاف تأثير التمييز: node.removeEventHandler(MouseEvent.MOUSE_ENTERED, highlight);

الخلاصة

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