I've made a Windows Forms application to track all of the processes running on my machine and it also saves the time an application is "active", an active application is the one that is on focus currently e.g your browser right now + it also reminds me every now and then (every hour) how much time I've spent on the internet.
Aside from that it shows all of your running processes with some basic information about them, there are also several different sorting options along with ascending/descending ordering.
Here's how it looks:
enter image description here Update
This line - winforms is not powerful seems to have caused quite some controversy and apparently I'm wrong. As pointed by CodyGray and proven by t3chb0t, windows forms can be really fast if the program is optimized properly and the controls are used the way they are meant to be used.
It works on a single thread (削除) and winforms is not powerful (削除ここまで) so it takes 1-2 seconds to refresh the content which happens every 10 seconds, unless requested manually.
Here is the main code:
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);
}
There is just 1 extension method here which helps me even out the width of the columns so they don't look empty but they aren't hiding too much content either :
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();
}
}
Feel free to use it, but keep in mind that there is no way to record how much time you spent on applications unless the program is opened.
4 Answers 4
Performance
[..] winforms is not powerful so it takes 1-2 seconds to refresh the content [..]
It's not WinForms because it's actually very fast and I have never had any issues with it. There is a 99.99% chance that the code is inefficient so let's have a look.
Expression.Compile()
This is what slows the application down and where the bottle-neck is hidden and where the Expression
s bite.
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(); }
There are two (!) Compile
s. They are very expensive and there is no need for them and the Expression
because there is nothing that dynamically changes. You have always a Process
and a value to get and to compare. Use just the Func
:
private void SortProcesses<T>(Func<Process, T> getProperty)
where T : IComparable
{
// ...
orderedProcessesInfo.Sort((process1, process2) =>
getProperty(process1.Value)
.CompareTo(getProperty(process2.Value))
);
// ...
}
This is a sort method and it needs to run fast. If it doesn't, you'll notice it right away.
Here's a link to @Eric's Lippert answer on Stack Overflow explaining what the Compile
method does: What does Lambda Expression Compile() method do?.
And one more link to another answer (by someone else) on Stack Overflow comparing execution times of various method calls: Performance of Expression.Compile vs Lambda, direct vs virtual calls
Another thing that I don't like about it is this
orderedProcessesInfo.Reverse()
The Sort
should already produce the right order. The above line looks like the sorting wouldn't work correctly and you need this workaround to fix it instead of fixing the sort function.
ListView.Items.Add()
The second method that most likely makes you think WinForms wouldn't be performing well is this one:
public void UpdateProcessList()
You call here
ListViewProcesses.Clear();
[..] this method removes all items and columns from the ListView control
only to immediately recreate the columns with
ListViewProcesses.Columns.Add(..);
Do you really want to do it each time you refresh the list? You have already created the list-view once. I guess what you really wanted to do is to just remove all the items with ListView.Items.Clear()
.
This and adding the many rows to the list in a loop without suspending it really hurt the performance because the list-view keeps refreshing after each change.
foreach (var processInfo in processesInfo) { // .. ListViewProcesses.Items.Add(..); // .. }
Consider using the BeginUpdate
and EndUpdate
methods and adding the rows inbetween or better, use the AddRange
method:
The preferred way to add multiple items to a ListView is to use the AddRange method of the ListView.ListViewItemCollection (accessed through the Items property of the ListView). This enables you to add an array of items to the list in a single operation. However, if you want to add items one at a time using the Add method of the ListView.ListViewItemCollection class, you can use the BeginUpdate method to prevent the control from repainting the ListView every time that an item is added. When you have completed the task of adding items to the control, call the EndUpdate method to enable the ListView to repaint. This way of adding items can prevent flickered drawing of the ListView when lots of items are being added to the control.
To further improve the performance you could try to derive your own list-view-item from the
public class ListViewItem
that luckily isn't sealed. Then instead of re-adding all items you could just refresh the items and the list-view could just update the values with ListView.Refresh()
. To keep track of the list-view-items and processes you could use another dictionaray and if a new process is added or removed you add/remove just this one and not all of them.
Design
private Dictionary<string, Action> sortingActions; private Dictionary<string, Action> orderingActions; private bool isAscendingOrder = false;
I find these three fields very confusing because they sound so similar.
How about this. First rename this one
sortingActions -> selectColumn
With the new defintion you selects the column (value) without invoking sorting yet:
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)
};
Then rename the other dictionary to
orderingActions -> orderBy
where the key is no longer a string but an enum:
enum OrderBy
{
Ascending,
Descending
}
so the new dictionary has now stronger keys and its items trigger the sort function by using the first dictionary to get the delegate for getting the column (value)
orderBy = new Dictionary<OrderBy, Action>
{
[OrderBy.Ascending] = () =>
{
SortProcesses(selectColumn[orderByColumn]);
currentOrderBy = OrderBy.Ascending;
},
[..]
}
where orderByColumn
is the name of the column to be ordered by that you set somewhere.
The isAscendingOrder
now becomes currentOrderBy
private OrderBy currentOrderBy = OrderBy.Descending;
(disclaimer: notepad coding, may not be 100% correct yet)
-
2\$\begingroup\$ Hey hey, nice one! Things you mention in the performance section are so clear to me, that I always ask myself why other people don't think about what such stuff as
add(..)
is doing and how many time it costs. [Edited] \$\endgroup\$realvictorprm– realvictorprm2017年01月08日 09:18:25 +00:00Commented Jan 8, 2017 at 9:18 -
8\$\begingroup\$ Indeed and that's the problem with C# (or rather those who code it), there's all this shiny black box stuff which makes it so easy to write cool-looking but inefficient code if you don't know what's going on underneath. \$\endgroup\$404– 4042017年01月08日 10:57:19 +00:00Commented Jan 8, 2017 at 10:57
-
\$\begingroup\$ Thank you for the great suggestions once again @t3chb0t, I removed the list.Reverse() and now i'm just comparing 2 -> 1 for ascending and 1 -> 2 for descending \$\endgroup\$Denis– Denis2017年01月08日 14:36:58 +00:00Commented Jan 8, 2017 at 14:36
-
2\$\begingroup\$ Worth pointing out that if you want to remove all items but not columns, listview.Items.Clear() is preferable to listview.Clear(). \$\endgroup\$IanF1– IanF12017年01月08日 15:09:34 +00:00Commented Jan 8, 2017 at 15:09
-
1\$\begingroup\$ @IanF1 You're right, I should have mentioned it from the beginning. Updated. \$\endgroup\$t3chb0t– t3chb0t2017年01月08日 15:16:06 +00:00Commented Jan 8, 2017 at 15:16
First thing I would do is change if (mainString.Length == desiredLength)
to if (mainString.Length >= desiredLength)
, since if it's longer it'll just do excess work with the StringBuilder
and you may as well return early on all conditions that would satisfy it.
I would also combine this try
/catch
block:
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) { }
With expression-bodied members, I've found it's easier to read when the 'lambda' syntax is on the same line as the member.
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});
To:
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});
Next, same block from above:
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});
Break each parameter on a new line if you're going to break one of them:
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});
Once we do that we see that it's a bit ugly, this method isn't a place for expression-bodied members:
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
});
}
I'll try to add more later, quite late right now and there's probably a lot more to say.
I added a few dummy rows in the listview and tried to refresh it a few times, to see how it looks. And guess what it was flickering and it was taking around 100ms maybe to refresh the list view. Which I consider slow, that's just 4-5 empty row refreshed come on, you can't call that fast. Try putting a lot of controls on your form it will give up eventually. The worst part about windows forms is probably the drawing of controls.
True, this would be terrible so here is one more quick & dirty example of a ListView
being refreshed with 100 items every 0.5sec and each value is refreshed every 50ms. No flickering, no delays. It's fast as hell and it should be. If it's not then something's not right.
This can be run in 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; }
}
Focusing only on:
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(); } }
You have a public
method which everyone can call so you should add some proper validation. You want to encapsulate the inner working of that method and therefore you don't want to expose implementation details about it.
What happens if mainString
is null
? By accessing the Length
property of the mainString
an ArgumentNullException
is thrown and the stacktrace will indicate exactly this which isn't what you want. You don't want to expose that you use the Length
property of that string but only that mainString
is null
.
What happens if desiredLength
is negative ? Nothing bad, but you should tell the caller of the code that he/she did something wrong by throwing an ArgumentOutOfRangeException
otherwise a hidden bug in the callers code can occur.
The name of that extension method is somehow misleading. How can a space be empty ?
That being said, you are reinventing the wheel here, because there is already a well optimized .NET method namely String.PadRight(Int32)
which you should use instead. Every decent programmer knows this method and wouldn't be surprised finding it in the code.
Explore related questions
See similar questions with these tags.
add(..)
every refresh ._.). \$\endgroup\$