winforms 如何组织非UI线程的层次结构并获取状态和进度以供UI使用

z5btuh9x  于 2022-11-16  发布在  其他
关注(0)|答案(3)|浏览(122)

  • 图1 -工作演示 *

我正在运行一个winforms应用程序,并且实现了一个状态/进度TreeView。它可以显示一组(可能是分层的)要完成的任务的状态(通过图标)和进度。我的问题不是关于如何实现TreeView控件本身。我已经涵盖了这一部分。TreeView只是后台工作向用户显示状态/进度的方式。
我有一组方法,我想在主UI线程之外运行。它们需要按顺序运行。它们是一个更大进程中的步骤。我可以将它们组织成层次结构;这将是一个很好的树结构。
这些方法中的每一个都将由树中的一个节点表示。我可能是从旧的Sql Server DTS状态面板中得到这种可视化方法的想法的。我仍然喜欢这个想法。
我想知道每个方法的完成时间和结果,可能还有一些文本状态。我还想知道一个通用的机制,可以用来冒泡进度。我将使用这些来在TreeView中与该方法对应的节点上创建一个所有者绘制的进度条。
我读过一些关于多线程和Task类的书,但并不完全理解它。我不在乎解决方案是否使用它。但也许那样会更优雅,我不知道。它看起来比回调更直接,但也许你知道得更清楚。
任务类:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Serialization;
using System.Linq;

namespace DeveloperWorkbench.Nodes
{
    public class Task : INode
    {
        public delegate void TaskStatusDelegate(Task sender, TaskStatus taskStatus);

        public event ProgressDelegate ProgressChanged;
        public event StatusDelegate Status;
        public event TaskStatusDelegate TaskStatusChanged;

        public Task()
        {
            _children = new List<Task>();
        }

        [XmlIgnore()]
        public bool CanHaveChildren { get; private set; }

        private List<Task> _children;
        public List<Task> Children
        {
            get
            {
                _children.ForEach(x => x.Parent = this);
                return _children;
            }
            set
            {
                _children = value;
                _children.ForEach(x => x.Parent = this);
            }
        }

        [XmlIgnore()]
        public List<string> ChildTypes { get; private set; }

        public string FullName { get; set; }

        private float _maxProgress = 0;
        [Browsable(false)]
        [XmlIgnore()]
        public float MaxProgress
        {
            get { return _maxProgress; }
            set
            {
                _maxProgress = value;
                RaiseProgress(this, Progress, MaxProgress);
            }
        }

        private Delegate _method;
        [Browsable(false)]
        [XmlIgnore()]
        public Delegate Method
        {
            get { return _method; }
            set
            {
                if (_method == value) return;
                _method = value;
                Name = Method.Method.Name;
                TypeName = Method.Method.ReflectedType.FullName;
            }
        }

