Java SWT實現MSN風格的下拉框

來源:互聯網
上載者:User
  SWT一個所謂的優點是它的本地化外觀,因為它是通過JNI叫用作業系統的組件,從而可以保證外觀上適合大多數使用者的需求,但是一些IM類 軟體商往往希望它們的產品有著一套獨特的外觀,這對SWT這種原生組件來說就有些力不從心了,嚴格來說如果你的使用者對外觀要求比較苛刻,那麼Swing是首選,因為L&F機制可以確保你做到這一點,另外Swing還有著許多SWT不具備的優點,例如半透明組件、渲染等,但是少數的這些特性用SWT還是可以類比的,本文就向大家介紹如何通過自訂群組件實現MSN風格的下拉框。

  通常來說,SWT提供的組件集基本上能滿足大多數使用者的需求,而自訂群組件通常分為2種,一種是將若干基本組件組合成一個複合組件(如行事曆群組件);第二是對現有組件改善外觀從而符合客戶的要求;或者將這兩種混合使用。利用SWT實現自訂群組件通常要繼承Composite或Canvas來實現,但是絕大多數採用繼承Composite實現,如果你查看SWT的原始碼,你會發現很多SWT進階組件(如ExpandBar)都是直接繼承Composite來實現的。

  如果要類比MSN的使用者名稱輸入組件,你需要採集一些資料,分別是:正常、禁用兩種狀態下邊框的顏色;正常、禁用兩種狀態下的背景色;右邊下拉按鈕的表徵圖。現在將這幾組資料給出。

  正常狀態下邊框的顏色:RGB 170,183,199
  禁用狀態下邊框的顏色:RGB 208,215,229
  正常狀態下的背景色:RGB 254, 254, 254
  禁用狀態下的背景色:RGB 238, 241, 249

  下拉按鈕的表徵圖:

  接下來建立一個類叫做ComboSelector繼承自Composite。需要指出的是,這個自訂群組件SWT組件庫支援,在Eclipse下如果有 VE、swt-designer這樣的外掛程式可以藉助嚮導將必要的庫匯入到工程的classpath下,此外如果部署SWT應用程式還需要一個動態庫,關於如何部署本文不作闡述。

  建立以上這些資料常量

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");

  另外你還需要一個基本文本組件用於輸入、一個菜單顯示儲存的資料。

private Text inputText;

private Menu selectorMenu;

  以上這些是和顯示相關的變數,但是除了這些還要儲存臨時的資料,分別是目前使用者選擇了的那一項、下拉框所有資料項目的集合。為了實現通用性和移植性這兩組資料均用Object儲存。

private Object selectedItem;

private Vector dataSet = new Vector();

  接著定義建構函式。

public ComboSelector(Composite parent) {...}

  需要注意的是,與Swing組件不同,任何SWT組件的構造器一定要有一個不為null的指向其父組件的參數,也就是說,SWT組件一旦被建立,就和它的父組件綁定了,其父組件不會提供任何add(...)、remove(...)方法添加或者移除組件,除非子組件調用dispose()方法銷毀自身。而Swing組件構造時無需指父組件,而是通過父組件調用add(Component comp)將組件加進來,從這一點來說,Swing複合JavaBean規範,這個優勢是SWT所無法比擬的。

  在完成建構函式之前,我們先定義一個輔助函數,用來擷取該組件在螢幕中的座標,其思想是迴圈調用getParent()方法擷取父組件,直到為null為止,因為這樣迴圈調用 getParent()總會找到最外層的視窗Shell對象。然後將各個子組件在其父組件上的座標依次相加。

  方法如下:

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);
}

  現在讓我們完成建構函式

super(parent, SWT.FLAT);
inputText = new Text(this, SWT.FLAT);
selectorMenu = new Menu(this);
setMenu(selectorMenu);

  首先實現父組件的構造器,注意,將風格設定為FLAT或者NONE。如果為BORDER,那麼運行時會發現組件是凹陷下去的外觀(WindowsXP以前就是這種外觀),通常對於自訂的外觀都需要將風格設定為SWT.FLAT或者SWT.NONE。然後建立基本文本、菜單。對於菜單需要注意的是除了在構造時候要指定父組外,還要調用setMenu將菜單加進來。

  接下來一步很關鍵,是要進行自訂繪製。繪製包括邊框和下拉按鈕的表徵圖。

  完整代碼如下:

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);
}
});

  首先根據組件是否可用決定邊框的顏色。調用drawRectangle完成繪製邊框的操作。

  然後繪製表徵圖,注意,drawImage後兩個參數是繪製的座標,也就是從哪裡開始畫起,類比MSN使用者名稱輸入組件時,下拉按鈕右端點x座標取距離組件最右端x座標(getSize().x)5像素處為最佳,因此計算得出下拉按鈕左端點x座標為getSize().x- COMBO_ICON.getBounds().width - 5。(左端點x座標與右端點x座標相差COMBO_ICON.getBounds().width應該很容易理解,另外讀者對座標系的概念應該有一定瞭解);對於按鈕的y座標,計算思想是使按鈕的垂直位置置中,因此計算y座標公式為(getSize().y - COMBO_ICON.getBounds().height) / 2)。

  接下來一步是確定基本文本組件的位置,完整代碼如下:

addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
inputText.setBounds(1, 1, getSize().x
- COMBO_ICON.getBounds().width - 15, getSize().y - 2);
}
});

  給該組件註冊Control監聽器時,當該組件尺寸發生變化,會觸發controlResized方法,在該方法內對基本文本組件的位置進行調整。類比 MSN使用者名稱輸入組件原則是,基本文本組件的邊框被隱藏(構造時候通過將Style設為SWT.FLAT),左端點x座標為1(為0的話會遮擋邊框線的左端),長度是整個組件長度減去下拉按鈕的長度再減15像素為最佳,從而保證與下拉按鈕之間有一段距離,高度是整個組件的高度減2像素,過高會遮擋邊框線。

  接著我們要重寫setEnabled方法,代碼如下:

public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
setBackground(enabled ? ENABLED_BG : DISABLED_BG);
inputText.setEnabled(enabled);
redraw();
}

  第一行的super.setEnabled(enabled); 表示保持父類enable屬性不變化,之後是設定背景,並設定inputText的enabled屬性,最後調用redraw方法通知群組件重繪。需要闡明的是,redraw方法會調用PaintListener中的方法,也就是說會調用到建構函式中public void paintControl(PaintEvent e){...}這段代碼,如果組件添加了多個繪製監聽器,那麼redraw會依次調用每個監聽器的paintControl方法,這與swing的事件機制是相同的。在redraw方法中根據isEnabled()的值決定邊框的顏色,所以每當setEnable方法被調用都應該執行重繪。

 

還需要指出,通過添加繪製監聽器來實現個人化的外觀,並在調用影響外觀的操作(比如setEnable)時調用redraw方法強制組件重繪,這是自訂群組件常用的實現手段。你會看到接下來的很多方法會經常調用redraw通知群組件重繪。

  除了setEnabled方法,還有一些方法需要補充,一併列出:

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);
}

  這些方法簡單易懂,不作解釋,以上列舉的只是最基本的方法,如果覺得功能不夠還可以定義其他方法,例如可以對使用者的輸入作驗證。

  接下來回到建構函式中來,QQ、MSN等一些軟體的登入除了點擊登入按鈕執行還可以在使用者名稱、口令輸入框上單擊斷行符號來實現,為了實現這一功能,需要為基本文本組件添加一個選擇監聽器。

inputText.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(SelectionEvent e) {
commit();
}
});

  這樣,當使用者在文本組件上單擊斷行符號,會執行commit方法。下面是commit方法的定義:

protected void commit() {};

  它不作任何事情,因為組件不知道實際會應用在何種場合,即斷行符號操作具體作什麼,這應該通過繼承該組件重寫commit方法實現具體功能。

  然後為組件添加滑鼠監聽器,實現使用者單擊下拉按鈕時菜單的彈出。完整代碼如下:

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條件句子是判斷滑鼠指標的落點是否位於下拉三角的地區內,計算方法讀者可以自己思考,之後設定快顯功能表出現的位置,根據前面定義的 getScreemLocation方法可方便得出,需要提出的是計算x座標的“+3”和y座標的“+23”,為什麼要再加上這個整數呢?是因為 Windows視窗的標題列高20像素,而getScreemLocation是無法自動計算出的,有些視窗可通過設定將標題列去掉(SWT的Shell 通過指定SWT.NO_TRIM樣式實現)“+3是使菜單彈出的位置不至於遮擋組件邊框線,因此位移3像素為最佳位置”。調用setVisible顯示菜單,不過前提條件是必須添加了功能表項目。建構函式最後是一步是設定組件為可用,雖然任何SWT/Swing組件在構造時預設都是可用的,但是正如前面所述,重寫setEnabled並不止設定是否被禁用,重要的是組件在兩態下的外觀,所以在建構函式最後添加setEnabled(true);

  以上講述過多的是如何裝飾組件的外觀,接下來的重點將介紹如何用該組件快取資料,使用MSN時候會發現,單擊登入使用者名稱的下拉按鈕時候,會彈出所有在本機登入過的使用者名稱列表(如果儲存的話),下面講述如何?這一功能。

  我們的資料均儲存在Vector這個集合中,由於實際應用變化萬千,組件不可能知道實際儲存何種類型的資料,因此Vector的元素類型統一設定為 Object,這也實在是一個不錯的設計,因為它不強制使用者去實現某某介面,或基類,假如設計成Vector中的元素必須是實現某一特定介面 IElement,

private Vector dataSet = new Vector();

  這樣的話,使用者就必須將其POJO作更改,以適應於此組件,而Object作為所有類的基類,因此可容納任何類型的資料。接下來的一步很重要,是將資料與菜單關聯起來。定義如下方法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);
}
}

  因為是load所有資料,所以第一步是將已有的資料清空,包括Vector中的資料和菜單中的功能表項目。然後是對傳入的Object數組作遍曆,對於每一項,將之添加進集合然後建立一個功能表項目,下一步item.setText(objects[i].toString()); 是設定功能表項目的文字,toString()方法是Object的固有方法,但是實際應用時必須重寫該方法的實現。接下來是item.setData (objects[i]); 為功能表項目設定資料,這一步非常重要,SWT的每一個組件都具有public void setData (Object data)和public Object getData ()方法。還有Hash結構的public void setData (String key, Object value)和public Object getData (String key)。稍後會看到通過item.getData(); 取出建立時存入的資料。最後一行是為功能表項目添加事件監聽器,並使組件本身作為監聽器,使組件本身實現SelectionListener介面,然後添加下面兩個方法:

public final void widgetDefaultSelected(SelectionEvent e)

public final void widgetSelected(SelectionEvent e)

  其中widgetDefaultSelected在單擊斷行符號時觸發,對文字框這樣的組件適用,widgetSelected是按一下滑鼠時觸發適用於按鈕、功能表項目。因此我們只處理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());
}

  首先取得事件來源即單擊的功能表項目,然後更新selectedItem引用指向這個功能表項目儲存的資料(先前通過setData方法添加的),接下來的代碼不作解釋,很容易理解。值得注意的是最後一行selected(item.getData()); 作用是當使用者選中菜單某一項時,根據當前選擇的那個資料自動執行相應的操作,selected方法定義如下:

protected void selected(Object object) {};

  與commit方法一樣,是需要根據實際情況自訂處理邏輯的。

  最後添加如下2個方法:

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用來設定當前選擇第幾個項,getSelectedItem返回目前使用者選擇的資料。

  到此為止,ComboSelector已經完成,可以作為API使用了,下面我們編寫一個程式測試該組件。

  首先編寫一個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;
}
}

  簡單至極的一個類,注意它的toString方法,返回使用者名稱屬性作為顯示。

  接下來通過一個demo看看實際運行效果。

  用swt-designer工具建立一個Shell,在createContents方法體內添加如下代碼:

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);
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.