head.WriteLine()

Dienstag, April 25, 2006

ListView im Virtual Mode

In .NET 2.0 wurde das ListView-Control um den so genannten "Virtual Mode" erweitert. In diesem werden die anzuzeigenden Elemente erst erzeugt, wenn sie im Darstellungsbereich erscheinen.

Dies ist immer dann sinnvoll, wenn Sie große Datenmengen in einer Liste anzeigen wollen. Normalerweise würden Sie alle Daten beim Öffnen der Form in die Liste einfügen. Wenn diese aus einer Datenbank kommen, kann es hierbei zu recht unschönen Verzögerungen kommen. Zudem belegen die Daten Speicher, da das ListView-Control diese zwischenspeichert. Aber auch bei der Anzeige unterschiedlicher Elementsymbole kann es zu Zeit- und Ressourcen-Engpässen kommen. Der Windows Explorer nutzt den Virtual Mode beispielsweise, um die Datei- und Verzeichnissymbole dynamisch nachzuladen.

Im Virtual Mode legt das Control einen Cache virtueller Elemente an und synchronisiert diese mit Größe und Position der Scrollbar. Die Aktivierung erfolgt über das Setzen der VirtualMode-Eigenschaft auf true. Die Anzahl der Elemente kann hierbei über die VirtualListSize-Eigenschaft angegeben werden. Wenn sich ein Element im Anzeigebereich befindet, wird das RetrieveVirtualItem-Event ausgelöst, in dem das entsprechende Element erzeugt und mit Daten gefüllt werden kann. Um welches Element es sich hierbei handelt, wird über die ItemIndex-Eigenschaft der RetrieveVirtualItemEventArgs-Klasse signalisiert.

Das folgende Beispiel legt eine virtuelle Liste mit 50 Einträgen an und erzeugt dynamisch Elemente, die den jeweiligen Index als Text anzeigen.

ListView listView1 = new System.Windows.Forms.ListView();
listView1.VirtualMode = true;
listView1.VirtualListSize = 50;
listView1.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(listView1_RetrieveVirtualItem);
...

private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
    e.Item = new ListViewItem(e.ItemIndex.ToString());
}

Sonntag, April 23, 2006

Das Median-Problem, Teil 2

Wie ich im ersten Teil bereits schmerzlich feststellen musste, sind benutzerdefinierte Aggregatfunktionen zwar im Grund eine gute Sache, eignen sich jedoch nicht für die Ermittlung des Median. In diesem Teil versuche ich daher einen anderen Ansatz, der auf T-SQL basiert.

Doch zunächst noch einmal zu den Anforderungen:

  • Die Anzahl der Zeilen wird benötigt, um den mittleren Wert zu ermitteln
  • Die Zahlenmenge muss in einer sortierten Liste vorliegen

Die erste Anforderung ist relativ einfach über eine COUNT()-Abfrage zu realisieren. Wenn beispielsweise der Median für alle Positionspreise aller Bestellungen ermittelt werden soll, könnte dies so aussehen:

DECLARE @RowCount int
SELECT @RowCount = COUNT(*) FROM Sales.SalesOrderDetail

Nun müssen die Einzelpreise aller Bestellungen ermittelt und in sortierter Form bereitgestellt werden.

SELECT LineTotal
FROM Sales.SalesOrderDetail
ORDER BY LineTotal

Doch wie kann ich nun auf dieser Ergebnismenge den Wert ermitteln, der in der Mitte steht? Hierzu verwende ich eine T-SQL-Neuerung des SQL Server 2005. Über die Rankingfunktion ROW_NUMBER() kann jede Zeile der Ergebnismenge um eine Zählerspalte erweitert werden.

SELECT ROW_NUMBER() OVER
(
    ORDER BY LineTotal DESC
)
AS Rank,
LineTotal
FROM Sales.SalesOrderDetail

Hierbei wird innerhalb der ROW_NUMBER()-Funktion ein Ausdruck angegeben, der die Sortierung der Ergebnismenge festlegt, auf die sich die Nummerierung beziehen soll. Das Ergebnis sieht hierbei etwas so aus:

Rank    LineTotal
1       2,3
2       2,8
3       3.2
4       5
...

Um nun den mittleren Wert zu ermitteln, könnte ich in der WHERE-Klausel die Rank-Spalte auf die Gesamtzeilenzahl durch zwei teilen.

WHERE Rank = @RowCount / 2

So einfach geht das jedoch nicht, da ROW_NUMBER() nicht innerhalb der WHERE-Klausel verwendet werden darf. Daher muss ich das Ganze in einer Unterabfrage kapseln und auf der Oberabfrage die Filterung vornehmen.

Alles in allem könnte dies zum Beispiel so aussehen:

DECLARE @RowCount int
SELECT @RowCount = COUNT(*) FROM Sales.SalesOrderDetail

SELECT Rank,
LineTotal
FROM
(
    SELECT ROW_NUMBER() OVER
    (
        ORDER BY LineTotal DESC
    )
AS Rank,
    LineTotal
    FROM Sales.SalesOrderDetail
) AS sub
WHERE Rank = @RowCount / 2


Am Ende habe ich nun meinen Median berechnet, konnte hierbei jedoch nicht den generischen Ansatz einer Aggregatfunktion verwenden. Dank der neuen Rankingfunktionen des SQL Server 2005, ist die Ermittlung jedoch wesentlich einfacher als mit den Vorgängerversionen. Hier hätte ich den Median nämlich in zeitaufwendigen Cursor- oder Schleifendurchläufen ermitteln müssen.

Freitag, April 21, 2006

Das Median-Problem, Teil 1

Der SQL Server 2005 bietet erstmals die Möglichkeit benutzerdefinierte Aggregatfunktionen zu entwickeln. Als ich über das Thema zum ersten Mal las, fand ich es ganz spannend und begann sogleich meine eigene Aggregatfunktion, namens „Median“ zu implementieren.

