WPF 模态进度窗口

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/18258006/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 09:27:17  来源:igfitidea点击:

WPF modal progress window

c#wpfdialogwindowmodal-dialog

提问by BrianVPS

I apologize if this question has been answered tons of times, but I can't seem to find an answer that works for me. I would like to create a modal window that shows various progress messages while my application performs long running tasks. These tasks are run on a separate thread and I am able to update the text on the progress window at different stages of the process. The cross-thread communication is all working nicely. The problem is that I can't get the window to be on top of only other application windows (not every application on the computer), stay on top, prevent interaction with the parent window, and still allow the work to continue.

如果这个问题已经被回答了很多次,我深表歉意,但我似乎找不到适合我的答案。我想创建一个模式窗口,在我的应用程序执行长时间运行的任务时显示各种进度消息。这些任务在单独的线程上运行,我能够在进程的不同阶段更新进度窗口上的文本。跨线程通信一切正常。问题是我不能让窗口只在其他应用程序窗口(不是计算机上的每个应用程序)的顶部,保持在顶部,防止与父窗口交互,并且仍然允许工作继续进行。

Here's what I've tried so far:

这是我迄今为止尝试过的:

First, my splash window is a custom class that extends the Window class and has methods to update the message box. I create a new instance of the splash class early on and Show/Hide it as needed.

首先,我的启动窗口是一个自定义类,它扩展了 Window 类并具有更新消息框的方法。我很早就创建了飞溅类的新实例,并根据需要显示/隐藏它。

In the simplest of cases, I instantiate the window and call .Show()on it:

在最简单的情况下,我实例化窗口并调用.Show()它:

//from inside my secondary thread
this._splash.Dispatcher.Invoke(new Action(() => this._splash.Show());

//Do things
//update splash text
//Do more things

//close the splash when done
this._splash.Dispatcher.Invoke(new Action(() => this._splash.Hide());

This correctly displays the window and continues running my code to handle the initialization tasks, but it allows me to click on the parent window and bring that to the front.

这会正确显示窗口并继续运行我的代码来处理初始化任务,但它允许我单击父窗口并将其置于最前面。

Next I tried disabling the main window and re-enabling later:

接下来我尝试禁用主窗口并稍后重新启用:

Application.Current.Dispatcher.Invoke(new Action(() => this.MainWindow.IsEnabled = false));

//show splash, do things, etc

Application.Current.Dispatcher.Invoke(new Action(() => this.MainWindow.IsEnabled = true));

This disables all the elements in the window, but I can still click the main window and bring it in front of the splash screen, which is not what I want.

这会禁用窗口中的所有元素,但我仍然可以单击主窗口并将其置于初始屏幕前,这不是我想要的。

Next I tried using the topmost property on the splash window. This keeps it in front of everything, and in conjunction with setting the main window IsEnabled property I could prevent interaction, but this makes the splash screen appear in front of EVERYTHING, including other applications. I don't want that either. I just want it to be the topmost window within THIS application.

接下来我尝试在启动窗口上使用 topmost 属性。这将它保持在所有东西的前面,并且结合设置主窗口 IsEnabled 属性我可以防止交互,但这会使启动画面出现在所有东西的前面,包括其他应用程序。我也不想要那个。我只是想让它成为这个应用程序中最顶层的窗口。

Then I found posts about using .ShowDialog()instead of .Show(). I tried this, and it correctly showed the dialog and did not allow me to click on the parent window, but calling .ShowDialog()makes the program hang waiting for you to close the dialog before it will continue running code. This is obviously, not what I want either. I suppose I could call ShowDialog()on a different thread so that that thread would hang but the thread doing the work would not...is that the recommended method?

然后我找到了关于使用.ShowDialog()而不是.Show(). 我试过了,它正确地显示了对话框,并且不允许我单击父窗口,但是调用.ShowDialog()会使程序挂起,等待您关闭对话框,然后再继续运行代码。这显然也不是我想要的。我想我可以调用ShowDialog()不同的线程,以便该线程挂起,但执行工作的线程不会......这是推荐的方法吗?

I have also considered the possibility of not using a window at all and instead putting a full-sized window element in front of everything else on the page. This would work except that I have other windows I open and I'd like to be able to use the splash screen when those are open too. If I used a window element I would have to re-create it on every window and I wouldn't be able to use my handy UpdateSplashTextmethod in my custom splash class.

我还考虑过完全不使用窗口,而是将全尺寸窗口元素放在页面上其他所有内容的前面的可能性。除了我打开了其他窗口并且我也希望能够在这些窗口打开时使用闪屏之外,这会起作用。如果我使用了一个窗口元素,我将不得不在每个窗口上重新创建它,而且我将无法UpdateSplashText在我的自定义启动类中使用我的方便方法。

So this brings me to the question. What is the right way to handle this?

所以这让我想到了这个问题。处理这个问题的正确方法是什么?

Thanks for your time and sorry for the long question but details are important :)

感谢您抽出宝贵时间,很抱歉问了这么长的问题,但细节很重要:)

回答by Servy

You are correct that ShowDialoggives you most of the UI behavior that you want.

您是正确的,它ShowDialog为您提供了您想要的大部分 UI 行为。

It does have the problem that as soon as you call it you block execution though. How could you possibly run some code after you show the form, but define what it should be before it's shown? That's your problem.

它确实存在一个问题,即一旦您调用它,您就会阻止执行。你怎么可能在显示表单之后运行一些代码,但在显示之前定义它应该是什么?那是你的问题。

You could just do all of the work within the splash class, but that's rather poor practice due to tight coupling.

您可以在飞溅类中完成所有工作,但由于紧密耦合,这种做法相当糟糕。

What you can do is leverage the Loadedevent of Windowto define code that should run after the window is shown, but where it is defined before you show it.

您可以做的是利用 的Loaded事件Window来定义应该在窗口显示后运行的代码,但在您显示它之前定义它的位置。

public static void DoWorkWithModal(Action<IProgress<string>> work)
{
    SplashWindow splash = new SplashWindow();

    splash.Loaded += (_, args) =>
    {
        BackgroundWorker worker = new BackgroundWorker();

        Progress<string> progress = new Progress<string>(
            data => splash.Text = data);

        worker.DoWork += (s, workerArgs) => work(progress);

        worker.RunWorkerCompleted +=
            (s, workerArgs) => splash.Close();

        worker.RunWorkerAsync();
    };

    splash.ShowDialog();
}

Note that this method is designed to encapsulate the boilerplate code here, so that you can pass in any worker method that accepts the progress indicator and it will do that work in a background thread while showing a generic splash screen that has progress indicated from the worker.

请注意,此方法旨在封装此处的样板代码,以便您可以传入任何接受进度指示器的工作方法,它会在后台线程中完成该工作,同时显示具有工作进度指示的通用启动画面.

This could then be called something like this:

这可以被称为这样的东西:

public void Foo()
{
    DoWorkWithModal(progress =>
    {
        Thread.Sleep(5000);//placeholder for real work;
        progress.Report("Finished First Task");

        Thread.Sleep(5000);//placeholder for real work;
        progress.Report("Finished Second Task");

        Thread.Sleep(5000);//placeholder for real work;
        progress.Report("Finished Third Task");
    });
}

回答by Marcus Kaseder

The accepted answer from @Servy helped me a lot! And I wanted to share my Version with the asyncand MVVM approach. It also contains a small delay to avoid "window flickering" for too fast operations.

@Servy 接受的答案对我帮助很大!我想用asyncMVVM 方法分享我的版本。它还包含一个小的延迟,以避免太快操作的“窗口闪烁”。

Dialog Method:

对话方法:

public static async void ShowModal(Func<IProgress<string>, Task> workAsync, string title = null, TimeSpan? waitTimeDialogShow = null)
{
    if (!waitTimeDialogShow.HasValue)
    {
        waitTimeDialogShow = TimeSpan.FromMilliseconds(300);
    }

    var progressWindow = new ProgressWindow();
    progressWindow.Owner = Application.Current.MainWindow;

    var viewModel = progressWindow.DataContext as ProgressWindowViewModel;
    Progress<string> progress = new Progress<string>(text => viewModel.Text = text);

    if(!string.IsNullOrEmpty(title))
    {
        viewModel.Title = title;
    }

    var workingTask = workAsync(progress);

    progressWindow.Loaded += async (s, e) =>
    {
        await workingTask;

        progressWindow.Close();
    };

    await Task.Delay((int)waitTimeDialogShow.Value.TotalMilliseconds);

    if (!workingTask.IsCompleted && !workingTask.IsFaulted)
    {
        progressWindow.ShowDialog();
    }
}

Usage:

用法:

    ShowModal(async progress =>
    {
        await Task.Delay(5000); // Task 1
        progress.Report("Finished first task");

        await Task.Delay(5000); // Task 2
        progress.Report("Finished second task");
    }); 

Thanks again @Servy, saved me a lot of time.

再次感谢@Servy,为我节省了很多时间。

回答by Dax Fohl

You can have your progress window's constructor take a Taskand then ensure the window calls task.Starton the OnLoadedevent. Then you use ShowDialogfrom the parent form, which will cause the progress window to start the task.

您可以让进度窗口的构造函数采用 a Task,然后确保窗口调用task.StartOnLoaded事件。然后你使用ShowDialog从父窗体,这将导致进度窗口启动任务。

Note you could also call task.Startin the constructor, or in the parent form anywhere before calling ShowDialog. Whichever makes most sense to you.

请注意,您还task.Start可以在调用ShowDialog. 哪个对你最有意义。

Another option would be just to use a progress bar in the status strip of the main window, and get rid of the popup. This option seems to be more and more common these days.

另一种选择是在主窗口的状态条中使用进度条,并摆脱弹出窗口。如今,此选项似乎越来越普遍。

回答by Shoe

You can use the Visibilityproperty on Windowto hide the whole window while the splash screen runs.

您可以使用Visibilityon 属性在Window启动画面运行时隐藏整个窗口。

XAML

XAML

<Window ... Name="window" />

Code

代码

window.Visibility = System.Windows.Visibility.Hidden;

//show splash 
//do work
//end splash

window.Visibility = System.Windows.Visibility.Visible;

回答by BrianVPS

I found a way to make this work by calling ShowDialog()on a separate thread. I created my own ShowMe()and HideMe()methods in my dialog class that handle the work. I also capture the Closingevent to prevent closing the dialog so I can re-use it.

我找到了一种通过调用ShowDialog()单独的线程来完成这项工作的方法。我创建了自己ShowMe()HideMe()方法在我的对话框类处理工作。我还捕获Closing事件以防止关闭对话框,以便我可以重新使用它。

Here's my code for my splash screen class:

这是我的启动画面类的代码:

public partial class StartupSplash : Window
{
    private Thread _showHideThread;

    public StartupSplash()
    {
        InitializeComponent();

        this.Closing += OnCloseDialog;
    }


    public string Message
    {
        get
        {
            return this.lb_progress.Content.ToString();
        }
        set
        {
            if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread)
                this.lb_progress.Content = value;
            else
                this.lb_progress.Dispatcher.Invoke(new Action(() => this.lb_progress.Content = value));
        }
    }


    public void ShowMe()
    {
        _showHideThread = new Thread(new ParameterizedThreadStart(doShowHideDialog));
        _showHideThread.Start(true);
    }

    public void HideMe()
    {
        //_showHideThread.Start(false);
        this.doShowHideDialog(false);
    }

    private void doShowHideDialog(object param)
    {
        bool show = (bool)param;

        if (show)
        {
            if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread)
                this.ShowDialog();
            else
                Application.Current.Dispatcher.Invoke(new Action(() => this.ShowDialog()));
        }
        else
        {
            if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread)
                this.Close();
            else
                Application.Current.Dispatcher.Invoke(new Action(() => this.Close()));
        }
    }


    private void OnCloseDialog(object sender, CancelEventArgs e)
    {
        e.Cancel = true;
        this.Hide();
    }
}