Create your own Process Explorer

I was playing around with showing some resizable content in a WPF window. I wanted 2 variable sized Lists, one on top of the other. The lists had various lengths, but also various widths: the user could adjust the size of the columns.

This smelled like a GridSplitter. That means I need 3 rows in a grid, with the middle one containing the splitter. For the lists at the top and bottom in the sample below, I used the Browse, as described here: Write your own Linq query viewer.

For the data, I used the running processes on your machine in the top view, with the bottom view showing the modules of the process selected in the top view.

This seemed to work fine initially. Dragging the splitter around seemed to resize the top and bottom just fine. However, because the Top and Bottom were both StackPanels, the ListViews inside would grow to fit, and so the vertical scrollbars would not show. Thus if the list were long, it was impossible to see the last items.

To fix this, the code subscribes to the SizeChanged event for the Top DockPanel (the Splitter implies that if the Top Size changed, the Bottom size changed as well). Here, it just adjusts the height of the Browse to the DockPanel ActualHeight.

Additional notes:

1. In a process explorer, it’s useful to know if the process is 32 bit or 64 bit. I added an Extension Method “ProcType” to the Process class that returns this information. It defaults to “?” in case of errors, like “Access Denied”

2. The code is all C# for ease of copy/paste, but you can use Xaml as well. The GridLengthConverter is used to create GridLength instances using the same syntax as Xaml (“*”, “Auto”, “5”)

3. The code adds the “Process” instance to the Query being displayed, so event handlers, like SelectionChanged, can use the TypeDescriptor class to get the Process instance.

4. Make sure the GrdSplitter has HorizontalAlignment=”Stretch”. I had HorizontalContentAlignment=”Stretch” and I couldn’t see my splitter !

5. You can create your own feature for things like Process Kill, Process properties, etc.

6. You can have a timer refresh the view, and use GetProcessTimes to calculate the CPU usage delta.

See also Write your own Linq query viewer

<Partial Sample Splitter use in Xaml>

 <Window x:Class="WpfApplication1.MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition  Height="*"/>
      <RowDefinition Height="14"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <DockPanel Name="dp" Grid.Row="0" />

    <GridSplitter Grid.Row="1" HorizontalAlignment="Stretch" Background="AliceBlue" ></GridSplitter>
    <ListView Name="lv1" Grid.Row="2" />

  </Grid>
</Window>

</Partial Sample Splitter use in Xaml>

<Code Sample>

 using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Shapes;

namespace WpfApplication1
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();
      this.Loaded += (ol, el) =>
      {
        try
        {
          Top = 0;
          Width = 1100;
          Height = 800;
          this.Title = "Process Explorer";
          //first we'll create a grid with 
          // 3 row definitions
          // We'll put a dockpanel 
          // in top and bottom row
          // and a gridsplitter in the middle row
          var grd = new Grid();
          grd.RowDefinitions.Add(
              new RowDefinition()
              {
                Height = ((GridLength)(
                new GridLengthConverter()
                .ConvertFromString("*")))
              });
          grd.RowDefinitions.Add(
              new RowDefinition()
              {
                Height = ((GridLength)(
                new GridLengthConverter()
                .ConvertFromString("5")))
              });
          grd.RowDefinitions.Add(
              new RowDefinition()
              {
                Height = ((GridLength)(
                new GridLengthConverter()
                .ConvertFromString("*")))
              });
          var dpTop = new DockPanel();
          Grid.SetRow(dpTop, 0);
          var gSplitter = new GridSplitter()
          {
            HorizontalAlignment = HorizontalAlignment.Stretch,
            ToolTip = "Drag me Around",
            Background = Brushes.Blue
          };
          Grid.SetRow(gSplitter, 1);
          var dpBottom = new DockPanel();
          Grid.SetRow(dpBottom, 2);
          grd.Children.Add(dpTop);
          grd.Children.Add(gSplitter);
          grd.Children.Add(dpBottom);
          this.Content = grd;


          // now we'll create content for the top dockpanel
          var q = from proc in Process.GetProcesses()
                  orderby proc.ProcessName
                  select
                      new
                      {
                        proc.ProcessName,
                        ProcType = proc.ProcType(),
                        proc.MainWindowTitle,
                        proc.HandleCount,
                        proc.WorkingSet64,
                        Threads = proc.Threads.Count,
                        _process = proc
                      }
                      ;
          var br = new Browse(q)
          {
            VerticalAlignment = VerticalAlignment.Top
          };
          var spTop = new StackPanel()
          {
            Orientation = Orientation.Vertical
          };
          spTop.Children.Add(br);
          dpTop.Children.Add(spTop);

          // the bottom will be filled with modules 
          // when the selection changes in the top
          // to see the modules of a 64 bit process, change the 
          // project->Properties->Build->Prefer 32 bit checkbox
          Browse brModules = null;
          // now we'll react to the 
          // SizeChanged event for the top Dockpanel
          var margin = 30;
          dpTop.SizeChanged += (os, es) =>
          {
            if (dpTop.ActualHeight > margin)
            {
              br.Height = dpTop.ActualHeight - margin;
            }
            if (dpBottom.ActualHeight > margin)
            {
              if (brModules != null)
              {
                brModules.Height = dpBottom.ActualHeight - margin;
              }
            }
          };
          br.SelectionChanged += (osel, esel) =>
          {
            if (esel.AddedItems.Count == 1)
            {
              var selItem = esel.AddedItems[0];
              Process proc = TypeDescriptor
                  .GetProperties(selItem)["_process"]
                  .GetValue(selItem) as Process;
              try
              {
                dpBottom.Children.Clear();
                var spBottom = new StackPanel()
                {
                  Orientation = Orientation.Vertical
                };
                var spInfo = new StackPanel()
                {
                  Orientation = Orientation.Horizontal
                };
                spBottom.Children.Add(spInfo);
                spInfo.Children.Add(
                    new TextBlock()
                    {
                      Text = string.Format("UserProcessorTime {0}", proc.UserProcessorTime.ToString("g")),
                      Margin = new Thickness(10, 0, 10, 0)
                    }
                    );
                spInfo.Children.Add(
                    new TextBlock()
                    {
                      Text = string.Format("TotalProcessorTime {0}", proc.TotalProcessorTime.ToString("g"))
                    }
                    );
                var qMod = from ProcessModule mod in proc.Modules
                           select new
                           {
                             mod.FileName,
                             mod.ModuleName,
                             mod.FileVersionInfo.FileVersion,
                             BaseAddress = mod.BaseAddress.ToString("x8"), // convert to hex
                             mod.ModuleMemorySize
                           };
                brModules = new Browse(qMod)
                {
                  Height = dpBottom.ActualHeight - margin
                };
                spBottom.Children.Add(brModules);
                dpBottom.Children.Add(spBottom);
              }
              catch (Exception ex)
              {
                // show exception
                dpBottom.Children.Add(
                    new TextBlock()
                    {
                      Text = string.Format("{0} {1}",
                      proc.ProcessName,
                      ex)
                    });
              }
            }
          };
        }
        catch (Exception ex)
        {
          // show exception
          this.Content = ex.ToString();
        }
      };
    }
  }

  public static class ExtensionMethods
  {
    public static string ProcType(this Process proc)
    {
      var retType = "?"; // access denied
      try
      {
        bool isWow64;
        if (NativeMethods.IsWow64Process(proc.Handle, out isWow64) && isWow64)
        {
          retType = "32";
        }
        else
        {
          retType = "64";
        }
      }
      catch (Exception)
      {
      }
      return retType;
    }
    internal static class NativeMethods
    {
      [DllImport("kernel32.dll", SetLastError = true)]
      internal static extern bool IsWow64Process([In] IntPtr process, [Out] out bool wow64Process);
    }

  }
  public class Browse : ListView
  {
    public Browse(IEnumerable query)
    {
      this.Margin = new System.Windows.Thickness(8);
      this.ItemsSource = query;
      var gridvw = new GridView();
      this.View = gridvw;
      var ienum = query.GetType().GetInterface(typeof(IEnumerable<>).FullName);

      var members = ienum.GetGenericArguments()[0].GetMembers().Where(m => m.MemberType == System.Reflection.MemberTypes.Property);
      foreach (var mbr in members)
      {
        if (mbr.Name.StartsWith("_")) // allow non-displayed values
        {
          continue;
        }
        var gridcol = new GridViewColumn();
        var colheader = new GridViewColumnHeader() { Content = mbr.Name };
        gridcol.Header = colheader;
        colheader.Click += new RoutedEventHandler(colheader_Click);
        gridvw.Columns.Add(gridcol);

        // now we make a dataTemplate with a Stackpanel containing a TextBlock
        // The template must create many instances, so factories are used.
        var dataTemplate = new DataTemplate();
        gridcol.CellTemplate = dataTemplate;
        var stackPanelFactory = new FrameworkElementFactory(typeof(StackPanel));
        stackPanelFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);

        var txtBlkFactory = new FrameworkElementFactory(typeof(TextBlock));
        var binder = new Binding(mbr.Name)
        {
          Converter = new MyValueConverter() // truncate things that are too long, add commas for numbers
        };
        txtBlkFactory.SetBinding(TextBlock.TextProperty, binder);
        stackPanelFactory.AppendChild(txtBlkFactory);
        txtBlkFactory.SetBinding(TextBlock.ToolTipProperty, new Binding(mbr.Name)); // the tip will have the non-truncated content

        //txtBlkFactory.SetValue(TextBlock.FontFamilyProperty, new FontFamily("courier new"));
        //txtBlkFactory.SetValue(TextBlock.FontSizeProperty, 10.0);

        dataTemplate.VisualTree = stackPanelFactory;
      }
      // now create a style for the items
      var style = new Style(typeof(ListViewItem));

      style.Setters.Add(new Setter(ForegroundProperty, Brushes.Blue));

      var trig = new Trigger()
      {
        Property = IsSelectedProperty,// if Selected, use a different color
        Value = true
      };
      trig.Setters.Add(new Setter(ForegroundProperty, Brushes.Red));
      trig.Setters.Add(new Setter(BackgroundProperty, Brushes.Cyan));
      style.Triggers.Add(trig);

      this.ItemContainerStyle = style;
    }

    private ListSortDirection _LastSortDir = ListSortDirection.Ascending;
    private GridViewColumnHeader _LastHeaderClicked = null;
    void colheader_Click(object sender, RoutedEventArgs e)
    {
      GridViewColumnHeader gvh = sender as GridViewColumnHeader;
      if (gvh != null)
      {
        var dir = ListSortDirection.Ascending;
        if (gvh == _LastHeaderClicked) // if clicking on already sorted col, reverse dir
        {
          dir = 1 - _LastSortDir;
        }
        try
        {
          var dataView = CollectionViewSource.GetDefaultView(this.ItemsSource);
          dataView.SortDescriptions.Clear();

          var sortDesc = new SortDescription(gvh.Content.ToString(), dir);
          dataView.SortDescriptions.Add(sortDesc);
          dataView.Refresh();
          if (_LastHeaderClicked != null)
          {
            _LastHeaderClicked.Column.HeaderTemplate = null; // clear arrow of prior column
          }
          SetHeaderTemplate(gvh);
          _LastHeaderClicked = gvh;
          _LastSortDir = dir;
        }
        catch (Exception)
        {
          // some types aren't sortable
        }
      }
    }

    void SetHeaderTemplate(GridViewColumnHeader gvh)
    {
      // now we'll create a header template that will show a little Up or Down indicator
      var hdrTemplate = new DataTemplate();
      var dockPanelFactory = new FrameworkElementFactory(typeof(DockPanel));
      var textBlockFactory = new FrameworkElementFactory(typeof(TextBlock));
      var binder = new Binding();
      binder.Source = gvh.Content; // the column name
      textBlockFactory.SetBinding(TextBlock.TextProperty, binder);
      textBlockFactory.SetValue(TextBlock.HorizontalAlignmentProperty, HorizontalAlignment.Center);
      dockPanelFactory.AppendChild(textBlockFactory);

      // a lot of code for a little arrow
      var pathFactory = new FrameworkElementFactory(typeof(Path));
      pathFactory.SetValue(Path.FillProperty, Brushes.DarkGray);
      var pathGeometry = new PathGeometry();
      pathGeometry.Figures = new PathFigureCollection();
      var pathFigure = new PathFigure();
      pathFigure.Segments = new PathSegmentCollection();
      if (_LastSortDir != ListSortDirection.Ascending)
      {//"M 4,4 L 12,4 L 8,2"
        pathFigure.StartPoint = new Point(4, 4);
        pathFigure.Segments.Add(new LineSegment() { Point = new Point(12, 4) });
        pathFigure.Segments.Add(new LineSegment() { Point = new Point(8, 2) });
      }
      else
      {//"M 4,2 L 8,4 L 12,2"
        pathFigure.StartPoint = new Point(4, 2);
        pathFigure.Segments.Add(new LineSegment() { Point = new Point(8, 4) });
        pathFigure.Segments.Add(new LineSegment() { Point = new Point(12, 2) });
      }
      pathGeometry.Figures.Add(pathFigure);
      pathFactory.SetValue(Path.DataProperty, pathGeometry);

      dockPanelFactory.AppendChild(pathFactory);
      hdrTemplate.VisualTree = dockPanelFactory;

      gvh.Column.HeaderTemplate = hdrTemplate;
    }
  }

  public class MyValueConverter : IValueConverter
  {
    private const int maxwidth = 700;
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      if (null != value)
      {
        Type type = value.GetType();
        //trim len of long strings. Doesn't work if type has ToString() override
        if (type == typeof(string))
        {
          //var str = value.ToString().Trim();
          //var ndx = str.IndexOfAny(new[] { '\r', '\n' });
          //var lenlimit = maxwidth;
          //if (ndx >= 0)
          //{
          //    lenlimit = ndx - 1;
          //}
          //if (ndx >= 0 || str.Length > lenlimit)
          //{
          //    value = str.Substring(0, lenlimit);
          //}
          //else
          //{
          //    value = str;
          //}
        }
        else if (type == typeof(Int32))
        {
          value = ((int)value).ToString("n0"); // Add commas, like 1,000,000
        }
        else if (type == typeof(Int64))
        {
          value = ((Int64)value).ToString("n0"); // Add commas, like 1,000,000
        }
      }
      return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      throw new NotImplementedException();
    }

  }

}

</Code Sample>