الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

ال EDT في مكتبات ال GUI

بدأه مهدي حجازي في 9 سبتمبر 2010 · 6 رد · 968 مشاهدة · في JavaSE
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

السلام عليكم....

مقدمه

ما دفعني لكتابه الموضوع هو اثاره موضوع ال EDT في درس للأخ محمود حيث قام الاستاذ عصام مشكورا بتنبيهه الى ضروره تحديث ال Swing UI من داخل ال EDT. طبعا ذهبت وبحثت عن الموضوع فلم أجد مقالات تشرح الموضوع بشكل مفصل لذلك قررت الكتابه عنه نظرا لأهميه الموضوع.

في البدايه لقد تعاملت مع ثلاث مكتبات مختلفه تعتمد بشكل أساسي على وجود ال EDT اثنتان منها للهواتف النقاله وهما LWUIT و BlackBerry UI وأخرى للحواسيب الشخصيه وهي Swing لذلك فان ال EDT موجوده في مكتبات عديده وحتى أن بعضها يوجد في مكتبات ليست مكتوبه بلغه جافا ولكن جميعها تشترك في الأساسيات.

عند عمل اي برنامج يعتمد على مكتبه GUI تعتمد في داخلها على وجود EDT فاعلم أنه لحظه ظهور ال GUI أصبح عندك خيطان الأول هو ال Main Thread وهو ما نعلمه جميعا والثاني هو ال EDT وهو ما نتحدث عنه اليوم. هذا الخيط الثاني هو المسؤول عن جميع عمليات رسم المكونات الرسوميه واستقبال الأحداث وتوزيعها على المكونات التي يجب عليها استقبال هذه الأحداث.

أين المشكله

تظهر أكثر المشاكل عند المبرمجين أثناء انشائهم خيوطا جديده في برنامجهم لعمل بعض العمليات وعندها يقومون خطأ بتعديل مكونات ال Swing من داخل هذه الخيوط الجديده وهو ما يشكل كارثه حقيقيه على البرنامج. مشكله هذا النوع من الأخطاء أنه صعب التتبع فعند وقوعه سوف يصدر الخطأ من داخل مكتبه ال Swing وسوف يكون خطأ غريبا مثلا يمكن أن ينطلق خطأ ArrayIndexOutOfBoundsException وعندها ستبقى أياما طوالا تحك رأسك كيف يخرج هذا الخطأ من داخل مكتبه ال Swing. وتفكر كيف ظهر خطأ مثل هذا وربما الكود الذي كتبته لا يحتوي على Array من الأساس.

امكانيه حدوث الخطأ

على الحاسوب الشخصي الامكانيه ضعيفه جدا حيث أني حاولت متعمدا عطب ال EDT في Swing ولكني لم أنجح ربما لأن جهازي Quad Core لا أعلم ولكن تبقي الامكانيه موجوده ( من يستطيع اعطاب ال EDT بمثال, أرجو أن يضعه ), أما على الهواتف النقاله فأنا أضمن لك أنك اذا لم تحدث ال GUI من داخل ال EDT فان برنامجك سيتعطل بنسبه 80% وربما على الهواتف الضعيفه تصل النسبه ال 90%.

القاعده الذهبيه

قم بتحديث ال GUI من داخل ال EDT فقط

تشريح ال EDT

أنا شخصيا أفضل الكود على الكلام المطول، أراه أسهل للفهم. لذلك سأقوم بوضع كود تخيلي لل EDT حتى نوضح المشكله, طبعا الكود يوضح عمل ال EDT لكنه مبسط بشكل كبير لتوضيح طريقه عمله أما في الواقع فهو أعقد من هذا.

import java.awt.Graphics;
import java.util.Vector;
import javax.swing.JFrame;

public class EDT extends Thread {

    private boolean quit;
    private Vector tasks; 
    private Graphics g;
    private long frameRate = 50;
    private JFrame currentFrame;

    @Override
    public void run() {
        while(!quit) {
            long currentTime = System.currentTimeMillis();
            handleEvents();
            currentFrame.paint(g);
            processTasks();
            long timeTaken = System.currentTimeMillis() - currentTime;
            try {
                Thread.sleep( frameRate - timeTaken );

            } catch(Exception ex ) {

            }

        }

    }

    private void handleEvents() {
        // handle events
    }

    private void processTasks() {
        for( int i=0;i<tasks.size();i++ ) {
            Runnable task = (Runnable)tasks.elementAt( i );
            task.run();
            tasks.removeElementAt( i );

        }

    }