Bei Median geht es darum - ähnlich wie bei AVG() - den Mittelwert einer Ergebnismenge zu berechnen. Doch anders als AVG() berechnet Median nicht den Durchschnittswert, sondern ermittelt stattdessen den Wert, der in der Mitte einer sortierten Liste steht. Dieses Verfahren wird gerne im statistischen Bereich verwendet, da das Ergebnis hierbei nicht durch einzelne extrem kleine oder große Werte verfälscht wird.

Soweit der Plan. Doch schauen wir uns das mal aus der Nähe an:

[Serializable]
[SqlUserDefinedAggregate(
    Format.UserDefined,
    IsInvariantToDuplicates=true,
    MaxByteSize=8000)]
public struct Median : IBinarySerialize
{
    private ArrayList m_values;

    public void Init()
    {
        m_values = new ArrayList();
    }

    public void Accumulate(SqlMoney Value)
    {
        if (Value.IsNull)
            return;

        m_values.Add(Value.ToDecimal());
    }

    public void Merge(Median Group)
    {
        m_values.AddRange(Group.m_values);
    }

    public SqlMoney Terminate()
    {
        return new SqlMoney(
            (Decimal)m_values[m_values.Count / 2]
);
    }
        ...
}

Zunächst lege ich die Membervariable m_values an, in die ich alle Werte speichere, die über die Accumulate() bzw. Merge()-Methode herein kommen. Zuletzt wird die Terminate()-Methode aufgerufen, in der ich den mittleren Wert der Liste ermittle.

Diese Aggregatfunktion könnte ich nun in T-SQL wie folgt verwenden:

SELECT Median(LineTotal) AS TheMedian
FROM Sales.SalesOrderDetail
ORDER BY LineTotal

Das sieht ja zunächst ganz brauchbar aus. Doch bei näherer Betrachtung stellt man fest, dass diese Lösung auf Sand gebaut ist. Der Grund hierfür ist, dass die Größe einer Aggregatinstanz 8K nicht übersteigen darf. Da das Aggregat die Werte zwischenspeichert, wächst die Größe jedoch stetig an, was dazu führt, das nach 500 Zeilen ein Fehler ausgelöst wird (16 Bytes * 500 = 8K).

Letztendlich macht die Limitation Sinn, da die Verfügbarkeit bei sehr großen Ergebnismengen in Mitleidenschaft gezogen werden kann. Doch wie ermittle ich nun meinen Median? Im zweiten Teil versuche ich einen anderen Weg.

Samstag, April 15, 2006

Schnelles Imaging mit PARGB

Wenn Sie eine große Anzahl von Images mit GDI+ auf den Bildschirm bringen, kann es zu Performance-Engpassen kommen, wenn deren Farbtiefe 32 Bit unterschreitet. Der Grund hierfür ist, dass diese zuvor automatisch in das PARGB-Format (pre-multiplied alpha blended RGB) konvertiert werden.

Wenn Sie beispielsweise eine Animation erzeugen, in dem Sie nacheinander mehrere GIF-Bilder auf den Bildschirm bringen, können Sie die Performance optimieren, in dem Sie diese vorher in das PARGB-Format wandeln. Die folgende Methode demonstriert die Vorgehensweise:

private void ConvertImage2PARGB(Image img)
{
    Bitmap bmp = img as Bitmap;
    Bitmap newImg = new Bitmap(bmp.Width, bmp.Height,
    System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
    using (Graphic g = Graphic.FromImage(newImg);
    {
        g.DrawImage(bmp, new Rectangle(0,0,bmp.Width,bmp.Height));
    }
    img = newImg;
}

Freitag, April 14, 2006

Fast User Switching mit WPF

Wie hier bereits beschrieben, existiert in .NET 2.0 erstmalig die Möglichkeit auf das Beenden der aktiven User Session zu reagieren.

Die gleiche Funktionalität lässt sich natürlich auch unter WPF nutzen. Trotzdem bietet WPF über seine Application-Klasse noch mal den gleichen Mechanismus in Form des SessionEnding-Events. Auch hier kann über SessionEndingCancelEventArgs.Cancel das Herunterfahren abgebrochen werden.

Eigentlich ist es unnötig die gleiche Funktionalität an zwei verschiedenen Stellen anzubieten, es erleichtert jedoch die Verwendung, da die Events der SystemEvents-Klasse über den Microsoft.Win32-Namespace nicht so leicht zu finden sind.

Donnerstag, April 13, 2006

Systeminformationen mit WPF ermitteln

WPF bietet eine Reihe von Klassen, mit denen Sie die verschiedensten Systeminformationen abrufen können. Dies kann beispielsweise dann hilfreich sein, wenn Sie ein ein Owner Drawn Control entwickeln, welches seine Darstellung entsprechend der Windows-Systemeinstellungen anpassen soll.

Die folgenden Klassen im System.Windows-Namespace spielen hierbei eine Rolle:

So können Sie beispielsweise über die Eigenschaft SystemParameters.CaptionWidth die Breite der Titelleiste ermitteln. Darüber hinaus finden sich in der Klasse aber auch einige allgemeingültige Informationen, wie die IsMediaCenter-Eigenschaft, die anzeigt ob es sich um einen Media Center PC handelt oder IsRemotelyControlled, um zu Ermitteln, ob die aktive User Session Remote gesteuert wird. Geht es bei Ihrer Verarbeitung so richtig zu Sache, können Sie über IsSlowMachine ermitteln, ob der aktive Rechner eher von der langsamen Sorte ist (was auch immer das bedeutet) und ggf. einen alternative Weg anbieten.

Die Liste ließe sich noch beliebig fortführen, da SystemParameters weitaus detailiertere Informationen über das aktive System als die bisherige SystemInformation-Klasse des System.Windows.Forms-Namespace bietet. Ein gutes Beispiel für den Einsatz der SystemParameters-Informationen findet sich im Samples-Bereich des WinFx-SDK.

SystemFonts stellt Font-Informationen für Windows-Standardelemente, wie Titelleisten oder Menüs bereit. Wenn Sie beispielsweise ermitteln wollen in welchem Font die Texte in einer Message Box angezeigt werden, können Sie dies über MessageFontStyle heraus finden.

SystemColors bietet Zugriff auf System Brushes, wie beispielsweise ActiveBorderBrush, was beim Zeichnen eines Rahmens im aktiven Zustand hilfreich ist. Ein Beispiel für die Verwendung von SystemFonts und SystemColors findet sich ebenfalls im WinFx SDK.

Mittwoch, April 12, 2006

ExtractAssociatedIcon()

In .NET 2.0 wurde die System.Drawing.Icon-Klasse um die ExtractAssociatedIcon()-Methode erweitert. Sie extrahiert das Symbol der angegebenen Datei.

Icon ico = Icon.ExtractAssociatedIcon("C:\\MyDoc.doc");

Was sich zunächst ganz sinnvoll anhört, entpuppt sich schnell als recht unbrauchbar. ExtractAssociatedIcon() extrahiert nämlich stets das Icon in der Standardgröße 32 x 32, die Auswahl einer anderen Größe ist nicht möglich. Zudem wird das Icon stets mit der im System eingestellten Farbtiefe ermittelt. Auch hier wäre eine explizite Steuerung sinnvoll.

Das größte Manko ist jedoch die Tatsache, dass bei bestimmten Dateitypen, wie zum Beispiel Bitmaps ein Vorschaubild zurückgegeben wird. Dies ist umso mehr erstaunlich, da eine private Überladung der Methode existiert, die zusätzlich einen Index entgegennimmt. Sie wird von der öffentlichen Methode aufgerufen und hierbei der Index 0 übergeben. Würde stattdessen 1 übergeben, so würden auch bei "Vorschau-Dateitypen" stets das Icon und kein Vorschaubild ermittelt.

Alles in allem finde ich das recht unbefriedigend, da man im Zweifelsfall mal wieder in die Windows-API-Trickkiste greifen muss, was nicht nur Zeitaufwendig ist, sondern vor allem eine erweiterte Berechtigung voraussetzt.

Dienstag, April 11, 2006

Fast User Switching Support in .NET 2.0

Mit Windows XP wurde das so genannte "Fast User Switching" eingeführt, welches das Wechseln zwischen laufenden Benutzer-Sessions ermöglicht. Mit .NET war es bisher jedoch nicht möglich, auf die Umschaltung zu reagieren.

Mit .NET 2.0 wurde diese Limitation nun behoben. Hierfür wurde die Klasse Microsoft.Win32.SystemEvents um die Events SessionSwitch, SessionEnding und SessionEnded erweitert.

Das SessionSwitch-Event ist hierbei das flexibelsten, da es über alle Session-Änderungen informiert. In welcher Form die Umschaltung vollzogen wurde, kann hierbei über die Reason-Eigenschaft der SessionSwitchEventArgs-Klasse ermittelt werden. Sie liefert einen Wert der SessionSwitchReason-Enumeration, die folgende Werte definiert:

  • SessionLock / SessionUnlock
    Eine Session wurde gesperrt oder entsperrt.
  • SessionLogon / SessionLogoff
    Ein Benutzer hat sich an einer Session an- oder abgemeldet.
  • ConsoleConnect / ConsoleDisconnect
    Eine Session wurde über die Konsole hergestellt oder getrennt.
  • RemoteConnect / RemoteDisconnect / SessionRemoteControl
    Eine Session wurde über eine Remoteverbindung hergestellt, getrennt oder geändert.

Wenn Sie jedoch lediglich darüber informiert werden wollen, wenn die aktive Session beendet wird, können Sie sich auch für die Events SessionEnding oder SessionEnded anmelden. Während SessionEnding die Möglichkeit bietet, die Aktion abzubrechen, informiert SessionEnded lediglich über die Beendigung. Zudem bieten beide Events über die Reason-Eigenschaft ihrer EventArgs-Klassen den Grund für die Beendigung (Logoff/SystemShutdown).

Samstag, April 08, 2006

Komponenten mit Smart Tags veredeln, Teil 3

Bei einigen Framework-Controls wird das Smart Tag-Window automatisch beim Einfügen auf die Form eingeblendet. So bekommt der Entwickler einen schnellen Überblick der Eigenschaften und kann ohne viel Geklicke die Komponente konfigurieren.

Wenn Sie dieses Verhalten auch in Ihrer Komponente umsetzen möchten, überschreiben Sie zunächst die InitializeNewComponent()-Methode Ihrer Designer-Klasse. Diese Methode wird aufgerufen, wenn die Komponente aus der Toolbox auf die Form gezogen wird.

Das Einblenden des Smart Tag-Windows können Sie nun über den DesignerActionUIService bewerkstelligen. Das folgende Beispiel zeigt die Implementierung:

public override void InitializeNewComponent(System.Collections.IDictionary defaultValues)
{
    base.InitializeNewComponent(defaultValues);

    DesignerActionUIService actionUIService =
        this.GetService(typeof(DesignerActionUIService)) as DesignerActionUIService;
    actionUIService.ShowUI(this.Component);
}


Hier wird über die GetService()-Methode der Designerbasisklasse eine vorhandene Instanz des Service ermittelt und über die ShowUI()-Methode das Window zur Anzeige gebracht.

Der gleiche Service kann übrigens auch verwendet werden, um das Fenster programmgesteuert zu schließen.

Komponenten mit Smart Tags veredeln, Teil 2

Wie Sie Ihre eigenen Controls mit Smart Tags ausstatten können, haben ich hier bereits erklärt. Heute möchte ich Ihnen zeigen, wie Sie das gleiche erreichen können, ohne eine eigene Designer-Klasse erstellen zu müssen.

Normalerweise wird die Komponente über das Designer-Attribute mit einem Designer verbunden, der über die ActionLists-Eigenschaft die Smart Tag Action List bereitstellt. Mit Hilfe des DesignerActionSerivce können Sie sich diesen Zusatzaufwand jedoch sparen.
Und das geht so:
  • Überschreiben der OnHandleCreated()-Methode in der eigenen Control-Klasse
  • Ermitteln einer DesignerActionService-Instanz
  • Einhängen der eigenen Action List über die Add()-Methode

Dies könnte beispielsweise so aussehen:

class LabelEx : Label
{
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        if (this.Site != null)
        {
            DesignerActionService actionService =
                this.Site.GetService(typeof(DesignerActionService)) as DesignerActionService;
            if (actionService != null)
            {
                actionService.Add(this, new LabelExActionList(this));
            }
        }
    }
}

Freitag, April 07, 2006

Text Rendering in .NET 2.0

Das Rendern von Text wurde in .NET 1.x fast vollständig durch interne GDI+-Funktionen realisiert und hierbei nicht auf entsprechende Windows-GDI-Funktionen zurückgegriffen. Dies hatte einige Nachteile. Neben der schlechteren Performance gab es auch Probleme bei der Darstellung von lokalisiertem Text. Zudem gab es einige Ungenauigkeiten bei der Berechnung von Textgrößen über die Graphics.MeasureString()-Methode.

.NET 2.0 geht nun quasi „back to the roots“ und setzt zum Rendern von Text wieder auf das gute, alte GDI. Aus Kompatibilitätsgründen konnte dies jedoch nicht vollständig unter der Haube geändert werden, sondern wurde ­ wie viele Neuerungen ­ als Erweiterung implementiert.

Die meisten Controls im Windows.Forms-Namespace gehen nun einen hybriden Weg: Je nach Einstellung, rendern Sie den Text entweder über GDI oder GDI+. Die Steuerung erfolgt hierbei über die SetCompatibleTextRenderingDefault()-Methode der Application-Klasse. Wenn Sie ein neues WinForms-Projekt in Visual Studio 2005 anlegen, wird standardmäßig die folgende Zeile eingefügt:

Application.SetCompatibleTextRenderingDefault(false);

Sie bewirkt, dass die Controls intern die neue Text Rendering-API verwenden. Wenn Sie selbst Text rendern, können Sie dies auch weiterhin über die Funktionen DrawText() und MeasureText() der Graphics-Klasse tun.

Graphics.DrawString(string text, Font font, Point point);
Graphics.MeasureString(string text, Font font);

Wollen Sie hingegen den GDI-Weg gehen, so steht Ihnen die neue TextRenderer-Klasse zu Verfügung, die folgende Methoden bietet:

TextRenderer.DrawText(Graphics gr, string text, Font font, Point point);
TextRenderer.MeasureText(string text, Font font);

Wie Sie sehen, funktionieren die Methoden sehr ähnlich.

Falls Sie das Thema näher interessiert, kann ich Ihnen den folgenden Artikel empfehlen, der in der März-Ausgabe des MSDN-Magazins erschienen ist:

Build World-Ready Apps Using Complex Scripts In Windows Forms Controls

Mittwoch, April 05, 2006

DrawToBitmap()

Manchmal benötigt man das grafische Abbild eines Controls, um es beispielsweise auszudrucken. In .NET 1.x musste man hierfür tief in die Trickkiste greifen und die gute alte API-Funktion BitBlt bemühen. Die folgende Hilfsfunktion zeigt die bekannte Vorgehensweise:

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, System.Int32 dwRop);

