تعدد المسالك Multi-Threading في لغة الجافا  الصف SwingWorker

واجهة المستخدم GUI وتعدد الخيوط

عند استخدام الخيوط المتعددة مع واجهات المستخدم GUI هناك أمور يجب أن تؤخذ في الحسبان:

  • من أجل التفاعل مع المستخدم عبر الواجهة GUI تقدم لغة الجافا مفهوم الأحداث والتي تمكن البرنامج من التقاط الأحداث التي تحصل على الواجهة مثل نقر زر الفأرة، تحريك مؤشر الفأرة…
  • يتم التقاط هذه الأحداث عن طريق خيط خاص يعمل في الخلفية يسمى Event Dispatch Thread اختصاراً EDT
  • تعديل واجهة المستخدم برمجياً يجب أن يتم حصراً عبر الخيط EDT، مثلاً تغيير لون الخلفية برمجياً، إضافة عنصر لقائمة منسدلة…
  • العمليات الطويلة نسبياً يجب أن تتم عبر خيط مستقل غير الخيط EDT وإلا فإن واجهة المستخدم ستصبح غير قابلة للاستجابة.

وهنا برزت المشكلة التالية:

أثناء تنفيذ العمليات الطويلة نسبياً في خيط مستقل قد نحتاج إلى تعديل واجهة المستخدم ومتابعة العمل، فمثلاً عند تحميل ملف من الانترنت عبر خيط مستقل يجب تحديث مؤشر التحديث في الواجهة بشكل مستمر أثناء التحميل كي يتمكن المستخدم من معرفة أين وصل التحميل.

أيضاً بعد انتهاء العمليات الطويلة قد نضطر لتعديل واجهة المستخدم من أجل عرض النتيجة، مثلاً بعد تحميل صورة من الانترنت نحن بحاجة لعرضها في الواجهة.

هناك الكثير من الاستراتيجيات لحل هذه المشكلة ولعل أسهلها استخدام الصف SwingWorker.

الصف SwingWorker

هو عبارة عن صف مجرد يستخدم لإنشاء خيط لتنفيذ العمليات الطويلة ويتيح إمكانية التعديل على واجهة المستخدم بشكل سهل ومرن.

كيفية استخدام الصف SwingWorker

سأقوم بشرح عمل هذا الصف من خلال مثال يحتوي على واجهة بسيطة (Jframe) بداخلها زر كتب عليه كلمة Download، وتحت الزر يوجد عنصر JProgressbar وهو عبارة عن شريط أفقي يظهر التقدم الحاصل في عملية التحميل.

في البداية باستخدام المصمم الخاص ببرنامج Netbeans قم بتصميم الواجهة التالية:

SwingWroker-GUI-Example-01

بعد ذلك قمم بالنقر مرتين على الزر لتنتقل إلى الدالة المسؤولة عن معالجة حدث النقر على الزر والتي تكون بالشكل التالي:


private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         
        
}

ضمن هذه الدالة يجب علينا كتابة الكود المسؤول عن عملية التحميل من الانترنت مع أخذ بعين الاعتبار الأمور التالية:

  • عملية التحميل يجب أن تكون في خيط مستقل
  • مع كل تقدم لعملية التحميل يجب تحديث ال progressBar ليشير إلى أين وصلت عملية التحميل
  • بعد انتهاء عملية التحميل بشكل كامل يجب إظهار رسالة للمستخدم تفيد أنه تم الانتهاء من عملية التحميل

ملاحظة

لن أقوم في هذا المثال بشرح كيفية تحميل الملفات من الانترنت، سأكتفي بوضع كود وهمي يقوم بتأخير زمني يوحي بأن هناك عملية تحميل في الخلفية، بالنسبة لتحميل الملفات سأشرح ذلك في دروس مستقلة تشمل كل ما يتعلق ببرمجة الشبكات بلغة الجافا.

قبل استخدام الصف SwingWorker في الحل علينا أن ندرك عدة أمور، أولها ما هو نمط البيانات الذي سأحصل عليه كنتيجة لتنفيذ الخيط، مثلاً في عملية تحميل صورة من الانترنت تكون النتيجة من نمط Image وفي تحميل ملف نصي تكون النتيجة من نمط String …وهكذا

الأمر الثاني الذي يجب علينا معرفته، هو نوع البيانات المرحلية التي يجب إرسالها إلى واجهة المستخدم، فمثلاً في عملية تحميل ملف من الانترنت يتم في كل مرحلة إرسال رقم من نمط Integer يعبر عن نسبة تقدم عملية التحميل مثلاً 1% ثم 2% ثم 3% …وهكذا. بعد أن عرفنا هذه المعلومات يمكننا متابعة الشيفرة البرمجية التالية:

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
    SwingWorker<Image, Integer> worker = new SwingWorker<Image, Integer>() {
        @Override
        protected Image doInBackground() throws Exception {
            for (int i = 0; i < 100; i++) {
                // put image downloading code
                publish(i);// send progress value to UI
                Thread.sleep(100);// psudo code to simulate image downloading
            }
            return null;
        }

        @Override
        protected void process(List<Integer> chunks) {
            // recive progress value sent by publish method
            int progress = chunks.get(chunks.size() - 1);
            // using the value to set progress bar
            jProgressBar1.setValue(progress);
        }

        @Override
        protected void done() {
            jProgressBar1.setValue(100);
            JOptionPane.showMessageDialog(null, "Download Completed");
        }

    };

    worker.execute();
}