    public void invokeLater( Runnable task ) {
        tasks.addElement( task );

    }

}

كما هو واضح فان ال EDT هو عباره عن حلقه تكراريه غير منتهيه حتى ينتهي التطبيق. تنقسم الحلقه التكراريه الى ثلاث أقسام رئيسه:

1. الجزء المسؤول عن معالجه الأحداث وارسالها الى المكونات المطلوبه handleEvents.

2. الجزء المسؤول عن رسم المكونات.

3. الجزء السؤول عن معالجه المهام التي يجب ان تنفذ داخل ال EDT وتمت اضافتها من خيوط أخرى ( غالبا ).

توضيح المشكله

الأن تعالوا نتخيل معا وجود JList داخل ال JFrame ووجود خيط ثاني Thread B ينتظر وصول معلومات من الشبكه لتحديث هذه القائمه.

لنفترض ان ال EDT كان يقوم بعمل رسم لمكونات ال JFrame والتي يمكن تصور الكود التابع لها بهذه الطريقه:

    public void paint(Graphics g) {
        for( int i=0;i<components.size();i++ ) {
            JComponent component = (JComponent)components.elementAt( i );
            component.paint( g );
        }

    }

الان أحد هذه ال components هو ال JList كما ذكرنا سابقا، ولنفترض ان ال EDT وصل في التنفيذ الى رسم ال JList والتي يمكن تخيل كود رسمها كالتالي:

    public void paint(Graphics g ) {
        for( int i=0;i<getModel().getSize();i++ ) {
            Object ob = getModel().getElementAt( i );
            // draw ob
        }

    }

الان تخيل معي ان النظام قرر تبديل الخيط و تشغيل ال Thread B وتوقف ال EDT عند الحلقه 30 من أصل 50 حلقه تساوي عدد العتاصر الموجوده في ال JList. في ال Thread B كان ينفذ الكود التالي داخل الخيط وليس داخل ال EDT:

        list.removeAll();

        String[] options = new String[10];
        for (int i = 0; i < 10; i++) {
            options = "New Option " + (i + 1);
        }
        list.setListData( options );

بعد تنفيذ هذا الكود عاد ال EDT للعمل وقد كان توقف عند الحلقه 30 سوف يدخل الى الحلقه 31 وعند أول سطر سوف ينطلق خطأ ArrayIndexOutOfBoundsException نتيجه لأن ال list أصبح بها الان 10 عناصر فقط وليس 50.

الحل

تنفيذ الكود السابق داخل ال EDT عن طريق invokeLater كالتالي:

        invokeLater( new Runnable() {

            public void run() {
                list.removeAll();

                String[] options = new String[10];
                for (int i = 0; i < 10; i++) {
                    options = "New Option " + (i + 1);
                }
                list.setListData( options );
            }
        });

بالتالي عند تنفيذ ال Thread B سوف نقوم بانشاء كائن من ال Runnable، لاحظ أن هذا ليس خيطا جديدا وهو يحتوي على ما يجب تنفيذه داخل ال EDT والان لنعد لمثالنا نحن توقفنا عند الحلقه 30 من ال EDT ثم انتقلنا ال Thread B الذي بدوره أضاف كائن ال Runnable الى ال tasks التي يجب تنفيذها داخل ال EDT ثم نعود الى EDT الذي يكمل حلقاته التكراريه الى الحلقه 50 دون حدوث خطـأ لأننا لم ننفذ كود تحديث ال list. الان في الحلقه الثانيه من ال while loop الرئيسه في ال EDT سوف يقوم بمعالجه الأحداث والرسم ومن ثم معالجه المهام المضافه من الخيوط الأخرى وعندها سوف يتم تحديث ال list دون مشاكل وفي الحلقه التي تليها سوف يرسم ال list الجديده.

الخلاصه

كل الكلام السابق اختصاره، قم بتحديث ال GUI داخل ال EDT :)

ملاحظه

أنا لست محترف في ال Swing لذلك كتبت المقال بالاعتماد بشكل أكبر على مكتبات أخرى، لذلك أرجو تصجيحي ان أخطأت في مكان ما.

تحياتي

تم تعديل هذه المشاركة بواسطة مهدي حجازي في 9 سبتمبر 2010 في 01:34

8
#2

+1

جميل اخى مهدى

جزاك الله خيرا

فى تقدم دائم ان شاء الله

تم تعديل هذه المشاركة بواسطة Mahmoud Kelany في 9 سبتمبر 2010 في 01:42

Software Developer
Mahmoudkelany.com


 

