现在基于Eclipse的应用越来越多,很多桌面应用都是用Eclipse开发的。Eclipse提供了一套SWT/JFACE
的控件库,使得人们开发界面应用极大的方便。但是,SWT/JFACE的控件库毕竟有限,在应用开发是我们不可避免地要自己开发一些自定义的控件。本文通
过开发一个颜色列表控件的实例介绍了Eclipse自定义控件开发中所要用到的技术。
目标读者必须熟悉Java开发,并且有一定的Eclipse开发经验。
在Eclipse网站上有一篇相关的文章"Creating Your Own Widgets using
SWT",该文介绍了开发自己控件的很多基本概念、方法,并且通过实例进行了介绍,非常好。但是其所用的实例比较简单,还有很多控件开发中所要涉及到的内
容,例如键盘、鼠标事件的处理,滚动条、焦点的处理等等没有提及。本文通过开发一个自定义的颜色列表控件的实例,全面地介绍了自定义控件所涉及的技术。同
时,读者也可以对该实例进行扩展,实现自己的列表控件。
SWT中提供的标准列表控件非常简单,只能提供字符串的选择。我们经常需要提供一些图形列表供用户选择,这就需要自己开发自定义的列表控件。颜色选择列表是我们常用的一种图形列表,我们就以此为例进行介绍。以下是我们将要开发的颜色列表。
我们在开发自定义控件时主要考虑以下问题:
1、	自定义控件的绘制:通常我们需要自己对控件的形状或图案进行绘制;
2、	控件对键盘事件的响应:当焦点进入控件,用户进行键盘操作,通过键盘对控件进行控制时,我们需要让控件对用户的操作进行响应。例如在列表中,用户会通过上下箭头改变列表的选择项;
3、	控件对鼠标事件的响应:当用户用鼠标选中控件,进行操作时,控件必须作出相应的反应;
4、	控件对焦点事件的响应:当界面焦点进入或移出控件,通常我们需要将控件绘制成得到或失去焦点的形状。例如,当焦点进入列表时,一般被选中的列表项会有虚框表示选中。
5、	响应TAB键:对于一个可操纵的控件,用户可以用TAB键将焦点移入或移出。
6、	响应滚动条事件:当控件有滚动条时,我们需要响应用户对滚动条的操作,完成对控件的绘制工作。
7、	提供事件监听机制:程序员使用你的控件时通常需要监听控件中发生的一些事件,这样当事件发生时,他们能够进行相应处理。
8、	提供辅助功能(Accessibility):辅助功能是方便残障人士使用时必须的,标准控件都会提供相应的支持,我们自定义的控件也不例外。
9、	提供功能接口方便程序员访问:通常为方便程序员使用时获取控件中的信息或进行设置,我们需要提供一些接口。
首先我们要开发的列表控件是一个基本控件,所以我们选择Canvas作为我们开发的基类。
    
        
            
            		public class ColorList extends Canvas { 
            Vector colors = new Vector();  // 用于保存我们颜色控件中的颜色值 
            Vector colorNames = new Vector(); // 用于保存颜色控件中的颜色名字 
             
            int rowSel = -1; // 用于保存当前选中的行号 
            int oldRowSel = -1; // 用于保存上一次选中的行号 
             
            int maxX, maxY;  // 用于保存列表的宽度和高度 
            int lineHeight; // 用于设置行高 
             
            int cx = 0;  // 滚动条滚动后,控件的图形相对于控件可见区域左上角的x坐标 
            int cy = 0;  // 滚动条滚动后,控件的图形相对于控件可见区域左上角的y坐标 
            } 
            
             | 
        
    
控件开发最重要的就是控件的绘制了。控件的绘制可以通过添加PaintListener,在它的paintControl方法中进行。
    
        
            
            		addPaintListener(new PaintListener() { 
            public void paintControl(PaintEvent e) { 
            GC gc = e.gc; 
            Point size = getSize(); 
            int beginx = e.x; 
            int beginy = (e.y / lineHeight) * lineHeight; 
            int beginLine = (e.y - cy) / lineHeight; 
            int endLine = beginLine + e.height / lineHeight + 1; 
            if (endLine > getItemCount()) 
            endLine = getItemCount(); 
            for (int i = beginLine; i < endLine; i++) { 
            boolean selected = false; 
            if (i == rowSel) 
            selected = true; 
            onPaint(gc, i, cx, beginy + (i - beginLine) * lineHeight, 
            selected); 
            } 
            } 
            });  
            
             | 
        
    