        private string _name;
        [ReadOnly(true)]
        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                //Method = GetMethodByName(???, _name);
                FullName = ProperCaseToSpaces(_name);
            }
        }

        [Browsable(false)]
        [XmlIgnore()]
        public INode Parent { get; set; }

        private float _progress = 0;
        [Browsable(false)]
        [XmlIgnore()]
        public float Progress
        {
            get { return _progress; }
            set
            {
                _progress = value;
                RaiseProgress(this, Progress, MaxProgress);
            }
        }

        public List<KeyValuePair<string, object>> RelatedItems { get; set; }

        private TaskStatus _taskStatus = TaskStatus.Created;
        [Browsable(false)]
        [XmlIgnore()]
        public TaskStatus TaskStatus
        {
            get { return _taskStatus; }
            set
            {
                _taskStatus = value;
                TaskStatusChanged(this, _taskStatus);
            }
        }

        [ReadOnly(true)]
        public string TypeName { get; set; }

        public bool Visited { get; set; }

        public Task Add(Task child)
        {
            Children.Add(child);
            child.Parent = this;
            child.ProgressChanged += Child_Progress;
            return child;
        }

        private void Done(System.Threading.Tasks.Task task)
        {
            TaskStatus = TaskStatus.RanToCompletion;
        }

        public void Execute()
        {
            Progress = 0;
            TaskStatus = TaskStatus.Running;
            var systemTask = new System.Threading.Tasks.Task((Action)Method);
            systemTask.ContinueWith(Done);
            systemTask.Start();
            if (Parent != null)
                systemTask.Wait();
        }

        private static string ProperCaseToSpaces(string text)
        {
            return Regex.Replace(text, @"(\B[A-Z]+?(?=[A-Z][^A-Z])|\B[A-Z]+?(?=[^A-Z]))", " $1");
        }

        public void RaiseProgress(INode sender, float progress = 0, float maxProgress = 100)
        {
            ProgressChanged(sender, progress, maxProgress);
        }

        public void RaiseStatus(string status = "Ready")
        {
            Status(status);
        }

        public void Refresh(bool force)
        {
            throw new NotImplementedException();
        }

        public void RefreshChildren(bool force, string childType = null)
        {
            throw new NotImplementedException();
        }

        public List<KeyValuePair<string, INode>> RefreshRelatedItems(bool force)
        {
            throw new NotImplementedException();
        }

        //Usage: myTask.SetMethod(() => MyMethod(0, 40));
        public void SetMethod(Action method)
        {
            Method = method;
        }

        //Usage: myTask.SetMethod(() => MyFunction(myArgument));
        public void SetMethod<T>(Func<T> function)
        {
            Method = function;
        }

        public void SetMethod(Object target)
        {
            if (target.GetType().FullName == TypeName)
                Method = GetMethodByName(target, Name);
            else
            {
                var name = Name;
                SetMethod(() => FakeExecute(this));
                Name = name;
                TypeName = null;
            }

            foreach (var child in Children)
            {
                child.SetMethod(target);
            }
        }

        public void Child_Progress(INode sender, float progress = 0, float maxProgress = 100)
        {
            MaxProgress = _children.Sum(x => x.MaxProgress);
            Progress = _children.Sum(x => x.Progress);
        }

        public static Task Create<T>(Func<T> method, Task parent = null)
        {
            var task = InnerCreate(parent);
            task.SetMethod(method);
            return task;
        }

        public static Task Create(Action method, Task parent = null)
        {
            var task = InnerCreate(parent);
            task.SetMethod(method);
            return task;
        }

        public static Task Create(string methodName, Task parent = null)
        {
            var task = InnerCreate(parent);
            task.SetMethod(() => FakeExecute(task));
            task.Name = methodName;
            task.TypeName = null;
            return task;
        }

        private static Task InnerCreate(Task parent)
        {
            var task = new Task();
            if (parent != null)
                parent.Add(task);
            return task;
        }

        public static Task CurrentTask(Task rootTask, int stackFrame = 1)
        {
            var taskMethodName = new StackFrame(stackFrame).GetMethod().Name;
            return Find(rootTask, taskMethodName);
        }

        private static void FakeExecute(Task task)
        {
            foreach (Task child in task.Children)
            {
                child.MaxProgress = 100;
                child.Progress = 0;
                child.TaskStatus = TaskStatus.WaitingToRun;
            }
            foreach (Task child in task.Children)
            {
                child.Execute();
            }
        }

        private static Task Find(Task task, string methodName)
        {
            return task.Method.Method.Name == methodName ?
            task :
            task.Children.Select(child => Find(child, methodName)).FirstOrDefault(found => found != null);
        }

        static Delegate GetMethodByName(object target, string methodName)
        {
            var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;
            MethodInfo method = target.GetType().GetMethod(methodName, bindingFlags);
            return method.ReturnType == typeof(void) ? Delegate.CreateDelegate(typeof(Action), target, method) : null;
        }
    }
}

StatusList类别:

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using Retalix.R10.DeveloperWorkbench.Nodes;
using Retalix.R10.DeveloperWorkbench.UI.Helpers;
using Task = Retalix.R10.DeveloperWorkbench.Nodes.Task;

