今天必须学习的内容

来源:互联网 发布:淘宝达人平台登录 编辑:程序博客网 时间:2024/05/04 06:32
 

来源:Java Desktop Technology - BlogJava

  SWT虽然没有Swing那么强大,尤其是在打造专业外观上,不支持L&F,但是通过自定义组件,同样可以达到用户要求。下面就向大家介绍本人实现的一个具备专业外观的Slider控件。

  首先来参考一下组件的实际运行效果,并和SWT原生组件进行一下对比。

  可以看出,经过自定义的组件在外观上要比SWT直接调用本地组件强得多。当用户托拽滑动块时,还会出现一个虚拟的滑动块用来标识将要移动到的位置。演示就到此为止,下面详细介绍这个很Cool的组件是如何通过SWT实现的。

   基本设计思想:与其他自定义组件一样,是通过继承org.eclipse.swt.widgets.Composite来实现,定义该类为 Slider,另外滑动块(thumb)也是Composite,并放在Slider之上,当鼠标移动thumb时,调用setBounds方法定位在 Slider在父组件(Slider)上的位置,从而达到拖拽thumb的目的。此外通过实现PaintListener接口进行自定义绘制,绘制的对象 包括组件边框、被填充的格子、未被填充的格子、虚拟滑块。

接触过GUI编程的程序员都应该知道像Scroll、Slider、ProgressBar这样的控件都有setMaxValue、 setMinValue、setValue这样的方法,除了鼠标拖拽thumb来改变当前数值外,可直接调用setValue来设置当前值。此外这些控件 还有水平(Horizontal)、垂直(Vertical)两种布局,对于事件处理一般都要有一个从java.util.EventObject继承而 来的事件类,还要编写事件监听器(Listener)接口,因此在开始编写Slider控件之前先定义3个类,代码都不是很长,如果你熟悉AWT、 Swing的事件处理机制,相信你能轻松跳过。

public enum SliderOrientation {
HORIZONTAL, VERTICAL;
}

public class SliderEvent extends EventObject {

private int value;

public SliderEvent(Object source, int value) {
super(source);
this.value = value;
}

public int getValue() {
return value;
}
}

public interface SliderListener {

public void valueChanged(SliderEvent event);
}

   接下来着重介绍Slider。首先是继承Composite并实现ControlListener、PaintListener、 MouseListener,、MouseMoveListener,、MouseTrackListener,然后自动生成接口方法代码,通过 Eclipse可以轻松实现,需要注意的是MouseListener,有java.awt.event.MouseListener和 org.eclipse.swt.events.MouseListener两种,不要混淆,否则错误很难找到。然后是要采集一些数据信息,分别是:边框 颜色、已有数据部分的填充颜色(上图中组件的绿色部分)、未达到数据部分的填充颜色(上图中组件的白色部分)、被禁用时的填充颜色、水平滑块的图标(正 常、托拽中两种)、垂直滑块图标(正常、托拽中两种)、水平、垂直虚拟滑块图标。以上这些数据对应的常量声明如下:

private final Color BORDER_COLOR = new Color(Display.getCurrent(), 180, 188, 203);

private final Color FILL_COLOR = new Color(Display.getCurrent(), 147, 217, 72);

private final Color BLANK_COLOR = new Color(Display.getCurrent(), 254, 254, 254);

private final Color DISABLE_COLOR = new Color(Display.getCurrent(), 192, 192, 192);

private final Image THUMB_ICON_V = new Image(Display.getDefault(), "slider_up_v.png");

private final Image THUMB_OVER_ICON_V = new Image(Display.getDefault(), "slider_over_v.png");

private final Image THUMB_ICON_H = new Image(Display.getDefault(), "slider_up_h.png");

private final Image THUMB_OVER_ICON_H = new Image(Display.getDefault(), "slider_over_h.png");

private final Image TEMP_H = new Image(Display.getDefault(), "temp_h.png");

private final Image TEMP_V = new Image(Display.getDefault(), "temp_v.png");

  除了这些常量,还应该声明默认最大值的常量,private final int DEFAULT_MAX_VALUE = 100;
  接下来定义当前数值和最大值,

private int value;
private int maxValue = DEFAULT_MAX_VALUE;

  并生成以上两个成员属性的get方法
  然后定义滑动块和布局

private SliderOrientation orientation;
private Composite thumb;

  要处理数值变化,需要实现一组监听器,添加如下代码

private List<SliderListener> listeners = new ArrayList<SliderListener>();
public void addSliderListener(SliderListener sliderListener) {
listeners.add(sliderListener);
}
public void removeSliderListener(SliderListener sliderListener) {
listeners.remove(sliderListener);
}

   接下来定义2个辅助方法,实现value<->pelsLength转换。其中value是当前的数值,由具体业务来决定,下文中称其业务值。例如一个 音量控制器,音量范围在0~500,那么从业务上来讲可以将数值设置在0~500之间的任何数,而pelsLength则由控件的长/高度来决定,单位是 像素。但是value与pelsLength之间存在着一个比例关系式:value/maxValue=pelsLength/控件长度或高度。这样不难 得出两个函数的定义。

private int valueToPels(int value) {
float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width
: getBounds().height;
return (int) (widgetLength * (float) value / (float) maxValue);
}

private int pelsToValue(int pels) {
float widgetLength = (orientation == SliderOrientation.HORIZONTAL) ? getBounds().width
: getBounds().height;
return (int) ((float) pels * (float) maxValue / (float) widgetLength);
}

  最后定义构造器。代码如下

public Slider(Composite parent, SliderOrientation orientation) {
super(parent, SWT.FLAT);
this.orientation = orientation;
thumb = new Composite(this, SWT.FLAT);
thumb
.setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V
: THUMB_ICON_H);
addControlListener(this);
addPaintListener(this);
thumb.addMouseListener(this);
thumb.addMouseMoveListener(this);
thumb.addMouseTrackListener(this);
}

  在构造器中,注入布局对象,然后在控件上创建滑动块组件thumb,并添加鼠标处理等。
  到此为止,基本的成员和方法的定义完毕,下面循序渐进讨论如何实现这一Slider。

  一、绘制边框
  由于是绘制操作,所以一切绘制代码均在paintControl方法内实现,先将如下代码拷贝到paintControl内

int w = getBounds().width;
int h = getBounds().height;
int fillLength = valueToPels(getValue());
GC gc = e.gc;
switch (orientation) {
case HORIZONTAL:
break;
case VERTICAL:
break;
}

  分析如下
  首先获取控件的长度与高度,因为接下来的绘制要经常用到这两个变量。
   “int fillLength = valueToPels(getValue()); ”这一行代码稍后作解释,然后是获得绘制上下文对象,下一步是根据布局不同采用不同的处理,除了paintControl函数,在其他很多地方都对布局进 行判断,但是简单起见,只对水平布局进行介绍,垂直部分参考完整程序。
接下来的绘制操作均在case HORIZONTAL中进行,首先将颜色设置为边框的颜色
gc.setForeground(BORDER_COLOR); 然后绘制一个矩形gc.drawRectangle(0, 2, w - 1, h - 5);
关于为什么要偏移2像素、长度为什么减1、高度为什么减5,请参考有关绘图的基本知识,上一篇也有简单的介绍。

  现在,你就可以编写测试程序来验证结果了,看看边框是否与示例的效果一样。

  二、托拽thumb的实现

  桌面GUI编程领域技术深浅的度量衡通常有4项指标:皮肤(外观,swing组件体系称其L&F)、绘图、自定义组件布局(Layout)、自定义组件。而托拽是实现自定义组件和绘图不可或缺的技术,也是难点之一,因此掌握的深浅是衡量桌面编程水平的标志。
虽 然作为难点,但是也有章可循,其基本实现简单到只监听鼠标事件这么简单,基本流程是:当鼠标在thumb上按下时,记住这个位置,然后按住鼠标左键托拽, 最后松开鼠标计算两个位置之间的距离(位移),根据位移量移动thumb的位置并换算出等价的value增量(可能为负值)进行业务逻辑处理。下面通过代 码循序渐进完成。

  定义一个位置变量用来存储鼠标单击的位置,private Point controlPoint; 然后实现public void mouseDown(MouseEvent e)和public void mouseUp(MouseEvent e)两个方法。
public void mouseDown(MouseEvent e) {
controlPoint = new Point(e.x, e.y);
thumb
.setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_OVER_ICON_V
: THUMB_OVER_ICON_H);
}

public void mouseUp(MouseEvent e) {
try {
thumb
.setBackgroundImage(orientation == SliderOrientation.VERTICAL ? THUMB_ICON_V
: THUMB_ICON_H);
countValue(e);
} finally {
controlPoint = null;
}
}

   “controlPoint = new Point(e.x, e.y); ”这一行实现记住鼠标点的位置,注意,这个位置是相对滑块thumb的,因为是thumb监听的鼠标事件。接下来是设置滑动块背景,不难理解当鼠标松开 时,应该将背景恢复。然后进行非常重要的换算工作,通过countValue方法实现,最后务必要把鼠标位置清空,用try-finally是有必要这样 的。之所以要在方法结束的时候清空controlPoint,是因为在鼠标移动的时候需要对controlPoint进行更加复杂的计算。稍后讲解 mouseMove实现的时候再作解释,接下来着重分析countValue方法。
如前所述,countValue完成计算鼠标按下、松开的位移量,换算成与业务相关的数据(value)。

private void countValue(MouseEvent e) {
switch (orientation) {
case HORIZONTAL:
int movedX = e.x - controlPoint.x;
setValue(getValue() + pelsToValue(movedX));
break;
case VERTICAL:
......
}
}

   “int movedX = e.x - controlPoint.x; ”实现了位移量的计算,保存到movedX,调用pelsToValue方法将movedX转换成业务值的增量,然后调用setValue重新赋值,注意 pelsToValue得到的是增量,需要与原值(getValue()得到)叠加。接下来分析setValue方法。

public void setValue(int value) {
if (value < 0) {
this.value = 0;
} else if (value > getMaxValue()) {
this.value = getMaxValue();
} else {
this.value = value;
}
try {
moveThumb();
redraw();
} finally {
for (SliderListener listener : listeners) {
try {
SliderEvent event = new SliderEvent(this, getValue());
listener.valueChanged(event);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

   方法开始处的一系列if语句对value进行验证后再赋值,然后是调用moveThumb实现滑块移动、调用redraw对组件重新绘制,最后是处理具 体的业务,可以看出处理业务是setValue方法的关键,所以要用try-finally,谁也不敢确保moveThumb、redraw不出问题。对 于实现业务是遍历监听器列表然后执行每个监听器的valueChanged方法,这种事件源-监听器模型也是Java2以后的GUI事件实现模型。

private void moveThumb() {
Image icon = thumb.getBackgroundImage();
int iconw = (icon != null) ? icon.getBounds().width : 0;
int iconh = (icon != null) ? icon.getBounds().height : 0;
switch (orientation) {
case HORIZONTAL:
int x = valueToPels(getValue()) - iconw / 2;
if (x < 0) {
x = 0;
} else if (x > getBounds().width - iconw) {
x = getBounds().width - iconw;
}
thumb.setBounds(x, 0, iconw, iconh);
break;
case VERTICAL:
...... }
}

  不难理解,moveThumb的任务就是根据业务值value来将滑块移动到正确的位置。
  以上代码声明滑块的位置是“x”,通过转换函数获得,但是还要减去滑块的一半,因为具体坐标应该落到滑动块的中间,仔细想想不难得出。if-else是对x进行验证,最后通过setBound来定位thumb。
  对于redraw,他的作用是触发paintControl方法进行重绘,因为重绘出了边框还要根据value绘制填充格子,而value已经在redraw方法调用前被赋了值,所以这时候应该进行重绘。
现在你可以托拽thumb了,美中不足的是滑动块不能随时跟随鼠标的轨迹移动,这个稍后会实现。

  三、填充格子

   现在的组件外观只是绘制了边框,现在进行格子填充。在讲述边框绘制的时候提到了一行代码“int fillLength = valueToPels(getValue()); ”现在不难理解吧,就是将当前业务值value转换成实际的长度。在绘制边框之后,加入如下代码:
if (getEnabled()) {
gc.setBackground(FILL_COLOR);
for (int i = 2; i < w - 2; i += 4) {
if (i > fillLength) {
gc.setBackground(BLANK_COLOR);
}
gc.fillRectangle(i, 4, 3, h - 8);
}
} else {
gc.setBackground(DISABLE_COLOR);
gc.fillRectangle(1, 4, w - 1, h - 8);
}

   首先判断是否是enable,然后设置填充颜色FILL_COLOR,然后在for循环中执行正方形格子的填充,递增量“i”从2开始是空开2像素间 隔,同理i也不能超过w-2(对称性),i+=4是相邻两个格子左边坐标间距4像素,然后“gc.fillRectangle(i, 4, 3, h - 8); ”这一行进行填充绘制正方形。留意,x坐标是i,y坐标是从4开始画的,出于对称高度也要“h-8”,长度之所以是“3”是保持相邻两个格子之间保持1像 素的间隔(想想“i+=4”就不难得出答案)。此外还要对fillLength进行判断以便决定颜色采用绿色还是白色以示区分。对于绘图操作来说,千万不 要埋怨考虑细节过多,事实上,GUI编程过程中“坐标系”这个概念是需要经常被考虑的,试想如果上述代码for循环中把“i+=4”,写成“i+=5”, 只是一个像素之差绘制效果差之千里,如果想知道笔者是如何得到这些坐标数据的,实话说,是靠多次调试得出的结果。

  现在你运行程序,应该能根据value进行格子填充了。到此为止,绝大多数的功能已经实现,但是人性化的界面设计应该在托拽时出现一个虚拟的滑动块用来标识将要移动到的位置。
原创粉丝点击