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

التخطيط الأساسي لواجهة المستخدم في مكتبة جافا إف إكس JavaFX


رضوى العربي

تُعدّ المُكوِّنات (components) اللبنة الأساسية بأي واجهة مُستخدِم رُسومية (graphical user interface). لا يَقْتصِر الأمر على إنشاء مُكوِّنات الواجهة بل تحتاج أيضًا إلى تَخْطِيط الكيفية التي ستَتَموْضَع بها تلك المُكوِّنات على الشاشة وكذلك تَخْصِيص أحجامها. في العموم، تُعدّ عملية حِسَاب إحداثيات المَواضِع معقدة نوعًا ما خاصةً إذا لم تَفْترِض ثبات حجم مساحة الرسم. تُوفِّر الجافا لحسن الحظ حلًا مُبسِّطًا لذلك.

تُشير المُكوِّنات (components) إلى الأشياء المرئية التي تُشكِّل واجهة المُستخدِم الرُسومية (GUI). بعضًا منها عبارة عن مُكوِّنات حاوية (containers) يُمكِنها أن تَتَضمَّن مُكوِّنات آخرى. بمصطلحات جافا إف إكس (JavaFX)، المُكوِّنات الحاوية (container) هي عُقَد مبيان مشهد (scene graph node) يُمكِنها أن تَتَضمَّن عقد مبيان مشهد آخرى كأبناء (children). لابد من ضَبْط مواضع وأحجام أبناء أي مُكوِّن حاوي فيما يُعرَف باسم "التخطيط (layout)"، وهو ما قد تُبرمجه بنفسك، ولكن تُستخدَم عادةً حلول أتوماتيكية بواسطة المُكوِّن الحاوي (container) نفسه. تُنفِّذ أنواع مختلفة من المُكوِّنات الحاوية سياسات تخطيط (layout policies) مختلفة لتَحْديد مواضع العقد الأبناء (child nodes). سنُغطِي في هذا القسم بعض أنواع المُكوِّنات الحاوية (containers) بمنصة جافا إف إكس (JavaFX) مع توضيح سياسات التخطيط الخاصة بكُلًا منها كما سنَفْحَص أمثلة برمجية متعددة.

لأن أي مُكوِّن حاوي (container) هو بالنهاية عُقدَة مبيان مشهد (scene graph node)، فيُمكِنك أن تُضيف مُكوِّن حاوي كابن لمُكوِّن حاوي آخر مما يَسمَح بتداخل مُعقد للمُكوِّنات كما هو مُبيَّن بالصورة التالية:

001Panels_inLayout.png

تُوضِح الصورة السابقة مُكوِّن حاوي (container) كبير يَتَكوَّن من مُكوِّنين حاويين أصغر كلًا منهما يَحمِل بدوره ثلاثة مُكوِّنات آخرى.

تُخصِّص كل عقدة مبيان مشهد (scene graph node) قيم صغرى وكبرى ومُفضّلة لكُلًا من عرضها وارتفاعها. يَستعِين أي مُكوِّن حاوي بتلك القيم عند تقريره لطريقة وَضْع أبنائه. من الجهة الآخرى، يُعدّ حجم بعض مُكوِّنات العُقَد ثابتًا -مثل الصنفين Canvas و ImageView-، وتَتَساوَى عندها كُلًا من القيم الصغرى والعظمى والمُفضّلة للعرض والارتفاع مع الحجم الفعليّ للمُكوِّن، ولا يُمكِن لأي مُكوِّن حاوي عندها تَغْيير ذلك الحجم أثناء التخطيط (layout). في العموم، سيَحسِب المُكوِّن الحاوي حجمه المُفضّل اعتمادًا على ما يَتَضمَّّنه من مُكوِّنات وبحيث يَسمَح ذلك الحجم بأن يَحصُل كل مُكوِّن داخله على حجمه المُفضّل على الأقل. سيَحسِب المُكوِّن الحاوي القيم الصغرى والعظمى لحجمه بنفس الطريقة أي وفقًا للأحجام الصغرى والعظمى لأبنائه.

أثناء التخطيط (layout)، تلتزم أغلب المُكوِّنات الحاوية (container) بضَبْط قيمة عَرْض (width) أي مُكوِّن ابن بحيث تتراوح بين القيمة الصغرى والعظمى لعَرْض المُكوِّن الابن ذاته حتى لو أدى ذلك إلى تَداخُله مع مُكوِّنات آخرى أو إلى تَمدُّده إلى خارج المُكوِّن الحاوي (قد يُصبِح ذلك الجزء المُتمدِّد مرئيًا أو لا بالاعتماد على المُكوِّن الحاوي) أو حتى إلى ترك مساحات فارغة بالمُكوِّن الحاوي. يَنطبِق نفس الأمر على ارتفاع المُكوِّنات الأبناء.