private Image GetControlImage(System.Windows.Forms.Control ctl)
{
    const Int32 SRCCOPY = 0xCC0020;
    
    Graphics gc1 = ctl.CreateGraphics();
    Image tmpImage = new Bitmap(
        ctl.ClientRectangle.Width,
        ctl.ClientRectangle.Height, gc1);
    Graphics gc2 = Graphics.FromImage(tmpImage);
    
    IntPtr dc1 = gc1.GetHdc();
    IntPtr dc2 = gc2.GetHdc();
    
    BitBlt(
        dc2,
        0, 0,
        ctl.ClientRectangle.Width, ctl.ClientRectangle.Height,
        dc1,
        0, 0,
        SRCCOPY);
    
    gc1.ReleaseHdc(dc1);
    gc2.ReleaseHdc(dc2);
    gc1.Dispose();
    gc2.Dispose();
    
    return tmpImage;
}

In .NET 2.0 ist die Ermittlung der Oberfläche erheblich leichter geworden. Die Control-Klasse bietet hierfür die DrawToBitmap()-Methode an, der ein leeres Bitmap-Objekt, sowie eine Rectangle-Struktur übergeben wird.

Das Ermitteln einer Button-Oberfläche könnte nun beispielsweise so erfolgen:

Bitmap bmp = new Bitmap(this.button1.Width, this.button1.Height);
this.button1.DrawToBitmap(bmp, this.button1.ClientRectangle);
this.pictureBox1.Image = bmp;

Wieder so eine Kleinigkeit, die das Leben leichter macht :-)

Dienstag, April 04, 2006

VisualStyles und VisualStylesRenderer, Teil 3

Wie ich in Teil 2 beschrieben habe, funktioniert die VisualStyles API nur unter diesen Voraussetzungen:

  • Visual Styles sind in Windows aktiviert
  • Visual Styles sind in der Anwendung aktiviert
  • Der aktive Theme unterstützt das zu zeichnende Element

