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


رضوى العربي

تَعلَّمنا خلال الأقسام السابقة كيف نَستخدِم سياقًا رُسوميًا (graphics context) للرَسْم على الشاشة كما تَعلَّمنا كيف نُعالِج كُلًا من أحداث الفأرة (mouse events) وأحداث لوحة المفاتيح (keyboard events). من ناحية، يُمثِل ذلك كل ما هو مُتعلِّق ببرمجة واجهات المُستخدِم الرسومية (GUI) في العموم، فإذا كنت تريد الآن برمجة الرسوم ومعالجة أحداث لوحة المفاتيح والفأرة، فليس هناك أي شيء آخر تحتاج لتَعلُّمه. من ناحية آخرى، فإنك وبهذه المرحلة ستَضطرّ إما للقيام بعَمَل أكبر بكثير مما ينبغي أو ستضطرّ لبرمجة واجهات مُستخدِم (user interfaces) بسيطة. في الواقع، عادةً ما تَتَكوَّن واجهات المُستخدِم التقليدية من مُكوِّنات واجهة قياسية كالأزرار وشرائط التمرير (scroll bars) وصناديق الإِدْخَال النصية والقوائم، ولقد كُتبَت بالفعل تلك المُكوِّنات بطريقة تُمكِّنها من رَسْم نفسها ومُعالجة أحداث لوحة المفاتيح والفأرة الخاصة بها لكي لا تَضطرّ لتَكْرار ذلك.

على سبيل المثال، أحد أبسط مُكوِّنات واجهة المُستخدِم هو "زر الضَغْط (push button)". الزر مُحاط بإطار (border) ويَعرِض نصًا معينًا يُمكِن تَغْييره. في بعض الأحيان، يَكُون الزر مُعطَّلًا (disabled) أي غَيْر مُفعَّل وعندها لا يُصبِح للنقر عليه أي تأثير كما يختلف مظهره نوعًا ما. عندما يَنقُر المُستخدِم على الزر، فإن مظهره يَتَغيَّر لحظة الضَغْط عليه ثم يَعود لمظهره العادي لحظة تَحْرِيره. لاحِظ أنه في حالة تحريك مُؤشِر الفأرة إلى خارج الزر قبل تَحْريره، سيَعُود عندها إلى مظهره العادي بذات اللحظة، كما أن تَحْريره لن يُمثِل نقرة على الزر. لتَتَمكَّن من تّنْفيذ ما سبَق، فلابُدّ أن تستجيب إما لحَدَث سَحْب الفأرة أو لحَدَث خُروج الفأرة. علاوة على ذلك، قد يُصبِح الزر مَوضِع التركيز (focus) ببعض المنصات، وعندها يَتَغيَّر مظهره، ولو ضَغَطَ المُستخدِم على مسطرة المسافات (space bar) في تلك اللحظة، فستُعدّ تلك الضغطة بمثابة نقرة على الزر. لذا لابُدّ أن يستجيب الزر لكُلًا من أحداث لوحة المفاتيح (keyboard events) والتركيز (focus events).

في الواقع، أنت لست مُضطرًا لبرمجة أي من ذلك بِفَرْض اِستخدَامك لكائن ينتمي إلى الصَنْف القياسي javafx.scene.control.Button.

يَتولَّى كائن الزر من الصَنْف Button مسئولية رَسْم نفسه وكذلك مُعالجة كُلًا من أحداث التركيز والفأرة ولوحة المفاتيح. ستحتاج للتَعامُل مع ذاك الزر فقط عندما يَنقُر المُستخدِم عليه أو عندما يَضغَط على مسطرة المسافات (space bar) بينما هو موضع التركيز (focus). عندما يَحدُث ذلك، سيُنشَئ كائن حَدَث (event object) ينتمي إلى الصنف javafx.event.ActionEvent ثم سيُرسَل إلى أي مُستمِع (listener) مُسجَّل لتبلّيغه بأن هنالك من ضَغَطَ على الزر، وعليه، سيَحصُل برنامجك فقط على المعلومة التي يحتاجها وهي أن الزر قد ضُغِطَ عليه.

