SWT一個(gè)所謂的優(yōu)點(diǎn)是它的本地化外觀,因?yàn)樗峭ㄟ^(guò)JNI調(diào)用操作系統(tǒng)的組件,從而可以保證外觀上適合大多數(shù)用戶的需求,但是一些IM類軟件商往往希望它們的產(chǎn)品有著一套獨(dú)特的外觀,這對(duì)SWT這種原生組件來(lái)說(shuō)就有些力不從心了,嚴(yán)格來(lái)說(shuō)如果你的用戶對(duì)外觀要求比較苛刻,那么Swing是首選,因?yàn)長(zhǎng)&F機(jī)制可以確保你做到這一點(diǎn),另外Swing還有著許多SWT不具備的優(yōu)點(diǎn),例如半透明組件、渲染等,但是少數(shù)的這些特性用SWT還是可以模擬的,本文就向大家介紹如何通過(guò)自定義組件實(shí)現(xiàn)MSN風(fēng)格的下拉框。
通常來(lái)說(shuō),SWT提供的組件集基本上能滿足大多數(shù)用戶的需求,而自定義組件通常分為2種,一種是將若干基本組件組合成一個(gè)復(fù)合組件(如日歷組件);第二是對(duì)現(xiàn)有組件改善外觀從而符合客戶的要求;或者將這兩種混合使用。利用SWT實(shí)現(xiàn)自定義組件通常要繼承Composite或Canvas來(lái)實(shí)現(xiàn),但是絕大多數(shù)采用繼承Composite實(shí)現(xiàn),如果你查看SWT的源代碼,你會(huì)發(fā)現(xiàn)很多SWT高級(jí)組件(如ExpandBar)都是直接繼承Composite來(lái)實(shí)現(xiàn)的。
準(zhǔn)備工作,首先將MSN登錄界面的截圖帖出來(lái)參考。

如果要模擬MSN的用戶名輸入組件,你需要采集一些數(shù)據(jù),分別是:正常、禁用兩種狀態(tài)下邊框的顏色;正常、禁用兩種狀態(tài)下的背景色;右邊下拉按鈕的圖標(biāo)。現(xiàn)在將這幾組數(shù)據(jù)給出。
正常狀態(tài)下邊框的顏色:RGB 170,183,199
禁用狀態(tài)下邊框的顏色:RGB 208,215,229
正常狀態(tài)下的背景色:RGB 254, 254, 254
禁用狀態(tài)下的背景色:RGB 238, 241, 249
下拉按鈕的圖標(biāo):