Doch wie male ich denn nun meine Oberfläche, wenn die Visual Styles deaktiviert wurden? Die Antwort ist einfach: So wie vorher. Dies bedeutet, ich muss entweder ControlPaint verwenden oder die Oberfläche mit GDI+ selbst zeichnen. Dies könnte im Falle eines Buttons zum Beispiel so aussehen:

ControlPaint.DrawButton(
    e.Graphics,
    new Rectangle(10, 10, 60, 40),
    ButtonState.Normal);

Es gibt jedoch eine Alternative: Für Standardelemente, wie Buttons und Check Boxen bietet das Framework vorgefertigte Renderer. Diese prüfen vor dem Zeichnen, ob Visual Styles aktiviert sind. Ist dies nicht so, verwenden Sie ControlPaint und GDI+-Methoden.

Um nun beispielsweise den oben gezeigten Button zu zeichnen, könnte ich auch wie folgt vorgehen:

ButtonRenderer.DrawButton(
    e.Graphics,
    new Rectangle(10, 10, 60, 40),
    PushButtonState.Normal);

Bei etwas komplexeren Controls müssen ggf. mehrere Methoden für die einzelnen Bestandteile der Oberfläche aufgerufen werden. In folgendem Beispiel wird eine Combo Box gezeichnet:

ComboBoxRenderer.DrawTextBox(
    e.Graphics,
    new Rectangle(10, 10, 130, 20),
    ComboBoxState.Normal);

ComboBoxRenderer.DrawDropDownButton(
    e.Graphics,
    new Rectangle(121, 11, 18, 18),
    ComboBoxState.Normal);

Die Renderer hilft hierbei nicht nur bei Berücksichtigung der Themes, sondern erleichtert vor allem das Zeichnen. Hierbei müssen nämlich einige Systemwerte, wie beispielsweise die im Betriebssystem eingestellte Fontgröße berücksichtigt werden. Die folgenden Renderer stehen standardmäßig zu Verfügung:

  • ButtonRenderer
  • CheckBoxRenderer
  • ComboBoxRenderer
  • GroupBoxRenderer
  • ProgressBarRenderer
  • RadioButtonRenderer
  • ScrollBarRenderer
  • TabRenderer
  • TextBoxRenderer
  • ToolStripRenderer
  • ToolStripSystemRenderer
  • ToolStripProfessionalRenderer
  • TrackBarRenderer
  • TextRenderer

Es gibt jedoch auch einen Haken: Nicht alle Renderer können auch bei deaktivierten Visual Styles verwendet werden. So löst beispielsweise der ComboBoxRenderer bei dem Versuch eine Exception aus. Allerdings bietet diese Klassen über die IsSupported-Eigenschaft die Möglichkeit dies zu prüfen.

Systemeinstellungen ermitteln
In manchen Fällen muss das Zeichnen der Oberfläche also auch weiterhin von Hand erfolgen. Hierbei sollten – wie bereits erwähnt - bestimmte Systemeinstellungen berücksichtigt werden. So kann beispielsweise die Höhe der Titelleiste je nach Konfiguration variieren.

Das Ermitteln solcher Informationen ist jedoch recht einfach über die SystemInformation-Klasse möglich. Sie bildet eine Reihe von Betriebssystemeinstellungen über statische Eigenschaften ab. Hier eine kleine Auswahl:

  • SystemInformation.CaptionHeight
  • SystemInformation.CaptionButtonSize
  • SystemInformation.FrameBorderSize
  • SystemInformation.IconSize
  • SystemInformation.IsFontSmoothingEnabled

VisualStyles und VisualStylesRenderer, Teil 2

Wie in Teil 1 beschrieben, können die Klassen VisualStyles und VisualStylesRenderer verwendet werden, um grafische Standardelemente im aktuellen Theme zu zeichnen. Doch was passiert eigentlich, wenn der Code unter Windows 2000 läuft, oder die Visual Styles deaktiviert wurden? Die Antwort ist einfach: Es kommt zu einer Exception.

Um zu prüfen, ob Visual Styles verfügbar sind, können Sie die IsSupported-Eigenschaft der VisualStylesRenderer-Klasse abfragen.

if (VisualStylesRenderer.IsSupported)
{
    ...
}

Jetzt kann es aber auch sein, dass die Anwendung, in der das eigene Control läuft, die Visual Styles abgeschaltet hat. Bekanntlich kann dies ja über Application.EnableVisualStyles() gesteuert werden. Das Control kann nun über

Application.RenderWithVisualStyles

abfragen, ob die Anwendung Visual Styles unterstützt. Darüber hinaus kann über Application.VisualStyleState auch noch festgestellt werden, für welche Bereiche die Visual Styles aktiviert wurden (Client-/NonClient-Area).

Kommen wir zum nächsten Problem: Manche Themes unterstützten nicht den vollständigen Satz an Windows-Elementen. Daher sollte vor dem Rendern eines Elements explizit geprüft werden, ob dieses vom aktiven Theme unterstützt wird. Und dies geschieht über

VisualStylesRenderer.IsElementDefined

Die vollständige Prüfung könnte somit wie folgt aussehen:

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);

    if (VisualStyleRenderer.IsSupported &&
        Application.RenderWithVisualStyles)
    {
        if (VisualStyleRenderer.IsElementDefined(VisualStyleElement.Button.PushButton.Normal))
        {
            VisualStyleRenderer renderer = new VisualStyleRenderer(VisualStyleElement.Button.PushButton.Normal);
            renderer.DrawBackground(
                e.Graphics,
                new Rectangle(100, 100, 100, 100));
        }
    }
}

Doch wie male ich denn eigentlich meine Oberfläche, wenn die Visual Styles deaktiviert wurden? Dies erfahren Sie im 3 Teil.

VisualStyles und VisualStylesRenderer, Teil 1

Bei der Entwicklung eigener Owner Drawn Controls benötigt man häufig Standardgrafikelemente, wie Buttons oder die Symbole von Combo- oder Check-Boxen. Hierfür konnte man bisher die Klasse ControlPaint verwenden, wie das folgende Beispiel zeigt:

ControlPaint.DrawButton(
    e.Graphics,
    new Rectangle(10, 10, 50, 30),
    ButtonState.Normal);

Das Problem dieser API ist jedoch, dass die Symbole ohne Berücksichtigung des aktiv eingestellten Windows-Theme gezeichnet werden. Zudem bietet ControlPaint nur eine sehr geringe Anzahl an Standardelementen.

Zur Lösung dieser Probleme wurden in .NET 2.0 die Klassen VisualStyles und VisualStylesRenderer eingeführt. Während Erstere eine Repräsentation der Windows-Standardsymbole bietet, ermöglicht Letztere das Zeichnen der Elemente. Hierbei wird der aktuell eingestellte Windows-Theme berücksichtigt.

VisualStyleElement (zu finden im Namespace System.Windows.Forms.VisualStyles) definiert 24 Unterklassen, die jeweils ein Element darstellen. Jedes dieser Elemente enthält ihrerseits Unterklassen, die den Typ näher spezifizieren. So definiert die Button-Klasse beispielsweise die Untertypen PushButton, CheckBox, RadioButton, GroupBox und UserButton. Jede dieser Klasse definiert wiederum verschiedene Stati in Form von Eigenschaften. Diese Stati repräsentieren den Zustand des zu zeichnenden Elements.
Ein einfacher Button könnte somit wie folgt definiert werden:

VisualStyleElement.Button.PushButton.Normal

Um den so definierten Button nun auf den Bildschirm zu bringen, kann die VisualStylesRenderer-Klasse verwendet werden.

protected override void OnPaint(PaintEventArgs e)
{
    VisualStyleRenderer renderer = new VisualStyleRenderer(
    VisualStyleElement.Button.PushButton.Normal);

    renderer.DrawBackground(
        e.Graphics, new Rectangle(0, 0, 100, 32));
}

VisualStyleRenderer zeichnet jedoch nur Visual Styles im jeweils eingestellten Theme. Wurden die Visual Styles in Windows deaktiviert, verweigert die Klasse seine Dienste. Wie dies ermittelt werden kann, beschreibe ich im 2 Teil dieser kleinen Serie.

Double Buffering in .NET 2.0

Ein Weg Performance-Probleme bei Zeichenoperationen zu umgehen ist Double Buffering. Hierbei wird die jeweilige Zeichenoperation nicht direkt auf dem Bildschirm ausgeführt, sondern zunächst im Speicher. Dieser Speicher wird daraufhin extrem schnell auf in den Bildschirmspeicher kopiert. Dies beschleunigt zwar nicht wirklich die Performance, vermeidet jedoch den altbekannten Flackereffekt.

Wollte man diese Technik unter .NET 1.x nutzen, so konnte man einen in der Control-Klasse eingebauten Mechanismus nutzen. Control bietet nämlich über die SetStyle()-Methode die Möglichkeit Double Buffering zu aktivieren. Das folgende Beispiel demonstriert dies:

this.SetStyle(
    ControlStyles.DoubleBuffer |
    ControlStyles.AllPaintingInWmPaint |
    ControlStyles.UserPaint |
    ControlStyles.ResizeRedraw,
    true);

Bei der Verwendung von Double Buffering sollten auch stets die Flags AllPaintingInWmPaint und UserPaint gesetzt werden. Sie weisen das Window an, keine WM_ERASEBKGND-Meldungen zu verarbeiten (Hintergrund löschen) und signalisiert, dass sich die Anwendung um das komplette Zeichnen der Oberfläche kümmert. Ist das Fenster oder Control resizable, so kann optional über ResizeRedraw ein automatisches Neuzeichnen bei Größenänderungen veranlasst werden.

In .NET 2.0 bietet die ControlStyles-Enumeration nun zusätzlich den Wert OptimizedDoubleBuffer. Dieser dient als Ersatz für DoubleBuffer und bietet in erster Linie eine bessere Performance. Die obere Anweisung könnte in .NET 2.0 somit wie folgt umgeschrieben werden.

this.SetStyle(
    ControlStyles.OptimizedDoubleBuffer |
    ControlStyles.AllPaintingInWmPaint |
    ControlStyles.UserPaint |
    ControlStyles.ResizeRedraw,
true);

Viele der im .NET-Framework 2.0 enthaltenen Komponenten nutzen übrigens diese Methode bereits standardmässig.

Double Buffering steuern
Darüber hinaus besteht nun auch die Möglichkeit dediziert in den Double Buffering-Prozess einzugreifen. Hierzu bietet der System.Drawing-Namespace die neuen Klassen BufferedGraphics, BufferedGraphicsContext und BufferedGraphicsManager. Das folgende Beispiel demonstriert die Verwendung:

// Grafikpuffer anlegen
Graphics gr = this.CreateGraphics();
BufferedGraphicsContext bufferedContext =
    BufferedGraphicsManager.Current;
bufferedContext.MaximumBuffer = this.Size;
BufferedGraphics bufferedGraphics =
    bufferedContext.Allocate(gr, this.ClientRectangle);

// Auf gepufferter Grafik zeichnen
this.DrawSomething(bufferedGraphics.Graphics);

// Grafik auf den Bildschirm rendern
bufferedGraphics.Render();


Hier wird zunächst ein Graphics-Objekt erzeugt, auf dem später gezeichnet werden soll. Daraufhin wird über BufferedGraphicsManager.Current ein BufferedGraphicsContext-Objekt ermittelt und dessen Puffer mit der Größe des Fensters initialisiert. Das Erzeugen des Speicherabbildes erfolgt die Allocate()-Methode, der das Graphics-Objekt, sowie eine Rectangle-Struktur mit Größe und Position des Zeichenbereichs übergeben wird. Allocate() gibt daraufhin ein BufferedGraphics-Objekt zurück, auf dem in der Folge gezeichnet werden kann. Es bietet alle Methoden und Eigenschaften wie Graphics, führt die Zeichenoperationen jedoch im Speicher und nicht auf dem Bildschirm aus. Für die Übertragung des Speicherabbildes auf den Bildschirm sorgt schließlich die Render()-Methode.