تُعرِّف العُقَد مُتغيِّرة الحجم (resizable nodes) -مثل مُكوِّنات التحكُّم (controls) وغالبية المُكوِّنات الحاوية- توابع نسخ (instance methods) لضَبْط قيم العَرْض الصغرى والعظمى والمُفضّلة، هي كالتالي: setMinWidth(w)‎ و setPrefWidth(w)‎ و setMaxWidth(w)‎ حيث المُعامِل w من النوع double بالإضافة إلى توابع نسخ مشابهة لضَبْط قيم الارتفاع. تَتَوفَّر أيضًا توابع مثل setMaxSize(w,h)‎ و setPrefSize(w,h)‎ لضَبْط قيمتي العرض والارتفاع بنفس الوقت. بالنسبة لمُكوِّن حاوي (container)، ستُبطِل القيم المضبوطة باِستخدَام هذه التوابع تلك القيم التي كانت ستُحسَب بالاعتماد على المُكوِّنات الأبناء.

بمنصة جافا إف إكس (JavaFX)، تُعرَّف المُكوِّنات الحاوية المسئولة عن التخطيط بواسطة الصنف Pane وأصنافه الفرعية (subclasses) الواقعة بحزمة javafx.scene.layout. سنَفْحَص الآن عددًا قليلًا من أصناف التخطيط (layout classes) بدءًا بالصَنْف Pane.

إعداد تخطيط مخصص

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

بفَرْض أن node عُقدة مبيان مشهد، تَضَع التَعْليمَة التالية الركن الأيسر العُلوي للعُقدة بإحداثيات النقطة (x,y) وفقًا لنظام إحداثيات المُكوِّن الحاوي (container) لتلك العُقدة:

node.relocate( x, y );

كما تَضبُط التَعْليمَة التالية حجم العُقدة بشَّرْط أن تَكُون مُتْغيِّرة الحجم (resizable):

node.resize( width, height )

لن يَكُون لتلك التَعْليمَة أي تأثير إذا كانت العُقدة ثابتة الحجم (non-resizable). ومع ذلك تَملُك بعض العُقد ثابتة الحجم -مثل Canvas- توابعًا مثل setWidth(w)‎ و setHeight(h)‎ لضَبْط أحجامها.

إذا كانت العُقدة ضِمْن مُكوِّن حاوي (container) مسئول عن التخطيط (layout) كليًا، فلن يَكُون لأي من التابعين relocate()‎ و resize()‎ أي تأثير. أما إذا كانت ضِمْن مُكوِّن حاوي مثل Pane، فسيُؤثِر التابع resize()‎ بالعُقد مُتْغيِّرة الحجم أما التابع relocate()‎ فلن يَكُون له أي تأثير. يُعدّ ما سَبَق صحيحًا فقط إذا كانت العُقدة مُدارة (managed). يُمكِن ضَبْط عُقدة معينة لتُصبِح غَيْر مُدارة (unmanaged) باستدعاء التالي:

node.setManaged(false);

سنَفْحَص مثالًا يَحتوِي على ٤ مُكوِّنات: زرين (buttons) وعنوان (label) وحاوي (canvas) يَعرِض رقعة شطرنج:

002Null_Layout_Demo.png

عندما يَنقُر المُستخدِم على الأزرار، سيَتبدَّل النص المعروض بمُكوِّن العنوان (label). إلى جانب ذلك، لن يُنفِّذ البرنامج أي شيء فعليّ آخر، فهو فقط مُجرّد مثال بسيط على التخطيط (layout). سنَستخدِم هذا المثال بالقسم ٧.٥ كنقطة انطلاق لبرنامج لعبة داما (checkers).

لأن هذا المثال يَستخدِم كائنًا من الصَنْف Pane كعُقدَة جذرية (root node) للمشهد وكمُكوِّن حاوي (container) للمُكوِّنات الأربعة الآخرى، فإن البرنامج سيَكُون مسئولًا عن ضَبْط مَواضِع المُكوِّنات باستدعاء تابعها relocate()‎ وإلا سيَقَع الركن الأيسر العُلوي لجميع المُكوِّنات بالمَوضِع الافتراضي (٠،٠). سنَضبُط أيضًا حجم الزرين (buttons) لكي يُصبِحا بنفس الحجم الذي سيَكُون أكبر قليلًا من حجمهما المُفضّل، ولكن لمّا كان الزران مدارين (managed)، فإن استدعاء تابعهما resize()‎ لن يَكُون له أي تأثير حيث سيُعيد المُكوِّن الحاوي ضَبْطهما لحجمهما المُفضّل، لذا سنحتاج أولًا لجعلهما غَيْر مُدارين (unmanaged). اُنظر الشيفرة التالية من تابع التطبيق start()‎ الذي يُنشِئ المُكوِّنات الأربعة ويَضبُط مَواضِعها وأحجامها:

/* أنشئ العقد الأبناء */

board = new Checkerboard(); // صنف فرعي من الصنف‫ Canvas
board.draw();  // ارسم محتويات رقعة الشطرنج