namespace DeveloperWorkbench.UI.Controls
{
    public partial class StatusList : UserControl
    {

        // Import the SetWindowRgn function from the user32.DLL
        // From the Unmanaged Code
        [DllImport("user32.DLL", EntryPoint = "SetWindowRgn")]
        private static extern int SetWindowRgn(int hWnd, int hRgn, int bRedraw);

        [System.Runtime.InteropServices.DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")]
        private static extern System.IntPtr CreateRoundRectRgn
        (
            int nLeftRect, // x-coordinate of upper-left corner
            int nTopRect, // y-coordinate of upper-left corner
            int nRightRect, // x-coordinate of lower-right corner
            int nBottomRect, // y-coordinate of lower-right corner
            int nWidthEllipse, // height of ellipse
            int nHeightEllipse // width of ellipse
        );
        [System.Runtime.InteropServices.DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
        private static extern bool DeleteObject(System.IntPtr hObject);

        public StatusList()
        {
            InitializeComponent();
        }

        private TreeNode Add(TreeNodeCollection nodes, string text, string imageKey, object tag)
        {
            var treeNode = nodes.Add(tag.GetHashCode().ToString(), text);
            treeNode.Tag = tag;
            treeNode.ImageKey = imageKey;
            treeNode.SelectedImageKey = imageKey;
            tvTreeView.ExpandAll();
            return treeNode;
        }

        public TreeNode Add(Task task)
        {
            var nodes = tvTreeView.Nodes;
            if (task.Parent != null)
                nodes = Find(task.Parent).Nodes;
            task.TaskStatusChanged += Task_TaskStatusChanged;
            task.ProgressChanged += Task_Progress;
            var treeNode = Add(nodes, task.FullName, StatusIcon(task.TaskStatus), task);
            foreach(var child in task.Children)
            {
                Add(child);
            }
            return treeNode;
        }

        private TreeNode Find(object tag)
        {
            var treeNodes = tvTreeView.Nodes.Find(tag.GetHashCode().ToString(), true);
            if (treeNodes.Length > 0)
                return treeNodes[0];
            return null;
        }

        private string StatusIcon(System.Threading.Tasks.TaskStatus status)
        {
            switch (status)
            {
                case TaskStatus.Canceled:
                case TaskStatus.Created:
                case TaskStatus.Faulted:
                case TaskStatus.RanToCompletion:
                    return status.ToString();
                    break;
                case TaskStatus.Running:
                case TaskStatus.WaitingForChildrenToComplete:
                    return TaskStatus.Running.ToString();
                    break;
                default:
                    if (status.ToString().StartsWith("Waiting"))
                        return "Waiting";
                    break;
            }
            return "Created";
        }

        private void tvTreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
        {
            var task = (Task) e.Node.Tag;

            if ((e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected)
            {
                e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds);
                //e.Graphics.DrawRectangle(SystemPens.ControlDark, e.Bounds.Left, e.Bounds.Top , e.Bounds.Width - 1, e.Bounds.Height - 1);
            }
            if(task.TaskStatus == TaskStatus.Running)
            {
                var borderBrush = new LinearGradientBrush(new Point(e.Bounds.Left + 1, e.Bounds.Top + 3), new Point(e.Bounds.Left + 1, e.Bounds.Bottom), Color.White, Color.FromArgb(200, Color.LightGray));
                var borderRectangle = new Rectangle(e.Bounds.Left + 1, e.Bounds.Top + 3, e.Bounds.Width - 10, e.Bounds.Height - 6);
                var borderGraphicsPath = RoundedRectangle.Create(borderRectangle);
                e.Graphics.FillPath(borderBrush, borderGraphicsPath);
                e.Graphics.DrawPath(Pens.DarkGray, borderGraphicsPath);
                //e.Graphics.FillRectangle(borderBrush, borderRectangle);
                //e.Graphics.DrawRectangle(pen, borderRectangle);
                if (task.Progress > 0)
                {
                    //pen.DashStyle = DashStyle.Dot;
                    var width = (task.Progress / task.MaxProgress) * (e.Bounds.Width - 11);
                    var progressRectangle = new Rectangle(e.Bounds.Left + 2, e.Bounds.Top + 4, (int)width, e.Bounds.Height - 7);
                    var progressGraphicsPath = RoundedRectangle.Create(progressRectangle, 5, RoundedRectangle.RectangleCorners.TopLeft | RoundedRectangle.RectangleCorners.BottomLeft);
                    //e.Graphics.DrawRectangle(pen, rectangle);
                    var progressBrush = new LinearGradientBrush(new Point(progressRectangle.Left, progressRectangle.Top - 1), new Point(progressRectangle.Left, progressRectangle.Bottom), Color.White, Color.LimeGreen);
                    e.Graphics.FillPath(progressBrush, progressGraphicsPath);
                    //e.Graphics.FillRectangle(progressLinearGradientBrush, progressRectangle);

                    //GraphicsPath path = RoundedRectangle.Create(rectangle);
                    //e.Graphics.DrawPath(Pens.Black, path);
                    //System.IntPtr ptrBorder = CreateRoundRectRgn(e.Bounds.Left, e.Bounds.Top, e.Bounds.Left + 50, e.Bounds.Bottom, 5, 5);
                    //try { SetWindowRgn(tvTreeView.Handle.ToInt32(), ptrBorder.ToInt32(), 1) ; }
                    //finally { DeleteObject(ptrBorder); }
                }
            }

            var textSize = e.Graphics.MeasureString(task.Name, tvTreeView.Font);
            var controlText = SystemBrushes.ControlText;
            e.Graphics.DrawString(task.Name, tvTreeView.Font, controlText, e.Bounds.Left - 1, e.Bounds.Top + e.Bounds.Height / 2f - textSize.Height / 2f);

            //if ((e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected)
            //    controlText = SystemBrushes.HighlightText;
        }

        public void Task_Progress(Nodes.INode sender, float progress = 0, float maxProgress = 100)
        {
            if (IsDisposed) return;
            if (InvokeRequired)
            {
                Invoke(new ProgressDelegate(Task_Progress), sender, progress, maxProgress);
            }
            else
            {
                if (tvTreeView.IsDisposed) return;
                var treeNode = Find(sender);
                if (treeNode != null)
                {
                    tvTreeView.Invalidate(treeNode.Bounds);
                }
            }
        }

        public void Task_TaskStatusChanged(Task sender, TaskStatus taskStatus)
        {
            if (IsDisposed) return;
            if (InvokeRequired)
            {
                Invoke(new Task.TaskStatusDelegate(Task_TaskStatusChanged), sender, taskStatus);
            }
            else
            {
                if (tvTreeView.IsDisposed) return;
                var treeNode = Find(sender);
                if (treeNode != null)
                {
                    treeNode.ImageKey = StatusIcon(taskStatus);
                    treeNode.SelectedImageKey = treeNode.ImageKey;
                }
            }            
        }
    }
}

以及如何使用:

using System;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using Task = Retalix.R10.DeveloperWorkbench.Nodes.Task;

namespace DeveloperWorkbench.UI
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            BuildTaskHierarchy();
        }