这里要注意的是从PaintEvent中获取的x,y,height,width是需要重绘的区域,x,y是以控件的左上角为原点的坐标。在我们的
程序中,为了性能起见,我们先根据需要重绘的区域计算出需要重绘的行数,只重绘相应的行,而不是将整个控件重绘。我们程序中用到的onPaint用于绘制
一行。
接下来,我们要让我们的控件响应键盘上下键对列表项进行选择。我们已对向上键的处理为例,首先当用户按了向上键时,我们需要改变选择,并且重绘旧的和新的选择项。如果选择项已经到了列表的顶部,我们还需要同时滚动滚动条。
    
        
            
            		addListener(SWT.KeyDown, new Listener() { 
            public void handleEvent(Event event) { 
            switch (event.keyCode) { 
            case SWT.ARROW_UP: // 处理向上键 
            if (rowSel != 0) { 
            oldRowSel = rowSel; 
            rowSel--; 
            if (oldRowSel != rowSel) { //发送消息让控件重绘 
            ((Canvas) event.widget).redraw(cx, (rowSel + cy 
            / lineHeight) 
            * lineHeight, maxX, lineHeight*2, false); 
            } 
            if (rowSel < -cy / lineHeight) { //如果需要,滚动滚动条 
            ScrollBar bar = ((Canvas) event.widget) 
            .getVerticalBar(); 
            bar.setSelection(bar.getSelection() - lineHeight); 
            scrollVertical(bar); 
            } 
            selectionChanged(); // 发送selectionChanged事件 
            } 
            break; 
            case SWT.ARROW_DOWN: // down arror key 
            … 
            break; 
            } 
            } 
            }); 
            
             | 
        
    
接下来,我们要让我们的控件响应鼠标对列表项进行选择。首先我们要计算出鼠标选中的行号,注意MouseEvent中的y值只是相对于控件左上角的坐标,我们需要加上滚动出了控件的部分。
    
        
            
            		addMouseListener(new MouseListener() { 
            public void mouseDoubleClick(MouseEvent e) { 
            } 
            public void mouseDown(MouseEvent e) { 
            int row = (e.y - cy) / lineHeight; //计算选中的行 
            if (row >= 0) { 
            oldRowSel = rowSel; 
            rowSel = row; 
            } 
            if (oldRowSel != rowSel) { // 重画旧的和新的选择项 
            ((Canvas) e.getSource()).redraw(cx, (e.y / lineHeight) 
            * lineHeight, maxX, lineHeight, false); 
            ((Canvas) e.getSource()).redraw(cx, (oldRowSel + cy 
            / lineHeight) 
            * lineHeight, maxX, lineHeight, false); 
            } 
            selectionChanged(); 
            } 
            public void mouseUp(MouseEvent e) { 
            } 
            }); 
            
             | 
        
    
当我们的控件获得焦点时,选中的列表项需要有虚框表示控件得到焦点。当获得或失去焦点是,我们这里只需要简单的通知选中的项重画。
    
        
            
            		addFocusListener(new FocusListener() { 
            public void focusGained(FocusEvent e) { 
            ((Canvas) e.getSource()).redraw(cx, rowSel * lineHeight, maxX, 
            lineHeight, true); 
            } 
            public void focusLost(FocusEvent e) { 
            ((Canvas) e.getSource()).redraw(cx, rowSel * lineHeight, maxX, 
            lineHeight, true); 
            } 
            }); 
            
             | 
        
    