Das manuelle Double Buffering lohnt sich jedoch nur für extrem aufwendige Zeichenoperationen, bei denen eine sehr feine Steuerung der Bildschirmübertragung erforderlich ist. In der Regel reicht schon der oben gezeigte SetStyle()-Aufruf, um die Performance enorm zu verbessern.

Montag, April 03, 2006

AttributeProviderAttribute

Diese kurios klingende Attributklasse ist neu in .NET 2.0 und sehr hilfreich bei der Angabe von Entwurfszeitinformationen. Wenn Sie beispielsweise eine Komponente entwickeln, die datenbindungsfähig sein soll, würden Sie ihr wahrscheinlich eine DataSource-Eigenschaft nach folgendem Strickmuster spendieren:

[Editor(typeof(DataSourceListEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(DataSourceConverter))]
public object DataSource
{
   get { ... }
   set { ... }
}

Die Attribute Editor und TypeConverter teilen hierbei dem PropertyGrid mit, welche UITypeEditor-Klasse, bzw. welche TypeConverter-Klasse zum Anzeigen und Konvertieren der Datenquelle verwendet werden soll. Hierdurch kann im Eigenschaftenfenster die Datenquelle bequem ausgewählt werden.



Mit Hilfe von AttributeProviderAttribute kann diese Deklaration nun drastisch verkürzt werden:

[AttributeProvider(typeof(IListSource))]
public object DataSource
{
   get { ... }
   set { ... }
}

AttributeProvider kann auf Eigenschaftsebene verwendet werden und nimmt ein Type-Objekt entgegen. Das PropertyGrid ermittelt nun die Attribute über den angegebenen Typ und verwendet diese zur Bindung.

Bei einem Blick auf IListSource entdeckt man schließlich wieder die üblichen Verdächtigen UITypeEditor, TypeConverter und co.

[Editor("System.Windows.Forms.Design.DataSourceListEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.Drawing.Design.UITypeEditor, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"),
TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"),
MergableProperty(false)]
public interface IListSource
{
   IList GetList();
   bool ContainsListCollection { get; }
}

Der Vorteil dieser Variante ist hierbei nicht nur die einfache Verwendung, sondern vor allem die Vorwärtskompatibilität: Ändert sich in einer späteren Version des Frameworks die Entwurfszeitunterstützung beim Data Binding, kann auf Ebene von IListSource einfach ein anderer UITypeEditor bzw. TypeConverter angegeben werden und alle darauf verweisenden Eigenschaften sind automatisch auf dem neusten Stand.

Komponenten mit Smart Tags veredeln

Ein nettes Feature des WinForms 2.0-Designers sind die Smart Tags. Sie fassen die wichtigsten Eigenschaften einer Komponente in übersichtlicher Form zusammen und ersparen einem die übliche Suche im Eigenschaftenfenster.
Eigene Komponenten mit einem Smart Tag zu versehen, ist relativ einfach, wenn man erst einmal die Funktionsweise der dahinter liegenden API verstanden hat. Die folgende Grafik zeigt den Aufbau:


Zunächst muss die Komponente über das Designer-Attribut auf eine von ControlDesigner abgeleiteten Klasse verweisen. Die ControlDesigner bringt nun eine ActionLists-Eigenschaft mit, die in der eigenen Designer-Klasse überschrieben werden kann. Sie gibt eine Instanz von DesignerActionListCollection zurück, die wiederum eine Instanz einer von DesignerActionList abgeleiteten Klasse enthält.
Was sich da zunächst etwas verwirrend anhört ist in der Praxis jedoch recht simpel, wie das folgende Beispiel zeigt:

Komponente:

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Windows.Forms;

namespace SimpleSmartTagDemo
{
    [Designer(typeof(LabelExDesigner))]
    class LabelEx : Label
    {
    }
}


Designer:

using System;
using System.ComponentModel.Design;
using System.Windows.Forms.Design;

namespace SimpleSmartTagDemo
{
    internal class LabelExDesigner : ControlDesigner
    {
        private DesignerActionListCollection m_actionLists;
        
        public override DesignerActionListCollection ActionLists
        {
            get
            {
                if (m_actionLists == null)
                {
                    m_actionLists = new DesignerActionListCollection();
                    m_actionLists.Add(new LabelExActionList((LabelEx)Control));
                }
                return m_actionLists;
            }
        }
    }
}


ActionList:

using System;
using System.Drawing;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Reflection;

namespace SimpleSmartTagDemo
{
    internal class LabelExActionList : DesignerActionList
    {
        private LabelEx m_labelEx;

        public LabelExActionList(LabelEx ctrl) : base(ctrl)
        {
            this.m_labelEx = ctrl;
        }

        public override DesignerActionItemCollection GetSortedActionItems()
        {
            DesignerActionItemCollection items = new DesignerActionItemCollection();

            // Header definieren
            items.Add(new DesignerActionHeaderItem("Text"));
            items.Add(new DesignerActionHeaderItem("Farben"));
            items.Add(new DesignerActionHeaderItem("Information"));

            // Text-Eigenschaft
            items.Add(new DesignerActionPropertyItem(
                "Text",
                "Anzeigetext:",
                "Text",
                "Setzt den Anzeigetext."));

            // TextAlign-Eigenschaft
            items.Add(new DesignerActionPropertyItem(
                "TextAlign",
                "Ausrichtung:",
                "Text",
                "Setzt die Textausrichtung."));

            // ForeColor-Eigenschaft
            items.Add(new DesignerActionPropertyItem(
                "ForeColor",
                "Vordergrundfarbe:",
                "Farben",
                "Setzt die Vordergrundfarbe."));

            // BackColor-Eigenschaft
            items.Add(new DesignerActionPropertyItem(
                "BackColor",
                "Hintergrundfarbe:",
                "Farben",
                "Setzt die erste Hintergrundfarbe."));

            // Information über die Größe
            items.Add(new DesignerActionTextItem(
                "Größe: Breite = " + m_labelEx.Width.ToString() + "; Höhe = " + m_labelEx.Height.ToString(),
                "Information"));

            // Information über die Position
            items.Add(new DesignerActionTextItem(
                "Position: X = " + m_labelEx.Left.ToString() + "; Y = " + m_labelEx.Top.ToString(),
                "Information"));

            return items;
        }

        public String Text
        {
            get { return m_labelEx.Text; }
            set { this.SetPropertyValue("Text", value); }
        }

        public ContentAlignment TextAlign
        {
            get { return m_labelEx.TextAlign; }
            set { this.SetPropertyValue("TextAlign", value); }
        }

        public Color ForeColor
        {
            get { return m_labelEx.ForeColor; }
            set { this.SetPropertyValue("ForeColor", value); }
        }

        public Color BackColor
        {
            get { return m_labelEx.BackColor; }
            set { this.SetPropertyValue("BackColor", value); }
        }

        private void SetPropertyValue(String name, object value)
        {
            PropertyDescriptor descriptor = TypeDescriptor.GetProperties(m_labelEx)[name];
            if (descriptor != null)
                descriptor.SetValue(m_labelEx, value);
            else
                throw new ArgumentException("Die gesuchte Eigenschaft konnte nicht ermittelt werden!", name);
        }
    }
}

Hierbei ist ein Verweis auf die Assembly System.Design.dll notwendig.

Das Ergebnis sieht nun in etwa so aus:



Liste meiner Vorträge

202420232022
2021
2020
    • Apps intelligenter machen: Machine Learning für Entwickler
      W-JAX 2020
    • Schön und ungebunden: Leichtgewichtige UI-Komponenten fürs Web entwickeln
      W-JAX 2020
    • Workshop: Schön und ungebunden: Leichtgewichtige UI-Komponenten fürs Web entwickeln
      Web Developer Conference 2020
    • Schön und ungebunden: Leichtgewichtige UI-Komponenten fürs Web entwickeln
      BASTA! 2020
    • Web UI Feuerwerk mit SVG und der Web Animation API
      BASTA! 2020
    • Workshop: Intelligente Apps entwickeln mit Azure Machine Learning, ML.NET und Cognitive Services
      BASTA! 2020
    • Von der Idee zur App: Agile Konzeption mit Storyboards
      DWX Home 2020
    • Keynote: Machine Learning: Softwareentwicklung 2.0?
      BASTA! Spring 2020
    • Let’s Flutter: Cross Platform a la Google
      BASTA! Spring 2020
    • User Experience Design für Entwickler
      BASTA! Spring 2020
    • Von der Idee zur App: Agile Konzeption mit Storyboards
      BASTA! Spring 2020
    • Workshop: Intelligente Apps entwickeln mit Azure Machine Learning, ML.NET und Cognitive Services
      BASTA! Spring 2020
    2019
    2018
    2017

    • Web Components mit Angular: UI-Feuerwerk mit Struktur
      (zusammen mit Manuel Rauber)
      W-JAX 2017
    • Retro Computing: Spaß mit C64 und 8 Bit – früher war doch alles besser!?
      (zusammen mit Christian Weyer)
      W-JAX 2017
    • Design First Development mit Storyboards
      EKON 2017
    • Workshop: Web Components mit Angular: UI-Feuerwerk mit Struktur
      (zusammen mit Manuel Rauber)
      Angular Days 2017
    • Keynote: Interaktion auf allen Ebenen: Die Zukunft des User Experience Design
      BASTA! 2017
    • Build, Test, Distribute: App-Entwicklung mit Visual Studio Mobile Center
      BASTA! 2017
    • Enterprise Apps mit Xamarin und den Azure App Services entwickeln
      BASTA! 2017
    • Design-First Development mit Storyboards
      BASTA! 2017
    • Workshop: Xamarin.Forms Deep Dive
      BASTA! 2017
    • Die Crossplattform-App-Strategie von Microsoft
      Pre-BASTA!- Meetup 2017
    • .NET Standard: One library to rule them all
      XPC: Heise Cross Platform Conference
    • Build, Measure, Learn: Erfolgreiche Mobile-Entwicklung im DevOps-Stil
      (zusammen mit Neno Loje)
      Seacon 2017
    • Agiles Testen in der Mobile-App-Entwicklung: Vom Vorgehen bis zur Umsetzung
      JAX 2017
    • Keynote: Build, Measure, Learn: Erfolgreiche Mobile-Entwicklung im DevOps-Stil
      (zusammen mit Neno Loje)
      .NET Summit 2017
    • Cross-Platform-App-Development mit Xamarin
      .NET Summit 2017
    • Workshop: Architekturen für XAML-basierte Apps
      .NET Summit 2017
    • Design First Development mit Storyboards
      MobileTechCon 1-2017
    • Cross-Plattform-Apps mit Xamarin.Forms entwickeln
      MobileTechCon 1-2017
    • Build, Test, Distribute: App-Entwicklung mit Visual Studio Mobile Center automatisieren
      BASTA! Spring 2017
    • Cross-Plattform-Apps mit Xamarin.Forms entwickeln
      BASTA! Spring 2017
    • Workshop: Cross-Plattform-App-Development mit Xamarin
      BASTA! Spring 2017
    2016

    2015
    2014
    2013
    2012
    2011
    2010
    2009
    2008
    2007

    2006
    2005

    Labels: