Montag, 8. Oktober 2012

WPF Diagramm zum Anzeigen der CPU Auslastung

Herausforderung

Ziel heute ist es ein eigenständiges UserControl in WPF zu schreiben welches "as is" in jede beliebige Alikation eingebunden werden. Aufgabe des UserControls ist das Anzeigen der Prozessorauslastung als Liniendiagramm.

1. Wie messe ich die aktuelle CPU Auslastung in %

Möglich wird das über den PerformanceCounter aus der Assembly System.Diagnostics
PerformanceCounter cpuCounter = new PerformanceCounter();
cpuCounter.CategoryName = "Processor";
cpuCounter.CounterName = "% Processor Time";
cpuCounter.InstanceName = "_Total";

Console.WriteLine("CPU Auslastung: {0} %", cpuCounter.NextValue());

Das war ja schonmal einfach... Jetzt fehlt nurnoch das Diagramm.

2. Liniendiagramm im XAML

Wie der Name schon sagt, besteht ein Liniendiagramm aus Linien, d.h wir benötigen ein ItemsControl, dass an eine Liste von Start-End-Punkten gebunden wird.
<ItemsControl ItemsSource="{Binding CPUUsagePoints}">
 <ItemsControl.ItemTemplate>
  <DataTemplate>
   <Canvas>
    <Line 
          X1="{Binding Start.X}" Y1="{Binding Start.Y}" 
          X2="{Binding End.X}" Y2="{Binding End.Y}" 
          Stroke="Green" StrokeThickness="0.3"
          >
    </Line>
   </Canvas>
  </DataTemplate>
 </ItemsControl.ItemTemplate>
</ItemsControl>
Da der C# System.Drawing.Point kein Referenzdatentyp, sondern ein Verbunddatentyp ist, benötigen wir eine analoge Klasse, und dazu noch eine Klasse für eine Strecke mit Start-Punkt und End-Punkt. Die Implementierung spar ich mir hier, und zeige lieber das aus dem ViewModel gebundene Property CPUUsagePoints.
public List<Point> _pointList = new List<Point>();
public List<Line> _diagramLines;
/// <summary>
/// diagram lines for binding
/// </summary>
public List<Line> DiagramLines
{
  get
  {
    if (_diagramLines == null)
    {
     _diagramLines = new List<Line>();
     InitDiagramm();
    }
  return _diagramLines;
  }
}

/// <summary>
/// initializes the diagram with f(x)=0
/// and connects the points with lines
/// </summary>
void InitDiagramm()
{
  int granularity = 2;
  for (int i = 0; i < (int)(_parent.RenderSize.Width / granularity); i++)
  {
    Line sep = new Line();
    if (_diagramLines.Count == 0)
    {
      sep.Start = new Point(i * granularity, _parent.RenderSize.Height - 1);
      _pointList.Add(sep.Start);
    }
    else
    {
      sep.Start = _diagramLines.Last().End;
    }

    sep.End = new Point(i * granularity, _parent.RenderSize.Height - 1);
    _pointList.Add(sep.End);

    _diagramLines.Add(sep);
  }
}

Wie man leicht sieht werden die Punkte so initialisiert, dass diese bei einem kartesischen Koordinatensystem die Funktion f(x)=0 abbilden, also eine klassische Nulllinie. Da wir in der Computergrafik ein anderes Koordinatensystem verwenden, müssen wir das Ganze die Y-Achse betreffend noch etwas umrechnen.

So weit, so gut - aber wie sollen wir nun die neuen Werte in das Diagramm "rein" bekommen, dafür gibt es bestimmt einige Möglichkeiten - ich habe mich dafür entschieden, die neuen Werte rechts anzuhängen und die bestehenden Punkte nach links zu verschieben. Abstrakt gesehen, brauche ich hier also 2 Funktionen:
  1. Füge_neuen_Wert_hinzu (prozent)
  2. Schiebe_alle_bisherigen_Punkte_nach_links()
Nicht ganz so abstrakt, aber mindestens genauso einfach wird die Implementierung:
/// <summary>
/// add a new value for the cpu usage
/// </summary>
/// <param name="percent">value in per cent</param>
void AddCpuUsage(int percent)
{
  try
  {
    percent = 100 - percent;
    lock (_pointList)
    {
      ShiftLeft(1);
      Point p = _pointList[_pointList.Count - 1];
      double valueToAchieve = ((float)percent / 100.0) * _parent.RenderSize.Height;
      p.Y = valueToAchieve;
    }
  }
  catch { }
 }

/// <summary>
/// shifts the diagram points left
/// </summary>
/// <param name="offset">the offset, how far to shift</param>
void ShiftLeft(int offset = 1)
{
  for (int i = 0; i < _pointList.Count - offset; i++)
  {
    Point p = _pointList[i];
    Point nextPoint = _pointList[i + offset];
    p.Y = nextPoint.Y;
  }
}
Wie man sieht, habe ich hier etwas geschummelt, ich verschiebe nicht die Punkte und hänge auch nichts hinten an, sondern shifte nur die Y-Werte nach links, und verändere dann den Y-Wert des letzten Punkts. Dadurch habe ich einen entscheidenden Vorteil - ich muss zum einen keine X-Verschiebung der einzelnen Punkte durchführen und zum Anderen kann ich die Liste DiagramLines so wie sie ist beibehalten, das Binding wird automatisch durch ein OnPropertyChanged in der Klasse Point bewerkstelligt.

Und für alle, die das Ganze nochmal praktisch nachvollziehen wollen, hier der Link zur Solution:
Solution CPUUsageVisualizer

Keine Kommentare:

Kommentar veröffentlichen