اذهب إلى المحتوى

إنشاء تطبيق جافا إف إكس JavaFX بسيط


رضوى العربي

غالبية مُستخدمِي الحاسوب غَيْر مُعتَادون في العموم على برامج سطر الأوامر (command-line programs)، حيث يَجدُون أسلوب التَفاعُل القائم على تَناوُب الحاسوب والمُستخدِم على كتابة النصوص غَيْر مألوف. منذ منتصف الثمانينات، أُتيحَت الحواسيب المنزلية المُدعِّمة لواجهات المُستخدِم الرُسومية (graphical user interfaces)، والتي تُوفِّر للمُستخدِم واجهة أكثر ثراءً يَتَمكَّن من خلالها من اِستخدَام أدوات إِدْخَال مُتعدِّدة مثل الفأرة ولوحة المفاتيح وغيرها للتَفاعُل مع مجموعة من مُكوِّنات الواجهة مثل النوافذ والقوائم والأزرار وصناديق الإِدْخَال النصية وشرائط التمرير وغيرها.

سنتناول خلال هذا القسم بعضًا من أساسيات برمجة الواجهات باِستخدَام مكتبة جافا إف إكس (JavaFX) من خلال دراسة تطبيق بسيط يَعرِض وَاجهة مُكوَّنة من نافذة (window) تَحتوِي على رسالة نصية وثلاثة أزرار كما هو مُبيَّن بالصورة التالية. لاحِظ أننا قد اِستخدَمنا مصطلح تطبيق (application) وليس برنامج (program) ضِمْن هذا السياق.

001HelloWorldFX_Screenshot.png

إذا ضَغَطت على زر "Say Hello"، سيَرُد الحاسوب بالرسالة "Hello World!" أما إذا ضَغَطت على زر "Say Goodbye"، فسيَتَغيَّر نص الرسالة إلى "Goodbye". تستطيع إغلاق التطبيق إما بالنَقْر على زر "Quit" أو على زر غَلْق النافذة.

تطبيقات جافا إف إكس (JavaFX)

تَحتوِي حزمة javafx.application على الصَنْف المُجرّد (abstract class)‏ Application، والذي يَحتوِي على تابع النُسخة (instance method) المُجرّد start()‎ من بين أشياء أخرى. لكي تُنشِئ تطبيق جافا إف إكس (JavaFX application)، ينبغي أن تُعرِّف صنفًا جديدًا مُمثِلًا للتطبيق. لابُدّ أن يتوسَّع (extend) الصَنْف الجديد من الصَنْف Application كما لابُدّ أن يُوفِّر تعريفًا (definition) للتابع start()‎. (اُنظر كُلًا من القسمين الفرعيين "توسيع (extend) الأصناف الموجودة"، و "الأصناف المجردة (abstract classes)" من الفصل "الوراثة والتعددية الشكلية (Polymorphism) والأصناف المجردة (Abstract Classes) في جافا").

سيَحتوِي الصَنْف أيضًا على التابع main()‎، والذي سيتولَّى مُهِمّة تّنْفيذ التطبيق، ويُكْتَب عمومًا على النحو التالي:

public static void main(String[] args) {
    launch(args);
}

عندما يُنفِّذ الحاسوب البرنامج main()‎، سيُنشِئ التابع launch()‎ خيطًا (thread) جديدًا يُعرَف باسم خيط تطبيق جافا إف إكس (JavaFX application thread). كما ذَكَرَنا بالقسم ١.٢، يَضُمّ كل خيط متتالية من التَعْليمَات يُمكِن تّنْفيذها بالتوازي مع خيوط أخرى. ينبغي عمومًا لأي شيفرة تَتَعامَل مع واجهة مُستخدِم رسومية (GUI) أن تَقَع ضِمْن خيط تطبيق جافا إف إكس، وهو ما سيَحدُث أتوماتيكيًا فيما يَتعلَّق بما سنقوم به ضِمْن هذا الفصل، ولكن عندما ننتقل إلى الحديث عن الخيوط (threads) تفصيليًا بالفصل ١٢، سنُطوِّر تطبيقات واجهة مُستخدِم رسومية (GUI) تَستخدِم أكثر من خيط، وعندها ستَّضِح أهمية ذلك. سيُنشِئ التابع launch()‎ الكائن المُمثِل للتطبيق، وهو في الواقع من نفس صنف التابع، ثم سيَستدعِي تابعه start()‎، والذي تُوكَل له مُهِمّة تجهيز واجهة المُستخدِم الرسومية (GUI)، ومن ثَمَّ فَتْح نافذة (window) بالشاشة.

