Display progress during matrix loop?

2019-08-20 01:26发布

问题:

I'm making a chessboard, I want to see progress while it's done. Chessboard is not classical, it contains millions of fields, the process of creation itself takes time. Sometimes the creation process takes up to 2 minutes. I want to visually see when the process itself will be over. It does not have to be a progress bar, it can be any control that will not slow down the process itself.

When I use Progress.Dispatcher.Invoke (()... I actually slow down the creation process and it takes 5 times longer than usual. When I use BackgroundWorker and ReportProgress ... I also slow down the creation process and it takes 5 to 8 times more than usual.

I just want to show the user progress by using any control or class that will not slow down the process. Some idea?

Rectangle[,] square = new Rectangle[x, x];
    for (int row = 0; row < x; row++)
        for (int col = 0; col < x; col++)
        {
            square[row, col] = new Rectangle()
            {
                Height = squareSize,
                Width = squareSize
            };
            Grid.SetColumn(square[row, col], col);
            Grid.SetRow(square[row, col], row);
            if ((row + col) % 2 == 0)
            {
                square[row, col].Fill = new SolidColorBrush(System.Windows.Media.Color.FromRgb(233, 223, 191));
            }
            else
            {
                square[row, col].Fill = new SolidColorBrush(System.Windows.Media.Color.FromRgb(112, 42, 44));
            }
            LayoutRoot.Children.Add(square[row, col]);

            // Watch process of creation in real time
            if (cbCreationProcess.IsChecked == true)
                Progress.Dispatcher.Invoke(() => Progress.Value = x, DispatcherPriority.Background);
        }

回答1:

This solution has worked for me in the past.

ProgressWindowControl.xaml

<Window
    x:Class="YourNamespace.ProgressWindowControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    WindowStartupLocation="CenterScreen" ShowInTaskbar="False" ResizeMode="NoResize"
    SizeToContent="WidthAndHeight" WindowStyle="None"
    mc:Ignorable="d">

    <Window.Style>
        <Style TargetType="Window">
            <Setter Property="AllowsTransparency" Value="True"/>
            <Setter Property="Background" Value="#00FFFFFF"/>
        </Style>
    </Window.Style>

    <Grid>
        <Grid Width="450" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0">
            <Grid x:Name="Back">
                <Border Background="Black" CornerRadius="3" Opacity="0.15"/>
                <Border CornerRadius="2" Margin="1" Background="White"/>
            </Grid>
            <Grid x:Name="Content_Area" Margin="12">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <TextBlock x:Name="Info" TextWrapping="Wrap" 
                           Text="{Binding Path=State,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" 
                           Grid.Row="0" Margin="12,12,12,0" Foreground="#FF2D2D2D"/>
                <ProgressBar Height="12"
                             Grid.Row="1"
                             Margin="12"
                             IsIndeterminate="{Binding Path=IsIndeterminate,RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                             Value="{Binding Path=Progress,RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                             Maximum="{Binding Path=MaxProgress,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />

                <Button x:Name="uxCancelBtn" Grid.Row="2" Height="22" Width="85" HorizontalAlignment="Right" Margin="0 0 12 0"
                        Click="CancelButton_Click" IsEnabled="False" Content="{x:Static resx:Strings.Cancel}">               
                </Button>
            </Grid>
        </Grid>
    </Grid>
</Window>

ProgressWindowControl.cs

public sealed partial class ProgressWindowControl : Window
{
    public static readonly DependencyProperty ProgressProperty =
     DependencyProperty.Register("Progress", typeof(double), typeof(ProgressWindowControl), new PropertyMetadata(0d));

    public static readonly DependencyProperty MaxProgressProperty =
     DependencyProperty.Register("MaxProgress", typeof(double), typeof(ProgressWindowControl), new PropertyMetadata(100d));

    public static readonly DependencyProperty IsIndeterminateProperty =
        DependencyProperty.Register("IsIndeterminate", typeof(bool), typeof(ProgressWindowControl), new PropertyMetadata(true));

    public static readonly DependencyProperty StateProperty =
    DependencyProperty.Register("State", typeof(string), typeof(ProgressWindowControl), new PropertyMetadata(string.Empty));

    public static readonly DependencyProperty IsCancelAllowedProperty =
     DependencyProperty.Register("IsCancelAllowed", typeof(bool), typeof(ProgressWindowControl), new PropertyMetadata(false));

    private ProgressWindowControl()
    {
        InitializeComponent();
    }

    public double Progress
    {
        get
        {
            return (double)GetValue(ProgressProperty);
        }
        set
        {
            SetValue(ProgressProperty, value);
        }
    }

    public double MaxProgress
    {
        get
        {
            return (double)GetValue(MaxProgressProperty);
        }
        set
        {
            SetValue(MaxProgressProperty, value);
        }
    }

    public bool IsIndeterminate
    {
        get
        {
            return (bool)GetValue(IsIndeterminateProperty);
        }
        set
        {
            SetValue(IsIndeterminateProperty, value);
        }
    }

    public string State
    {
        get
        {
            return (string)GetValue(StateProperty);
        }
        set
        {
            SetValue(StateProperty, value);
        }
    }

    public Action OnProgressWindowCancel { get; set; }

    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        if (OnProgressWindowCancel != null)
        {
            uxCancelBtn.IsEnabled = false;
            uxCancelBtn.Content = Strings.Cancelling;
            OnProgressWindowCancel();
        }
    }

    [DllImport("user32.dll")]
    private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

    private const int GWL_HWNDPARENT = -8;

    private static ProgressWindowControl _progressWindowControl;
    private static bool _isVisible;
    private static Window _owner;
    private static ResizeMode? _ownerResizeMode;
    private static bool _ownerIsHitTestVisible;
    private static bool _ownerFocusable;

    public static void ShowProgressWindow(Window owner = null)
    {
        if (!_isVisible)
        {
            IntPtr ownerHandle = IntPtr.Zero;
            if (owner != null)
            {
                _owner = owner;
                ownerHandle = GetHandler(_owner);
                //Block owner window input while the progress bar is opened
                _ownerResizeMode = _owner.ResizeMode;
                _ownerIsHitTestVisible = _owner.IsHitTestVisible;
                _ownerFocusable = _owner.Focusable;
                _owner.ResizeMode = ResizeMode.NoResize;
                _owner.IsHitTestVisible = false;
                _owner.Focusable = false;
                _owner.PreviewKeyDown += Owner_PreviewKeyDown;
                _owner.PreviewMouseDown += Owner_PreviewMouseDown;
                _owner.Closing += Owner_Closing;
            }
            //Run window in its own thread
            Thread thread = new Thread(new ThreadStart(() =>
            {
                SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
                _progressWindowControl = new ProgressWindowControl();
                // Shutdown the dispatcher when the window closes
                _progressWindowControl.Closed += (s, e) =>
                    Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background);

                // When the progress window has loaded, if an owner has been specified, attach it to the window, otherwise set Topmost = true
                ProgressWindowControl._progressWindowControl.Loaded += (s, e) =>
                {
                    if (owner != null)
                    {
                        IntPtr ownedWindowHandle = GetHandler(_progressWindowControl);
                        SetOwnerWindowMultithread(ownedWindowHandle, ownerHandle);
                    }
                    else
                    {
                        _progressWindowControl.Topmost = true;
                    }
                };
                _progressWindowControl.Show();
                _isVisible = true;
                System.Windows.Threading.Dispatcher.Run();
            }));
            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start();
        }
    }

    private static void Owner_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        e.Cancel = true;
    }

    private static void Owner_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        e.Handled = true;
    }

    private static void Owner_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
    {
        e.Handled = true;
    }

    private static void SetOwnerWindowMultithread(IntPtr windowHandleOwned, IntPtr intPtrOwner)
    {
        if (windowHandleOwned != IntPtr.Zero && intPtrOwner != IntPtr.Zero)
        {
            SetWindowLong(windowHandleOwned, GWL_HWNDPARENT, intPtrOwner.ToInt32());
        }
    }

    private static IntPtr GetHandler(Window window)
    {
        var interop = new WindowInteropHelper(window);
        return interop.Handle;
    }

    public static void CloseProgressWindow()
    {
        if (_progressWindowControl != null && _isVisible)
        {
            if (_progressWindowControl.Dispatcher.CheckAccess())
            {
                _progressWindowControl.Close();
            }
            else
            {
                _progressWindowControl.Dispatcher.Invoke(DispatcherPriority.Normal,
                    new ThreadStart(_progressWindowControl.Close));
            }
            if (_owner != null)
            {
                //Unblock owner input
                _owner.ResizeMode = _ownerResizeMode ?? ResizeMode.CanResize;
                _owner.IsHitTestVisible = _ownerIsHitTestVisible;
                _owner.Focusable = _ownerFocusable;
                _owner.PreviewKeyDown -= Owner_PreviewKeyDown;
                _owner.PreviewMouseDown -= Owner_PreviewMouseDown;
                _owner.Closing -= Owner_Closing;
            }
            //Reset fields
            _ownerResizeMode = null;
            _ownerIsHitTestVisible = false;
            _ownerFocusable = false;
            _progressWindowControl = null;
            _owner = null;
            _isVisible = false;
        }
    }

    public static void SetProgress(double progress, double maxProgress)
    {
        if (_progressWindowControl != null)
        {
            if (_progressWindowControl.Dispatcher.CheckAccess())
            {
                _progressWindowControl.IsIndeterminate = false;
                _progressWindowControl.Progress = progress;
                _progressWindowControl.MaxProgress = maxProgress;
            }
            else
            {
                _progressWindowControl.Dispatcher.Invoke(DispatcherPriority.Normal,
                    new ThreadStart(() =>
                    {
                        _progressWindowControl.IsIndeterminate = false;
                        _progressWindowControl.Progress = progress;
                        _progressWindowControl.MaxProgress = maxProgress;
                    }));
            }
        }
    }

    public static void SetIsIndeterminate(bool isIndeterminate)
    {
        if (_progressWindowControl != null)
        {
            if (_progressWindowControl.Dispatcher.CheckAccess())
            {
                _progressWindowControl.IsIndeterminate = isIndeterminate;
            }
            else
            {
                _progressWindowControl.Dispatcher.Invoke(DispatcherPriority.Normal,
                    new ThreadStart(() =>
                    {
                        _progressWindowControl.IsIndeterminate = isIndeterminate;
                    }));
            }
        }
    }

    public static void SetState(string state)
    {
        if (_progressWindowControl != null)
        {
            if (_progressWindowControl.Dispatcher.CheckAccess())
            {
                _progressWindowControl.State = state;
            }
            else
            {
                _progressWindowControl.Dispatcher.Invoke(DispatcherPriority.Normal,
                    new ThreadStart(() =>
                    {
                        _progressWindowControl.State = state;
                    }));
            }
        }
    }

    public static void SetIsCancelAllowed(bool isCancelAllowed, Action progressWindowCancel)
    {
        if (_progressWindowControl != null)
        {
            if (_progressWindowControl.Dispatcher.CheckAccess())
            {
                _progressWindowControl.OnProgressWindowCancel = progressWindowCancel;
                _progressWindowControl.uxCancelBtn.IsEnabled = isCancelAllowed;
                _progressWindowControl.uxCancelBtn.Content = Strings.Cancel;
            }
            else
            {
                _progressWindowControl.Dispatcher.Invoke(DispatcherPriority.Normal,
                    new ThreadStart(() =>
                    {
                        _progressWindowControl.OnProgressWindowCancel = progressWindowCancel;
                        _progressWindowControl.uxCancelBtn.IsEnabled = isCancelAllowed;
                        _progressWindowControl.uxCancelBtn.Content = Strings.Cancel;
                    }));
            }
        }
    }
}

A helper class to open the window:

public static class ProgressWindowHelper
{
    public static void Show(Window owner = null)
    {
        ProgressWindowControl.ShowProgressWindow(owner);
    }

    public static void Close()
    {
        ProgressWindowControl.CloseProgressWindow();
    }

    public static void SetProgress(double progress, double maxProgress)
    {
        ProgressWindowControl.SetProgress(progress, maxProgress);
    }

    public static void SetIsIndeterminate(bool isIndeterminate)
    {
        ProgressWindowControl.SetIsIndeterminate(isIndeterminate);
    }

    public static void SetState(string state)
    {
        ProgressWindowControl.SetState(state);
    }

    public static void SetIsCancelAllowed(bool isCancelAllowed, Action progressWindowCancel)
    {
        ProgressWindowControl.SetIsCancelAllowed(isCancelAllowed, progressWindowCancel);
    }
}

A service so you can use Dependency Injection (I didn't include the interface, just create one as needed):

 public class ProgressWindowService : IProgressWindowService
{
    public void Show(Window owner = null)
    {
        ProgressWindowHelper.Show(owner);
    }

    public void Close()
    {
        ProgressWindowHelper.Close();
    }

    public void SetProgress(double progress, double maxProgress)
    {
        ProgressWindowHelper.SetProgress(progress, maxProgress);
    }

    public void SetIsIndeterminate(bool isIndeterminate)
    {
        ProgressWindowHelper.SetIsIndeterminate(isIndeterminate);
    }

    public void SetState(string state)
    {
        ProgressWindowHelper.SetState(state);
    }

    public void SetIsCancelAllowed(bool isCancelAllowed, Action progressWindowCancel)
    {
        ProgressWindowHelper.SetIsCancelAllowed(isCancelAllowed, progressWindowCancel);
    }
}

And finally in your ViewModel (assuming you have injected the service):

ProgressBarService.SetProgress(current, total);

ProgressBarService.SetState(state);

ProgressBarService.Show();

You can also pass a window to the Show method and then the window will be attached to it and input will be blocked while the progress window is shown, if no window is provided, the progress bar is shown on top of any other window (TopMost=true):

ProgressBarService.Show(YourWindow);

You could also use a messenger to trigger the progress window.

EDIT

Removed DevExpress dependency.



回答2:

obviously any addition of functionality is going to slow it down as you're well aware.

what I would do if I was told to use the above code is likely create a timer and poke into where-ever you have your current row and col increments and its up to you what you do with your calculation.
All you would have to do is handle the Timer.Elapsed event (whatever kind of timer) and calculate/report current progress in percent or such.
Otherwise, you could write up a separate thread and run it in a loop that sleeps N-milloseconds until drawing is complete—essentially tandem with the idea of using a timer. Generally, this is what we would do to report progress on a playing mp3 or some such object manipulated on a separate thread.

there is a lot of room for you to be creative here and milk some wisdom out of this scenario to optimize results but before considering such...
bare in mind that WPF is a beast when it comes to how it deals with drawing things since it relies heavily on the video hardware, memory, etc... I tend to think of WPF as like html for OpenGL or DirectX. I hope your GPU has a good connection to the heat-sync and doesn't get too hot and/or that you're not working on a laptop with an embedded GPU. I'm being aggressive here, but only because I've blown pleanty of hardware over the years writing software in the write/compile/run/and-repeat cycle. I'd have gotten much more life out of lots of hardware if I played a bit safer. not to mention recent years are getting harder and harder on our hardware.

for the sake of being as creative as possible here

  • since we know how many squares there are and can be sure of the resulting grid's dimensions, have you thought rendering a custom pattern as a background and explicitly setting the size of the grid? Once that's done, you could be creative when it comes to further populating needed or spin up some interesting transitions, etc
  • learning/using System.Threading.Thread(ThreadStart) will enable you to set process-priority to a higher value than standard defaults — as opposed to using BackgroundWorker or some other such... but I haven't quite wrapped my mind around an implementation with such as this.
  • build up some objects outside the UI such as a List<> and render at the end of a row, column or perhaps every N incremenet
  • we know that we have x*x boxes in the end, so if we're explicit about the size, there is so much room for creativity here. I hadn't tinkered with WPF in a while, but this would be a good case for studying and looking at different ways to lay such a thing out (eg: CustomLayoutPanel tut). If you're stuck on the notion of a Grid, there is only so much you can do---I would instinctively be working with Drawing methods for doing this, and would likely come up with a way to draw only what is visible on the screen as needed... or
  • generally @dymanoid's response is poking you into the correct direction but it breaks the logic of the question being asked, therefore overshoots the scope of the question... but we want to enlighten here and generally yes any rendering process stuff in WPF should be thought out using existing optimization strategies such as MVVM and styles/templates/transitions.