【问题标题】:SWT Dialog does not display correctlySWT 对话框无法正确显示
【发布时间】:2017-08-14 18:43:54
【问题描述】:

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

示例

最初我在 2014 年 12 月遇到了这个问题,当时也被许多内部开发人员报告过,他们使用不同的开发系统,然后我们的几个客户也报告了同样的问题。

可以使用以下环境重现此行为:

  • Windows 版本:7 Pro 64 位 - 6.1.7601
  • Java 版本:RE 1.8.0_121_b13
  • SWT 版本
    • 3.8.2
    • 4.6.2
    • 4.7M6
    • I20170319-2000

我只能在 Windows 7 上使用 Windows 基本主题/设计/样式(不能使用经典或航空)重现该问题。 在 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. 启动应用程序
  2. 点击按钮。
  3. 不断点击父shell的右下角(避免点击新的打开对话框),直到鼠标光标变为等待图标,父shell改变颜色。
  4. 等到新对话框出现。

在视频中重现的步骤

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

  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 性能选项 -> 视觉效果 -> 禁用“在窗口和按钮上使用视觉样式”是我发现的唯一提供解决方法的选项, 这似乎与将设计/主题/样式更改为经典相同。

最后,我有以下问题: 是 SWT 还是 Windows 问题? Windows 的错误条目或 Eclipse Bugzilla 中是否有任何相关主题? 有没有其他人遇到过同样的问题?请分享经验。 SWT 或 Windows 中是否有任何设置会影响其外观并解决问题?

【问题讨论】:

  • 在 SWT UI 线程中执行 Thread.sleep 肯定会导致问题,因为它会停止调用 display.readAndDispatch 的线程,这对于 SWT 上的正确操作至关重要。
  • @greg-449 Thread.sleep 只是模拟 UI 线程上不可避免的更长操作。例如在TestDialog inputDialog = new TestDialog(shell);inputDialog.open(); 之间也可能发生这种情况,关键是操作完成后UI 没有赶上描述的情况并导致描述的结果。有没有办法强制正确绘制对话框?
  • 您不得在 UI 线程上执行任何长操作 - 您必须在后台线程中执行这些操作。不及时拨打readAndDispatch总会出问题。
  • 我知道响应式 UI 需要 Display.readAndDispatch()(在这种情况下我不关心)。我不知道您不允许在 UI 线程上执行任何长时间的操作,这对我来说似乎很奇怪。 @greg-449 请分享您指定的来源。
  • 几乎任何关于 SWT 的描述都会告诉你这一点。首先出现的是 Eclipse 后台任务中的Vogella tutorial(Eclipse 只是一个大型 SWT 应用程序)。请参阅第 2.1 节“主线程”。正如我所说,要求是必须不断调用readAndDispatch,任何延迟调用都会导致问题。

标签: user-interface windows-7 dialog swt


【解决方案1】:

最后,我有以下问题:是 SWT 问题还是 Windows 问题?

两者都不是。正如其他人所提到的,您当然不应该将 UI 线程与任何长时间运行的任务捆绑在一起。该工作属于后台线程。

关于使用后台线程,有几种方法可以解决这个问题,具体取决于您希望Dialog 的行为方式。

一种选择是启动后台线程,然后在任务完成后打开对话框。我个人并不关心这个,因为当任务运行时,用户可能会认为什么都没有发生。

另一种选择是打开对话框但显示“正在加载”消息或类似的内容以提供有意义的反馈并让用户知道应用程序未冻结(如示例中的外观/响应方式) )。

策略是:

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

如果您搜索一下使用Executors,您应该会找到一些更好的示例以及如何使用它们的详细信息。

下面是一个简短的例子来说明它可能是什么样子: (注意:这段代码肯定有一些问题,但为了简洁和说明这一点,我选择了一个稍微幼稚的解决方案。还有一些 Java 8 风格的方法会更短一些,但是再次,这说明了使用后台线程背后的想法;同样的概念适用)

给定一个Callable(或Runnable,如果你不需要返回值),

public class LongTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(15000);
        return "Hello, World!";
    }
}

您可以使用Executors 类创建线程池,然后使用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 来返回String,因此FutureCallback 应该使用String 进行参数化。您可能想要使用您创建的其他一些类,它也可以正常工作。

请注意,我们使用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();
        }

    }

}

【讨论】:

    【解决方案2】:

    代码似乎很简单,只是您让主线程休眠 15 秒,因此延迟。如果不需要,请取消睡眠或将睡眠时间减少到 5 秒左右。

    【讨论】:

    • sleep 的目的是模拟在打开对话框期间在 ui 线程上不可避免的较长时间的操作。在代码示例案例中,sleep 使测试人员能够在对话框打开时单击父 shell。我想给出一个最小的代码示例,以确保它不是导致问题的代码。处理这种情况的方法是错误的吗?
    • 您已经使用了用于此目的的模态,或者将对话框放入新线程并检查。
    • 我不太明白你的意思。你能给我一个代码示例吗?
    • 你能不能给我发一份关于要求的文件。
    • 不,我不能。 “将对话框放入新线程”是什么意思?你的意思是另一个主/用户界面线程?
    猜你喜欢
    • 1970-01-01
    • 2017-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-17
    • 2017-10-07
    • 1970-01-01
    • 2011-10-09
    相关资源
    最近更新 更多