我们在绘制每一个列表项时可以加入判断当前控件是否得到焦点,如果控件得到了焦点,我们就在选中的项目上画一个虚框。下面是我们绘制一个列表项的代码,注意在代码的最后绘制焦点的虚框。
    
        
            
            	void onPaint(GC gc, int row, int beginx, int beginy, boolean isSelected) { 
            Color initColor = gc.getBackground(); 
            Color initForeColor = gc.getForeground(); 
            if (isSelected) { 
            gc.setBackground(Display.getCurrent().getSystemColor( 
            SWT.COLOR_LIST_SELECTION)); 
            gc.fillRectangle(beginx, beginy, maxX, lineHeight); 
            gc.setForeground(Display.getCurrent().getSystemColor( 
            SWT.COLOR_LIST_SELECTION_TEXT)); 
            } else { 
            gc.setBackground(initColor); 
            } 
            gc.drawString((String) colorNames.get(row), beginx + 24, beginy); 
            Color color = Display.getCurrent().getSystemColor( 
            ((Integer) colors.get(row)).intValue()); 
            gc.setBackground(color); 
            gc.fillRectangle(beginx + 2, beginy + 2, 20, lineHeight - 4); 
            gc.setBackground(initColor); 
            gc.setForeground(initForeColor); 
            if (isFocusControl() && isSelected) 
            gc.drawFocus(cx, beginy, maxX, lineHeight); 
            } 
            
             | 
        
    
作为一个可操作的控件,TAB键的支持也是很重要的。由于我们的控件是从Canvas继承过来的,不支持TAB键。下面的代码使我们的控件有TAB键的支持:
    
        
            
            addTraverseListener(new TraverseListener() { 
            public void keyTraversed(TraverseEvent e) { 
            if (e.detail == SWT.TRAVERSE_TAB_NEXT 
            || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) { 
            e.doit = true; 
            } 
            }; 
            }); 
            
             | 
        
    
很多时候,我们需要有滚动条的支持。对于滚动条,我们只要在上面加上selectionListener,处理它的widgetSelected事件就可以。
    
        
            
            bar = getVerticalBar(); 
            if (bar != null) { 
            bar.addSelectionListener(new SelectionAdapter() { 
            public void widgetSelected(SelectionEvent event) { 
            scrollVertical((ScrollBar) event.widget); 
            } 
            }); 
            } 
             
             | 
        
    
下面是函数scrollVertical的代码。一旦用户对滚动条操作,我们就可以计算出要滚动的区域,然后调用scroll函数。对函数scroll函数的调用会导致相应区域的重绘。
    
        
            
            void scrollVertical(ScrollBar scrollBar) { 
            Rectangle bounds = getClientArea(); 
            int y = -scrollBar.getSelection(); 
            if (y + maxY < bounds.height) { 
            y = bounds.height - maxY; 
            } 
            if( y%lineHeight !=0 ) 
            y = y - y % lineHeight - lineHeight; 
            scroll(cx, y, cx, cy, maxX, maxY, false); 
            cy = y; 
            } 
            
             | 
        
    
现在我们的程序已经基本成形了,我们来进一步完善它。由于我们开发的控件是提供给程序员的,我们需要提供接口,让外部知道控件中发生的事件。其中最
重要的是列表项的选中事件。我们需要提供接口让程序员能够添加事件监控器(listener)来监控发生的事件,并且一旦发生事件,我们需要通知监控器。
首先,我们添加一个成员来保存添加的事件监控器:
    
        
            
            Vector selectionListeners = new Vector(); 
             
             | 
        
    
我们再增加一个函数addSelectionListener,让程序员可以添加监控器
    
        
            
            public void addSelectionListener(SelectionListener listener) { 
            selectionListeners.addElement(listener); 
            } 
            
             | 
        
    
在我们前面的代码中,我们注意到每次选择项改变,我们都会调用selectionChanged函数。下面是selectionChanged函数
代码。这里,我们会生成一个SelectionEvent事件,并且逐个调用事件监控器的widgetSelected方法。这样别人就可以监听到我们的
事件了。
    
        
            
            public void selectionChanged() { 
            Event event = new Event(); 
            event.widget = this; 
            SelectionEvent e = new SelectionEvent(event); 
            for (int i = 0; i < selectionListeners.size(); i++) { 
            SelectionListener listener = (SelectionListener) selectionListeners.elementAt(i); 
            listener.widgetSelected(e); 
            } 
            } 
            
             | 
        
    
