Wer kennt nicht das Problem “mal eben schnell” den Verlauf eines Prozess darstellen zu wollen? Etwa in Form des Windows eigenen Taskmanager? Die im Framework enthaltenen Steuerelemente bieten auf den ersten Blick nichts vergleichbares. Eine Sammlung von Steuerelementen eines Dritthersteller nutzen? Nein.
Für einfache Diagramme bietet GDI+ mit der Graphics-Klasse erstaunliche und einfache Möglichkeiten. Eine Anwendung wie sie dieser Screenshot zeigt, ist wirklich relativ einfach zu verwirklichen.
Ich habe bewusst das Aussehen des Taskmanager gewählt, da dieser so ziemlich jedermann geläufig sein sollte. Wie auf dem Screenshot zu sehen ist, werden zwei verschiedenen Steuerelemente verwendet. Eine vertikale Füllstandsanzeige und eine horizontale Verlaufsanzeige. Auch der aktuelle Wert wird als Grafik in der Füllstandsanzeige dargestellt.
Nun zur Umsetzung. Beide Steuerelemente sind einfach nur ein Panel. In den Eigenschaften der Panels wird lediglich die Eigenschaft BackColor auf Black gesetzt und als Rahmenattribut Fixed3D eingestellt um etwas Tiefe darzustellen. Alles andere wird im Paint-Ereignis des Panel erledigt.
Werfen wir als erstes einen Blick auf die Füllstandsanzeige. Es sind zwei verschiedene Farben darzustellen und der aktuelle Wert als Zeichenfolge. Um die Berechnung der Werte für die Positionen der Füllstandsmarken nachvollziehbar zu halten, habe ich im Beispiel feste Größen der Panels gewählt. Das Panel hat eine Größe von 60x140px. Abzüglich des Rand und der darzustellenden Zeichenfolge habe ich eine Höhe von 100px zur Darstellung des Füllgrad festgelegt. mit diesem Wert lässt sich auch einfacher rechnen, wenn man eine maximale Füllung von 100% bedenkt. Es ist also jeweils der leere Bereich von 100 bis zum aktuellen Wert und der volle Bereich ab dem aktuellen Wert bis 0 zu berechnen. Um überhaupt Werte zu erhalten mit denen gerechnet werden kann, habe ich ein Random Objekt verwendet, das zufällige Werte zwischen 0 und 100 zurückgibt. Schauen wir uns also das Paint-Ereignis des ersten Panel einmal an.
Die Verwendung von i + 10 in den Schleifen, dient zur Kompensation des oberen Rand von 10px. Pro Schleifendurchgang werden zwei Linien erzeugt um einen kleinen Abstand in der Mitte der Skala zu erhalten.
private void panel1_Paint(object sender, PaintEventArgs e)
{
// das Graphic Objekt zum zeichnen abrufen
Graphics graphic = e.Graphics;
// den zufälligen Wert in einen string konvertieren
string usageString = (this.usage * 2).ToString() + " MB";
// Position für die Zeichenfolge berechnen, so dass sie immer
// annähernd mittig dargestellt wird.
PointF stringLocation = new PointF(
28 -
(graphic.MeasureString(
usageString,
this.font).Width / 2),
120F);
// den Speicherverbrauch als Zeichenfolge darstellen
graphic.DrawString(
usageString,
this.font,
this.aktiveBrush,
stringLocation);
// passive Zeichnen
for (int i = 0; i <= (100 - this.usage); i += 3)
{
graphic.DrawLine(this.passivePen, 9, i + 10, 27, i + 10);
graphic.DrawLine(this.passivePen, 28, i + 10, 46, i + 10);
}
// aktive Zeichnen
for (int i = (100 - usage); i < 100; i += 3)
{
graphic.DrawLine(this.aktivePen, 9, i + 10, 27, i + 10);
graphic.DrawLine(this.aktivePen, 28, i + 10, 46, i + 10);
}
graphic.Dispose();
}Wenden wir uns jetzt dem zweiten Panel zu. Bei der Erzeugung der horizontalen Verlaufsanzeige soll ein Raster erzeugt werden. Der Einfachheit halber habe ich einen Abstand von 10px gewählt. Bei einer Größe von 250x140 lässt sich das einfach aufteilen. Allerdings soll nicht nur ein statisches Raster gezeichnet werden, sonder es soll den Anschein erwecken als ob es über den Bildschirm wandert. Das lässt sich einfach mit einem variablen Anfangspunkt der vertikalen Linien erzielen, in dem dieser abwechselnd um den halben Linienabstand versetzt wird. Da das Raster in einer anderen Farbe dargestellt werden soll, wird noch eine dritte Farbe benötigt. Werfen wird jetzt einen Blick in den Code und sehen was bis jetzt vorhanden ist.
private void panel2_Paint(object sender, PaintEventArgs e)
{
// das Graphic Objekt zum zeichnen abrufen
Graphics graphic = e.Graphics;
// horizontale Linien zeichnen
for (int i = 10; i < 140; i += 10)
{
graphic.DrawLine(this.gridPen, 0, i, 250, i);
}
// vertikale Linien Zeichnen und mit offset beginnen
for (int i = this.offset; i < 250; i += 10)
{
graphic.DrawLine(this.gridPen, i, 0, i, 140);
}
graphic.Dispose();
}
Es wird bis jetzt die Füllstandsanzeige und das Raster aus horizontalen und vertikalen Linien gezeichnet. Da da die Werte der Felder
usage und
offsetmit 0 initialisiert werden, sollte das Ergebnis jetzt so aussehen:

