单元格编辑器的需求出现
最近我需要自己做一个小的应用程序,使用SWT来设计界面,其中需要用到JFace的TableViewer,在使用之后,发现它比Swing的JTable更好用,而且在向表格的单元格中加入编辑器的时候,更加方便、容易(也有可能是我对Swing的JTable中的单元格编辑器研究的不够造成的错觉……)
我需要向TableViewer生成的对象的单元格中加入编辑日期的编辑器(如图1所示)
图1 单元格中的日期编辑器
在网上利用关键字“JFace CellEditor”等等组合,可以容易地搜索到很多关于JFace界面组件中CellEditor这个类的覆写方法,但是,我查找了几天,也只是找到了一些对我没有用处的文章。
从8月29日到9月1日,用了四天的时候,终于很好的解决了这个编辑器的问题,所以现在用代码来小结一下收获:
自定义的日期单元格编辑器的代码
实现这个单元格编辑器的代码如下:
注:请快速浏览代码之后,详细的了解后面关于代码的解释
/**
* Updated at 2007-8-30 下午04:31:44
* <p>
*
* @author 郭凯
*
*/
public class DatepickerCellEditor extends CellEditor {
public static Logger log = Logger.getLogger(DatepickerCellEditor.class);
private DatePickerCombo datepickerWidget;
protected Date selectedDate;
public DatepickerCellEditor() {
}
public DatepickerCellEditor(Composite parent, int style) {
super(parent, style);
}
protected Control createControl(Composite parent) {
datepickerWidget = new DatePickerCombo(parent, getStyle());
datepickerWidget.getDp().addSelectionListener(new SelectionAdapter() {
public void widgetDefaultSelected(SelectionEvent event) {
applyEditorValueAndDeactivate();
}
public void widgetSelected(SelectionEvent event) {
selectedDate = datepickerWidget.getDate();
}
});
datepickerWidget.addTraverseListener(new TraverseListener() {
public void keyTraversed(TraverseEvent e) {
if (e.detail == SWT.TRAVERSE_ESCAPE
|| e.detail == SWT.TRAVERSE_RETURN) {
e.doit = false;
}
}
});
return datepickerWidget;
}
protected void applyEditorValueAndDeactivate() {
selectedDate = datepickerWidget.getDate();
markDirty();
fireApplyEditorValue();
deactivate();
}
protected Object doGetValue() {
focusLost(); // 在获取该单元格编辑器的值的时候,也应该令该单元格编辑器失去焦点
return Constants.format(selectedDate);
}
protected void doSetFocus() {
datepickerWidget.getDp().setFocus();
}
protected void doSetValue(Object value) {
Assert.isTrue(datepickerWidget != null && (value instanceof String));
selectedDate = Constants.parse((String) value);
datepickerWidget.setDate(selectedDate);
}
protected void focusLost() {
System.out.println("focusLost");
if (isActivated()) {
applyEditorValueAndDeactivate();
}
}
public Date getSelectedDate() {
return datepickerWidget.getDate();
}
public void setSelectedDate(Date date) {
datepickerWidget.setDate(date);
}
}
首先,得关注几个与其他CellEditor的实现不一样的地方
1. 在createControl()方法中,我们没有对datepickerCombo或者datepickerCombo中的属性dp添加焦点监听器(代码中addFocusListener这部分被注释掉了)。
2. doGetValue()方法中,我们让当前的CellEditor失去了焦点(这里可以参考ComboBoxCellEditor的代码,不难发现它的doGetValue()方法是不处理焦点的)
这段代码依旧不完美,因为doGetValue()方法中如果让CellEditor失去焦点的话,还会出现问题——因为doGetValue()调用的时机是在CellEditor的值发生改变的时候,即是说每次我们打开日期CellEditor,修改一次日期,就会丢失该单元格编辑器的焦点,那样的结果使得用户不得不点击一下其他的单元格,再点回到这个单元格,重新选择其他日期(假定他第一次没有选对的话)。这就意味着这里的实现很ugly!
该怎么办呢?重新分析~~
我们的最初的问题是这样的
来自http://sourceforge.net/projects/swt-datepicker的SWT的日期控件是相当不错的,当然自己用的时候,还是需要修改相关代码。
该控件如图2所示,分为三个部分——一个编辑框、一个按钮、一个画有日历的面板
图2 SWT-Datepicker控件截图
根据源代码,我们了解到他们各自的事件控制分别在textEvent()、arrowEvent()、dpEvent(),辅助的事件控制在popupEvent()、comboEvent()之中
直接使用没有修改过的DatepickerCombo到我们的DatepickerCellEditor当中的时候,会出现焦点无法捕获的问题。我们在DatepickerCombo类的构造器中,可以找到一段定义Listener接口对象的代码
Listener listener = new Listener() {
public void handleEvent(Event event) {
log.info(event);
if (popup == event.widget) {
popupEvent(event);
return;
}
……
}
};
我们在handleEVent()方法的第一行加上log,而后看看这些事件信息。
结果使用未修改的代码的时候,我们看到当日历面板被箭头按钮弹出的时候,它获得焦点后马上就丢失了焦点。
………………………………(这里省略了思路出现问题绕弯子的过程)
最终我们关注到了一个已有的实现类ComboBoxCellEditor类的代码,发现这个类里面使用的是CCombo,而CCombo的代码与DatepickerCombo之间存在着惊人的相似。可是在使用ComboBoxCellEditor的时候却不会出现焦点方面的问题。
仔细查看了代码之后,发现CCombo与DatepickerCombo最大的不同就在于焦点处理的代码:
DatepickerCombo类是将FocusIn和FocusOut两个事件的代码放到textEvent()、dpEvent()、arrowEvent()等几个事件处理方法之中,而CCombo则将焦点处理集中交给handleFocus()方法,其代码如下:
void handleFocus (int type) {
if (isDisposed ()) return;
switch (type) {
case SWT.FocusIn: {
if (hasFocus) return;
if (getEditable ()) text.selectAll ();
hasFocus = true;
Shell shell = getShell ();
shell.removeListener (SWT.Deactivate, listener);
shell.addListener (SWT.Deactivate, listener);
Display display = getDisplay ();
display.removeFilter (SWT.FocusIn, filter);
display.addFilter (SWT.FocusIn, filter);
Event e = new Event ();
notifyListeners (SWT.FocusIn, e);
break;
}
case SWT.FocusOut: {
if (!hasFocus) return;
Control focusControl = getDisplay ().getFocusControl ();
if (focusControl == arrow || focusControl == list || focusControl == text) return;
hasFocus = false;
Shell shell = getShell ();
shell.removeListener(SWT.Deactivate, listener);
Display display = getDisplay ();
display.removeFilter (SWT.FocusIn, filter);
Event e = new Event ();
notifyListeners (SWT.FocusOut, e);
break;
}
}
}
关键代码以蓝低白字显示。而CCombo类中的一个重要的属性对象的filter的声明代码则放在CCombo的构造器中,其代码如下:
filter = new Listener() {
public void handleEvent(Event event) {
Shell shell = ((Control)event.widget).getShell ();
if (shell == CCombo.this.getShell ()) {
handleFocus (SWT.FocusOut);
}
}
};
这个监听器对象意味着,shell的任何响应事件将会引发FocusOut事件处理代码的执行,这也使得FocusIn和FocusOut事件处理代码中都应该及时的清除filter监听器,否则可能导致GUI直接当在事件的响应死循环中。(另外我们可以看到SWT文档中声明了filter的对性能可能造成的影响,所以在使用这种可能影响性能的工具的时候,还是应该多多参考Eclipse附带的源代码)
最终DatepickerCellEditor类的源代码为:
/**
* Updated at 2007-8-30 下午04:31:44
* <p>
*
* @author 郭凯
*
*/
public class DatepickerCellEditor extends CellEditor {
public static Logger log = Logger.getLogger(DatepickerCellEditor.class);
private DatePickerCombo datepickerWidget;
protected Date selectedDate;
public DatepickerCellEditor() {
}
public DatepickerCellEditor(Composite parent, int style) {
super(parent, style);
}
protected Control createControl(Composite parent) {
datepickerWidget = new DatePickerCombo(parent, getStyle());
datepickerWidget.getDp().addSelectionListener(new SelectionAdapter() {
public void widgetDefaultSelected(SelectionEvent event) {
applyEditorValueAndDeactivate();
}
public void widgetSelected(SelectionEvent event) {
selectedDate = datepickerWidget.getDate();
}
});
datepickerWidget.addTraverseListener(new TraverseListener() {
public void keyTraversed(TraverseEvent e) {
if (e.detail == SWT.TRAVERSE_ESCAPE
|| e.detail == SWT.TRAVERSE_RETURN) {
e.doit = false;
}
}
});
datepickerWidget.addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent e) {
log.info("datepickerWidget focus lost caused by :\n" + e);
DatepickerCellEditor.this.focusLost();
}
});
return datepickerWidget;
}
protected void applyEditorValueAndDeactivate() {
selectedDate = datepickerWidget.getDate();
markDirty();
fireApplyEditorValue();
deactivate();
}
protected Object doGetValue() {
return Constants.format(selectedDate);
}
protected void doSetFocus() {
datepickerWidget.setFocus();
}
protected void doSetValue(Object value) {
Assert.isTrue(datepickerWidget != null && (value instanceof String));
selectedDate = Constants.parse((String) value);
datepickerWidget.setDate(selectedDate);
}
……
}
小结:参考Eclipse官方的源代码,仍然是学习的重要手段,加上多看示例代码,这两种方法可以大大减少重复劳动。