如何在SWT拖拽(DND)过程中延迟布局更新?
SWT拖拽创建控件时的布局延迟优化方案
问题背景
我正在开发类似Keynote的拖拽创建控件功能,用户可拖拽生成Button、Label等控件,简化实现代码如下:
Display display = Display.getDefault() ; Shell shell = new Shell( display ) ; shell.setSize( 500, 500 ) ; shell.setLayout( new FillLayout() ) ; Composite container = new Composite( shell, SWT.NONE ) ; GridLayoutFactory.swtDefaults().numColumns(2).applyTo( container ) ; Composite infoComp = new Composite( container, SWT.NONE ) ; GridLayoutFactory.swtDefaults().applyTo( infoComp ) ; Label info = new Label( infoComp, SWT.BORDER ) ; info.setText( "Create a Button" ) ; DragSource dragSource = new DragSource( info, DND.DROP_MOVE ) ; dragSource.setTransfer( new Transfer[] { TextTransfer.getInstance() } ) ; dragSource.addListener( DND.DragSetData, e -> { e.data = "Button" ; } ) ; info = new Label( infoComp, SWT.BORDER ) ; info.setText( "Create a Label" ) ; dragSource = new DragSource( info, DND.DROP_MOVE ) ; dragSource.setTransfer( new Transfer[] { TextTransfer.getInstance() } ) ; dragSource.addListener( DND.DragSetData, e -> { e.data = "Label" ; } ) ; Composite comp = new Composite( container, SWT.BORDER ) ; GridDataFactory.swtDefaults().grab(true, true).align(SWT.FILL, SWT.FILL).applyTo(comp); GridLayoutFactory.swtDefaults().applyTo( comp ) ; DropTarget dropTarget = new DropTarget( comp, DND.DROP_MOVE ) ; dropTarget.setTransfer( new Transfer[] { TextTransfer.getInstance() } ) ; dropTarget.addListener( DND.Drop, e -> { String data = (String) e.data ; switch (data) { case "Button" -> { Button button = new Button( comp, SWT.NONE ) ; button.setText( "Button" ) ; button.requestLayout() ; } case "Label" -> { Label label = new Label( comp, SWT.NONE ) ; label.setText( "Label" ) ; label.requestLayout() ; } } } ) ; shell.open() ; while ( !shell.isDisposed() ) { if ( !display.readAndDispatch() ) { display.sleep() ; } } display.dispose() ;
问题根源
SWT的DragSource在拖拽过程中会设置externalEventLoop标记为true:
private void drag(Event dragEvent) { ... String externalLoopKey = "org.eclipse.swt.internal.win32.externalEventLoop"; int result = COM.DRAGDROP_S_CANCEL; try { createCOMInterfaces(); display.setData(externalLoopKey, Boolean.TRUE); result = COM.DoDragDrop(iDataObject.getAddress(), iDropSource.getAddress(), operations, pdwEffect); } finally { display.setData(externalLoopKey, Boolean.FALSE); ... }
而Composite的layout方法中,只有当externalEventLoop为false且带有SWT.DEFER标记时,才会触发延迟布局:
public void layout (Control [] changed, int flags) { ... /** * display.externalEventLoop is true here */ if (!display.externalEventLoop && (flags & SWT.DEFER) != 0) { setLayoutDeferred (true); display.addLayoutDeferred (this); } for (int i=updateCount-1; i>=0; i--) { update [i].updateLayout (false); } ... }
拖拽触发Drop事件时,externalEventLoop仍为true,导致每次调用requestLayout()都会立即执行布局更新。实际业务中还需要同步更新右侧设置面板,多次布局会带来明显性能开销,需要实现正常的布局延迟处理。
解决方案
1. 异步批量触发布局
在Drop事件中先完成所有控件创建和设置更新,再通过Display.asyncExec()将布局操作放到拖拽外部的事件循环中执行——此时externalEventLoop已恢复为false,延迟布局机制会正常生效:
dropTarget.addListener(DND.Drop, e -> { String data = (String) e.data; // 先创建控件、更新设置,不立即触发布局 switch (data) { case "Button" -> { Button button = new Button(comp, SWT.NONE); button.setText("Button"); } case "Label" -> { Label label = new Label(comp, SWT.NONE); label.setText("Label"); } } updateSettingsPanel(); // 更新右侧设置区域内容 // 异步触发所有需要的布局更新 Display.getDefault().asyncExec(() -> { comp.requestLayout(); settingsComposite.requestLayout(); }); });
2. 手动启用布局延迟模式
调用Composite.setLayoutDeferred(true)开启延迟布局,完成所有操作后再关闭延迟模式,统一触发所有待处理的布局更新:
dropTarget.addListener(DND.Drop, e -> { try { // 开启目标区域和设置面板的延迟布局 comp.setLayoutDeferred(true); settingsComposite.setLayoutDeferred(true); String data = (String) e.data; switch (data) { case "Button" -> { Button button = new Button(comp, SWT.NONE); button.setText("Button"); button.requestLayout(); } case "Label" -> { Label label = new Label(comp, SWT.NONE); label.setText("Label"); label.requestLayout(); } } updateSettingsPanel(); } finally { // 关闭延迟布局,统一执行所有布局更新 comp.setLayoutDeferred(false); settingsComposite.setLayoutDeferred(false); } });
3. 临时修改externalEventLoop标记
在Drop事件处理期间,临时将externalEventLoop设为false,让原生延迟布局逻辑生效,处理完成后恢复原值:
dropTarget.addListener(DND.Drop, e -> { Display display = Display.getDefault(); String externalLoopKey = "org.eclipse.swt.internal.win32.externalEventLoop"; Boolean originalValue = (Boolean) display.getData(externalLoopKey); try { // 临时关闭externalEventLoop标记 display.setData(externalLoopKey, Boolean.FALSE); String data = (String) e.data; switch (data) { case "Button" -> { Button button = new Button(comp, SWT.NONE); button.setText("Button"); button.requestLayout(); } case "Label" -> { Label label = new Label(comp, SWT.NONE); label.setText("Label"); label.requestLayout(); } } updateSettingsPanel(); } finally { // 恢复原始标记值,避免影响后续拖拽逻辑 display.setData(externalLoopKey, originalValue); } });
方案对比
- 方法1最安全,完全依托SWT原生事件循环机制,无版本兼容性风险,适合大多数场景。
- 方法2是SWT官方推荐的批量布局方式,可控性强,适合多组件联动更新的复杂场景。
- 方法3依赖SWT私有实现细节,存在版本适配风险,仅在其他方案无法满足需求时使用。
内容的提问来源于stack exchange,提问作者Bourbon_7
相关产品推荐
相关产品推荐

