WPF模态进度窗口 [英] WPF modal progress window

查看:91
本文介绍了WPF模态进度窗口的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如果这个问题已经被回答了无数次,我深表歉意,但是我似乎找不到适合我的答案.我想创建一个模式窗口,在我的应用程序执行长时间运行的任务时显示各种进度消息.这些任务在单独的线程上运行,我可以在流程的不同阶段更新进度窗口上的文本.跨线程通信运行良好.问题是我无法使窗口仅位于其他应用程序窗口(而不是计算机上的每个应用程序)的顶部,停留在顶部,防止与父窗口的交互以及仍然允许工作继续进行. >

这是我到目前为止尝试过的:

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

在最简单的情况下,我实例化窗口并在其上调用.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());

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

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

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));

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

接下来,我尝试使用初始窗口上的top属性.这样可以将其置于所有内容的前面,并且与设置主窗口的IsEnabled属性一起可以防止交互,但这会使启动屏幕显示在包括其他应用程序在内的所有内容的前面.我也不想要我只希望它成为此应用程序中最顶层的窗口.

然后,我找到了有关使用.ShowDialog()而不是.Show()的帖子.我尝试了此操作,它正确显示了对话框,并且不允许我单击父窗口,但是调用.ShowDialog()会使程序挂起,等待您关闭对话框,然后继续运行代码.显然,这不是我想要的.我想我可以在另一个线程上调用ShowDialog(),以便该线程挂起,但是执行工作的线程不会...是推荐的方法吗?

我还考虑了根本不使用窗口,而是将全尺寸窗口元素放在页面上所有其他内容前面的可能性.除了我打开其他窗口,并且也希望在打开这些窗口时也可以使用启动屏幕,这将起作用.如果我使用window元素,则必须在每个窗口上重新创建它,并且无法在自定义启动类中使用方便的UpdateSplashText方法.

这使我想到了这个问题.处理这个问题的正确方法是什么?

感谢您的时间,对冗长的问题深表歉意,但细节很重要:)

解决方案

您正确的认为ShowDialog为您提供了所需的大多数UI行为.

它确实有一个问题,那就是您一调用它就立即阻止执行.在显示表单之后如何运行一些代码,但是在显示表单之前定义它应该是什么?那是你的问题.

您可以在Splash类中完成所有工作,但是由于紧密耦合,所以实践很差.

您可以做的是利用WindowLoaded事件来定义应在显示窗口之后运行的代码,但应在显示窗口之前对其进行定义.

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();
}

请注意,此方法旨在在此处封装样板代码,以便您可以传入接受进度指示器的任何worker方法,并且它将在后台线程中完成该工作,同时显示已显示进度的通用启动屏幕来自工人.

然后可以这样称呼它:

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");
    });
}

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.

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

//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.

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?

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 UpdateSplashText method in my custom splash class.

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 :)

解决方案

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

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 Loaded event of Window to define code that should run after the window is shown, but where it is defined before you show it.

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");
    });
}

这篇关于WPF模态进度窗口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