#3

موضوع الSwing & Threads من المواضيع المهمة للمبرمجين .. فشكراً جزيلاً لهذا الطرح الرائع..

بالنسبة لتغيير الGui component من خارج الEDT فكما ذكرت قد يسبب ذلك مشكلة وهي في الغالب كما ذكرت ArrayIndexOutOfBoundsException ... نشاهد المثال التالي الذي يقوم بتعديل القيم في combobox ، في المثال زران الأول يغيره بطريقة صحيحة والأخر بطريقة خاطئة.. لاحظ المخرج يعد الضغط على الزر الخاطئ مرتان أو ثلاث:

post-51148-019704400 1283993289_thumb.pn

المثال من كتاب Core Java 2 (أحد أفضل كتب الجافا بالطبع) :

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

/**
   This program demonstrates that a thread that 
   runs in parallel with the event dispatch thread
   can cause errors in Swing components.
*/
public class SwingThreadTest
{  
   public static void main(String[] args)
   {  
      SwingThreadFrame frame = new SwingThreadFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setVisible(true);
   }
}

/**
   This frame has two buttons to fill a combo box from a
   separate thread. The "Good" button uses the event queue, 
   the "Bad" button modifies the combo box directly.
*/
class SwingThreadFrame extends JFrame
{  
   public SwingThreadFrame()
   {  
      setTitle("SwingThreadTest");

      final JComboBox combo = new JComboBox();
      combo.insertItemAt(new Integer(Integer.MAX_VALUE), 0);
      combo.setPrototypeDisplayValue(combo.getItemAt(0));
      combo.setSelectedIndex(0);

      JPanel panel = new JPanel();

      JButton goodButton = new JButton("Good");
      goodButton.addActionListener(new ActionListener()
         {  
            public void actionPerformed(ActionEvent event)
            {  
               new Thread(new GoodWorkerRunnable(combo)).start();
            }
         });
      panel.add(goodButton);
      JButton badButton = new JButton("Bad");
      badButton.addActionListener(new ActionListener()
         {  
            public void actionPerformed(ActionEvent event)
            {  
               new Thread(new BadWorkerRunnable(combo)).start();
            }
         });
      panel.add(badButton);
      panel.add(combo);
      add(panel);
      pack();
   }
}

/**
   This runnable modifies a combo box by randomly adding
   and removing numbers. This can result in errors because
   the combo box methods are not synchronized and both the worker
   thread and the event dispatch thread access the combo box.
*/  
class BadWorkerRunnable implements Runnable
{  
   public BadWorkerRunnable(JComboBox aCombo)
   {  
      combo = aCombo;
      generator = new Random();
   }

   public void run()
   {  
      try
      {
         while (true)
         {  
            combo.showPopup();
            int i = Math.abs(generator.nextInt());
            if (i % 2 == 0)
               combo.insertItemAt(new Integer(i), 0);
            else if (combo.getItemCount() > 0)
               combo.removeItemAt(i % combo.getItemCount());
            Thread.sleep(1);
         }
      }
      catch (InterruptedException e) {}
   }

   private JComboBox combo;
   private Random generator;
}

/**
   This runnable modifies a combo box by randomly adding
   and removing numbers. In order to ensure that the
   combo box is not corrupted, the editing operations are
   forwarded to the event dispatch thread.
*/  
class GoodWorkerRunnable implements Runnable
{  
   public GoodWorkerRunnable(JComboBox aCombo)
   {  
      combo = aCombo;
      generator = new Random();
   }

   public void run()
   {  
      try
      {
         while (true)
         {  
            EventQueue.invokeLater(new 
               Runnable()
               {  
                  public void run()
                  {  
                     combo.showPopup();
                     int i = Math.abs(generator.nextInt());
                     if (i % 2 == 0)
                        combo.insertItemAt(new Integer(i), 0);
                     else if (combo.getItemCount() > 0)
                        combo.removeItemAt(i % combo.getItemCount());
                  }
               });
            Thread.sleep(1); 
         }
      }
      catch (InterruptedException e) {} 
   }

   private JComboBox combo;
   private Random generator;
}

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

في المرفق مثال صغير كنت قد كتبته لأحد الاخوة أراد أن يستخدم اشارة التحميل التي تبدوا على الصورة التالية أثناء العمل (الكلاس InfiniteProgressPanel ) + عمل progress bar أيضا.. المثال يقوم بعرض جميع الملفات على القرص سي في جدول.. ويستخدم SwingWorker ..

