SWT 对话框显示不正确

SWT Dialog does not display correctly

打开新对话框时,在加载时,您在父对话框 shell 上单击几次,显然新对话框无法正确显示。 请看下面的例子:

例子

最初我在 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();
            }
        }
    }
}

重现步骤

  1. 启动应用程序
    • 它应该看起来像:https://i.stack.imgur.com/dMJ9e.png
  2. 点击按钮。
  3. 不断点击父级shell的右下角(避免点击新打开的对话框),直到鼠标光标变为等待图标并且父级shell改变颜色。
    • 它应该如下所示:https://i.stack.imgur.com/c1Ikp.png
  4. 等到新对话框出现。
    • 它看起来像下面这样:https://i.stack.imgur.com/kTDgQ.png(显示不正确)
    • 改为:https://i.stack.imgur.com/cHVjn.png(正确显示)

在视频中完成的重现步骤

当您将鼠标悬停在某些 UI 元素(最初未正确绘制)时,您会注意到其中一些被绘制(例如 table 行)。

  1. https://i.stack.imgur.com/kkMKn.png(打开对话框之前)
  2. https://i.stack.imgur.com/ZXIKc.png(打开对话框后)
  3. 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 在你的例子中)。

策略是:

  1. 创建对话框
  2. 在后台线程上启动长任务并注册回调
  3. 打开带有 "Loading" 消息的对话框
  4. 任务完成后,对话框将从回调中更新

如果您稍微搜索一下 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();
        }

    }

}