في السطر الأول قمنا بإنشاء كائن من الصف SwingWorker ولاحظ أثناء عملية إنشاء الكائن قمت بتمرير ثلاث وسطاء (قوالب) ضمن إشارتي <> هذه الوسطاء هي على الترتيب من اليسار إلى اليمين:

نمط البيانات النهائية (ناتج تنفيذ الخيط)

نمط البيانات المرحلية (الرقم الذي يعبر عن نسبة تقدم التحميل)

وبما أن هذا الصف صف مجرد فنحن بحاجة لتحقيق الدالة المجرد doInBackground. وقد استخدمت هنا طريقة الصف المجهول لتحقيق هذه الدالة.

ضمن الصف المجهول الخاص بي ستجد ثلاث دوال هي على الترتيب

doInBackground

process

done

أسماء هذه الدوال توحي فعلاً بعملها فضمن الدالة doInBackground يجب وضع الشيفرة البرمجية المسؤولة عن عملية التحميل.

وضمن الدالة process يجب وضع الشيفرة المسؤولة عن معالجة النتائج المرحلية (تحديث نسبة تقدم عملية التحميل في واجهة المستخدم).

وفي الدالة done يجب وضع الشيفرة المسؤولة عن معالجة النتيجة النهائية (إظهار رسالة تفيد بانتهاء عملية التحميل).

الدالة doInBackground

ضمن هذه الدالة يجب وضع الشيفرة المسؤولة عن عملية تحميل الملف، وللسهولة قمت بوضع شيفرة وهمية عبارة عنا حلقة for تتكرر 100 مرة، ومع كل تكرار أقوم بعملية تأخير زمني بسيط.

لاحظ أنني ضمن حلقة for استخدمت الدالة publish والتي تقوم بإرسال النتائج المرحلية إلى الدالة process لذلك قمت بإرسال المتحول i عبر تمريره كوسيط للدالة publish.

بعد إرسال انتائج المرحلية استخدمت الدالة sleep لأحداث تأخير زمني بمقدار 100 ميلي ثانية وبما أن الحلقة تتكرر 100 مرة فإن الزمن الكلي لتنفيذ الحلقة هو 10 ثوان وهو زمن كافي لملاحظة التغيرات على واجهة المستخدم.

الدالة process

بعد كل استدعاء للدالة publish يتم استدعاء الدالة process بشكل تلقائي.

ضمن هذه الدالة نقوم باستقبال النتائج المرحلية الواردة من الدالة doInBackground عبر الوسيط chunks وهو عبارة عن قائمة (List) تحوي جميع البيانات المرحلية التي تم استقبالها حتى الآن، على سبيل المثال في بداية التنفيذ تكون القائمة بالشكل التالي:

{1}

ثم

{1, 2}

{1, 2, 3}

وهكذا حتى انتهاء عملية التحميل، ولكننا في كل مرة نحتاج فقط لآخر قيمة كي نقوم بتحديث ال progessBar لذلك قمنا بجلب آخر قيمة عبر التعليمة

int progress = chunks.get(chunks.size() - 1);

وثم قمنا بإرسال هذه القيمة إلى واجهة المستخدم

الدالة done

يتم تنفيذ هذه الدالة بعد انتهاء الدالة doInBackground وتتيح لنا الحصول على النتيجة النهائية لتنفيذ الخيط وعمل التعديلات اللازمة على واجهة المستخدم.

قد يتسائل سائل لماذا نستخدم هذه الدالة في حين أنه يمكننا بكل بساطة في نهاية الدالة doInBackground الحصول على النتيجة النهائية؟

الجواب أن الدالة done لديها قدرة التعديل على واجهة المستخدم بدون أي مشاكل في حين أن الدالة doInBackground تعمل في خيط مستقل وتعديل واجهة المستخدم من خلالها قد يسبب الكثير من المشاكل.

كانت هذه مقدمة بسيطة يمكنك الاعتماد عليها كمدخل لاستخدام الصف SwingWorker وسأقوم في وقت لاحق بعمل دروس متقدمة في هذا المجال، أما الآن فيمكنك الرجوع إلى المراجع الموجودة في نهاية الصفحة للحصول على مزيد من المعلومات حول تعدد الخيوط بلغة الجافا.

رابط السلسة على GitHub

https://github.com/mutasemhajhasan/Java-Multi-Threading