﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>BlogJava-Java桌面技术-随笔分类-SWT</title><link>http://www.blogjava.net/javagui/category/26769.html</link><description>Java Desktop Technology</description><language>zh-cn</language><lastBuildDate>Fri, 06 Jul 2012 03:46:03 GMT</lastBuildDate><pubDate>Fri, 06 Jul 2012 03:46:03 GMT</pubDate><ttl>60</ttl><item><title>SWT自定义组件之Slider</title><link>http://www.blogjava.net/javagui/archive/2007/10/23/155271.html</link><dc:creator>sun_java_studio@yahoo.com.cn(电玩)</dc:creator><author>sun_java_studio@yahoo.com.cn(电玩)</author><pubDate>Tue, 23 Oct 2007 05:35:00 GMT</pubDate><guid>http://www.blogjava.net/javagui/archive/2007/10/23/155271.html</guid><wfw:comment>http://www.blogjava.net/javagui/comments/155271.html</wfw:comment><comments>http://www.blogjava.net/javagui/archive/2007/10/23/155271.html#Feedback</comments><slash:comments>11</slash:comments><wfw:commentRss>http://www.blogjava.net/javagui/comments/commentRss/155271.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/javagui/services/trackbacks/155271.html</trackback:ping><description><![CDATA[<p>&nbsp;&nbsp; 曾经介绍过<a href="http://blog.csdn.net/www_hero/archive/2007/09/10/1778678.aspx">用SWT实现MSN风格的下拉框</a>，SWT虽然没有Swing那么强大，尤其是在打造专业外观上，不支持L&amp;F，但是通过自定义组件，同样可以达到用户要求。下面就向大家介绍本人实现的一个具备专业外观的Slider控件。<br />
&nbsp;&nbsp;&nbsp; 首先来参考一下组件的实际运行效果，并和SWT原生组件进行一下对比。&nbsp; </p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img height="296" alt="" src="http://www.blogjava.net/images/blogjava_net/javagui/slider_demo.png" width="259" border="0" /></p>
<p>可以看出，经过自定义的组件在外观上要比SWT直接调用本地组件显得更加专业。当用户托拽滑动块时，还会出现一个虚拟的滑动块用来标识将要移动到的位置。演示就到此为止，下面详细介绍这个很Cool的组件是如何通过SWT实现的。</p>
<p>&nbsp;&nbsp;&nbsp; 基本设计思想：与其他自定义组件一样，是通过继承org.eclipse.swt.widgets.Composite来实现，定义该类为Slider，另外滑动块（thumb）也是Composite，并放在Slider之上，当鼠标移动thumb时，调用setBounds方法定位在Slider在父组件（Slider）上的位置，从而达到拖拽thumb的目的。此外通过实现PaintListener接口进行自定义绘制，绘制的对象包括组件边框、被填充的格子、未被填充的格子、虚拟滑块。</p>
<p>&nbsp;&nbsp;&nbsp; 接触过GUI编程的程序员都应该知道像Scroll、Slider、ProgressBar这样的控件都有setMaxValue、setMinValue、setValue这样的方法，除了鼠标拖拽thumb来改变当前数值外，可直接调用setValue来设置当前值。此外这些控件还有水平（Horizontal）、垂直（Vertical）两种布局，对于事件处理一般都要有一个从java.util.EventObject继承而来的事件类，还要编写事件监听器（Listener）接口，因此在开始编写Slider控件之前先定义3个类，代码都不是很长，如果你熟悉AWT、Swing的事件处理机制，相信你能轻松跳过。</p>
<p><br />
public enum SliderOrientation {<br />
&nbsp;HORIZONTAL, VERTICAL;<br />
}</p>
<p>public class SliderEvent extends EventObject {</p>
<p>&nbsp;private int value;</p>
<p>&nbsp;public SliderEvent(Object source, int value) {<br />
&nbsp;&nbsp;super(source);<br />
&nbsp;&nbsp;this.value = value;<br />
&nbsp;}</p>
<p>&nbsp;public int getValue() {<br />
&nbsp;&nbsp;return value;<br />
&nbsp;}<br />
}</p>
<p>public interface SliderListener {</p>
<p>&nbsp;public void valueChanged(SliderEvent event);<br />
}</p>
<p>&nbsp;&nbsp;&nbsp; 接下来着重介绍Slider。首先是继承Composite并实现ControlListener、PaintListener、MouseListener,、MouseMoveListener,、MouseTrackListener，然后自动生成接口方法代码，通过Eclipse可以轻松实现，需要注意的是MouseListener，有java.awt.event.MouseListener和org.eclipse.swt.events.MouseListener两种，不要混淆，否则错误很难找到。然后是要采集一些数据信息，分别是：边框颜色、已有数据部分的填充颜色（上图中组件的绿色部分）、未达到数据部分的填充颜色（上图中组件的白色部分）、被禁用时的填充颜色、水平滑块的图标（正常、托拽中两种）、垂直滑块图标（正常、托拽中两种）、水平、垂直虚拟滑块图标。以上这些数据对应的常量声明如下：</p>
<p>&nbsp;private final Color BORDER_COLOR = new Color(Display.getCurrent(), 180, 188, 203);</p>
<p>&nbsp;private final Color FILL_COLOR = new Color(Display.getCurrent(), 147, 217, 72);</p>
<p>&nbsp;private final Color BLANK_COLOR = new Color(Display.getCurrent(), 254, 254, 254);</p>
<p>&nbsp;private final Color DISABLE_COLOR = new Color(Display.getCurrent(), 192, 192, 192);</p>
<p>&nbsp;private final Image THUMB_ICON_V = new Image(Display.getDefault(), "slider_up_v.png");</p>
<p>&nbsp;private final Image THUMB_OVER_ICON_V = new Image(Display.getDefault(), "slider_over_v.png");</p>
<p>&nbsp;private final Image THUMB_ICON_H = new Image(Display.getDefault(),&nbsp;"slider_up_h.png");</p>
<p>&nbsp;private final Image THUMB_OVER_ICON_H = new Image(Display.getDefault(), "slider_over_h.png");</p>
<p>&nbsp;private final Image TEMP_H = new Image(Display.getDefault(), "temp_h.png");</p>
<p>&nbsp;private final Image TEMP_V = new Image(Display.getDefault(), "temp_v.png");</p>
<p>&nbsp;除了这些常量，还应该声明默认最大值的常量，private final int DEFAULT_MAX_VALUE = 100;<br />
接下来定义当前数值和最大值，<br />
private int value;<br />
private int maxValue = DEFAULT_MAX_VALUE;<br />
并生成以上两个成员属性的get方法<br />
然后定义滑动块和布局<br />
private SliderOrientation orientation;<br />
private Composite thumb;<br />
要处理数值变化，需要实现一组监听器，添加如下代码<br />
private List&lt;SliderListener&gt; listeners = new ArrayList&lt;SliderListener&gt;();<br />
public void addSliderListener(SliderListener sliderListener) {<br />
&nbsp;listeners.add(sliderListener);<br />
}<br />
public void removeSliderListener(SliderListener sliderListener) {<br />
&nbsp;listeners.remove(sliderListener);<br />
}<br />
接下来定义2个辅助方法，实现value&lt;-&gt;pelsLength转换。其中value是当前的数值，由具体业务来决定，下文中称其业务值。例如一个音量控制器，音量范围在0~500，那么从业务上来讲可以将数值设置在0~500之间的任何数，而pelsLength则由控件的长/高度来决定，单位是像素。但是value与pelsLength之间存在着一个比例关系式：value/maxValue=pelsLength/控件长度或高度。这样不难得出两个函数的定义。<br />
private int valueToPels(int value) {<br />
&nbsp;&nbsp;float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width<br />
&nbsp;&nbsp;&nbsp;&nbsp;: getBounds().height;<br />
&nbsp;&nbsp;return (int) (widgetLength * (float) value / (float) maxValue);<br />
&nbsp;}</p>
<p>&nbsp;private int pelsToValue(int pels) {<br />
&nbsp;&nbsp;float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width<br />
&nbsp;&nbsp;&nbsp;&nbsp;: getBounds().height;<br />
&nbsp;&nbsp;return (int) ((float) pels * (float) maxValue / (float) widgetLength);<br />
&nbsp;}<br />
最后定义构造器。代码如下<br />
public Slider(Composite parent, SliderOrientation orientation) {<br />
&nbsp;&nbsp;super(parent, SWT.FLAT);<br />
&nbsp;&nbsp;this.orientation = orientation;<br />
&nbsp;&nbsp;thumb = new Composite(this, SWT.FLAT);<br />
&nbsp;&nbsp;thumb<br />
&nbsp;&nbsp;&nbsp;&nbsp;.setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: THUMB_ICON_H);<br />
&nbsp;&nbsp;addControlListener(this);<br />
&nbsp;&nbsp;addPaintListener(this);<br />
&nbsp;&nbsp;thumb.addMouseListener(this);<br />
&nbsp;&nbsp;thumb.addMouseMoveListener(this);<br />
&nbsp;&nbsp;thumb.addMouseTrackListener(this);<br />
&nbsp;}<br />
在构造器中，注入布局对象，然后在控件上创建滑动块组件thumb，并添加鼠标处理等。<br />
到此为止，基本的成员和方法的定义完毕，下面循序渐进讨论如何实现这一Slider。</p>
<p>一、绘制边框<br />
由于是绘制操作，所以一切绘制代码均在paintControl方法内实现，先将如下代码拷贝到paintControl内<br />
int w = getBounds().width;<br />
int h = getBounds().height;<br />
int fillLength = valueToPels(getValue());<br />
GC gc = e.gc;<br />
switch (orientation) {<br />
&nbsp;case HORIZONTAL:<br />
&nbsp;&nbsp;break;<br />
&nbsp;case VERTICAL:<br />
&nbsp;&nbsp;break;<br />
}<br />
分析如下<br />
首先获取控件的长度与高度，因为接下来的绘制要经常用到这两个变量。<br />
&#8220;int fillLength = valueToPels(getValue());&#8221;这一行代码稍后作解释，然后是获得绘制上下文对象，下一步是根据布局不同采用不同的处理，除了paintControl函数，在其他很多地方都对布局进行判断，但是简单起见，只对水平布局进行介绍，垂直部分参考完整程序。<br />
接下来的绘制操作均在case HORIZONTAL中进行，首先将颜色设置为边框的颜色<br />
gc.setForeground(BORDER_COLOR);然后绘制一个矩形gc.drawRectangle(0, 2, w - 1, h - 5);<br />
关于为什么要偏移2像素、长度为什么减1、高度为什么减5，请参考有关绘图的基本知识，<a href="http://blog.csdn.net/www_hero/archive/2007/09/10/1778678.aspx">上一篇</a>也有简单的介绍。</p>
<p>现在，你就可以编写测试程序来验证结果了，看看边框是否与示例的效果一样。<br />
</p>
<p>二、托拽thumb的实现<br />
桌面GUI编程领域技术深浅的度量衡通常有4项指标：皮肤（外观，swing组件体系称其L&amp;F）、绘图、自定义组件布局（Layout）、自定义组件。而托拽是实现自定义组件和绘图不可或缺的技术，也是难点之一，因此掌握的深浅是衡量桌面编程水平的标志。<br />
虽然作为难点，但是也有章可循，其基本实现简单到只监听鼠标事件这么简单，基本流程是：当鼠标在thumb上按下时，记住这个位置，然后按住鼠标左键托拽，最后松开鼠标计算两个位置之间的距离（位移），根据位移量移动thumb的位置并换算出等价的value增量（可能为负值）进行业务逻辑处理。下面通过代码循序渐进完成。<br />
定义一个位置变量用来存储鼠标单击的位置，private Point controlPoint;然后实现public void mouseDown(MouseEvent e)和public void mouseUp(MouseEvent e)两个方法。<br />
public void mouseDown(MouseEvent e) {<br />
&nbsp;&nbsp;controlPoint = new Point(e.x, e.y);<br />
&nbsp;&nbsp;thumb<br />
&nbsp;&nbsp;&nbsp;&nbsp;.setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_OVER_ICON_V<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: THUMB_OVER_ICON_H);<br />
&nbsp;}</p>
<p>&nbsp;public void mouseUp(MouseEvent e) {<br />
&nbsp;&nbsp;try {<br />
&nbsp;&nbsp;&nbsp;thumb<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: THUMB_ICON_H);<br />
&nbsp;&nbsp;&nbsp;countValue(e);<br />
&nbsp;&nbsp;} finally {<br />
&nbsp;&nbsp;&nbsp;controlPoint = null;<br />
&nbsp;&nbsp;}<br />
&nbsp;}<br />
&#8220;controlPoint = new Point(e.x, e.y);&#8221;这一行实现记住鼠标点的位置，注意，这个位置是相对滑块thumb的，因为是thumb监听的鼠标事件。接下来是设置滑动块背景，不难理解当鼠标松开时，应该将背景恢复。然后进行非常重要的换算工作，通过countValue方法实现，最后务必要把鼠标位置清空，用try-finally是有必要这样的。之所以要在方法结束的时候清空controlPoint，是因为在鼠标移动的时候需要对controlPoint进行更加复杂的计算。稍后讲解mouseMove实现的时候再作解释，接下来着重分析countValue方法。<br />
如前所述，countValue完成计算鼠标按下、松开的位移量，换算成与业务相关的数据（value）。<br />
private void countValue(MouseEvent e) {<br />
&nbsp;&nbsp;switch (orientation) {<br />
&nbsp;&nbsp;case HORIZONTAL:<br />
&nbsp;&nbsp;&nbsp;int movedX = e.x - controlPoint.x;<br />
&nbsp;&nbsp;&nbsp;setValue(getValue() + pelsToValue(movedX));<br />
&nbsp;&nbsp;&nbsp;break;<br />
&nbsp;&nbsp;case VERTICAL:<br />
&nbsp;&nbsp;&nbsp;......<br />
&nbsp;&nbsp;}<br />
&nbsp;}<br />
&#8220;int movedX = e.x - controlPoint.x;&#8221;实现了位移量的计算，保存到movedX，调用pelsToValue方法将movedX转换成业务值的增量，然后调用setValue重新赋值，注意pelsToValue得到的是增量，需要与原值（getValue()得到）叠加。接下来分析setValue方法。<br />
public void setValue(int value) {<br />
&nbsp;&nbsp;if (value &lt; 0) {<br />
&nbsp;&nbsp;&nbsp;this.value = 0;<br />
&nbsp;&nbsp;} else if (value &gt; getMaxValue()) {<br />
&nbsp;&nbsp;&nbsp;this.value = getMaxValue();<br />
&nbsp;&nbsp;} else {<br />
&nbsp;&nbsp;&nbsp;this.value = value;<br />
&nbsp;&nbsp;}<br />
&nbsp;&nbsp;try {<br />
&nbsp;&nbsp;&nbsp;moveThumb();<br />
&nbsp;&nbsp;&nbsp;redraw();<br />
&nbsp;&nbsp;} finally {<br />
&nbsp;&nbsp;&nbsp;for (SliderListener listener : listeners) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;try {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SliderEvent event = new SliderEvent(this, getValue());<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;listener.valueChanged(event);<br />
&nbsp;&nbsp;&nbsp;&nbsp;} catch (Exception e) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;}<br />
&nbsp;}<br />
方法开始处的一系列if语句对value进行验证后再赋值，然后是调用moveThumb实现滑块移动、调用redraw对组件重新绘制，最后是处理具体的业务，可以看出处理业务是setValue方法的关键，所以要用try-finally，谁也不敢确保moveThumb、redraw不出问题。对于实现业务是遍历监听器列表然后执行每个监听器的valueChanged方法，这种事件源-监听器模型也是Java2以后的GUI事件实现模型。</p>
<p>&nbsp;private void moveThumb() {<br />
&nbsp;&nbsp;Image icon = thumb.getBackgroundImage();<br />
&nbsp;&nbsp;int iconw = (icon != null) ? icon.getBounds().width : 0;<br />
&nbsp;&nbsp;int iconh = (icon != null) ? icon.getBounds().height : 0;<br />
&nbsp;&nbsp;switch (orientation) {<br />
&nbsp;&nbsp;case HORIZONTAL:<br />
&nbsp;&nbsp;&nbsp;int x = valueToPels(getValue()) - iconw / 2;<br />
&nbsp;&nbsp;&nbsp;if (x &lt; 0) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;x = 0;<br />
&nbsp;&nbsp;&nbsp;} else if (x &gt; getBounds().width - iconw) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;x = getBounds().width - iconw;<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;thumb.setBounds(x, 0, iconw, iconh);<br />
&nbsp;&nbsp;&nbsp;break;<br />
&nbsp;&nbsp;case VERTICAL:<br />
&nbsp;&nbsp;&nbsp;......&nbsp;&nbsp;}<br />
&nbsp;}<br />
不难理解，moveThumb的任务就是根据业务值value来将滑块移动到正确的位置。<br />
以上代码声明滑块的位置是&#8220;x&#8221;，通过转换函数获得，但是还要减去滑块的一半，因为具体坐标应该落到滑动块的中间，仔细想想不难得出。if-else是对x进行验证，最后通过setBound来定位thumb。<br />
对于redraw,他的作用是触发paintControl方法进行重绘，因为重绘出了边框还要根据value绘制填充格子，而value已经在redraw方法调用前被赋了值，所以这时候应该进行重绘。<br />
现在你可以托拽thumb了，美中不足的是滑动块不能随时跟随鼠标的轨迹移动，这个稍后会实现。</p>
<p>三、填充格子<br />
现在的组件外观只是绘制了边框，现在进行格子填充。在讲述边框绘制的时候提到了一行代码&#8220;int fillLength = valueToPels(getValue());&#8221;现在不难理解吧，就是将当前业务值value转换成实际的长度。在绘制边框之后，加入如下代码：<br />
if (getEnabled()) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.setBackground(FILL_COLOR);<br />
&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 2; i &lt; w - 2; i += 4) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (i &gt; fillLength) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;gc.setBackground(BLANK_COLOR);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;gc.fillRectangle(i, 4, 3, h - 8);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;} else {<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.setBackground(DISABLE_COLOR);<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.fillRectangle(1, 4, w - 1, h - 8);<br />
&nbsp;&nbsp;&nbsp;}<br />
首先判断是否是enable，然后设置填充颜色FILL_COLOR，然后在for循环中执行正方形格子的填充，递增量&#8220;i&#8221;从2开始是空开2像素间隔，同理i也不能超过w-2（对称性），i+=4是相邻两个格子左边坐标间距4像素，然后&#8220;gc.fillRectangle(i, 4, 3, h - 8);&#8221;这一行进行填充绘制正方形。留意，x坐标是i，y坐标是从4开始画的，出于对称高度也要&#8220;h-8&#8221;，长度之所以是&#8220;3&#8221;是保持相邻两个格子之间保持1像素的间隔（想想&#8220;i+=4&#8221;就不难得出答案）。此外还要对fillLength进行判断以便决定颜色采用绿色还是白色以示区分。对于绘图操作来说，千万不要埋怨考虑细节过多，事实上，GUI编程过程中&#8220;坐标系&#8221;这个概念是需要经常被考虑的，试想如果上述代码for循环中把&#8220;i+=4&#8221;，写成&#8220;i+=5&#8221;，只是一个像素之差绘制效果差之千里，如果想知道笔者是如何得到这些坐标数据的，实话说，是靠多次调试得出的结果。<br />
现在你运行程序，应该能根据value进行格子填充了。到此为止，绝大多数的功能已经实现，但是人性化的界面设计应该在托拽时出现一个虚拟的滑动块用来标识将要移动到的位置。好，继续实现这一功能。<br />
定义一个int变量纪录thumb的临时位置，private int tempLocation;然后在paintControl添加如下红色代码<br />
case HORIZONTAL:<br />
&nbsp;&nbsp;&nbsp;gc.setForeground(BORDER_COLOR);<br />
&nbsp;&nbsp;&nbsp;gc.drawRectangle(0, 2, w - 1, h - 5);<br />
&nbsp;&nbsp;&nbsp;if (getEnabled()) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.setBackground(FILL_COLOR);<br />
&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 2; i &lt; w - 2; i += 4) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (i &gt; fillLength) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;gc.setBackground(BLANK_COLOR);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;gc.fillRectangle(i, 4, 3, h - 8);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;<span style="color: #ff0000">&nbsp;&nbsp;if (controlPoint != null) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;gc.drawImage(TEMP_H, tempLocation, 0);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
</span>&nbsp;&nbsp;&nbsp;} else {<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.setBackground(DISABLE_COLOR);<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.fillRectangle(1, 4, w - 1, h - 8);<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;break;<br />
很直观，对于水平布局就是在(tempLocation,0)画出&#8220;TEMP_H&#8221;图标。外面的if很重要，还记得mouseDown方法中的语句&#8220;controlPoint = new Point(e.x, e.y);&#8221;和mouseUp方法中的语句&#8220;controlPoint = null;&#8221;吗，当鼠标按下托拽开始时，为controlPoint赋值，当鼠标完成托拽松开时，将controlPoint置null，在这个托拽过程中controlPoint一直保持非null状态，所以paintControl方法才将图标画出。如果现在就着急运行程序则会发现，鼠标托拽时候虚拟图标总停留在最左边（如果垂直布局停留在最上边，因为并没有为tempLocation赋值默认是0），不会跟随鼠标移动。如果要实现真正的托拽那么必须在鼠标移动时反复执行paintControl，下面花大量的笔墨详细地介绍mouseMove方法的实现，并简单介绍绘图操作的一些原理和流程。<br />
开门见山，直接将mouseMove函数的全部代码列出。<br />
public void mouseMove(MouseEvent e) {<br />
&nbsp;&nbsp;if (controlPoint == null) {<br />
&nbsp;&nbsp;&nbsp;return;<br />
&nbsp;&nbsp;}<br />
&nbsp;&nbsp;int maxLength;<br />
&nbsp;&nbsp;int maxLocator;<br />
&nbsp;&nbsp;switch (orientation) {<br />
&nbsp;&nbsp;case HORIZONTAL:<br />
&nbsp;&nbsp;&nbsp;maxLength = valueToPels(getMaxValue());<br />
&nbsp;&nbsp;&nbsp;maxLocator = maxLength - TEMP_H.getBounds().width;<br />
&nbsp;&nbsp;&nbsp;int movedX = e.x - controlPoint.x;<br />
&nbsp;&nbsp;&nbsp;redraw(tempLocation, 0, TEMP_H.getBounds().width,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEMP_H.getBounds().height, false);<br />
&nbsp;&nbsp;&nbsp;tempLocation = valueToPels(getValue()) + movedX<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- TEMP_H.getBounds().width / 2;<br />
&nbsp;&nbsp;&nbsp;if (tempLocation &lt; 0) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;tempLocation = 0;<br />
&nbsp;&nbsp;&nbsp;} else if (tempLocation &gt; maxLocator) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;tempLocation = maxLocator;<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;break;<br />
&nbsp;&nbsp;case VERTICAL:<br />
&nbsp;&nbsp;&nbsp;......<br />
&nbsp;&nbsp;&nbsp;break;<br />
&nbsp;&nbsp;}<br />
&nbsp;}<br />
最前面的if语句表明，只有鼠标按下时移动鼠标才算托拽，道理前面已经阐明了。maxLength代表最大值转换得到的像素，maxLocator是虚拟图标左端（上端）最大坐标，movedX代表托拽的位移量。最下面的if-else if目的很明了。整个mouseMove函数中<br />
redraw(tempLocation, 0, TEMP_H.getBounds().width,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEMP_H.getBounds().height, false);<br />
&nbsp;&nbsp;&nbsp;tempLocation = valueToPels(getValue()) + movedX<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- TEMP_H.getBounds().width / 2;<br />
是整个托拽操作最难懂也是技术含量最高的两条语句。简单起见暂时用下面的语句代替<br />
tempLocation = valueToPels(getValue()) + movedX<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- TEMP_H.getBounds().width / 2;<br />
&nbsp;&nbsp;&nbsp;if (tempLocation &lt; 0) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;tempLocation = 0;<br />
&nbsp;&nbsp;&nbsp;} else if (tempLocation &gt; maxLocator) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;tempLocation = maxLocator;<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;redraw();<br />
其中&#8220;tempLocation&#8221;的赋值语句不变，变化的是redraw函数的调用位置和参数。这样的变化使得意思就不难理解了，首先确定tempLocation的值，等号右边的计算结果也不难理解，然后调用redraw方法重画组件。如果这时候你运行程序，托拽时确实虚拟光标会跟随鼠标移动，但是也会发现组件闪烁得很厉害！具体程度取决于用户计算机的性能，关于&#8220;组件重绘时闪烁&#8221;的问题是绘图操作的一个常见问题，不仅仅是Java，任何支持绘图的计算机语言都可以暴露这样的问题，当年在大学用MFC、VB的编写过画图板的人应该熟悉这类问题。<br />
在具体讨论之前先简单讲述鼠标监听器中的mouseMove操作<br />
一旦为GUI组件添加鼠标移动监听器，当鼠标光标在组件上移动时便调用监听器接口的mouseMove(MouseEvent e)方法，mouseMove调用的频率与鼠标移动的快慢有关，移动越快mouseMove被调用的次数就越少，反之就越多。假设鼠标从组件上的A点移动到B点，如果鼠标移动得足够快，那么就可以理解为从A直接到B而不经过中间的任何一个点，那么mouseMove函数仅仅调用一次。反之鼠标慢慢从A移动到B，那么中间可能会经过C、D、E、F......一系列的点，而mouseMove也会被执行多次。总之当鼠标在组件上移动时，mouseMove会频繁被调用，之所以闪烁问题出在redraw方法，如果redraw方法不加任何参数那么将对组件全部重绘，对于鼠标托拽这种操作鼠标每移动一次就要对组件全部重绘，性能的代价可想而知，不闪才怪呢。解决的办法就是只重绘变化的部分。<br />
如何做到这一点，要先了解必要的绘图机制，在SWT中（Swing绘图原理与其类似）redraw其实会包含2个含义，擦除、绘制，所以得名于redraw，意思就是重绘。首先是根据传入redraw方法的参数确认需要擦除的范围，如果没有则擦除全部，然后<span style="color: #ff0000">底层会创建一个绘制请求交给操作系统去处理，但是这个请求不会在redraw方法调用完毕后立即被处理，即重绘操作不会立即执行，它是被送进底层的事件队列中</span>。处理时的绘制操作是由paintControl完成，所以redraw的调用会导致paintControl的执行。<br />
再将那两行代码列出来。<br />
redraw(tempLocation, 0, TEMP_H.getBounds().width,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEMP_H.getBounds().height, false);<br />
&nbsp;&nbsp;&nbsp;tempLocation = valueToPels(getValue()) + movedX<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- TEMP_H.getBounds().width / 2;<br />
如果光标从A移到B处，redraw方法所做的事情是通知重绘光标在A点时虚拟图标范围内的图像，然后立即创建一个绘制请求送到系统事件队列，然后更新tempLocation的值。现在再将绘制的那部分代码列出<br />
if (controlPoint != null) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;gc.drawImage(TEMP_H, tempLocation, 0);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
将这两部分代码列出来做个比较，还有非常重要的一点需要指出，托拽时虚拟光标能呈连续性移动，这一功能之所以能得以实现非常重要的一点是：<span style="color: #ff0000">从底层请求送入事件队列到请求被执行存在时间差</span>，利用这个时间差（时间非常短）可以执行一些&#8220;更新操作&#8221;，比如上面的更新tempLocation。简单的理解是redraw方法调用会以异步方式执行擦除、绘制。当执行绘制操作&#8220;gc.drawImage(TEMP_H, tempLocation, 0);&#8221;时，tempLocation已经是更新后的值了，而redraw表明擦除旧区域的图像，因为当paintControl执行时这部分图像区域根据计算结果已经不再是虚拟滑块了。下面做一个试验。<br />
添加下面红色代码<br />
redraw(tempLocation, 0, TEMP_H.getBounds().width,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TEMP_H.getBounds().height, false);<br />
&nbsp;&nbsp;&nbsp;<span style="color: #ff0000">try {<br />
&nbsp;&nbsp;&nbsp;&nbsp;Thread.sleep(1000);<br />
&nbsp;&nbsp;&nbsp;} catch (InterruptedException e1) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;e1.printStackTrace();<br />
&nbsp;&nbsp;&nbsp;}<br />
</span>&nbsp;&nbsp;&nbsp;tempLocation = valueToPels(getValue()) + movedX<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- TEMP_H.getBounds().width / 2;<br />
这样在绘制执行时，tempLocation还没有得到更新，效果运行下知晓。<br />
<br />
现在，你可以运行完整的程序了，看一下托拽时的效果。<br />
完整的程序<a title="这里" href="http://blogs.oracle.com/Swing/resource/Slider_Demo.zip">这里</a>下载</p><img src ="http://www.blogjava.net/javagui/aggbug/155271.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/javagui/" target="_blank">sun_java_studio@yahoo.com.cn(电玩)</a> 2007-10-23 13:35 <a href="http://www.blogjava.net/javagui/archive/2007/10/23/155271.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>用SWT实现MSN风格的下拉框</title><link>http://www.blogjava.net/javagui/archive/2007/10/23/155266.html</link><dc:creator>sun_java_studio@yahoo.com.cn(电玩)</dc:creator><author>sun_java_studio@yahoo.com.cn(电玩)</author><pubDate>Tue, 23 Oct 2007 05:33:00 GMT</pubDate><guid>http://www.blogjava.net/javagui/archive/2007/10/23/155266.html</guid><wfw:comment>http://www.blogjava.net/javagui/comments/155266.html</wfw:comment><comments>http://www.blogjava.net/javagui/archive/2007/10/23/155266.html#Feedback</comments><slash:comments>5</slash:comments><wfw:commentRss>http://www.blogjava.net/javagui/comments/commentRss/155266.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/javagui/services/trackbacks/155266.html</trackback:ping><description><![CDATA[SWT一个所谓的优点是它的本地化外观，因为它是通过JNI调用操作系统的组件，从而可以保证外观上适合大多数用户的需求，但是一些IM类软件商往往希望它们的产品有着一套独特的外观，这对SWT这种原生组件来说就有些力不从心了，严格来说如果你的用户对外观要求比较苛刻，那么Swing是首选，因为L&amp;F机制可以确保你做到这一点，另外Swing还有着许多SWT不具备的优点，例如半透明组件、渲染等，但是少数的这些特性用SWT还是可以模拟的，本文就向大家介绍如何通过自定义组件实现MSN风格的下拉框。<br />
&nbsp; 通常来说，SWT提供的组件集基本上能满足大多数用户的需求，而自定义组件通常分为2种，一种是将若干基本组件组合成一个复合组件（如日历组件）；第二是对现有组件改善外观从而符合客户的要求；或者将这两种混合使用。利用SWT实现自定义组件通常要继承Composite或Canvas来实现，但是绝大多数采用继承Composite实现，如果你查看SWT的源代码，你会发现很多SWT高级组件（如ExpandBar）都是直接继承Composite来实现的。<br />
&nbsp; 准备工作，首先将MSN登录界面的截图帖出来参考。<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <img height="103" alt="" src="http://www.blogjava.net/images/blogjava_net/javagui/msn.jpg" width="274" border="0" /><a href="http://blog.sina.com.cn/main/html/showpic.html#url=http://s10.album.sina.com.cn/pic/575d6c48020015q9" target="_blank"></a><br />
如果要模拟MSN的用户名输入组件，你需要采集一些数据，分别是：正常、禁用两种状态下边框的颜色；正常、禁用两种状态下的背景色；右边下拉按钮的图标。现在将这几组数据给出。<br />
正常状态下边框的颜色：RGB 170,183,199<br />
禁用状态下边框的颜色：RGB 208,215,229<br />
正常状态下的背景色：RGB 254, 254, 254<br />
禁用状态下的背景色：RGB 238, 241, 249<br />
下拉按钮的图标：<img height="4" alt="" src="http://www.blogjava.net/images/blogjava_net/javagui/combo.png" width="7" border="0" /><br />
接下来创建一个类叫做ComboSelector继承自Composite。需要指出的是，这个自定义组件SWT组件库支持，在Eclipse下如果有VE、swt-designer这样的插件可以借助向导将必要的库导入到工程的classpath下，此外如果部署SWT应用程序还需要一个动态库，关于如何部署本文不作阐述。<br />
创建以上这些数据常量<br />
private final Color ENABLED_LINE_COLOR = new Color(Display.getCurrent(), 170, 183, 199);
<p><font face="宋体">private final Color DISABLED_LINE_COLOR = new Color(Display.getCurrent(), 208, 215, 229);</font></p>
<p><font face="宋体">private final Color ENABLED_BG = new Color(Display.getCurrent(), 254, 254, 254);</font></p>
<p><font face="宋体">private final Color DISABLED_BG = new Color(Display.getCurrent(), 238, 241, 249);</font></p>
<p><font face="宋体">private final Image COMBO_ICON = new Image(Display.getDefault(), "combo.png");</font></p>
<p><font face="宋体">另外你还需要一个基本文本组件用于输入、一个菜单显示保存的数据。</font></p>
<p><font face="宋体">private Text inputText;</font></p>
<p><font face="宋体">private Menu selectorMenu;</font></p>
<p><font face="宋体">以上这些是和显示相关的变量，但是除了这些还要保存临时的数据，分别是当前用户选择了的那一项、下拉框所有数据项的集合。为了实现通用性和移植性这两组数据均用Object保存。</font></p>
<p><font face="宋体">private Object selectedItem;</font></p>
<p><font face="宋体">private Vector dataSet = new Vector();</font></p>
<p><font face="宋体">接着定义构造函数。</font></p>
<p><font face="宋体">public ComboSelector(Composite parent) {...}</font></p>
<p><font face="宋体">需要注意的是，与Swing组件不同，任何SWT组件的构造器一定要有一个不为null的指向其父组件的参数，也就是说，SWT组件一旦被创建，就和它的父组件绑定了，其父组件不会提供任何add(...)、remove(...)方法添加或者移除组件，除非子组件调用dispose()方法销毁自身。而Swing组件构造时无需指父组件，而是通过父组件调用add(Component comp)将组件加进来，从这一点来说，Swing复合JavaBean规范，这个优势是SWT所无法比拟的。</font></p>
<p><font face="宋体">在完成构造函数之前，我们先定义一个辅助函数，用来获取该组件在屏幕中的坐标，其思想是循环调用getParent()方法获取父组件，直到为null为止，因为这样循环调用getParent()总会找到最外层的窗口Shell对象。然后将各个子组件在其父组件上的坐标依次相加。</font></p>
<p><font face="宋体">方法如下：</font></p>
<p><font face="宋体">private Point getScreemLocation() {<br />
&nbsp;&nbsp;Control control = this;<br />
&nbsp;&nbsp;int width = control.getLocation().x;<br />
&nbsp;&nbsp;int height = control.getLocation().y;<br />
&nbsp;&nbsp;while (control.getParent() != null) {<br />
&nbsp;&nbsp;&nbsp;control = control.getParent();<br />
&nbsp;&nbsp;&nbsp;width += control.getLocation().x;<br />
&nbsp;&nbsp;&nbsp;height += control.getLocation().y;<br />
&nbsp;&nbsp;}<br />
&nbsp;&nbsp;return new Point(width, height);<br />
}</font></p>
<p><font face="宋体">现在让我们完成构造函数</font></p>
<p><font face="宋体">super(parent, SWT.FLAT);<br />
inputText = new Text(this, SWT.FLAT);<br />
selectorMenu = new Menu(this);<br />
setMenu(selectorMenu);</font></p>
<p><font face="宋体">首先实现父组件的构造器，注意，将风格设置为FLAT或者NONE。如果为BORDER，那么运行时会发现组件是凹陷下去的外观（WindowsXP以前就是这种外观），通常对于自定义的外观都需要将风格设置为SWT.FLAT或者SWT.NONE。然后创建基本文本、菜单。对于菜单需要注意的是除了在构造时候要指定父组外，还要调用setMenu将菜单加进来。</font></p>
<p><font face="宋体">接下来一步很关键，是要进行自定义绘制。绘制包括边框和下拉按钮的图标。</font></p>
<p><font face="宋体">完整代码如下：</font></p>
<p><font face="宋体">addPaintListener(new PaintListener() {<br />
&nbsp;&nbsp;&nbsp;public void paintControl(PaintEvent e) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;GC gc = e.gc;<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.setForeground(isEnabled() ? ENABLED_LINE_COLOR<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: DISABLED_LINE_COLOR);<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.drawRectangle(0, 0, getSize().x - 1, getSize().y - 1);<br />
&nbsp;&nbsp;&nbsp;&nbsp;gc.drawImage(COMBO_ICON, getSize().x<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- COMBO_ICON.getBounds().width - 5,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(getSize().y - COMBO_ICON.getBounds().height) / 2);<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;});<br />
首先根据组件是否可用决定边框的颜色。调用drawRectangle完成绘制边框的操作。</font></p>
<p><font face="宋体">然后绘制图标，注意，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)。</font></p>
<p><font face="宋体">接下来一步是确定基本文本组件的位置，完整代码如下：</font></p>
<p><font face="宋体">addControlListener(new ControlAdapter() {<br />
&nbsp;&nbsp;&nbsp;@Override<br />
&nbsp;&nbsp;&nbsp;public void controlResized(ControlEvent e) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;inputText.setBounds(1, 1, getSize().x<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- COMBO_ICON.getBounds().width - 15, getSize().y - 2);<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;});<br />
给该组件注册Control监听器时，当该组件尺寸发生变化，会触发controlResized方法，在该方法内对基本文本组件的位置进行调整。模拟MSN用户名输入组件原则是，基本文本组件的边框被隐藏（构造时候通过将Style设为SWT.FLAT），左端点x坐标为1（为0的话会遮挡边框线的左端），长度是整个组件长度减去下拉按钮的长度再减15像素为最佳，从而保证与下拉按钮之间有一段距离，高度是整个组件的高度减2像素，过高会遮挡边框线。</font></p>
<p><font face="宋体">接着我们要重写setEnabled方法，代码如下：</font></p>
<p><font face="宋体">public void setEnabled(boolean enabled) {<br />
&nbsp;&nbsp;super.setEnabled(enabled);<br />
&nbsp;&nbsp;setBackground(enabled ? ENABLED_BG : DISABLED_BG);<br />
&nbsp;&nbsp;inputText.setEnabled(enabled);<br />
&nbsp;&nbsp;redraw();<br />
&nbsp;}</font></p>
<p><font face="宋体">第一行的super.setEnabled(enabled);表示保持父类enable属性不变化，之后是设置背景，并设置inputText的enabled属性，最后调用redraw方法通知组件重绘。需要阐明的是，redraw方法会调用PaintListener中的方法，也就是说会调用到构造函数中public void paintControl(PaintEvent e){...}这段代码，如果组件添加了多个绘制监听器，那么redraw会依次调用每个监听器的paintControl方法，这与swing的事件机制是相同的。在redraw方法中根据isEnabled()的值决定边框的颜色，所以每当setEnable方法被调用都应该执行重绘。</font></p>
<p><font face="宋体">还需要指出，通过添加绘制监听器来实现个性化的外观，并在调用影响外观的操作（比如setEnable）时调用redraw方法强制组件重绘，这是自定义组件常用的实现手段。你会看到接下来的很多方法会经常调用redraw通知组件重绘。</font></p>
<p><font face="宋体">除了setEnabled方法，还有一些方法需要补充，一并列出：</font></p>
<p><font face="宋体">public void setEditable(boolean editable) {<br />
&nbsp;&nbsp;inputText.setEditable(editable);<br />
&nbsp;}</font></p>
<p><font face="宋体">public String getText() {<br />
&nbsp;&nbsp;return inputText.getText();<br />
&nbsp;}</font></p>
<p><font face="宋体">public void setText(String text) {<br />
&nbsp;&nbsp;inputText.setText(text);<br />
&nbsp;}</font></p>
<p><font face="宋体">public void setTextLimit(int limit) {<br />
&nbsp;&nbsp;inputText.setTextLimit(limit);<br />
&nbsp;}</font></p>
<p><font face="宋体">这些方法简单易懂，不作解释，以上列举的只是最基本的方法，如果觉得功能不够还可以定义其他方法，例如可以对用户的输入作验证。</font></p>
<p><font face="宋体">接下来回到构造函数中来，QQ、MSN等一些软件的登录除了点击登录按钮执行还可以在用户名、口令输入框上单击回车来实现，为了实现这一功能，需要为基本文本组件添加一个选择监听器。</font></p>
<p><font face="宋体">inputText.addSelectionListener(new SelectionAdapter() {<br />
&nbsp;&nbsp;&nbsp;@Override<br />
&nbsp;&nbsp;&nbsp;public void widgetDefaultSelected(SelectionEvent e) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;commit();<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;});</font></p>
<p><font face="宋体">这样，当用户在文本组件上单击回车，会执行commit方法。下面是commit方法的定义：</font></p>
<p><font face="宋体">protected void commit() {<br />
};</font></p>
<p><font face="宋体">它不作任何事情，因为组件不知道实际会应用在何种场合，即回车操作具体作什么，这应该通过继承该组件重写commit方法实现具体功能。</font></p>
<p><font face="宋体">然后为组件添加鼠标监听器，实现用户单击下拉按钮时菜单的弹出。完整代码如下：</font></p>
<p><font face="宋体">addMouseListener(new MouseAdapter() {<br />
&nbsp;&nbsp;&nbsp;@Override<br />
&nbsp;&nbsp;&nbsp;public void mouseUp(MouseEvent e) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;if (e.x &gt; getBounds().width - COMBO_ICON.getBounds().width - 15<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&amp;&amp; e.x &lt; getBounds().width &amp;&amp; e.y &gt; 0<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&amp;&amp; e.y &lt; getBounds().height) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;selectorMenu.setLocation(getScreemLocation().x + 3,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;getScreemLocation().y + getSize().y + 23);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;selectorMenu.setVisible(true);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;});</font></p>
<p><font face="宋体">if条件句子是判断鼠标指针的落点是否位于下拉三角的区域内，计算方法读者可以自己思考，之后设置弹出菜单出现的位置，根据前面定义的getScreemLocation方法可方便得出，需要提出的是计算x坐标的&#8220;+3&#8221;和y坐标的&#8220;+23&#8221;，为什么要再加上这个整数呢？是因为Windows窗口的标题栏高20像素，而getScreemLocation是无法自动计算出的，有些窗口可通过设置将标题栏去掉（SWT的Shell通过指定SWT.NO_TRIM样式实现）&#8220;+3是使菜单弹出的位置不至于遮挡组件边框线，因此偏移3像素为最佳位置&#8221;。调用setVisible显示菜单，不过前提条件是必须添加了菜单项。构造函数最后是一步是设置组件为可用，虽然任何SWT/Swing组件在构造时默认都是可用的，但是正如前面所述，重写setEnabled并不止设置是否被禁用，重要的是组件在两态下的外观，所以在构造函数最后添加setEnabled(true);</font></p>
<p><font face="宋体"><br />
以上讲述过多的是如何装饰组件的外观，接下来的重点将介绍如何用该组件缓存数据，使用MSN时候会发现，单击登录用户名的下拉按钮时候，会弹出所有在本机登录过的用户名列表（如果保存的话），下面讲述如何实现这一功能。</font></p>
<p><br />
我们的数据均保存在Vector这个集合中，由于实际应用变化万千，组件不可能知道实际保存何种类型的数据，因此Vector的元素类型统一设置为Object，这也实在是一个不错的设计，因为它不强制使用者去实现某某接口，或基类，假如设计成Vector中的元素必须是实现某一特定接口IElement，</p>
<p>private Vector dataSet = new Vector();</p>
<p>这样的话，使用者就必须将其POJO作更改，以适应于此组件，而Object作为所有类的基类，因此可容纳任何类型的数据。接下来的一步很重要，是将数据与菜单关联起来。定义如下方法public void loadMenuItems(Object[] objects)，顾名思义是一次性读取一组元素，完整的代码如下：</p>
<p>public void loadMenuItems(Object[] objects) {<br />
&nbsp;&nbsp;dataSet.clear();<br />
&nbsp;&nbsp;MenuItem[] items = selectorMenu.getItems();<br />
&nbsp;&nbsp;for (MenuItem item : items) {<br />
&nbsp;&nbsp;&nbsp;item.removeSelectionListener(this);<br />
&nbsp;&nbsp;&nbsp;item.dispose();<br />
&nbsp;&nbsp;}<br />
&nbsp;&nbsp;for (int i = 0; i &lt; objects.length; i++) {<br />
&nbsp;&nbsp;&nbsp;dataSet.add(objects[i]);<br />
&nbsp;&nbsp;&nbsp;MenuItem item = new MenuItem(selectorMenu, SWT.PUSH);<br />
&nbsp;&nbsp;&nbsp;item.setText(objects[i].toString());<br />
&nbsp;&nbsp;&nbsp;item.setData(objects[i]);<br />
&nbsp;&nbsp;&nbsp;item.addSelectionListener(this);<br />
&nbsp;&nbsp;}<br />
&nbsp;}</p>
<p>因为是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接口，然后添加下面两个方法：</p>
<p>public final void widgetDefaultSelected(SelectionEvent e)</p>
<p>public final void widgetSelected(SelectionEvent e)</p>
<p>其中widgetDefaultSelected在单击回车时触发，对文本框这样的组件适用，widgetSelected是鼠标单击时触发适用于按钮、菜单项。因此我们只处理widgetSelected。</p>
<p>public final void widgetSelected(SelectionEvent e) {<br />
&nbsp;&nbsp;MenuItem item = (MenuItem) e.getSource();<br />
&nbsp;&nbsp;selectedItem = item.getData();<br />
&nbsp;&nbsp;String text = item.getData().toString();<br />
&nbsp;&nbsp;inputText.setText(text);<br />
&nbsp;&nbsp;inputText.setSelection(0, text.length());<br />
&nbsp;&nbsp;selected(item.getData());<br />
&nbsp;}</p>
<p>首先取得事件源即单击的菜单项，然后更新selectedItem引用指向这个菜单项保存的数据（先前通过setData方法添加的），接下来的代码不作解释，很容易理解。值得注意的是最后一行selected(item.getData());作用是当用户选中菜单某一项时，根据当前选择的那个数据自动执行相应的操作，selected方法定义如下：</p>
<p>protected void selected(Object object) {<br />
};</p>
<p>与commit方法一样，是需要根据实际情况自定义处理逻辑的。</p>
<p>最后添加如下2个方法：</p>
<p>public void select(int index) {<br />
&nbsp;&nbsp;MenuItem[] items = selectorMenu.getItems();<br />
&nbsp;&nbsp;if (index &lt; 0 || index &gt;= items.length) {<br />
&nbsp;&nbsp;&nbsp;throw new ArrayIndexOutOfBoundsException(<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"the index value must between " + 0 + "and "<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ (items.length - 1));<br />
&nbsp;&nbsp;}<br />
&nbsp;&nbsp;selectedItem = items[index].getData();<br />
&nbsp;&nbsp;inputText.setText(items[index].getText());<br />
&nbsp;}</p>
<p>select用来设置当前选择第几个项，getSelectedItem返回当前用户选择的数据。</p>
<p><br />
到此为止，ComboSelector已经完成，可以作为API使用了，下面我们编写一个程序测试该组件。</p>
<p>首先编写一个POJO，如下：</p>
<p>package swt.custom;</p>
<p>public class Person {<br />
&nbsp;private String userName;</p>
<p>&nbsp;private String password;</p>
<p>&nbsp;public Person(String userName, String password) {<br />
&nbsp;&nbsp;this.userName = userName;<br />
&nbsp;&nbsp;this.password = password;<br />
&nbsp;}</p>
<p>&nbsp;public String getPassword() {<br />
&nbsp;&nbsp;return password;<br />
&nbsp;}</p>
<p>&nbsp;public String getUserName() {<br />
&nbsp;&nbsp;return userName;<br />
&nbsp;}</p>
<p>&nbsp;@Override<br />
&nbsp;public String toString() {<br />
&nbsp;&nbsp;return userName;<br />
&nbsp;}<br />
}</p>
<p>简单至极的一个类，注意它的toString方法，返回用户名属性作为显示。</p>
<p>接下来通过一个demo看看实际运行效果。<br />
用swt-designer工具创建一个Shell，在createContents方法体内添加如下代码：</p>
<p>final ComboSelector selector = new ComboSelector(this) {<br />
&nbsp;&nbsp;&nbsp;@Override<br />
&nbsp;&nbsp;&nbsp;protected void commit() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("current data is "<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ ((Person) getSelectedItem()).getUserName());<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;@Override<br />
&nbsp;&nbsp;&nbsp;protected void selected(Object object) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(((Person) object).getPassword());<br />
&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;};<br />
&nbsp;&nbsp;selector.setBounds(114, 78, 200, 20);<br />
&nbsp;&nbsp;Person[] persons = new Person[] {<br />
&nbsp;&nbsp;&nbsp;&nbsp;new Person("play_station3@sina.com", "111111"),<br />
&nbsp;&nbsp;&nbsp;&nbsp;new Person("rehte@hotmail.com", "222222"),<br />
&nbsp;&nbsp;&nbsp;&nbsp;new Person("yitong.liu@bea.com", "password"),<br />
&nbsp;&nbsp;&nbsp;&nbsp;new Person("<font face="宋体">使用其他Windows Live ID 登录</font>", "no") };<br />
&nbsp;&nbsp;selector.loadMenuItems(persons);<br />
&nbsp;&nbsp;selector.select(1);</p>
<p>运行结果如下：<br />
<img height="376" alt="" src="http://www.blogjava.net/images/blogjava_net/javagui/MSNDemo.jpg" width="501" border="0" /></p>
<p>&nbsp; 本程序的完整代码<a href="http://blogs.oracle.com/Swing/resource/MSN_Combox_Demo.zip">这里</a>下载</p><img src ="http://www.blogjava.net/javagui/aggbug/155266.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/javagui/" target="_blank">sun_java_studio@yahoo.com.cn(电玩)</a> 2007-10-23 13:33 <a href="http://www.blogjava.net/javagui/archive/2007/10/23/155266.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>