newGameButton = new Button("New Game");
newGameButton.setOnAction( e -> doNewGame() );

resignButton = new Button("Resign");
resignButton.setOnAction( e -> doResign() );

message = new Label("Click \"New Game\" to begin.");
message.setTextFill( Color.rgb(100,255,100) ); // Light green.
message.setFont( Font.font(null, FontWeight.BOLD, 18) );

// ‫اضبط موضع كل ابن باستدعاء تابعه relocate() 

board.relocate(20,20);
newGameButton.relocate(370, 120);
resignButton.relocate(370, 200);
message.relocate(20, 370);

/* اضبط حجم الأزرار. لابد أن تجعلها غير مدارة أولًا */

resignButton.setManaged(false);
resignButton.resize(100,30);
newGameButton.setManaged(false);
newGameButton.resize(100,30);

يُمثِل المُكوِّن الحاوي من الصَنْف Pane العُقدَة الجذرية (root node) للمشهد، لذا سيُضبَط حجم نافذة البرنامج بما يتناسب مع الحجم المُفضَّل لذلك المُكوِّن. افتراضيًا، يُحسَب الحجم المُفضَّل للمُكوِّن الحاوي بحيث يَكُون كبيرًا كفاية لعَرْض عُقده الأبناء المُدارة، ولأننا ضَبطَنا الأزرار لتُصبِح غَيْر مُدارة (unmanaged)، فإنها لن تُؤثِر على حجمه المُفضَّل. سنَضبُط عَرْض المُكوِّن الحاوي وارتفاعه بالقيم التالية:

Pane root = new Pane();
root.setPrefWidth(500);
root.setPrefHeight(420);

سنضيف الآن كُلًا من الأزرار (buttons) والعنوان (label) والرقعة (board) كأبناء بالمكون الحاوي بخطوة واحدة كالتالي:

root.getChildren().addAll(board, newGameButton, resignButton, message);

أو على عدة خطوات بحيث يُضَاف كل مُكوِّن على حدى باِستخدَام التَعْليمَة التالية:

root.getChildren().add(board);

أو يُمكِنك أن تُمرِّر العُقد الأبناء (child nodes) كمُعامِلات (parameters) لبَانِي الكائن (constructor) كالتالي:

Pane root = new Pane(board, newGameButton, resignButton, message);

لابُدّ من ضَبْط المُكوِّن الحاوي من الصَنْف Pane ليَكُون المُكوِّن الجذري (root) للمشهد (scene). ينبغي أيضًا أن نَضَع ذلك المشهد بالمرحلة (stage) التي لابُدّ من ضَبْطها وعَرْضها على الشاشة. اُنظر كامل الشيفرة المصدرية بالملف OwnLayoutDemo.java.

قد يَكُون تَخْصِيص التخطيط (layout) بالمثال السابق أمرًا سهلًا على نحوٍ معقول، ولكنه سيُصبِح أصعب كثيرًا إذا كان حجم النافذة عامًلا مُتْغيِّرًا. في تلك الحالة، يُفضَّل عمومًا الاعتماد على أحد المُكوِّنات الحاوية القياسية لتُنفِّذ التخطيط (layout) بدلًا منك، ولكن إذا أردت تّنْفيذه، فلابُدّ من أن تُعرِّف صنفًا فرعيًا من الصَنْف Pane أو من صَنْفها الأعلى Region ثم ينبغي أن تُعيد تَعرِيف (override) التابع layoutChildren()‎ الذي يَستدعِيه النظام لحَث المُكوِّن الحاوي (container) على تطبيق التخطيط (layout) بما في ذلك تَغْيِير حجم المُكوِّن الحاوي.

BorderPane

يُعدّ مُكوِّن الحاوية BorderPane صنفًا فرعيًا (subclass) من الصَنْف Pane. قد يَشتمِل ذلك المُكوِّن على ما يَصِل إلى ٥ مُكوِّنات (components) يَقَع أكبرها بالمنتصف بينما يُرتَّب البقية حوله بالأعلى وبالأسفل وعلى اليسار وعلى اليمين كما هو مُبيَّن بالصورة التالية:

003Border_Layout.png

قد يَشتمِل مُكوِّن الحاوية BorderPane على عدد أقل من خمسة مُكوِّنات لذا أنت لست مُضطّرًا لأن تُخصِّص مُكوِّن بكل مَوْضِع ولكن عادةً ما يُخصَّص مُكوِّن بمنتصف الحاوية.

يُوفِّر الصَنْف BorderPane بانيين (constructors): لا يَستقبِل أحدهما أية مُعامِلات (parameters) بينما يَستقبِل الآخر مُعامِلًا واحدًا يُخصِّص مُكوِّن ابن (child) يُفْترَض وَضعُه بمنتصف الحاوية. بالإضافة إلى ذلك، يُمكِنك أن تُخصِّص العُقد الأبناء (child nodes) الآخرى لحاوية pane من الصَنْف BorderPane باِستخدَام التوابع (methods) التالية:

pane.setCenter(node);
pane.setTop(node);
pane.setRight(node);
pane.setBottom(node);
pane.setLeft(node);

ملحوظة: إذا مَرَّرت مُعامِلًا (parameter) قيمته تُساوِي null لأي من تلك التوابع، فسيُحذَف المُكوِّن الموجود بذلك المَوْضِع ضِمْن الحاوية

تَضبُط مُكوِّنات الحاوية من الصَنْف BorderPane حجم عُقدها الأبناء (child nodes) على النحو التالي (لابُدّ أن يتراوح كُلًا من عرض أي مُكوّن وارتفاعه بين قيمته العظمى والصغرى المُخصَّصة): تَحتَل المُكوِّنات بالمَوْضِع العلوي والسفلي -إن وجدت- مساحة ارتفاعها يُساوِي القيمة المُفضَّلة لارتفاع المُكوِّن أما عَرْضها فيُساوِي عَرْض الحاوية الكلي. في المقابل، تَحتَل المُكوِّنات على اليمين واليسار مساحة عَرْضها يُساوِي القيمة المُفضَّلة لعَرْض المُكوِّن أما ارتفاعها فيُساوِي قيمة ارتفاع الحاوية مطروحًا منها المسافة التي احتلتها المُكوِّنات بكُلًا من المَوْضِع العلوي والسفلي. أخيرًا، يَمتدّ المُكوِّن بمنتصف الحاوية عَبْر المساحة المُتْبقيّة.

تُضبَط القيمة المُفضَّلة لحجم الحاوية على نحو يتناسب مع الحجم المُفضَّل لأبنائها من العُقد المُدارة (managed). تُحسَب القيمة الصغرى بنفس الطريقة أما القيمة العظمى الافتراضية فهي لا نهائية.

تُتيِح بعض الأصناف الفرعية (subclasses) المُشْتقَّة من الصَنْف Pane تطبيق ما يُعرَف باسم "قيود التَخْطِيط (layout constraint)" لتَعْدِيل تَخْطِيط (layout) العُقد الأبناء. على سبيل المثال، ماذا سيَحدُث بحاوية من النوع BorderPane إذا لم يَكُن مَسمُوحًا بإعادة ضَبْط حجم مُكوِّن ابن لكي يتناسب تمامًا مع المساحة المُتوفرة؟ في تلك الحالة، يَحتَل ذلك المُكوِّن مَوْضعًا افتراضيًا ضِمْن تلك المساحة. يَقَع مكون بمنتصف مساحة الحاوية (pane) بينما يقع آخر بالركن الأيسر السفلي منها وهكذا. يُمكِنك أن تُعدِّل الطريقة الافتراضية باِستخدَام التابع الساكن (static method) التالي المُعرَّف بالصَنْف BorderPane:

BorderPane.setAlignment( child, position );

يُشير child إلى العُقدة التي تَرغَب بتَعْدِيل مَوْضِعها أما position فيَحتوِي على إحدى ثوابت نوع التعداد Pos من حزمة package javafx.geometry مثل Pos.CENTER أو POS.TOP_LEFT أو Pos.BOTTOM_RIGHT أو غيرها.

يَسمَح الصَنْف BorderPane بإضافة هامش (margin) لأي مُكوِّن ابن يَقَع ضِمْن الحاوية. الهامش عبارة عن مسافة فارغة تُحيِط بالمُكوِّن الابن وتُلوَّن بنفس لون خلفية الحاوية، ويُسنَد إليها قيمة من النوع Insets المُعرَّف بحزمة package javafx.geometry. يَملُك أي كائن من النوع Insets أربع خاصيات (properties) من النوع double هي top و right و bottom و left والتي يُمكِنك ضَبْطها باِستخدَام بَانِي الكائن (constructor):

new Insets(top,right,bottom,left)

يَتَوفَّر باني كائن (constructor) آخر يَستقبِل مُعاملًا (parameter) واحدًا يُمثِل قيمة الخاصيات (properties) الأربعة. عند اِستخدَامه لتَخْصِيص هامش (margin) حول مُكوِّن ابن معين، تُحدِّد تلك الخاصيات الأربعة عَرْض الهامش (margin) حول جوانبه الأربعة: أعلاه ويمينه وأسفله ويساره.

تستطيع تَخْصِيص الهامش (margin) باِستخدَام التابع الساكن (static method) التالي:

BorderPane.setMargin( child, insets );

على سبيل المثال:

BorderPane.setMargin( topchild, new Insets(2,5,2,5) );

يُمكِنك أيضًا اِستخدَام لغة أوراق الأنماط المتعاقبة (CSS) لتَعْدِيل مَظهَر حاوية (container)، وهي في الواقع واحدة من أسهل الطرائق لضَبْط خاصيات مثل لون الخلفية.

HBox و VBox

إذا أردت ترتيب مجموعة من المُكوِّنات ضِمْن صف أفقي أو عمود رأسي، يُمكِنك اِستخدَام الصنفين HBox و VBox. يُرتِّب الصَنْف الفرعي HBox المُشْتق من الصَنْف Pane أبنائه إلى جوار بعضها البعض ضِمْن صف أفقي بينما يُرتِّب الصَنْف الفرعي VBox المُشتق من الصَنْف Pane أبنائه ضِمْن عمود أفقي. يُمكِنك مثلًا أن تَستخدِم حاوية من الصَنْف HBox بالمَوْضِع السفلي لحاوية من الصَنْف BorderPame مما سيَسمَح باصطفاف عدة مُكوِّنات على طول الحافة السفلية للحاوية. بالمثل، قد تَستخدِم حاوية من الصَنْف VBox بالمَوْضِع الأيسر أو الأيمن لحاوية من الصَنْف BorderPane. سنُناقش الصَنْف HBox فقط ولكن تستطيع اِستخدَام الصَنْف VBox بنفس الكيفية.

بإِمكَانك ضَبْط الصَنْف HBox لكي يَترُك فراغًا بين كل ابن والابن الذي يليه. بفَرْض وجود حاوية hbox، يُمكِنك تَخْصِيص مقدار الفراغ بتمرير قيمة من النوع double للتابع التالي:

hbox.setSpacing( gapAmount );

القيمة الافتراضية تُساوِي صفر. تُضَاف الأبناء إلى حاوية hbox من الصَنْف HBox بنفس الكيفية التي تُضَاف بها إلى حاوية من الصَنْف Pane أي باستدعاء hbox.getChildren().add(child)‎ أو hbox.getChildren().addAll(child1,child2,...)‎. يُوفِّر الصَنْف HBox بانيين (constructor). لا يَستقبِل الأول أية مُعاملات (parameters) بينما يَستقبِل الثاني مُعاملين هما حجم الفراغ المتروك وأي عُقد أبناء (child nodes) قد ترغب بإضافتها إلى تلك الحاوية. تَضْبُط أي حاوية من النوع HBox حجم كل ابن وفقًا لقيمة العَرْض المُفضَّلة لذلك الابن مع إِمكانية تَرك مسافة فارغة إضافية إلى اليمين. إِذا كان مجموع قيم العَرْض المُفضَّلة للأبناء ضِمْن حاوية معينة أكبر من القيمة الفعلية لعَرْض تلك الحاوية، يُقلَّص عَرْض كل ابن منها في حدود قيمة العَرْض الصغرى الخاصة به. في المقابل، يُضبَط ارتفاع الأبناء إلى الارتفاع المتاح بالحاوية بما يَتَوافَق مع كُلًا من القيم الصغرى والعظمى المُخصَّصة لارتفاع تلك الأبناء.

إذا أردت تَوْسِيع عَرْض عدة أبناء ضِمْن حاوية من النوع HBox إلى ما هو أبعد من قيمها المُفضَّلة بغَرْض مَلْئ مساحة مُتاحَة ضِمْن تلك الحاوية، فينبغي أن تُطبِق "قيد تخطيطي (layout constraint)" على كل ابن تَرغَب بتوسيعه باستدعاء التابع الساكن (static method) التالي:

HBox.setHgrow( child, priority );

يَستقبِل التابع بالأعلى مُعاملًا ثانيًا عبارة عن ثابت (constant) من نوع التعداد Priority المُعرَّف بحزمة package javafx.scene.layout. إذا اِستخدَمت القيمة Priority.ALWAYS، فدائمًا ما سيَتَقاسَم ذلك الابن أي مساحة إضافية مُتْبقيّة، ولكنه مع ذلك سيَظَلّ مقيدًا بقيمة العَرْض العظمى الخاصة به لذا قد تَضطّر إلى زيادتها لتَسمَح له بالتَوسُع كما ينبغي.

لنَفْترِض مثلًا وجود حاوية من الصَنْف HBox تَحتوِي على ثلاثة أزرار هي but1 و but2 و but3 والتي تُريِدها أن تَتَوسَّع بما يَكفِي لمَلْئ الحاوية بالكامل. ستحتاج إذًا إلى ضَبْط القيد التخطيطي HGrow لكل زر منها. بالإضافة إلى ذلك، لمّا كانت كُلًا من القيمة العظمى والمُفضَّلة لعَرْض زر مُتساوية، كان من الضروري زيادة القيمة القصوى. بالمثال التالي، ضُبِطَت قيمة العَرْض العُظمى لكل زر إلى قيمة الثابت Double.POSITIVE_INFINITY مما سيُمكِّنها من التَوسُّع بدون أي قيود نهائيًا:

HBox.setHgrow(but1, Priority.ALWAYS); 
HBox.setHgrow(but2, Priority.ALWAYS); 
HBox.setHgrow(but3, Priority.ALWAYS); 
but1.setMaxWidth(Double.POSITIVE_INFINITY); 
but2.setMaxWidth(Double.POSITIVE_INFINITY); 
but3.setMaxWidth(Double.POSITIVE_INFINITY);

ستُوزَّع الآن أي مساحة إضافية بالتَساوِي على الأزرار الثلاثة وتُضاف إلى قيمة عَرْضها المُفضَّلة. لا يَعنِي ذلك أن عَرْضها سيُصبِح متساويًا بالضرورة؛ فقد لا تَكُون قيمها المُفضَّلة الأصلية مُتساوية أساسًا. إذا أردت ضَبْط الأزرار الثلاثة لتُصبِح بنفس الحجم، فلابُدّ من ضَبْط قيمة العَرْض المُفضَّلة لكُلًا منها لتَحمِل نفس القيمة كما يَلي:

but1.setPrefWidth(1000);
but2.setPrefWidth(1000);
but3.setPrefWidth(1000);

سيَحتَلّ الآن كل زر ضِمْن تلك الحاوية مساحة عَرْضها مُتساوي.

تَتَوفَّر طرائق آخرى لتَعْدِيل التخطيط (layout) ضِمْن حاوية من النوع HBox. يُمكِنك مثلًا إضافة هامش (margin) حول أي مُكوِّن ابن باِستخدَام تابع ساكن (static method) مُشابه لذلك المُستخدَم لنفس الغرض بالحاويات من النوع BorderPane. تستطيع أيضًا استدعاء hbox.setFillHeight(false)‎ لضَبْط ارتفاع حاوية معينة إلى القيمة المُفضَّلة لارتفاع أبنائها بدلًا من توسيعهم ليتلائموا مع ارتفاع تلك الحاوية. قد تَستدعِي hbox.setAlignment(position);‎ لضَبْط مَوْضِع ابن ضِمْن حاوية عندما لا يَملَؤها بالكامل. لاحِظ أن مُعاملها (parameter) من النوع Pos بقيمة افتراضية تساوي Pos.TOP_LEFT. يُمكِنك أيضًا أن تُطبِّق عليها خاصيات لغة أوراق الأنماط المتعاقبة (CSS).

سنَفْحَص الآن مثالًا لواجهة مُستخدِم رُسومية (GUI) مبنية بالكامل على الصَنْفين HBox و VBox. يُمكِنك الإطلاع على شيفرة البرنامج بالملف SimpleCalc.java. تَتَضمَّن نافذة البرنامج ما يلي: حَقْلين نصيين من الصَنْف TextField تَسمَح للمُستخدِم بكتابة عدد، بالإضافة إلى أربعة أزرار يُمكِن للمُستخدِم النَقْر عليها لإجراء عملية جمع أو طرح أو ضرب أو قسمة على العددين المُدْخَلين، وأخيرًا عنوان نصي من الصَنْف Label يَعرِض نتيجة العملية المُجراة. تُبيِّن الصورة التالية ما تبدو عليه نافذة البرنامج:

004Simple_Calc.png

تُستخدَم حاوية من الصَنْف VBox كعُقدة جذرية لنافذة البرنامج، وتَحتوِي على أربعة عناصر: الثلاثة الأولى منها عبارة عن حاويات من الصَنْف HBox. يَحتوِي أول حاوي منها على عنوان من النوع Label يَعرِض النص "‎x =‎" بالإضافة إلى حقل نصي من الصَنْف TextField. يُنشَئ ذلك باستخدام الشيفرة التالية:

xInput = new TextField("0");  // Text input box initially containing "0"
HBox xPane = new HBox( new Label(" x = "), xInput );

يُنشَئ العنوان (label) باستخدام باني (constructor) ثم يُضاف مباشرةً إلى الحاوية لعدم حاجتنا إلى مَرجِع (reference) يُشير إليه فيما بَعْد. ستُضاف هذه الحاوية لاحقًا كابن للحاوية الخارجية من الصنف VBox.

سيَعرِض ثالث حاوي الأزرار الأربعة. ولأن القيمة الافتراضية لعَرْض الزر صغيرة نسبيًا، فسنُطبِّق القيد hgrow عليها كما سنُزيد قيمة العرض العظمى لكُلًا منها لكي نَتَأكَّد من كَوْنها تملئ عَرْض الحاوية بالكامل. يُمكِنك القيام بما سبق باِستخدَام الشيفرة التالية:

// ‫انشئ الأزرار الأربعة وحاوية HBox لحملها

Button plus = new Button("+");
plus.setOnAction( e -> doOperation('+') );

Button minus = new Button("-");
minus.setOnAction( e -> doOperation('-') );

Button times = new Button("*");
times.setOnAction( e -> doOperation('*') );

Button divide = new Button("/");
divide.setOnAction( e -> doOperation('/') );

HBox buttonPane = new HBox( plus, minus, times, divide );