تُعرِّف واجهة برمجة تطبيقات منصة جافا إف إكس لواجهات المُستخدِم الرسومية (JavaFX GUI API) العديد من مُكوِّنات التَحكُّم (controls) القياسية باستخدام أصناف فرعية (subclasses) من الصنف Control المُعرَّف بحزمة package javafx.scene.control. يستطيع مُستخدِم البرنامج أن يَتعامَل مع تلك المُكوِّنات مما يؤدي إلى إنتاج مُدْخَلات أو وقوع أحداث (events). تُعرِّف مُكوِّنات التَحكُّم (controls) العديد من التوابع المفيدة سنَستعرِض منها ثلاثة يُمكِن اِستخدَامها مع أي مُكوِّن تَحكُّم control من الصنف Control:

  • control.setDisable(true)‎: يُعطِّل (disable) مُكوِّن التحكُّم ويُمكِنك استدعاء control.setDisable(false)‎ لتَفْعيله مرة آخرى. عندما يَكُون مُكوِّن التَحكُّم مُعطَّلًا (disabled)، يَتَغيَّر مظهره ولا يُمكِن له عندها أن يَكُون مقصدًا (event target) لأحداث المفاتيح أو الفأرة. في الواقع، لا يَقْتصِر استدعاء تلك الدالة (function) على مُكوِّنات التَحكُّم (controls) وإنما يُمكِن استدعائها لأي عقدة (node) بمبيان المشهد (scene graph). لاحِظ أنه عند تعطيل عُقدة (node) معينة، فإن جميع العُقد (nodes) المُضمَّنة داخل تلك العُقدة تُصبِح مُعطَّلة أيضًا. تَتَوفَّر الدالة control.isDisabled()‎ والتي تُعيد قيمة منطقية تُحدِّد ما إذا كان مُكوِّن تَحكُّم (control) معين مُعطَّلًا سواء كان ذلك نتيجة لطلب تعطيله صراحةً أو لكَوْنه مُضمَّن بعُقدَة (مُكوِّن حاوي [container]) كانت قد عُطِّلَت.
  • control.setToolTipText(string)‎: يَستقبِل سِلسِلة نصية ويَضبُطها لتُصبِح تلميحًا (tooltip) لمُكوِّن التَحكُّم. عادةً ما يُعطِي ذلك التلميح (tooltip) بعضًا من المعلومات عن مُكوِّن التَحكُّم وطريقة اِستخدَامه ويَظهَر عندما يَقَع مؤشر الفأرة بالمُكوِّن ولا يتحرك لعدة ثواني.
  • control.setStyle(cssString)‎: يَضبُط قواعد الأنماط لمُكوِّن التَحكُّم. تُكْتَب تلك القواعد بلغة أوراق الأنماط المتعاقبة (CSS) التي ناقشناها بالقسم الفرعي ٦.٢.٥.

لكي تَستخدِم مُكوِّن تحكُّم (control) أو أي عقدة آخرى بمبيان المشهد (graph node)، تحتاج عمومًا للقيام بعدة خطوات: لابُدّ أولًا أن تُنشِئ الكائن المُمثِل للمُكوِّن باستخدام باني الكائن (constructor) ثم تُضيفه إلى مُكوِّن حَاوِي (container). غالبًا ما ستحتاج إلى تسجيل مُستمِع (listener) يستجيب إلى الأحداث (events) الصادرة من ذلك المُكوِّن. قد تُخزِّن مَرجِعًا (reference) إلى المُكوِّن بمُتْغيِّر نُسخة (instance variable) في بعض الحالات لكي تَتَمكَّن من إعادة اِستخدَام المُكوِّن بَعْد إنشائه. سنَفْحَص خلال هذا القسم عددًا قليلًا من مُكوِّنات التَحكُّم (controls) القياسية البسيطة المُتاحة بمنصة جافا إف إكس (JavaFX)، والمُعرَّفة باستخدام أصناف بحزمة javafx.scene.control. سنَتَعلَّم بالقسم التالي كيفية وَضْع مُكوِّنات التحكُّم تلك بمُكوِّنات حاوية (containers).

ImageView

قبل مناقشة مُكوِّنات التحكُّم (controls)، سنتناول سريعًا نوع عُقدَة (node) آخر هو ImageView مُعرَّف بحزمة javafx.scene.image. كما ذَكَرَنا سابقًا بالقسم الفرعي ٦.٢.٣، تُمثِل الكائنات من الصنف Image صورًا يُمكِن تحميلها من ملفات موارد (resource files)، ولأن كائنات الصَنْف Image لا تُعدّ عُقدًا من النوع Node، لا يُمكِن لها إذًا أن تَكُون جزءًا من مبيان المشهد (scene graph)، ولهذا سنَضطرّ لرسَمها على كائن حاوية من النوع Canvas.

في الواقع، يَسمَح الصنف ImageView بإضافة صورة إلى مبيان مشهد دون الحاجة إلى رَسْمها على حاوية (canvas)، فكائناتها عبارة عن عُقدَ مبيان مشهد (scene graph node) يَعمَل كُلًا منها كمُغلِّف (wrapper) للصورة تمهيدًا لعَرْضها. تُخصَّص الصورة كمُعامِل (parameter) إلى باني الصَنْف ImageView. لنَفْترِض أن مسار ملف صورة معينة هو "icons/tux.png"، يُمكِننا إذًا أن نُنشِئ كائنًا من النوع ImageView لعَرْض الصورة كالتالي:

Image tux = new Image("icons/tux.png");
ImageView tuxIcon = new ImageView( tux );

نحن هنا نفكر بالصورة كما لو كانت "أيقونة" أي كما لو كانت صورة صغيرة معروضة فوق زر أو ضمن عنوان نصي (label) أو بعنصر قائمة لإضافة لمسة رسومية إلى جانب النص البسيط المعروض، وهو في الواقع ما سنرى كيفية القيام به بمنصة جافا إف إكس (JavaFX).

العناوين Label والأزرار Button

