سنتناول في هذا الموضوع لمحة بسيطة عن استخدام الJList وعمل Extending لها لكي تتناسب مع حاجة المستخدم ،، والJList وكغيرها من الSwing Component مبنية على مفهوم الModel-View-Controller وبالتالي اذا كنت تريد تغيير الObject الموجود داخل الComponent سوف تتعامل مع الModel ، واذا أردت تغيير طريقة العرض بالشكل الذي تريدها سوف تتعامل مع الView ، وهذه مرونة ممتازة في
تصميم الComponent .
* Inserting and Removing Values
* Rendering Values
* Custom List
نبدأ بالحالة الأولى والأسهل وهي الحالة الإفتراضية للJList وسنقوم بعمل JList يتكون من العديد من النصوص String ، كما يبين ذلك في الصورة من المثال التالي:
سنقوم بعمل الJList مكونة من العديد من النصوص :
String mySite[] = {"arabteam2000.com" , "sudancs.com", "stackoverflow.com" , "google.com" }; JList list = new JList( mySite );
وبالشكل الإفتراضي الJList ليست Scrollable لذلك يجب أن تضيف الlist الى JScrollPane وتضيف الأخير للPanel :
JScrollPane pane = new JScrollPane(list); panel.add(pane );
هذه الlist أيضا بالوضع الإفتراضي تتكون من 8 خانات يمكنك تغييرها باستخدام الدالة setVisibleRowCount :
list.setVisibleRowCount(4);
الإتجاه Orientation في هذه الlist في الوضع الإفتراضي هو الvertical ويتم تحديد ذلك من خلال الدالة setLayoutOrientation ، ومن القيم لهذه الدالة:
JList.VERTICAL (الإفتراضي وكل الitems ستكون عموديا أسفل بعضها )
JList.VERTICAL_WRAP وهنا تكون الItems أيضا عموديا ولكن يتم الإنتقال لعمود أخر اذا كان عدد الitems أكبر من المسموح به في العمود الواحد)
JList.HORIZONTAL_WRAP نفس السابق ولكنه أفقيا ..
طريقة التحديد Selection mode في الوضع الإفتراضي هي Multiple وتسمح لك باختيار أكثر من item سواء بالضغط على الitems مع الزر CTRL لتحديد عدة items متفرقة أو SHIFT لتحديد عدة items متجاورة . وتستطيع تغيير الselection mode باستخدام الدالة setSelectionMode كما يلي :
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // select one item at a time list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); // select one item or one range of items
بالنسبة للتعامل مع الأحداث في الJList فسوف يحدث الevent بمجرد تحديد أي من الitem في الlist ، وهنا يجب أن يكون لديك listener يقوم بتطبيق implements الListSelectionListener ويعرف الدالة valueChanged
public void valueChanged(ListSelectionEvent evt)
في الحقيقة عند الضغط على الitem سوف يحدث حدثين واحد عن الضغط بالفأرة mouse down والأخر عند رفع الفأرة mouse up ، لذلك في الغالب سوف نتعامل مع حدث واحد ، ويمكن معرفة نوع الحدث الحالى عن طريق الدالة :
event.isAdjusting()
في حال أرجعت الدالة true هذا يعني أنه الأن تم الضغط بالفأرة mouse down ، والا فإنه mouse up .. عادة سنتعامل مع الMouse up ولك الخيار فيما تريد.
بعد أن تم تنبيهك بالحدث ، يجب أن تعرف ما هي الitems التي تم اختيارها ، ويمكنك ذلك من خلال الدالة getSelectedValues وهي ترجع مصفوفه من الكائنات Array of Objects ، يمكنك عمل cast to string لأي خانه منها ، كما يلي :
Object[] values = list.getSelectedValues(); for (Object value : values) do something with (String) value;
في حال كانت الlist لا تسمح بالإختيار المتعدد ، فيمكنك الحصول على الitem الذي تم الضغط عليه من خلال:
String value = (String) list.getSelectedValue();
هذا هو المثال :
import java.awt.EventQueue; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JList ; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionListener; import javax.swing.event.ListSelectionEvent; public class SimpleJList { public static void main (String[] args) { EventQueue.invokeLater( new Runnable() { public void run () { SimpleListFrame app = new SimpleListFrame(); app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); app.setVisible( true ); } }); } } class SimpleListFrame extends JFrame { public SimpleListFrame () { // set application title setTitle("Simple JList Test"); // set Application size setSize(300, 150); // set application location setLocationByPlatform(true); // fill JList list = new JList( mySite ); list.setVisibleRowCount(5); list.addListSelectionListener( new ListSelectionListener() { public void valueChanged ( ListSelectionEvent event ) { Object[] value = list.getSelectedValues(); StringBuilder text = new StringBuilder(prefix); for (Object obj : value ) { String word = (String) obj; text.append( word ); text.append(" "); } lbl.setText ( text.toString() ); } }); lbl = new JLabel("no Selected site"); JScrollPane pane = new JScrollPane(list); JPanel panel = new JPanel(); panel.add(pane ); add( panel ,BorderLayout.NORTH ); add( lbl ,BorderLayout.SOUTH ); } private String mySite[] = {"arabteam2000.com" , "sudancs.com", "stackoverflow.com" , "google.com" }; private String prefix = " Im Visit : "; private JList list; private JLabel lbl; }
List Internals :
كما ذكرنا في بداية الموضوع أنه الJList مصممه بالMVC ، وهو يفصل الواجهه (التي تعرض Rendered بشكل ما ) عن المحتوى (Underlying Objects) . وهنا سنجد أن الكلاس JList يمثل الواجهه View (أي هو المسؤول عن العرض) ، أما المحتوي فيحصل عليها من خلال كائن يطبق implements الواجهه interface التاليه :
public interface ListModel { int getSize(); Object getElementAt(int i); void addListDataListener(ListDataListener l); void removeListDataListener(ListDataListener l); }
من خلال هذه الواجهه يستطيع الJList الحصول على عدد الitems والحصول على الitems ، أيضا يضع listener على أي تغيير (اضافة وحذف) في المحتوي وبالتالي يستطيع اعادة الرسم مرة أخرى بعد عمل التحديث المطلوب .
وبما أن JList فقط يحصل على الitems ولا يهمه كيف يخزن ، فيمكن أن تقوم بعمل دالة getElementAt لكنها تقوم بتوليد نصوص في وقت التشغيل.
هناك interface أخر مفيد وهو AbstractListModel وهو مشابه لListModel ولكنه يعرف الaddListDataListener و removeListDataListener وبالتالي سوف يسهل علينا كثيرا ( فقط سنقوم بتعريف getSize و getElementAt ) .. :
class MyNewModel extends AbstractListModel { public int getSize() { /* anythings */; } public Object getElementAt(int n) { // Anythings . . . } . . . }
Inserting and Removing Values
لعمل list قابلة للتعديلات (الإضافة والحذف ) فهذا يتطلب اضافة بسيطة وذلك لأن الJList والListModel لا يحتويان على أي دوال للإضافة والحذف ، وهنا سوف نستخدم الكلاس DefaultListModel ونقوم بادخال العناصر فيها باستخدام addElement .. وأخيرا ندخل الكائن من الDefaultListModel في الJList :
DefaultListModel model = new DefaultListModel(); model.addElement("site1"); model.addElement("site 2"); // other adding ...... JList list = new JList ( model );
الأن يمكنك اجراء التعديلات من خلال الmodel وهو سيتولي اخبار الJList بعملية التغيير وحينها سيقوم الJList باعادة الرسم :
model.removeElement("site 1"); model.addElement("site 3");
Rendering Values
الى هنا فإن أي list قمنا بعمله يتكون من Strings فقط ، وفي الحقيقة يمكن انشاء list من أي Objects كان ، ولكن لكي يتم عرضه بشكل صحيح ، يجب أن نضع طريقة لعرض المحتوي List Cell Rendering في الlist وذلك عن طريق أي كائن يطبق الواجهه التالية :
interface ListCellRenderer { Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus); }
هذه الدالة getListCellRendererComponent يتم استدعائها لعرض كل items وهي ترجع الitem لكي يتم وضعه في الlist . وأحد الطرق لذلك هو وراثه أي من الComponent كما يلي :
class MyCellRenderer extends JComponent implements ListCellRenderer { public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { // stash away information that is needed for painting and size measurement return this; } public void paintComponent(Graphics g) { // paint code goes here } public Dimension getPreferredSize() { // size measurement code goes here } // instance fields }
المثال التالي سوف نقوم بعمل list مكونه من ImageIcon كما في الشكل التالي :
والRendering يتم عن طريق الوراثه من الJLabel وبالتالي لن نحتاج الى الpaintComponent والgetPreferredSize فهو يقوم بها بالطريقة التي نريد .
import java.awt.Color; import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.Component; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JLabel; import javax.swing.ImageIcon; import javax.swing.Icon; import javax.swing.SwingConstants; import javax.swing.JList ; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionListener; import javax.swing.event.ListSelectionEvent; import javax.swing.ListCellRenderer; import javax.swing.JScrollPane; public class ListTest { public static void main (String[] args) { EventQueue.invokeLater( new Runnable() { public void run (){ FrameList app = new FrameList(); app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); app.setVisible(true); } }); } } class FrameList extends JFrame { public FrameList () { setTitle("List Test"); setSize(300,300); Icon[] icon = { new ImageIcon( ("gpg.png") , "Encryption" ), new ImageIcon( ("folder_outbox.png") , "Send Message" ), new ImageIcon( ("image.png") , "Hide in Photo") }; list = new JList( icon ); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION ); list.setCellRenderer( new MyCellRenderer() ); list.setVisibleRowCount(3); JScrollPane pane = new JScrollPane(list); JPanel panel = new JPanel(); panel.add( pane ); add ( panel , BorderLayout.WEST ); list.addListSelectionListener ( new ListSelectionListener() { public void valueChanged ( ListSelectionEvent event ) { if ( event.getValueIsAdjusting() ) return; int s = (int) list.getSelectedIndex(); System.out.println(s); } }); } private JList list; } class MyCellRenderer extends JLabel implements ListCellRenderer { public MyCellRenderer () { setOpaque(true); } public Component getListCellRendererComponent (JList list, Object value , int index, boolean isSelected , boolean cellHasFocus ) { ImageIcon icon = (ImageIcon) value; setText( icon.getDescription() ); setIcon( icon ); setVerticalTextPosition(SwingConstants.BOTTOM); setHorizontalTextPosition(SwingConstants.CENTER ); if ( isSelected ) { setBackground( Color.GRAY ); setForeground( Color.WHITE ); } else { setBackground( Color.WHITE ); setForeground( Color.BLACK ); } return this; } }
Custom JLists
لو فرضنا أن لديك list من 1000 خانة ، فعملية البحث عن العنصر المطلوب أمر مرهق وفي هذه الحالة يعتبر التصميم bad design ، لكن ماذا لو قمنا بوضع text field يكتب فيه المستخدم الحرف الأول من الخانه وسيتم تعديل الlist وفقط تظهر الخانات التي تبدأ بهذا الحرف الأول .. كما توضح ذلك الصورة التالية عند الضغط على الحرف S سيظهر كل الأسماء التي تبدأ بS .
في هذه الحالة سوف يكون لدينا Model ولكن طريقتين للعرض ، الأولى عرض جميع الخانات (تكون النصوص موجودة في مصفوفه )والثانية عرض جميع الخانات التي تبدأ بالحرف المعين (وأيضا سيتم تخزينها في مصفوفه أخرى ) ،،
سنقوم الأن بتصميم الكلاس FilteredList وسيكون بداخلها كلاسين الأول FilterModel للعرض والثاني FilterField للكتابة .
نبدأ الأن بالوراثة من الJList ونضيف الأشياء المطلوبة :
class FilteredList extends JList { public FilteredList () { super(); setModel( new FilteredModel() ); filteredField = new FilteredField(20); } public void setMode ( ListModel model ) { if ( ! ( model instanceof FilteredModel ) ) throw ( new IllegalArgumentException() ); super.setModel ( model ); } public void addItem ( Object o ) { ((FilteredModel)getModel()).addElement(o); } public JTextField getFilteredField () { return filteredField; } private FilteredField filteredField; }
هذه الlist الجديدة ، وفي دالة البناء وضعنا الModel بأنه هو FilteredModel وهو كلاس داخلي يحتفظ بالنصوص جميعها والنصوص التي تبدأ بالحرف المعين وبالطبع هو يرث AbstractListModel كما سنشاهد بعد قليل ، ويحتوي الlist الجديد على دالة setMode تتأكد من النوع أولا .. وهناك دالة لإدخال العناصر في الmodel وهناك دالة لأرجاع الtextfield حيث أننا نريد أن نضعه على الframe .
أما الكلاس FilteredModel فهو يرث AbstractListModel ويعيد تعريف الدالة getElementAt و getSize ، ويحتوي كما ذكرنا على مصفوفتين الأولى لجميع النصوص وهي التي سوف تضاف للmodel ، والأخرى للنصوص التي تحتوي على الحرف المعين ..
أما الكلاس FilterField فهو يرث الJTextField ووظيفته باستدعاء الدالة refilter متى تغير أي شيء فيه .
الكود هنا :
import java.awt.*; import javax.swing.*; import javax.swing.event.*; import java.util.*; public class CustomeList { public static void main (String[] args){ EventQueue.invokeLater ( new Runnable() { public void run () { CustomeListFrame app = new CustomeListFrame(); app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); app.setVisible( true ); } }); } } class CustomeListFrame extends JFrame { public CustomeListFrame () { setTitle("Filter List"); setSize(300,300); fList = new FilteredList(); for (int i=0; i<names.length; i++ ) fList.addItem ( names[i] ); JScrollPane pane = new JScrollPane (fList, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); add (pane, BorderLayout.CENTER); add (fList.getFilteredField(),BorderLayout.NORTH); } private String[] names = {"Ahmed","Abdu","Ali","Sami","Salem","Sameer","Salman","Khalid","Bassem" }; private FilteredList fList; } class FilteredList extends JList { public FilteredList () { super(); setModel( new FilteredModel() ); filteredField = new FilteredField(20); } public void setMode ( ListModel model ) { if ( ! ( model instanceof FilteredModel ) ) throw ( new IllegalArgumentException() ); super.setModel ( model ); } public void addItem ( Object o ) { ((FilteredModel)getModel()).addElement(o); } public JTextField getFilteredField () { return filteredField; } private class FilteredModel extends AbstractListModel { public FilteredModel () { super(); items = new ArrayList<String>(); fItems = new ArrayList<String>(); } public Object getElementAt (int index ){ if ( index < fItems.size() ) return fItems.get(index); else return null; } public int getSize () { return fItems.size(); } public void addElement (Object o ) { items.add((String)o); refilter(); } private void refilter () { fItems.clear(); String term = getFilteredField().getText(); for (int i=0; i<items.size(); i++) if ( items.get(i).toString().indexOf(term , 0 ) != -1 ) fItems.add ( items.get(i ) ); fireContentsChanged (this, 0, getSize()); } private ArrayList<String> items; private ArrayList<String> fItems; } private class FilteredField extends JTextField implements DocumentListener { public FilteredField (int width ) { super(width ); getDocument().addDocumentListener(this); } public void changedUpdate (DocumentEvent e ){ ((FilteredModel)getModel()).refilter(); } public void insertUpdate ( DocumentEvent e) { ((FilteredModel)getModel()).refilter(); } public void removeUpdate (DocumentEvent e ) { ((FilteredModel)getModel()).refilter(); } } private FilteredField filteredField; }
أمل أن يكون الموضوع المفيد ،، ويمكن تخصيص الlist بالشكل الذي تريد بعد الأن ..
وسبب كتابتي للموضوع هو أني أستخدمت list بها أيقونات في مشروع بسيط ، وأردت مشاركة الفكرة ، واجهه المشروع Frame تقسم الى قسمين Panel على اليمين وPanel على اليسار (باستخدام JSplitterPane ) و تكون هناك JList بها أيقونات على الجهه اليسري من الJSplitterPane، وكل أيقونة تمثل حدث معين مثلا Connect أو Send أو Encryption .. وعند الضغط على احد الأيقونات فسوف يظهر البانل الخاص به على الجهه اليمني من الJSplitterPane. سوف يظهر أي بانل في الجهه اليمني وبنفس الحجم ، والسبب هو استخدام الCardLayout في تقسيم الجهه اليمني من الJSplitterPane .
References for more :
Swing Hacks , By Chris Adamson, Joshua Marinacci
Core Java™ Volume II–Advanced Features
Happy Swing Hacking .
مقال جميل يا وجدي، وكم يسرني رؤية محتوى عربي عن الجافا بهذا المستوى. لدي ملاحظتين حول المقال:
1- ابتداءً من جافا 7، كلاسات الـ JList أصبحت generic، وذلك يشمل AbstractListModel و ListCellRenderer.
2- عن نفسي لا أحبذ استخدام setSize بشكل صريح، ولكن أفضّل استخدام pack اللتي تعمتد على الـ preferred size الخاص بكل component.
1- ممتاز أول مرة أعلم عن ذلك في جافا 7 سوف اقرأ عن الجديد بخصوص GUI API فيها.
2- نعم pack افضل بالتأكيد وتعطيك حجم مناسب فوق الComponents خاصتك، لكنها تجدي في الأمثله السريعه 🙂
سعدت بمرورك وملاحظاتك