我已经制作了一个Windows Forms应用程序来跟踪机器上运行的所有进程,它还节省了应用程序处于“活动”状态的时间,活动应用程序是当前重点关注的应用程序,例如您的浏览器+时不时地提醒我(每个小时)我花了多少时间。

除此之外,它还显示了所有正在运行的进程以及有关它们的一些基本信息,不同的排序选项以及升序/降序。

外观如下:


更新

此行-winforms是不强大的功能似乎引起了一些争议,显然我是错的。正如CodyGray指出并经t3chb0t证明的那样,如果适当地优化了程序并以应有的方式使用控件,则Windows窗体可以非常快速地运行。 winforms功能不强大,因此每10秒刷新一次内容需要1-2秒,除非手动请求。

这里是主要代码: />
这里只有一种扩展方法,可以帮助我使列的宽度均匀,这样它们看起来不会空着,但是也不会隐藏太多内容:

public partial class Form1 : Form
{
    private class ProcessInfo
    {
        public Process Process { get; }
        public TimeSpan TimeActive { get; set; }

        public ProcessInfo(Process process, TimeSpan timeActive)
        {
            Process = process;
            TimeActive = timeActive;
        }
    }

    private readonly Timer updateTimer = new Timer();
    private readonly Timer focusTimeTimer = new Timer();

    private Dictionary<int, Process> processesInfo = new Dictionary<int, Process>();
    private List<KeyValuePair<int, Process>> orderedProcessesInfo;

    private Dictionary<string, Action> sortingActions;

    private Dictionary<string, Action> orderingActions;

    private bool isAscendingOrder = false;

    private static Dictionary<int, ProcessInfo> processesActiveTime = new Dictionary<int, ProcessInfo>();

    private static readonly Func<Process, int> GetMemoryUsageInMB = p => (int) (p.WorkingSet64 / (1024 * 1024));
    private static readonly Func<Process, TimeSpan> GetRuntimeOfProcess = p => DateTime.Now - p.StartTime;
    private static readonly Func<Process, TimeSpan> GetActiveTimeOfProcess = p => processesActiveTime[p.Id].TimeActive;

    //save state after update
    private string lastSortAction = string.Empty;

    public Form1()
    {
        InitializeComponent();
        LoadProcesses();
        InitializeSortingActions();
        InitializeOrderingActions();
        UpdateProcessList();

        updateTimer.Interval = 1000 * 10;
        updateTimer.Tick += UpdateTimer_Tick;
        updateTimer.Start();

        focusTimeTimer.Interval = 1000;
        focusTimeTimer.Tick += FocusTimeTimer_Tick;
        focusTimeTimer.Start();
    }

    private void FocusTimeTimer_Tick(object sender, EventArgs e)
    {
        TextBoxProcessCount.Text = processesInfo.Count.ToString();
        IntPtr activatedHandle = GetForegroundWindow();
        if (activatedHandle == IntPtr.Zero)
        {
            return;
        }
        int activeProcessId;
        GetWindowThreadProcessId(activatedHandle, out activeProcessId);
        ProcessInfo activeProcess;
        if (processesActiveTime.TryGetValue(activeProcessId, out activeProcess))
        {
            activeProcess.TimeActive =
                activeProcess.TimeActive.Add(new TimeSpan(0, 0, focusTimeTimer.Interval / 1000));
            if (activeProcess.TimeActive.Seconds == 0 && activeProcess.TimeActive.Minutes == 0 &&
                activeProcess.TimeActive.TotalHours > 0)
            {
                MessageBox.Show(
                    $@"You've spent {activeProcess.TimeActive.TotalHours} on {activeProcess.Process.ProcessName}");
            }
        }
        else
        {
            LoadProcesses();
            UpdateProcessList();
        }
    }

    private void LoadProcesses()
    {
        if (processesActiveTime.Count > 0)
        {
            try
            {
                processesActiveTime =
                    processesActiveTime.Where(p => !p.Value.Process.HasExited)
                        .ToDictionary(pair => pair.Key, pair => pair.Value);
            }
            catch (InvalidOperationException) { }
        }

        processesInfo.Clear();
        Process[] allProcesses = Process.GetProcesses();
        foreach (var process in allProcesses)
        {
            try
            {
                //ensures process wont deny access
                if (!process.HasExited)
                {
                    DateTime runtime = process.StartTime;
                }
            }
            catch (Win32Exception)
            {
                continue;
            }
            try
            {
                //ensures process wont exit
                processesInfo.Add(process.Id, process);
                if (!processesActiveTime.ContainsKey(process.Id))
                {
                    processesActiveTime.Add(process.Id, new ProcessInfo(process, new TimeSpan()));
                }
            }
            catch (InvalidOperationException) { }
        }
        orderedProcessesInfo = processesInfo.ToList();
    }

    private void InitializeSortingActions()
    {
        sortingActions = new Dictionary<string, Action>
        {
            ["Name"] = () => SortProcesses(p => p.ProcessName),
            ["Status"] = () => SortProcesses(p => p.Responding),
            ["Start Time"] = () => SortProcesses(p => p.StartTime),
            ["Total Runtime"] = () => SortProcesses(p => GetRuntimeOfProcess(p)),
            ["Memory Usage"] = () => SortProcesses(p => GetMemoryUsageInMB(p)),
            ["Active Time"] = () => SortProcesses(p => GetActiveTimeOfProcess(p))
        };

        foreach (var sortingAction in sortingActions)
        {
            ComboBoxSorting.Items.Add(sortingAction.Key);
        }
    }

    private void InitializeOrderingActions()
    {
        orderingActions = new Dictionary<string, Action>
        {
            ["Ascending"] = () =>
            {
                isAscendingOrder = true;
                if (!string.IsNullOrEmpty(lastSortAction))
                {
                    sortingActions[lastSortAction].Invoke();
                }
            },
            ["Descending"] = () =>
            {
                isAscendingOrder = false;
                if (!string.IsNullOrEmpty(lastSortAction))
                {
                    sortingActions[lastSortAction].Invoke();
                }
            },
        };
        foreach (var orderingAction in orderingActions)
        {
            ComboBoxOrders.Items.Add(orderingAction.Key);
        }
    }

    private void SortProcesses<T>(Expression<Func<Process, T>> lambda)
        where T : IComparable
    {
        orderedProcessesInfo.RemoveAll(p => p.Value.HasExited);

        orderedProcessesInfo.Sort(
            (process1, process2) =>
                lambda.Compile()
                    .Invoke(process1.Value).CompareTo(lambda.Compile()
                        .Invoke(process2.Value)));
        if (isAscendingOrder)
        {
            orderedProcessesInfo.Reverse();
        }
        processesInfo = orderedProcessesInfo.ToDictionary(pair => pair.Key, pair => pair.Value);

        UpdateProcessList();
    }

    private void UpdateTimer_Tick(object sender, EventArgs e)
    {
        RefreshList();
    }

    public void UpdateProcessList()
    {
        //refresh the timer's interval
        updateTimer.Stop();
        updateTimer.Start();

        ListViewProcesses.Clear();

        ListViewProcesses.Columns.Add("Name".ExtendWithEmptySpaces(GetAverageLengthOf(p => p.ProcessName.Length)));
        ListViewProcesses.Columns.Add("Status");
        ListViewProcesses.Columns.Add("Total Runtime");
        ListViewProcesses.Columns.Add("Active Runtime");
        ListViewProcesses.Columns.Add(
            "Start time".ExtendWithEmptySpaces(GetAverageLengthOf(p => p.StartTime.ToString().Length)));
        ListViewProcesses.Columns.Add(
            "Memory Usage".ExtendWithEmptySpaces(GetAverageLengthOf(p => GetMemoryUsageInMB(p).ToString().Length)));
        ListViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
        ListViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);

        foreach (var processInfo in processesInfo)
        {
            TimeSpan runtime = GetRuntimeOfProcess(processInfo.Value);
            TimeSpan activeTime = GetActiveTimeOfProcess(processInfo.Value);
            ListViewProcesses.Items.Add(
                CreateListViewRow(
                    name: processInfo.Value.ProcessName,
                    status: processInfo.Value.Responding ? "Active" : "Not responding",
                    runtime: $"{(int) runtime.TotalHours} h : {runtime.Minutes} min",
                    activeTime: $"{(int) activeTime.TotalHours} h : {activeTime.Minutes} min",
                    startTime: processInfo.Value.StartTime.ToString("g"),
                    memoryUsage: GetMemoryUsageInMB(processInfo.Value) + " MB"));
        }
    }

    private void bUpdate_Click(object sender, EventArgs e)
    {
        RefreshList();
    }

    private void RefreshList()
    {
        LoadProcesses();
        if (!string.IsNullOrEmpty(lastSortAction))
        {
            sortingActions[lastSortAction].Invoke();
        }
        else
        {
            UpdateProcessList();
        }
    }

    private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
        string startTime, string memoryUsage)
        => new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});

    private int GetAverageLengthOf(Func<Process, int> predicate)
        => (int) Math.Ceiling(processesInfo.Values.Where(p => !p.HasExited).Average(predicate.Invoke));

    private void ComboBoxSorting_SelectedIndexChanged(object sender, EventArgs e)
    {
        lastSortAction = ((Control) sender).Text;
        sortingActions[lastSortAction].Invoke();
    }

    private void ComboBoxOrders_SelectedIndexChanged(object sender, EventArgs e)
    {
        orderingActions[((Control)sender).Text].Invoke();
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    private static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);
}


随意使用它,但请记住,除非打开程序,否则无法记录您在应用程序上花费了多少时间。

评论

“ winforms并不强大”这根本不是事实。 WinForms应用程序运行缓慢的原因有两个:(1)代码编写效率不高(与所有内容一样),(2)它是.NET框架,托管应用程序在本质上仍然比非托管本机编译应用程序运行慢。这里的许多低效率只是标准的C#/。NET低效率,与WinForms本身无关。

我有点被您的声明@denis触发。 WinForms仍然很强大。

@ t3chb0t到目前为止,我知道如果要使用WinForms和C ++,托管代码是无法解决的,还是我错了?如果是这样,则使用C ++的WinForms的运行速度应仅比使用C#的WinForms快。但是WinForms的运行速度非常快,如果人们不尝试答案中提到的东西(每次刷新._。都会添加(..)。)都应该没有问题。

@ t3chb0t我经历了许多GUI系统(java swing,javafx,Qt,wxWidgets,WinForms和WPF),我只能同意GUI功能的原始OS功能是最快的。我绘制了Waveforms,并且对性能非常严格,毕竟WinForms在性能方面是完美的。结论:WinForms并不慢!

在WinForms中,ListView闪烁。这是一个错误。在线提供了许多解决方法。从此处开始进行排序。另请参阅此博客文章。我有自己的实现,可以在类库中修复这些错误以及更多错误,因此我可以改用它。其他UI问题是由于操作效率低下或无法使用BeginUpdate / EndUpdate引起的。如果正确执行此操作,则不会有问题。没有人需要一个表单上的100多个控件;这是用户体验问题。

#1 楼

性能


[..] winforms功能不强大,因此刷新内容需要1-2秒[..]


WinForms,因为它实际上非常快,而且我从未遇到过任何问题。代码效率低下的可能性为99.99%,因此让我们看一下。

Expression.Compile()

这是使应用程序变慢的地方,




Expression s被咬住。


Compile s被咬住。它们非常昂贵,不需要它们和Expression,因为没有动态变化的东西。您总是有一个Process和一个要获取和比较的值。仅使用Func

private void SortProcesses<T>(Expression<Func<Process, T>> lambda)
  where T : IComparable
{
  orderedProcessesInfo.RemoveAll(p => p.Value.HasExited);

  orderedProcessesInfo.Sort(
      (process1, process2) =>
          lambda.Compile()
              .Invoke(process1.Value).CompareTo(lambda.Compile()
                  .Invoke(process2.Value)));
  if (isAscendingOrder)
  {
      orderedProcessesInfo.Reverse();
  }
  processesInfo = orderedProcessesInfo.ToDictionary(pair => pair.Key, pair => pair.Value);

  UpdateProcessList();
}


这是一种排序方法,它需要快速运行。如果不是,那么您会立刻注意到它。


这是堆栈溢出上@Eric的Lippert答案的链接,解释了Compile方法的作用:Lambda Expression Compile( )方法执行吗? />

我不喜欢的另一件事是这个


private void SortProcesses<T>(Func<Process, T> getProperty)
    where T : IComparable
{

    // ...

    orderedProcessesInfo.Sort((process1, process2) =>
            getProperty(process1.Value)
            .CompareTo(getProperty(process2.Value))
    );

    // ...
}



Sort应该已经产生了正确的顺序。上一行似乎无法正确进行排序,您需要此解决方法来解决它,而不是要解决排序功能。


ListView.Items.Add()

第二种可能使您认为WinForms表现不佳的方法是这种方法:


orderedProcessesInfo.Reverse()



您在此处调用


public void UpdateProcessList()


[..]此方法从ListView控件中删除所有项目和列


只是立即使用


ListViewProcesses.Clear();



重新创建列您是否真的想在每次刷新列表时都这样做?您已经创建了一次列表视图。我想您真正想做的就是用ListView.Items.Clear()删除所有项目。

并在循环中将许多行添加到列表而不暂停它确实会影响性能,因为list-view每次更改后都会保持刷新。


ListViewProcesses.Columns.Add(..);



考虑使用BeginUpdateEndUpdate方法,并添加介于两者之间或更好的行,请使用AddRange方法:


将多个项目添加到ListView的首选方法是使用ListView.ListViewItemCollection的AddRange方法(可通过ListView的Items属性访问)。这使您可以通过一次操作将一系列项目添加到列表中。但是,如果要一次使用ListView.ListViewItemCollection类的Add方法一次添加一项,则可以使用BeginUpdate方法来防止控件在每次添加项时重新绘制ListView。完成向控件添加项目的任务后,请调用EndUpdate方法以使ListView重新绘制。当将大量项目添加到控件时,这种添加项目的方法可以防止ListView的闪烁。


ListView.BeginUpdate方法


要进一步提高性能,您可以尝试从


foreach (var processInfo in processesInfo)
{     
    // ..
    ListViewProcesses.Items.Add(..);
    // ..
}
派生自己的list-view-item,幸运的是,密封。然后,无需重新添加所有项目,您只需刷新项目即可,列表视图仅可以使用ListView.Refresh()更新值。要跟踪列表视图项和流程,可以使用另一个字典,如果添加或删除了新流程,则仅添加/删除一个而不是全部。


设计


public class ListViewItem



我觉得这三个领域非常令人困惑,因为它们听起来是如此相似。

这个怎么样。首先将其重命名

private Dictionary<string, Action> sortingActions;

private Dictionary<string, Action> orderingActions;

private bool isAscendingOrder = false;


使用新的定义,您可以选择列(值)而不调用排序:
然后将另一个字典重命名为

sortingActions -> selectColumn


其中键不再是字符串而是枚举:

private Dictionary<string, Func<Process, IComparable>> selectColumn;

selectColumn = new Dictionary<string, Func<Process, IComparable>>
{
    ["Name"] = p => p.ProcessName,
    ["Status"] = p => p.Responding,
    ["Start Time"] = p => p.StartTime,
    ["Total Runtime"] = p => GetRuntimeOfProcess(p),
    ["Memory Usage"] = p => GetMemoryUsageInMB(p),
    ["Active Time"] = p => GetActiveTimeOfProcess(p)
};


,因此新字典现在具有更强大的键,并且它的项通过使用第一个字典来获取用于获取列(值)的委托来触发排序功能

orderingActions -> orderBy


其中orderByColumn是要在某处设置的要排序的列的名称。

isAscendingOrder现在变为currentOrderBy

enum OrderBy
{
    Ascending,
    Descending
}


(免责声明:记事本编码,可能尚未100%正确)

评论


\ $ \ begingroup \ $
嘿,好人!您在性能部分提到的事情对我来说是如此清晰,我总是问自己为什么其他人不考虑add(..)之类的东西在做什么以及花费多少时间。 [编辑]
\ $ \ endgroup \ $
– Arthur M.
17年1月8日在9:18

\ $ \ begingroup \ $
确实,这就是C#(或编码它的人)的问题,所有这些闪亮的黑匣子使您很容易编写看起来很酷但效率低下的代码,如果您不知道其底层是什么。
\ $ \ endgroup \ $
– 404
17年1月8日在10:57

\ $ \ begingroup \ $
再次感谢您提出的很棒的建议@ t3chb0t,我删除了list.Reverse(),现在我只比较2-> 1表示升序,1-> 2表示降序
\ $ \ endgroup \ $
–丹尼斯
17年1月8日在14:36

\ $ \ begingroup \ $
值得指出的是,如果要删除所有项目而不是列,则listview.Items.Clear()比listview.Clear()更可取。
\ $ \ endgroup \ $
– IanF1
17年1月8日在15:09

\ $ \ begingroup \ $
@ IanF1是的,我应该从一开始就提到它。更新。
\ $ \ endgroup \ $
–t3chb0t
17年1月8日在15:16

#2 楼

我要做的第一件事是将if (mainString.Length == desiredLength)更改为if (mainString.Length >= desiredLength),因为如果更长,它将仅对StringBuilder做过多的工作,并且您可能会在满足条件的所有条件下尽早返回。


我也可以结合使用以下try / catch块:


try
{
    //ensures process wont deny access
    if (!process.HasExited)
    {
        DateTime runtime = process.StartTime;
    }
}
catch (Win32Exception)
{
    continue;
}
try
{
    //ensures process wont exit
    processesInfo.Add(process.Id, process);
    if (!processesActiveTime.ContainsKey(process.Id))
    {
        processesActiveTime.Add(process.Id, new ProcessInfo(process, new TimeSpan()));
    }
}
catch (InvalidOperationException) { }



To:

try
{
    //ensures process wont deny access
    if (!process.HasExited)
    {
        DateTime runtime = process.StartTime;
    }

    //ensures process wont exit
    processesInfo.Add(process.Id, process);
    if (!processesActiveTime.ContainsKey(process.Id))
    {
        processesActiveTime.Add(process.Id, new ProcessInfo(process, new TimeSpan()));
    }
}
catch (Win32Exception) { continue; }
catch (InvalidOperationException) { }

/>

对于表达式强壮的成员,我发现'lambda'语法与该成员在同一行时更容易阅读。


private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
    string startTime, string memoryUsage)
    => new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});



到:

private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
    string startTime, string memoryUsage) =>
    new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});



接下来,从上面看相同的方块:


private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
    string startTime, string memoryUsage) =>
    new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});



如果要破坏其中一个,请在新行中断开每个参数:

private static ListViewItem CreateListViewRow(string name,
                                              string status,
                                              string runtime
                                              string activeTime,
                                              string startTime,
                                              string memoryUsage) =>
    new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});



一旦这样做,我们就会发现它有点丑陋,这种方法不适用于表达强健的成员:

private static ListViewItem CreateListViewRow(string name,
                                              string status,
                                              string runtime
                                              string activeTime,
                                              string startTime,
                                              string memoryUsage)
{
    return new ListViewItem(new[]
    {
        name,
        status,
        runtime,
        activeTime,
        startTime, 
        memoryUsage
    });
}



我会稍后再添加,现在很晚了,可能还有很多话要说。 r />

#3 楼


我在列表视图中添加了一些虚拟行,并尝试将其刷新几次,以查看其外观。并猜测它闪烁了什么,大概需要100毫秒来刷新列表视图。我认为这很慢,只有4-5个空行刷新来了,您不能这么快。尝试在表单上放很多控件,这些控件最终将放弃。关于Windows窗体,最糟糕的部分可能是控件的绘制。


正确,这太可怕了,所以这里是一个更快更脏的示例,一个ListView每0.5刷新100个项目秒,每个值每50毫秒刷新一次。没有闪烁,没有延迟。速度如此之快,应该如此。如果不是,则表示不正确。

可以在LINQPad中运行:

void Main()
{
    var form = new Form();

    var lv = new MyListView
    {
        Dock = DockStyle.Fill
    };
    lv.Columns.Add("Column1");
    lv.Columns.Add("Column2");
    lv.Columns.Add("Column3");
    lv.View = View.Details;
    form.Controls.Add(lv);
    lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);

    var itemsTimer = new System.Windows.Forms.Timer
    {
        Interval = 500,
    };

    var suspendLayout = true;
    var itemCount = 100;

    var valueTimer = new System.Windows.Forms.Timer
    {
        Interval = 50
    };

    itemsTimer.Tick += (sender, e) =>
    {
        valueTimer.Stop();
        if (suspendLayout) lv.BeginUpdate();
        lv.Items.Clear();
        for (int i = 0; i < itemCount; i++)
        {
            lv.Items.Add(new ListViewItem(new string[] { "Foo", "Bar", "Baz" }));
        }
        if (suspendLayout) lv.EndUpdate();
        valueTimer.Start();
    };
    itemsTimer.Start();


    var rnd = new Random();
    valueTimer.Tick += (sender, e) =>
    {
        if (suspendLayout) lv.BeginUpdate();
        for (int i = 0; i < itemCount; i++)
        {
            lv.Items[i].SubItems[rnd.Next(0, 3)].Text = rnd.Next(0, 10).ToString();
        }
        if (suspendLayout) lv.EndUpdate();
    };

    form.FormClosing += (sender, e) =>
    {
        valueTimer.Stop();
        itemsTimer.Stop();
    };

    form.Show();
}

class MyListView : ListView
{
    public MyListView() { DoubleBuffered = true; }
    protected override bool DoubleBuffered { get; set; }
}




#4 楼

仅关注以下内容:
public static class Extensions
{
    public static string ExtendWithEmptySpaces(this string mainString, int desiredLength)
    {
        if (mainString.Length == desiredLength)
        {
            return mainString;
        }
        StringBuilder extendedStringBuilder = new StringBuilder(mainString);
        for (int i = 0; i < desiredLength - mainString.Length; i++)
        {
            extendedStringBuilder.Append(" ");
        }
        return extendedStringBuilder.ToString();
    }
}
您有一个public方法,每个人都可以调用,因此您应该添加一些适当的验证。您希望封装该方法的内部工作,因此不想公开有关该方法的实现细节。

如果mainStringnull会发生什么?通过访问LengthmainString属性,将抛出ArgumentNullException,并且堆栈跟踪将准确地表明这不是您想要的。您不想公开使用该字符串的Length属性,而只是mainStringnull

如果desiredLength为负会怎样?没什么不好,但是您应该通过抛出ArgumentOutOfRangeException来告诉代码调用者他/她做错了什么,否则,调用者代码中可能会出现隐藏的错误。

该扩展方法的名称在某种程度上具有误导性。怎样才能腾空?

话虽如此,您正在这里重新发明轮子,因为已经有一个经过优化的.NET方法,即String.PadRight(Int32),您应该改用它。每个体面的程序员都知道这种方法,并且在代码中找到它也不会感到惊讶。