现在辅助功能(Accessibility)也日益成为软件重要的部分,它是的残疾人也能够方便的使用我们的软件。美国已经立法,不符合
Accessibility规范的软件不能够在政府部门销售。我们开发的控件也需要支持Accessibility.下面的代码使我们的控件有
Accessibility支持。其中最重要的是getRole和getValue函数。我们的控件是从Canvas继承,我们在getRole函数中返
回ACC.ROLE_LIST,这样我们的控件才能让屏幕阅读软件将我们的控件作为列表控件对待。
Accessible accessible = getAccessible();
accessible.addAccessibleControlListener(new AccessibleControlAdapter() {
public void getRole(AccessibleControlEvent e) {
int role = 0;
int childID = e.childID;
if (childID == ACC.CHILDID_SELF) {
role = ACC.ROLE_LIST;
} else if (childID >= 0 && childID < colors.size()) {
role = ACC.ROLE_LISTITEM;
}
e.detail = role;
}
public void getValue(AccessibleControlEvent e){
int childID = e.childID;
if (childID == ACC.CHILDID_SELF) {
e.result = getText();
} else if (childID >= 0 && childID < colors.size()) {
e.result = (String)colorNames.get(childID);
}
}
public void getChildAtPoint(AccessibleControlEvent e) {
Point testPoint = toControl(new Point(e.x, e.y));
int childID = ACC.CHILDID_NONE;
childID = (testPoint.y - cy)/lineHeight;
if (childID == ACC.CHILDID_NONE) {
Rectangle location = getBounds();
location.height = location.height - getClientArea().height;
if (location.contains(testPoint)) {
childID = ACC.CHILDID_SELF;
}
}
e.childID = childID;
}
public void getLocation(AccessibleControlEvent e) {
Rectangle location = null;
int childID = e.childID;
if (childID == ACC.CHILDID_SELF) {
location = getBounds();
}
if (childID >= 0 && childID < colors.size()) {
location = new Rectangle(cx,childID*lineHeight+cy,maxX,lineHeight);
}
if (location != null) {
Point pt = toDisplay(new Point(location.x, location.y));
e.x = pt.x;
e.y = pt.y;
e.width = location.width;
e.height = location.height;
}
}
public void getChildCount(AccessibleControlEvent e) {
e.detail = colors.size();
}
public void getState(AccessibleControlEvent e) {
int state = 0;
int childID = e.childID;
if (childID == ACC.CHILDID_SELF) {
state = ACC.STATE_NORMAL;
} else if (childID >= 0 && childID < colors.size()) {
state = ACC.STATE_SELECTABLE;
if (isFocusControl()) {
state |= ACC.STATE_FOCUSABLE;
}
if (rowSel == childID) {
state |= ACC.STATE_SELECTED;
if (isFocusControl()) {
state |= ACC.STATE_FOCUSED;
}
}
}
e.detail = state;
}
});
最后,我们需要提供一些方法方便程序员使用我们的控件。
    
        
            
            	public void setSelection(int index) { 
            if (index >= getItemCount() || index < 0) 
            return; 
            oldRowSel = rowSel; 
            rowSel = index; 
            selectionChanged(); 
            } 
            public int getSelectionIndex() { 
            return rowSel; 
            } 
            public int getItemHeight() { 
            return lineHeight; 
            } 
            public void setItemHeight(int height) { 
            lineHeight = height; 
            } 
            public int getItemCount() { 
            return colors.size(); 
            } 
            public void add(int colorIndex, String colorName) { 
            colorNames.add(colorName); 
            colors.add(new Integer(colorIndex)); 
            } 
            
             | 
        
    
 我们开发的控件的使用也是非常简单的。
    
        
            
            CustomList customlist = new CustomList( parent, SWT.V_SCROLL | SWT.H_SCROLL ); 
            customlist.add(SWT.COLOR_BLACK,"BLACK"); 
            customlist.add(SWT.COLOR_BLUE,"BLUE"); 
            customlist.setSelection(1); 
            customlist.setSize(400,400); 
            customlist.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_LIST_BACKGROUND)); 
             
             | 
        
    
以上我们介绍了如何开发一个简单的自定义控件所需要涉及的技术。这里我们只以一个简单的颜色控件为例,但是一旦我们掌握了方法,我们很容易就可以开发出各种不同的漂亮控件。
整个程序完整的代码清参考:ColorList.java。