SWT 对话框显示不正确
SWT Dialog does not display correctly
打开新对话框时,在加载时,您在父对话框 shell 上单击几次,显然新对话框无法正确显示。
请看下面的例子:
例子
- https://i.stack.imgur.com/ZovxE.png(日食IDE例子)
- https://i.stack.imgur.com/5zVar.png
- https://i.stack.imgur.com/u86b9.png
- https://i.stack.imgur.com/FGaAr.png
最初我在 2014 年 12 月遇到了这个问题,当时使用不同开发系统的内部开发人员也报告了同样的问题,然后我们的几个客户报告了同样的问题。
可以使用以下环境重现此行为:
- Windows Version: 7 Pro 64 Bit - 6.1.7601
- Java Version: RE 1.8.0_121_b13
- SWT Versions
- 3.8.2
- 4.6.2
- 4.7M6
- I20170319-2000
我只能在 Windows 7 上使用 windows 基本 theme/design/style(不是经典或 aero)重现该问题。
在 windows 10 上,它不可重现。
重现
重现代码
package test;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
public class Main {
public static void main(String[] args) {
Display display = new Display();
final Shell shell = createShell(display);
createButton(shell);
shell.open();
eventLoop(display, shell);
display.dispose();
}
private static Shell createShell(Display display) {
final Shell shell = new Shell(display);
shell.setLayout(new RowLayout());
shell.setSize(500, 200);
return shell;
}
private static void createButton(final Shell shell) {
final Button openDialog = new Button(shell, SWT.PUSH);
openDialog.setText("Click here to open Dialog ...");
openDialog.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
TestDialog inputDialog = new TestDialog(shell);
inputDialog.open();
}
});
}
private static void eventLoop(Display display, final Shell shell) {
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
}
class TestDialog extends Dialog {
public TestDialog(Shell parent) {
super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.MIN | SWT.MAX | SWT.RESIZE);
setText("Dialog");
}
public void open() {
Shell shell = new Shell(getParent(), getStyle());
shell.setText(getText());
createContents(shell);
shell.pack();
initializeBounds(shell);
shell.open();
eventLoop(shell);
}
private void createContents(final Shell shell) {
shell.setLayout(new GridLayout(2, true));
Label label = new Label(shell, SWT.NONE);
label.setText("Some Label text ...");
final Text text = new Text(shell, SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
text.setLayoutData(data);
createCloseButton(shell);
/* time for the user to create the misbehavior */
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void createCloseButton(final Shell shell) {
Button closeButton = new Button(shell, SWT.PUSH);
closeButton.setText("Close");
GridData data = new GridData(GridData.FILL_HORIZONTAL);
closeButton.setLayoutData(data);
closeButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
shell.close();
}
});
shell.setDefaultButton(closeButton);
}
private void initializeBounds(Shell shell) {
Rectangle bounds = shell.getBounds();
Rectangle parentBounds = getParent().getBounds();
bounds.x = parentBounds.x;
bounds.y = parentBounds.y;
shell.setBounds(bounds);
}
private void eventLoop(Shell shell) {
Display display = getParent().getDisplay();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
}
重现步骤
- 启动应用程序
- 它应该看起来像:https://i.stack.imgur.com/dMJ9e.png
- 点击按钮。
- 不断点击父级shell的右下角(避免点击新打开的对话框),直到鼠标光标变为等待图标并且父级shell改变颜色。
- 它应该如下所示:https://i.stack.imgur.com/c1Ikp.png
- 等到新对话框出现。
- 它看起来像下面这样:https://i.stack.imgur.com/kTDgQ.png(显示不正确)
- 改为:https://i.stack.imgur.com/cHVjn.png(正确显示)
在视频中完成的重现步骤
当您将鼠标悬停在某些 UI 元素(最初未正确绘制)时,您会注意到其中一些被绘制(例如 table 行)。
- https://i.stack.imgur.com/kkMKn.png(打开对话框之前)
- https://i.stack.imgur.com/ZXIKc.png(打开对话框后)
- https://i.stack.imgur.com/25M7S.jpg(鼠标悬停后)
即使在对话框打开后调用 Shell.update()
或 Shell.redraw()
也无法修复它。
在 Windows 性能选项 -> 视觉效果 -> 禁用 "Use visual styles on windows and buttons" 是我找到的唯一提供解决方法的选项,
这似乎与将 design/theme/style 更改为经典相同。
最后,我有以下问题:
是 SWT 还是 Windows 问题?
Windows 或 Eclipse Bugzilla 的错误条目中是否有任何相关主题?
还有其他人遇到过同样的问题吗?请分享经验。
SWT 或 Windows 中是否有任何设置会影响其外观并解决问题?
代码似乎很简单,只是您让主线程休眠 15 秒,因此延迟。如果不需要,请取消睡眠或将睡眠时间减少到 5 秒左右。
In the end, I have following questions: Is it a SWT or Windows problem?
都没有。正如其他人所提到的,您当然不应该将 UI 线程与任何长 运行 任务捆绑在一起。该工作属于后台线程。
关于使用后台线程,有几种方法可以解决这个问题,具体取决于您希望 Dialog
的行为方式。
一种选择是启动后台线程,然后在任务完成后打开对话框。我个人并不关心这个,因为当任务是 运行 时,用户可能会认为什么都没有发生。
另一种选择是打开对话框但显示一条 "Loading" 消息,或类似的东西以提供有意义的反馈并让用户知道应用程序未被冻结(比如它如何 looks/responds 在你的例子中)。
策略是:
- 创建对话框
- 在后台线程上启动长任务并注册回调
- 打开带有 "Loading" 消息的对话框
- 任务完成后,对话框将从回调中更新
如果您稍微搜索一下 Executors
,您应该会找到一些更好的示例以及有关如何使用它们的详细信息。
这里有一个简短的例子来说明它可能是什么样子:
(注意:此代码肯定存在一些问题,但为了简洁和说明这一点,我选择了一个稍微幼稚的解决方案。还有 Java 8 种方式可以稍微短一点,但这再次说明了使用后台线程背后的想法;同样的概念适用)
给定 Callable
(如果不需要 return 值,则为 Runnable
),
public class LongTask implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(15000);
return "Hello, World!";
}
}
您可以使用Executors
class to create a thread pool, and then an ExecutorService
提交Callable
执行。然后,使用 Futures.addCallback()
,您可以注册一个回调,该回调将根据任务是成功还是失败执行两种方法之一。
final ExecutorService threadPool = Executors.newFixedThreadPool(1);
final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(threadPool);
final ListenableFuture<String> future = executorService.submit(new LongTask());
Futures.addCallback(future, new FutureCallback(){...});
在我看来,在这种情况下,我使用了 Google Guava 实现 ListeningExecutorService
,这让事情变得更清晰、更简单。但是同样,如果您选择更 "Java 8" 的方法,您甚至可能不需要这个。
至于回调,当任务成功时,我们会用结果更新 Dialog
。如果它失败了,我们可以用一些东西来更新它以表明失败:
public static class DialogCallback implements FutureCallback<String> {
private final MyDialog dialog;
public DialogCallback(final MyDialog dialog) {
this.dialog = dialog;
}
@Override
public void onSuccess(final String result) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus(result);
}
});
}
@Override
public void onFailure(final Throwable t) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus("Failure");
}
});
}
}
在这种情况下,我选择了 Callable
到 return 和 String
,因此 FutureCallback
应该用 String
参数化。您可能想使用您创建的其他一些 class,它们也同样有效。
请注意,我们使用 Display.asyncExec()
方法来确保更新 UI 的代码在 UI 线程上运行,因为回调可能在后台线程上执行。
就像我说的,这里还有一些问题,包括在任务完成之前单击取消按钮时会发生什么等等。但希望这有助于说明处理长运行背景的方法不阻塞 UI 线程的任务。
完整示例代码:
public class DialogTaskExample {
private final Display display;
private final Shell shell;
private final ListeningExecutorService executorService;
public DialogTaskExample() {
display = new Display();
shell = new Shell(display);
shell.setLayout(new GridLayout());
executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
final Button button = new Button(shell, SWT.PUSH);
button.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
button.setText("Start");
button.addSelectionListener(new SelectionAdapter() {
@SuppressWarnings("synthetic-access")
@Override
public void widgetSelected(final SelectionEvent e) {
final MyDialog dialog = new MyDialog(shell);
dialog.setBlockOnOpen(false);
dialog.open();
dialog.setStatus("Doing stuff...");
final ListenableFuture<String> future = executorService.submit(new LongTask());
Futures.addCallback(future, new DialogCallback(dialog));
}
});
}
public void run() {
shell.setSize(200, 200);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
executorService.shutdown();
display.dispose();
}
public static void main(final String... args) {
new DialogTaskExample().run();
}
public static class DialogCallback implements FutureCallback<String> {
private final MyDialog dialog;
public DialogCallback(final MyDialog dialog) {
this.dialog = dialog;
}
@Override
public void onSuccess(final String result) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus(result);
}
});
}
@Override
public void onFailure(final Throwable t) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus("Failure");
}
});
}
}
public static class LongTask implements Callable<String> {
/**
* {@inheritDoc}
*/
@Override
public String call() throws Exception {
Thread.sleep(15000);
return "Hello, World!";
}
}
public static class MyDialog extends Dialog {
private Composite baseComposite;
private Label label;
/**
* @param parentShell
*/
protected MyDialog(final Shell parentShell) {
super(parentShell);
}
/**
* {@inheritDoc}
*/
@Override
protected Control createDialogArea(final Composite parent) {
baseComposite = (Composite) super.createDialogArea(parent);
label = new Label(baseComposite, SWT.NONE);
return baseComposite;
}
public void setStatus(final String text) {
label.setText(text);
baseComposite.layout();
}
}
}
打开新对话框时,在加载时,您在父对话框 shell 上单击几次,显然新对话框无法正确显示。 请看下面的例子:
例子
- https://i.stack.imgur.com/ZovxE.png(日食IDE例子)
- https://i.stack.imgur.com/5zVar.png
- https://i.stack.imgur.com/u86b9.png
- https://i.stack.imgur.com/FGaAr.png
最初我在 2014 年 12 月遇到了这个问题,当时使用不同开发系统的内部开发人员也报告了同样的问题,然后我们的几个客户报告了同样的问题。
可以使用以下环境重现此行为:
- Windows Version: 7 Pro 64 Bit - 6.1.7601
- Java Version: RE 1.8.0_121_b13
- SWT Versions
- 3.8.2
- 4.6.2
- 4.7M6
- I20170319-2000
我只能在 Windows 7 上使用 windows 基本 theme/design/style(不是经典或 aero)重现该问题。 在 windows 10 上,它不可重现。
重现
重现代码
package test;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
public class Main {
public static void main(String[] args) {
Display display = new Display();
final Shell shell = createShell(display);
createButton(shell);
shell.open();
eventLoop(display, shell);
display.dispose();
}
private static Shell createShell(Display display) {
final Shell shell = new Shell(display);
shell.setLayout(new RowLayout());
shell.setSize(500, 200);
return shell;
}
private static void createButton(final Shell shell) {
final Button openDialog = new Button(shell, SWT.PUSH);
openDialog.setText("Click here to open Dialog ...");
openDialog.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
TestDialog inputDialog = new TestDialog(shell);
inputDialog.open();
}
});
}
private static void eventLoop(Display display, final Shell shell) {
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
}
class TestDialog extends Dialog {
public TestDialog(Shell parent) {
super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.MIN | SWT.MAX | SWT.RESIZE);
setText("Dialog");
}
public void open() {
Shell shell = new Shell(getParent(), getStyle());
shell.setText(getText());
createContents(shell);
shell.pack();
initializeBounds(shell);
shell.open();
eventLoop(shell);
}
private void createContents(final Shell shell) {
shell.setLayout(new GridLayout(2, true));
Label label = new Label(shell, SWT.NONE);
label.setText("Some Label text ...");
final Text text = new Text(shell, SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
text.setLayoutData(data);
createCloseButton(shell);
/* time for the user to create the misbehavior */
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void createCloseButton(final Shell shell) {
Button closeButton = new Button(shell, SWT.PUSH);
closeButton.setText("Close");
GridData data = new GridData(GridData.FILL_HORIZONTAL);
closeButton.setLayoutData(data);
closeButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent event) {
shell.close();
}
});
shell.setDefaultButton(closeButton);
}
private void initializeBounds(Shell shell) {
Rectangle bounds = shell.getBounds();
Rectangle parentBounds = getParent().getBounds();
bounds.x = parentBounds.x;
bounds.y = parentBounds.y;
shell.setBounds(bounds);
}
private void eventLoop(Shell shell) {
Display display = getParent().getDisplay();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
}
重现步骤
- 启动应用程序
- 它应该看起来像:https://i.stack.imgur.com/dMJ9e.png
- 点击按钮。
- 不断点击父级shell的右下角(避免点击新打开的对话框),直到鼠标光标变为等待图标并且父级shell改变颜色。
- 它应该如下所示:https://i.stack.imgur.com/c1Ikp.png
- 等到新对话框出现。
- 它看起来像下面这样:https://i.stack.imgur.com/kTDgQ.png(显示不正确)
- 改为:https://i.stack.imgur.com/cHVjn.png(正确显示)
在视频中完成的重现步骤
当您将鼠标悬停在某些 UI 元素(最初未正确绘制)时,您会注意到其中一些被绘制(例如 table 行)。
- https://i.stack.imgur.com/kkMKn.png(打开对话框之前)
- https://i.stack.imgur.com/ZXIKc.png(打开对话框后)
- https://i.stack.imgur.com/25M7S.jpg(鼠标悬停后)
即使在对话框打开后调用 Shell.update()
或 Shell.redraw()
也无法修复它。
在 Windows 性能选项 -> 视觉效果 -> 禁用 "Use visual styles on windows and buttons" 是我找到的唯一提供解决方法的选项, 这似乎与将 design/theme/style 更改为经典相同。
最后,我有以下问题: 是 SWT 还是 Windows 问题? Windows 或 Eclipse Bugzilla 的错误条目中是否有任何相关主题? 还有其他人遇到过同样的问题吗?请分享经验。 SWT 或 Windows 中是否有任何设置会影响其外观并解决问题?
代码似乎很简单,只是您让主线程休眠 15 秒,因此延迟。如果不需要,请取消睡眠或将睡眠时间减少到 5 秒左右。
In the end, I have following questions: Is it a SWT or Windows problem?
都没有。正如其他人所提到的,您当然不应该将 UI 线程与任何长 运行 任务捆绑在一起。该工作属于后台线程。
关于使用后台线程,有几种方法可以解决这个问题,具体取决于您希望 Dialog
的行为方式。
一种选择是启动后台线程,然后在任务完成后打开对话框。我个人并不关心这个,因为当任务是 运行 时,用户可能会认为什么都没有发生。
另一种选择是打开对话框但显示一条 "Loading" 消息,或类似的东西以提供有意义的反馈并让用户知道应用程序未被冻结(比如它如何 looks/responds 在你的例子中)。
策略是:
- 创建对话框
- 在后台线程上启动长任务并注册回调
- 打开带有 "Loading" 消息的对话框
- 任务完成后,对话框将从回调中更新
如果您稍微搜索一下 Executors
,您应该会找到一些更好的示例以及有关如何使用它们的详细信息。
这里有一个简短的例子来说明它可能是什么样子: (注意:此代码肯定存在一些问题,但为了简洁和说明这一点,我选择了一个稍微幼稚的解决方案。还有 Java 8 种方式可以稍微短一点,但这再次说明了使用后台线程背后的想法;同样的概念适用)
给定 Callable
(如果不需要 return 值,则为 Runnable
),
public class LongTask implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(15000);
return "Hello, World!";
}
}
您可以使用Executors
class to create a thread pool, and then an ExecutorService
提交Callable
执行。然后,使用 Futures.addCallback()
,您可以注册一个回调,该回调将根据任务是成功还是失败执行两种方法之一。
final ExecutorService threadPool = Executors.newFixedThreadPool(1);
final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(threadPool);
final ListenableFuture<String> future = executorService.submit(new LongTask());
Futures.addCallback(future, new FutureCallback(){...});
在我看来,在这种情况下,我使用了 Google Guava 实现 ListeningExecutorService
,这让事情变得更清晰、更简单。但是同样,如果您选择更 "Java 8" 的方法,您甚至可能不需要这个。
至于回调,当任务成功时,我们会用结果更新 Dialog
。如果它失败了,我们可以用一些东西来更新它以表明失败:
public static class DialogCallback implements FutureCallback<String> {
private final MyDialog dialog;
public DialogCallback(final MyDialog dialog) {
this.dialog = dialog;
}
@Override
public void onSuccess(final String result) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus(result);
}
});
}
@Override
public void onFailure(final Throwable t) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus("Failure");
}
});
}
}
在这种情况下,我选择了 Callable
到 return 和 String
,因此 FutureCallback
应该用 String
参数化。您可能想使用您创建的其他一些 class,它们也同样有效。
请注意,我们使用 Display.asyncExec()
方法来确保更新 UI 的代码在 UI 线程上运行,因为回调可能在后台线程上执行。
就像我说的,这里还有一些问题,包括在任务完成之前单击取消按钮时会发生什么等等。但希望这有助于说明处理长运行背景的方法不阻塞 UI 线程的任务。
完整示例代码:
public class DialogTaskExample {
private final Display display;
private final Shell shell;
private final ListeningExecutorService executorService;
public DialogTaskExample() {
display = new Display();
shell = new Shell(display);
shell.setLayout(new GridLayout());
executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
final Button button = new Button(shell, SWT.PUSH);
button.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
button.setText("Start");
button.addSelectionListener(new SelectionAdapter() {
@SuppressWarnings("synthetic-access")
@Override
public void widgetSelected(final SelectionEvent e) {
final MyDialog dialog = new MyDialog(shell);
dialog.setBlockOnOpen(false);
dialog.open();
dialog.setStatus("Doing stuff...");
final ListenableFuture<String> future = executorService.submit(new LongTask());
Futures.addCallback(future, new DialogCallback(dialog));
}
});
}
public void run() {
shell.setSize(200, 200);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
executorService.shutdown();
display.dispose();
}
public static void main(final String... args) {
new DialogTaskExample().run();
}
public static class DialogCallback implements FutureCallback<String> {
private final MyDialog dialog;
public DialogCallback(final MyDialog dialog) {
this.dialog = dialog;
}
@Override
public void onSuccess(final String result) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus(result);
}
});
}
@Override
public void onFailure(final Throwable t) {
dialog.getShell().getDisplay().asyncExec(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
dialog.setStatus("Failure");
}
});
}
}
public static class LongTask implements Callable<String> {
/**
* {@inheritDoc}
*/
@Override
public String call() throws Exception {
Thread.sleep(15000);
return "Hello, World!";
}
}
public static class MyDialog extends Dialog {
private Composite baseComposite;
private Label label;
/**
* @param parentShell
*/
protected MyDialog(final Shell parentShell) {
super(parentShell);
}
/**
* {@inheritDoc}
*/
@Override
protected Control createDialogArea(final Composite parent) {
baseComposite = (Composite) super.createDialogArea(parent);
label = new Label(baseComposite, SWT.NONE);
return baseComposite;
}
public void setStatus(final String text) {
label.setText(text);
baseComposite.layout();
}
}
}