        private Task _rootTask;
        public void BuildTaskHierarchy()
        {
            var roottaskXml = @"c:\temp\roottask.xml";
            if (File.Exists(roottaskXml))
            {
                //method hierarchy can be deserialized...
                _rootTask = (Task)Serialization.Deserialize(typeof(Task), roottaskXml);
                _rootTask.SetMethod(target: this);
            }
            else
            {
                //...or constructed from scratch
                _rootTask = Task.Create("Avert War With The Klingons");
                Task.Create(GetToTheEnterprise, _rootTask);
                var taskC = Task.Create("Kill General Chang", _rootTask);
                Task.Create(FindThatThingsTailpipe, taskC);
                Task.Create(TargetThatExplosionAndFire, taskC);
                Task.Create(ThwartCampKhitomerAssassination, _rootTask);
                Task.Create(ExplainToHighCommand, _rootTask);
                Serialization.Serialize(_rootTask, roottaskXml);
            }

            statusList1.Add(_rootTask);
        }

        private void GetToTheEnterprise()
        {
            LongOp();
        }

        private void FindThatThingsTailpipe()
        {
            LongOp();
        }

        private void TargetThatExplosionAndFire()
        {
            LongOp();
        }

        private void ThwartCampKhitomerAssassination()
        {
            LongOp();
        }

        private void ExplainToHighCommand()
        {
            LongOp();
        }

        private void LongOp()
        {
            var task = Task.CurrentTask(_rootTask, 2);
            task.MaxProgress = 100;
            for (var i = 0; i <= 50; i++)
            {
                task.Progress = i*2;
                Thread.Sleep(25);
            }

        }

        private void button1_Click(object sender, EventArgs e)
        {
            _rootTask.Execute();
        }
    }
}

我只是在发布我的进度。我已经在我的实际应用程序中测试过了,它是有效的。我仍然需要一个方便的函数来提高任何方法的进度。我仍然在寻找关于如何减少这里所需的工具的反馈。我想要尽可能少的侵入性策略。在运行时监视调用链的东西将是一个很棒的补充。

sg24os4d

sg24os4d1#

Progress类使得用进度更新UI变得非常容易。
只需在UI中创建一个progress示例;它可以获取后台进程当前拥有的任何信息,并相应地更新UI:

Progress<Tuple<Operation, int>> progress = new Progress<Tuple<Operation, int>>();
progress.ProgressChanged += (_, info) =>
{
    TreeView node = GetTreeviewFromOperation(info.Item1);
    UpdateNodeWithProgress(node, info.Item2);
};

您可以根据自己的情况进行调整。假定后台进程将具有某种类型来表示操作,您可以将其Map回表示该操作的树节点。您还可以传递更新UI所需的任何其他信息。如果您有大量信息要传递,请考虑创建一个新的命名类型来表示它。而不是像这里一样使用Tuple
然后将进度传递给后台进程,不管它是什么(它可能是一个新线程、一个任务、一个异步方法的回调,或者其他什么)。

//this is the work to do in the background
public static void DoWork(IProgress<Tuple<Operation, int>> progress)
{
    Thread.Sleep(1000); //placeholder for real work
    progress.Report(something, 50);
}

//start that work in a new task; call from the UI thread 
//right after creating the `Progress` instance
Task.Run(()=> DoWork(progress));

如果您没有.NET 4.5,则可以相当轻松地创建自己的此类版本:

public interface IProgress<T>
{
    void Report(T data);
}

public class Progress<T> : IProgress<T>
{
    SynchronizationContext context;
    public Progress()
    {
        context = SynchronizationContext.Current
            ?? new SynchronizationContext();
    }

    public Progress(Action<T> action)
        : this()
    {
        ProgressReported += action;
    }

    public event Action<T> ProgressReported;

    void IProgress<T>.Report(T data)
    {
        var action = ProgressReported;
        if (action != null)
        {
            context.Post(arg => action((T)arg), data);
        }
    }
}
eh57zj3b

eh57zj3b2#

阅读BackgroundWorker类,它是一种古老但非常简单的方法,可以在不涉及线程复杂性的情况下完成后台工作。
您所要做的就是创建一个,处理它的DoWork事件来执行您的逻辑(它将在后台运行),并通过它的ReportProgress函数将进度传递回主ui线程,然后您将处理该函数来更新树的ui。

beq87vna

beq87vna3#

更新UI的最佳选择是将同步责任留给.NET本身。利用asyncawait,在Task完成时帮助从后台线程切换到UI线程。
This library解决了您所需的确切目的。
看看WPF和Blazor here的例子。
NUGET包here

相关问题