12

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

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

首先,我的启动窗口是一个自定义类,它扩展了 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));

这会禁用窗口中的所有元素,但我仍然可以单击主窗口并将其带到启动屏幕的前面,这不是我想要的。

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

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

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

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

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

4

5 回答 5

21

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

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

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

您可以做的是利用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();
}

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

然后可以这样称呼它:

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");
    });
}
于 2013-08-15T18:05:10.473 回答
3

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

对话方法:

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

用法:

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

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

于 2018-05-16T12:29:11.433 回答
0

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

XAML

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

代码

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

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

window.Visibility = System.Windows.Visibility.Visible;
于 2013-08-15T17:47:10.477 回答
0

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

请注意,您也可以task.Start在构造函数中调用,或者在调用ShowDialog. 哪个对你最有意义。

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

于 2013-08-15T17:43:09.053 回答
-1

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

这是我的初始屏幕类的代码:

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();
    }
}
于 2013-08-15T17:48:55.310 回答