post-51148-090577100 1283994290_thumb.pn

الفكرة أولاً بالوراثه من الكلاس SwingWorker (وبما أنه Generic يجب تمرير معاملين له) .. المعامل الأول سوف يكون هو الناتج من الدالة doInBackground (وهذه الدالة يجب أن تعيد تعريفها لأنها بمثابة الدالة run في الthread العادي) .. والمعامل الثاني سوف تكون القيم المتغيرة التي ستغير بها الواجهه gui التي تريد..

هذه الدالة doInBackground تستدعي بشكل دائم دالة تسمى publish والتي بدورها تستدعي الدالة process والتي ستقوم بالتعديل الذي تريده على gui (سوف تعيد تعريف الدالة process وهذه الدالة عندما يتم استدعائها سوف تعمل في الEDT) ..

الدالة process يجب أن تستقبل معامل List من نوع مطابق للمعامل الثاني الذي قم بكتابته عند وراثه SwingWorker (بمعنى القيم التي تريد أن تكتبها في الواجهه gui) .. حينها سوف تعمل process وتغير الواجهه بتلك القيم.. وهكذا يستمر العمل..

في النهايه عند الانتهاء سوف يتم استدعاء done (أيضا في الEDT) لاجراء التغييرات النهائية على الواجهه..

فقط :)

ملاحظة : الدوال التالية يمكن استدعائها من أي Thread عادي لأنها دوال أمنه:

JTextComponent.setText

JTextArea.insert

JTextArea.append

JTextArea.replaceRange

JComponent.repaint

JComponent.revalidate

بالتوفيق..

guiTest.zip

المرفقات
EDT.pngTestSwing.png

تم تعديل هذه المشاركة بواسطة Wajdy Essam في 9 سبتمبر 2010 في 04:11

2

http://informatic-ar.com منصة تعليمية عربية في علوم الحاسب والبرمجة

https://moalfat.com  للكتب الالكترونية والكورسات التعليمية

Everything we see now is just an engineering solution based on old science

#4

شكرا جزيلا اخي وجدي..المثال بالفعل نجح عندي وأوقف ال EDT كنت أجرب شيء مشابه لكن لم أتخيل أنه يتوجب تشغيل الخيط في حلقه لا منتهيه، بالنسبه ل SwingWorker فانا لا لم أعمل عليه من قبل, سوف اتطلع عليه شكرا لك..

تم تعديل هذه المشاركة بواسطة مهدي حجازي في 9 سبتمبر 2010 في 11:46

#5

أريد أن أوضح أن موضوع تشغيل الخيط في حلقة إنما هو لزيادة حدوث الأمر

بينما ممكن أن تحصل نفس المشكلة بدون أن يكون هناك تكرار

تحياتي

حزمة المحرك الإصدارة 0.8

أي أحد يجد أني ظلمته فليراسلني

وبإذن الله لو كان له حق سيأخذه

728x90.png

#6
علاء الصالحي كتب:

أريد أن أوضح أن موضوع تشغيل الخيط في حلقة إنما هو لزيادة حدوث الأمر

بينما ممكن أن تحصل نفس المشكلة بدون أن يكون هناك تكرار

تحياتي

اووووو

ايه الكلام الكبير دا

طب فهمنى طيب

لانى مش فاهم حاجه :nose_pick:

تم تعديل هذه المشاركة بواسطة Mahmoud Kelany في 10 سبتمبر 2010 في 09:04

Software Developer
Mahmoudkelany.com


 

#7

ولا كبير ولا حاجة يا محمود

في مثال الأخ وجدي لاحظ العامل السيء BadWorker

تعمد الأخ وجدي أن يضع تكرار يقوم كل مرة بعمليات تحديث على الواجهة حتى يضمن ظهور المشكلة التي يتكلم عنها الأخ مهدي

في الأغلب البرامج العادية تكون لا تحتوي على تكرار كالذي قام به الأخ وجدي وفي الأغلب نادراً ما تحصل المشكلة التي نتكلم عنها هنا

لكن المشكلة أن هناك برامج لو حصلت فيها هذه المشكلة وتعطل النظام قد نتسبب في كوارث طبيعية (مثل برامج المفاعلات النووية) أو كوارث اقتصادية على مستوى الشركات أو المؤسسات (والأمثلة عليها لا حصر لها)

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

تحياتي

حزمة المحرك الإصدارة 0.8

أي أحد يجد أني ظلمته فليراسلني

وبإذن الله لو كان له حق سيأخذه

728x90.png

مواضيع مشابهة