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

بناء تطبيقات كاملة باستعمال مكتبة جافا إف إكس JavaFX


رضوى العربي

لقد ناقشنا حتى الآن الكثير من الجوانب الأساسية المُستخدَمة ببرمجة واجهات المُستخدِم الرسومية (GUI)، وتَعلَّمنا ما يَكفِي لكتابة بعض البرامج الشيقة، ولهذا سنَفْحَص مثالين كاملين يُطبقان ما درسناه حتى الآن عن برمجة الواجهات الرُسومية (GUI) إلى جانب ما درسناه عن البرمجة في العموم بالفصول السابقة. ما يزال هنالك الكثير من الأمور الآخر لتَعْلُّمها، ولهذا سنعود للحديث عن نفس الموضوع بالفصل ١٣.

لعبة ورق بسيطة

برنامجنا الأول عبارة عن نُسخة من برنامج سطر الأوامر HighLow.java من القسم الفرعي ٥.٤.٣، ولكنها ستَكُون مُدعَّمة بواجهة مُستخدِم رُسومية (GUI). بنُسخة البرنامج الجديدة HighLowGUI.java، ينبغي أن تَنظُر إلى ورقة لعب ثم تَتَوقَّع ما إذا كانت قيمة ورقة اللعب التالية ستَكُون أكبر أو أقل من قيمة ورقة اللعب الحالية. إذا كان تَوقُّعك خاطئًا، فستَخسَر المباراة أما إذا كان صحيحًا لثلاث مرات متتالية، فستَفوز بالمباراة. بَعْد انتهاء أية مباراة، يُمِكنك النَقْر على "New Game" لبدء مباراة جديدة. تُوضِح الصورة التالية ما سيَبدُو عليه البرنامج خلال المباراة:

001High_Low_Gui.png

شيفرة البرنامج بالكامل مُتاحَة بالملف HighLowGUI.java. حاول تَصْرِيفها وتَشْغِيلها. ملحوظة: يَتَطلَّب البرنامج كُلًا من الملفات Card.java و Deck.java و Hand.java من القسم ٥.٤ حيث تُعرِّف أصنافًا (classes) مُستخدَمة ضِمْن البرنامج كما أنه يَتَطلَّب ملف صورة ورق اللعب cards.png الذي اِستخدَمناه بالبرنامج RandomCards.java بالقسم الفرعي ٦.٢.٤.

ربما من السهل تَخْمِين التخطيط (layout) المُستخدَم بالبرنامج: يَستخدِم HighLowGUI حاوية من النوع BorderPane كمُكوِّن جذري (root) لمبيان المشهد (scene graph). تَتَضمَّن تلك الحاوية حاوية آخرى من النوع Canvas مرسُوم عليها ورق لعب ورسالة وتَقَع بمنتصف الحاوية الخارجية. يَتَضمَّن المَوْضِع السفلي من الحاوية الخارجية حاوية آخرى من النوع HBox تَحتوِي بدورها على ثلاثة أزرار ضُبِطَت بحيث يَكُون لها جميعًا نفس قيمة العَرْض بغَرْض مَلْئ الحاوية بالكامل كما أَوضَحنا بالقسم الفرعي ٦.٥.٣. يُنفِّذ التابع start()‎ التالي ما سبق:

public void start(Stage stage) {

    cardImages = new Image("cards.png");     // حمل صورة ورق اللعب
    board = new Canvas(4*99 + 20, 123 + 80); // مسافة أربعة أوراق

    Button higher = new Button("Higher");    // أنشئ المفاتيح
    higher.setOnAction( e -> doHigher() );   // وجهز معالجات الأحداث
    Button lower = new Button("Lower");
    lower.setOnAction( e -> doLower() );
    Button newGame = new Button("New Game");
    newGame.setOnAction( e -> doNewGame() );

    HBox buttonBar = new HBox( higher, lower, newGame );

    // اضبط كل زر لكي يحصل على ثلث العرض المتاح
    higher.setPrefWidth(board.getWidth()/3.0); 
    lower.setPrefWidth(board.getWidth()/3.0);  
    newGame.setPrefWidth(board.getWidth()/3.0);

    BorderPane root = new BorderPane();  // أنشئ المكون الجذري لمبيان المشهد
    root.setCenter(board);
    root.setBottom(buttonBar);

    doNewGame();  // جهز المباراة الأولى

    Scene scene = new Scene(root);  // Finish setting up the scene and stage.
    stage.setScene(scene);
    stage.setTitle("High/Low Game");
    stage.setResizable(false);
    stage.show();

}  // end start()

تَستدعِي مُعالِجات الأحداث (event handlers) بالأعلى توابعًا مُعرَّفة بمكان آخر من البرنامج مثل التابع doNewGame()‎. تُعدّ برمجة تلك التوابع (methods) تمرينًا جيدًا على التفكير وفقًا لما يُعرَف باسم "آلة الحالة (state machine)": ينبغي أن تُفكِر بالحالات (states) التي يُمكِن للمباراة أن تَقَع ضِمْنها، وبالكيفية التي قد تَتَغيَّر بها من حالة لآخرى، وكذلك بالكيفية التي ستَعتمِد بها مُعالجات الأحداث (events) على تلك الحالات. لاحِظ أن الأسلوب الذي اتِبعَناه بالنسخة الأصلية النصية من البرنامج بالقسم الفرعي ٥.٤.٣ غَيْر مُناسِب هنا حيث ستُربكَك محاولة التفكير بالمباراة تفصيليًا خطوة بخطوة من البداية وحتى النهاية أكثر مما ستُساعِدك.