تستطيع إصدارات معينة من الجافا أن تُشغِّل تطبيقات جافا إف إكس (JavaFX) حتى لو لم تَتَضمَّن التابع main()‎، ولكن لا تَعتمِد على ذلك، واِحرِص دومًا على كتابة التابع main()‎ ضِمْن التطبيقات الخاصة بك، وهو ما سنلتزم به.

ها هي شيفرة التطبيق التي سنُناقِشها تفصيليًا بهذا القسم:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.application.Platform;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
import javafx.scene.text.Font;

public class HelloWorldFX extends Application {

    public void start(Stage stage) {

        Label message = new Label("First FX Application!");
        message.setFont( new Font(40) );

        Button helloButton = new Button("Say Hello");
        helloButton.setOnAction( e -> message.setText("Hello World!") );
        Button goodbyeButton = new Button("Say Goodbye");
        goodbyeButton.setOnAction( e -> message.setText("Goodbye!!") );
        Button quitButton = new Button("Quit");
        quitButton.setOnAction( e -> Platform.exit() );

        HBox buttonBar = new HBox( 20, helloButton, goodbyeButton, quitButton );
        buttonBar.setAlignment(Pos.CENTER);
        BorderPane root = new BorderPane();
        root.setCenter(message);
        root.setBottom(buttonBar);

        Scene scene = new Scene(root, 450, 200);
        stage.setScene(scene);
        stage.setTitle("JavaFX Test");
        stage.show();

    } // end start();

    public static void main(String[] args) {
        launch(args);  // Run this Application.
    }

} // end class HelloWorldFX

عادةً ما تَستخدِم تطبيقات جافا إف إكس (JavaFX) أصنافًا عديدة، غالبيتها مُعرَّف بحزم javafx الفرعية (subpackages)، ولهذا ستُلاحِظ وجود عدد كبير من تَعْليمَات الاستيراد import ببداية أي من تطبيقاتها كالمثال بالأعلى. سنَذكُر دومًا الحزمة التي يَقَع بها صَنْف معين عندما نَتَعرَّض له لأول مرة كما تستطيع البحث عن أي صَنْف منها بـ توثيق واجهة برمجة تطبيقات جافا إف إكس (JavaFX API).

يحتوي الصنف المُمثِل للتطبيق HelloWorldFX -بالأعلى- على التابع main المسئول عن بدء تّنْفيذ التطبيق، بالإضافة إلى التابع start()‎. نُضيف عادةً توابعًا آخرى لذلك الصنف بحيث يَستدعِيها التابع start()‎ عند تّنْفيذه. علاوة على ذلك، يَحتوِي الصنف المُجرّد Application على عددًا من التوابع (methods) الآخرى، التي يُمكِنك إعادة تعريفها (override) كالتابعين init()‎ و stop()‎. يَستدعِي النظام init()‎ قبل استدعاء start()‎، بينما يَستدعِى stop()‎ عندما يَكُون التطبيق على وشك الإغلاق. في حين أن تعريفهما ضِمْن الصَنْف Application لا يَتَضمَّن أي شيء فعليّ، تستطيع أن تُعيد تعريفهما لإجراء بعض التهيئة المبدئية (initialization) أو لإجراء أي تنظيف (cleanup) ضروري. ومع ذلك، يُمكِن تّنْفيذ أي تهيئة مطلوبة ضِمْن التابع start()‎، لذا نادرًا ما سنلجأ إليهما.

المرحلة Stage والمشهد Scene ومبيان المشهد SceneGraph

تُوفِّر حزمة javafx.stage الصَنْف Stage، والذي يُمثِل أي كائن منه نافذةً (window) على شاشة الحاسوب. يُنشِئ النظام كائن مرحلة (stage) من ذلك الصَنْف، ويُمرِّره كمُعامِل (parameter) إلى التابع start()‎. يُمثِل كائن المرحلة، في تلك الحالة تحديدًا، نافذة البرنامج الرئيسية، ويُشار إليه عادةً باسم "المرحلة الرئيسية (primary stage)". قد يُنشِئ البرنامج كائنات مرحلة (stage) آخرى من نفس الصَنْف إذا أراد فَتْح نوافذ آخرى.

تُعدّ أي نافذة (window) بمثابة مساحة ضِمْن شاشة الحاسوب، والتي يُمكِن مَلْؤها بمحتوى معين، فمثلًا قد نَملْؤها ببعض مما يُعرَف باسم مُكونات واجهة المُستخدِم الرُسومية (GUI components) كالقوائم، والأزرار، وصناديق الإِدْخَال النصية، ومساحات الرسم (drawing areas) التي كنا قد اِستخدَمناها ببعض البرامج الرسومية بالقسم ٣.٩. كما ذَكَرَنا مُسْبَقًا، يُنشِئ النظام كائنًا من الصنف Stage، ليُمثِل النافذة الرئيسية -ويُعرَف باسم "المرحلة الرئيسية (primary stage)"- قبل استدعاء التابع start()‎، ومع ذلك تَكُون تلك النافذة (window) ما تزال فارغة وغَيْر مرئية إلى حين استدعائه، وتُوكَل إليه في الحقيقة مُهِمّة مَلْؤها بالمحتوى المناسب، ومن ثَمَّ إظهارها على الشاشة. فمثلًا، بتطبيق HelloWorldFX بالأعلى، يُظهِر السطر الأخير من التابع start()‎ -أيّ التعبير stage.show()‎- النافذة على الشاشة بينما تُنشِئ أسطر التابع الآخرى المحتوى، وتُضيفه إلى النافذة إلى جانب ضَبْط بعض الخيارات المُتعلِّقة بكُلًا من المحتوى والنافذة ذاتها، فمثلًا، تَضبُط الشيفرة التالية نص شريط عنوان النافذة:

stage.setTitle("JavaFX Test");

تتكوَّن أي مرحلة (stage) -تُمثِل نافذة- من مساحة لعَرْض المحتوى (content area)، والتي يُمكِن مَلْئها بـ"مشهد (scene)" يَضُمّ مُكونات واجهة المُستخدِم الرسومية (GUI components) المُفْترَض عَرْضها بالنافذة. تُوفِّر حزمة javafx الصَنْف Scene، والذي يُمثِل أي كائن منه مشهدًا (scene). يُمكِننا ضَبْط المشهد (scene) المُفْترَض عَرْضه بمساحة المحتوى (content area) الخاصة بمرحلة معينة (stage) باِستخدَام التَعْليمَة التالية:

stage.setScene(scene);

يُمكِنك أن تمَلْئ أي مشهد (scene) بمُكوِّنات واجهة المُستخدِم الرسومية (GUI components) كالأزرار وأشرطة القوائم وغيرها. تُوفِّر منصة جافا إف إكس (JavaFX) صَنْفًا لكل مُكوِّن منها، فمثلًا، يُمكِنك أن تَستخدِم كائنًا من الصنف Button من حزمة javafx.scene.control لتمثيل "زر ضغط (push button)" مثل الزر "Say Hello" بالتطبيق السابق. بالإضافة إلى ذلك، تُوفِّر منصة جافا إف إكس (JavaFX) أصنافًا لمُكوِّنات تَعمَل بمثابة حاويات (containers) تُمثِل قسمًا من النافذة (window) مثل الكائن buttonBar من النوع HBox. قد يَشتمِل ذلك القسم من النافذة على مُكوِّنات واجهة آخرى بما في ذلك أي مُكوِّن حَاوِي آخر. بتعبير آخر، قد تحتوي نافذة معينة على عدد من مُكوِّنات واجهة المُستخدِم الرسومية داخل حاويات (containers) تَقَع بدورها ضِمْن حاويات أكبر، وجميعها مُمثَل بواسطة كائن. تُشكِّل كل تلك الكائنات ما يُعرَف باسم "مبيان المشهد (scene graph)"، والذي يُبيِّن علاقات الاحتواء بين جميع مُكوِّنات المشهد (scene). تُبيِّن الصورة التالية "مبيان المشهد (scene graph)" للتطبيق السابق:

002Scene_graph.png

لا تُمثِل الصورة بالأعلى سلالة أصناف (class hierarchy) كما قد تَظُنّ، فهي لا تُبيِّن العلاقات بين أصناف تلك الكائنات، وإنما هي بمثابة تَسَلسُل هرمي لعلاقات الاحتواء (containment hierarchy) بين تلك الكائنات، أي تُبيِّن الكيفية التي تَتَضمَّن بها بعض مُكوِّنات المشهد البعض الآخر. فمثلًا، يَتَّضِح من مبيان المشهد (scene graph) أن كُلًا من root و buttonBar عبارة عن مُكوِّنات حاوية (containers) أما message بالإضافة إلى الأزرار الثلاثة فهي مُجرّد مُكوِّنات بسيطة.

يَحتوِي أي مشهد (scene) على مُكوِّن جذري (root component) وحيد. هذا المُكوِّن هو عبارة عن حَاوِي (container) لجميع المُكوِّنات الآخرى الموجودة بالمشهد. أطلقنا الاسم root على المُكوِّن الجذري بالتطبيق السابق، ويُمكِنك بالطبع أن تختار أي اسم آخر تُفضِّله. يُمكِنك ضَبْط المُكوِّن الجذري لمشهد معين بينما تُنشِئ كائن الصنف Scene كالتالي:

Scene scene = new Scene(root, 450, 200);

تُحدِّد الأعداد المُمرَّرة لهذا الباني (constructor) كُلًا من عرض المشهد وارتفاعه بوحدة البكسل (pixel). يُمكِنك حَذْف تلك الأعداد، وفي تلك الحالة، ستُحسَب مساحة المشهد بما يتناسب مع محتوياتها.

العقد (nodes) والتخطيط (layout)

يتكوَّن أي مبيان مشهد (scene graph) من مجموعة من الكائنات، تُعرَف باسم "العُقَد (nodes)". لابُدّ أن تنتمي تلك الكائنات إلى إحدى الأصناف الفرعية (subclasses) المُشتقَّة من الصَنْف javafx.scene.Node، وقد يَعمَل بعضها كمُكوِّنات حاوية (container)، وفي تلك الحالة، لابُدّ لها من أن تنتمي إلى إحدى الأصناف الفرعية المُشتقَّة من الصَنْف javafx.scene.Parent، والذي هو بدوره صنف فرعي من نفس الصنف Node. لمّا كانت العُقْدة الجذرية (root node) تَعمَل كمُكوِّن حاوي، فلابُدّ أن تَكُون من الصنف Parent. تَشتمِل العُقَد المُمثلة لمُكوِّنات حاوية (containers) على مجموعة من العُقَد (nodes) الآخرى، والتي تُعدّ أبناءً (children) لها.

بتطبيق HelloWorldFX السابق، اِستخدَمنا كائنات من النوع Button لتمثيل الأزرار. في الواقع، هذا الصنف هو صنف فرعي (subclass) من الصنف Parent، وسنرى لاحقًا أن بإمكانه أن يَتَضمَّن عُقَدًا (nodes) آخرى. يَستقبِل باني (constructor) الصنف Button مُعامِلًا لتَخْصِيص النص المكتوب على الزر. بالمثل، message هو عبارة عن كائن عُقْدة (node) من النوع Label المُعرَّف بحزمة javafx.scene.control، ويُستخدَم لعَرْض سِلسِلة نصية من النوع String. يَتَضمَّن أي كائن عُقْدة من الصنف Label على خاصية نوع الخط، والتي تَتَحكَّم بحجم محارف السِلسِلة النصية، وهيئتها، ويُمكِن ضَبْطها باِستخدَام التابع setFont()‎ المُعرَّف بنفس الصنف. بالتطبيق السابق، اِستخدَمنا الباني new Font(40)‎، والذي يُحدِّد مُعامِله (parameter) الوحيد حجم الخط المطلوب.