سنَفْحَص الآن أربعة من مُكوِّنات التَحكُّم (controls) تتشارك جميعها في أنها تَعرِض سِلسِلة نصية للمُستخدِم يُمكِنه أن يراها لكن دون أن يُجرِي عليها أي تَعْدِيل كما يُمكِنها أيضًا أن تَعرِض عنصرًا رسوميًا (graphical element) إلى جوار تلك السِلسِلة النصية أو كبديل عنها. يُمكِن لأي كائن من النوع Node أن يُمثِل ذلك العنصر الرسومي، ولكن عادةً ما تُعرَض أيقونة صغيرة مُنفَّذة (implement) باستخدام كائن من النوع ImageView. تَرِث مُكوِّنات التحكُّم (controls) الأربعة سُلوكها (behavior) من صَنْف أعلى (superclass) مشترك هو الصنف Labeled. بالقسم الفرعي ٦.٦.٢، سنَفْحَص عناصر القوائم والتي تَرِث سلوكها من نفس الصَنْف. يُعرِّف الصنف Labeled عددًا من توابع النُسخ (instance methods) التي يُمكِننا أن نَستخدِمها مع العناوين النصية (labels) والأزرار وغيرها من مُكوِّنات التحكُّم المُعنونة، نَستعرِض بعضا منها فيما يلي:

  • setText(string)‎: يَضبُط السِلسِلة النصية المعروضة بمُكوِّن التحكُّم، والتي قد تَتكوَّن من عدة أسطر. يدل محرف سطر جديد ‎\n بسِلسِلة نصية على نهاية السطر.

  • setGraphic(node)‎: يَضبُط العنصر الرسومي (graphical element) لمُكوِّن التحكُّم.

  • setFont(font)‎: يَضبُط الخط المُستخدَم لرسم السِلسِلة النصية.

  • setTextFill(color)‎: يَضبُط لون الملء المُستخدَم لرسم السِلسِلة النصية.

  • setGraphicTextGap(size)‎: يَضبُط عَرْض المسافة الفارغة بين كُلًا من السِلسِلة النصية والعنصر الرسومي. لاحظ أن المُعامِل size من النوع double.

  • setContentDisplay(displayCode)‎: يَضبُط موقع العنصر الرسومي بالنسبة للسِلسِلة النصية. لاحظ أن قيمة المُعامِل ستَكُون واحدة من ثوابت التعداد ContentDisplay أي ContentDisplay.LEFT أو ContentDisplay.RIGHT أو ContentDisplay.TOP أو ContentDisplay.BOTTOM.

يَتَوفَّر تابع جَلْب (getter methods) لكل تابع من توابع الضَبْط (setter methods) السابقة مثل getText()‎ و getFont()‎. يَتَوفَّر أيضًا تابع آخر لضَبْط خاصية لون الخلفية. بفَرْض أن c هو مُكوِّن تحكُّم (control)، يُمكِننا اِستخدَام الشيفرة التالية لضَبْط لون خلفيته إلى الأبيض:

c.setBackground(new Background(new BackgroundFill(Color.WHITE,null,null)));

حيث الأصناف Background و BackgroundFill مُعرَّفة بحزمة javafx.scene.layout. في الواقع، يُمكِنك القيام بنفس الشيء بطريقة أسهل باِستخدَام التابع setStyle()‎ لضَبْط قواعد أنماط CSS الخاصة بمُكوِّن التحكُّم (control)، ويُمكِنك أيضًا اِستخدَامها لضَبْط كُلًا من الإطار (border) والحشوة (padding).

تُمثِل العناوين من الصَنْف Label أحد أبسط أنواع مُكوِّنات التحكُّم، فهو لا يُضيف أي شيء تقريبًا إلى الصنف Labeled، ويُستخدَم لعَرْض نص غَيْر قابل للتَعْدِيل مع رسمة أو بدون. يُعرِّف الصنف Label بانيين (constructors)، يَستقبِل الأول مُعامِلًا من النوع String يُحدِّد نصًا للعنوان (label) أما الثاني فيَستقبِل مُعامِلًا إضافيًا من النوع Node يُحدِّد رسمة (graphic) للعنوان. بفَرْض أن tuxIcon هو كائن من النوع ImageView من القسم الفرعي السابق، اُنظر المثال التالي:

Label message = new Label("Hello World");
Label linuxAd = new Label("Choose Linux First!", tuxIcon);

لاحِظ أن خلفية العناوين (labels) من النوع Label تَكُون شفافة وبدون إطار (border) أو حشوة (padding) افتراضيًا. غالبًا يُفضَّل إضافة قليل من الحشوة على الأقل. اُنظر المثال التالي لضَبْط الخاصيات الثلاثة باِستخدَام لغة CSS:

message.setStyle("-fx-border-color: blue; -fx-border-width: 2px; " +
                           "-fx-background-color: white; -fx-padding: 6px");

لقد تَعرَّضنا للأزرار بالقسم ٦.١. تَعرِض الأزرار من الصنف Button نصًا مع رسمة أو بدون. يُعرِّف الصَنْف Button بانيين (constructors) تمامًا مثل الصَنْف Label:

Button stopButton = new Button("Stop");
Button linuxButton = new Button("Get Linux", tuxIcon);

عندما يَضغَط المُستخدِم على زر، يَقَع حَدَث (event) من النوع ActionEvent، ويُستخدَم التابع setOnAction لتسجيل مُعالِج حدث (event handler) كالتالي:

stopButton.setOnAction( e -> animator.stop() );

بالإضافة إلى التوابع (methods) الموروثة من الصَنْف Labeled، يُعرِّف الصنف Button توابع نُسخ (instance methods) آخرى مثل setDisable(boolean)‎ و setToolTip(string)‎ والتي ذَكَرناها ببداية هذا القسم. يُمكِننا أيضًا اِستخدَام التابعين setDisable()‎ و setText()‎ لإعطاء المُستخدِم بعض المعلومات عما يَحدُث بالبرنامج. اِستخدَامك لزر مُعطَّل (disabled) هو أفضل عمومًا من اِستخدَامك لزر يُعطِي رسالة خطأ مثل "عذرًا، لا يُمكِنك الضَغْط على الزر الآن" بَعْد الضَغْط عليه. لنَفْترِض مثلًا أن لدينا زرين لتَشْغِيل تحريكة (animation) مُمثَلة بالكائن animator من الصَنْف AnimationTimer وإيقافها، فينبغي تَعْطيل زر بدء التَشْغِيل عندما تَكُون التحريكة مُشغَّلة أما زر الإيقاف فينبغي أن يُعطَّل عندما تَكُون التحريكة مُتوقِّفة مؤقتًا. اُنظر الشيفرة التالية:

Button startButton = new Button("Run Animation");
Button stopButton = new Button("Stop Animation");
stopButton.setDisable(true); // Stop button is initially disabled.
startButton.setOnAction( e -> {
    animator.start();
    startButton.setDisable(true);
    stopButton.setDisable(false);
} );
stopButton.setOnAction( e -> {
    animator.stop();
    startButton.setDisable(false);
    stopButton.setDisable(true);
} );

تتأكَّد الشيفرة بالأعلى من أن المُستخدِم لا يُمكِنه أن يحاول بدء تَشْغِيل تحريكة (animation) بينما هي مُشغَّلة بالفعل أو إيقافها عندما تَكُون مُتوقِّفة.

غالبًا ما يُوفِّر البرنامج زرًا يؤدي لحُدوث فِعل (action) افتراضي معين. فمثلًا، قد يُدخِل المُستخدِم مجموعة بيانات بصناديق إِدْخَال نصية ثم يَنقُر على زر "Compute" لمعالجة تلك البيانات. سيَكُون من الرائع لو تَمكَّن المُستخدِم من مُجرّد الضغط على المفتاح "Return" عند انتهاءه من الكتابة بدلًا من النقر على الزر. في الحقيقة، يُمكِنك ضَبْط الكائن button من الصَنْف Button ليُصبِح زر النافذة الافتراضي (default button) باستدعاء التالي:

button.setDefaultButton(true);

في حالة وجود زر افتراضي (default button) بنافذة (window) معينة، فإن الضَغْط على المفتاح "Return" أو "Enter" بلوحة المفاتيح يَكُون مكافئًا للنقر على ذلك الزر الافتراضي إلا لو اُستُهلِك حَدَث (event) الضَغْط عليه من قِبَل مُكوِّن آخر.

مربعات الاختيار CheckBox وأزرار الانتقاء RadioButton

مربعات الاختيار (check box) من الصَنْف CheckBox هي نوع آخر من مُكوِّنات التَحكُّم. أي مربع اختيار له "حالة (state)"، فإما أن يَكُون مختارًا (selected) أو غَيْر مُختار (unselected)، ويستطيع المُستخدِم عمومًا أن يُغيِّر حالة مربع اختيار (check box) معين بالنقر عليه. تُستخدَم قيمة من النوع boolean لتمثيل حالة مربع الاختيار بحيث تَكُون مُساوية للقيمة المنطقية true إذا كان مختارًا (selected) وللقيمة المنطقية false إذا كان غَيْر مُختار. علاوة على ذلك، يَعرِض أي مربع اختيار (checkbox) عنوانًا (label) يُخصَّص عند إنشائه كالتالي:

CheckBox showTime = new CheckBox("Show Current Time");

لأن الصَنْف CheckBox هو صَنْف فرعي (subclass) من الصَنْف Labeled، يُمكِنك بطبيعة الحال اِستخدَام جميع توابع النُسخ (instance methods) المُعرَّفة بالصَنْف Labeled مع كائنات مربعات الاختيار (checkboxes). بالمثل من مُكوِّنات التَحكُّم السابقة (controls)، يُمكِن لمربعات الاختيار أن تَعرِض رسمة (graphic)، ولكنها لا تُوفِّر باني (constructor) لضَبْط تلك الرسمة وإنما لابُدّ من استدعاء setGraphic(node)‎ لضَبْطها.

عادةً ما يَكُون المُستخدِم هو المسئول عن ضَبْط حالة مربع اختيار من الصَنْف CheckBox بالنقر عليه، ولكن من الممكن أيضًا ضَبْط حالته (state) برمجيًا باستدعاء التابع setSelected(boolean)‎. على سبيل المثال، إذا كان لديك كائن مربع اختيار showTime من الصَنْف CheckBox، فبإمكانك اختياره (selected) باستدعاء showTime.setSelected(true)‎ أو إلغاء الاختيار باستدعاء showTime.setSelected(false)‎. يَتَوفَّر أيضًا التابع isSelected()‎ والذي يُعيد قيمة منطقية من النوع boolean تُحدِّد الحالة (state) الحالية لمربع الاختيار.

عندما يُغيِّر مُستخدِم من حالة (state) مربع اختيار (checkbox) من الصَنْف CheckBox، يَقَع حَدَث (event) من النوع ActionEvent، لذا إذا أردت أن تُنفِّذ شيئًا أثناء تَغيُّر حالة مربع اختيار معين، فلابُدّ إذًا من أن تُسجِّل معالجًا (handler) بذلك المربع عن طريق استدعاء تابعه setOnAction()‎. ولكن في العادة، لا تحتاج البرامج لذلك وتَقْتصِر على فَحْص حالة مربعات الاختيار باستدعاء التابع isSelected()‎. لاحِظ أنه في حالة تَغيُّر الحالة برمجيًا أي باستدعاء التابع setSelected()‎، لا يَقَع حَدَث من النوع ActionEvent، ولكن هنالك تابع آخر مُعرَّف بالصنف CheckBox اسمه هو fire()‎، والذي يُحاكِي حُدوث نقرة على مربع الاختيار ويُولِّد حَدَثًا من النوع ActionEvent.