Um das ganze mit Daten zur Anzeigen zu befüllen, wird das bereits angesprochene Random Objekt verwendet. Um noch einen zeitlichen Ablauf zu erreichen verwenden wir einen Timer aus dem System.Windows.FormsNamensraum. Im Ereignishandler des Tick Ereignis dieses Timer, wird ein neuer Wert vom Random Objekt zurückgegeben und die beiden Panels werden neu gezeichnet. Da wir aber einen Verlauf der ausgegebenen Werte darstellen wollen, müssen noch irgendwie diese Werte vorgehalten werden. Wegen der einfachen Handhabung habe ich mich für eine generische Klasse List<T> mit Werten vom Typ float entschieden. float deshalb, weil die Parameter der Methode Graphics.DrawLine vom Typ float sind und sich mit Werten vom Typ float einfach rechnen lässt. Zur Erinnerung: das Panel hat einen Höhe von 140px und es stehen maximal 100% zur Verfügung. Also muss mit dem Faktor 1.4 skaliert werden. Doch zurück zur Liste mit den Werten. Da beim Start der Anwendung die Liste zunächst leer ist, muss natürlich auch die Darstellung des Verlauf am Rand beginnen. Mit fortschreitender Füllung der Liste, soll auch die Darstellung im Raster wandern. Also muss die Position mit dem Füllstand der Liste synchronisiert werden. Es muss nur eine Konstante festgelegt werden, die den Fortschritt der Anzeige je Durchlauf vorgibt. Der Einfachheit halber wird hier fünf verwendet. Das Panel ist 250px lang, 5px pro Durchlauf. Also muss die volle Liste 50 Werte enthalten. Andersherum kann mit der Anzahl der Elemente in der Liste multipliziert mit der Konstanten, die Position im Raster ermittelt werden. Klingt vieleicht kompliziert, ist es aber nicht wenn man einen Blick auf die eine Zeile Code wirft:
// aktuelle horizontale Position ermittel
int x = 250 - (this.pointList.Count * 5);
Wenn die Liste ihre 50 Werte erreicht hat, wird immer das erste Element aus der Liste entfernt und der aktuellste Wert an das Ende der Liste angefügt. Da dies alles im Timer.Tick Ereignis verarbeitet wird, hier die komplette Methode:
void timer_Tick(object sender, EventArgs e)
{
// zufälligen Wert zwischen 0 und 100 erzeugen
this.usage = rnd.Next(0, 101);
// prüfen ob schon ein Wert gespeichert
if (this.firstItem == 0F)
{
// aktuellen Wert zuweisen
this.firstItem = this.usage * 1.4F;
}
// prüfen ob die Liste gefüllt ist
if (this.pointList.Count >= 50)
{
// ersten Eintrag aus der Liste sichern
this.lastItem = this.pointList[0];
// den ersten Eintrag aus der Liste löschen
this.pointList.RemoveAt(0);
// neuen Wert am Ende der Liste anfügen
this.pointList.Add((float)this.usage * 1.4F);
}
else
{
// Wert neu zuweisen
this.lastItem = this.firstItem;
// aktuellen Wert zur Liste hinzufügen
this.pointList.Add((float)this.usage * 1.4F);
}
// offset verschieben
this.offset = this.offset == 0 ? 5 : 0;
// beide Panel neuzeichnen
this.panel1.Invalidate();
this.panel2.Invalidate();
}Nach dem die Verwendung der Liste geklärt ist, nun zum zeichnen der Verlaufskurve. In diesem Fall wird keine echte Kurve verwendet, sondern eine Aneinanderreihung von kleinen einzelnen Linien, die jeweils die Länge einer Teilmenge der 250px Gesamtlänge entsprechen. Da eine Linie immer aus einem Anfangs- und Endpunkt besteht und in diesem Fall der Endpunkt einer Linie auch gleich der Anfangspunkt der nächsten Linie ist, lässt sich die Abbildung einfach realisieren. Da die Liste beim Durchlaufen immer nur einen Wert zurückgibt, wird einfach der vorangegangene Wert in einer Variablen gehalten; hier als lastItem bezeichnet. Für die zweite Koordinate wird einfach der aktuelle Wert um die Konstante 5 verkleinert. Hier nun die komplette Methode zum Darstellen des Verlauf:
private void panel2_Paint(object sender, PaintEventArgs e)
{
// das Graphic Objekt zum zeichnen abrufen
Graphics graphic = e.Graphics;
// horizontale Linien zeichnen
for (int i = 10; i < 140; i += 10)
{
graphic.DrawLine(this.gridPen, 0, i, 250, i);
}
// vertikale Linien Zeichnen und mit offset beginnen
for (int i = this.offset; i < 250; i += 10)
{
graphic.DrawLine(this.gridPen, i, 0, i, 140);
}
// aktuelle horizontale Position ermittel
int x = 250 - (this.pointList.Count * 5);
// den Verlauf aus der Liste darstellen
foreach(var item in this.pointList)
{
// Teillinie zeichnen
graphic.DrawLine( this.memoryPen, x - 5, 140 - this.lastItem, x, 140 - item);
// aktuellen Wert für den nächsten Durchlauf speichern
this.lastItem = item;
// X-Koordinate um 5 erhöhen
x += 5;
}
graphic.Dispose();
}Wie ich bereits eingangs erwähnt habe, sind diese Diagramm Steuerelemente mit relativ wenig Aufwand zu erstellen. Es ist auch nicht sonderlich schwer die Panels in ihrer Größe änderbar zu gestalten. Es müssen lediglich die im Moment festen Werte von z.B. 250px Länge durch den Wert der Eigenschaft Width die das Panel zurückgibt, ersetzt werden. Als kleines Beispiel die bereits bekannte Berechnung der horizontalen Position anhand der Anzahl der Elemente in der Liste:
// aktuelle horizontale Position ermittel
int x = ((Panel)sender).Width - (this.pointList.Count * 5);
Genauso ist mit der Höhe und den anderen Werten zu verfahren. Auch die Darstellung der Diagramme weist noch viele Möglichkeiten auf. Es kann jede beliebige geometrische Form dargestellt und mit den verschiedensten Mustern gefüllt werden. Sich vorzustellen was damit alles Realisiert werden kann, überlasse ich jedem selbst.
Interessierte können sich das Projekt herunterladen.
ChartTest.Zip
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
