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:
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.
add(..)
every refresh ._.). \$\endgroup\$