في الواقع، هنالك حالة ثالثة لمربعات الاختيار (checkboxes): "غير مُحدَّد (indeterminate)" على الرغم من كَوْنها غَيْر مُفعَّلة افتراضيًا. اُنظر توثيق واجهة برمجة التطبيقات (API) لمزيد من التفاصيل.

تتشابه أزرار الانتقاء (radio buttons) مع مربعات الاختيار (check boxes) نوعًا ما. كما هو الحال مع مربعات الاختيار، يُمكِن لأي زر انتقاء أن يَكُون مُختارًا (selected) أو غَيْر مُختار. يُفْترَض لأزرار الانتقاء (radio buttons) أن تَقَع ضِمْن مجموعات، ويُسمَح باختيار زر انتقاء وحيد على الأكثر ضِمْن المجموعة بأي لحظة، بتعبير آخر، تَسمَح مجموعات أزرار الانتقاء للمُستخدِم باختيار وحيد من عدة بدائل. تُستخدَم كائنات الصَنْف RadioButton بمنصة جافا إف إكس لتمثيل أزرار الانتقاء (radio button). عند اِستخدَامها بمفردها، فإنها تكون شبيهة تمامًا بمربع اختيار من الصَنْف CheckBox، فلديها نفس الباني (constructor)، والتوابع (methods)، والأحداث (events) بما في ذلك التوابع الموروثة من الصنف Labeled. لكنها غالبًا ما تُستخدَم ضِمْن مجموعة، ويُستخدَم عندها كائن من الصنف ToggleGroup لتمثيل المجموعة. لاحِظ أن الصنف ToggleGroup ليس بمُكوِّن ولا يظهر على الشاشة، فهو يعمل فقط خلف الكواليس كمُنظم لمجموعة من أزرار الانتقاء (radio buttons) ليتأكَّد من اختيار زر واحد فقط ضِمْن المجموعة بأي لحظة.

لكي تَستخدِم مجموعة أزرار انتقاء (radio buttons)، لابُدّ أن تُنشِئ كائنًا من الصَنْف RadioButton لكل زر انتقاء ضِمْن المجموعة بالإضافة إلى كائن واحد من النوع ToggleGroup لتنظيمها. لا يلعب الصنف ToggleGroup أي دور فيما يتعلَّق برسم الأزرار على الشاشة، فلابُدّ أن تُضيف كُلًا منها على حدى إلى مبيان المشهد (scene graph) لكي يَظهَر على الشاشة. لابُدّ أيضًا أن تُضيف كُلًا منها إلى كائن الصنف ToggleGroup من خلال استدعاء تابع النُسخة setToggleGroup(group)‎ بكائن زر الانتقاء. إذا أردت اختيار أحد الأزرار بصورة افتراضية، يُمكِنك استدعاء التابع setSelected(true)‎ لذلك الزر، وإن لم تَفعَل، فلن يَكُون أيًا منها مُختارًا (selected) إلى أن يَنقُر المُستخدِم على إحداها.

على سبيل المثال، تَستعرِض الشيفرة التالية طريقة تهيئة مجموعة من أزرار الانتقاء (radio buttons) التي تَسمَح للمُستخدِم باختيار لون:

RadioButton redRadio, blueRadio, greenRadio, yellowRadio;
         // تمثل المتغيرات أزرار انتقاء
         // قد تكون تلك متغيرات نسخة لكي تستخدم عبر البرنامج

ToggleGroup colorGroup = new ToggleGroup();

redRadio = new RadioButton("Red");   // اِنشئ زر
redRadio.setToggleGroup(colorGroup); // أضفه إلى مجموعة الأزرار

blueRadio = new RadioButton("Blue");
blueRadio.setToggleGroup(colorGroup);

greenRadio = new RadioButton("Green");
greenRadio.setToggleGroup(colorGroup);

yellowRadio = new RadioButton("Yellow");
yellowRadio.setToggleGroup(colorGroup);

redRadio.setSelected(true);  // Make an initial selection.

بدلًا من استدعاء التابع redRadio.setSelected(true)‎، يُمكِنك أن تَستدعِي تابع النسخة selectToggle()‎ المُعرَّف بالصَنْف ToggleGroup لاختيار زر الانتقاء (radio button) كالتالي:

colorGroup.selectToggle( redRadio );

تمامًا كمربعات الاختيار (checkboxes)، لست مضطرًّا إلى أن تُسجِّل مُستمِعًا (listener) لأحداث أزرار الانتقاء (radio buttons). يُمكِنك فَحْص حالة (state) زر انتقاء معين من الصَنْف RadioButton باستدعاء تابعه isSelected()‎ أو باستدعاء التابع getSelectedToggle()‎ بكائن المجموعة من الصَنْف ToggleGroup. يُعيد ذلك التابع قيمة نوعها Toggle عبارة عن واجهة (interface) يُنفِّذها (implement) الصَنْف RadioButton. اُنظر المثال التالي:

Toggle selection = colorGroup.getSelectedToggle();
if (selection == redRadio) {
    color = Color.RED;
}
else if (selection == greenRadio){
   .
   .
   .

تُوضِح الصورة التالية كيف ستبدو أزرار الانتقاء (radio buttons) بالأعلى عند اصطفافها رأسيًا ضِمْن مُكوِّن حاوية (container):

001_Color_Radio_Buttons.png

الحقول النصية TextField والمساحات النصية TextArea

يُمثِل الصنفان TextField و TextArea مُكوِّنات إِدْخَال نصي (text input component) حيث تَعرِض نصًا يستطيع المُستخدِم أن يُعدّله. يُمكِن لأي حقل نصي من الصنف TextField أن يَحمِل سطرًا واحدًا فقط أما المساحة النصية من الصَنْف TextArea فيُمكِنها أن تَحمِل عدة أسطر. تستطيع أن تضبط حقلًا نصيًا TextField معينًا أو مساحة نصية TextArea معينة بحيث تَكُون للقراءة فقط (read-only) أي سيَتَمكَّن المُستخدِم عندها من قراءة النص الموجود لكنه لن يَتَمكَّن من تَعْديله. في الواقع، الأصناف TextField و TextArea هي أصناف فرعية (subclasses) من صَنْف مُجرّد (abstract class) اسمه TextInputControl يُعرِّف بعضًا من الخاصيات المشتركة بينها.

يشترك الصَنْفان TextField و TextArea بكثير من التوابع (methods) سنَستعرِض بعضًا منها فيما يلي: يُعيد تابع النُسخة getText() قيمة من النوع String تُمثِل محتويات مُكوِّن إِدْخَال معين. يُستخدَم تابع النسخة setText(text) لتَغْيير النص المعروض بمُكوِّن إِدْخَال حيث يَستقبِل مُعامِلًا من النوع String ويستبدله بالنص الحالي لمُكوِّن الإدخال. يُستخدَم تابع النسخة appendText(text)‎ لإضافة سِلسِلة نصية من النوع String بنهاية النص الموجود بالفعل بمُكوِّن إِدْخَال. قد يَتَضمَّن النص المُمرَّر إلى التابعين setText()‎ و appendText()‎ محرف سطر جديد ‎\n لتمثيل نهاية السطر، ولكنه لن يُؤثِر بالحقول النصية من الصَنْف TextField. يُمكِنك أيضًا أن تَستخدِم تابع النسخة setFont(font)‎ لتَغْيير الخط المُستخدَم بمُكوِّن الإدخال النصي.

يُمكِنك أيضًا استدعاء التابع setEditable(false)‎ لمَنْع المُستخدِم من تَعْدِيل نص مُكوِّن إِدْخَال معين. مَرِّر القيمة المنطقية true لنفس التابع لكي تجعله قابل للتَعْدِيل مرة آخرى.

يستطيع المُستخدِم أن يَكْتُب بمُكوِّن إِدْخَال نصي (input component) معين فقط بَعْد جَعَله موضع التركيز (focus) عن طريق النقر عليه بالفأرة. يُمكِنك أيضًا استدعاء التابع requestFocus()‎ لمُكوِّن إِدْخَال -مثل الحقول النصية (text fields)- لضبطه برمجيًا ليُصبِح موضع التركيز (focus)، وهو ما قد يَكُون مفيدًا في بعض الأحيان.

يستطيع المُستخدِم أيضًا أن يُحدِّد جزءًا من نص مُكوِّن إدخال ويَظهَر عندها مُظلَّلًا ويُمكِن قَصُه (cut) أو نَسخُه (copy) كما يَتَضمَّن الصَنْف TextInputComponent مجموعة من توابع النسخ (instance methods) لأغراض تَحْدِيد النصوص (text selection) منها التابع selectAll()‎ والذي يُحدِّد نص مُكوِّن إِدْخَال بأكمله.

على سبيل المثال، عندما تَكْتشِف وجود خطأ بالقيمة التي أَدْخَلها المُستخدِم بحَقْل نصي input من الصَنْف TextField، يُمكِنك عندها أن تَستدعِي كُلًا من التابعين input.requestFocus()‎ و input.selectAll()‎، وهو ما يُساعِد المُستخدِم على اكتشاف مكان حُدوث الخطأ كما يَسمَح له ببدء تصحيحه مباشرة؛ فبمُجرّد أن يبدأ بالكتابة، سيُحذَف النص المُظلَّل.

يُعرِّف الصَنْفين TextField و TextArea بانيين (constructors). لا يَستقبِل الباني الأول أي مُعامِلات (parameter) ويُستخدَم لإنشاء مُكوِّن إِدْخَال نصي فارغ أما الثاني فيَستقبِل مُعامِلًا من النوع String يُخصِّص نصًا افتراضيًا لمُكوِّن الإِدْخَال.

تملك الحقول النصية من الصَنْف TextField خاصية عدد الأعمدة (columns)، والتي تُخصِّص العرض (width) المُفضّل للحقل، وتُساوِي ١٢ بشكل افتراضي. تُستخدَم تلك القيمة المُفضّلة لضَبْط حجم الحقل إذا لم يُعاد ضَبْطه بواسطة البرنامج على سبيل المثال. بفَرْض وجود كائن حقل نصي input من الصَنْف TextField، يُمكِنك استدعاء التابع input.setPrefColumnCount(n)‎ لضَبْط عدد الأعمدة حيث n هي عدد صحيح موجب.

على نحو مُشابه، تَملُك المساحات النصية من الصَنْف TextArea عدد أعمدة يُساوِي ٤٠ افتراضيًا وكذلك عدد صفوف يُساوِي ١٠ افتراضيًا. يُستخدَم تابعي النسخة setPrefColumnCount(n)‎ و setPrefRowCount(n)‎ بالصَنْف TextArea لتَعْدِيل قيمتهما على الترتيب.

إلى جانب التوابع (methods) الموروثة (inherit) من الصَنْف TextInputControl، يُعرِّف الصَنْف TextArea عدة توابع آخرى بما في ذلك تابع لضَبْط مقدار المسافة التي نزلها النص، وآخر لجَلْب ذلك المقدار. مثال آخر هو التابع setWrapText(wrap)‎ حيث wrap هو مُعامِل من النوع boolean. يُخصِّص ذلك التابع طريقة عَرْض الأسطر النصية الطويلة التي لا تَتَناسَب مع حجم المساحة النصية (text area). إذا مرَّرنا true كقيمة للمُعامِل wrap، فإنها ستُقسَّم إلى عدة أسطر مع إضافة محرف سطر جديد بين الكلمات إن أمكن. أما إذا مرَّرنا القيمة false، فإنها ستمتد إلى خارج المساحة النصية وسيضطرّ المُستخدِم إلى تمرير المساحة النصية أفقيًا (scroll) ليرى محتوى السطر بالكامل. لاحِظ أن قيمة wrap الافتراضية هي false.

لمّا كانت الحاجة لتَحرِيك مساحة نصية (text area) من الصَنْف TextArea أفقيًا (scroll) ضرورية في أحيان كثيرة لكي نَتَمكَّن من رؤية مُحتواها النصي بالكامل، فإن ذلك الصَنْف يُوفِّر شرائط تمرير (scroll bars) تُصبِح مرئية عند الضرورة فقط أي عندما لا يَتَناسَب النص مع المساحة المتاحة.

يُمكِنك الإطلاع على ملف البرنامج TextInputDemo.java والذي يُوظِّف حقلًا نصيًا (text field) ومساحة نصية (text area) ضِمْن مثال بسيط. تُبيِّن الصورة التالية نافذة البرنامج بَعْد تَعْدِيل النص وتمريره للأسفل قليلًا:

002_Text_Input_Demo.png

المنزلق Slider

يَسمَح مُنزلِق (slider) من الصَنْف Slider للمُستخدِم باختيار قيمة عددية صحيحة ضِمْن نطاق من القيم المُحتملة عن طريق سَحْب عُقدة على طول شريط. يُمكِنك تَزْيِين المُنزلِق بعلامات تجزئة (tick marks) أو بعناوين (labels). تَعرِض نافذة البرنامج SliderDemo.java بالأسفل ثلاثة مُنزلِقات يَسمَح كُلًا منها بنطاق مُختلف من القيم كما أنها مُزيَّنة بشكل مختلف:

003Slider_Demo.png

اُستخدِمت علامات تجزئة لتَزْيِين المُنزلِق الثاني بينما اُستخدِمت عناوين لتَزْيِين المُنزلِق الثالث. يُمكِنك أيضًا تَزْيِين مُنزلِق واحد بكلتا الطريقتين معًا.

يُخصِّص الباني (constructor) الأكثر شيوعًا بالصَنْف Slider بداية ونهاية نطاق القيم المُحتملة للمُنزلِق (slider) وكذلك قيمته الافتراضية عند عَرْضه على الشاشة لأول مرة. يُكْتَب ذلك الباني كالتالي:

public Slider(double minimum, double maximum, double value)

إذا لم نُمرِّر أي مُعامِل (parameter) للباني، ستُستخدَم القيم ٠ و ١٠٠ و ٠ افتراضيًا وعلى الترتيب. يَظهَر المُنزلِق أفقيًا بصورة افتراضية، ولكن يُمكِنك استدعاء setOrientation(Orientation.VERTICAL)‎ لضَبْطه بحيث يَظهَر رأسيًا. لاحِظ أن Orientation عبارة عن تعداد مُعرَّف بحزمة package javafx.geometry.

يُمكِنك قراءة القيمة الحالية لمُنزلِق (slider) من الصَنْف Slider بأي لحظة باستدعاء تابعه getValue()‎ والذي يُعيد قيمة من النوع double. أما إذا أردت ضَبْط قيمته، فيُمكِنك استدعاء التابع setValue(val)‎ والذي يَستقبِل مُعاملًا (parameter) من النوع double. إذا كانت القيمة المُمرَّرة لتابع الضَبْط واقعة خارج نطاق القيم المسموح بها، فإنها ستُعدَّل لكي تُصبِح ضِمْن ذلك النطاق.

إذا أردت أن تُنفِّذ شيئًا عندما يُغيِّر المُستخدِم قيمة مُنزلِق (slider) معين، فينبغي أن تُسجِّل مُستمِعًا (listener) بذلك المُنزِلق. بخلاف مُكوِّنات التَحكُّم الآخرى، لا تُولِّد المنزلِقات من الصَنْف Slider أحداثًا من النوع ActionEvent، وإنما تَملُك خاصية قابلة للمُراقبة (observable) من النوع Double تُمثِل قيمته (انظر القيم الفرعي ٦.٣.٧). بفَرْض وجود كائن مُنزلِق slider من الصَنْف Slider، يُمكِنك استدعاء التابع slider.valueProperty()‎ لمَعرِفة قيمته. يُمكِنك أيضًا أن تُسجِّل مُستمِعًا (listener) لتلك الخاصية سيُستدعَى بكل مرة تَتَغيَّر فيها قيمة المُنزلِق. تُبيِّن الشيفرة التالية كيفية إضافة مُستمِع (listener) لكائن مُنزلِق:

slider1.valueProperty().addListener( e -> sliderValueChanged(slider1) );

سيُستدعَى ذلك المُستمِع (listener) كلما تَغيَّرت قيمة المُنزلِق سواء كان ذلك نتيجة لسَحْب العقدة على الشريط أو لاستدعاء البرنامج للتابع setValue()‎. يُمكِنك استدعاء التابع isValueChanging()‎ المُعرَّف بكائن المُنزلِق لمَعرِفة ما إذا كان المُستخدِم هو المسئول عن وقوع الحَدَث (event) حيث يُعيد ذلك التابع القيمة المنطقية true إذا كان المُستخدِم يَسحَب العقدة على الشريط.

لعرض علامات التجزئة (tick marks) على مُنزلِق (slider)، ستحتاج للقيام بالخطوتين التاليتين: اُضبط أولًا الفاصل بين علامات التجزئة (tick marks) ثم بلِّغ المُنزلِق برغبتك بعَرْض علامات التجزئة. في الحقيقة، هنالك نوعين من علامات التجزئة: علامات تجزئة رئيسية (major) وأخرى ثانوية (minor)، ويُمكِنك عَرْْض واحدة منهما فقط أو كلتاهما. علامات التجزئة الرئيسية تَكُون أطول قليلًا بالمُوازنة مع علامات التجزئة الثانوية. يَستقبِل التابع setMajorTickSpacing(x)‎ مُعامِلًا من النوع double ويُشير إلى أن علامات التجزئة الرئيسية ينبغي أن تُعرَض بفارق x من الوحدات على طول المُنزلِق. لاحِظ أن الفاصل بين تلك العلامات يَكُون على أساس قيم المُنزلِق وليس البكسل. في المقابل، يَستقبِل التابع setMinorTickCount(n)‎ مُعامِلًا من النوع int، ويُخصِّص عدد علامات التجزئة الثانوية المُفْترَض عَرْضها بين كل علامتين رئيسيتين متتاليتين وتُساوِي ٤ افتراضيًا. يُمكِنك ضَبْطها إلى صفر إذا لم ترغب بعَرْض أي علامات تجزئة ثانوية. لاحِظ أن استدعاء تلك التوابع (methods) ليس كافيًا لعَرْض علامات التجزئة (tick marks)، بل ستحتاج أيضًا إلى استدعاء setShowTickMarks(true)‎. على سبيل المثال، تُنشِئ التَعْليمَات التالية المُنزلِق (slider) الثاني من البرنامج السابق وتَضبُطه:

slider2 = new Slider();  // استخدم القيم الافتراضية ‫(0,100,0)
slider2.setMajorTickUnit(25); // المسافة بين علامات التجزئة الرئيسية
slider2.setMinorTickCount(5); // ‫5 علامات تجزئة بين كل علامتي تجزئة
slider2.setShowTickMarks(true);

تُعالَج العناوين (labels) على المنزلق (slider) بنفس الطريقة، فسيُعرَض العنوان بكل علامة تجزئة رئيسية، ولكن قد تُحذَف بعض العناوين إذا تداخلت مع بعضها البعض. ستحتاج إلى استدعاء setShowTickLabels(true)‎ لعَرْض العناوين. على سبيل المثال، تُنشِئ التَعْليمَات التالية المُنزلِق (slider) الثالث من البرنامج السابق وتَضبُطه:

slider3 = new Slider(2000,2100,2018);
slider3.setMajorTickUnit(50); // لا تعرض علامات التجزئة
slider3.setShowTickLabels(true)

لاحِظ أن قيمة أي مُنزلِق (slider) تَكُون من النوع double. أحيانا، قد تَرغَب بقَصرِها على الأعداد الصحيحة فقط أو على مضاعفات قيمة معينة. في مثل هذه الحالات، يُمكِنك استدعاء التابع slider.setSnapToTicks(true)‎، وعندها ستُضبَط قيمة المُنزلِق تلقائيًا إلى أقرب علامة تجزئة رئيسية أو ثانوية -حتى لو لم تَكُن مرئية- بعدما ينتهي المُستخدِم من سَحْب العُقدَة. لا يُطبَق ذلك التقييد بينما يسَحَب المُستخدِم العُقدَة وإنما تُضبَط قيمته فقط بَعْد انتهائه. لا يَلتزِم أيضًا التابع setValue(x)‎ بتلك القيود، لذا قد تَستخدِم التابع adjustValue(x)‎ بدلًا عنه والذي يَضبُط قيمة المُنزِلق لأقرب علامة تجزئة. على سبيل المثال، إذا أردت لمُنزلِق أن يَقْتصِر على الأعداد الصحيحة بنطاق يتراوح من ٠ إلى ١٠، يُمكِنك اِستخدَام التالي:

Slider sldr = new Slider(0,10,0);
sldr.setMajorTickUnit(1);  // major ticks 1 unit apart
sldr.setMinorTickCount(0); // لا توجد علامة تجزئة ثانوية
sldr.setSnapToTicks(true);

ضُبِطَ المُنزلِق الثالث بالمثال التوضيحي بحيث يَقْتصِر على قيمة صحيحة بنهاية أي عملية سَحْب.

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





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن