前几天在做对话框界面过程中,对行文本框中的输入数值需要进行验证,于是对Text组件做了扩展,做了一个可以验证输入的字符串是否为数值的NumText组件,参见 《java SWT:限制数值输入的Text文本框通用组件》,但是在使用过程中发现,这种方式有缺陷,就是如果用户输入非法的字符,结果就是输不进去,界面上不会有报错也不会有任何提示,用户体验不好。
之前对databinding有过简单的了解,知道它可以实现UI组件和用户数据之前的同步更新以及数据类型转换和验证,但是觉着它太复杂,我的应用似乎用不上,所以一直没有进一步深入了解。发现自己设计的NumText组件用户体验存在问题后,才下决心对jface databinding做深入的了解。
如果你已经知道怎么进行数据绑定可以跳过本节
之前看过一些关于jface databinding方面的文章,文章都很长,代码好多,感觉好复杂的样子,一下子把我吓住了,尼玛,这jface databinding本是要简化代码设计的,要是需要写更多更复杂代码,还不累死人呀。
后来发现,在WindowBuilder下数据绑定操作已经可以像UI设计一样在UI界面下点点鼠标就能生成代码了,大大降低了学习门槛,简化了代码编写工作量。
使用WindowBuilder对进行数据绑定(databinding)比较直观方便,可以帮助我们自动生成一些必要的代码。
我们以一个Text文本框为例,来说明如果将一个文本框的内容与一个POJO对象中的属性进行绑定。
如下图,一个简单对话框中有一个Text文本框,
鼠标右键点击文本框,选择绑定功能(Bindings),然后选择Text的text属性,也就是保存Text文本框文本内容的属性。
然后会出现这样的界面,让我们选择要绑定的数据对象,在本例中数据对象的类型是TestBinding.Configurtion,变量名是editorConfig,要绑定的属性是globalAspectRatio(看这个变量名,你应该能猜到这是个浮点型数据,这就引出了后面的数据类型转换)
然后会显示绑定的细节属性,这里我们都使用默认值,所以点击确定就好了
这样一个简单的数据绑定就完成了。
如果数据挷定的两个对象属性的类型是一样,那上面的工作就算完成了。
但如果类型不同,就需要涉及到数据类型转换和数据验证的技术了。
jface databinding提供了两个基本的接口用于数据类型转换和数据验证
分别是IConverter和IValidator。
对于基本的数据类型转换,jface已经提供了IConverter接口的实现:
StringToNumberConverter类用于将String转换成数值(Float,Double,Integer,Long,BigDecimal…)
NumberToStringConverter类用于将数值转换成String
而IValidator则需要根据实际需求自己来写。
下面的代码实现了Float类型的属性与Text组件之间的数据绑定,这其中用到了StringToNumberConverter进行数据类型转换。同时用IValidator实现数据的合法性验证。
TestBinding.java
package testwb;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Text;
import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.core.databinding.beans.PojoProperties;
import org.eclipse.core.databinding.conversion.StringToNumberConverter;
import org.eclipse.swt.widgets.Display;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;
import org.eclipse.jface.databinding.swt.DisplayRealm;
public class TestBinding extends Dialog {
/** * 数据对象定义 * @author guyadong * */
public class Configuration {
private Float globalAspectRatio;
public Float getGlobalAspectRatio() {
return globalAspectRatio;
}
public void setGlobalAspectRatio(Float globalAspectRatio) {
this.globalAspectRatio = globalAspectRatio;
System.out.printf("updated %f\n",globalAspectRatio);
}
public Configuration(Float globalAspectRatio) {
super();
this.globalAspectRatio = globalAspectRatio;
}
}
private DataBindingContext m_bindingContext;
/** * 成员变量:数据对象 */
protected Configuration editorConfig=new Configuration(0.5f);
/** * Text组件 */
private Text globalAspectRatioValue;
/** * Create the dialog. * @param parentShell */
public TestBinding(Shell parentShell) {
super(parentShell);
}
/** * Create contents of the dialog. * @param parent */
@Override
protected Control createDialogArea(Composite parent) {
Composite container = (Composite) super.createDialogArea(parent);
container.setLayout(null);
globalAspectRatioValue = new Text(container, SWT.BORDER);
globalAspectRatioValue.setBounds(38, 28, 73, 23);
return container;
}
/** * Create contents of the button bar. * @param parent */
@Override
protected void createButtonsForButtonBar(Composite parent) {
createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
m_bindingContext = initDataBindings();
}
/** * Return the initial size of the dialog. */
@Override
protected Point getInitialSize() {
return new Point(362, 298);
}
@Override
protected void configureShell(Shell newShell) {
newShell.setText("设置");
super.configureShell(newShell);
}
public static void main(String[] args) {
Display display = Display.getDefault();
Realm.runWithDefault(DisplayRealm.getRealm(display), new Runnable() {
public void run() {
try {
TestBinding setting = new TestBinding(null);
setting.open();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
protected DataBindingContext initDataBindings() {
DataBindingContext bindingContext = new DataBindingContext();
IObservableValue observeTextGlobalAspectRatioValueObserveWidget = WidgetProperties.text(SWT.Modify).observe(globalAspectRatioValue);
IObservableValue globalAspectRatioEditorConfigObserveValue = PojoProperties.value("globalAspectRatio").observe(editorConfig);
// 创建String 到 Float的转换器
StringToNumberConverter converter = StringToNumberConverter.toFloat(false);
// 更新策略对象(Text内容改变时更新)
UpdateValueStrategy updateStrategy = new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE).setConverter(converter);
// 设置Set方法验证器(Set方法修改Text文本内容之前验证)
// Lambda表达式实现,验证失败返回错误信息
updateStrategy.setBeforeSetValidator((value) -> {
// 这里value的类型是数据对象的属性类型 (Float)
if ((Float)value<100) {
return ValidationStatus.ok();
}
return ValidationStatus.error("globalAspectRatio must <100");
});
// 设置Get方法验证器(Get方法获取Text文本内容之后验证)
// 传统匿名类实现,验证失败返回错误信息
updateStrategy.setAfterGetValidator(new IValidator() {
@Override
public IStatus validate(Object value) {
// 这里value的类型是Text对象的text属性类型(String)
boolean ok=false;
try{
Float s = (Float) converter.convert(value);
if (s<100) {
ok=true;
return ValidationStatus.ok();
}
return ValidationStatus.error("globalAspectRatio must <100");
}catch(Exception e){
return ValidationStatus.error("无效数字");
}finally{
// 数据无效时 disable OK按钮
getButton(IDialogConstants.OK_ID).setEnabled(ok);
}}
});
// 调用bindValue完成Text.text到数据对象的globalAspectRatio属性的绑定
Binding bindValue = bindingContext.bindValue(observeTextGlobalAspectRatioValueObserveWidget, globalAspectRatioEditorConfigObserveValue,
updateStrategy,
null);
// 创建错误提示组件,当验证失败时显示提示信息
ControlDecorationSupport.create(bindValue, SWT.TOP | SWT.LEFT);
return bindingContext;
}
}
如下图运行程序,当输入无效字符时,Text左上角会显示出错的红X,鼠标移动到X上会显示我们的验证器(IValidator)返回的错误信息。
这是由这行代码ControlDecorationSupport.create(bindValue, SWT.TOP | SWT.LEFT);
创建的ControlDecorationSupport对象实现的
当验证失败,数据对象的绑定属性不会被更新。
说明:
本例中只是实现了Text组件向Configuration类的Float类型属性的单向数据同步。事实上jface databinding可以实现双向数据同步。
参考资料:
《AJFace Data Binding - Tutorial》
《JFace Data Binding》