/* ‫ينبغي أن تضبط الأزرار الأربعة قليلًا لكي تملئ الحاوية buttonPane 
 * بإمكانك القيام بذلك بضبط القيمة العظمى لعرض كل زر إلى قيمة أكبر */

HBox.setHgrow(plus, Priority.ALWAYS);
plus.setMaxWidth(Double.POSITIVE_INFINITY);
HBox.setHgrow(minus, Priority.ALWAYS);
minus.setMaxWidth(Double.POSITIVE_INFINITY);
HBox.setHgrow(times, Priority.ALWAYS);
times.setMaxWidth(Double.POSITIVE_INFINITY);
HBox.setHgrow(divide, Priority.ALWAYS);
divide.setMaxWidth(Double.POSITIVE_INFINITY);

يَعرِض القسم الأخير بالحاوية الخارجية من النوع VBox مُكوِّنًا واحدًا عبارة عن عنوان من النوع Label. نظرًا لكَوْنه مُكوِّنًا وحيدًا، يُمكِننا إضافته إلى الحاوية الخارجية مباشرةً دون الحاجة لوَضْعه أولًا ضِمْن حاوية من النوع HBox. لكي نَضْمَن ظهور النص بمنتصف النافذة بدلًا من جانبها الأيسر، سنحتاج إلى زيادة قيمة العَرْض العظمى لمُكوِّن العنوان (label) وكذلك إلى ضَبْط خاصية محاذاة النص (alignment) بمُكوِّن العنوان (label) إلى المنتصف بدلًا من اليسار. اُنظر الشيفرة التالية:

answer.setMaxWidth(Double.POSITIVE_INFINITY);
answer.setAlignment(Pos.CENTER);

عندما يَنقُر المُستخدِم على أي من الأزرار الأربعة، يُستدعَى التابع doOperation()‎ ليُنفِّذ ما يَلي: يقرأ العَدَدين الذين أَدْخَلهما المُستخدِم بالحقول النصية (text fields) ثم يُجرِي العملية الحسابية المطلوبة ويَعرِض الناتج بنص العنوان (label). نظرًا لأن محتويات الحقول النصية (text fields) تُعاد كسَلاسِل نصية من النوع String، لابُدّ من تَحْوِيلها إلى قيم عددية أولًا. إذا فشلت عملية التَحْوِيل، يَعرِض العنوان (label) رسالة خطأ:

private void doOperation( char op ) {

    double x, y;  // الأعداد المدخلة

    try {  // اقرأ x من أول صندوق إدخال
        String xStr = xInput.getText();
        x = Double.parseDouble(xStr);
    }
    catch (NumberFormatException e) {
        // ‫لم يكن xStr عدد صالح
        // ‫اعرض رسالة خطأ وانقل موضع التركيز إلى xInput
        // وحدد محتوياته بالكامل
        answer.setText("Illegal data for x.");
        xInput.requestFocus();
        xInput.selectAll();
        return; // توقف عن المعالجة عند حدوث خطأ
    }

    try {  // ‫اقرأ y من ثاني صندوق إدخال
        String yStr = yInput.getText();
        y = Double.parseDouble(yStr);
    }
    catch (NumberFormatException e) {
        answer.setText("Illegal data for y.");
        yInput.requestFocus();
        yInput.selectAll();
        return;
    }

    /* نفذ العملية الحسابية بالاعتماد على قيمة المعامل الممررة */

    if (op == '+')
        answer.setText( "x + y = " + (x+y) );
    else if (op == '-')
        answer.setText( "x - y = " + (x-y) );
    else if (op == '*')
        answer.setText( "x * y = " + (x*y) );
    else if (op == '/') {
        if (y == 0) {  // Can't divide by zero! Show an error message.
            answer.setText("Can't divide by zero!");
            yInput.requestFocus();
            yInput.selectAll();
        }
        else {
            answer.setText( "x / y = " + (x/y) );
        }
    }

} // end doOperation()

يُمكِنك الإطلاع على شيفرة البرنامج بالكامل بالملف SimpleCalc.java.

GridPane و TilePane

سنَطلِّع الآن على الصَنْف الفرعي GridPane المُشتَّق من الصَنْف Pane. يُرتِّب ذلك الصَنْف أبنائه ضِمْن شبكة (grid) من الصفوف والأعمدة مُرقَّمة بدءًا من الصفر. تُبيِّن الصورة التالية شبكة مُكوَّنة من ٤ صفوف و ٥ أعمدة:

005Grid_Layout.png

قد لا تَكُون الصفوف بنفس الارتفاع كما قد لا تَكُون الأعمدة بنفس العرض.

يُمكِنك أن تَتَرُك فراغات بين الصفوف أو بين الأعمدة وستَظهَر خلفية الحاوية بتلك الفراغات. إذا كان grid عبارة عن حاوية من النوع GridPane، يُمكِنك استدعاء التالي لضَبْط حجم تلك الفراغات:

grid.setHGap( gapSize );  // مسافة بين الأعمدة
gris.setVGap( gapSize );  // مسافة بين االصفوف

إذا أردت أن تُضيِف ابنًا (child) إلى حاوية من النوع GridPane، يُمكِنك استدعاء التابع التالي، والذي تستطيع من خلاله تَخْصِيص كُلًا من رقمي الصف والعمود لمَوْضِع ذلك الابن ضِمْن الشبكة:

grid.add( child, column, row );

ملحوظة: يُخصَّص رقم العمود أولًا.

قد يَحتَل ابن (child) أكثر من مجرد صف أو عمود واحد ضِمْن الحاوية. يَستقبِل التابع add عدد الأعمدة والصفوف التي ينبغي لابن أن يَحتَلّها كما يلي:

grid.add( child, column, row, columnCount, rowCount );

يُحدَّد عدد الصفوف والأعمدة ضِمْن شبكة معينة بناءً على قيم المواضع التي أُضيفت إليها الأبناء.

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

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

gridpane.getRowConstraints().addAll(
     new RowConstraints(100), // ارتفاع الصف رقم 0 يساوي 100
     new RowConstraints(150), // ارتفاع الصف رقم 1 يساوي 150
     new RowConstraints(100), // ارتفاع الصف رقم 2 يساوي 100
     new RowConstraints(200), // ارتفاع الصف رقم 3 يساوي 200
);

بالمثال الأعلى، الارتفاع الكلي للحاوية عبارة عن قيمة ثابتة بغَضْ النظر عن مقدار المساحة المُتوفِّرة.

أما إذا اِستخدَمنا النسب (percentages)، فستتوسَّع الحاوية لتَملئ أي مساحة إضافية مُتوفِّرة، وسيُحسَب ارتفاع الصف وعَرْض العمود وفقًا لقيمة تلك النسب. بفَرْض أن gridpane هي كائن حاوية من الصَنْف GridPane مُكوَّنة من خمسة أعمدة، يُمكِننا اِستخدَام الشيفرة التالية للتأكُّد من أن الحاوية تُغطِي العَرْض المُتاح بالكامل، وكذلك للتأكُّد من أن عَرْض جميع أعمدتها متساوي:

for (int i = 0; i < 5; i++) {
   ColumnConstraints constraints = new ColumnConstraints();
   constraints.setPercentWidth(20); // لا يوجد باني يقوم بذلكّ
   gridpane.getColumnConstraints().add(constraints);
}

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

يَستخدِم البرنامج SimpleColorChooser.java من القسم الفرعي ٦.٢.١ حاوية من الصَنْف GridPane. ينبغي الآن أن تَكُون قادرًا على قرائته بالكامل وفهمه.

إذا كنت تحتاج إلى شبكة (grid) مُكوَّنة من مستطيلات مُتساوية الحجم، قد تَستخدِم حاوية من الصَنْف TilePane حيث يُقسَّم سطحها إلى "بلاطات (tiles)" متساوية الحجم ضِمْن صفوف وأعمدة. تُناظِر كل بلاطة (tile) عُقدة واحدة والعكس أي لا يُمكِن تَوزِيع عُقدة على أكثر من بلاطة واحدة.

تَملُك أي حاوية tpane من الصَنْف TilePane عددًا مُفضَّلًا من الصفوف والأعمدة يُمكِنك ضَبْطُه باستدعاء ما يلي:

tpane.setPrefColumns(cols);

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

من الشائع جدًا اِستخدَام حاويات من الصَنْف TilePane عدد أعمدتها المُفضَّل يُساوِي واحد، وتُشبِه عندها الحاويات من الصَنْف VBox كما يَشيِع اِستخدَام حاويات عدد أعمدتها مُساوِي لعدد العُقد الأبناء (child nodes) داخلها، وتُشبِه في تلك الحالة الحاويات من الصَنْف HBox.

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

يُوفِّر الصَنْف TilePane باني كائن (constructor) بدون أية مُعاملات (parameters) وباني آخر يَستقبِل قائمة مُكوَّنة من أي عدد من الأبناء المطلوب إضافتها للحاوية. تستطيع أيضًا إضافة أي أبناء آخرى لاحقًا باِستخدَام أي مما يلي:

tpane.getChildren().add(child);
tpane.getChildren().addAll(child1, child2, ...);

قد تَستخدِم باني الكائن (constructor) التالي لتَخْصِيص حجم الفراغ الأفقي بين الأعمدة أو حجم الفراغ الرأسي بين الصفوف، وستَظهَر خلفية الحاوية بتلك المسافات الفارغة:

TilePane tpane = new TilePane( hgapAmount, vgapAmount );

أو قد تُخصِّصها لاحقًا باستدعاء التوابع tpane.setHgap(h)‎ و tpane.setVgap(v)‎.

ترجمة -بتصرّف- للقسم Section 5: Basic Layout من فصل 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.


×
×
  • أضف...