المُكوِّنات الحاوية (containers) هي أيضًا عُقَد (nodes) من النوع Node، ولكن يُمكِنها أن تَشتمِل على عُقَد آخرى كأبناء (children)، ويُشار إلى الكيفية التي يُرتَّب بها هؤلاء الأبناء على الشاشة باسم "التخطيط (layout)". يعني التخطيط (layout) عمومًا ضَبْط كُلًا من حجم المُكوِّنات الواقعة ضِمْن الحاوي ومَوضِعها. على الرغم من تَوفُّر إمكانية لضَبْط تلك القيم بصورة مباشرة، فلربما تُفضِّل الاعتماد على ضَبْطها أتوماتيكيًا بالاستعانة بالمُكوِّن الحاوي (container) نفسه، وفي الواقع، تُعدّ تلك الطريقة أكثر شيوعًا. عمومًا، يُطبِق كل مُكوِّن حاوي سياسة تخطيط (layout policy) مختلفة، فمثلًا، تُرتِّب المُكوِّنات الحاوية من الصنف HBox مُكوِّناتها ضِمْن صف أفقي. اُنظر الباني (constructor) التالي:

HBox buttonBar = new HBox( 20, helloButton, goodbyeButton, quitButton );

تُمثِل جميع مُعامِلات (parameter) ذلك الباني فيما عدا الأول عُقَدًا (nodes) ينبغي إضافتها كأبناء (children) للمُكوِّن الحاوي (container)، بحيث يَفصِل بينها فراغًا تُحدِّد مساحته القيمة المُمرَّرة لمُعامِل الباني الأول.

في المقابل، تُطبِق المُكوِّنات الحاوية من الصنف BorderPane سياسة تخطيط (layout policy) مختلفة تمامًا. يستطيع أي مُكوِّن حاوي منها أن يحتوى على ما يَصِل إلى ٥ مُكوِّنات (components)، واحدة بالمنتصف، أما البقية فبالأعلى وبالأسفل وعلى اليسار وعلى اليمين. بالتطبيق السابق، كان المُكوِّن الجذري للمشهد عبارة عن مُكوِّن حاوي من الصنف BorderPane، واستخدمنا التَعْليمَات التالية لإضافة مُكوّنات آخرى إلى منتصفه والجزء السفلي منه:

root.setCenter(message);
root.setBottom(buttonBar);

تتوفَّر أيضًا الكثير من الخيارات لضَبْط التخطيط (layout)، فمثلًا يَستخدِم التطبيق السابق إحداها بالتَعْليمَة التالية:

buttonBar.setAlignment(Pos.CENTER);

تُستخدَم تلك التَعْليمَة لوَضْع الأزرار بمنتصف مُكوّن حاوي من الصنف HBox؛ فبدونه، ستَقَع الأزرار على حافة النافذة اليسرى. تعتمد منصة جافا إف إكس (JavaFX) على أنواع التعداد (enumerated type) بكثرة، والتي كنا قد ناقشناها بالقسم الفرعي ٢.٣.٤، وذلك لتخصيص الخيارات المختلفة، مثل نوع التعداد Pos بالشيفرة السابقة.

الأحداث (event) ومعالجاتها (handlers)

لا يَقْتصِر دور التابع start()‎ على ضَبْط تخطيط (layout) النافذة، وإنما يُستخدَم أيضًا لضَبْط ما يُعرَف باسم "معالجة الحَدَث (event handling)". مثلًا، بتطبيق HelloWorldFX بالأعلى، عندما يَنقُر المُستخدِم على زر معين، يَقَع ما يُعرَف باسم "الحَدَث (event)"، ويُمكِن عندها للتطبيق أن يُعالِج (handle) ذلك الحَدَث من خلال ما يُعرَف باسم "مُعالِجات الأحداث (event handlers)". تَشتمِل معالجة أي حَدَث على كائنين، يُعبِر الأول عن الحَدَث نفسه ويَحمِل معلومات عنه، فمثلًا، عند النقر على زر، يَكُون كائن الحَدَث من النوع ActionEvent ويَحمِل معلومات عن الزر المنقور عليه. أما الكائن الآخر فيُعبِر عن مُعالِج الحدث، ويَكُون من نوع واجهة نوع الدالة EventHandler، والتي تُعرِّف التابع handle(e)‎ حيث e هو كائن الحَدَث. والآن لكي تُعالِج حَدَثًا معينًا، ينبغي أن تُنشِئ صنفًا يُنفِّذ (implements) الواجهة EventHandler، ويُوفِّر تعريفًا للتابع handle()‎. ولكن لمّا كانت EventHandler عبارة عن واجهة نوع دالة (functional interface)، فيُمكِن كتابة المُعالِج (handler) بصورة تعبير لامدا (lambda expression). كنا قد ناقشنا تعبيرات لامدا تفصيليًا بالقسم ٤.٥، وهي في العموم شائعة الاِستخدَام بتطبيقات جافا إف إكس (JavaFX)، فتُستخدَم لكتابة مُعالِجات الأحداث (event handlers) من ضِمْن عدة استخدامات آخرى. اُنظر تعبير لامدا التالي على سبيل المثال:

e -> message.setText("Hello World!")

يُمثِل ذلك التعبير مُعالِج حَدَث (event handler) يَستجيب لحَدَث (event) ما بتغيير نص الرسالة إلى "Hello World". يَستقبِل ذلك المُعالِج مُعامِلًا e، يُمثِل الحَدَث، ويَكُون من النوع ActionEvent، والذي لابُدّ من كتابته دائمًا حتى لو لم يَستخدِمه المُعالِج كالمثال بالأعلى؛ وذلك ليَستوفِي صيغة تعبير اللامدا (lambda expression).

ينبغي الآن أن نُسجِّل (register) مُعالِج الحَدَث (event handler) بَعْد كتابته بالكائن المُنتج للحَدَث. فمثلًا بنفس المثال، كان الكائن هو helloButton، ونستطيع أن نُسجِّل المُعالِج (handler) باستدعاء تابع الكائن setOnAction()‎، كالتالي:

helloButton.setOnAction( e -> message.setText("Hello World!") );

ضَبطَنا أيضًا مُعالجي (handlers) الزرين الآخرين بنفس الطريقة. لاحِظ أن لدينا ثلاثة كائنات: كائن مُنتج للحَدَث (event) نتيجة لفعل (action) قام به المُستخدِم، وكائن يُمثِل الحَدَث ذاته، وأخيرًا كائن يُمثِل مُعالِج الحَدَث (event handler) كما يَحتوِي على الشيفرة المطلوب تّنْفيذها استجابةً للحَدَث (event)، كما هو مُوضَح بالصورة التالية:

003Event_Handling.png

يَتبقَّى لنا فقط توضيح الاستجابة على فعل (action) النقر على زر "Quit"، اِستخدَمنا التعبير Platform.exit()‎ لاستدعاء التابع الساكن exit()‎ المُعرَّف بالصنف Platform. يُفضَّل عمومًا اِستخدَام تلك الطريقة للإنهاء البرمجي لتطبيقات جافا إف إكس (JavaFX)؛ لأنها تُغلِق خيط التطبيق (application thread)، كما تَستدعِي التابع stop()‎ المُعرَّف بالصنف المُمثِل للتطبيق، مما يُعطِي فرصة للمبرمج لإجراء أي تنظيف (clean up) قد يَرغَب به قَبْل الإغلاق، وذلك بخلاف التابع System.exit()‎ مثلًا.

يُعدّ هذا القسم بمثابة نظرة عامة ومُختصرة لتطبيقات جافا إف إكس (JavaFX)، ومع ذلك فقد تَعرَّضنا خلالها للكثير من المفاهيم الأساسية. بالأقسام التالية، سنتناول كل ذلك على نحو تفصيلي.

ترجمة -بتصرّف- للقسم Section 1: A Basic JavaFX Application من فصل Chapter 6: Introduction to GUI Programming من كتاب Introduction to Programming Using Java.


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...