接下來(lái)創(chuàng)建一個(gè)類叫做ComboSelector繼承自Composite。需要指出的是,這個(gè)自定義組件SWT組件庫(kù)支持,在Eclipse下如果有VE、swt-designer這樣的插件可以借助向?qū)⒈匾膸?kù)導(dǎo)入到工程的classpath下,此外如果部署SWT應(yīng)用程序還需要一個(gè)動(dòng)態(tài)庫(kù),關(guān)于如何部署本文不作闡述。
創(chuàng)建以上這些數(shù)據(jù)常量
private final Color ENABLED_LINE_COLOR = new Color(Display.getCurrent(), 170, 183, 199);
private final Color DISABLED_LINE_COLOR = new Color(Display.getCurrent(), 208, 215, 229);
private final Color ENABLED_BG = new Color(Display.getCurrent(), 254, 254, 254);
private final Color DISABLED_BG = new Color(Display.getCurrent(), 238, 241, 249);
private final Image COMBO_ICON = new Image(Display.getDefault(), "combo.png");
另外你還需要一個(gè)基本文本組件用于輸入、一個(gè)菜單顯示保存的數(shù)據(jù)。
private Text inputText;
private Menu selectorMenu;
以上這些是和顯示相關(guān)的變量,但是除了這些還要保存臨時(shí)的數(shù)據(jù),分別是當(dāng)前用戶選擇了的那一項(xiàng)、下拉框所有數(shù)據(jù)項(xiàng)的集合。為了實(shí)現(xiàn)通用性和移植性這兩組數(shù)據(jù)均用Object保存。
private Object selectedItem;
private Vector dataSet = new Vector();
接著定義構(gòu)造函數(shù)。
public ComboSelector(Composite parent) {...}
需要注意的是,與Swing組件不同,任何SWT組件的構(gòu)造器一定要有一個(gè)不為null的指向其父組件的參數(shù),也就是說(shuō),SWT組件一旦被創(chuàng)建,就和它的父組件綁定了,其父組件不會(huì)提供任何add(...)、remove(...)方法添加或者移除組件,除非子組件調(diào)用dispose()方法銷毀自身。而Swing組件構(gòu)造時(shí)無(wú)需指父組件,而是通過(guò)父組件調(diào)用add(Component comp)將組件加進(jìn)來(lái),從這一點(diǎn)來(lái)說(shuō),Swing復(fù)合JavaBean規(guī)范,這個(gè)優(yōu)勢(shì)是SWT所無(wú)法比擬的。
在完成構(gòu)造函數(shù)之前,我們先定義一個(gè)輔助函數(shù),用來(lái)獲取該組件在屏幕中的坐標(biāo),其思想是循環(huán)調(diào)用getParent()方法獲取父組件,直到為null為止,因?yàn)檫@樣循環(huán)調(diào)用getParent()總會(huì)找到最外層的窗口Shell對(duì)象。然后將各個(gè)子組件在其父組件上的坐標(biāo)依次相加。
方法如下:
private Point getScreemLocation() {
Control control = this;
int width = control.getLocation().x;
int height = control.getLocation().y;
while (control.getParent() != null) {
control = control.getParent();
width += control.getLocation().x;
height += control.getLocation().y;
}
return new Point(width, height);
}
現(xiàn)在讓我們完成構(gòu)造函數(shù)
super(parent, SWT.FLAT);
inputText = new Text(this, SWT.FLAT);
selectorMenu = new Menu(this);
setMenu(selectorMenu);
首先實(shí)現(xiàn)父組件的構(gòu)造器,注意,將風(fēng)格設(shè)置為FLAT或者NONE。如果為BORDER,那么運(yùn)行時(shí)會(huì)發(fā)現(xiàn)組件是凹陷下去的外觀(WindowsXP以前就是這種外觀),通常對(duì)于自定義的外觀都需要將風(fēng)格設(shè)置為SWT.FLAT或者SWT.NONE。然后創(chuàng)建基本文本、菜單。對(duì)于菜單需要注意的是除了在構(gòu)造時(shí)候要指定父組外,還要調(diào)用setMenu將菜單加進(jìn)來(lái)。
接下來(lái)一步很關(guān)鍵,是要進(jìn)行自定義繪制。繪制包括邊框和下拉按鈕的圖標(biāo)。
完整代碼如下:
addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
GC gc = e.gc;
gc.setForeground(isEnabled() ? ENABLED_LINE_COLOR
: DISABLED_LINE_COLOR);
gc.drawRectangle(0, 0, getSize().x - 1, getSize().y - 1);
gc.drawImage(COMBO_ICON, getSize().x
- COMBO_ICON.getBounds().width - 5,
(getSize().y - COMBO_ICON.getBounds().height) / 2);
}
});
首先根據(jù)組件是否可用決定邊框的顏色。調(diào)用drawRectangle完成繪制邊框的操作。
然后繪制圖標(biāo),注意,drawImage后兩個(gè)參數(shù)是繪制的坐標(biāo),也就是從哪里開(kāi)始畫(huà)起,模擬MSN用戶名輸入組件時(shí),下拉按鈕右端點(diǎn)x坐標(biāo)取距離組件最右端x坐標(biāo)(getSize().x)5像素處為最佳,因此計(jì)算得出下拉按鈕左端點(diǎn)x坐標(biāo)為getSize().x- COMBO_ICON.getBounds().width - 5。(左端點(diǎn)x坐標(biāo)與右端點(diǎn)x坐標(biāo)相差COMBO_ICON.getBounds().width應(yīng)該很容易理解,另外讀者對(duì)坐標(biāo)系的概念應(yīng)該有一定了解);對(duì)于按鈕的y坐標(biāo),計(jì)算思想是使按鈕的垂直位置居中,因此計(jì)算y坐標(biāo)公式為(getSize().y - COMBO_ICON.getBounds().height) / 2)。
接下來(lái)一步是確定基本文本組件的位置,完整代碼如下:
addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
inputText.setBounds(1, 1, getSize().x
- COMBO_ICON.getBounds().width - 15, getSize().y - 2);
}
});
給該組件注冊(cè)Control監(jiān)聽(tīng)器時(shí),當(dāng)該組件尺寸發(fā)生變化,會(huì)觸發(fā)controlResized方法,在該方法內(nèi)對(duì)基本文本組件的位置進(jìn)行調(diào)整。模擬MSN用戶名輸入組件原則是,基本文本組件的邊框被隱藏(構(gòu)造時(shí)候通過(guò)將Style設(shè)為SWT.FLAT),左端點(diǎn)x坐標(biāo)為1(為0的話會(huì)遮擋邊框線的左端),長(zhǎng)度是整個(gè)組件長(zhǎng)度減去下拉按鈕的長(zhǎng)度再減15像素為最佳,從而保證與下拉按鈕之間有一段距離,高度是整個(gè)組件的高度減2像素,過(guò)高會(huì)遮擋邊框線。
接著我們要重寫(xiě)setEnabled方法,代碼如下:
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
setBackground(enabled ? ENABLED_BG : DISABLED_BG);
inputText.setEnabled(enabled);
redraw();
}
第一行的super.setEnabled(enabled);表示保持父類enable屬性不變化,之后是設(shè)置背景,并設(shè)置inputText的enabled屬性,最后調(diào)用redraw方法通知組件重繪。需要闡明的是,redraw方法會(huì)調(diào)用PaintListener中的方法,也就是說(shuō)會(huì)調(diào)用到構(gòu)造函數(shù)中public void paintControl(PaintEvent e){...}這段代碼,如果組件添加了多個(gè)繪制監(jiān)聽(tīng)器,那么redraw會(huì)依次調(diào)用每個(gè)監(jiān)聽(tīng)器的paintControl方法,這與swing的事件機(jī)制是相同的。在redraw方法中根據(jù)isEnabled()的值決定邊框的顏色,所以每當(dāng)setEnable方法被調(diào)用都應(yīng)該執(zhí)行重繪。
還需要指出,通過(guò)添加繪制監(jiān)聽(tīng)器來(lái)實(shí)現(xiàn)個(gè)性化的外觀,并在調(diào)用影響外觀的操作(比如setEnable)時(shí)調(diào)用redraw方法強(qiáng)制組件重繪,這是自定義組件常用的實(shí)現(xiàn)手段。你會(huì)看到接下來(lái)的很多方法會(huì)經(jīng)常調(diào)用redraw通知組件重繪。
除了setEnabled方法,還有一些方法需要補(bǔ)充,一并列出:
public void setEditable(boolean editable) {
inputText.setEditable(editable);
}
public String getText() {
return inputText.getText();
}
public void setText(String text) {
inputText.setText(text);
}
public void setTextLimit(int limit) {
inputText.setTextLimit(limit);
}
這些方法簡(jiǎn)單易懂,不作解釋,以上列舉的只是最基本的方法,如果覺(jué)得功能不夠還可以定義其他方法,例如可以對(duì)用戶的輸入作驗(yàn)證。
接下來(lái)回到構(gòu)造函數(shù)中來(lái),QQ、MSN等一些軟件的登錄除了點(diǎn)擊登錄按鈕執(zhí)行還可以在用戶名、口令輸入框上單擊回車來(lái)實(shí)現(xiàn),為了實(shí)現(xiàn)這一功能,需要為基本文本組件添加一個(gè)選擇監(jiān)聽(tīng)器。
inputText.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(SelectionEvent e) {
commit();
}
});
這樣,當(dāng)用戶在文本組件上單擊回車,會(huì)執(zhí)行commit方法。下面是commit方法的定義:
protected void commit() {
};
它不作任何事情,因?yàn)榻M件不知道實(shí)際會(huì)應(yīng)用在何種場(chǎng)合,即回車操作具體作什么,這應(yīng)該通過(guò)繼承該組件重寫(xiě)commit方法實(shí)現(xiàn)具體功能。
然后為組件添加鼠標(biāo)監(jiān)聽(tīng)器,實(shí)現(xiàn)用戶單擊下拉按鈕時(shí)菜單的彈出。完整代碼如下:
addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
if (e.x > getBounds().width - COMBO_ICON.getBounds().width - 15
&& e.x < getBounds().width && e.y > 0
&& e.y < getBounds().height) {
selectorMenu.setLocation(getScreemLocation().x + 3,
getScreemLocation().y + getSize().y + 23);
selectorMenu.setVisible(true);
}
}
});
if條件句子是判斷鼠標(biāo)指針的落點(diǎn)是否位于下拉三角的區(qū)域內(nèi),計(jì)算方法讀者可以自己思考,之后設(shè)置彈出菜單出現(xiàn)的位置,根據(jù)前面定義的getScreemLocation方法可方便得出,需要提出的是計(jì)算x坐標(biāo)的“+3”和y坐標(biāo)的“+23”,為什么要再加上這個(gè)整數(shù)呢?是因?yàn)閃indows窗口的標(biāo)題欄高20像素,而getScreemLocation是無(wú)法自動(dòng)計(jì)算出的,有些窗口可通過(guò)設(shè)置將標(biāo)題欄去掉(SWT的Shell通過(guò)指定SWT.NO_TRIM樣式實(shí)現(xiàn))“+3是使菜單彈出的位置不至于遮擋組件邊框線,因此偏移3像素為最佳位置”。調(diào)用setVisible顯示菜單,不過(guò)前提條件是必須添加了菜單項(xiàng)。構(gòu)造函數(shù)最后是一步是設(shè)置組件為可用,雖然任何SWT/Swing組件在構(gòu)造時(shí)默認(rèn)都是可用的,但是正如前面所述,重寫(xiě)setEnabled并不止設(shè)置是否被禁用,重要的是組件在兩態(tài)下的外觀,所以在構(gòu)造函數(shù)最后添加setEnabled(true);
以上講述過(guò)多的是如何裝飾組件的外觀,接下來(lái)的重點(diǎn)將介紹如何用該組件緩存數(shù)據(jù),使用MSN時(shí)候會(huì)發(fā)現(xiàn),單擊登錄用戶名的下拉按鈕時(shí)候,會(huì)彈出所有在本機(jī)登錄過(guò)的用戶名列表(如果保存的話),下面講述如何實(shí)現(xiàn)這一功能。
我們的數(shù)據(jù)均保存在Vector這個(gè)集合中,由于實(shí)際應(yīng)用變化萬(wàn)千,組件不可能知道實(shí)際保存何種類型的數(shù)據(jù),因此Vector的元素類型統(tǒng)一設(shè)置為Object,這也實(shí)在是一個(gè)不錯(cuò)的設(shè)計(jì),因?yàn)樗粡?qiáng)制使用者去實(shí)現(xiàn)某某接口,或基類,假如設(shè)計(jì)成Vector中的元素必須是實(shí)現(xiàn)某一特定接口IElement,
private Vector dataSet = new Vector();
這樣的話,使用者就必須將其POJO作更改,以適應(yīng)于此組件,而Object作為所有類的基類,因此可容納任何類型的數(shù)據(jù)。接下來(lái)的一步很重要,是將數(shù)據(jù)與菜單關(guān)聯(lián)起來(lái)。定義如下方法public void loadMenuItems(Object[] objects),顧名思義是一次性讀取一組元素,完整的代碼如下:
public void loadMenuItems(Object[] objects) {
dataSet.clear();
MenuItem[] items = selectorMenu.getItems();
for (MenuItem item : items) {
item.removeSelectionListener(this);
item.dispose();
}
for (int i = 0; i < objects.length; i++) {
dataSet.add(objects[i]);
MenuItem item = new MenuItem(selectorMenu, SWT.PUSH);
item.setText(objects[i].toString());
item.setData(objects[i]);
item.addSelectionListener(this);
}
}
因?yàn)槭莑oad所有數(shù)據(jù),所以第一步是將已有的數(shù)據(jù)清空,包括Vector中的數(shù)據(jù)和菜單中的菜單項(xiàng)。然后是對(duì)傳入的Object數(shù)組作遍歷,對(duì)于每一項(xiàng),將之添加進(jìn)集合然后創(chuàng)建一個(gè)菜單項(xiàng),下一步item.setText(objects[i].toString());是設(shè)置菜單項(xiàng)的文字,toString()方法是Object的固有方法,但是實(shí)際應(yīng)用時(shí)必須重寫(xiě)該方法的實(shí)現(xiàn)。接下來(lái)是item.setData(objects[i]);為菜單項(xiàng)設(shè)置數(shù)據(jù),這一步非常重要,SWT的每一個(gè)組件都具有public void setData (Object data)和public Object getData ()方法。還有Hash結(jié)構(gòu)的public void setData (String key, Object value)和public Object getData (String key)。稍后會(huì)看到通過(guò)item.getData();取出創(chuàng)建時(shí)存入的數(shù)據(jù)。最后一行是為菜單項(xiàng)添加事件監(jiān)聽(tīng)器,并使組件本身作為監(jiān)聽(tīng)器,使組件本身實(shí)現(xiàn)SelectionListener接口,然后添加下面兩個(gè)方法:
public final void widgetDefaultSelected(SelectionEvent e)
public final void widgetSelected(SelectionEvent e)
其中widgetDefaultSelected在單擊回車時(shí)觸發(fā),對(duì)文本框這樣的組件適用,widgetSelected是鼠標(biāo)單擊時(shí)觸發(fā)適用于按鈕、菜單項(xiàng)。因此我們只處理widgetSelected。
public final void widgetSelected(SelectionEvent e) {
MenuItem item = (MenuItem) e.getSource();
selectedItem = item.getData();
String text = item.getData().toString();
inputText.setText(text);
inputText.setSelection(0, text.length());
selected(item.getData());
}
首先取得事件源即單擊的菜單項(xiàng),然后更新selectedItem引用指向這個(gè)菜單項(xiàng)保存的數(shù)據(jù)(先前通過(guò)setData方法添加的),接下來(lái)的代碼不作解釋,很容易理解。值得注意的是最后一行selected(item.getData());作用是當(dāng)用戶選中菜單某一項(xiàng)時(shí),根據(jù)當(dāng)前選擇的那個(gè)數(shù)據(jù)自動(dòng)執(zhí)行相應(yīng)的操作,selected方法定義如下:
protected void selected(Object object) {
};
與commit方法一樣,是需要根據(jù)實(shí)際情況自定義處理邏輯的。
最后添加如下2個(gè)方法:
public void select(int index) {
MenuItem[] items = selectorMenu.getItems();
if (index < 0 || index >= items.length) {
throw new ArrayIndexOutOfBoundsException(
"the index value must between " + 0 + "and "
+ (items.length - 1));
}
selectedItem = items[index].getData();
inputText.setText(items[index].getText());
}
select用來(lái)設(shè)置當(dāng)前選擇第幾個(gè)項(xiàng),getSelectedItem返回當(dāng)前用戶選擇的數(shù)據(jù)。
到此為止,ComboSelector已經(jīng)完成,可以作為API使用了,下面我們編寫(xiě)一個(gè)程序測(cè)試該組件。
首先編寫(xiě)一個(gè)POJO,如下:
package swt.custom;
public class Person {
private String userName;
private String password;
public Person(String userName, String password) {
this.userName = userName;
this.password = password;
}
public String getPassword() {
return password;
}
public String getUserName() {
return userName;
}
@Override
public String toString() {
return userName;
}
}
簡(jiǎn)單至極的一個(gè)類,注意它的toString方法,返回用戶名屬性作為顯示。
接下來(lái)通過(guò)一個(gè)demo看看實(shí)際運(yùn)行效果。
用swt-designer工具創(chuàng)建一個(gè)Shell,在createContents方法體內(nèi)添加如下代碼:
final ComboSelector selector = new ComboSelector(this) {
@Override
protected void commit() {
System.out.println("current data is "
+ ((Person) getSelectedItem()).getUserName());
}
@Override
protected void selected(Object object) {
System.out.println(((Person) object).getPassword());
}
};
selector.setBounds(114, 78, 200, 20);
Person[] persons = new Person[] {
new Person("play_station3@sina.com", "111111"),
new Person("rehte@hotmail.com", "222222"),
new Person("yitong.liu@bea.com", "password"),
new Person("使用其他Windows Live ID 登錄", "no") };
selector.loadMenuItems(persons);
selector.select(1);
運(yùn)行結(jié)果如下:

本程序的完整代碼這里下載