تَتَضمَّن حالة المباراة ورق لعب (cards) مُمثَل بكائن (object) من النوع Hand بالإضافة إلى رسالة مُمثَلة بكائن من النوع String. تُخزَّن تلك القيم ضِمْن مُتْغيِّرات نُسخ (instance variables). هنالك جانب آخر أقل وضوحًا سنُضْمّنه بحالة (state) المباراة: في بعض الأحيان، يَكُون هنالك مباراة قَيْد التّنْفِيذ ونَكُون بانتظار اختيار المُستخدِم لتَوقُّعه عن ورقة اللعب التالية. في بعض الأحيان الآخرى، نَكُون للتو قد انتهينا من مباراة وبانتظار نَقْر المُستخدِم على الزر "New Game". لذلك، ربما من الجيد أن نُضيِف عنصرًا جديدًا إلى حالة المباراة يُميِّز ذلك الاختلاف البسيط. يَستخدِم البرنامج مُتْغيِّر نُسخة (instance variable) من النوع boolean اسمه gameInProgress لذلك الغرض.

عندما يَنقُر المُستخدِم على أحد الأزرار، فقد تَتَغيَّر حالة المباراة. يُعرِّف البرنامج ثلاثة توابع (methods) للاستجابة على أحداث الضَغْط على زر هي: doHigher()‎ و doLower()‎ و newGame()‎، والتي يُوكَل إليها مُهِمّة مُعالِجة الأحداث (event-handling).

لأننا لا نُريد أن نَسمَح للمُستخدِم ببدء مباراة جديدة طالما كان هنالك مباراة آخرى قَيْد التّنْفِيذ بالفعل، فإن استجابة التابع newGame()‎ ستَختلِف بناءً على قيمة مُتْغيِّر الحالة gameInProgress. إذا كانت قيمته تُساوِي true أي أن هنالك مباراة قَيْد التّنْفِيذ، فينبغي أن يُضبَط مُتْغيِّر النُسخة message إلى رسالة خطأ معينة. في المقابل، إذا كانت قيمة المُتْغيِّر تُساوِي false أي ليس هنالك أي مباراة، فينبغي ضَبْط جميع مُْتْغيِّرات الحالة (state variables) إلى القيم المناسبة لبدء مباراة جديدة. بجميع الأحوال، لابُدّ من إعادة رَسْم الرقعة (board) حتى يَرَى المُستخدِم أن الحالة قد تَغيَّرت. يُمكِننا تَعرِيف التابع newGame()‎ على النحو التالي:

private void doNewGame() {
    if (gameInProgress) {
        // إذا لم تنته المباراة الحالية بعد، اعرض رسالة خطأ
        message = "You still have to finish this game!";
        drawBoard();
        return;
    }
    deck = new Deck();   // أنشئ مجموعة ورق اللعب واليد
    hand = new Hand();
    deck.shuffle();
    hand.addCard( deck.dealCard() );  // اسحب ورقة اللعب الأولى

    message = "Is the next card higher or lower?";
    gameInProgress = true;
    drawBoard();
} // end doNewGame()

التابعان doHigher()‎ و doLower()‎ مُتطابقان تقريبًا، وربما كان بإِمكَاننا دَمْجهُما ضِمْن تابع واحد يَستقبِل مُعاملًا (parameter) واحدًا. عندما يَنقُر المُستخدِم على الزر "Higher"، يُستدعَى البرنامج doHigher()‎، ولأن ذلك الاستدعاء ليس له أي معنى إلا في حالة وجود مباراة قَيْد التّنْفِيذ، يَفْحَص التابع قيمة مُتْغيِّر الحالة gameInProgress أولًا. إذا كانت قيمته تُساوِي false، فإنه يَعرِض رسالة خطأ أما إذا كانت قيمته تساوي true أي توجد مباراة قَيْد التّنْفِيذ، فإنه يُضيِف ورقة لعب جديدة إلى اليد (hand) ثم يَفْحَص تَوقُّع المُستخدِم. قد تنتهي المباراة في تلك اللحظة بفوز المُستخدِم أو بخسارته، وعندها يََضبُط التابع قيمة مُتْغيِّر الحالة gameInProgress إلى القيمة false لأن المباراة قد انتهت ببساطة. بجميع الأحوال، يُعاد رَسْم الرُقعة (board) لضمان عَرْض الحالة (state) الجديدة. اُنظر التابع doHigher()‎:

private void doHigher() {
    if (gameInProgress == false) {
        // إذا كانت المباراة قد انتهت، فمن الخطأ النقر
        // ‫على زر "Higher" لذلك اعرض رسالة خطأ وانهي المعالجة
        message = "Click \"New Game\" to start a new game!";
        drawBoard();
        return;
    }
    hand.addCard( deck.dealCard() );     // اسحب ورقة لعب إلى اليد
    int cardCt = hand.getCardCount();
    Card thisCard = hand.getCard( cardCt - 1 );  // تم سحب ورقة لعب
    Card prevCard = hand.getCard( cardCt - 2 ); 
    if ( thisCard.getValue() < prevCard.getValue() ) {
        gameInProgress = false;
        message = "Too bad! You lose.";
    }
    else if ( thisCard.getValue() == prevCard.getValue() ) {
        gameInProgress = false;
        message = "Too bad!  You lose on ties.";
    }
    else if ( cardCt == 4) {
        gameInProgress = false;
        message = "You win!  You made three correct guesses.";
    }
    else {
        message = "Got it right!  Try for " + cardCt + ".";
    }
    drawBoard();
} // end doHigher()

يَعتمِد التابع drawBoard()‎ على قيم مُتْغيِّرات الحالة (state variables) لتَقْرِير ما ينبغي له أن يَرسِمه ضِمْن الحاوية (canvas)، فيَعرِض السِلسِلة النصية المُخزَّنة بالمُتْغيِّر message ويَرسِم ورق اللعب ضِمْن اليد hand. وأخيرًا، إذا كانت المباراة قَيْد التّنْفِيذ، فإنه يَرسِم ورقة لعب إضافية مقلوبة تُمثِل ورقة اللعب التالية بمجموعة ورق اللعب (deck). ملحوظة: لقد ناقشنا التقنية المُستخدَمة لرَسْم ورقة لعب (card) مُفرّدة بالقسم ٦.٢.

القوائم Menu وشريط القوائم MenuBar

سنَعرِض الآن برنامج رَسْم اسمه "MosaicDraw". تَتَوفَّر شيفرة البرنامج بالملف MosaicDraw.java كما يَتَطلَّب الصَنْف MosaicCanvas.java. تُظهِر الصورة التالية رسمة مَصنُوعة باِستخدام البرنامج:

002Mosaic_Draw.png

عندما يَنقُر المُستخدِم على الفأرة ويَسحَبها داخل مساحة الرسم (drawing area)، فإن الفأرة تَترُك أثرًا مُكوَّنًا من مربعات صغيرة مُلوَّنة عشوائيًا. بالإضافة إلى ذلك، يُوفِّر البرنامج شريط قوائم (menu bar) أعلى مساحة الرسم. أولًا، تحتوي قائمة "Control" على أوامر لمَلْئ مساحة الرسم وتنظيفها مع خيارات آخرى تُؤثِر على مَظهَر الصورة. ثانيًا، تَسمَح قائمة "Color" بتَخْصِيص اللون المُستخدَم أثناء الرسم. ثالثًا، تُؤثِر قائمة "Tools" على سلوك الفأرة، فعندما نَستخدِم أداة "Draw" الافتراضية، تَترُك الفأرة أثرًا مُكوَّنًا من مُربعات مُفردّة، ولكن عندما نَستخدِم أداة "Draw 3x3"، فإنها تترُك أثرًا يَبلُغ عَرْضه ثلاثة مربعات. تَتَوفَّر أيضًا أداة "Erase" لإعادة ضَبْط المربعات إلى اللون الأسود الافتراضي.

سنُمثِل مساحة الرسم (drawing area) داخل البرنامج باستخدام الصَنْف الفرعي MosaicCanvas المُشتَق من الصَنْف Canvas المُعرَّف بالملف MosaicCanvas.java. لا يُدعِّم ذلك الصَنْف "رسوم الفسيفساء" مباشرةً، ولكن بإمكاننا اِستخدَامه لمحاكاتها عن طريق ضَبْط لون كل مربع على حدى. أَعدّ البرنامج MosaicDraw معالجين لحدثيّ الفأرة MousePressed و MouseDragged الواقعين ضِمْن الحاوية (canvas) بحيث تَستجِيب تلك المُعالجات بتطبيق الأداة (tool) المُختارة حاليًا على مَوضِع مُؤشر الفأرة عند النَقْر. يُعدّ هذا البرنامج في العموم مثالًا جيدًا على تَطبيق مُستمع حَدَث (event listeners) على كائن (object) مما يَسمَح له بالقيام بأمور لم يَكُن هو ذاته مُبرمَجًا للقيام بها.

ستَجِد شيفرة البرنامج بالكامل بالملف MosaicDraw.java. لن نُناقش جميع تفاصيلها هنا، ولكن يُفْترَض أن تَكُون قادرًا على فهمها بالكامل بَعْد قراءة هذا القسم. في المقابل، تَعتمِد شيفرة الملف MosaicCanvas.java على بعض التقنيات التي لن تَتَمكَّن من استيعابها في الوقت الحالي، ومع ذلك، يُمكِنك قراءة التعليقات الموجودة بالملف لكي تَتَعلَّم المزيد عن واجهة برمجة التطبيقات (API) الخاصة بالصَنْف MosaicCanvas.

سنَستخدِم مُكوِّن شريط القوائم (menu bar) للمرة الأولى ضِمْن البرنامج MosaicDraw. تَسمَح لك مكتبة جافا إف إكس (JavaFX) بإضافة قائمة إلى البرنامج بسهولة عن طريق استخدام كائنات (objects) تَنتمِي للصَنْف MenuItem أو إلى أي من أصنافها الفرعية (subclasses) الُمعرَّفة بحزمة javafx.scene.control لتَمثيِل عناصر القائمة. تَعمَل عناصر القائمة تقريبًا بنفس الكيفية التي تعمل بها الأزرار (buttons). يُوفِّر الصَنْف MenuItem باني كائن (constructor) بمُعامل واحد عبارة عن نص عنصر القائمة. بإمكانك استخدامه لإنشاء كائن من ذلك الصَنْف كالتالي:

MenuItem fillCommand = new MenuItem("Fill");

نظرًا لأن عناصر القائمة قد تَتَكوَّن من صورة ونص مثل الأزرار، يُوفِّر الصَنْف باني كائن (constructor) آخر بمُعاملين لتَخْصِيصهما. عندما يختار المُستخدِم عنصر قائمة من النوع MenuItem من قائمة، يَقَع حَدَث من النوع ActionEvent. يُمكِنك إضافة مُستمِع حَدَث (event listener) إليه باِستخدَام تابعه setOnAction(handler)‎. بالإضافة إلى ذلك، قد تَستخدِم التابع setDisable(disabled)‎ لتَفْعيل عنصر قائمة أو تَعْطِيله، وكذلك التابع setText()‎ لتَغْيِير النص المعروض.

يختلف عنصر القائمة (menu item) عن الزر (button) بكَوْنه يَظهَر ضِمْن قائمة (menu). في الواقع، أي عنصر قائمة هو عبارة عن عقدة من النوع Node تَظهَر عادةً ضِمْن قائمة، ولكنها قد تَظهَر أيضًا بأي مكان آخر ضِمْن مبيان المشهد (scene graph). تُمثَل القوائم (menus) بمكتبة جافا إف إكس (JavaFX) باِستخدَام الصَنْف Menu. لمّا كان ذلك الصَنْف صنفًا فرعيًا مُشْتقًا من الصَنْف MenuItem، فإنه من المُمكن إضافة قائمة ضِمْن قائمة آخرى كعنصر، وتَكُون عندها قائمة فرعية (submenu) من القائمة الأصلية المُضافة إليها. أي قائمة من الصَنْف Menu لها اسم يُمكِنك تَخْصِيصه من خلال باني الكائن (constructor). يُعيد تابع النسخة getItems()‎ المُعرَّف بالصَنْف Menu قائمة (list) بعناصر القائمة (menu items)، ويُمكِنك أن تُضيِف عنصر قائمة جديد بإضافته إلى تلك القائمة. تُوضِح الشيفرة التالية طريقة إضافة عناصر (items) إلى قائمة:

Menu sampleMenu = new Menu("Sample");
sampleMenu.getItems().add( menuItem );  // اضف عنصر قائمة إلى القائمة
sampleMenu.getItems().addAll( item1, item2, item3 );  // أضف عدة عناصر

ينبغي أن تضيف القائمة (menu) بَعْد إنشائها إلى شريط قوائم (menu bar) يُمثَل باِستخدَام الصنف MenuBar. لا يملك شريط القوائم اسمًا، فهو يعمل فقط بمثابة حاوي (container) لعدة قوائم. يمكنك استدعاء باني الكائن (constructor) الخاص به بدون أي معاملات أو بقائمة مُعاملات (parameter list) تحتوي على القوائم (menus) المطلوب إضافتها إليه. يعيد تابع النسخة getMenus()‎ قائمة من القوائم (menus).

يُعيد تابع النسخة getMenus()‎ المُعرَّف بالصَنْف MenuBar قائمة (list) بالقوائم (menus) ضمن الشريط، ويستخدم كُلًا من التابعين add()‎ و addAll()‎ لإضافة قوائم (menus) جديدة إلى الشريط. يَستخدَم برنامج MosaicDraw ثلاثة قوائم هي controlMenu و colorMenu و toolMenu. تُنشِئ التَعْليمَات التالية شريط قوائم (menu bar) وتُضيِف إليه عدة قوائم (menus):

MenuBar menuBar = new MenuBar();
menuBar.getMenus().addAll(controlMenu, colorMenu, toolMenu);

أو قد نُمرِّر القوائم (menu) مباشرةً إلى الباني (constructor) كالتالي:

MenuBar menuBar = new MenuBar(controlMenu, colorMenu, toolMenu);

تَبقَّى لنا إضافة شريط القوائم (menu bar) إلى مبيان المشهد (scene) الخاص بالبرنامج. عادةً ما يَكُون شريط القوائم أعلى النافذة، ولكن من الممكن وَضْعُه بأي مكان آخر. عادةً ما تَستخدِم البرامج -المُحتويّة على شريط قوائم- كائن حاوية من النوع BorderPane يَعمَل كمُكوِّن جذري (root) لمبيان المشهد (scene graph). يَظهَر شريط القوائم (menu bar) بالمُكوِّن الموجود أعلى الحاوية أما المُكوِّنات الرسومية (GUI components) الآخرى فتَظهَر بالمواضع الأربعة الآخرى.

يُمكِنك إذًا إنشاء أي قائمة على النحو التالي: أولًا، اِنشِئ شريط قوائم (menu bar). ثانيًا، اِنشِئ قوائم (menus) وأَضِفها إلى ذلك الشريط. ثالثًا، اِنشِئ عناصر قائمة (menu items) وأَضِفها إلى تلك القوائم. أَضِف مُستمعِي أحداث للأحداث (events) الصادرة عن تلك العناصر لمُعالجتها. أخيرًا، ضَعْ شريط القوائم (menu bar) بأعلى مُكوِّن الحاوية من النوع BorderPane.

تُوفِّر مكتبة جافا إف إكس (JavaFX) أنواعًا آخرى من عناصر القوائم (menu items)، والتي يُمكِن بطبيعة الحال إضافتها إلى أي قائمة. تلك العناصر مُعرَّفة كأصناف فرعية (subclasses) من الصَنْف MenuItem. على سبيل المثال، يَستخدِم المثال التالي عنصر قائمة من النوع SeparatorMenuItem لإضافة خط بين عناصر القائمة الأخرى:

menu.getItems().add( new SeparatorMenuItem() );

يتوفَّر أيضًا الصنفين الفرعيين CheckMenuItem و RadioMenuItem.

يُمثِل الصَنْف CheckMenuItem عنصر قائمة يُمكِنه أن يَكُون بواحدة من حالتين: مُختار (selected) أو غير مُختار (not selected). تَتَبدَّل حالة العنصر عندما يختاره المُستخدِم من القائمة الحاوية له. يَعمَل ذلك العنصر بنفس الطريقة التي تَعمَل بها مُربعات الاختيار من الصَنْف CheckBox (اُنظر القسم الفرعي ٦.٤.٣).

يَستخدِم البرنامج MosaicDraw ثلاثة عناصر من النوع CheckMenuItems بقائمة "Control": يُستخدَم إحداها لتَفْعِيل ميزة الألوان العشوائية للمربعات وإلغاؤها. يُستخدَم آخر لغَلْق خاصية التَماثُل (symmetry) وفَتْحها. عند تَفْعِيل تلك الخاصية، تَنعكِس الرسمة أفقيًا ورأسيًا لإنتاج نمط مُتماثِل. أخيرًا، يَعرِض عنصر القائمة الثالث ما يُعرَف بـ"الحشو (grouting)" ويُخفِيه. يَتَكوَّن ذلك الحشو من خطوط رمادية مرسومة حول كل مربع داخل الرسمة. اُنظر التَعْليمَات التالية المُستخدَمة لإضافة عنصر القائمة المُمثِل للخيار "Use Randomness" إلى قائمة "Control":

useRandomness = new CheckMenuItem("Use Randomness");
useRandomness.setSelected(true);  // شغل العشوائية افتراضيًا
controlMenu.getMenus().add(useRandomness);   // أضف عنصر قائمة إلى القائمة

لم نُضِف مُعالجًا للحدث ActionEvent الصادر عن عنصر القائمة useRandomness حيث يَفْحَص البرنامج حالته باستدعاء useRandomness.isSelected()‎ عند حاجته إلى تلوين أي مربع لكي يُقرِّر ما إذا كان سيُضيف بعض العشوائية للون أم لا. في المقابل، عندما يختار المُستخدِم مربع الاختيار "Use Grouting"من القائمة، فلابُدّ أن يُعِيد البرنامج رَسْم الحاوية (canvas) مباشرةً حتى تَعكِس حالتها الجديدة. تُضيِف الشيفرة التالية معالجًا (handler) إلى عنصر القائمة useGrouting المُمثِل لذلك الخيار لكي يَستدعِي التابع المناسب:

useGrouting.setOnAction( e -> doUseGrouting(useGrouting.isSelected()) );

تحتوي قائمة "Tools" وقائمة "Color" بالبرنامج على عناصر قائمة من النوع RadioMenuItem تَعمَل بنفس الطريقة التي تَعمَل بها أزرار الانتقاء من الصَنْف RadioButton (اُنظر القسم الفرعي ٦.٤.٣). يُمكِن لأي عنصر قائمة من ذلك النوع أن يَكُون مختارً (selected) أو غير مختار، ولكن إذا أضفت عدة عناصر منها إلى كائن من الصَْنْف ToggleGroup، فلن تَتَمكَّن من اختيار أكثر من عنصر واحد فقط ضِمْن تلك المجموعة.

يستطيع المُستخدِم أن يختار الأداة التي يَرغَب باِستخدَامها من قائمة "Tools"، ولأنه سيختار بطبيعة الحال أداة واحدة بكل مرة، كان من البديهي تَمثيِل تلك الأدوات بهيئة عناصر قائمة من الصَنْف RadioMenuItem، ومن ثَمَّ إضافتها جميعًا إلى كائن من الصَنْف ToggleGroup. يُؤشَر بعلامة على عنصر القائمة المُمثِل للأداة المُختارة حاليًا بالقائمة، وعندما يختار المُستخدِم أداة جديدة، يَتبدَّل مكان ذلك التأشير كاِستجابة مرئية تُوضِح الأداة المُختارَة حاليًا. بالإضافة إلى ذلك، تَملُك الكائنات من الصَنْف ToggleGroup خاصية "قابلة للمراقبة (observable)" تُمثِل الخيار المُحدَّد حاليًا (اُنظر القسم ٦.٣.٧). يُضيِف البرنامج مُستمِعًا (listener) إلى تلك الخاصية ويُخصِّص لها مُعالِج حَدَث (event handler) يُستدعَى تلقائيًا بكل مرة يختار فيها المُستخدِم أداة جديدة. اُنظر الشيفرة المسئولة عن إنشاء قائمة "Tools":

Menu toolMenu = new Menu("Tools");
ToggleGroup toolGroup = new ToggleGroup();
toolGroup.selectedToggleProperty().addListener( 
                 e -> doToolChoice(toolGroup.getSelectedToggle()) );
addRadioMenuItem(toolMenu,"Draw",toolGroup, true);
addRadioMenuItem(toolMenu,"Erase",toolGroup, false);
addRadioMenuItem(toolMenu,"Draw 3x3",toolGroup, false);
addRadioMenuItem(toolMenu,"Erase 3x3",toolGroup, false);

وبحيث يُعرَّف التابع addRadioMenuItem على النحو التالي:

private void addRadioMenuItem(Menu menu, String command, 
                                   ToggleGroup group, boolean selected) {
    RadioMenuItem menuItem = new RadioMenuItem(command);
    menuItem.setToggleGroup(group);
    menu.getItems().add(menuItem);
    if (selected) {
        menuItem.setSelected(true);
    }
}

الشيفرة المسئولة عن إنشاء شريط القوائم (menua bar) مُعرَّفة بالكامل ضِمْن تابع (method) اسمه هو createMenuBar()‎.

Scene و Stage

قبل أن ننتهي من هذه المقدمة المُختصَرة عن برمجة واجهات المُستخدِم الرسومية (GUI)، سنَفْحَص صَنْفين أساسيين على نحو أكثر تفصيلي: Scene من حزمة package javafx.scene و Stage من حزمة package javafx.stage.

يُمثِل أي مشهد من الصَنْف Scene مساحة المحتويات ضِمْن نافذة، ويَحمِل المُكوِّن الجذري لمبيان المشهد (scene graph) الخاص بها. يُعرِّف الصَنْف Scene بواني كائن (constructors) كثيرة تَستقبِل جميعها المُكوِّن الجذري لمبيان المشهد كمُعامل (parameters) إلى جانب عدة معاملات آخرى. على سبيل المثال، يَستقبِل الباني new Scene(root)‎ مُعاملًا واحدًا فقط يُمثِل المُكوِّن الجذري، ويُعدّ أكثر بواني الصنف شيوعًا.

يُمكِنك أيضًا أن تُمرِّر عَرْض المشهد وارتفاعه كمُعاملات للباني باِستخدَام Scene(root,width,height)‎. إذا كان المُكوِّن الجذري عبارة عن حاوية من النوع Pane، يُضبَط حجم الحاوية بحيث يُساوِي حجم المشهد وتُعرَض محتوياتها بناءً على ذلك الحجم أما إذا لم يُخصَّص حجم المشهد، فإنه يُضْبَط إلى القيمة المفضلة لحجم المشهد.

في المقابل، إذا كان المُكوِّن الجذري من الصَنْف Group المُعرَّف بحزمة package javafx.scene، فإنه لا يُضْبَط إلى نفس حجم المشهد وإنما يُقْصّ أي لا يَظهَر أي جزء يَقَع خارج المشهد (scene). لا تستطيع البرامج ضَبْط عَرْض أو ارتفاع مشهد من الصَنْف (Scene)، ولكن إذا تَغيَّر حجم المرحلة (stage) -من الصَنْف Stage- الحاوية لمشهد (scene)، فإن حجم المشهد يَتَغيَّر أتوماتيكيًا ليُطابِق الحجم الجديد لمساحة المُحتويات بالمرحلة (stage) كما يَتَغيَّر حجم المُكوِّن الجذري (root) للمشهد (scene) إذا كان من النوع Pane.

يُمكِنك أن تُخصِّص لون مَلْئ (fill) -من النوع Paint- لخلفية مشهد (scene) عن طريق باني الكائن (constructor). مع ذلك، لا تَكُون خلفية المشهد مرئية في العموم لكَوْنها مغطاة بخلفية المُكوِّن الجذري (root). يُضْبَط لون خلفية المُكوِّن الجذري إلى الرمادي الفاتح افتراضيًا، ولكن قد تَضبُطه ليُصبِح شفافًا (transpatent) إذا أردت رؤية خلفية المشهد.

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

تحتوي أي مرحلة (stage) على مشهد (scene) يَملَئ مساحة المحتويات (content area) الخاصة بها. يستخدم تابع النسخة stage.setScene(scene)‎ لضَبْط المشهد (scene) الخاص بمرحلة (stage). يُمكِنك عَرْض مرحلة لا تحتوي على أي مشهد، وستَظهَر عندها مساحة محتوياتها على هيئة مستطيل فارغ.

علاوة على ذلك، تَحتوِي أي مرحلة (stage) على شريط عنوان (title bar) يَظهَر فوق مساحة محتوياتها (content area). يَتَكوَّن ذلك الشريط من عنوان نافذة إلى جانب مجموعة من أزرار التَحكُّم التي يستطيع المُستخدِم أن يَنقُر عليها للقيام بأشياء مثل غَلْق النافذة أو تكبيرها. يُضيِف نظام التشغيل (operating system) -لا الجافا- ذلك الشريط تلقائيًا، ويَتَحكَّم بتصميمه الخارجي. يُستخدَم تابع النسخة stage.setTitle(string)‎ لضَبْط النص المَعرُوض بشريط العنوان، وتستطيع استدعائه بأي وقت.

افتراضيًا، يستطيع المُستخدِم أن يَضبُط حجم أي مرحلة (stage) بسَحْب حوافها أو أركانها. مع ذلك، يُمكِنك استدعاء stage.setResizable(false)‎ لكي تَمنَعه من ذلك. لاحِظ أنه حتى بَعْد استدعاء ذلك التابع على مرحلة، فما تزال شيفرة البرنامج ذاتها قادرة على تَغْيِير حجم تلك المرحلة باستدعاء توابع النسخة stage.setWidth(w)‎ و stage.setHeight(h)‎. عادةً ما يُحدَّد الحجم المبدئي لأي مرحلة بحجم المشهد (scene) الموجود داخلها، ولكن بإمكانك استدعاء setWidth()‎ و setHeight()‎ لضَبْط قيمة الحجم المبدئي قَبْل عَرْض النافذة.

عندما يَكُون حجم مرحلة (stage) قابلًا للضَبْط من قِبَل المُستخدِم، فإن بإِمكانه تَصغِير النافذة أو تَكْبيرها إلى أي قيمة عشوائية افتراضيًا. يُمكِنك مع ذلك اِستخدَام توابع النسخ stage.setMinWidth(w)‎ و stage.setMaxWidth(w)‎ و stage.setMinHeight(h)‎ و stage.setMaxHeight(h)‎ لوَضْع بعض القيود على القيم المسموح بها. تُطبَق تلك القيود على ما يستطيع المُستخدِم القيام به أثناء سَحْبه لحواف النافذة أو أركانها.

يُمكِنك تَغْيِير موضع مرحلة (stage) على الشاشة -عادةً قبل عَرْضها- باستدعاء توابع النُسخ stage.setX(x)‎ و stage.setY(y)‎. تُخصِّص الإحداثيات x و y مَوضِع الركن الأيسر العلوي من النافذة وفقًا لنظام إحداثيات الشاشة.

أخيرًا، تَذكَّر أن أي مرحلة (stage) تَظلّ غَيْر مرئية إلى أن تَعرِضها على الشاشة صراحةً باستدعاء تابع النسخة stage.show()‎. عادةً ما يَكُون إظهار "المرحلة الرئيسية (primary stage)" هو آخر ما يُنفِّذه التابع start()‎ الخاص بالتطبيق.

إنشاء ملفات Jar

الملف المُنتهِي بالامتداد ‎.jar هو عبارة عن ملف جافا أرشيفي (java archive) يَتَضمَّن عددًا من ملفات الأصناف (class files) والموارد (resource files) المُستخدَمة ضِمْن البرنامج. عندما تُنشِئ برنامجًا يَعتمِد على أكثر من ملف واحد، يُنصَح عمومًا بوَضْع جميع الملفات التي يَتَطلَّبها ذلك البرنامج ضِمْن ملف جافا أرشيفي، وعندها لن يحتاج المُستخدِم لأكثر من ذلك الملف حتى يَتَمكَّن من تَشْغِيل البرنامج. يُمكِنك أيضًا أن تُنشِئ ما يُعرَف باسم "ملف جافا أرشيفي تّنْفِيذي (executable jar file)"، والذي يستطيع المُستخدِم أن يُشغِّله بنفس الكيفية التي يُشغِّل بها أي تطبيق آخر أي بالنَقْر المُزدوج على أيقونة الملف، ولكن لابُدّ من وجود نُسخة مُثبتَّة وصحيحة من الجافا على حاسوبه كما ينبغي أن تَكُون إعدادات حاسوبه مَضْبُوطَة بطريقة معينة. عادةً ما تُضبَط تلك الإعدادات تلقائيًا عند تثبيت الجافا بنظامي ويندوز وماك على الأقل.

تَعتمِد طريقة إنشاء ملفات الجافا الأرشيفية (jar file) على بيئة البرمجة (programming environment) المُستخدَمة. لقد ناقشنا بالفعل نوعين أساسين منها هما: بيئة سطر الأوامر (command line) وبيئة التطوير المُتكاملة (IDE) بالقسم ٢.٦. تُوفِّر أي بيئة تطوير مُتكاملة للجافا (Java IDE) أمرًا لإنشاء ملفات الجافا الأرشيفية (jar files). اِتبِع مثلًا الخطوات التالية ببيئة تَطوِير إكلبس (Eclipse): اُنقُر بزر الفأرة الأيمن على المشروع داخل نافذة مُتصفِح الحزم (Package Explorer) ثُمَّ اِختَر "Export" من القائمة. ستَظهَر نافذة، اِختَر منها "JAR file" ثُمَّ اُنقُر على "Next". ستَظهَر نافذة أخرى، أَدْخِل اسمًا للملف الأرشيفي بصندوق الإدخال "JAR file" (يُمكنِك أيضًا النَقْر على زر "Browse" المجاور لذلك الصندوق لاختيار اسم الملف من خلال نافذة). لابُدّ أن يَنتهِي اسم الملف بالامتداد ‎.jar. إذا كنت تُنشِئ ملفًا أرشيفيًا (jar file) عاديًا لا ملفًا تنفيذيًا، تستطيع أن تَنقُر على "Finish" لإنشاء الملف. أما إذا كنت تريد أن تُنشِئ ملفًا أرشيفيًا تنفيذيًا (executable)، اُنقُر على زر "Next" مرتين إلى أن تَصِل إلى شاشة "Jar Manifest Specification". ستَجِد صندوق الإدخال "Main class" بنهاية الشاشة. ينبغي أن تُدْخِل به اسم الصَنْف المُتْضمِّن للبرنامج main()‎ المُراد تَشْغِيله عند تّنْفِيذ الملف الأرشيفي. إذا ضَغطَت على زر "Browse" المجاور للصندوق "Main class"، ستَظهَر قائمة بالأصناف المُحتويّة على برنامج main()‎ والتي يُمكِنك الاختيار من بينها. بَعدّ اختيار الصَنْف، اُنقُر على زر "Finish" لإنشاء الملف التّنْفِيذي.

بالنسبة لبيئة سطر الأوامر، تَتَضمَّن عُدّة تطوير جافا (Java Development Kit - JDK) برنامج سطر الأوامر jar المُستخدَم لإنشاء ملفات جافا أرشيفية (jar files). إذا كانت جميع الأصناف (classes) واقعة ضِمْن الحزمة الافتراضية (default package) كغالبية الأمثلة التي تَعرَّضنا لها، يَكُون اِستخدَام الأمر jar سهلًا نوعًا ما. فمثلًا، لكي تُنشِئ ملفًا أرشيفيًا غَيْر تّنفِيذي بسطر الأوامر، اِنتقِل إلى المجلد الذي يَحتوِي على ملفات الأصناف المطلوب إضافتها إلى الملف الأرشيفي، ثُمَّ اِستخدِم الأمر التالي:

jar  cf  JarFileName.jar  *.class

حيث JarFileName هو اسم ملف الجافا الأرشيفي الذي تَرغَب بإنشائه. لاحِظ أن المحرف * بـ ‎*.class هو عبارة عن محرف بدل (wildcard) يَجعَل ‎*.class تُطابِق جميع أسماء ملفات الأصناف (class files) الموجودة بالمجلد الحالي أي سيَحتوِي الملف الأرشيفي (jar file) على كل ملفات الأصناف ضِمْن المجلد. إذا كان البرنامج يَستخدِم ملفات موارد (resource files) مثل الصور، فلابُد من أن تُضيفها إلى الأمر jar أيضًا. أما إذا كنت تريد أن تُضيِف ملفات أصناف مُحدَّدة فقط، فينبغي أن تَذكُر أسمائها صراحةً بحيث تَفصِل مسافة فارغة بين كل اسم والذي يليه. إذا كانت ملفات الأصناف والموارد مُعرَّفة بحزم غَيْر الحزمة الافتراضية (default package)، تُصبِح الأمور أكثر تعقيدًا، ففي تلك الحالة، لابُدّ أن تَقَع ملفات الأصناف بمجلدات فرعية داخل المجلد الذي تُنفِّذ به الأمر jar. اُنظُر القسم الفرعي ٢.٦.٧.

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

Main-Class: ClassName

يُشير ClassName -بالأعلى- إلى اسم الملف المُحتوِي على البرنامج main()‎. على سبيل المثال، إذا كان البرنامج main()‎ مُعرَّف ضِمْن الصنف MosaicDraw، فينبغي أن يَحتوِي الملف على العبارة "Main-Class: MosaicDraw". يُمكِنك أن تُسمِي الملف بأي اسم تريده، ولكن لابُدّ من أن تَضعُه داخل نفس المجلد الذي تُنفِّذ به الأمر jar كما ينبغي أن تَكتُب الأمر jar بالصياغة التالية:

jar  cmf  ManifestFileName  JarFileName.jar  *.class

يستطيع الأمر jar تّنْفِيذ الكثير من العمليات المختلفة. يخصص مُعامله (parameter) الأول cf أو cmf العملية المطلوب تّنْفِيذها.

بالمناسبة، إذا استطعت إنشاء ملف أرشيفي تّنْفِيذي (executable jar file)، يُمكِنك اِستخدَام الأمر java -jar لتَشْغِيله عبر سطر الأوامر كالتالي:

java  -jar  JarFileName.jar

ترجمة -بتصرّف- للقسم Section 6: Complete Programs من فصل Chapter 6: Introduction to GUI Programming من كتاب Introduction to Programming Using Java.


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

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

بتاريخ 20 ساعات قال Soft Root:

موضوع جميل جدا ، جزاك الله خيرا على ما قدمت ،
لكن عندي سؤال : ألا وهو كيفية استدعاء Custom Arabic Font في javafx ؟

يتم الأمر بإستخدام لغة css تقوم بإنشاء ملف تنسيقات و تضيف له الخط عن طريق الخاصية font-face:

@font-face {
    font-family: 'Cairo';
    src: url('fonts/Cairo-Regular.ttf');
}

ثم تقوم بتطبيقه على العناصر التي تريد:

.label {
    -fx-font-family: 'Cairo';
    -fx-font-size: 50;
}

.button .text {
    -fx-font-family: 'Cairo';
}

بعد ذلك تستدعي و تُطبق التنسيقات على ال scene:

scene.getStylesheets().add(getClass().getResource("resources/style.css").toExternalForm());

المثال موجود على مستودع github من خلال هذا الرابط: JavaFx Font Test Application و هذه النتيجة استخدمت خط Cairo:

javafx-font-test.thumb.JPG.651483d0c1be1e8b54daae5c8049fab9.JPG

رابط هذا التعليق
شارك على الشبكات الإجتماعية

شكرا جزيلا أخي الكريم ، هذه كانت مشكلة تواجهني في الزمن الماضي السحيق :) ومن أجلها تركت التطوير بجافا fx لفترة طويلة واتجهت للويب والأندرويد و ال native development ! مشكلة تقطع الأحرف ، ولو أنصفت لم أعطها قدرها من البحث ! شكرا مرة أخرى :) 

رابط هذا التعليق
شارك على الشبكات الإجتماعية



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...