titel.fm Seite 1 Dienstag, 20. August 2002 3:48 15
Windows Forms
titel.fm Seite 2 Dienstag, 20. August 2002 3:48 15
titel.fm Seite 3 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
Michael Kofler
Windows Forms Grafische Benutzerschnittstellen
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
titel.fm Seite 4 Dienstag, 20. August 2002 3:48 15
Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig eingetragene Produktbezeichnungen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.
5 05
4
3 04
2
1
03
02
ISBN 3-8273-1994-3 © 2002 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Einbandgestaltung: Barbara Thoben, Köln Lektorat: Christiane Auf,
[email protected], Tobias Draxler,
[email protected] Korrektorat: Andrea Stumpf, München Herstellung: Monika Weiher,
[email protected] Satz: reemers publishing services gmbh, Krefeld, www.reemers.de Druck und Verarbeitung: Media Print, Paderborn Printed in Germany
winIVZ.fm Seite 5 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
Inhalt Vorwort
7
1
Explorer-Benutzeroberfläche
11
1.1 1.2 1.3 1.4 1.5 1.6 1.7
Programmaufbau Fensterlayout (Splitter) Verzeichnisstruktur anzeigen (TreeView) Liste der Bitmaps anzeigen (ListView) Detailansicht einer einzelnen Bitmap (PictureBox) Zustandsinformationen anzeigen (StatusBar) Symbolleiste verwalten (ToolBar)
12 15 21 24 37 38 40
2
Formularinterna
43
2.1 2.2 2.3 2.4 2.5 2.6 2.7
Codeausführung Ereignisreihenfolge Nachrichtenschleife (message loop) Hintergrundberechnungen mit DoEvents Windows Form Designer Code Automatische DPI-Anpassung Formular dynamisch erzeugen
43 44 45 46 48 53 57
3
Steuerelemente dynamisch verwalten
59
3.1 3.2 3.3
Ereignisprozeduren einrichten Steuerelemente dynamisch einfügen Schleife über alle Steuerelemente
59 61 65
4
Owner-drawn-Steuerelemente
67
4.1 4.2 4.3
Buttons grafisch gestalten Owner-drawn-Listenfelder Owner-drawn-Menüs
68 70 74
5
Verwaltung mehrerer Fenster
81
5.1 5.2
Modale Dialoge Gleichberechtigte Fenster
81 84
6
MDI- und Docking-Anwendungen
89
6.1 6.2 6.3 6.4
Menüverwaltung MDI-Fensterverwaltung Die Magic-Bibliothek Docking mit der Magic-Bibliothek
89 94 99 101
5
winIVZ.fm Seite 6 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6
Inhalt
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
6.5 6.6
Dialogblätter mit der Magic-Bibliothek RichText-Docking-Editor
109 112
7
Multithreading
125
7.1 7.2 7.3 7.4
Grundlagen Windows-Multithreading Verzeichniseigenschaften in einem eigenen Thread ermitteln Mehrere Fenster in eigenen Threads öffnen
125 130 136 141
8
Optische Effekte
145
8.1 8.2
Windows-XP-Optik Begrüßungsbild (Splash-Bitmap)
145 147
9
Zwischenablage und Drag&Drop
151
9.1 9.2 9.3 9.4 9.5
Zwischenablage nutzen Drag&Drop Symbolleiste verschieben Drag&Drop zwischen ListView-Steuerelementen Datei-Drop aus dem Windows-Explorer
151 155 159 162 165
Stichwortverzeichnis
169
WINFORM.fm Seite 7 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
Vorwort Wer die Medienberichte über .NET verfolgt hat, bekam vielleicht den Eindruck, .NET sei nur für Internetanwendungen konzipiert. Diese Einschätzung ist vollkommen falsch! Das .NET-Framework bietet mit der neuen Bibliothek System.Windows.Forms auch zur Entwicklung von Windows-Anwendungen eine Menge neuer Möglichkeiten, von denen VB6- oder C++-Programmierer früher nur träumen konnten. In diesem Buch stelle ich Ihnen einige (zum Teil schon etwas fortgeschrittene) Programmiertechniken für die Windows.Forms-Bibliothek vor. Das Ziel besteht einerseits darin, Ihnen konkrete Lösungsansätze zu Problemen anzubieten, die bei der Gestaltung eigener Benutzeroberflächen immer wieder auftreten. Andererseits versucht dieses Buch aber auch, einen Blick hinter die Kulissen zu werfen – etwa wenn es darum geht, welche Aufgaben der von der Entwicklungsumgebung erzeugte Code erfüllt oder wie sich Multithreading mit einer Windows-Anwendung vereinen lässt. Dieses Buch setzt voraus,
Voraussetzungen
왘 dass Ihnen die Visual Basic .NET- bzw. Visual Studio .NET-Entwicklungsumgebung von Microsoft zur Verfügung steht, 왘 dass Sie Visual Basic .NET grundsätzlich beherrschen und mit der Entwicklungsumgebung umgehen können, 왘 dass Sie elementare Aufgaben der Windows-Programmierung (Button einfügen, Eigenschaften einstellen, Ereignisprozedur hinzufügen) entweder schon beherrschen oder sich selbst aneignen und 왘 dass Sie hier keine Referenz aller Windows.Forms-Steuerelemente oder -Programmiertechniken erwarten. (Dazu gibt es die Online-Hilfe.) Dieses Buch richtet sich also explizit an Leser mit Vorwissen! Ein solcher Ansatz hat den Vorteil, dass Sie sich hier nicht durch zweihundert einleitenden Seiten quälen müssen, um endlich zu den essentials zu kommen. Dieses Buch setzt genau bei diesen essentials ein! Das vorliegende Buch will Ihnen helfen, professionelle Benutzeroberflächen unter .NET rasch und effizient zu entwickeln. Dabei wünsche ich Ihnen viel Erfolg und Spaß!
Viel Erfolg!
Michael Kofler, Juli 2002 http://www.kofler.cc
7
WINFORM.fm Seite 8 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
Vorwort
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Hinweis für Leser meines VB.NET-Buchs Mein anderes Visual Basic .NET-Buch
Leser meines Buchs Visual Basic .NET – Grundlagen, Programmiertechniken, Windows-Programmierung werden in diesem Buch nur wenig Neues finden. Die beiden Bücher überlappen sich im Bereich Windows.Forms stark. Mein Visual Basic .NET-Buch gibt darüber hinaus aber eine vollständige Einführung in die Programmierung mit Visual Basic .NET (objektorientierte Programmierung, Zugriff auf Dateien, Grafik und Drucken, Weitergabe von Anwendungen etc.). Das hier vorliegende Buch richtet sich dagegen an Leser, die Visual Basic .NET bereits grundsätzlich beherrschen und sich speziell zum Thema Windows.Forms-Benutzeroberflächen einlesen möchten.
Programmcode Alle Programme wurden mit Visual Basic .NET entwickelt. Dieselben Programmiertechniken können natürlich aber auch unter C# angewendet werden. C#-Freunde werden feststellen, dass sie den Code meist mühelos lesen können. Die Verwendung von Eigenschaften und Methoden erfolgt in beiden Programmiersprachen auf identische Art und Weise, und auch sonst wurde versucht, möglichst keine Visual Basic .NET-spezifischen Sprachelemente einzusetzen. Kompletter Code im Internet
Aus Platzgründen sind bei Programmlistings generell nur die für den jeweiligen Abschnitt interessanten Passagen abgedruckt. Den vollständigen Code finden Sie als ZIP-Archiv auf meiner Website www.kofler.cc oder auf www.dotnet-essentials.de. Um Ihnen bei der Suche nach den Beispieldateien zu helfen, finden Sie am Beginn jedes Listings einen Kommentar der Art 'Beispiel fenster\multi1. Das bedeutet, dass sich der Code im entsprechenden Verzeichnis innerhalb des ZIP-Archivs befindet.
Parameter von Ereignisprozeduren
Um Platz zu sparen und an Übersichtlichkeit zu gewinnen, sind Ereignisprozeduren meist ohne die Parameterliste abgedruckt. Statt Private Sub Button1_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button1.Click ... Programmcode in der Ereignisprozedur End Sub
wird also oft nur Private Sub Button1_Click(...) _ Handles Button1.Click ... Programmcode in der Ereignisprozedur End Sub
8
WINFORM.fm Seite 9 Dienstag, 20. August 2002 3:48 15
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
abgedruckt. Das erscheint mir deswegen zweckmäßig, weil an Windows-Ereignisprozeduren ohnedies immer dieselben zwei Parameter übergeben werden: sender mit dem zugrunde liegenden Objekt und e mit ereignisspezifischen Daten. Zudem wird in vielen Ereignisprozeduren keiner dieser Parameter ausgewertet.
9
WINFORM.fm Seite 10 Dienstag, 20. August 2002 3:48 15
WINFORM.fm Seite 11 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1
Explorer-Benutzeroberfläche
In díesem Kapitel steht ein konkretes Beispielprogramm im Vordergrund, das eine ähnliche Benutzeroberfläche erzeugt, wie Sie sie vom Windows Explorer, von Outlook und von vielen anderen Programmen kennen: links ein hierarchisches Listenfeld, mit dem Sie ein Verzeichnis oder ein Objekt auswählen können; rechts oben ein gewöhnliches Listenfeld, das den Inhalt des Verzeichnisses anzeigt; und rechts unten ein Dokumentfeld, das Details zu dem im Listenfeld ausgewählten Objekt anzeigt.
erstellt von ciando
Abbildung 1.1: Der Bitmap-Viewer
Ziel des Beispielprogramms ist, einen raschen Überblick über die Bilddateien in einem Verzeichnis zu geben. Dazu können Sie links das gewünschte Verzeichnis auswählen. Es werden dann rechts alle im Verzeichnis gefundenen Bitmaps (*.bmp, *.ico, *.gif, *.png, *.tif, *.jpg) in einer Detail- oder Miniaturansicht (siehe Abbildung 1.1) angezeigt. Wenn Sie eine einzelne Bitmap auswählen, wird diese in voller Größe im Bildbereich rechts unten dargestellt.
11
WINFORM.fm Seite 12 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
In diesem Kapitel geht es natürlich nicht einfach darum, die Details des Programms zu beschreiben. Vielmehr werden auch die folgenden Fragen beantwortet:
왘 Wie kann ein komplexes Fensterlayout mit variabler Platzaufteilung umgesetzt werden? 왘 Wie erfolgt die Verwaltung der Daten in einem TreeView-Steuerelement? 왘 Wie können Listen mit spezifischen Daten nach eigenen Kriterien dargestellt und sortiert werden? 왘 Wie können beliebig große Bitmaps mit Schiebebalken angezeigt werden? 왘 Wie kann während längerer Berechnungen ein Zustandsbalken innerhalb eines StatusBar-Steuerelements angezeigt werden? 왘 Wie können in einer Symbolleiste Radio-Buttons realisiert werden? Darüber hinaus vermittelt dieses Kapitel natürlich auch Grundlageninformationen zu allen eingesetzten Steuerelementen. Leser, die in die Windows.Forms-Programmierung gerade erst eingestiegen sind, werden in diesem Kapitel auch ein paar praktische Tipps zum Umgang mit der Entwicklungsumgebung finden. Die Eingabe des gesamten Programms über die Entwicklungsumgebung entsprechend der folgenden Beschreibung ist ziemlich mühsames und fehleranfälliges Verfahren, das ich nicht empfehle. Wenn Sie das Programm ausprobieren möchten, finden Sie die Beispieldateien im Internet (siehe Vorwort). Wenn Sie dagegen – ausgehend von den hier präsentierten Ideen – ein eigenes Programm entwickeln möchten, ist es besser, mit einem leeren Projekt zu starten und dieses dann wirklich selbstständig zu entwickeln. Dadurch können Sie die einzelnen Programmelemente Schritt für Schritt testen und erweitern; wenn dabei Fehler auftreten, ist die Ursache meist leicht festzustellen.
1.1 Programmaufbau Bei dem Programm handelt es sich um eine gewöhnliche WindowsAnwendung (DATEI|NEU|PROJEKT, Projekttyp WINDOWS-ANWENDUNG). Das Programm besteht aus nur einem einzigen Formular Form1, dessen Code sich in der Datei form1.vb befindet. Der Code hat folgende Struktur:
12
WINFORM.fm Seite 13 Dienstag, 20. August 2002 3:48 15
Programmaufbau
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Beispiel explorer\bitmap-viewer Option Strict On ' Code zur Verwaltung des Fensters Public Class Form1 Inherits System.Windows.Forms.Form [Vom Windows Form Designer generierter Code] ' Klassenvariablen und –konstante ' Meldung, wenn Verzeichnis ohne Bitmaps Const no_bitmaps_msg As String = _ "keine Bitmaps in diesem Verzeichnis ..." ' Sortierordnung für Detailansicht Private srtColumn As enums.lvCol = _ enums.lvCol.name Private srtOrder As SortOrder = _ SortOrder.Ascending ' gibt an, wie viele Bitmaps eingelesen wurden Private progress As Double ... eigener Code End Class ' Klasse, um einen Listeneintrag zu verwalten Public Class bitmapItem ... End Class ' Klasse, um Spalten zu sortieren Public Class CompareColumn ... End Class ' allgemein verwendbare Aufzählungen Public Module enums ... End Class
Die Anweisung Option Strict On bewirkt, dass der Visual Basic .NETCompiler bei jeder Variablendeklaration die Angabe des Datentyps bzw. der Klasse verlangt und dass er immer überprüft, ob Sie bei der Anwendung der Variablen keine unerlaubten Typkonvertierung vornehmen. Option Strict On erzeugt präziseren Code, der weniger anfällig gegenüber Flüchtigkeitsfehlern ist.
Option Strict
Die Variablen srtColumn, srtOrder und progress werden in verschiedenen Prozeduren der Klasse Form1 benötigt und sind daher als Klassenvariablen deklariert.
13
WINFORM.fm Seite 14 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Klassen und Module Form-Klassen
Jede Windows.Forms-Anwendung besteht aus zumindest einer Klasse, die das Formular bzw. Fenster beschreibt (hier Form1). Der Code innerhalb dieser Klasse besteht aus zwei Teilen: der eine Teil (Vom Windows Form Designer generierter Code) wird automatisch erstellt und ist dafür verantwortlich, dass die in der Entwicklungsumgebung eingefügten Steuerelemente beim Programmstart tatsächlich am richtigen Ort und mit den richtigen Eigenschaften erscheinen. Hintergrundinformationen zu diesem Codeblock folgen in Abschnitt 3.5. Der andere Teil besteht aus Ereignisprozeduren, die Sie selbst eingeben müssen. Die Ereignisprozeduren bewirken, dass das Programm auf das Anklicken eines Buttons oder auf die Auswahl eines Listenelements tatsächlich reagiert. Darüber hinaus dürfen Sie beliebig viele weitere Klassen und Module definieren. Dabei ist es egal, ob Sie jede Klasse in einer eigenen Datei speichern oder ob Sie wie bei diesem Beispiel alle Klassen der Reihe nach in einer einzigen Datei anordnen. (Ich persönlich bevorzuge die zweite Variante dann, wenn die einzelnen Klassen bzw. Module nur wenig Code enthalten. Das erspart in der Entwicklungsumgebung den ständigen Wechsel zwischen unterschiedlichen Codefenstern. Aber letzten Endes ist das eine reine Geschmacksfrage.)
Aufzählungen (Enums) KonstantenAufzählungen
Im Code werden mehrfach Konstanten eingesetzt, um auf die in ImageList-Steuerelementen gespeicherten Bitmaps zuzugreifen bzw. um die Spalten des ListView-Steuerelements in der Detailansicht komfortabel anzusprechen. Damit diese Konstanten in allen Klassen gleichermaßen zur Verfügung stehen, wurden sie in einem Modul deklariert. (In einem Modul als Global deklarierte Konstrukte können im gesamten Code ohne die Nennung des Modulnamens verwendet werden, also z.B. in der Form ilistIndex.drive. Wenn die Aufzählung dagegen in einer Klasse deklariert worden wäre, würde die Schreibweise so aussehen: klassenname.ilistIndex.drive.) ' Beispiel explorer\bitmap-viewer Public Module enums ' Zugriff auf Spalten in listview1 Public Enum lvCol name = 0 'Spalte 1: Dateiname fSize = 1 'Spalte 2: Dateigröße bSize = 2 'Spalte 3: Bitmap-Größe type = 3 'Spalte 4: Dateityp change = 4 'Spalte 4: Datum der letzten 'Änderung End Enum ' Zugriff auf Buttons in ilistTreeView
14
WINFORM.fm Seite 15 Dienstag, 20. August 2002 3:48 15
Fensterlayout (Splitter)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Public Enum ilistIndex folder = 0 'Bitmap 1: Verzeichnis open_folder = 1 'Bitmap 2: geöffnetes 'Verzeichnis drive = 2 'Bitmap 3: Laufwerk bitmap = 3 'Bitmap 4: Symbol für 'Bitmap (Bild) End Enum End Module
1.2 Fensterlayout (Splitter) Die erste Herausforderung dieses Beispielprogramms liegt nicht im Code, sondern bereits beim Fensterlayout. Das Ziel besteht darin, dass die einzelnen Steuerelemente sich selbstständig an die Fenstergröße anpassen und dass die Größe der drei Bereiche des Fensters (links, rechts oben und rechts unten) variabel ist, also vom Anwender des Programms verändert werden kann.
Steuerelemente andocken Mit der Dock-Eigenschaft können Sie ein Steuerelement an einen Fensterrand andocken (Dock = Left, Right, Bottom oder Top). Das Steuerelement ist damit gewissermaßen an einen Fensterrand geklebt. Außerdem nimmt das Steuerelement automatisch die gesamte Fensterbreite bzw. -höhe an (je nachdem, wo es angedockt wird).
Dock-Eigenschaft
Mit Dock=Fill erreichen Sie, dass das Steuerelement den gesamten zur Verfügung stehenden Raum einnimmt, der nicht bereits von anderen angedockten Steuerelementen beansprucht wird. Grundsätzlich ist es möglich, mehrere Steuerelemente in einem Fenster anzudocken. In der Praxis funktioniert das allerdings nur dann zufriedenstellend, wenn alle Steuerelemente nur horizontal oder nur vertikal angedockt werden. Wenn Sie mehrere Steuerelemente auf einer Seite nebeneinander andocken möchten, bestimmt die Einfügereihenfolge die Position (d.h., welches Steuerelement ganz am Rand, welches etwas weiter eingerückt ist etc.). Nachträglich können Sie die Reihenfolge angedockter Steuerelemente durch die Kontextmenükommandos IN DEN HINTERGRUND oder IN DEN VORDERGRUND verändern.
Mehrere Steuerelemente andocken
Wenn Sie Steuerelemente sowohl horizontal als auch vertikal andocken möchten, besteht die beste Strategie darin, zuerst die Steuerelemente für eine Ausrichtung (vertikal oder horizontal) anzudocken. In den verbleibenden Leerraum fügen Sie ein Panel-Steuerelement ein und stellen dessen Dock-Eigenschaft auf Fill. Nun fügen Sie alle weiteren Steuerelemente in das Panel ein. Für diese Steuerelemente bestimmt die DockEigenschaft die Platzierung innerhalb des Panel. Auf diese Weise kön-
Panel-Steuerelement
15
WINFORM.fm Seite 16 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
nen Sie beinahe beliebig komplexe Layouts erreichen. (Das PanelSteuerelement ist ein Container für andere Steuerelemente. Im laufenden Programm ist es unsichtbar.)
Splitter-Steuerelement Das Steuerelement stellt eine per Maus verschiebbare Linie dar. Es wird dazu verwendet, um den Innenbereich eines Fensters in zwei (oder mehr) unterschiedlich große Bereiche zu unterteilen. Das Splitter-Steuerelement ist leider recht wählerisch, was die Voraussetzungen für seine Anwendung betrifft: Nur für angedockte Steuerelemente
왘 Der Splitter kann nur dazu verwendet werden, die Größe von angedockten Steuerelementen zu verändern. (Das liegt daran, dass auch das Splitter-Element selbst immer angedockt sein muss. Die Einstellung Dock=None ist unzulässig.) Im Regelfall sollte das Steuerelement auf der einen Seite des Splitter mit Dock=Left, Right, Bottom oder Up an einem Fensterrand angedockt sein. Das Steuerelement an der anderen Seite sollte mit Dock=Fill den Rest des Fensters füllen. (Wenn Sie nicht das ganze Fenster füllen möchten, fügen Sie einfach beide Steuerelemente sowie den Splitter in ein Panel-Steuerelement ein!)
Reihenfolge
왘 Beim Einfügen der Steuerelemente in das Formular spielt die Reihenfolge eine wichtige Rolle. Der Splitter sollte unmittelbar nach dem Steuerelement eingefügt werden, das an einen Fensterrand gedockt ist. Genau genommen ist nicht die Einfügereihenfolge entscheidend, sondern der so genannte Z-Order-Wert. Dieser Wert gibt an, welches Steuerelement welches andere überdeckt (sofern sich die Steuerelemente überhaupt überlappen). Damit der Splitter rechts von einem mit Dock=Left angedockten Steuerelement erscheint, muss der Splitter in der Z-Reihenfolge über dem Steuerelement liegen. Die interne Reihenfolge kann per Kontextmenü (IN DEN HINTERGRUND, IN DEN VORDERGRUND) verändert werden.
Horizontaler und vertikaler Splitter
왘 Das Splitter-Steuerelement ist per Default selbst gedockt, und zwar mit Dock=Left. Wenn Sie mit dem Splitter die Größe eines an den rechten Fensterrand gedockten 3 verändern möchten, müssen Sie Dock=Right einstellen. Um eine horizontale Teilung zwischen zwei Steuerelementen zu erreichen, müssen Sie Dock auf Top oder Bottom stellen (je nachdem, wie das anliegende Steuerelement gedockt ist). Um mehr als zwei Fensterbereiche zu erreichen, ist es am einfachsten, ein Panel-Steuerelement zu Hilfe zu nehmen. Dazu führen Sie zuerst eine Zweiteilung vor (z.B. links das Steuerelement A mit Dock=Left, rechts ein Panel-Steuerelement). Anschließend fügen Sie in das Panel die Steuerelemente B mit Dock=Up ein, dann den zweiten Splitter mit Dock=Up und schließlich C mit Dock=Fill.
16
WINFORM.fm Seite 17 Dienstag, 20. August 2002 3:48 15
Fensterlayout (Splitter)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Aussehen und Verhalten Per Default ist der Splitter nur drei Pixel breit. Wenn Sie ein breiteres Steuerelement möchten, verändern Sie die Size-Eigenschaft (am besten direkt im Eigenschaftsfenster). BorderStyle bestimmt das Aussehen des Splitter-Randes. Per Default wird der Rand gar nicht angezeigt, Sie können aber auch einen zwei- oder dreidimensionalen Rand verwenden. MinSize gibt an, wie klein das neben dem Splitter angedockte Steuerele-
ment werden darf (per Default 25 Pixel breit oder hoch). MinExtra gibt an, wie klein das auf der anderen Seite des Splitter befindliche Steuerelement (mit Dock=Fill) werden darf (per Default ebenfalls 25 Pixel). Probleme mit dem Splitter gibt es oft, wenn die Größe des Fensters verändert wird. Die Position des Splitter wird in diesem Fall nicht automatisch verändert, und die MinXxx-Eigenschaften werden nicht berücksichtigt! Das kann dazu führen, dass einzelne Bereiche des Fensters ganz unsichtbar werden. Die einzige Abhilfe besteht darin, die minimale Fenstergröße zu limitieren und die Splitter-Position in der Resize- oder Layout-Ereignisprozedur des Fensters zu überwachen und gegebenenfalls zu verändern.
Splitter-Probleme bei der Änderung der Fenstergröße
Die Steuerelemente des Beispielprogramms Tabelle 1.1 gibt an, welche Steuerelemente sich im Formular befinden, welche Namen und Aufgaben sie haben und welche Einstellungen im Eigenschaftsfenster vorgenommen wurden. (Details zu einzelnen Steuerelementen folgen im weiteren Verlauf des Kapitels.) Dabei bezeichnet ilistXxx jeweils ein ImageList-Steuerelement. lblXxx sind Labels, bntXxx sind Buttons. Bei allen anderen Steuerelementen geht der Steuerelementtyp eindeutig aus dem Namen hervor. ilistTreeview
enthält vier Bitmaps für die Steuerelemente treeview1 und listview1.
ilistToolbar
enthält zwei Bitmaps für das Steuerelement toolbar1.
ilistListviewSmall
ist vorerst leer. Das Steuerelement wird im Programm als Container für die Bitmaps von listview1 in der Detailansicht dienen.
ilistListviewLarge
ist vorerst leer. Das Steuerelement wird im Programm als Container für die Bitmaps von listview1 in der Miniaturansicht dienen. Eigenschaften: ImageSize=96;96
toolbar1
enthält einige Buttons zur Steuerung der Listenansicht. Eigenschaften: Dock=Top, ImageList=ilistToolbar, Buttons=...
Tabelle 1.1: Die Steuerelemente des Beispielprogramms Bitmap-Viewer
17
WINFORM.fm Seite 18 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
statusbar1
dient zur Anzeige von Status- und Fehlermeldungen. Eigenschaften: Dock=Bottom, ShowPanels=True, Panels=... (zwei StatusBarPanels mit den Namen sbpText und sbpProgress)
panelAll
umfasst das gesamte Fenster mit Ausnahme der Symbol- und Statuszeile.
treeview1
dient zur Anzeige des Verzeichnisbaums. Das Steuerelement wird in panelAll eingefügt.
Eigenschaften: Dock=Fill
Eigenschaften: Dock=Left, ImageList=ilistTree, Sorted=True splitter1
dient als Teiler zwischen linkem und rechtem Fensterabschnitt. Das Steuerelement wird in panelAll eingefügt.
panelRight
umfasst den rechten Fensterbereich (also alles rechts von treeview1). Das Steuerelement wird in panelAll eingefügt.
Eigenschaften: Dock=Left, MinSize=50, MinExtra=50
Eigenschaften: Dock=Fill. listview1
dient zur Anzeige der Bitmap-Liste. Das Steuerelement wird in panelRight eingefügt. Eigenschaften: Dock=Top, Font=Arial, Multi Select=False, View=Details, SmallImageList= ilistTreeView, LargeImageList=ilistListView, Columns=...
splitter2
dient als Teiler zwischen dem oberen und dem unteren rechten Fensterabschnitt. Das Steuerelement wird in panelRight eingefügt. Eigenschaften: Dock=Top, MinSize=50, MinExtra=50
panelHeading
umfasst den mittleren rechten Fensterbereich (Beschriftung der Bitmap und die Buttons GRÖSSE ANPASSEN und ORIGINALGRÖSSE). Das Steuerelement wird in panelRight eingefügt. Eigenschaften: Dock=Fill, BackColor=ActiveCaption
lblBitmapName
dient zur Anzeige des Dateinamens der Bitmap. Der Label wird in panelHeading eingefügt. Eigenschaften: Anchor=Left,Right,Top, ForeColor=ActiveCaptionText, Font=12 pt;Bold
btnStretch, btnOriginal
dienen zur Einstellung der Bitmap-Anzeige. Beide Buttons werden in panelHeading eingefügt. Eigenschaften: BackColor=Control
Tabelle 1.1: Die Steuerelemente des Beispielprogramms Bitmap-Viewer (Forts.)
18
WINFORM.fm Seite 19 Dienstag, 20. August 2002 3:48 15
Fensterlayout (Splitter)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
umfasst den unteren rechten Fensterbereich (Vollansicht der Bitmap). Wird in panelRight eingefügt.
panelBitmap
Eigenschaften: Dock=Fill, AutoScroll=True, BackColor=AppWorkspace dient zur Anzeige der Bitmap. Wird in panelBitmap eingefügt.
picturebox1
Tabelle 1.1: Die Steuerelemente des Beispielprogramms Bitmap-Viewer (Forts.)
Es ist relativ schwierig, die Eigenschaften eines Splitter-Steuerelements nachträglich einzustellen, weil das Splitter-Steuerelement beim Programmentwurf fast unsichtbar ist und nur schwer mit der Maus angeklickt werden kann. Sie können das Steuerelement aber auch ganz einfach mit dem Listenfeld des Eigenschaftsfenster aktivieren!
Splitter im Eigenschaftsfenster auswählen
Wie aus der obigen Beschreibung der Dock-Eigenschaft und des Splitter-Steuerelements hervorgegangen ist, müssen die gedockten Steuerelemente in der richtigen Reihenfolge eingefügt werden (entsprechend der Reihenfolge in der Tabelle). Die Struktur des Fensters geht auch aus Abbildung 1.2 hervor. Wichtig ist aber auch der Ort des Einfügens, weil das Beispielprogramm mehrere Panel-Steuerelemente als Container für andere Steuerelemente verwendet. Um ein neues Steuerelement in ein Panel einzufügen, klicken Sie in der Entwicklungsumgebung zuerst das gewünschte Panel-Steuerelement an und führen dann in der Toolbox einen Doppelklick aus, um das neue Steuerelement einzufügen.
toolbar1
treeview1
splitter1
panelAll panelRight listview1
splitter2 panelHeading mit zwei Buttons und einem Label
panelBitmap mit einer PictureBox
statusbar1
Abbildung 1.2: Die Hierararchie der Steuerelemente innerhalb des Programms
19
WINFORM.fm Seite 20 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Mindestgröße für panelHeading und panelBitmap sicherstellen Nachdem Sie die Steuerelemente wie oben beschrieben eingefügt haben, können Sie das Programm zum ersten Mal starten. Zwar bleiben die Steuerelemente vorerst leer, aber Sie können bereits ausprobieren, wie das Programm reagiert, wenn Sie die Fenstergröße bzw. die Fensteraufteilung durch das Verschieben der Splitter-Steuerelemente ändern. Resize-Ereignis
Ein Aspekt hat sich beim Test des Programms oft als störend herausgestellt: Wenn die Fensterhöhe zu stark verkleinert wird, dann wird der rechte untere Fensterbereich mit den beiden Buttons und der Vollansicht der Bitmap unsichtbar. Die folgende Resize-Ereignisprozedur für das Steuerelement panelRight vermeidet das: Wenn der Fensterbereich unterhalb des ListView-Steuerelements kleiner als 80 Punkte ist und insgesamt genug Platz ist, dann wird die Höhe des ListView-Steuerelements so verkleinert, dass darunter 80 Punkte Raum für die anderen Steuerelemente ist (splitter2, panelHeading und panelBitmap). ' Beispiel explorer\bitmap-viewer ' Klasse Form1 Private Sub panelRight_Resize( _ ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles panelRight.Resize Dim h As Integer If Me.WindowState = _ FormWindowState.Minimized Then Exit Sub h = panelRight.ClientSize.Height If listview1.Height > h - 80 Then If h > 160 Then listview1.Height = h – 80 End If End If End Sub
Ereignisprozeduren effizient eingeben
20
Wenn Sie Ereignisprozeduren eingeben, können Sie sich dabei von der Entwicklungsumgebung helfen lassen: In Visual Basic .NET wählen Sie im Codefenster im linken Listenfeld das gewünschte Steuerelement (hier panelRight) und im rechten Listenfeld das gewünschte Ereignis aus (hier Resize). Damit wird eine Schablone für die Ereignisprozedur eingefügt. In C# finden Sie eine vergleichbare Funktion im Eigenschaftsfenster. Dort klicken Sie den gelben Pfeil an. Das Fenster zeigt nun alle zur Auswahl stehenden Ereignisse an. Per Doppelklick können Sie nun die Schablone für die gewünschte Prozedur einfügen. Weitere Details zum Umgang mit Ereignisprozeduren folgen in Abschnitt 4.1.
WINFORM.fm Seite 21 Dienstag, 20. August 2002 3:48 15
Verzeichnisstruktur anzeigen (TreeView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
1.3 Verzeichnisstruktur anzeigen (TreeView) Das TreeView-Steuerelement ermöglicht die Darstellung hierarchischer Listen. Das bekannteste Beispiel für eine derartige Liste ist die Verzeichnisstruktur einer Festplatte, die ja auch im Windows-Explorer in einer hierarchischen Form dargestellt wird.
Verwaltung der Listeneinträge Die Einträge der hierarchischen Liste werden durch TreeNode-Objekte verwaltet. Die Nodes-Eigenschaft des TreeView-Steuerelements verweist auf die Einträge der untersten Ebene, SelectedNode verweist auf das momentan aktive Listenelement. Wenn Sorted=True gilt (was per Default nicht der Fall ist!), werden die Listeneinträge innerhalb jeder Ebene automatisch sortiert.
TreeNode-Klasse
Bei jedem TreeNode-Objekt verweist die Eigenschaft Nodes auf deren untergeordneten Einträge. Ausgehend von einem gegebenen Listeneintrag können Sie mit NextNode und PrevNode das nächste bzw. vorige Element ermitteln. (Die Eigenschaften liefern Nothing, wenn es kein weiteres Element mehr gibt.) FirstNode und LastNode liefern das erste bzw. letzte Element innerhalb der aktuellen Hierarchiegruppe. Parent verweist auf die übergeordnete Ebene (bzw. enthält Nothing, wenn sich das aktuelle Element in der ersten Hierarchieebene befindet). FullPath gibt den kompletten Namen eines Eintrags zurück, der aus
Name eines TreeNodes
allen übergeordneten Einträgen zusammengesetzt wird. Dabei werden die einzelnen Teile durch den Inhalt der PathSeparator-Eigenschaft des ListView-Steuerelements getrennt (per Default \).
Bitmaps anzeigen Üblicherweise wird links von jedem Listeneintrag eine kleine Bitmap angezeigt. Damit das funktioniert, müssen alle erforderlichen Bitmaps vorher in ein ImageList-Steuerelement eingefügt werden. Die Bitmaps sollten eine Größe von 16*16 Pixel haben. ImageList ist ein unsichtbares Steuerelement, dessen einzige Aufgabe darin besteht, Bitmaps für die spätere Verwendung in anderen Steuerelementen zu speichern. In der Entwicklungsumgebung können die Bitmaps über den IMAGE-AUFLISTUNGS-EDITOR eingefügt werden. Abbildung 1.3 zeigt diesen Dialog mit den Bitmaps, die im Beispielprogramm zum Einsatz kommen: Die drei ersten Bitmaps werden im TreeView-Steuerelement verwendet, das vierte in der Detailansicht des ListView-Steuerelements.
ImageListSteuerelement
Nachdem die Bitmaps (mühsam) in das ImageList-Steuerelement geladen wurden, muss die ImageList-Eigenschaft des TreeView-Steuerelements auf das ImageList-Steuerelement gerichtet werden.
21
WINFORM.fm Seite 22 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Abbildung 1.3: Die Bitmaps des ImageList-Steuerelements ilistTree
Außerdem muss bei jedem TreeNode-Eintrag mit den Eigenschaften ImageIndex und SelectedImageIndex angegeben werden, welche Bitmap neben dem Listeneintrag angezeigt werden soll. Die beiden Eigenschaften ermöglichen es, zwei Bitmaps anzugeben, je nachdem, ob der Listeneintrag gerade ausgewählt ist oder nicht.
Beispielprogramm Laufwerke des Rechners ermitteln
Grundsätzlich kann das TreeView-Steuerelement in der Visual Studio .NET-Benutzeroberfläche initialisiert werden – das ist aber selten sinnvoll. Meistens erfolgt diese Initialisierung ausschließlich per Code. Im Beispielprogramm ist dafür die Prozedur Form1_Load verantwortlich, die beim Programmstart automatisch ausgeführt wird. Die Prozedur ermittelt mit Environment.GetLogicalDrives ein String-Feld mit den Namen aller am Rechner bekannten Laufwerke. Jedes dieser Laufwerke wird mit Nodes.Add in das TreeView-Steuerelement eingefügt. Dabei wird jeder Eintrag mit dem Laufwerk-Bitmap verbunden (unabhängig davon, ob der Eintrag gerade ausgewählt ist oder nicht). Die letzten Zeilen der Prozedur machen Nodes(1) (das ist üblicherweise C:) zum aktiven Listeneintrag und löschen die Texte in der Statusbar und im Beschriftungslabel für die Vollansicht der Bitmap. ' Beispiel explorer\bitmap-viewer ' Klasse Form1 Private Sub Form1_Load(...) Handles MyBase.Load Dim s As String Dim drvs As String() = _
22
WINFORM.fm Seite 23 Dienstag, 20. August 2002 3:48 15
Verzeichnisstruktur anzeigen (TreeView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Environment.GetLogicalDrives() Dim tn As TreeNode treeview1.Nodes.Clear() For Each s In drvs tn = treeview1.Nodes.Add(s) tn.ImageIndex = ilistIndex.drive tn.SelectedImageIndex = ilistIndex.drive Next ' sollte normalerweise c: auswählen Try treeview1.SelectedNode = treeview1.Nodes(1) Catch End Try lblBitmapName.Text = "" sbpText.Text = "" End Sub
Jedes Mal, wenn ein Listenelement per Doppelklick ausgewählt wird, kommt es zum Aufruf von treeview1_DoubleClick. Dort wird mit der Methode GetNodeAt anhand der Mausposition (die von absoluten in relative Koordinaten umgerechnet wird) das angeklickte Listenelement festgestellt. (Dieses Element wird unverständlicherweise nicht als Parameter an die Ereignisprozedur übergeben.) Wenn das Ergebnis nicht Nothing lautet und wenn das Listenelement nicht schon Untereinträge hat, wird die Prozedur ReadDirectories aufgerufen, um alle Unterverzeichnisse zu ermitteln und in das Listenfeld einzutragen.
TreeNode unter der Maus ermitteln
' Beispiel explorer\bitmap-viewer ' Klasse Form1 Private Sub treeview1_DoubleClick(...) _ Handles treeview1.DoubleClick Dim tn As TreeNode tn = treeview1.GetNodeAt( _ treeview1.PointToClient( _ treeview1.MousePosition())) If Not IsNothing(tn) Then treeview1.SelectedNode = tn If tn.Nodes.Count = 0 Then ReadDirectories(tn) End If End If End Sub ReadDirectories verwendet ein IO.DirectoryInfo-Objekt und dessen Methode GetDirectories, um alle Unterverzeichnisse eines gegebenen Verzeichnisses zu ermitteln. (Der Pfad des Ausgangsverzeichnisses wird der FullPath-Eigenschaft des übergebenen TreeNode-Parameters entnommen.)
Verzeichnisse ermitteln
23
WINFORM.fm Seite 24 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Fehlerabsicherung
Die Prozedur ist durch Try-Catch zweifach gegen Fehler abgesichtert. Die wahrscheinlichste Fehlerursache besteht darin, dass das gesamte Verzeichnis oder auch nur ein einzelnes Unterverzeichnis aufgrund mangelnder Zugriffsrechte nicht gelesen werden dürfen. Fehlermeldungen werden in der Statuszeile angezeigt. (sbpText bezeichnet das erste Panel-Feld innerhalb der Statuszeile, siehe Abschnitt 1.6.) tn.Nodes.Add fügt den Namen des gefundenen Unterverzeichnisses in
das TreeView-Steuerelement ein. Dabei werden zwei unterschiedliche Bitmaps angegeben, so dass das gerade aktive Verzeichnis durch ein Symbol für einen geöffneten Ordner hervorgehoben wird. ' Beispiel explorer\bitmap-viewer ' Klasse Form1 Private Sub ReadDirectories( _ ByVal tn As TreeNode) Dim di As New IO.DirectoryInfo(tn.FullPath) Dim subdir As IO.DirectoryInfo Dim newtn As TreeNode sbpText.Text = "" Try 'falls Fehler bereits bei GetDirectories 'auftritt, z.B. bei Diskettenlaufwerk 'ohne Diskette For Each subdir In di.GetDirectories() Try 'falls Fehler bei einem einzelnen 'Verzeichnis auftritt, z.B. bei 'fehlenden Leserechten newtn = tn.Nodes.Add(subdir.Name) newtn.ImageIndex = ilistIndex.folder newtn.SelectedImageIndex = _ ilistIndex.open_folder Catch ex As Exception sbpText.Text = "Fehler: " + ex.Message End Try Next tn.Expand() Catch ex As Exception sbpText.Text = "Fehler: " + ex.Message End Try End Sub
1.4 Liste der Bitmaps anzeigen (ListView) ListView-Steuerelement
24
Das ListView-Steuerelement ermöglicht vier unterschiedliche Darstellungsformen von Listen. Das Beispielprogramm unterstützt davon nur zwei Ansichten, nämlich eine Miniaturansicht (View=LargeIcon), bei der jede Bitmap durch ein großes Icon dargestellt wird (siehe Abbildung 1.1), sowie die Detailansicht (View=Details), bei der zu jedem Listenein-
WINFORM.fm Seite 25 Dienstag, 20. August 2002 3:48 15
Liste der Bitmaps anzeigen (ListView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
trag Detaildaten in weiteren Spalten angezeigt werden (siehe Abbildung 1.5). Die beiden anderen View-Einstellungen ermöglichen eine mehrspaltige Darstellung der Liste mit kleinen Icons.
Verwaltung der Listendaten Listeneinträge werden in ListViewItem-Einträgen gespeichert. Der Zugriff auf alle Listeneinträge erfolgt über die Items-Eigenschaft. SelectedItems gibt an, welche Listeneinträge momentan ausgewählt sind. (Je nach der Einstellung von MultiSelect ist es möglich, mehrere Listeneinträge gleichzeitig zu markieren. Im Beispielprogramm gilt aber MultiSelect = False.)
ListViewItem-Klasse
Dim lvitem As ListViewItem lvitem = ListView1.Items.Add("listeneintrag 1")
Die ListViewItem-Klasse eignet sich nur zur Speicherung von Zeichenketten (Text-Eigenschaft). Wenn Sie in Listenelementen weitere Informationen speichern möchten (die nicht unbedingt auch angezeigt werden sollen), ist es das Beste, eine eigene Klasse zu definieren, die von ListViewItem abgeleitet ist. Ein entsprechendes Beispiel folgt etwas weiter unten.
Detailansicht Wenn Sie die Detailansicht (View=Details) nutzen möchten, müssen Sie zuerst Spalten definieren. Das lässt sich am bequemsten in der Entwicklungsumgebung erledigen. Ein Doppelklick auf die Eigenschaft Columns führt in den Einstellungsdialog.
Abbildung 1.4: Beschriftung der Spalten in der Entwicklungsumgebung
25
WINFORM.fm Seite 26 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Abbildung 1.4 zeigt die Spalten für den Bitmap-Viewer (im Hintergrund das Programm in der Entwurfsansicht, im Vordergrund der Dialog für die Columns-Eigenschaft). Bei jeder Spalte kann der Name, die Defaultbreite und die Textausrichtung (links, rechts oder zentriert) angegeben werden. Spaltenbeschriftung per Code
Grundsätzlich können Sie die Spalten aber auch per Code erzeugen, wie die folgenden Zeilen beweisen: ListView1.Columns.Clear() ListView1.Columns.Add("spalte 1", 120, _ HorizontalAlignment.Left) ListView1.Columns.Add("spalte 2", 60, _ HorizontalAlignment.Right) ListView1.Columns.Add("spalte 3", 60, _ HorizontalAlignment.Right)
Der Anwender kann im laufenden Programm die Breite der Spalten verändern. (Dazu ist kein Code erforderlich.) Durch AllowColumnReorder können Sie auch erlauben, dass die Reihenfolge der Spalten umgestellt wird. Spaltentexte eintragen
Bei der Detailansicht des Listenfelds beschreibt ListViewItem.Text den Text der ersten Spalte. Der Text für die weiteren Spalten wird durch ListViewSubItem-Objekte definiert. Dim lvitem As ListViewItem lvitem = ListView1.Items.Add("spalte1") lvitem.SubItems.Add("spalte2") lvitem.SubItems.Add("spalte3")
ListView-Inhalt effizient ändern
Wenn Sie im laufenden Programm größere Veränderungen an den Listenelementen durchführen, sollten Sie zu Beginn die Methode BeginUpdate und zum Abschluss EndUpdate ausführen. Sie vermeiden damit, dass das Steuerelement bei jeder einzelnen Veränderung am Bildschirm aktualisiert wird, was lange dauert und ein störendes Flackern verursacht. Wenn Sie zahlreiche Listeneinträge besonders effizient einfügen möchten, können Sie dazu die AddRange-Methode verwenden. Diese Methode erwartet als Parameter ein Feld von ListViewItem-Objekten (die Sie also vorweg erzeugen müssen).
26
WINFORM.fm Seite 27 Dienstag, 20. August 2002 3:48 15
Liste der Bitmaps anzeigen (ListView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Abbildung 1.5: Der Bitmap-Viewer in der Detailansicht
Listeneinträge mit Bitmaps darstellen Wenn Sie die Listeneinträge mit Bildern verschönern möchten, müssen Sie die Bitmaps in zwei ImageList-Steuerelementen speichern. Dabei sollte die eine ImageList kleine Bitmaps enthalten (üblich sind 16*16 Pixel für View=List, SmallIcon oder Details), die andere ImageList große Bitmaps (üblich sind 32*32 Pixel, das Beispielprogramm verwendet allerdings 96*96 Pixel). Beachten Sie, dass die Bitmap-Größe des ImageList-Steuerelements eingestellt werden muss, bevor (!) die Bitmaps eingefügt werden. Per Default gilt eine Größe von 16*16 Pixel.
ImageListSteuerelement
Die Verbindung zwischen den beiden ImageList-Steuerelementen und dem ListView-Steuerelement erfolgt über die Eigenschaften Large- und SmallImageList, die Sie üblicherweise im Eigenschaftsfenster einstellen. Außerdem müssen Sie nun bei jedem Listeneintrag eine Indexnummer angeben, die beschreibt, welches Bild aus der ImageList verwendet werden soll. Dazu sieht die ListViewItem-Klasse die Eigenschaft ImageIndex vor.
Liste sortieren Das ListView-Steuerelement sieht eine einfache Möglichkeit vor, die Listeneinträge zu sortieren: Wenn Sie die Eigenschaft Sorting auf Ascending oder Descending setzen, werden die Listeneinträge ihren Texten entsprechend auf- oder absteigend sortiert. (Per Default ist Sorting auf None gestellt. Die Listeneinträge werden dann in der Reihenfolge angezeigt, in der sie eingefügt wurden.) Bei der mehrspaltigen Detailansicht sollte es eigentlich möglich sein, die Liste auch nach den Inhalten der weiteren Spalten zu sortieren. Leider bietet das ListView-Steuerelement hierfür keine einfache Möglichkeit. Stattdessen muss an die ListViewItemSorter-Eigenschaft ein Objekt übergeben werden, welches die IComparer-Schnittstelle realisiert. Die Methode Compare dieser Klasse wird dann jedes Mal aufgerufen, wenn das ListView-Steuerelement zwei Listeneinträge miteinander vergleicht.
IComparer-Schnittstelle
27
WINFORM.fm Seite 28 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
(Auch wenn die Eigenschaft ListViewItemSorter es nahelegt, müssen Sie sich um das Sortieren auch weiterhin nicht kümmern. Sie müssen aber eine Klasse programmieren, die zwei ListViewItem-Objekte vergleicht. Ein entsprechendes Beispiel folgt gleich.) Der Code zur Anwendung dieser neuen Klasse sieht folgendermaßen aus: ListView1.ListViewItemSorter = _ New SorterClass(...)
Bei der Anwendung der ListViewItemSorter-Eigenschaft müssen Sie einige Besonderheiten beachten:
왘 Bei der Veränderung der ListViewItemSorter-Eigenschaft wird die Liste automatisch sortiert. Zu einem späteren Zeitpunkt können Sie die Sortierung manuell mit der Sort-Methode auslösen. Falsche Sortierung
왘 Wenn Sie neue Einträge in das ListView-Steuerelement einfügen, werden diese entsprechend der aktuellen Sortierordnung automatisch korrekt eingeordnet. Allerdings funktioniert das nicht immer zuverlässig. Abhilfe schafft erst das manuelle Ausführen der SortMethode am Ende der Einfügeoperation.
Ineffiziente Sortierung
왘 Darüber hinaus hat sich herausgestellt, dass das Einfügen von Listenelementen bei der Verwendung einer eigenen Sortierfunktion unglaublich langsam erfolgt (und umso langsamer, je mehr Einträge die Liste bereits hat). Das gilt auch dann, wenn vor dem Einfügen der neuen Einträge BeginUpdate und danach EndUpdate ausgeführt wird. Abhilfe schafft es, die Sortierfunktion mit ListViewItemSorter = Nothing vorübergehend abzuschalten und erst nach dem Ende der Einfügeoperationen wieder zu aktivieren. Bleibt als letzte Frage noch, wo die Sortierung ausgelöst werden soll? Der geegnete Ort ist üblicherweise die ColumnClick-Ereignisprozedur des ListView-Steuerelements: An die Prozedur wird die gerade angeklickte Spalte übergeben (e.Column). Nun kann ListViewItemSorter entsprechend eingestellt werden.
Das ListView-Steuerelement im Bitmap-Viewer Nachdem die Basisfunktionen des ListView-Steuerelements nun einigermaßen klar sein sollten, ist es an der Zeit, zum Bitmap-Viewer zurückzukehren. Die Grundeinstellungen für das ListView- und die beiden zugeordneten ImageList-Steuerelemente gehen aus der Tabelle in Abschnitt 3.2 hervor, die Spaltenbeschriftung aus Abbildung 1.4. Vererbungs-Beispiel
28
Zur Verwaltung der Listeneinträge wird im Beispielprogramm die Klasse bitmapItem verwendet, die mit Inherits von ListViewItem abgeleitet (vererbt) ist. Die Klasse zeichnet sich durch einige zusätzliche Klas-
WINFORM.fm Seite 29 Dienstag, 20. August 2002 3:48 15
Liste der Bitmaps anzeigen (ListView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
senvariablen aus, die dazu verwendet werden, den vollständigen Dateinamen der Bitmap, ihren Typ (z.B. "BMP"), die Dateigröße, die Bitmap-Größe und den Zeitpunkt der letzten Änderung zu speichern. Die neue Klasse ist mit zwei New-Konstruktoren ausgestattet. Der erste ist nur für Notfälle gedacht und erwartet als Parameter eine Zeichenkette, die in der Text-Eigenschaft der ListViewItem-Basisklasse gespeichert wird. Normalerweise wird der zweite Konstruktor verwendet, der als Parameter ein FileInfo-Objekt und die Indexnummer für eine ImageListBitmap erwartet. Diese New-Prozedur initialisiert die meisten Klassenvariablen sowie die Beschriftung der Spalten (Text- und Sub Items-Eigenschaften). ' Beispiel explorer\bitmap-viewer ' Klasse bitmapItem Public Class bitmapItem Inherits ListViewItem Public fullname As String Public type As String Public fileSize As Long Public lastChange As Date Public width As Integer Public height As Integer Public Sub New(ByVal s As String) Me.Text = s End Sub Public Sub New(ByVal fi As IO.FileInfo, _ ByVal imageindex As Integer) ' interne Daten initialisieren Me.fullname = fi.FullName Me.type = UCase(Mid(fi.Extension, 2)) Me.fileSize = fi.Length Me.lastChange = fi.LastWriteTime ' ListViewItem-Daten Me.Text = fi.Name Me.ImageIndex = imageindex Me.SubItems.Add( _ (fileSize \ 1024).ToString + " kB") Me.SubItems.Add("") Me.SubItems.Add(type) Me.SubItems.Add( _ lastChange.ToShortDateStringh+ " " + _ lastChange.ToShortTimeString) End Sub End Class
29
WINFORM.fm Seite 30 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Bitmap-Liste einlesen Wenn der Anwender im TreeView-Steuerelement ein Verzeichnis auswählt, tritt das AfterSelect-Ereignis auf. In der dazugehörenden Ereignisprozedur werden zuerst einige Initialisierungsarbeiten durchgeführt, die vorhandene Daten im ListView- und in den dazugehörenden Image List-Steuerelemente löschen und die weitere Arbeit so effizient wie machen sollen. Dateien eines Verzeichnisses ermitteln
Anschließend ermittelt die GetFiles-Methode eine Liste aller Dateien im aktuellen Verzeichnis. Nur Dateinamen, die mit einer der in exts aufgeführten Kennungen enden, werden in das Listenfeld aufgenommen. Dazu wird ein neues bitmapItem-Objekt erzeugt. Außerdem wird in das ilistListViewSmall-Steuerelement ein Verweis auf die Bitmap 3 aus dem ilistTree eingerichtet. Das bewirkt, dass in der Detailansicht bei jedem Listenelement das in Abbildung 1.3 dargestellte Bitmap-Symbol angezeigt wird. Wenn im aktuellen Verzeichnis keine Bitmap gefunden wurde, wird ein einziges Listenelement mit dem Text keine Bitmaps in diesem Verzeichnis eingefügt. Dieser Text ist in der Konstanten no_bitmaps_msg enthalten. Bis hierhin sollte der Code selbst bei Verzeichnissen mit vielen Bitmaps relativ rasch ausgeführt werden. Was nun noch fehlt, ist die Ermittlung der Größe aller Bitmaps sowie die Berechnung der Mini-Bitmaps für die Miniaturansicht. Diese beiden (bei vielen Bitmaps sehr zeitaufwändigen) Aufgaben werden in der Prozedur LoadMiniBitmaps erledigt, wobei das Listenfeld in seiner aktuellen Form vorher durch Refresh angezeigt wird. ' Beispiel explorer\bitmap-viewer ' Klasse Form1 Private Sub treeview1_AfterSelect(...) _ Handles treeview1.AfterSelect Dim fi As IO.FileInfo Dim di As IO.DirectoryInfo Dim tn As TreeNode Dim lvi As ListViewItem Dim ext, s As String Dim exts() As String = _ {".bmp", ".ico", ".png", ".gif", _ ".jpeg", ".jpg", ".tif", ".tiff"} Dim ok As Boolean tn = treeview1.SelectedNode If IsNothing(tn) Then Exit Sub ' Titelzeile ändern Me.Text = "Bitmap-Viewer: " + tn.FullPath Me.Cursor = Cursors.WaitCursor ' ListView-Einträge löschen
30
WINFORM.fm Seite 31 Dienstag, 20. August 2002 3:48 15
Liste der Bitmaps anzeigen (ListView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
listview1.Items.Clear() ilistListViewSmall.Images.Clear() ilistListViewLarge.Images.Clear() ' ListView möglichst effizient bearbeiten listview1.ListViewItemSorter = Nothing listview1.BeginUpdate() ' Verzeichnis durchsuchen di = New IO.DirectoryInfo(tn.FullPath) Try For Each fi In di.GetFiles() ' testen, ob richtige Dateikennung ok = False ext = LCase( _ IO.Path.GetExtension(fi.Name)) For Each s In exts If ext = s Then ok = True Exit For End If Next ' Eintrag in Listview aufnehmen If ok Then ilistListViewSmall.Images.Add( _ ilistTree.Images(ilistIndex.bitmap)) listview1.Items.Add( _ New bitmapItem(fi, 0)) End If Next Catch ex As Exception sbpText.Text = "Fehler: " + ex.Message End Try ' keine einzige Bitmap gefunden If listview1.Items.Count = 0 Then listview1.Items.Add( _ New bitmapItem(no_bitmaps_msg)) End If listview1.ListViewItemSorter = _ New CompareColumn(srtColumn, srtOrder) listview1.EndUpdate() listview1.Refresh() LoadMiniBitmaps() listview1.Refresh() Me.Cursor = Cursors.Default End Sub
31
WINFORM.fm Seite 32 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Bitmaps für Miniaturansicht erstellen LoadMiniBitmaps führt eine Schleife über alle Listeneinträge aus. Darin
wird versucht, die Datei zu laden und in ilistViewLarge einzufügen. (Beim Einfügen wird die Bitmap automatisch auf die vorgesehene Bitmap-Größe des ListView-Steuerelements skaliert. Im Beispielprogramm sind das 96*96 Pixel.) Außerdem wird die Größe der Bitmap ermittelt; diese Daten werden in das bitmapItem-Objekt eingetragen. Statusbar aktualisieren
Nach jeder verarbeiteten Bitmap wird die Statusbar aktualisiert. Dort wird sowohl ein Text (z.B. Lese Bitmap 27 von 351) als auch ein Zustandsbalken angezeigt, der dem Inhalt der Klassenvariable progress entspricht. (Details zur Darstellung des Zustandsbalkens folgen in Abschnitt 1.6.) Zum Ende der Prozedur wird die Statusbar ein letztes Mal aktualisiert. Nun wird darin angezeigt, wie viele Bitmaps sich im aktuellen Verzeichnis befinden. Der Zustandsbalken wird zurückgesetzt. ' Beispiel explorer\bitmap-viewer ' Klasse Form1 Private Sub LoadMiniBitmaps() Dim bmi As bitmapItem Dim lvi As ListViewItem Dim bm As Bitmap Dim i, n As Integer 'Anzahl der zu bearbeitenden Einträge n = listview1.Items.Count If n = 1 And _ listview1.Items(0).Text = no_bitmaps_msg _ Then Exit Sub ' Schleife über alle Listeneinträge For Each lvi In listview1.Items bmi = CType(lvi, bitmapItem) Try bm = New Bitmap(bmi.fullname) bmi.width = bm.Width bmi.height = bm.Height bmi.SubItems(lvCol.bSize).Text = _ bm.Width.ToString + " x " + _ bm.Height.ToString Catch ' falls ein Fehler auftritt, einfach 'leere Bitmap verwenden bm = New Bitmap(1, 1) End Try ilistListViewLarge.Images.Add(bm) bm.Dispose()
32
WINFORM.fm Seite 33 Dienstag, 20. August 2002 3:48 15
Liste der Bitmaps anzeigen (ListView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Statusbar aktualisieren i += 1 sbpText.Text = "Lese Bitmap " + _ i.ToString + " von " + n.ToString progress = i / n statusbar1.Refresh() Next ' Statusbar aktualisieren progress = 0 If listview1.Items.Count > 2 Then sbpText.Text = _ listview1.Items.Count.ToString + _ . " Bitmaps" Else sbpText.Text = "" End If statusbar1.Refresh() End Sub
Listeneinträge in der Detailansicht sortieren Wenn in der Detailansicht ein Spaltentitel angeklickt wird, dann wird die Liste entsprechend der Einträge in dieser Spalte sortiert. Wie oben bereits erwähnt wurde, muss dazu an die ListViewItemSorter-Eigenschaft ein Objekt übergeben werden, das die IComparer-Schnittstelle realisiert. Dazu muss die Klasse eine Compare-Methode zum Vergleich von zwei Objekten enthalten.
Liste nach Spalten sortieren
Für den Bitmap-Viewer ist zum Objektvergleich die Klasse Compare Column vorgesehen. Bei der Initialisierung eines neuen Objekts dieser Klasse (New-Konstruktor) muss angegeben werden, nach welcher Spalte und ob auf- oder absteigend sortiert werden soll. Diese Informationen werden in den Klassenvariablen column und order gespeichert. Das ListView-Steuerelement ruft nun bei jedem Vergleichsvorgang die Compare-Methode auf und übergibt normalerweise zwei ListViewItemObjekte. Da im Beispielprogramm ausschließlich bitmapItem-Objekte im Listenfeld gespeichert werden, können die beiden Compare-Parameter durch CType ohne vorherigen Typentest in bitmapItem-Objekte umgewandelt werden. Die anschließende Select-Case-Konstruktion zur Durchführung des Vergleichs sollte unmittelbar verständlich sein.
Vergleichsalgorithmus
' Beispiel explorer\bitmap-viewer ' Klasse CompareColumn Public Class CompareColumn Implements Icomparer ' gibt an, ob auf- oder absteigend sortiert ' werden soll (System.Windows.Forms.SortOrder) Public order As SortOrder ' gibt an, nach welcher Spalte sortiert werden
33
WINFORM.fm Seite 34 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
' soll Public column As lvCol Public Sub New(ByVal column As lvCol, ByVal order As SortOrder) Me.column = column Me.order = order End Sub Public Function Compare( _ ByVal x As Object, ByVal y As Object) _ As Integer Implements IComparer.Compare Dim result As Integer Dim bm1, bm2 As bitmapItem bm1 = CType(x, bitmapItem) bm2 = CType(y, bitmapItem) Select Case column Case lvCol.name ' Namensvergleich (Groß- und ' Kleinschreibung ignorieren) result = String.Compare(bm1.Text, _ bm2.Text, True) Case lvCol.type ' Typenvergleich result = String.Compare(bm1.type, _ bm2.type, True) Case lvCol.fSize ' Dateigröße result = bm1.fileSize.CompareTo( _ bm2.fileSize) Case lvCol.bSize ' Bitmap-Größe Dim pixel1, pixel2 As Long pixel1 = bm1.height * bm1.width pixel2 = bm2.height * bm2.width result = pixel1.CompareTo(pixel2) Case lvCol.change ' Datumsvergleich result = Date.Compare(bm1.lastChange, _ bm2.lastChange) End Select ' falls erstes Sortierkriterium wirkungslos ' war, als zweites Kriterium den Namen ' verwenden If result = 0 Then result = String.Compare(bm1.Text, _ bm2.Text, True) End If
34
WINFORM.fm Seite 35 Dienstag, 20. August 2002 3:48 15
Liste der Bitmaps anzeigen (ListView)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Ergebnis umdrehen, falls absteigend If order = SortOrder.Ascending Then Return result Else Return –result End If End Function End Class
Nun zur Anwendung von Objekten dieser Klasse: Die Ereignisprozedur listview1_ColumnClick wird immer dann aufgerufen, wenn der Anwender eine Spalte des Listenfelds anklickt. Wenn die Spalte angeklickt wurde, die bereits jetzt als Sortierkriterium gilt, dann wird nur die Sortierreihenfolge geändert; andernfalls gilt die angeklickte Spalte als neues Kriterium und die Sortierreihenfolge ist per Default ansteigend. Basierend auf diesen Informationen wird ein neues CompareColumnObjekt erzeugt und der ListViewItemSorter-Eigenschaft zugewiesen.
Sortierung ändern
Der Rest des Codes dient dazu, in der Spalte, die als Sortierkriterium gilt, einen nach oben oder nach unten gerichteten Pfeil anzuzeigen. Das ist allerdings nicht ganz so einfach – das ListView-Steuerelement sieht dazu eigentlich keine Möglichkeit vor.
Unicode-Pfeile im Spaltentitel anzeigen
왘 Um ein Pfeilsymbol in den Programmcode einzufügen, geben Sie das Pfeilzeichen zuerst mit Hilfe eines Textverarbeitungsprogramms in ein beliebiges Dokument ein (in Microsoft Word z.B. mit EINFÜGEN|SYMBOL, SCHRIFTART Arial, SUBSET Pfeile). Über die Zwischenablage kopieren Sie die Zeichen und fügen sie dann in die Visual Studio .NET-Entwicklungsumgebung ein. (Dort fehlt leider ein Dialog zum Einfügen von Sonderzeichen.) 왘 Damit das Unicode-Zeichen im Code auch gespeichert wird, führen Sie in der Entwicklungsumgebung DATEI|SPEICHERN UNTER|SPEICHERN MIT CODIERUNG aus und wählen als Codierung z.B. UNICODE (UTF-8 MIT SIGNATUR). Andernfalls verwendet die Entwicklungsumgebung zum Speichern das ANSI-Format, und das Pfeilzeichen ginge wieder verloren. (Die Entwicklungsumgebung zeigt automatisch eine Warnung an, wenn Sie diesen Schritt vergessen.) 왘 Zu guter Letzt müssen Sie noch die Schriftart des ganzen Formulars oder zumindest des ListView-Steuerelements von Microsoft Sans Serif auf Arial umstellen. Zwar sind grundsätzlich beide Schriften Unicode-kompatibel, Arial enthält aber viel mehr Sonderzeichen. Bei Microsoft Sans Serif werden dagegen viele Sonderzeichen durch schwarze Striche dargestellt. Wenn das Programm unter Windows 98/ME ausgeführt wird, kann es sein, dass die Pfeile nicht korrekt dargestellt werden. Das liegt darin, dass das ListView-Steuerelement von einer Betriebssystembibliothek
Probleme mit Windows 98/ME
35
WINFORM.fm Seite 36 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
gezeichnet wird. Bei Windows 98/ME ist diese Bibliothek nicht Unicode-kompatibel. ' Beispiel explorer\bitmap-viewer ' Klasse Form Private Sub listview1_ColumnClick( _ ByVal sender As System.Object, ByVal e As _ System.Windows.Forms.ColumnClickEventArgs) _ Handles listview1.ColumnClick Dim oldcol As Integer Dim newSrtColumn As lvCol Dim colText As String oldcol = CType(srtColumn, Integer) newSrtColumn = CType(e.Column, lvCol) ' bisherige Spaltenmarkierung entfernen colText = listview1.Columns(oldcol).Text If Strings.Right(colText, 2) = " " Or _ Strings.Right(colText, 2) = " " Then listview1.Columns(oldcol).Text = _ Strings.Left(colText, Len(colText)h- 2) End If ' neue Sortierordnung bestimmen If srtColumn = newSrtColumn Then ' die gleiche Spalte wurde angeklickt If srtOrder = SortOrder.Ascending Then srtOrder = SortOrder.Descending Else srtOrder = SortOrder.Ascending End If Else ' eine andere Spalte wurde angeklickt srtColumn = newSrtColumn srtOrder = SortOrder.Ascending End If ' Spaltentitel markieren If srtOrder = SortOrder.Ascending Then listview1.Columns(e.Column).Text += " " Else listview1.Columns(e.Column).Text += " " End If ' neu sortieren listview1.ListViewItemSorter = _ New CompareColumn(srtColumn, srtOrder) End Sub
36
WINFORM.fm Seite 37 Dienstag, 20. August 2002 3:48 15
Detailansicht einer einzelnen Bitmap (PictureBox)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
1.5 Detailansicht einer einzelnen Bitmap (PictureBox) Einfacher als die Verwaltung des Listenfelds ist es, eine dort ausgewählte Bitmap-Datei im PictureBox-Steuerelement anzuzeigen, das sich rechts unten im Beispielprogramm befindet. Die folgende Click-Ereignisprozedur löscht nach einigen Validitätstests die bisher angezeigte Bitmap (um deren Speicher freizugeben) und lädt die neue Bitmap dann durch New Bitmap(dateiname). Die Bitmap wird der Image-Eigenschaft der PictureBox zugewiesen. Die aus Platzgründen nicht abgedruckte Prozedur CenterBitmap zentriert das PictureBox-Steuerelement innerhalb von panelBitmap, falls die Bitmap kleiner ist als der dort zur Verfügung stehende Platz.
PictureBoxSteuerelement
' Beispiel explorer\bitmap-viewer ' Klasse Form Private Sub listview1_Click(...) _ Handles listview1.Click Dim bmi As bitmapItem If listview1.SelectedItems.Count = 0 Then _ Exit Sub bmi = CType(listview1.SelectedItems(0), _ bitmapItem) If bmi.Text = no_bitmaps_msg Then lblBitmapName.Text = "" Exit Sub End If lblBitmapName.Text = bmi.fullname Try If Not IsNothing(picturebox1.Image) Then picturebox1.Image.Dispose() picturebox1.Image = Nothing End If picturebox1.Image = New Bitmap(bmi.fullname) CenterBitmap() sbpText.Text = bmi.width.ToString + _ " x " + bmi.height.ToString + " Pixel" Catch ex As Exception sbpText.Text = "Fehler: " + ex.Message End Try End Sub
Bitmap-Größe anpassen Je nachdem, welcher der beiden Buttons im Bildbereich zuletzt angeklickt wurde, wird die Bitmap entweder in Originalgröße angezeigt oder so gedehnt bzw. gestreckt, dass sie exakt den zur Verfügung stehenden Platz ausfüllt. Die PictureBox übernimmt diese Aufgaben automatisch – es muss nur die SizeMode-Eigenschaft entsprechend eingestellt
SizeMode-Eigenschaft
37
WINFORM.fm Seite 38 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
werden (StretchImage oder AutoSize). Außerdem wird in den beiden folgenden Click-Ereignisprozeduren die Dock-Eigenschaft und die Position der Bitmap innerhalb des Panel verändert, damit die Bitmap bei der Stretch-Variante den gesamten Raum füllt (Dock=Fill). ' Beispiel explorer\bitmap-viewer ' Klasse Form Private Sub btnStretch_Click(...) _ Handles btnStretch.Click picturebox1.Dock = DockStyle.Fill picturebox1.Location = New Point(0, 0) picturebox1.SizeMode = _ pictureBoxSizeMode.StretchImage picturebox1.Size = panelBitmap.Size End Sub Private Sub btnOriginal_Click(...) _ Handles btnOriginal.Click picturebox1.Dock = DockStyle.None picturebox1.SizeMode = _ PictureBoxSizeMode.AutoSize CenterBitmap() End Sub
Große Bitmaps mit Scrollbalken anzeigen
Wenn die Bitmap größer ist als der zur Verfügung stehende Raum, werden bei der Original-Variante Scrollbalken angezeigt. Das erfolgt automatisch, ohne dass hierfür Code notwendig ist. Das Geheimnis liegt in der Kombination SizeMode=AutoSize für das PictureBox-Steuerelement, wodurch die PictureBox automatisch so groß wie die Bitmap wird, und AutoScroll=True für das Panel-Steuerelement, in dem die PictureBox enthalten ist. Dadurch werden im Panel automatisch Scrollbalken angezeigt, sobald das oder die enthaltenen Steuerelemente größer sind als der zur Verfügung stehende Platz.
1.6 Zustandsinformationen anzeigen (StatusBar) StatusBar-Steuerelement
38
Im einfachsten Fall dient das StatusBar-Steuerelement dazu, am unteren Fensterrand einen Text mit Statusinformationen zum laufenden Programm anzuzeigen. Wenn Sie gleichzeitig mehrere Informationen in unterteilten Bereichen der Statusleiste anzeigen möchten, können Sie über die Panels-Eigenschaft und den dazugehörenden Dialog mehrere Bereiche (StatusBarPanels) definieren. Damit diese Bereiche angezeigt werden, muss ShowPanels auf True gesetzt werden. (Vorsicht, die Defaulteinstellung lautet False. Damit bleiben die definierten Panels unsichtbar!)
WINFORM.fm Seite 39 Dienstag, 20. August 2002 3:48 15
Zustandsinformationen anzeigen (StatusBar)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Für den Bitmap-Viewer wurden zwei Panels definiert:
왘 sbpText zur Anzeige eines Texts mit AutoSize=Spring, BorderStyle=None und Style=Text 왘 sbpProgress
zur Anzeige eines Fortschrittsbalkens mit Border-
Style=Sunken, Style=OwnerDrawn und Width=150
Das bedeutet, dass rechts 150 Pixel für sbpProgress reserviert sind und der gesamte restliche Raum für sbpText zur Verfügung steht.
Owner-drawn-Panel Die Einstellung Style=OwnerDrawn für das zweite Panel bedeutet, dass Sie sich selbst darum kümmern, den Inhalt dieses Bereichs zu zeichnen. (Diese Technik wird ausführlich in Kapitel 5 beschrieben, aber im Fall eines StatusBarPanel ist sie so einfach, dass Sie den folgenden Code auch ohne die Lektüre dieses Kapitels verstehen sollten. Die einzige Voraussetzung besteht darin, dass Sie sich schon einmal ein bisschen mit Grafikprogrammierung beschäftigt haben.)
StatusBar-Panel selbst zeichnen
Style=OwnerDrawn bewirkt, dass die DrawItem-Ereignisprozedur der Statusbar jedesmal aufgerufen wird, wenn Teile des Steuerelements neu zu zeichnen sind. In der Ereignisprozedur müssen Sie testen, welches Panel das Ereignis betrifft. Für Grafikausgaben verwenden Sie das mit dem Parameter e übergebene Graphics-Objekt. Der vorgesehenen Zeichenbereichs geht aus e.Bounds hervor. Sie können sich darauf verlassen, dass der Hintergrund des Steuerelements bereits gezeichnet ist.
DrawItem-Ereignis
Die folgende Prozedur wertet die Klassenvariable progress aus und zeichnet im Panel einen dunkelblauen Balken, dessen Größe progress entspricht. (progress muss einen Wert zwischen 0 und 1 enthalten.) Zu einem Aufruf der Prozedur kommt es automatisch, wenn das Panel vorübergehend verdeckt war, oder wenn Sie statusbar1.Refresh() ausführen. (Werfen Sie nochmals einen Blick in die in Abschnitt 1.4 beschriebene Prozedur LoatMiniBitmaps!)
Refresh-Methode
' Beispiel explorer\bitmap-viewer ' Klasse Form Private Sub statusbar1_DrawItem( _ ByVal sender As Object, ByVal e As _ Windows.Forms.StatusBarDrawItemEventArgs) _ Handles statusbar1.DrawItem Dim gr As Graphics = e.Graphics If e.Panel Is sbpProgress Then gr.FillRectangle( _ Brushes.DarkBlue, _ e.Bounds.X, e.Bounds.Y, _ CInt(e.Bounds.Width * progress), _
39
WINFORM.fm Seite 40 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
1 Explorer-Benutzeroberfläche
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
e.Bounds.Height) End If End Sub
Abbildung 1.6: Die Statusbar, während die Prozedur LoadMiniBitmaps 247 Bitmaps verarbeitet
1.7 Symbolleiste verwalten (ToolBar) ToolBar-Steuerelement
Das ToolBar-Steuerelement ermöglicht es, an einer beliebigen Position im Fenster (üblicherweise am oberen Rand) eine Symbolleiste darzustellen. Die Vorgehensweise zur Erstellung einer eigenen Symbolleiste ist einfach:
왘 Sie fügen in Ihr Formular ein ImageList- und ein ToolBar-Steuerelement ein. 왘 Beim ToolBar-Steuerelement stellen Sie die ImageList-Eigenschaft so ein, dass Sie auf das ImageList-Steuerelement verweist. 왘 In das ImageList-Steuerelement fügen Sie die Bilder der Buttons ein, die Sie in der Symbolleiste anzeigen möchten. (Die Bilder sollten in der Regel 16*16 Pixel groß sein. Einige Beispiele finden Sie im Verzeichnis Programme\Microsoft Visual Studio .NET\Common7\Graphics\ Bitmaps\OffCtlBr\Small\Color). 왘 Im Dialog zur Einstellung der Buttons-Eigenschaft des ToolBar-Steuerelements können Sie schließlich die einzelnen Buttons einfügen, benennen (Name-Feld) und mit Symbolen aus dem ImageList-Steuerelement versehen (ImageIndex-Eigenschaft). Sie können bei jedem Button dessen Typ angeben (Style-Eigenschaft). Zur Auswahl stehen: PushButton – ein normaler Button ToggleButton – ein Umschaltbutton Separator – eine Trennlinie zwischen einer Gruppe von Buttons DropDownButton – ein Button, der zu einem anderen Eingabeelement
führt Angeklickten Button feststellen
40
Wenn ein ToolBarButton mit der Maus angeklickt wird, tritt für das ToolBar-Objekt ein ButtonClick-Ereignis auf. An die Ereignisprozedur wird mit e.Button ein Verweis auf den angeklickten Button übergeben. Um
WINFORM.fm Seite 41 Dienstag, 20. August 2002 3:48 15
Symbolleiste verwalten (ToolBar)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
festzustellen, welcher Button angeklickt wurde, ist eine Reihe von Tests in der Form e.Button Is ToolBarButtonXxxx erforderlich.
Radio-Buttons Die Anwendung der Symbolleiste ist im Regelfall unkompliziert. Das Beispielprogramm Bitmap-Viewer demonstriert aber einen Aspekt, der bei der Konzeption der ToolBar offensichtlich vergessen worden ist: Radio-Buttons. Wenn Sie mit mehreren Buttons eine Option auswählen möchten, dann soll der gedrückte Button gedrückt bleiben, alle anderen Buttons der Gruppe aber gelöst werden. Dazu markieren Sie die Buttons beim Entwurf der Symbolleiste als ToggleButtons und entwerfen die Click-Ereignisprozedur nach dem folgenden Muster. (Die Prozedur schaltet das ListView-Steuerelement zwischen der Detail- und der Miniaturansicht um.) Private Sub toolbar1_ButtonClick( _ ByVal sender As System.Object, ByVal e As _ Windows.Forms.ToolBarButtonClickEventArgs) _ Handles toolbar1.ButtonClick ' Detailansicht If e.Button Is tbbDetail Then 'der angeklickte Button tbbDetail.Pushed = True ' alle anderen Buttons der Gruppe tbbIcons.Pushed = False listview1.View = View.Details End If ' Miniaturansicht If e.Button Is tbbIcons Then tbbIcons.Pushed = True tbbDetail.Pushed = False listview1.View = View.LargeIcon End If End Sub
41
WINFORM.fm Seite 42 Dienstag, 20. August 2002 3:48 15
WINFORM.fm Seite 43 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2
Formularinterna
Bei der Entwicklung von komplexen Windows.Forms-Anwendungen hilft es sehr, wenn Sie die Interna eines Formulars ansatzweise verstehen. Dieser Abschnitt stellt daher kaum konkrete Lösungen vor, sondern vermittelt Hintergrundwissen. Unter anderem werden die folgenden Fragen beantwortet:
왘 In welcher Reihenfolge treten Ereignisse beim Erzeugen bzw. Schließen eines Fensters auf? 왘 Wann endet ein Programm mit mehreren Fenstern? 왘 Was ist eine message loop? 왘 Wie können auch während einer längeren Berechnung Ereignisse verarbeitet werden? 왘 Wozu dient der normalerweise unsichtbare Codeblock Vom Windows Form Designer generierter Code? 왘 Wie wird ein Formular an die DPI-Einstellung des Computers angepasst? 왘 Wie kann ein Formular dynamisch erzeugt werden (also ohne es im Form Designer zu entwerfen)?
2.1 Codeausführung Was ist ein Formular bzw. Fenster? Intern handelt es sich bei jedem selbst erstellten Fenster um ein Objekt der Klasse Form1, Form2 etc. (oder wie auch immer Sie Ihre Formularklasse benannt haben). Diese Klasse ist wiederum direkt von System. Windows.Forms.Form abgeleitet. Der Code, in den Sie beispielsweise eigene Ereignisprozeduren oder die Deklaration eigener Klassenvariablen einfügen, ist der Code, der die Klasse Form1, Form2 etc. beschreibt. Mit anderen Worten, jedes Mal, wenn Sie ein Formular entwerfen, entwerfen Sie eine neue Klasse.
Wo beginnt die Codeausführung? Wenn Sie in der Entwicklungsumgebung bei einer Windows-Anwendung DEBUGGEN|STARTEN ausführen bzw. die kompilierte *.exe-Datei starten, beginnt das Programm zu laufen, und das Fenster erscheint. Aber was passiert dabei wirklich?
43
WINFORM.fm Seite 44 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Application.Run-Methode
Wenn in der Entwicklungsumgebung bei den Projekteigenschaften als Startobjekt ein Formular angegeben ist (bei Windows-Anwendungen ist das per Default der Fall), dann wird für dieses Formular zum Start der folgende Code ausgeführt: Application.Run(New formname())
Durch New wird das Formular mit all seinen Steuerelementen erzeugt. Der dabei ausgeführte Code befindet sich im Codeabschnitt Vom Windows Form Designer generierter Code und wird etwas weiter unten beschrieben. Außerdem treten die beiden Formularereignisse Resize und SizeChanged auf.
Wann endet das Programm? Eine Windows-Anwendung endet automatisch, wenn das an Application.Run übergebene Fenster geschlossen wird. (Bei Programmen mit mehreren Fenstern würden Sie vielleicht erwarten, dass das Programm läuft, bis das letzte Fenster geschlossen ist. Das ist nicht der Fall – entscheidend ist nur das Startfenster. Wie Sie dieses Verhalten ändern können, wird in Kapitel 6 beschrieben.) Das Schließen des Fensters wird entweder vom Benutzer (X-Button) oder durch die Methode Me.Close ausgelöst. In der Folge wird die Closing-Ereignisprozedur ausgeführt, in der das Programmende durch e.Cancel=True verhindert werden kann. Andernfalls wird die ClosedEreignisprozedur und schließlich die Dispose-Prozedur in dem vom Windows Form Designer generierten Code ausgeführt. Dort werden das Fenster und alle seine Steuerelemente durch Dispose aus dem Speicher entfernt.
2.2 Ereignisreihenfolge Fenster erzeugen
Beim Erzeugen eines neuen Fensters treten die folgenden Ereignisse auf: Resize, SizeChanged, Move, Load, Layout, VisibleChanged, Activated und Paint
Das Fenster wird erst zwischen dem Layout- und dem VisibleChangedEreignis sichtbar. Die Prozeduren New und InitializeComponent in dem vom Windows Form Designer generierten Code werden vor allen Ereignissen (also noch vor Resize) ausgeführt. Wenn Sie ein neues Fenster per Code öffnen, sieht die Zuordnung des Ereignisflusses zum Code so aus: ' Button1 in Fenster1 erzeugt ein neues Fenster2 Private Sub Button1_Click(...) Handles _ Button1.Click
44
WINFORM.fm Seite 45 Dienstag, 20. August 2002 3:48 15
Nachrichtenschleife (message loop)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Dim f As New Form2() 'löst Resize und 'SizeChanged aus f.Show() 'löst Move, Load, Layout, 'VisibleChanged und Activated aus [...] 'beliebiger weiterer Code End Sub 'erst nach End Sub tritt das 'Paint-Ereignis auf
Eine wichtige Konsequenz aus der Ereignisabfolge besteht darin, dass Initialisierungsarbeiten, die in der Load-Ereignisprozedur durchgeführt werden, in den Resize-, SizeChanged- und Move-Ereignisprozeduren noch nicht vorausgesetzt werden dürfen! Gegebenenfalls müssen Sie Initialisierungscode, der vor Resize- oder Move ausgeführt werden soll, in die Prozeduren New oder InitializeComponent des vom Windows Form Designer generierten Code einfügen. Beachten Sie, dass es am Beginn von New aber noch gar keine Steuerelemente gibt!
Vorsicht: Die Ereignisse Resize und Move treten schon vor Load auf!
Fenstergröße ändern: Layout, Resize, SizeChanged und Paint
Neue Fenstergröße
Wenn sich die Position oder Größe von Steuerelementen durch Anchoring ändert, erfolgt dies erst nach der Layout-Prozedur. Die aktuelle Fenstergröße kann bereits in der Layout-Prozedur aus Size oder ClientSize ermittelt werden. Fenster in Icon verkleinern: Move, Layout, Resize, SizeChanged und
Fenster verkleinern
Deactivate
Beachten Sie, dass die Positions- und Größeneigenschaften bei der Darstellung des Fensters als Icon (Taskleisten-Button) recht ungewöhnliche Werte enthalten: Left und Top enthalten je -32000, Width und Height entsprechen der Größe des Buttons in der Taskleiste. Fenster wieder in Normalzustand bringen: Activated, Move, Layout, Resize, SizeChanged und Paint
Fenster vergrößern
Fenster maximieren: Move, Layout, Resize, SizeChanged und Paint
Fenster maximieren
Fenster schließen: Closing, Closed, VisibleChanged und Deactivate
Fenster schließen
Nach Deactivate wird die Dispose-Prozedur in dem vom Windows Form Designer generierten Code ausgeführt.
2.3 Nachrichtenschleife (message loop) Wie oben erwähnt wurde, wird zum Anzeigen des ersten Fensters intern Application.Run ausgeführt. Damit wird im aktuellen Thread eine Nachrichtenschleife (message loop) eingerichtet.
message loop
Die durch Application.Run initialisierte Nachrichtenschleife empfängt alle Eingaben bzw. Ereignisse, die für die vom Programm angezeigten Fenster relevant sind (z.B. Tastatureingaben oder Mausklicks), und
45
WINFORM.fm Seite 46 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
trägt sie in einen Zwischenspeicher ein. Die Ereignisse werden der Reihe nach abgearbeitet und führen zum Aufruf der entsprechenden Ereignisprozedur (natürlich nur, wenn es für ein bestimmtes Ereignis auch eine Prozedur zur Reaktion gibt). Blockierung durch Ereignisprozeduren
Der sequentielle Aufruf von Ereignisprozeduren hat eine wichtige Konsequenz: Wenn die Ausführung einer Ereignisprozedur länger dauert, ist das Programm während dieser Zeit blockiert (d.h., es kann nicht auf andere Ereignisse wie z.B. eine versuchte Menüauswahl reagieren). Bei Anwendungen mit mehreren Fenstern wird das gesamte Programm durch die Ereignisprozedur eines Fensters blockiert.
Ereignisse auch während einer längeren Berechnung empfangen DoEvents-Methode
Eine einfache Form der Abhilfe besteht darin, während einer länger andauernden Ereignisprozedur – z.B. während einer Berechnung – regelmäßig Application.DoEvents() auszuführen. Dadurch werden alle mittlerweile in die Nachrichtenschleife eingetragenen Ereignisse verarbeitet, bevor der Code fortgesetzt wird. Die Verwendung von DoEvents führt allerdings leicht zu unübersichtlichem Code. Durch DoEvents kann es auch dazu kommen, dass die gerade laufende Prozedur ein zweites Mal aufgerufen wird. Das sollte im Regelfall vermieden werden (indem z.B. der Button oder das Menükommando, das die Berechnung startet, vorübergehend deaktiviert wird). Falls die Berechnung durch ein anderes Ereignis abgebrochen werden kann, muss nach DoEvents ein Test erfolgen, ob die Abbruchbedingung erfüllt ist. DoEvents sollte nicht zu oft ausgeführt werden, weil es das Programm
verlangsamt. Eine Alternative zur Anwendung von DoEvents ist die Neukonzeption des Programms als Multithreading-Anwendung (siehe Kapitel 8).
2.4 Hintergrundberechnungen mit DoEvents Sleep-Methode
Das folgende Beispielprogramm füllt ein kleines PictureBox-Steuerelement mit weißen und blauen Punkten (die zufällig ausgewählt werden, siehe Abbildung 2.1). Es findet also keine richtige Berechnung statt, es geht nur darum, eine solche zu simulieren. Nach jedem Punkt wird Threading.Thread.Sleep(3) ausgeführt, um so die Programmausführung für drei Millisekunden zu unterbrechen und den Effekt einer langsamen Berechnung zu simulieren. Die Besonderheit des Programms besteht darin, dass die Berechnung jederzeit durch den zweiten Button abgebrochen werden kann (bzw. das Programm durch den X-Button beendet werden kann).
46
WINFORM.fm Seite 47 Dienstag, 20. August 2002 3:48 15
Hintergrundberechnungen mit DoEvents
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Abbildung 2.1: Die Berechnung der Grafik kann dank DoEvents jederzeit abgebrochen werden.
Die Berechnung der Grafik erfolgt in Button1_Click. Um einen rekursiven Aufruf der Prozedur zu vermeiden (das wäre wegen DoEvents möglich), wird der Button durch Enabled=False vorübergehend deaktiviert. Mit der Methode Graphics.FromHwnd wird das Graphics-Objekt des PictureBox-Steuerelements ermittelt. (Beachten Sie, dass FromHwnd nur in der höchsten .NET-Sicherheitsstufe ausgeführt werden darf. Wenn die .NET-Defaultsicherheitseinstellungen gelten, muss das Programm von der lokalen Festplatte gestartet werden! Wenn Sie diese Methode einsetzen, sollten Sie den Code durch Try-Catch absichern, was hier aus Gründen der Übersichtlichkeit nicht erfolgte.)
Graphics-Objekt eines Steuerelements
Die Variable nextdoevent gibt an, zu welchem Zeitpunkt das nächste Mal DoEvents ausgeführt werden soll. Diese Variable stellt sicher, dass DoEvents maximal vier Mal pro Sekunde ausgeführt wird. (Würde DoEvents einfach nach jedem Pixel ausgeführt, würde das Programm spürbar langsamer.) Nach DoEvents wird anhand der Klassenvariable cancelCalculation getestet, ob die Prozedur vorzeitig abgebrochen werden soll. Andernfalls wird der Zeitpunkt für das nächste DoEvents bestimmt (Variable nextdoevent) und die Berechnung fortgesetzt.
Berechnung abbrechen
cancelCalculation wird in Button2_Click und in Form1_Closing auf True gesetzt. (Vergessen Sie nicht Form1_Closing, sonst kann es passieren, dass
das Fenster geschlossen wird, Button1_Click aber noch fortgesetzt wird. Dann ist die Variable gr, die auf das Graphics-Objekt des PictureBox-Steuerelements verweist, nicht mehr gültig, und es kommt zu einem Fehler.) ' Beispiel interna\doevents-test Dim cancelCalculation As Boolean = False Private Sub Button1_Click(...) _ Handles Button1.Click Dim x, y As Integer Dim gr As Graphics = _ Graphics.FromHwnd(picBox1.Handle) Dim nextdoevent As Date = _ Now.AddMilliseconds(250) Button1.Enabled = False cancelCalculation = False gr.Clear(SystemColors.Control)
47
WINFORM.fm Seite 48 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
For x = 0 To picBox1.ClientSize.Width – 1 For y = 0 To picBox1.ClientSize.Height – 1 ' einen Punkt zeichnen If Rnd() > 0.5 Then gr.FillRectangle(Brushes.White, _ x, y, 1, 1) Else gr.FillRectangle(Brushes.Blue, _ x, y, 1, 1) End If Threading.Thread.Sleep(3) '3 ms warten 'DoEvents ausführen If Now > nextdoevent Then Application.DoEvents() If cancelCalculation Then gr.Dispose() Button1.Enabled = True Exit Sub End If nextdoevent = Now.AddMilliseconds(250) End If Next Next gr.Dispose() Button1.Enabled = True End Sub ' Berechnung abbrechen Private Sub Button2_Click(...) _ Handles Button2.Click cancelCalculation = True End Sub Private Sub Form1_Closing(...) _ Handles MyBase.Closing cancelCalculation = True End Sub
2.5 Windows Form Designer Code Form Designer
48
Üblicherweise entwerfen Sie das Layout von Fenstern im Windows Form Designer, d.h., Sie verwenden die Entwicklungsumgebung, um Steuerelemente von der Toolbox in ein Fenster einzufügen und deren Eigenschaften einzustellen. Die Entwicklungsumgebung merkt sich diese Operationen, indem sie Programmcode in einen normalerweise ausgeblendeten Codeabschnitt einfügt (Vom Windows Form Designer generierter Code). Darin wird die Liste aller Steuerelemente samt ihrer Eigenschaften gespeichert.
WINFORM.fm Seite 49 Dienstag, 20. August 2002 3:48 15
Windows Form Designer Code
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Der Designer-Code besteht aus drei Prozeduren:
왘 New: Das ist der Konstruktor der Formularklasse. Die Prozedur wird immer dann aufgerufen, wenn ein neues Objekt dieser Klasse erzeugt wird (d.h. beim Programmstart bzw. wenn Dim f As New FormXyz() ausgeführt wird). New führt wiederum InitializeComponents aus, um die Steuerelemente zu erzeugen. 왘 Dispose: Diese Prozedur kümmert sich darum, dass das Formular aus dem Speicher entfernt wird, wenn es nicht mehr benötigt wird. Die Prozedur wird automatisch ausgeführt, wenn das Fenster geschlossen wird. 왘 InitializeComponents: In dieser Prozedur werden die Steuerelemente erzeugt und in das Formular eingefügt. Zudem werden die Eigenschaften der Steuerelemente und des Formulars eingestellt. Außerdem werden innerhalb des Designer-Codes zahlreiche Variablen deklariert, die auf die Steuerelemente verweisen.
Beispiel Sie können sich den Designer-Code bei jedem Ihrer Programme ansehen, indem Sie den Codeblock auseinanderklappen. Als Hilfestellung bei der Interpretation des Codes sind im Folgenden einige Ausschnitte abgedruckt. Immer gleich sind die beiden Prozeduren New und Dispose. In New wird die Prozedur InitializeComponent aufgerufen, um die Steuerelemente zu initialisieren. Dispose entfernt zuerst alle Steuerelemente und dann das Formular selbst aus dem Speicher. In den folgenden Zeilen sind die Stellen markiert, in denen Sie New bzw. Dispose gegebenenfalls selbst erweitern können. Im Code stoßen Sie häufig auf das Schlüsselwort Me, manchmal auch auf MyBase. Me verweist auf die Instanz der aktuellen Klasse, hier Form1. MyBase verweist dagegen auf das Objekt der Basisklasse, hier Form. Während also Me.Dispose() die Dispose-Prozedur von Form1 aufruft, bewirkt MyBase.Dispose() den Aufruf der Dispose-Methode der Form-Klasse. Me ist fast immer optional, d.h., Me.components und components sind gleichwertig. (components ist eine Klassenvariable, die etwas weiter unten deklariert ist.)
Me und MyBase
' Beispiel interna\designer-code Public Class Form1 'jedes Formular ist von Windows.Forms.Form 'abgeleitet Inherits System.Windows.Forms.Form
49
WINFORM.fm Seite 50 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
#Region "Vom Windows Form Designer generierter Code" 'Konstruktor für Form1-Klasse Public Sub New() MyBase.New() InitializeComponent() 'hier kann eigener Code eingefügt werden ... End Sub 'Form1-Objekt aus dem Speicher entfernen Protected Overloads Overrides Sub Dispose( _ ByVal disposing As Boolean) If disposing Then 'eigener Code ... If Not (components Is Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub
Nach den Prozeduren New und Dispose folgen die Deklarationen der Variablen zur Verwaltung der Steuerelemente. components hat den Zweck, alle unsichtbaren Komponenten des Formulars in eine Gruppe zusammenzufassen, die in der Dispose-Prozedur einfach aus dem Speicher entfernt werden kann. WithEvents
Die Variablen GroupBox1, TextBox1 etc. verweisen auf die Steuerelemente des Formulars. Sie sind mit WithEvents deklariert, um einen einfachen Empfang von Ereignissen zu ermöglichen (siehe auch Abschnitt 4.1, in dem die Hintergründe von Ereignisprozeduren sowohl für Visual Basic .NET als auch für C# beschrieben sind). Wenn Sie in Ihrem Programm in einer Ereignisprozedur mit [Me.]TextBox1.Text den Text eines Steuerelements auslesen, dann ist TextBox1 einfach eine Klassenvariable des Formulars, die auf ein Objekt des Typs TextBox verweist. Umgangssprachlich wird das meist – wie auch in diesem Buch – verkürzt: Da heißt es dann einfach, TextBox1 ist ein Steuerelement.
Modifier-Eigenschaft
Im Eigenschaftsfenster finden Sie bei jedem Steuerelement die ModifierEigenschaft, wobei Sie zwischen den Einstellungen Protected, Private, Friend oder Public wählen können. In Wirklichkeit ist Modifier gar keine richtige Eigenschaft. Vielmehr gibt die Modifier-Eigenschaft an, wie das Steuerelement in dem vom Windows Form Designer generierten Code deklariert werden soll. Im Regelfall spricht nichts gegen die Defaulteinstellung Friend: Sie bewirkt, dass Sie in allen Teilen Ihres Programms auf die Steuerelemente zugreifen können. Durch Private erreichen Sie, dass das Steuerelement nur innerhalb der Formularklasse zugänglich ist. Umgekehrt
50
WINFORM.fm Seite 51 Dienstag, 20. August 2002 3:48 15
Windows Form Designer Code
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
erreichen Sie durch Public, dass das Steuerelement auch von externen Programmen angesprochen werden kann. Das ist dann sinnvoll, wenn Sie das Formular als Teil einer Bibliothek weitergeben möchten. 'Verwaltung der unsichtbaren Steuerelemente '(z.B. Timer) Private components As _ System.ComponentModel.Icontainer 'Verweise auf alle Steuerelemente Friend WithEvents GroupBox1 _ As System.Windows.Forms.GroupBox Friend WithEvents TextBox1 _ As System.Windows.Forms.TextBox Friend WithEvents TextBox2 _ As System.Windows.Forms.TextBox Friend WithEvents Timer1 _ As System.Windows.Forms.Timer ...
Die InitializeComponent-Prozedur dient dazu, die Steuerelementobjekte mit New zu erzeugen, ihre Eigenschaften einzustellen und sie schließlich mit AddRange in das Fenster bzw. in andere Container-Steuerelemente einzufügen. Damit der Code möglichst effizient ausgeführt wird, werden durch SuspendLayout vorübergehend Layout-Ereignisse unterdrückt. Im Code werden Eigenschaften nur dann eingestellt, wenn sie nicht den Defaultwert enthalten. Insofern kann man durch das Lesen der InitializeComponent-Prozedur rasch einen Überblick darüber gewinnen, welche Eigenschaften im Eigenschaftsfenster eingestellt wurden.
Steuerelemente erzeugen und anzeigen
Das DebuggerStepThrough-Attribut bewirkt, dass die Prozedur bei einer zeilenweisen Ausführung des Programmcodes – also bei der Fehlersuche – als Block ausgeführt wird (wie eine einzige Zeile). Wenn Sie innerhalb der Prozedur einen Fehler vermuten, müssen Sie das Attribut entfernen oder einen Haltepunkt setzen. Das Attribut hat keine Auswirkung auf den endgültigen Code, sondern ist nur für den Debugger relevant. Nicht alle Eigenschaften von Steuerelementen können ohne weiteres durch einfache Zuweisungen per Code eingestellt werden. Beispielsweise ist es auf diese Weise nicht möglich, die Image-Eigenschaft eines PictureBox-Steuerelements einzustellen, nachdem Sie hierfür im Eigenschaftsfenster eine Bitmap geladen haben. In solchen Fällen werden die Daten automatisch in einer zum Formular gehörenden Ressourcendatei gespeichert. InitializeComponent enthält dann zusätzlichen Code, um diese Ressourcendatei zu öffnen und die darin enthaltenen Daten auszulesen. Ressourcendateien kommen auch dann zum Einsatz, wenn das Programm lokalisiert ist, d.h., wenn z.B. für Text1.Text je nach Sprache unterschiedliche Texte zur Auswahl stehen.
Ressourcendatei für Binärdaten
51
WINFORM.fm Seite 52 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
<System.Diagnostics.DebuggerStepThrough()> _ Private Sub InitializeComponent() ' Steuerelemente/Komponenten erzeugen Me.components = _ New System.ComponentModel.Container() Me.GroupBox1 = _ New System.Windows.Forms.GroupBox() Me.TextBox1 = _ New System.Windows.Forms.TextBox() Me.TextBox2 = _ New System.Windows.Forms.TextBox() Me.Timer1 = New System.Windows.Forms.Timer( _ Me.components) ... 'Eigenschaften effizient einstellen Me.GroupBox1.SuspendLayout() Me.SuspendLayout() 'GroupBox1-Eigenschaften Me.GroupBox1.Anchor = _ Windows.Forms.AnchorStyles.Top Or _ Windows.Forms.AnchorStyles.Bottom Or _ Windows.Forms.AnchorStyles.Left 'Steuerelemente in die GroupBox einfügen Me.GroupBox1.Controls.AddRange( _ New System.Windows.Forms.Control() _ {Me.TextBox1, ...}) Me.GroupBox1.Location = _ New System.Drawing.Point(8, 144) Me.GroupBox1.Name = "GroupBox1" Me.GroupBox1 ... 'TextBox1-Eigenschaften Me.TextBox1.Anchor = ... Me.TextBox1.Location = _ New System.Drawing.Point(16, 184) Me.TextBox1 ... 'Form1-Eigenschaften Me.AutoScaleBaseSize = _ New System.Drawing.Size(6, 15) Me.ClientSize = _ New System.Drawing.Size(544, 463) 'Steuerelemente in das Formular einfügen Me.Controls.AddRange( _ New System.Windows.Forms.Control() _ {Me.TextBox2, Me.GroupBox1, ...}) Me.Name = "Form1"
52
WINFORM.fm Seite 53 Dienstag, 20. August 2002 3:48 15
Automatische DPI-Anpassung
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Me.Text = "Fenstertitel" Me.GroupBox1.ResumeLayout(False) Me.ResumeLayout(False) End Sub #End Region [... diverse Ereignisprozeduren] End Class
Designer-Code ändern Die Kommentare in dem vom Designer generierten Code empfehlen, den Code nicht zu verändern. Generell ist es sicher eine gute Idee, diesen Ratschlag zu befolgen. Wenn Sie dennoch Änderungen durchführen möchten, sollten Sie wissen, was Sie tun! Der beste Ort für eigene Änderungen sind die New- und Dispose-Prozeduren, in denen Sie Initialisierungs- bzw. Aufräumarbeiten durchführen können. (Nach Möglichkeit sollten Sie das in den Load- bzw. ClosedEreignisprozeduren tun, es gibt aber Situationen, wo dies dort nicht möglich ist.) Wesentlich problematischer sind Änderungen in der InitializeComponent-Prozedur. Änderungen, die Sie im Code durchführen, werden beim Anzeigen des Formulars ausgeführt und so sichtbar gemacht. Umgekehrt werden Änderungen im Designmodus bei der Rückkehr in die Codeansicht durchgeführt. Soweit ich das abschätzen kann, wird dabei der gesamte InitializeComponent-Code neu erstellt. Das bedeutet, dass im Code durchgeführte Änderungen, die der Designer nicht richtig interpretieren kann, bei einem Wechsel in den Designmodus und zurück in den Code aus dem Code entfernt werden.
InitializeComponent nicht ändern!
Eine mögliche Anwendung von Codeänderungen in der InitializeComponent-Prozedur besteht darin, dass Sie durch Suchen und Ersetzen umfassende Änderungen oft weit effizienter als im Eigenschaftsfenster durchführen können. Wenn Sie in den InitializeComponent-Code einen Fehler einbauen, kann das Formular im Designmodus möglicherweise nicht mehr angezeigt werden. Stattdessen erscheint eine Fehlermeldung. In solchen Fällen sollten Sie versuchen, den Fehler in der Codeansicht zu beheben.
Fehler in InitializeComponent
2.6 Automatische DPI-Anpassung Grundsätzlich können Sie unter Windows angeben, wie viele Punkte pro Zoll (Dots per Inch, DPI) Ihr Monitor darstellen kann. Der Zweck dieser Einstellung besteht darin, die Größe der Schriften so an die Auflösung anzupassen, dass Text immer gleich groß ist (und etwa auch dann noch lesbar ist, wenn Sie vor einem 17-Zoll-Bildschirm sitzen, der mit einer Auflösung von 1600*1200 Punkten betrieben wird). Besonders
Die Font- und Steuerelementgröße ist von der DPI-Einstellung abhängig
53
WINFORM.fm Seite 54 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
praktisch ist eine geänderte DPI-Einstellung bei Notebooks, bei denen der Bildschirm häufig klein, die Auflösung aber hoch ist. Aber auch Anwender mit Augenproblemen schätzen es sehr, wenn die Bedienungselemente von Programmen allein durch die Veränderung der DPI-Einstellung besser lesbar werden. Um die DPI-Einstellung zu verändern, klicken Sie den Windows-Desktop-Hintergrund mit der rechten Maustaste an, wählen EIGENSCHAFTEN|EINSTELLUNGEN|ERWEITERT und suchen sich den gewünschten Schriftgrad aus. KLEINE SCHRIFTEN entspricht der Defaulteinstellung von 96 DPI. GROSSE SCHRIFTEN entspricht 120 DPI. Außerdem können Sie mit der Einstellung ANDERE einen beliebigen DPI-Faktor einstellen. Die DPI-Einstellung wird nach einem Windows-Neustart von allen Programmen automatisch übernommen. (Bei manchen Programmen klappt es auch ohne Windows-Neustart, bei der Visual Basic .NET-Entwicklungsumgebung aber nicht!) Menütexte sollten nun in einer entsprechend kleineren oder größeren Schrift dargestellt werden, die Größe der Symbole in Symbolleisten sollten angepasst werden, ebenso Text in Dialogboxen etc. In der Praxis klappt das oft nur mit Einschränkungen. (Beispielsweise sind die wenigsten Programme in der Lage, Symbolleisten an die DPI-Einstellung anzupassen.)
DPI-Einstellung bei Windows.Forms AutoScale-Eigenschaft
Auch das Aussehen von Fenstern und Dialogen, die Sie mit Visual Basic .NET erzeugen, ändert sich je nach DPI-Einstellung. Mit zunehmendem DPI-Wert werden die einzelnen Steuerelemente größer und rutschen nach links bzw. nach unten. Diese automatische Größenanpassung erfolgt nur, wenn die Eigenschaft AutoScale des Formulars True enthält. (Das ist die Defaulteinstellung.) Die automatische Größenanpassung erfolgt übrigens auch in der Entwicklungsumgebung. Wenn Sie beispielsweise ein Formular bei 96 DPI entwerfen und später (oder auf einem anderen Rechner) bei 120 DPI wieder in die Entwicklungsumgebung laden, dann verändern sich auch dort Größe und Position der Steuerelemente. In Abbildung 2.2 sehen Sie links einen kleinen Dialog bei einer Bildschirmauflösung von 96 DPI. Rechts sehen Sie, wie derselbe Dialog aussieht, wenn das Programm bei 120 DPI ausgeführt wurde. (Der Dialog wurde in der Entwicklungsumgebung bei 96 DPI erstellt.) Die Position und Größe von Steuerelementen innerhalb eines Formulars wird in Pixel angegeben. Vor dem Anzeigen des Formulars werden alle derartigen Angaben an die jeweilige DPI-Einstellung angepasst. Das bedeutet, dass die Eigenschaften Size und Location der diversen Steuerelemente je nach DPI-Einstellung unterschiedliche Werte liefern!
54
WINFORM.fm Seite 55 Dienstag, 20. August 2002 3:48 15
Automatische DPI-Anpassung
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Abbildung 2.2: Links ein Dialog bei 96 DPI, rechts bei 120 DPI
Beispielsweise befindet sich das linke obere Eck von Button 1 bei 96 DPI an der Position (16, 72), bei 120 DPI aber an der Position (20, 89). Diese Position wird auch im Eigenschaftsfenster angezeigt, wenn Sie das Programm in der Entwicklungsumgebung bei 120 DPI bearbeiten. Im Codeblock Vom Windows Form Designer generierter Code werden Sie dagegen weiterhin die ursprüngliche Position vorfinden. Wie aus Abbildung 2.2 hervorgeht, ändern sich nicht nur die Koordinaten der Steuerelemente, sondern auch die Schriftgrößen. (Das ist ja gerade der Sinn der DPI-Anpassung.) Wenn Sie sich aber die Einstellung der Schriftgröße im Eigenschaftsfenster der Entwicklungsumgebung ansehen, werden Sie feststellen, dass sie dort unverändert geblieben ist. Der Grund besteht darin, dass die Schriftgröße nicht in Pixel, sondern in Punkt (point) angegeben wird. Ein Punkt entspricht 1/72 Inch. Wie groß die Schrift in Pixel gemessen ist, hängt aber von der DPI-Einstellung ab. Deswegen beansprucht eine Schrift mit der Größe 8 pt je nach DPI-Einstellung unterschiedlich viel Platz (gemessen in Pixel).
Interna Als Vergleichsmaßstab für die automatische Größenanpassung dient die AutoScaleBaseSize-Eigenschaft des Formulars. Diese Eigenschaft wird beim Erzeugen eines neuen Formulars in der Entwicklungsumgebung initialisiert und kann danach nicht mehr verändert werden (zumindest nicht im Eigenschaftsfenster, in dem die Eigenschaft gar nicht angezeigt wird).
AutoScaleBaseSizeEigenschaft
AutoScaleBaseSize enthält nicht einfach den DPI-Wert, sondern offensichtlich die typische Größe eines Buchstabens (gemessen in Bildschirmpixel) unter Anwendung der Defaultschriftart des Formulars und der aktuellen DPI-Einstellung. Diese Größe kann auch mit der Methode GetScaleBaseSize ermittelt werden. (Leider sind sowohl AutoScaleBaseSize also auch GetScaleBaseSize äußerst dürftig dokumentiert.)
GetScaleBaseSizeMethode
Wenn Sie einen Blick in den Codeblock Vom Windows Form Designer generierter Code werfen, werden Sie entdecken, dass AutoScaleBaseSize dort eingestellt wird, und zwar beispielsweise mit Size(5, 13) bei einem DPI-Wert von 96 bzw. mit Size(6, 15) bei einem DPI-Wert von 120. (Diese Angaben gelten für die Defaultschrift Sans Serif 8pt.)
55
WINFORM.fm Seite 56 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
#Region " Vom Windows Form Designer generierter Code" Private Sub InitializeComponent() Me.AutoScaleBaseSize = _ New System.Drawing.Size(5, 13) ...
Offensichtlich werden erst beim Anzeigen des Formulars (nach der Ausführung von InitalizeComponent) die im Code gespeicherten Pixelkoordinaten der Steuerelemente in die tatsächlichen Koordinaten umgerechnet. Dabei wird offensichtlich getestet, ob der Platzbedarf eines Zeichens in der Formularschriftart von AutoScaleBaseSize abweicht. In diesem Fall werden sämtliche Positions- und Größenangaben korrigiert.
Probleme durch die automatische DPI-Anpassung Problematisch kann die automatische Größenanpassung dann werden, wenn im Fenster auch Objekte angezeigt werden, deren Größe unveränderlich ist (Bitmaps, Icons etc.), oder wenn Sie mit Graphics-Methoden direkt in das Fenster zeichnen und sich darauf verlassen, dass sich die Steuerelemente an dem Ort befinden, an dem sie sich während des Programmentwurfs befanden. Generell ist es also eine gute Idee, ein Programm mit unterschiedlichen DPI-Einstellungen zu testen. (Ich weiß aus eigener Erfahrung, dass das lästig ist, weil der Rechner neu gestartet werden muss. Aber das ist nicht zu ändern.) Wenn es Probleme gibt, bieten sich unterschiedliche Lösungsansätze an:
왘 Sie setzen die AutoScale-Eigenschaft des Formulars auf False. Der offensichtliche Nachteil besteht darin, dass gerade Notebook-Besitzer darüber klagen werden, dass die Bedienungselemente ihres Programms unleserlich klein sind. 왘 Sie berücksichtigen bei Bildschirmausgaben (Graphics-Methoden) die tatsächliche Position und Größe von Steuerelementen. 왘 Sie stellen Bitmaps in PictureBox-Steuerelementen mit SizeMode = StretchImage dar. Das bewirkt, dass die Bitmaps je nach DPI-Einstellung entsprechend verkleinert bzw. vergrößert werden. (Leider ist das auch mit einem gewissen Qualitätsverlust verbunden.)
DPI-Einstellung ermitteln DpiX- und DpiYEigenschaft
56
Die aktuelle DPI-Einstellung können Sie aus den Eigenschaften DpiX und DpiY eines Graphics-Objekt entnehmen, auf das Sie beispielsweise in der Paint-Ereignisprozedur zu jedem Steuerelement zugreifen können:
WINFORM.fm Seite 57 Dienstag, 20. August 2002 3:48 15
Formular dynamisch erzeugen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Private Sub Form1_Paint( _ ByVal sender As Object, _ ByVal e As Windows.Forms.PaintEventArgs) _ Handles MyBase.Paint Dim gr As Graphics = e.Graphics MsgBox("DPI-X: " & gr.DpiX & vbCrLf & _ "DPI-Y: " & gr.DpiY) End Sub
Ein entsprechendes Graphics-Objekt erhalten Sie auch außerhalb einer Paint-Ereignisprozedur, wenn Sie die Handle-Eigenschaft eines Steuerelements oder Formulars nutzen. Beachten Sie aber, dass die Methode FromHwnd aus Sicherheitsgründen per Default nur funktioniert, wenn das Programm von einer lokalen Festplatte (nicht von einem Netzwerklaufwerk) ausgeführt wird! Beachten Sie auch, dass Sie das Graphics-Objekt nach der Ermittlung der DPI-Werte wieder aus dem Speicher entfernen sollten (Dispose). Dim gr As Graphics = _ Graphics.FromHwnd(Me.Handle) ... DPI-Werte ermitteln gr.Dispose()
2.7 Formular dynamisch erzeugen Üblicherweise entwerfen Sie ein Formular bzw. Fenster in der Entwicklungsumgebung und verwenden die so erzeugte Klasse, um das Fenster dann zu erzeugen (frm = New FormName()) und anzuzeigen (frm.Show()). Das ist zweifellos der einfachste Weg, grundsätzlich ist es aber auch möglich, ein Formular vollständig per Code zu erzeugen. Der dafür erforderliche Code sieht so ähnlich aus wie die vom Designer erzeugte InitializeComponent-Prozedur. Die einzige Besonderheit besteht darin, dass das Formular bzw. dessen Steuerelemente mit AddHandler mit Ereignisprozeduren verbunden werden müssen.
Fenster per Code erzeugen
Das folgende Programm gibt dafür ein denkbar einfaches Beispiel. Ausgangspunkt ist ein gewöhnliches Fenster (Form1). Wenn Sie dessen einzigen Button anklicken, wird ein neues Fenster mit zufälliger Hintergrundfarbe erzeugt. Das neue Fenster ist ebenfalls mit einem Button ausgestattet, mit dem das Fenster geschlossen werden kann. Button1_Click erzeugt ein neues Fenster (auf der Basis der Form-Klasse) und fügt dort einen Button ein. Dabei werden die elementarsten Eigenschaften des Fensters eingestellt. Mit AddHandler wird das Click-Ereignis des neuen Buttons mit der Prozedur mybtn_Click verbunden. Diese Prozedur ermittelt über die Parent-Eigenschaft von sender das Formularobjekt und schließt es mit Close.
57
WINFORM.fm Seite 58 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
2 Formularinterna
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Abbildung 2.3: Die vier Fenster rechts wurden dynamisch erzeugt. ' Beispiel interna\dynamic-window Private Sub Button1_Click(...) _ Handles Button1.Click Dim frm As New Form() Dim btn As New Button() Dim rand As New Random() frm.Text = "Dynamisch erzeugtes Fenster" frm.Size = New Size(300, 100) frm.BackColor = _ Color.FromArgb(rand.Next(255), _ rand.Next(255), rand.Next(255)) btn.Text = "Fenster schließen" btn.Bounds = New Rectangle(10, 10, 200, 30) btn.BackColor = SystemColors.Control AddHandler btn.Click, AddressOf mybtn_click frm.Controls.AddRange( _ New System.Windows.Forms.Control() {btn}) frm.Show() End Sub Private Sub mybtn_click(_ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim btn As Button = CType(sender, Button) CType(btn.Parent, Form).Close() End Sub
58
WINFORM.fm Seite 59 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
3
Steuerelemente dynamisch verwalten
Normalerweise legen Sie bereits beim Formularentwurf fest, wie viele Steuerelemente es gibt, wo sie sich befinden etc. Wenn Sie mehr Flexibilität wünschen, können Sie Steuerelemente aber auch erst bei Bedarf in Ihr Fenster einfügen. Dabei müssen Sie eigentlich nur darauf achten, dass Sie die Ereignisprozeduren richtig einrichten. Dieses Kapitel geht auf die folgenden Fragen ein:
왘 Welche Möglichkeiten gibt es, Ereignisse mit Ereignisprozeduren zu verknüpfen? 왘 Wie können in ein Fenster neue Steuerelemente eingefügt werden? 왘 Wie kann auf die Steuerelemente in einer Schleife zugegriffen werden, um auch die Auswertung von großen Steuerelementgruppen effizient durchzuführen?
3.1 Ereignisprozeduren einrichten Ein Steuerelement, das im Form Designer in das Formular eingefügt wurden, ist intern mit WithEvents deklariert (siehe Abschnitt 2.5). Um ein Ereignis dieses Steuerelements mit einer Ereignisprozedur zu verbinden, muss an die Deklaration der Ereignisprozedur lediglich Handles steuerelementname.ereignisname hinzugefügt werden. (Beachten Sie, dass der Name der Prozedur, der normalerweise ebenfalls aus dem Steuerelement- und dem Ereignisnamen zusammengesetzt ist, für den Empfang der Ereignisse keine Rolle spielt. Das ist ein wichtiger Unterschied zu VB6!)
WithEvents und Handles
Private Sub Button1_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button1.Click ... End Sub
Es gibt drei Methoden, um das Gerüst einer Ereignisprozedur in den Code einzufügen:
Ereignisprozedur in den Code einfügen
왘 Am bequemsten und effizientesten ist ein Doppelklick auf das jeweilige Steuerelement im Designmodus. Die Entwicklungsumgebung zeigt dann automatisch den Programmcode zum Formular an und fügt dort das Gerüst der Prozedur zum Defaultereignis ein.
59
WINFORM.fm Seite 60 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
3 Steuerelemente dynamisch verwalten
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
왘 Bei allen anderen Ereignissen (also nicht dem Defaultereignis) wechseln Sie in das Codefenster. Dort wählen Sie zuerst im linken Listenfeld das gewünschte Steuerelement, dann im rechten Listenfeld den Ereignisnamen aus. Damit fügt die Entwicklungsumgebung das Gerüst der gewünschten Prozedur ein. Um eine Ereignisprozedur für das Fenster einzufügen, wählen Sie links den Listeneintrag BASISKLASSENEREIGNISSE aus. 왘 Natürlich können Sie den Code auch einfach über die Tastatur eingeben. Das ist aber mühsam, weil Sie sowohl die Parameterliste als auch den Ereignistyp (Handles XyEvent) exakt angeben müssen. Welche Parametertypen die Prozedur erwartet, können Sie dem Objektbrowser entnehmen. Vorsicht beim Einfügen und Löschen von Steuerelementen!
Wenn Sie ein Steuerelement aus einem Formular entfernen, bleiben die zum Steuerelement gehörenden Ereignisprozeduren erhalten. Allerdings wird der Nachsatz Handles steuerelementname.ereignisname entfernt, weil dieser Nachsatz syntaktisch falsch ist, solange es das Steuerelement gar nicht gibt. Wenn Sie nun das Steuerelement wieder einfügen (mit dem ursprünglichen Namen), wird der Handles-Nachsatz nicht mehr wiederhergestellt! Das bedeutet, dass die noch vorhandenen Ereignisprozeduren nicht mehr mit dem Steuerelement verbunden sind und daher wirkungslos bleiben. Abhilfe schaffen Sie, indem Sie den Handles-Nachsatz manuell wieder hinzufügen.
Eine Ereignisprozedur für mehrere Ereignisse Syntaktisch ist es auch erlaubt, eine Ereignisprozedur für mehrere Ereignisse oder für mehrere Steuerelemente (oder beides) zu verwenden. Dazu fügen Sie einfach die Namen der Ereignisse an das HandlesSchlüsselwort an. Diese Vorgehensweise ist natürlich nur sinnvoll, wenn mehrere Ereignisse auf die gleiche Art und Weise verarbeitet werden sollen. (Das Steuerelement, das das Ereignis ausgelöst hat, kann anhand des sender-Parameters identifiziert werden.) Private Sub ListBoxes_DragEnter( _ ByVal sender As Object, _ ByVal e As Windows.Forms.DragEventArgs) _ Handles ListBox1.DragEnter, ListBox2.DragEnter
Ereignisprozeduren dynamisch zuweisen AddHandler
60
Mit AddHandler können Sie ein Ereignis auch dynamisch mit einer Ereignisprozedur verbinden. Die einzige Voraussetzung besteht darin, dass die mit AddressOf angegebene Prozedur die zum Ereignis passende Parameterliste aufweist. Die dynamische Zuweisung von Ereignisprozeduren eignet sich insbesondere für Steuerelemente, die ebenfalls dynamisch in das Formular eingefügt werden.
WINFORM.fm Seite 61 Dienstag, 20. August 2002 3:48 15
Steuerelemente dynamisch einfügen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
AddHandler Button1.Click, _ AddressOf ereignisprozedur ... Private Sub ereignisprozedur( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) ... End Sub
Ereignisse in C# C# geht mit Ereignissen anders um als Visual Basic .NET. Die beiden folgenden Punkte nennen die wichtigsten Unterschiede:
왘 Die Defaultereignisprozedur kann auch bei der C#-Entwicklungsumgebung einfach durch einen Doppelklick in den Code eingefügt werden. Schablonen für alle anderen Ereignisprozeduren werden dagegen über das Eigenschaftsfenster eingefügt: Dazu klicken Sie im Eigenschaftsfenster den gelben Pfeil an. Das Fenster zeigt nun alle zur Auswahl stehenden Ereignisse an. Per Doppelklick können Sie nun die gewünschte Prozedur einfügen.
Ereignisprozedur einfügen
왘 Die Sprache C# kennt keine äquivalenten Konstrukte zu WithEvents und Handles. Stattdessen werden Ereignisse und die zugeordneten Prozeduren immer durch AddHandler miteinander verbunden. Statt AddHandler bietet C# Ihnen die bequeme Kurzschreibweise steuerelement. ereignisname += new System.EventHandler(...).
WithEvents und Handles ist in C# nicht verfügbar.
Der Code, um einen Button mit einer Ereignisprozedur zu verbinden, sieht folgendermaßen aus: this.button1.Click += new System.EventHandler(this.button1_Click);
Die dazugehörende Prozedur (in C# müsste man eigentlich sagen: Funktion) sieht so aus: private void button1_Click(object sender, System.EventArgs e) { ... }
3.2 Steuerelemente dynamisch einfügen In manchen Anwendungen kann es sinnvoll sein, Steuerelemente erst bei Bedarf zu aktivieren (Enable=True/False) bzw. sichtbar zu machen (Visible=True/False). Noch mehr Flexibilität beim Formularentwurf erzielen Sie, wenn Sie Steuerelemente dynamisch – d.h. im laufenden
61
WINFORM.fm Seite 62 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
3 Steuerelemente dynamisch verwalten
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Programm – einfügen. Grundsätzlich ist das recht einfach zu bewerkstelligen:
왘 Zuerst erzeugen Sie mit New das neue Steuerelement (z.B. btn = New Button). 왘 Anschließend stellen Sie dessen Eigenschaften ein (z.B. btn.Text = "abc"). Besonders wichtig sind natürlich Größe und Position. (Leider steht für Steuerelemente keine Clone-Methode zur Verfügung, um eine exakte Kopie eines Steuerelements zu erstellen. Das würde viel Arbeit bei der Einstellung diverser Eigenschaften ersparen.) 왘 Schließlich fügen Sie das Steuerelement mit Add in die Controls-Aufzählung des Formulars oder eines Container-Steuerelements ein (z.B. Form1.Controls.Add(btn)). Ereignisse von dynamisch erzeugten Steuerelementen
Das einzige Problem, das jetzt noch bleibt, ist die Verwaltung der Ereignisse. Da die Steuerelemente erst bei Bedarf erzeugt werden, muss auch die Zuordnung zwischen Ereignissen und der aufzurufenden Prozedur dynamisch erfolgen. Visual Basic .NET sieht dazu das Schlüsselwort AddHandler vor. An dieses Kommando übergeben Sie in zwei Parametern den Ereignisnamen und die Adresse der Ereignisprozedur. (Die Adresse ermitteln Sie mit AddressOf.) Um beispielsweise einen dynamisch erzeugten Button mit seiner Click-Ereignisprozedur zu verbinden, führen Sie die folgende Anweisung aus. Dim btn As New Button() ... AddHandler btn.Click, AddressOf ereignisprozedur
Damit dieser Code fehlerfrei kompiliert wird, muss die ereignisprozedur korrekt deklariert sein. (Entscheidend ist insbesondere, dass die Parameter im richtigen Typ angegeben sind. Die erforderlichen Informationen finden Sie am einfachsten im Objektbrowser zur Klasse Ihres Steuerelements.) Für das Click-Ereignis eines Buttons sieht die Deklaration folgendermaßen aus. (Beachten Sie, dass die Deklaration nicht mit Handles ereignisname endet!) Private Sub ereignisprozedur( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs)
Da Sie üblicherweise dieselbe Ereignisprozedur für mehrere gleichartige Steuerelemente verwenden, muss es innerhalb der Prozedur eine Möglichkeit geben, um festzustellen, für welches Steuerelement das Ereignis ausgelöst wurde. Diese Aufgabe übernimmt der sender-Parameter, der einen Verweis auf das zugrunde liegende Steuerelement enthält. Zur Auswertung müssen Sie diese Variable mit CType in den entsprechenden Objekttyp umwandeln (z.B. CType(sender, Button)).
62
WINFORM.fm Seite 63 Dienstag, 20. August 2002 3:48 15
Steuerelemente dynamisch einfügen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Wenn die Ereignisprozedur für unterschiedliche Steuerelementtypen verwendet wird, müssen Sie natürlich eine Fallunterscheidung für den Objekttyp einfügen (If TypeOf sender Is Button Then...).
Beispiel Das in Abbildung 3.1 dargestellte Beispielprogramm zeigt nach dem Start nur zwei Buttons an. Jedes Mal, wenn Sie NEUER BUTTON anklicken, wird ein neuer Button in das Formular eingefügt. Wenn Sie einen der neuen Buttons anklicken, wird dieser Button wieder entfernt. Mit 1000 NEUE BUTTONS können Sie die Grenzen dieses Verfahrens ausloten. Auf meinem Rechner (Athlon 1,4 GHz) dauerte es weniger als eine Sekunde, um die Buttons einzufügen. Damit die Buttons auch alle zugänglich sind, wurde für das Formular AutoScroll=True eingestellt. Damit erreichen Sie, dass das Fenster automatisch mit Schiebebalken ausgestattet wird, wenn es zu klein ist, um alle enthaltenen Steuerelemente anzuzeigen.
AutoScroll-Eigenschaft
Das Programm war anschließend weiterhin problemlos zu bedienen, auch das Scrolling durch das nun ziemlich große Formular funktionierte noch in einer akzeptablen Geschwindigkeit. Mit anderen Worten: Nichts hindert Sie daran, beinahe beliebig große Dialoge dynamisch erzeugen. (Eine mögliche Anwendung könnte z.B. ein Fragebogen sein, bei dem Sie die Fragen aus einer Datenbank lesen und auf dieser Basis das Formular zusammenstellen. Das ist sicher mit weniger Aufwand verbunden, als hundert Kontrollkästchen manuell einzufügen.)
Abbildung 3.1: Buttons dynamisch erzeugen und entfernen
In Button1_Click wird mit New Button ein neuer Button erzeugt. Die folgenden Zeilen dienen dazu, dessen Größe, Position und den enthaltenen Text einzustellen. Controls.Add fügt den neuen Button in die Controls-Aufzählung des Formulars ein und AddHandler gibt an, welche Ereignisprozedur bei einem Click-Ereignis aufgerufen werden soll.
63
WINFORM.fm Seite 64 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
3 Steuerelemente dynamisch verwalten
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Mehr Effizienz durch Suspend- und ResumeLayout
Die Methoden SuspendLayout und ResumeLayout in Button2_Click weisen .NET an, vorübergehend keine Ereignisse auszulösen, die sich durch das Einfügen der Steuerelemente normalerweise ergeben würden. (Das betrifft insbesondere das Layout-Ereignis, das bei Veränderungen im Formularaufbau automatisch ausgelöst wird.) Die Verwendung der beiden Methoden beschleunigt die Prozedur um ein Mehrfaches! ' Beispiel steuerelemente\dynamic-controls ' Button-Zähler zur Positionierung neuer Buttons Dim dynamic_buttons As Integer = 0 ' einen Button einfügen Private Sub Button1_Click(...) _ Handles Button1.Click Dim btn As New Button() dynamic_buttons += 1 ' Größe von Button1 kopieren btn.Size = Button1.Size ' Position berechnen btn.Left = Button1.Left + _ (Button1.Width + 10) * _ (dynamic_buttons Mod 5) btn.Top = Button1.Top + _ (Button1.Height + 10) * _ CInt(Int(dynamic_buttons / 5)) btn.Text = "Button" + _ (dynamic_buttons + 1).ToString Me.Controls.Add(btn) AddHandler btn.Click, AddressOf btn_Click End Sub ' 1000 neue Buttons einfügen Private Sub Button2_Click(...) _ Handles Button2.Click Dim i As Integer Me.SuspendLayout() For i = 1 To 1000 Button1_Click(Nothing, Nothing) Next Me.ResumeLayout() End Sub ' einen der neuen Buttons entfernen Private Sub btn_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim btn As Button = CType(sender, Button) MsgBox(btn.Text + " wird entfernt.") Me.Controls.Remove(btn) End Sub
64
WINFORM.fm Seite 65 Dienstag, 20. August 2002 3:48 15
Schleife über alle Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
3.3 Schleife über alle Steuerelemente Die Eigenschaft Controls von Formularen bzw. von Container-Steuerelementen verweist auf ein Control.ControlCollection-Objekt, das wiederum auf alle Steuerelemente im Formular bzw. Container verweist. Dank dieser Aufzählung können Sie mühelos auf alle Steuerelemente zugreifen.
Controls-Eigenschaft
Menüeinträge können übrigens nicht mit Controls angesprochen werden, weil die MenuItem-Klasse nicht von der Basisklasse Control abgeleitet ist und Menüeinträge daher in einer von Controls unabhängigen Aufzählung (MainMenu1.MenuItems) verwaltet werden.
Beispiel Das folgende Beispielprogramm zeigt dafür eine einfache Anwendung: In einem Programm gibt es eine Menge Kontrollkästchen. Per ButtonKlick können diese alle zurückgesetzt werden (siehe Abbildung 3.2).
Abbildung 3.2: Der Button deaktiviert alle Kontrollkästchen.
Die For-Each-Schleife greift der Reihe nach auf alle Steuerelemente des Formulars zu. Mit TypeOf wird getestet, ob es sich bei dem Steuerelement um ein Kontrollkästchen handelt. Nur wenn das der Fall ist, wird mit CType eine Typumwandlung durchgeführt, damit anschließend die Checked-Eigenschaft auf False gesetzt werden kann. ' Beispiel steuerelemente\controls-loop Private Sub Button1_Click(...) _ Handles Button1.Click Dim c As Control, cb As CheckBox For Each c In Controls If TypeOf c Is CheckBox Then
65
WINFORM.fm Seite 66 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
3 Steuerelemente dynamisch verwalten
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
cb = CType(c, CheckBox) cb.Checked = False End If Next End Sub
Steuerelemente rekursiv durchlaufen Controls verweist immer nur auf die unmittelbar enthaltenen Steuerele-
mente. Steuerelemente können aber ineinander verschachtelt werden. Beispielsweise können die Steuerelemente Panel, GroupBox und TabControl selbst andere Steuerelemente enthalten. Wenn Sie also wirklich alle Steuerelemente eines Formulars durchlaufen möchten, müssen Sie einen rekursiven Ansatz wählen. Private Sub Button1_Click(...) _ Handles Button1.Click ProcessControls(Me.Controls) End Sub Private Sub ProcessControls( _ ByVal ctrls As Control.ControlCollection) Dim c As Control For Each c In ctrls [... Steuerelement c bearbeiten] ' ProcessControls rekursiv aufrufen ProcessControls(c.Controls) Next End Sub
66
WINFORM.fm Seite 67 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4
Owner-drawn-Steuerelemente
Bei so genannten owner-drawn-Steuerelementen kümmert sich nicht das .NET-Framework darum, den Inhalt von Steuerelementen anzuzeigen, sondern Sie sind selbst dafür verantwortlich. Der Lohn für diese Mehrarbeit besteht darin, dass Sie beispielsweise in einem Listenfeld Einträge in beliebigen Schriftarten darstellen, in Ihren Menüs auch Bilder anzeigen und in der Statusbar einen eigenen Fortschrittsbalken zeichnen können. Owner-drawn-Steuerelemente geben Ihnen also zusätzliche Gestaltungsmöglichkeiten. Die folgenden Steuerelemente bieten die Möglichkeit, eigene Ausgabeprozeduren einzubinden:
왘 Buttons (Button-Klasse, CheckBox-Klasse, RadioButton-Klasse) 왘 Grafikfeld (PictureBox-Klasse) 왘 Listenfelder (ListBox-Klasse) 왘 Menüs (MenuItem-Klasse) 왘 Statusbar (StatusBarPanel-Klasse) 왘 Beschriftungsbereich von mehrblättrigen Dialogen (TabControlKlasse) Button, CheckBox, RadioButton und PictureBox werden oft nicht zu den owner-drawn-Steuerelementen gezählt, weil der Zeichenmechanismus dort besonders einfach ist: Sie setzen die Text-Eigenschaft auf eine leere Zeichenkette und statten das Steuerelement mit einer Paint-Prozedur aus, in der Sie den Inhalt des Steuerelements zeichnen.
Eine Grundvoraussetzung für jede selbstständige Gestaltung von Steuerelementen besteht darin, dass Sie elementare Kenntnisse über das .NET-Grafiksystem besitzen (GDI+, Bibliothek System.Drawing). Deswegen beginnt dieses Kapitel mit einer Blitzeinführung in die Grafikprogrammierung. Als Zeichenobjekte dienen dabei nicht wie sonst üblich der Formularhintergrund oder eine PictureBox, sondern Buttons. Die folgenden Abschnitte zeigen dann, wie Owner-drawn-Listenfelder und -Menüs realisiert werden können. Ein Beispiel für die Statusbar gab es bereits in Abschnitt 3.6. Bei mehrblättrigen Dialogen sieht die Vorgehensweise entsprechend aus, so dass auf ein eigenes Beispiel verzichtet wird.
67
WINFORM.fm Seite 68 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4 Owner-drawn-Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
4.1 Buttons grafisch gestalten Paint-Ereignis
Abbildung 4.1 zeigt drei Buttons, die nicht einfach einen Beschriftungstext enthalten, sondern grafische Muster. Der Weg zu derartigen Buttons führt über die Paint-Ereignisprozedur. Diese Prozedur steht für das Formularobjekt und für einige Steuerelemente zur Verfügung (z.B. Button, CheckBox, RadioButton und PictureBox). Das Paint-Ereignis wird immer dann ausgelöst, wenn der Inhalt des Formulars oder Steuerelements neu gezeichnet werden muss – also unmittelbar nach dem Programmstart und wieder dann, wenn das Fenster vorübergehend von einem anderen Fenster verdeckt war.
Abbildung 4.1: Drei grafisch gestaltete Buttons
68
Graphics-Klasse
Jede Grafikausgabe setzt voraus, dass Sie das Graphics-Objekt des Steuerelements besitzen. Ein derartiges Objekt steht Ihnen in der Paint-Prozedur durch den Ereignisparameter e zur Verfügung.
Pen-Klasse
Auf das Graphics-Objekt können Sie nun verschiedene Grafikmethoden anwenden, z.B. DrawEllipse, um Kreise und Ellipsen zu zeichnen. An diese Methode müssen Sie im ersten Parameter ein Pen-Objekt übergeben, das den Zeichenstift beschreibt. Die Aufzählung Pens.farbe stellt für eine Menge vordefinierter Farben fertige Pen-Objekte mit einer Zeichenbreite von einem Pixel zur Verfügung. Wenn Sie einen breiteren Zeichenstift wünschen, strichlierte Linien zeichnen möchten etc., können Sie sich aber auch ein eigenes Pen-Objekt erzeugen.
ClientSize-Eigenschaft
Die restlichen Parameter von DrawEllipse geben den Koordinatenbereich eines Rechtecks an, innerhalb dessen die Ellipse gezeichnet werden soll. Innerhalb des Steuerelements gilt ein lokales Koordinatensystem mit der Einheit Pixel. Im linken oberen Eck befindet sich der Punkt (0,0). Die x-Achse zeigt nach rechts, die y-Achse nach unten. Die Breite und Höhe des Steuerelements können Sie mit ClientSize.Width und .Height ermitteln.
Zeichenqualität einstellen
Damit haben Sie alle Informationen, um im ersten Button einige blaue Kreise zu zeichnen. Durch die Einstellung der SmoothingMode-Eigenschaft erreichen Sie eine besonders hohe Zeichenqualität (Kantenglättung).
WINFORM.fm Seite 69 Dienstag, 20. August 2002 3:48 15
Buttons grafisch gestalten
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' in Button1 blaue Kreise darstellen Private Sub Button1_Paint( _ ByVal sender As Object, _ ByVal e As Windows.Forms.PaintEventArgs) _ Handles Button1.Paint Dim gr As Graphics = e.Graphics Dim radius As Integer Dim w As Integer = sender.ClientSize.Width Dim h As Integer = sender.ClientSize.Height gr.SmoothingMode = _ Drawing.Drawing2D.SmoothingMode.AntiAlias For radius = 10 To 40 Step 3 gr.DrawEllipse(Pens.Blue, _ w / 2.0F - radius, h / 2.0F - radius, _ 2 * radius, 2 * radius) Next End Sub
Im zweiten Button wird eine gefüllte Ellipse dargestellt. Zum Zeichnen von Ellipsen ist die Methode FillEllipse vorgesehen. Der einzig wesentliche Unterschied zu DrawEllipse besteht darin, dass im ersten Parameter ein Brush-Objekt erwartet wird, das das Hintergrundmuster beschreibt. Für einfarbige Muster können Sie ein derartiges Objekt aus der Brushes.farbe-Aufzählung auswählen. Für das folgende Beispiel wurde aber ein LinearGradientBrush-Objekt erzeugt, um einen Farbverlauf zwischen den Farben Weiß und Schwarz darzustellen. Beachten Sie, dass das Brush-Objekt (wie die meisten Grafikobjekte) nach seiner Verwendung durch Dispose wieder freigegeben werden sollte.
Brush-Klasse
' in Button2 eine Ellipse mit einem Farbverlauf ' darstellen Private Sub Button2_Paint(...) _ Handles Button2.Paint Dim gr As Graphics = e.Graphics Dim w As Integer = sender.ClientSize.Width Dim h As Integer = sender.ClientSize.Height Dim br As New Drawing2D.LinearGradientBrush( _ New Point(0, 0), _ New Point(w, h), _ Color.White, Color.Black) gr.SmoothingMode = _ Drawing.Drawing2D.SmoothingMode.AntiAlias gr.FillEllipse(br, 3, 3, w - 7, h - 7) br.Dispose() End Sub
69
WINFORM.fm Seite 70 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4 Owner-drawn-Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Zur Darstellung des dritten Buttons wird ein Font-Objekt für die Schriftfamilie Arial in einer Größe von 25 Punkt und mit dem Attribut fett erzeugt. Um eine möglichst hohe Ausgabequalität zu erreichen, wird diesmal die TextRenderingHint-Eigenschaft eingestellt. Die DrawStringMethode sieht leider keine Möglichkeit vor, die Zeichenkette in einem beliebigen Winkel auszugeben. Deswegen wird mit RotateTransform das gesamte Koordinatensystem des Graphics-Objekts um 10 Grad gedreht. ' in Button3 einen schrägen Text darstellen Private Sub Button3_Paint(...) _ Handles Button3.Paint Dim gr As Graphics = e.Graphics Dim fnt As New Font("Arial", 25, _ FontStyle.Bold) gr.TextRenderingHint = _ Drawing.Text.TextRenderingHint.AntiAlias gr.RotateTransform(-10) gr.DrawString("Ende", fnt, Brushes.Black, _ 0, 20) fnt.Dispose() End Sub
4.2 Owner-drawn-Listenfelder
70
DrawMode-Eigenschaft
Normalerweise kümmert sich das ListBox-Steuerelement selbst um die Darstellung der Listenelemente. Allerdings kann auf diese Weise pro Listenelement nur ein einfacher Text mit einer einheitlichen Schrift angezeigt werden. Wenn Sie eine grafische Gestaltung der Listeneinträge realisieren möchten (z.B. Darstellung von Bitmaps, Verwendung unterschiedlicher Schriftarten, Farben etc.), müssen Sie die Eigenschaft DrawMode auf OwnerDrawFixed (gleichbleibende Höhe der Listenelemente, Eigenschaft ItemHeight) oder OwnerDrawVariable (variable Höhe) setzen. Allerdings müssen Sie jetzt Prozeduren für das Ereignis DrawItem und gegebenenfalls auch für MeasureItem schreiben.
DrawItem-Ereignis
Die DrawItem-Ereignisprozedur dient erwartungsgemäß zur Darstellung eines einzelnen Listeneintrags, wobei ausgewählte Einträge invers (oder auf eine andere Weise hervorgehoben) gezeichnet werden müssen. An die Ereignisprozedur werden mit dem Parameter (Klasse DrawItemEventArgs) alle zum Zeichnen erforderlichen Daten übergeben (siehe Tabelle 4.1). Back- und ForeColor berücksichtigen bereits den Zustand des Listeneintrags, d.h., die Farben sind automatisch invertiert, wenn der Listeneintrag ausgewählt ist. Den Hintergrund des Steuerelements können Sie komfortabel auch mit der Methode e.DrawBackground zeichnen.
WINFORM.fm Seite 71 Dienstag, 20. August 2002 3:48 15
Owner-drawn-Listenfelder
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
e.Graphics
Objekt zur Anwendung der Grafikmethoden
e.Bounds
Zeichenbereich (als Rectangle-Objekt)
e.BackColor
Hintergrundfarbe
e.ForeColor
Vordergrundfarbe
e.Font
Defaultschriftart für das Steuerelement
e.Index
Indexnummer des Listeneintrags
e.State
Zustand des Listeneintrags (z.B. DrawItemState. Selected, wenn der Eintrag ausgewählt ist)
.NET
Essentials
Tabelle 4.1: Eigenschaften der Klasse DrawItemEventArgs
Die MeasureItem-Ereignisprozedur wird nur aufgerufen, wenn DrawMode = OwnerDrawVariable gilt. (Bei DrawMode = OwnerDrawFixed wird die Höhe des Listeneintrags der Eigenschaft ListBox.ItemHeight entnommen.)
MeasureItem-Ereignis
Die MeasureItem-Prozedur wird für jedes Listenelement einmal aufgerufen. In der Prozedur müssen Sie in den Parametern e.ItemHeight und ItemWidth angeben, wie groß der Platzbedarf für das Listenelement ist. Diese Daten werden bis zum Programmende gespeichert, d.h. die ListBox geht davon aus, dass sich der Platzbedarf pro Listenelement nach dem Programmstart nicht mehr ändert. Sie können aber selbstverständlich die Liste erweitern (ListBox.Items.Add etc.) – dann wird die MeasureItem-Ereignisprozedur automatisch für das neue Listenelement aufgerufen. An die Ereignisprozedur wird der Parameter e der Klasse MeasureItem EventArgs übergeben (siehe Tabelle 4.2). e.Graphics
Objekt zur Anwendung der Grafikmethoden
e.Index
Indexnummer des Listeneintrags
e.ItemWidth
die Breite des Listeneintrags (Rückgabewert)
e.ItemHeight
die Höhe des Listeneintrags
Tabelle 4.2: Eigenschaften der Klasse MeasureItemEventArgs
Beispiel Abbildung 4.2 zeigt ein kleines Beispielprogramm mit einem Listenfeld, dessen Einträge die am Rechner verfügbaren Schriften enthält, wobei die Einträge in der jeweils richtigen Schriftart angezeigt werden.
71
WINFORM.fm Seite 72 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4 Owner-drawn-Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Abbildung 4.2: Listenfeld mit allen am Rechner verfügbaren Schriftfamilien
FontFamily.FamiliesAufzählung
Für das ListBox-Steuerelement wurde im Eigenschaftsfenster DrawMode=OwnerDrawVariable und eine Schriftgröße von 12 Punkt eingestellt. Die Initialisierung des Listenfelds erfolgt in Form1_Load. Dabei wird die Aufzählung FontFamily.Families ausgewertet. Private Sub Form1_Load(...) Handles MyBase.Load Dim ff As FontFamily For Each ff In FontFamily.Families ListBox1.Items.Add(ff.Name) ff.Dispose() Next End Sub
Fehlerabsicherung
Jedes Mal, wenn in Form1_Load die Add-Methode für ListBox1 ausgeführt wird, kommt es zum Aufruf der MeasureItem-Prozedur. Dort wird ein Font-Objekt erzeugt, das dem Namen des Listeneintrags entspricht. Anschließend wird mit MeasureString berechnet, wie groß der Platzbedarf zur Darstellung der Zeichenkette ist. (Die Methode liefert ein SizeF-Objekt, dessen Eigenschaften Width und Height angeben, wie viel Platz die Ausgabe einer Zeichenkette bei Verwendung einer bestimmten Schriftart beanspricht.) Die Try-Catch-Konstruktion ist für die Fälle vorgesehen, bei denen eine Schriftart im Defaultstil FontStyle.Regular nicht verfügbar ist. (Auf meinem Rechner gibt es z.B. bei der Schriftart Monotype Corsiva Probleme, die nur kursiv zur Verfügung steht.) Private Sub ListBox1_MeasureItem( _ ByVal sender As Object, ByVal e As _ System.Windows.Forms.MeasureItemEventArgs) _ Handles ListBox1.MeasureItem Dim fnt As Font Dim itemvalue As String = _ ListBox1.Items(e.Index).ToString Dim sizf As SizeF
72
WINFORM.fm Seite 73 Dienstag, 20. August 2002 3:48 15
Owner-drawn-Listenfelder
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Try fnt = New Font(itemvalue, _ ListBox1.Font.SizeInPoints) Catch fnt = New Font("Arial", _ ListBox1.Font.SizeInPoints) End Try sizf = _ e.Graphics.MeasureString(itemvalue, fnt) e.ItemHeight = CInt(sizf.Height) e.ItemWidth = CInt(sizf.Width) fnt.Dispose() End Sub
Die DrawItem-Ereignisprozedur ähnelt der MeasureItem-Prozedur. Der Unterschied besteht darin, dass diesmal tatsächlich Grafikausgaben erfolgen: Mit DrawBackground wird der Hintergrund neu gezeichnet. (Anders als in Paint-Ereignisprozeduren geschieht das nicht automatisch.) Außerdem wird mit DrawString der Text des Listeneintrags in der gewünschte Schrift ausgegeben. Dabei wandelt RectangleF.op_Implicit das Rectangle-Objekt e.Bounds in ein RectangleF-Objekt um (mit Singlestatt mit Integer-Werten).
DrawItem-Ereignis
Private Sub ListBox1_DrawItem( _ ByVal sender As Object, ByVal e _ As System.Windows.Forms.DrawItemEventArgs) _ Handles ListBox1.DrawItem Dim fnt As Font Dim itemvalue As String = _ ListBox1.Items(e.Index).ToString Dim br As Brush br = New SolidBrush(e.ForeColor) Try fnt = New Font(itemvalue, _ e.Font.SizeInPoints) Catch fnt = New Font("Arial", e.Font.SizeInPoints) End Try e.DrawBackground() e.Graphics.DrawString(itemvalue, fnt, br, _ RectangleF.op_Implicit(e.Bounds)) br.Dispose() fnt.Dispose() End Sub
73
WINFORM.fm Seite 74 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4 Owner-drawn-Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
4.3 Owner-drawn-Menüs Menüs werden in .NET-Programmen mit den Klassen MenuItem und MainMenu realisiert (siehe auch Abschnitt 6.1). Obwohl es seit einem halben Jahrzehnt moderne Menügestaltungsformen gibt (wie im OfficePaket oder in der Visual Studio .NET-Entwicklungsumgebung), hat es Microsoft leider nicht der Mühe wert befunden, entsprechende optische Merkmale auch für die Windows.Forms-Menüs vorzusehen. Wenn Sie also keine langweiligen grauen Menüs wollen, sondern mehrfarbige Menüs mit Icons, müssen Sie für jeden Menüeintrag OwnerDrawn=True einstellen und die Menüs selbst zeichnen. Durch OwnerDrawn=True kommt es zum Aufruf von MeasureItem- und DrawItem-Ereignissen. Grundsätzlich sind diese Ereignisse schon aus dem vorigen Abschnitt bekannt. Allerdings müssen Sie bei der Darstellung von Menüs zahlreiche Besonderheiten berücksichtigen: An die DrawItem-Prozedur werden im Parameter e alle zur grafischen Darstellung erforderlichen Daten übergeben: e.ForeColor, e.BackColor, e.Graphics, e.Bounds etc. e.State gibt an, in welchem Zustand sich der Menüeintrag gerade befindet. Die folgende Aufzählung nennt einige Punkte, die Sie bei der Darstellung beachten sollten. &-Zeichen
왘 Wenn im Menütext das Zeichen & enthalten ist, dürfen Sie dieses Zeichen nicht direkt ausgeben, sondern müssen den folgenden Buchstaben unterstreichen. (Den Effekt können Sie einfach durch ein StringFormat-Objekt mit HotkeyPrefix=Show erzielen.) 왘 Vor dem Menütext muss je nach dem Zustand von Checked und Radio Checked ein Auswahlsymbol angezeigt werden.
Tastenkürzel
왘 Neben dem Menütext sollte (möglichst rechtsbündig) das durch ShortCut angegebene Tastenkürzel angezeigt werden. Das ist besonders mühsam, weil ShortCut ein Element einer riesigen Aufzählung ist und die ToString-Methode lediglich englische Abkürzungen statt der korrekten deutschsprachigen Beschriftung liefert (also z.B. "CtrS" statt "Strg+S"). Ich habe in der .NET-Bibliothek leider keine Methode gefunden, um Tastenkürzel in die jeweilige Landessprache zu übersetzen. Ich bin mir ziemlich sicher, dass es eine solche Methode gibt – für Menüs, die .NET selbst zeichnet, funktioniert es schließlich auch. Mehrere Experimente endeten aber mit einem out of memory-Fehler. Das wäre noch nicht so schlimm, aber anschließend konnten bis zu einem Neustart des Rechners sämtliche vom Betriebssystem gezeichneten Kontextmenüs (z.B. im Internet Explorer) nicht mehr verwendet werden! Mit einer gewissen Skepsis darüber, wie ausgereift .NET nun wirklich ist, habe ich die Suche schließlich aufgegeben.
74
WINFORM.fm Seite 75 Dienstag, 20. August 2002 3:48 15
Owner-drawn-Menüs
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
왘 Wenn für den Menüeintrag DefaultMenuItem=True gilt, dann sollte der Menüeintrag fett angezeigt werden. 왘 Wenn für den Menüeintrag Enabled=False gilt, sollte der Menüeintrag mit grauer Schrift angezeigt werden. 왘 Wenn der Menüeintrag nur aus dem Zeichen – besteht, muss er als Trennlinie dargestellt werden.
Trennlinie
왘 Wenn sich die Maus über einem Hauptmenüeintrag befindet (ohne diesen noch anzuklicken, in e.State ist das Attribut DrawItem State.HotLight gesetzt), sollte der Eintrag hervorgehoben werden. Üblicherweise wird dazu ein dünner Rahmen um den Menüeintrag gezeichnet. Den Pfeil, der gegebenenfalls auf Untermenüs verweist, zeichnet das System übrigens trotz OwnerDrawn = True selbst, d.h., darum müssen Sie sich nicht kümmern. Aber wozu das Rad neu erfinden, wenn es bereits fertige Lösungen gibt? Beispielsweise bietet die in Abschnitt 6.3 vorgestellte Magic-Bibliothek neue Menüklassen, die ein ansprechenderes Layout ermöglichen.
Magic-Bibliothek
Beispiel Das folgende Beispiel zeigt eine einfache Umsetzung von owner-drawnMenüs, die ein ähnliches Aussehen wie die Menüs der Visual Studio .NET-Entwicklungsumgebung haben (siehe Abbildung 4.3). Der Code ist keineswegs perfekt, sollte aber für eigene Experimente bzw. als Ausgangsbasis für eine Weiterentwicklung ausreichen. Verbesserungsbedarf besteht bei der Darstellung von Tastaturabkürzungen, bei der Konfigurierbarkeit, bei der Anpassungsfähigkeit an unterschiedliche Menütextgrößen etc. Anders als bei der Magic-Bibliothek basiert das Beispielprogramm nicht auf einer eigenen Klasse. Das ist zwar weniger elegant (vom Standpunkt der objektorientierten Programmierung aus betrachtet), hat aber den Vorteil, dass der Menüentwurf problemlos durch den Menüeditor der Entwicklungsumgebung erfolgt. Einzig die im Menü gewünschten Icons müssen in ein ImageList-Steuerelement eingefügt werden und durch zusätzliche ElseIf-Abfragen innerhalb der DrawString-Ereignisprozedur berücksichtigt werden. Form1_Load ruft die rekursive Funktion AddHandlerForAllMenuItems auf, die alle Menüeinträge mit den Ereignisprozeduren MenuItems_MeasureItem und -_DrawItem verbindet und anschließend OwnerDraw auf True setzt. (Die Prozedur ist in Abschnitt 6.1 abgedruckt.)
75
WINFORM.fm Seite 76 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4 Owner-drawn-Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Abbildung 4.3: Optisch ansprechende Owner-drawn-Menüs
StringFormat-Klasse
MenuItems_MeasureItem berechnet den Platzbedarf für jedes Menüelement. Im Wesentlichen wird mit MeasureString der Platzbedarf (die Breite) für die anzuzeigende Zeichenkette ermittelt. Dabei wird die Schriftart SystemInformation.MenuFont sowie ein spezielles StringFormatObjekt verwendet, das das &-Zeichen als Markierungszeichen für (Alt)-Tastenkürzel interpretiert. (Ein StringFormat-Objekt kann als optionaler Parameter an Draw- und MeasureString übergeben werden. Es steuert diverse Details der Textausgabe.)
Die Höhe des Menüeintrags wird mit SystemInformation.MenuHeight ermittelt. Wenn es sich bei dem Menüeintrag nur um eine Trennlinie handelt, wird sie mit 10 Pixeln festgelegt. Des Weiteren wird die Breite bei Hauptmenüeinträgen um 10 Pixel reduziert (weil der Abstand zwischen den Menüeinträgen sonst unverhältnismäßig hoch ist), bei allen anderen Menüeinträgen dagegen um SystemInformation.MenuButtonSize vergrößert (um Platz für eine Spalte mit kleinen Icons zu machen). ' Beispiel owner-drawn\od-menu Private Sub MenuItems_MeasureItem( _ ByVal sender As Object, ByVal e As _ System.Windows.Forms.MeasureItemEventArgs) Dim mi As MenuItem = CType(sender, MenuItem) Dim fnt As Font = SystemInformation.MenuFont '& als _ darstellen Dim sf As New StringFormat() sf.HotkeyPrefix = _ Drawing.Text.HotkeyPrefix.Show ' Höhe: Trennlinie oder normaler Text? If mi.Text = "-" Then e.ItemHeight = 10 Else e.ItemHeight = SystemInformation.MenuHeight End If ' Breite: mit MeasureString ermitteln e.ItemWidth = CInt(e.Graphics.MeasureString( _ mi.Text+GetShortcutText(mi), fnt, 0, sf). _ Width)
76
WINFORM.fm Seite 77 Dienstag, 20. August 2002 3:48 15
Owner-drawn-Menüs
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
If mi.Parent Is MainMenu1 Then e.ItemWidth -= 10 Else e.ItemWidth += _ SystemInformation.MenuButtonSize.Width End If End Sub
Die ziemlich lange Prozedur MenuItems_DrawItem ist für die grafische Darstellung des Menüeintrags zuständig. Der Code beginnt damit, dass einige RectangleF-Objekte initialisiert werden. rfAll gibt den gesamten Zeichenbereich, rfText und rfIcon den Textbereich bzw. die Iconspalte an (nur bei Menüeinträgen, die sich nicht unmittelbar im Hauptmenü befinden). Private Sub MenuItems_DrawItem( _ ByVal sender As Object, ByVal e As _ System.Windows.Forms.DrawItemEventArgs) Dim mi As MenuItem = CType(sender, MenuItem) Dim fnt As Font = SystemInformation.MenuFont Dim brText, brBack, brIcon As Brush Dim sf As New StringFormat() Dim rfAll, rfText, rfIcon As RectangleF Dim gr As Graphics = e.Graphics ' Ausgaberechteck für Text und Icons rfAll = RectangleF.op_Implicit(e.Bounds) rfText = rfAll rfIcon = rfAll If mi.Parent Is MainMenu1 Then rfText.X += 2 rfText.Width -= 2 Else rfText.X += _ SystemInformation.MenuButtonSize.Width + 4 rfText.Width -= _ SystemInformation.MenuButtonSize.Width + 4 rfIcon.Width = _ SystemInformation.MenuButtonSize.Width End If
Der nächste Schritt besteht darin, den Hintergrund zu zeichnen. Dazu ist eigentlich die Methode e.DrawBackground vorgesehen. Diese Methode hat sich aber als ungeeignet erwiesen, weil sie aus unerfindlichen Gründen die Farbe SystemColors.Windows (üblicherweise Weiß) statt System Colors.Control (üblicherweise Grau) verwendet. Außerdem ist der Hintergrund bei diesem Beispiel ja zweigeteilt (außer bei den Hauptmenüeinträgen). Wegen all dem ist der Code zur Ermittlung der Hintergrundfarben ein wenig unübersichtlich.
Hintergrund zeichnen
77
WINFORM.fm Seite 78 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4 Owner-drawn-Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
' Hintergrund zeichnen If e.BackColor.Equals( _ SystemColors.Window) Then ' normale Darstellung, per Default weiß If mi.Parent Is MainMenu1 Then brBack = New _ SolidBrush(SystemColors.Control) Else brBack = New SolidBrush( _ Color.FromArgb(224, 224, 224)) End If brIcon = New SolidBrush( _ SystemColors.Control) Else ' inverse Darstellung brBack = New SolidBrush(e.BackColor) brIcon = brBack End If e.Graphics.FillRectangle(brBack, rfAll) ' Hintergrund für die Iconspalte zeichnen If Not (mi.Parent Is MainMenu1) Then gr.FillRectangle(brIcon, rfIcon) End If
Aktuellen Eintrag hervorheben
Wenn sich die Maus über einem noch nicht angeklickten Menüeintrag befindet, sollte es ein unauffälliges Feedback geben. Im Beispielprogramm wird der Menüeintrag grau umrandet. Eigentlich sollte dazu der Aufruf von e.DrawFocusRectangle ausreichen, allerdings bleibt diese Methode wirkungslos. DrawRectangle ist zum Glück auch nicht viel schwieriger aufzurufen. ' Rahmen zeichnen If (e.State And DrawItemState.HotLight) = _ DrawItemState.HotLight Then Dim rect As Rectangle rect = e.Bounds rect.Inflate(-1, -1) gr.DrawRectangle(Pens.Gray, rect) End If
Menütext oder Trennlinie ausgeben
78
Die nächsten Zeilen geben entweder den eigentlichen Menütext aus oder zeichnen eine horizontale Trennlinie. Zur Textausgabe muss je nach mi.Enabled-Zustand eine geeignete Farbe ausgewählt werden. Das Tastenkürzel wird mit der Prozedur GetShortcutText ermittelt (siehe etwas weiter unten).
WINFORM.fm Seite 79 Dienstag, 20. August 2002 3:48 15
Owner-drawn-Menüs
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Text-/Linienausgabe If mi.Text = "-" Then gr.DrawLine(Pens.Gray, _ rfText.X + 2, rfText.Y + 4, _ rfText.Right - 2, rfText.Y + 4) Else ' Textfarbe je nach Enabled-Zustand If mi.Enabled Then brText = New SolidBrush(e.ForeColor) Else brText = New SolidBrush(Color.DarkGray) End If sf.HotkeyPrefix = _ Drawing.Text.HotkeyPrefix.Show e.Graphics.DrawString( _ mi.Text + GetShortcutText(mi), _ fnt, brText, rfText, sf) End If
Bei Menüeinträgen, die durch Checked=True ausgewählt sind, wird in der Iconspalte ein aus zwei kurzen Linien bestehendes Auswahlhäkchen oder ein gefüllter Kreis gezeichnet. Wenn Checked=False gilt, wird durch mehrere Is-Vergleiche ermittelt, welcher Menüeintrag gerade bearbeitet wird. Wenn es dazu im ImageList-Steuerelement ein Icon gibt, wird es angezeigt. (Dieser Teil des Codes ist nicht besonders elegant, eine bessere Lösung ist aber nur auf der Basis einer eigenen MenuItemKlasse möglich.)
Auswahlhäkchen zeichnen
' Checkbox / Icon darstellen If mi.Checked Then Dim x0, y0 As Single Dim pn As Pen x0 = rfAll.X + 5 y0 = rfAll.Y + 5 gr.SmoothingMode = _ Drawing2D.SmoothingMode.HighQuality If mi.RadioCheck Then ' Optionskreis zeichnen gr.FillEllipse(brText, x0, y0, 8, 8) Else ' CheckBox-Häkchen zeichnen pn = New Pen(brText, 3) gr.DrawLine(pn, _ x0, y0 + 4, x0 + 4, y0 + 8) gr.DrawLine(pn, _ x0 + 4, y0 + 8, x0 + 12, y0) End If Else
79
WINFORM.fm Seite 80 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
4 Owner-drawn-Steuerelemente
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
' Menüicon zeichnen Dim pt As New Point( _ CInt(rfAll.X + 2), CInt(rfAll.Y + 2)) If mi Is MenuItemFileOpen Then gr.DrawImageUnscaled( _ ImageList1.Images(0), pt) ElseIf mi Is MenuItemFileSave Then gr.DrawImageUnscaled( _ ImageList1.Images(1), pt) ElseIf ... für alle weiteren Icons ... End If End If ' aufräumen If Not IsNothing(brBack) Then brBack.Dispose() If Not IsNothing(brText) Then brText.Dispose() If Not IsNothing(brIcon) Then brIcon.Dispose() If Not IsNothing(sf) Then sf.Dispose() End Sub
Die Funktion GetShortcutText versucht (leider vergeblich), durch die Angabe eines CultureInfo-Objekts in der ToString-Methode eine Übersetzung des Tastenkürzels zu erreichen. Die Funktion liefert aber nur englische Tastenkürzel. Private Function GetShortcutText( _ ByVal mi As MenuItem) As String Dim s As String If mi.Shortcut <> Shortcut.None Then s = " (" + _ mi.Shortcut.ToString("g", _ Globalization.CultureInfo. _ CurrentCulture()) + ")" End If Return s End Function
80
WINFORM.fm Seite 81 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
5
Verwaltung mehrerer Fenster
In der Praxis haben Sie es oft mit Anwendungen zu tun, die aus mehreren Fenstern bestehen. Dieses Kapitel beschreibt einige Techniken im Umgang mit mehreren Fenstern. Insbesondere geht es darum, wie mehrere Fenster per Code geöffnet und geschlossen werden und unter welchen Umständen das gesamte Programm beendet wird. Eine Sonderrolle bei der Verwaltung mehrerer Fenster nehmen MDIund Docking-Anwendungen ein. Dabei werden mehrere Subfenster innerhalb eines Hauptfensters angezeigt. Kapitel 7 beschreibt die Programmierung derartiger Anwendungen.
5.1 Modale Dialoge Der einfachste Fall besteht darin, dass Sie – ausgehend von einem Hauptfenster – weitere Fenster als modale Dialoge anzeigen. Das bedeutet, dass das Hauptfenster erst dann wieder verwendet werden kann, nachdem der Dialog beendet wurde. Zum Aufruf des Dialogs müssen Sie ein neues Objekt der Klasse des Formulars erzeugen. Anschließend verwenden Sie ShowDialog, um den Dialog anzuzeigen. (In den folgenden Beispielen heißen die Formulare Form1, Form2 etc., in realen Anwendungen ist es aber natürlich zielführend, aussagekräftigere Namen zu verwenden.)
ShowDialog-Methode
' Beispiel fenster\multi1 Dim frm As New Form2 frm.ShowDialog()
Wenn der Code des Dialogformulars es vorsieht, liefert ShowDialog den Rückgabewert des Formulars (ein Element der DialogResult-Aufzählung) als Ergebnis. Üblicherweise wird dieser Rückgabewert dazu verwendet, anzugeben, mit welchem Button (z.B. OK, ABBRUCH, JA, NEIN) der Dialog beendet wurde. Dazu kann in den Button-Ereignisprozeduren des Dialogformulars Me.DialogResult=... ausgeführt werden. Wenn die Button-Eigenschaft DialogResult im Eigenschaftsfenster eingestellt wird, können Sie sich diese Codezeile sparen.
Gestaltung/Programmierung modaler Dialoge Bei der Gestaltung bzw. Programmierung von modalen Dialogformularen sollten Sie einige Details beachten:
81
WINFORM.fm Seite 82 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
5 Verwaltung mehrerer Fenster
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
왘 In den Ereignisprozeduren zu den Buttons OK und ABBRUCH müssen Sie Me.Close oder Me.Hide ausführen, um das Fenster zu schließen. (Bei modalen Dialogen haben Close und Hide weitgehend dieselbe Wirkung. Der einzige Unterschied besteht darin, dass bei Hide die Closing- und Closed-Ereignisprozeduren des Dialogs nicht aufgerufen werden.) 왘 Wenn der Anwender die Fenstergröße nicht ändern soll, stellen Sie FormBorderStyle am besten auf FixedSingle. In diesem Fall sollten Sie auch Maximize- und MinimizeBox=True angeben. 왘 Damit der Dialog nicht in der Taskleiste angezeigt wird, stellen Sie ShowInTaskBar auf False.
Dialoge jedes Mal neu erzeugen oder wieder verwenden? Grundsätzlich gibt es zwei mögliche Strategien zur Anzeige von Dialogen. Die eine besteht darin, den Dialog jedes Mal neu zu erzeugen. In diesem Fall sollte der Dialog anschließend mit Dispose wieder aus dem Speicher entfernt werden. Private Sub Button1_Click(...) _ Handles Button1.Click Dim frm As New Form2 Dim result As DialogResult result = frm.ShowDialog() ... Auswertung frm.Dispose() End Sub
Die andere Variante besteht darin, das Formularobjekt einmal zu erzeugen und immer wieder neu anzuzeigen. In diesem Fall muss im Formularcode (also im Form2-Code) Hide verwendet werden, nicht Close! Dim frm As New Form2 Private Sub Button1_Click(...) _ Handles Button1.Click result = frm.ShowDialog() ... Auswertung End Sub
Die zweite Variante hat Vor- und Nachteile. Zuerst die Vorteile: Gerade bei aufwändigen Dialogen steht der Dialog sofort zur Verfügung und muss nicht jedes Mal neu erzeugt werden. (Wenn beispielsweise im Dialog eine umfangreiche Liste dargestellt wird, muss diese nur einmal initialisiert werden.) Außerdem sind alle Steuerelemente noch exakt so eingestellt, wie dies beim vorigen Aufruf des Dialogs der Fall war. Wenn Sie also beim ersten Dialogaufruf in einem Textfeld etwas eingegeben haben, steht diese Eingabe beim nächsten Aufruf noch zur Verfü-
82
WINFORM.fm Seite 83 Dienstag, 20. August 2002 3:48 15
Modale Dialoge
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
gung. (Wenn das Dialogobjekt dagegen jedes Mal neu erzeugt wird, sind die Eingabefelder immer wieder leer bzw. enthalten die bei der Initialisierung vorgesehenen Werte.) Der Nachteil: Das frm-Objekt wird während des Programmstarts des Hauptprogramms erzeugt und verlangsamt somit den Programmstart. Außerdem wird der Speicher für das frm-Objekt bereits beim Programmstart beansprucht und dann nicht mehr freigegeben.
Datenauswertung Normalerweise wollen Sie nicht nur wissen, ob der Dialog mit OK oder ABBRUCH (oder einem anderen Button) beendet wurde, sondern auch, welche Daten in dem Dialog eingegeben, welche Listeneinträge ausgewählt und welche Optionen angeklickt wurden. Die einfachste Art der Auswertung besteht darin, einfach die Steuerelemente auszulesen. frm.ShowDialog() If frm.TextBox1.Text = ...
Wenn Sie innerhalb der Dialogklasse (also im Form2-Code) Klassenvariablen mit Friend oder Public deklarieren, können Sie darauf wie auf die Steuerelemente zugreifen. If frm.myIntegerVar > 10 Then ...
Die eleganteste Methode besteht darin, den Dialog mit zusätzlichen Eigenschaften und Methoden auszustatten, um die eingegebenen Daten zu ermitteln (oder vor dem Aufruf des Dialogs voreinzustellen).
Dialog unter der Maus zentrieren Per Default, d.h. mit der Einstellung StartPosition=WindowsDefaultLocation, erscheinen die Dialogfenster im Regelfall irgendwo am Bildschirm, oft weit entfernt vom Ausgangsfenster und von der Maus. (Nach welchen Gesichtspunkten Windows die Position errechnet, weiß ich nicht.) Eine deutliche Verbesserung bewirkt die Einstellung StartPosition = CenterParent. Damit wird der Dialog über dem Ausgangsfenster zentriert.
StartPositionEigenschaft
Mit StartPosition=Manual können Sie die Startposition selbst bestimmen. Der Code zum Aufruf des Dialogs könnte dann so aussehen: ' Beispiel fenster\multi1 frm.Left = Me.MousePosition.X - frm.Width \ 2 frm.Top = Me.MousePosition.Y - frm.Height \ 2 result = frm.ShowDialog()
83
WINFORM.fm Seite 84 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
5 Verwaltung mehrerer Fenster
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Damit wird der Dialog über der aktuellen Mausposition zentriert. Wenn Sie möchten, können Sie die Berechnung der Startkoordinaten natürlich noch ausfeilen, um sicherzustellen, dass der Dialog nicht teilweise über die Bildschirmgrenzen ragt. (Ein Rectangle-Objekt mit den Bildschirmgrenzen können Sie Screen.Bounds entnehmen.)
5.2 Gleichberechtigte Fenster Show-Methode
Die Methode Show ist der einfachste Weg, ein zweites Fenster zu öffnen, so dass dieses parallel zum ersten verwendet werden kann. Für die hier beschriebenen Konzepte spielt es keine Rolle, ob die neuen Fenster alle von der gleichen Klasse (Form1) stammen oder ob unterschiedliche Fenster auf der Basis unterschiedlicher Formulare (Form1, Form2, Form3 etc.) erzeugt werden. ' Beispiel benutzeroberflaeche\multi2 Private Sub Button1_Click(...) _ Handles Button1.Click Dim frm As New Form1() frm.Show() End Sub
Programmende
Diese Vorgehensweise ist zwar einfach, aber sie hat einige gravierende Nachteile (die Sie mit dem Beispielprogramm fenster/multi2 testen können).
왘 Wenn Sie Fenster 1 (also das Startfenster) schließen, endet das Programm. (Wenn die beiden Fenster wirklich gleichberechtigt wären, sollte das Programm laufen, bis alle Fenster geschlossen sind.) 왘 Obwohl die beiden Fenster parallel verwendet werden können, haben sie doch eine gemeinsame Nachrichtenschleife (message loop). Das hat zur Folge, das eine länger andauernde Operation in einem Fenster alle anderen Fenster blockiert. Wenn Sie also durch einen Button-Klick eine Berechnung starten, die zehn Sekunden dauert, sind während dieser Zeit alle Fenster blockiert. 왘 Es ist nicht ohne weiteres möglich, zwischen den Fenstern Daten auszutauschen. Anders als in VB6 gibt es keine globale Forms-Aufzählung, die auf alle geöffneten Fenster verweist.
Windows.Form-Programme in einem eigenen Modul starten Windows.FormsStartprozess
84
Wie bereits in Abschnitt 3.1 beschrieben, wird das erste Fenster einer Windows-Anwendung per Default durch Application.Run(Newformname()) angezeigt. Statt sich auf diesen Automatismus zu verlassen, können Sie Application.Run() auch selbst durchführen. Das hat den Vorteil, dass Sie den Startprozess genauer steuern können.
WINFORM.fm Seite 85 Dienstag, 20. August 2002 3:48 15
Gleichberechtigte Fenster
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Dazu fügen Sie in Ihr Projekt ein neues Modul ein und programmieren dort die Prozedur Main nach dem unten angegebenen Muster. Zudem geben Sie bei den Projekteigenschaften an, dass nicht Form1, sondern Module1 als Startobjekt gelten soll. Damit beginnt die Programmausführung mit der Prozedur Module1.Main. Module Module1 Sub Main() Application.Run(New Form1()) ' wenn das Fenster geschlossen wird oder ' Application.Exit() ausgeführt wird, wird ' die Programmausführung an diesem Punkt ' fortgesetzt; wenn es hier keinen Code mehr ' gibt, endet das Programm End Sub End Module
Die folgende Aufzählung beschreibt einige weitere Merkmale von Run:
Run-Methode
왘 Run kann im selben Programm normalerweise nur ein einziges Mal aufgerufen werden (weil es pro Thread nur eine Nachrichtenschleife geben darf). 왘 Die Methode Run wird erst abgeschlossen, wenn das an Run übergebene Formular geschlossen wird oder wenn in einer Ereignisprozedur Application.Exit ausgeführt wird. 왘 Es gibt auch die Möglichkeit, Run ohne Parameter aufzurufen und anschließend die Fenster wie üblich durch f = New form1() und f.Show() anzuzeigen. Allerdings kommt es nun zu keinem automatischen Programmende mehr, wenn das bzw. alle Fenster geschlossen werden. Ein Programmende müssen Sie nun durch Application.Exit erzwingen! Beachten Sie, dass die Methode Application.Exit (wie übrigens auch End) nur in der höchsten .NET-Sicherheitsstufe ausgeführt werden darf, d.h. per Default nur dann, wenn das Programm von der lokalen Festplatte gestartet wurde.
.NET-Sicherheit
Mehrere gleichberechtigte Fenster öffnen Das im Folgenden vorgestellte Beispielprogramm multi3 verbessert multi2 in zwei Aspekten:
왘 Die Programmausführung endet automatisch, wenn alle geöffneten Fenster geschlossen werden. (Die Reihenfolge, in der die Fenster geschlossen werden, ist gleichgültig.) 왘 Über die allen Modulen zugängliche Aufzählung myForms kann jedes Formular auf alle anderen Formulare zugreifen.
85
WINFORM.fm Seite 86 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
5 Verwaltung mehrerer Fenster
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Die Programmausführung beginnt in Module1.Main. Dort wird mit ShowWindow das Startfenster angezeigt. Dazu wird die selbst definierte
Methode ShowWindow verwendet, die die Formularvariable in eine ArrayList-Aufzählung einfügt und außerdem frm.Show ausführt. Anschließend wird die Nachrichtenschleife für das Fenster durch Application. Run ohne die Angabe weiterer Parameter gestartet. (Das bedeutet, dass das Programm explizit mit Application.Exit beendet werden muss!)
Abbildung 5.1: Mehrere gleichberechtigte Fenster (Beispiel multi3) ' Beispiel benutzeroberflaeche\multi3 Module Module1 Public myForms As New Collections.ArrayList() Sub Main() ShowWindow(New Form1()) Application.Run() MsgBox("Jetzt endet Main().") End Sub Public Sub ShowWindow(ByVal frm As Form) myForms.Add(frm) frm.Show() End Sub End Module
Im Form1-Code können mit dem Button NEUES FENSTER ANZEIGEN (Prozedur btnShow_Click) beliebig viele neue Fenster geöffnet werden. Daneben sieht das Programm zwei Buttons für ein Programmende vor. Bei der sanften Variante werden in btnClose_Click alle Formulare durchlaufen und durch Close geschlossen. (Damit wird in jedem Fenster die Closing-Prozedur ausgeführt, so dass das Programmende dort noch verhindert werden kann.) Die radikale Variante besteht darin, einfach Application.Exit auszuführen, was ein sofortiges Programmende bewirkt.
86
WINFORM.fm Seite 87 Dienstag, 20. August 2002 3:48 15
Gleichberechtigte Fenster
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Der Button MUSTER ZEICHNEN führt dazu, dass zehn Sekunden lang zufällige Rechtecke in einem PictureBox-Steuerelement gezeichnet werden. Damit können Sie testen, wie das Programm reagiert, wenn eine Ereignisprozedur längere Zeit dauert. Form1_Closed stellt sicher, dass das Programm nach dem Schließen aller Fenster tatsächlich beendet wird. Dazu wird das aktuelle Formular aus der myForms-Aufzählung entfernt. Wenn myForms keine anderen Elemente mehr enthält, wird Application.Exit ausgeführt. Public Class Form1 [vom Windows Form Designer generierter Code] ' neues Fenster öffnen Private Sub btnShow_Click(...) _ Handles btnShow.Click ShowWindow(New Form1()) End Sub ' alle Fenster durch Close schließen Private Sub btnClose_Click(...) _ Handles btnClose.Click Dim i As Integer For i = myForms.Count - 1 To 0 Step –1 CType(myForms(i), Form).Close() Next End Sub ' Programm durch Application.Exit ' gewaltsam beenden Private Sub btnEnd_Click(...) _ Handles btnEnd.Click Application.Exit() End Sub ' zehn Sekunden lang Zufallsmuster zeichnen Private Sub btnWork_Click(...) _ Handles btnWork.Click Dim endtime As Date = Now.AddSeconds(10) While Now < endtime [... Rechtecke zeichnen] End While End Sub ' Rückfrage vor dem Schließen des Fensters Private Sub Form1_Closing(...) _ Handles MyBase.Closing Me.BringToFront() If MsgBox( _ "Soll das Fenster geschlossen werden?", _ MsgBoxStyle.YesNo) = MsgBoxResult.No Then e.Cancel = True
87
WINFORM.fm Seite 88 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
5 Verwaltung mehrerer Fenster
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
End If End Sub ' das Fenster wurde geschlossen, ' eventuell Programmende Private Sub Form1_Closed(...) _ Handles MyBase.Closed myForms.Remove(Me) If myForms.Count = 0 Then Application.Exit() End Sub End Class
Voraussetzungen
Wenn Sie das obige Beispielprogramm als Basis für Ihre eigenen Programme verwenden möchten, müssen Sie drei Dinge beachten:
왘 Die Programmausführung muss mit Module1.Main beginnen. 왘 Alle Fenster müssen mit OpenWindow geöffnet werden. 왘 Alle Fenster müssen die obige Closed-Ereignisprozedur aufweisen, die sicherstellt, dass das Programm beendet wird. Ein Problem bleibt auch in multi3 noch bestehen: Die Fenster teilen sich eine gemeinsame Nachrichtenschleife: Während in einem Fenster eine Berechnung ausgeführt wird, sind alle anderen Fenster blockiert. Es gibt zwei mögliche Lösungsansätze für dieses Problem:
왘 Sie können in zeitaufwändigen Prozeduren (hier: btnWork_Click) regelmäßig Application.DoEvents ausführen. Allerdings müssen Sie dann in diesen Prozeduren auch Kommunikations- und Abbruchmechanismen vorsehen. 왘 Sie können jedes Fenster in einem eigenen Thread öffnen. Damit erhalten Sie eine echte Multithreading-Windows-Anwendung. Probleme bereitet hier vor allem die Kommunikation zwischen den Fenstern, die synchronisiert werden muss (siehe Kapitel 8).
88
WINFORM.fm Seite 89 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6
MDI- und Docking-Anwendungen
MDI steht für Multiple Document Interface und bedeutet, dass in einem meist bildschirmfüllenden Hauptfenster mehrere Dokumentfenster angezeigt werden. Diese Art der Benutzeroberfläche gibt es schon ziemlich lange und ist beispielsweise bei allen älteren Versionen von Microsoft Office vorzufinden. (Für das Gegenstück zu MDI gibt es die seltener verwendete Abkürzung SDI für Single Document Interface.) In den vergangen Jahren sind MDI-Anwendungen seltener geworden. Mit der Visual Studio .NET-Entwicklungsumgebung könnte MDI allerdings in einer neuen Docking-Variante wieder populär werden: Im Unterschied zu klassischen MDI-Programmen sind in der Entwicklungsumgebung per Default alle Fenster irgendwo angedockt. (Man kann sogar darüber diskutieren, ob ein derartiges Docking-System überhaupt noch etwas mit dem herkömmlichen MDI-Konzept zu tun hat. Diese Art von Docking wird von der Windows.Forms-Bibliothek leider noch nicht richtig unterstützt. Diese Lücke füllt aber zum Glück die kostenlose Magic-Zusatzbibliothek.) Eine Gemeinsamkeit von MDI- und Docking-Anwendungen besteht darin, dass diese fast immer mit einem Menü ausgestattet sind. Daher beschreibt dieses Kapitel zuerst die Grundlagen von Menüs in .NETProgrammen und erst dann die Entwicklung von MDI- und DockingAnwendungen.
6.1 Menüverwaltung Um ein Formular (egal ob es sich um eine MDI- oder eine gewöhnliche Anwendung handelt) mit einem Menü auszustatten, fügen Sie per Doppelklick in der Toolbox das Steuerelement MainMenu in das Formular ein. Das noch leere Menü erscheint nun automatisch am oberen Ende des Formulars. Nun können Sie einfach per Tastatur die einzelnen Menüelemente benennen. (Alt)-Tastaturabkürzungen werden wie bei Buttons mit dem Zeichen & definiert (d.h., die Eingabe &Datei führt zum Menüeintrag DATEI). Trennstriche zwischen Menügruppen erreichen Sie durch die Eingabe eines Minuszeichens.
Menüeditor
Menüeinträge können mit der Maus verschoben und mit (Strg) kopiert werden. Für komplexere Umbauarbeiten empfiehlt es sich, ganze Menüs mit den Kontextmenükommandos AUSSCHNEIDEN, KOPIEREN und EINFÜGEN zu verschieben.
89
WINFORM.fm Seite 90 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Bevor Sie per Doppelklick Click-Ereignisprozeduren zu den einzelnen Menüeinträgen in den Code einfügen, sollten Sie den einzelnen Menüeinträgen unbedingt aussagekräftige Namen geben. Dazu können Sie wie üblich im Eigenschaftsfenster die Name-Eigenschaft verändern. Viel effizienter ist es aber, den Menüeditor per Kontextmenü in den Modus NAMEN BEARBEITEN umzuschalten: Nun können Sie die internen Namen der einzelnen Menüeinträge bequem im Menüeditor verändern. (Leider ist diese Funktion noch sehr unzuverlässig. Immer wieder ist es mir passiert, dass die Namensänderungen einfach nicht gespeichert wurden.) Alle anderen Eigenschaften der Menüeinträge müssen im Eigenschaftsfenster eingestellt werden Wenn Sie bei mehreren Menüeinträgen dieselbe Eigenschaft verändern möchten, können Sie mehrere Einträge einer Hierarchiegruppe gemeinsam markieren (mit (Strg) oder mit (Shift)).
Menüklassen MainMenu- und MenuItem-Klasse
Intern wird das Hauptmenü durch ein MainMenu-Objekt und jeder einzelne Menüeintrag durch ein MenuItem-Objekt realisiert. Die Klassen MenuItem und MainMenu sind nicht von Control, sondern direkt von Component abgeleitet. Da die Menu-Objekte keine richtigen Steuerelemente sind, fehlen leider viele von Steuerelementen bekannte Eigenschaften (darunter Name und Tag). Das MainMenu-Objekt verweist mit MenuItems auf alle MenuItem-Objekte des Hauptmenüs. Deren MenuItem-Objekte verweisen wiederum mit MenuItems auf Untermenüeinträge etc. Zur internen Verwaltung der Menüeinträge einer Menüebene dient die Menu.MenuItemCollection-Klasse. Die Klasse ist vor allem dann praktisch, wenn man Menüs dynamisch während des Programms verändern möchten (mit den Methoden Add und Remove).
Verhalten und Aussehen von Menüeinträgen
90
Enabled-Eigenschaft
Enabled und Visible geben wie bei Steuerelementen an, ob der Menüeintrag aktiv ist bzw. ob er angezeigt wird. Menüeinträge mit Enabled = False werden in grauer Schrift angezeigt und können nicht verwendet werden.
ShortCut-Eigenschaft
Mit ShortCut können Sie (zusätzlich zum (Alt)-Kürzel) eine weitere Tastaturabkürzung definieren. Das ist praktisch, wenn einzelne Menüeinträge mit Funktionstasten wie (Shift)+(Einf) ausgewählt werden können. Sie brauchen sich dann um die Auswertung dieser Funktionstasten nicht zu kümmern, die entsprechende Menüereignisprozedur wird automatisch aufgerufen. ShowShortCut gibt an, ob die Tastaturabkürzung auch im Menütext angezeigt wird (per Default: True).
WINFORM.fm Seite 91 Dienstag, 20. August 2002 3:48 15
Menüverwaltung
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Durch Checked=True können Sie vor einem Menüeintrag ein Auswahlhäkchen anzeigen lassen. (Checked wird durch eine Menüauswahl nicht automatisch verändert, sondern muss von Ihnen in der Click-Ereignisprozedur gesetzt bzw. gelöscht werden!)
.NET
Essentials
Checked-Eigenschaft
Wenn zusätzlich zu Checked auch RadioChecked auf True gesetzt wird, wird statt des Auswahlhäkchens ein gefüllter Optionskreis angezeigt. Diese Variante eignet sich dann, wenn der Anwender per Menü eine von mehreren Varianten auswählen kann. In diesem Fall müssen Sie in der Click-Ereignisprozedur die Checked-Eigenschaft des zuletzt auf diese Weise ausgewählten Eintrags zurücksetzen. Es gibt keine Möglichkeit, die Schriftart zur Darstellung der Menüeinträge zu verändern. Die Schriftart wird durch die Systemeinstellung vorgegeben und kann mit SystemInformation.MenuFont ermittelt werden. Wenn Sie andere Vorstellungen davon haben, wie Menüs aussehen sollen, müssen Sie die Menüeinträge selbst zeichnen (siehe Abschnitt 4.3).
Menüeinträge dynamisch einfügen Wenn Sie Menüeinträge per Code erzeugen, ist es leider nicht möglich, mit den MenuItem-Objekten irgendwelche Kontextinformationen zu speichern. Daher ist es bei der Verwendung einer gemeinsamen Ereignisprozedur für die neuen Menüeinträge schwierig, den ausgewählten Eintrag zu identifizieren. (Natürlich können Sie die Text-Eigenschaft auswerten, aber das ist fehleranfällig und inkompatibel mit einer eventuellen Lokalisierung des Programms.) Die einfachste Lösung besteht darin, die neu erzeugten MenuItemObjekte als Schlüssel in einer Hashtable zu verwenden, um so auf kontextspezifische Daten zuzugreifen. Im folgende Beispiel erweitert Button1 das TEST-Menü (MenuItemTest) um einen weiteren Eintrag. Jeder Menüeintrag wird in menuhash gespeichert. Bei der Auswahl eines derartigen Eintrags wird menuhash ausgewertet, um die Nummer des Menüeintrags zu ermitteln. ' Beispiel mdi\menu-test Dim menuhash As New Hashtable() Private Sub Button1_Click(...) _ Handles Button1.Click Static i As Integer = 1 Dim mi As MenuItem mi = MenuItemTest.MenuItems.Add( _ "neuer Eintrag " + i.ToString) menuhash.Add(mi, i) AddHandler mi.Click, AddressOf _ NewMenuItems_Click i += 1
91
WINFORM.fm Seite 92 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
End Sub Private Sub NewMenuItems_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim mi As MenuItem = CType(sender, MenuItem) MsgBox("Der neue Menüeintrag Nummer " + _ CType(menuhash(mi), Integer).ToString + _ " wurde ausgewählt.") End Sub
Menüereignisse Das wichtigste MenuItem-Ereignis ist zweifellos Click. Es tritt auf, wenn ein Menüeintrag angeklickt oder per Tastatur ausgewählt wird. Es tritt hingegen nicht auf, wenn ein Menüeintrag mit Untermenüs angeklickt wird. In diesem Fall tritt das Popup-Ereignis auf (siehe unten), anschließend wird das Untermenü angezeigt. Zentrale Ereignisprozedur für alle Menüeinträge
Die Konzeption der MenuItem-Klasse sieht vor, dass jeder Menüeintrag mit einer eigenen Ereignisprozedur ausgestattet wird. Das führt bei umfangreichen Menüs allerdings zu einer riesigen Anzahl von Ereignisprozeduren. Wenn Sie alle Menüeinträge mit einer einheitlichen Ereignisprozedur ausstatten möchten, können Sie die rekursive Prozedur AddHandlerForAllMenuItems dazu verwenden, alle MenuItem-Objekte mit der Prozedur zu verbinden. ' Beispiel mdi\menu-test Private Sub Form1_Load(...) Handles MyBase.Load AddHandlerForAllMenuItems(MainMenu1.MenuItems) End Sub Private Sub AddHandlerForAllMenuItems( _ ByVal mitems As Menu.MenuItemCollection) Dim mi As MenuItem For Each mi In mitems AddHandler mi.Click, _ AddressOf MenuItems_Click ' Prozedur rekursiv aufrufen AddHandlerForAllMenuItems(mi.MenuItems) Next End Sub ' Sammelprozedur für die Click-Ereignisse ' aller Menüeinträge Private Sub MenuItems_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim mi As MenuItem = CType(sender, MenuItem) MsgBox("click Menüeintrag " + mi.Text) If mi Is MenuItemFileNew Then
92
WINFORM.fm Seite 93 Dienstag, 20. August 2002 3:48 15
Menüverwaltung
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' ... ElseIf mi Is MenuItemFileSave Then ' ... End If End Sub
Das zweite wichtige Ereignis ist Popup. Es tritt für Menüeinträge auf, bevor deren Untermenü erscheint. Das Popup-Ereignis bietet die Möglichkeit, ein Menü vor dem Erscheinen dynamisch zu verändern. Beispielsweise können Sie vor dem Erscheinen des BEARBEITEN-Menüs testen, ob die Zwischenablage Daten enthält, die Ihr Programm verarbeiten kann. Wenn das der Fall ist, setzen Sie Enabled des Menüeintrags EINFÜGEN auf True, sonst auf False.
Popup-Ereignis
Private Sub MenuItemEdit_Popup(...) _ Handles MenuItemEdit.Popup If Clipboard.GetDataObject(). _ GetDataPresent(DataFormats.Text) Then MenuItemEditPaste.Enabled = True Else MenuItemEditPaste.Enabled = False End If End Sub
Kontextmenüs Kontextmenüs sind kleine Menüs, die an einer beliebigen Stelle im Programm mit der rechten Maustaste aufgerufen werden können. Um ein eigenes Kontextmenü zusammenzustellen, fügen Sie in das Formular ein ContextMenu-Steuerelement ein. Wenn Sie das Steuerelement anklicken, verschwindet vorübergehend das Hauptmenü (wenn Ihr Fenster eines hat), und Sie können den Menüeditor zur Gestaltung des Kontextmenüs verwenden. Auch die Verwaltung der Ereignisprozeduren erfolgt wie bei gewöhnlichen Menüs.
ContextMenu-Klasse
Kontextmenüs automatisch anzeigen: Wenn Sie möchten, dass das Kontextmenü durch einen Klick mit der rechten Maustaste automatisch erscheint, stellen Sie die ContextMenu-Eigenschaft eines Formulars oder Steuerelements so ein, dass sie auf das gewünschte Kontextmenü verweist. Diese Einstellung wird an alle untergeordneten Steuerelemente vererbt. Wenn Sie also die ContextMenu-Eigenschaft für das Fenster einstellen, erscheint das Kontextmenü auch bei allen im Fenster enthaltenen Steuerelementen automatisch. Wenn Sie das nicht möchten, fügen Sie ein zweites, leeres ContextMenu-Steuerelement in Ihr Formular ein und verweisen im Steuerelement mit dessen ContextMenu-Eigenschaft auf das leere Kontextmenü. (Selbstverständlich können Sie auch jedem Steuerelement ein eigenes Kontextmenü zuweisen.)
93
WINFORM.fm Seite 94 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Kontextmenüs manuell anzeigen: In vielen Anwendungen ist es sinnvoll, beim Drücken der rechten Maustaste zuerst einen Test durchzuführen, ob ein Erscheinen des Kontextmenüs zweckmäßig ist. In diesem Fall verzichten Sie auf die Einstellung der ContextMenu-Eigenschaft und schreiben stattdessen eine MouseDown-Ereignisprozedur, in der Sie unter anderem die Mauskoordinaten auswerten können. Wenn Sie das Menü anzeigen möchten, führen Sie ContextMenu1.Show aus. An die Methode müssen das Ausgangssteuerelement oder -formular sowie die Koordinaten für die Position des Menüs übergeben werden. Ereignisse: Vor dem Erscheinen des Kontextmenüs tritt für das ContextMenu-Objekt ein Popup-Ereignis auf. Sie können in der Ereignisprozedur
die Elemente des Menüs verändern. Sie können allerdings an dieser Stelle nicht mehr verhindern, dass das Menü erscheint. Für die einzelnen Menüeinträge tritt nach deren Auswahl wie gewohnt ein ClickEreignis auf.
6.2 MDI-Fensterverwaltung MDI-Anwendungen bestehen zumindest aus einem Hauptfenster und einem oder mehreren Sub- oder Dokumentfenstern (siehe Abbildung 6.1). Das Hauptfenster ist in der Regel mit einem zentralen Menü ausgestattet, dessen Kommandos für alle Subfenster gelten.
Abbildung 6.1: Ein einfaches MDI-Beispielprogramm
Hauptfenster IsMdiContainerEigenschaft
94
Das Hauptfenster ist das Fenster, mit dem das Programm startet. Der einzig wesentliche Unterschied besteht darin, dass die Eigenschaft IsMdiContainer auf True gestellt werden muss. (Damit ändert sich auch die Hintergrundfarbe des Fensters: Als Farbe wird nun SystemColors.AppWorkspace verwendet. Diese Farbe kann nur durch die Systemeinstellung geändert werden.)
WINFORM.fm Seite 95 Dienstag, 20. August 2002 3:48 15
MDI-Fensterverwaltung
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
In das Hauptfenster dürfen zwar Steuerelemente eingefügt werden, diese müssen aber an einem der vier Ränder angedockt werden. Im Regelfall enthält das Hauptfenster nur ein Menü und eventuell Symbolund Statusleisten. (Nicht angedockte Steuerelemente sind prinzipiell auch zulässig, sie werden aber über allen Subfenstern angezeigt und stören damit erheblich.) Das Hauptfenster sollte zumindest die Möglichkeit bieten, neue Fenster zu erzeugen. Üblicherweise erfolgt das über das Hauptmenü, denkbar wäre aber auch die Verwendung eines Kontextmenüs oder einer Symbolleiste. Beachten Sie, dass das Hauptfenster wegen IsMdiContainer=True keine Mausereignisse (MouseDown, Click etc.) mehr empfängt.
Subfenster Die Basis für das Subfenster ist ein zweites Formular, das mit PROJEKT|WINDOWS FORMS HINZUFÜGEN in das aktuelle Projekt eingefügt wird. Häufig enthält dieses Fenster nur ein einziges Steuerelement (z.B. ein Textfeld oder ein Bildfeld) mit Dock=Fill, so dass es den gesamten Fensterinhalt füllt. Grundsätzlich können Sie das MDI-Subfenster aber vollkommen frei gestalten. Um ein Subfenster in das Hauptfenster einzufügen, führen Sie in der entsprechenden Ereignisprozedur (z.B. zum Menükommando DATEI|NEU) die folgenden Zeilen aus. Damit erzeugen Sie ein neues Objekt der Klasse Form2. (Form2 ist der Name der MDI-Subfensterklasse.) Entscheidend ist die Einstellung von MdiParent, die mit Me auf das Hauptfenster verweist.
MdiParent-Eigenschaft
Dim frm As New Form2() frm.MdiParent = Me frm.Text = "Dokumentfenster " + i.ToString frm.Show()
Wenn eines der Subfenster über den Rand des Hauptfensters hinausreicht, werden darin automatisch Scrollbalken angezeigt.
Verwaltung der Subfenster Tabelle 6.1 zählt die Methoden und Eigenschaften des Form-Objekts des Hauptfensters auf, die bei der Verwaltung der Subfenster helfen: ActiveMdiChild (Eigenschaft)
verweist auf das aktive MDI-Subfenster. Die Eigenschaft enthält Nothing, wenn noch kein MDI-Fenster geöffnet wurde.
ActiveMdiChild.ActiveControl (Eigenschaft)
verweist auf das Steuerelement mit dem Eingabefokus innerhalb des Subfensters.
Tabelle 6.1: Verwaltung der MDI-Subfenster
95
WINFORM.fm Seite 96 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
MdiChildren (Eigenschaft)
verweist auf ein Feld mit den Form-Objekten aller Subfenster.
ActivateMdiChild (Methode)
aktiviert das als Parameter angegebene Subfenster.
MdiChildActivate (Ereignis)
tritt auf, wenn sich das aktive Subfenster ändert (z.B. wenn ein anderes Subfenster angeklickt wird oder wenn ein Subfenster geschlossen wird). Das Ereignis tritt auch dann auf, wenn das letzte Subfenster geschlossen wird. ActiveMdiChild enthält dann Nothing.
Tabelle 6.1: Verwaltung der MDI-Subfenster (Forts.)
Als Alternative zur MdiChildren-Aufzählung können Sie natürlich auch eine eigene Aufzählung verwalten, die Sie jedes Mal, wenn ein Subfenster geöffnet oder geschlossen wird, entsprechend ändern. Zugriff auf Subfensterdaten
ActiveMdiChild bzw. MdiChildren liefern Form-Objekte zurück. Wenn Sie diese mit CType in den Datentyp des Subfensters umwandeln, können Sie auf alle Steuerelemente und auf alle Klassenvariablen zugreifen, die mit Friend oder Public deklariert wurden. Die folgenden Zeilen befinden sich im Klassenmodul des Hauptfensters und setzen voraus, dass alle Subfenster Objekte der Klasse Form2 sind und dass Form2 ein Textfeld TextBox1 und eine öffentliche Integer-Variable myvar1 enthält. Dim frm As Form2 If Not IsNothing(Me.ActiveMdiChild) Then frm = CType(Me.ActiveMdiChild, Form2) frm.TextBox1.Text = "abc" frm.myvar1 = 3 End If
Zugriff auf das MDIHauptfenster
Code, der in Subfenstern ausgeführt wird, kann mit der bereits erwähnten Eigenschaft MdiParent auf das Hauptfenster zurückverweisen. Über diesen Umweg können Sie von einem Subfenster auf andere Subfenster zugreifen.
Menüs bei MDI-Anwendungen Ein Kennzeichen beinahe aller MDI-Anwendungen ist das so genannte Fenstermenü. Das ist ein Menü, das die Namen aller Subfenster enthält (siehe Abbildung 6.2). Mit dem Menü kann eines der Fenster aktiviert werden. Im Regelfall enthält das Menü auch Kommandos, um die Fensteranordnung zu ändern (also um die Fenster beispielsweise überlappend anzuordnen). MdiList-Eigenschaft
96
Die Umsetzung eines derartigen Menüs ist einfach: Sie fügen im Hauptfenster ein neues Menü FENSTER ein und stellen dessen Eigenschaft MdiList auf True. Damit erreichen Sie, dass die Namen aller MDI-
WINFORM.fm Seite 97 Dienstag, 20. August 2002 3:48 15
MDI-Fensterverwaltung
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Fenster automatisch als Einträge dieses Menüs angezeigt werden. Das gerade aktive Fenster wird durch ein Auswahlhäkchen gekennzeichnet. Durch die Auswahl eines dieser Menüeinträge verändert sich das aktive Fenster.
Abbildung 6.2: MDI-Fenstermenü
Darüber hinaus können Sie dem Fenstermenü noch einige eigene Einträge hinzufügen, um das Fensterlayout zu ändern. In den dazugehörenden Ereignisprozeduren führen Sie dann die LayoutMdi-Methode aus, z.B.: Me.LayoutMdi(MdiLayout.Cascade)
Nicht nur das Hauptfenster, auch die Subfenster können mit Menüs ausgestattet werden. Diese Menüs werden allerdings im Hauptfenster angezeigt, und zwar nach den Menüeinträgen des Hauptfensters. Wenn das Hauptfenster die Menüs DATEI und FENSTER enthält und das Subfenster die Menüs BEARBEITEN und HILFE, dann gilt im resultierenden Gesamtmenü die eher unübliche Reihenfolge DATEI, FENSTER, BEARBEITEN und HILFE. (Per Code können Sie auf das Gesamtmenü über die Eigenschaft MergedMenu des Hauptfensters zugreifen.)
Menüeinträge der Subfenster
Es besteht leider keine Möglichkeit, die Reihenfolge zu ändern. Wenn Sie damit nicht zufrieden sind, bleibt als Alternative nur die dynamische Veränderung des Hauptmenüs per Programmcode (z.B. immer dann, wenn sich das aktive Subfenster ändert). Dazu stehen zwar einige recht leistungsfähige Methoden zur Verfügung (insbesondere MergeMenu), der Codeaufwand ist aber dennoch erheblich. Das Programmende wird wie bei gewöhnlichen Programmen dadurch eingeleitet, dass für das Hauptfenster Me.Close ausgeführt wird bzw. der X-Button dieses Fensters angeklickt wird. In der Folge kommt es zuerst zum Aufruf der Closing-Ereignisprozedur für alle Subfenster. Anschließend wird auch die Closing-Prozedur des Hauptfensters ausgeführt. Wenn auch nur in einer einzigen dieser Prozeduren e.Cancel=True aus-
Programmende
97
WINFORM.fm Seite 98 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
geführt wird, stoppt der Ereignisfluss und das Programm wird fortgesetzt. Ein sofortiges Programmende können Sie durch Application.Exit erreichen (wenn Ihr Programm in der höchsten .NET-Sicherheitsstufe ausgeführt wird).
MDI-Beispielprogramm Das in Abbildung 6.1 dargestellte Beispielprogramm erfüllt keine konkrete Aufgabe, sondern zeigt lediglich die Struktur einer einfachen MDI-Anwendung samt Menü. Mit DATEI|NEU können neue Fenster geöffnet, mit DATEI|SCHLIEßEN wieder geschlossen werden. Das FENSTER-Menü ermöglicht den Fensterwechsel und die Neuanordnung der Fenster. Die Subfenster (Form2) bestehen aus einem Textfeld, das dank Dock=Fill das gesamte Fenster füllt. Der gesamte Code befindet sich in den Ereignisprozeduren des Hauptfensters (Form1) und sollte auf Anhieb verständlich sein. ' Beispiel mdi\mdi-intro ' neues Fenster öffnen Private Sub MenuItemFileNew_Click(...) _ Handles MenuItemFileNew.Click Dim frm As New Form2() Static i As Integer = 1 frm.MdiParent = Me frm.Text = "Dokumentfenster " + i.ToString frm.Show() i += 1 End Sub ' Fensterlayout ändern Private Sub MenuItemWindowCascade_Click(...) _ Handles MenuItemWindowCascade.Click Me.LayoutMdi(MdiLayout.Cascade) End Sub Private Sub MenuItemWindowHorizontal_Click(..) _ Handles MenuItemWindowHorizontal.Click Me.LayoutMdi(MdiLayout.TileHorizontal) End Sub Private Sub MenuItemWindowVertical_Click(...) _ Handles MenuItemWindowVertical.Click Me.LayoutMdi(MdiLayout.TileVertical) End Sub ' Subfenster schließen Private Sub MenuItemFileClose_Click(...) _ Handles MenuItemFileClose.Click ' wenn kein MDI-Fenster geöffnet ist, dann
98
WINFORM.fm Seite 99 Dienstag, 20. August 2002 3:48 15
Die Magic-Bibliothek
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' verweist ActiveForm auf das Hauptfenster If Not IsNothing(Me.ActiveMdiChild) Then Me.ActiveMdiChild.Close() End If End Sub
MDI und Docking Docking kennen Sie wahrscheinlich schon von Experimenten mit der Dock-Eigenschaft von Steuerelementen. Damit können Sie Steuerelemente an einen Rand des Fensters quasi festkleben. In diesem Abschnitt sollen allerdings nicht Steuerelemente, sondern Subfenster innerhalb eines Hauptfensters angedockt werden. Damit nicht genug: Diese Subfenster sollen trotz des Dockings verschiebbar bleiben. Es soll also möglich sein, derartige Fenster per Maus an eine neue Position zu verschieben, sie wie Dialogblätter zu gruppieren etc.
Dock-Eigenschaft
In MDI-Anwendungen können Sie zwar MDI-Subfenster mit fenster. Dock=... andocken, Experimente mit dieser Art von Docking funktionieren aber nur zufriedenstellend, solange es nur ein einziges Fenster gibt. Bei mehreren Fenstern ändert sich beim Anklicken eines Fensters die Größe anderer Fenstern unvorhersehbar, es gibt Probleme mit der Verankerung (Anchor-Eigenschaft) von Steuerelementen innerhalb der MDI-Subfenster etc. Auch die Darstellung von Fenstern innerhalb von Dialogblättern ist problematisch. Das lässt sich erreichen, indem Sie die Eigenschaft TopLevel eines Fensters auf False stellen. Anschließend kann das Fenster mit Panel1.Controls.Add(frm) bzw. mit TabPage1.TabPages(n).Controls. Add(frm) in das Panel bzw. in eine Dialogseite eingefügt werden. Leider führen die so erzielbaren Effekte in eine Sackgasse: So gibt es beispielsweise bei einem Fensterwechsel per Mausklick Probleme mit dem Tastaturfokus. Außerdem ist diese Art der Darstellung mit dem MDIKonzept inkompatibel.
TopLevel-Eigenschaft
Kurz und gut, es ist leider nicht möglich, mit den Basisklassen der Windows.Forms-Bibliothek ein modernes Docking-Programm zu entwickeln. (Sie können sich selbst von den Problemen überzeugen, indem Sie das Testprogramm mdi\docking1 bzw. mdi\dockint2 ausprobieren. Die Programme stammen aus meinem großen Visual Basic .NET-Buch und sind hier aus Platzgründen nicht weiter beschrieben.)
6.3 Die Magic-Bibliothek It's a kind of magic ... möchte man meinen, wenn man sich unter http:// www.dotnetmagic.com/ die Beschreibung der Magic-Bibliothek durchliest. Die Bibliothek bietet unter anderem
99
WINFORM.fm Seite 100 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
왘 einen Docking-Manager, um Fenster dynamisch anzudocken, 왘 eine neue TabControl-Klasse mit mehr Gestaltungsmöglichkeiten als die Windows.Forms.TabControl-Klasse, 왘 eine neue MenuControl-Klasse zur Darstellung von Menüeinträgen, die aussehen wie bei der Visual Studio .NET-Benutzeroberfläche und 왘 einen neuen InertButton, der im Gegensatz zum gewöhnlichen Button keinen Tastaturfokus kennt. (Der Vorteil besteht darin, dass beim Anklicken dieses Buttons der Fokus dort bleibt, wo er bisher war.) Die gesamte Bibliothek liegt im C#-Quellcode vor und darf kostenlos eingesetzt werden (auch in kommerziellen Projekten, sofern diese nicht das Ziel haben, selbst eine vergleichbare Bibliothek zu bilden). Der Autor bittet aber darum, im INFO-Dialog des Programms auf die Bibliothek und dessen Website hinzuweisen. Details zu den Lizenzbedingungen finden Sie in der Datei Readme.htm. Unter www.dotnetmagic.com finden Sie eine Download-Möglichkeit für die aktuelle Version als *.msi-Datei (Microsoft Installer). Zur Installation reicht ein einfacher Doppelklick auf die Datei. Damit wird die Bibliothek in den Global Assembly Cache installiert (GAC, Verzeichnis Winnt\assembly). Außerdem werden der gesamte Quellcode, die Dokumentation sowie einige Beispielprogramme für C# und Visual Basic .NET in ein beliebig wählbares Verzeichnis kopiert. Anhand dieser Beispiele können Sie sich rasch davon überzeugen, dass die Bibliothek hält, was sie verspricht. (Dieses Kapitel basiert auf der MagicVersion 1.6.) Nachteile
Wahrscheinlich werden Sie sich mittlerweile fragen, wo der Haken dieser Bibliothek liegt. Den gibt es tatsächlich – es ist die bisweilen umständliche Programmierung. Normalerweise verwenden Sie zum Entwurf von Formularen, Dialogen, Menüs etc. den in die Entwicklungsumgebung integrierten Forms-Designer. Dieser kennt aber die neuen Klassen der Magic-Bibliothek nicht. Wenn Sie also ein Menü wie in Abbildung 6.3 zusammensetzen möchten, müssen Sie jeden einzelnen Eintrag durch eine oder mehrere Codezeilen bilden. Ein zweiter Nachteil der Bibliothek besteht darin, dass die an sich gut gegliederte Dokumentation nur einen Bruchteil der Klassen, Methoden und Eigenschaften beschreibt und nur auf Englisch verfügbar ist.
Verwendung der Bibliothek Damit Sie die Magic-Bibliothek nutzen können, müssen Sie per Kontextmenü im Projektmappenexplorer oder mit PROJEKT|VERWEIS HINZUFÜGEN einen Verweis auf die Magic-Bibliothek einrichten. Die Namen aller Klassen dieser Bibliothek beginnen mit Crownwood.Magic.
100
WINFORM.fm Seite 101 Dienstag, 20. August 2002 3:48 15
Docking mit der Magic-Bibliothek
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Abbildung 6.3: Die mit der Magic-Bibliothek mitgelieferten Beispielprogramme
Um den Tippaufwand zu minimieren, empfiehlt es sich, am Beginn der Codedatei die folgende Imports-Anweisung einzufügen. (Die weiteren Beispiele gehen davon aus.) Imports Crownwood.Magic
Der Platz in diesem Buch reicht leider nicht aus, um alle oder auch nur die wichtigsten Funktionen der Magic-Bibliothek zu beschreiben. In den beiden folgenden Abschnitten werden daher nur die Grundfunktionen zur Verwaltung von Docking-Fenstern und zur Gestaltung von TabControls beschrieben. Anschließend folgt ein etwas umfangreicheres Beispiel.
6.4 Docking mit der Magic-Bibliothek Bevor die wichtigsten Details einiger Docking-Klassen beschrieben werden, soll Ihnen das in Abbildung 6.4 dargestellte Beispiel den Einstieg erleichtern. Das Programm besteht aus zwei Formularen für das Hauptfenster (MainForm) und das Subfenster (ChildForm). MainForm ist mit einem Menü ausgestattet, mit dem Sie neue Subfenster erzeugen können. Diese Subfenster erscheinen per Default links angedockt mit einer Breite von 150 Pixeln (wie Fenster 1 und 2 in Abbildung 6.4). Die Fenster können dann mit der Maus frei verschoben werden (Fenster 3 und 4) und an anderen Fensterrändern – auch in Gruppen – angedockt werden (Fenster 5 und 6). Wenn die Pinboard-Nadel angeklickt wird, werden die Fenster automatisch ausgeblendet, wenn Sie nicht benötigt werden (das hier ausgeblendete Fenster 7 und das sichtbare Fenster 8).
Einführungsbeispiel
101
WINFORM.fm Seite 102 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
DockingManager-Klasse
Der erforderliche Programmcode für das Programm ist überraschend kurz. ChildForm kommt ganz ohne Code aus, in MainForm sind es wenige Zeilen. Im Mittelpunkt steht dabei ein DockingManager-Objekt, das über die Variable dockManag angesprochen wird. Das Objekt muss mit dem Formular verbunden werden, in dem die Subfenster angeordnet werden können. Das können Sie entweder im New-Konstruktur innerhalb des Codes des Form-Designers oder in der Form_Load-Ereignisprozedur tun. Der DockingManager kümmert sich in der Folge um die Verwaltung aller im Fenster dargestellten Subfenster.
Abbildung 6.4: Magic-Docking-Einführungsbeispiel ' Beispiel mdi\magic-docking Imports Crownwood.Magic Public Class MainForm Inherits System.Windows.Forms.Form [ Vom Windows Form Designer generierter Code ...] Friend WithEvents dockManag As _ Docking.DockingManager Private Sub MainForm_Load(...) _ Handles MyBase.Load dockManag = New Docking.DockingManager( _ Me, Common.VisualStyle.IDE) End Sub End Class
Content-Klasse
102
Um ein neues Subfenster zu öffnen, erzeugt MenuWindowNew_Click ein neues Objekt der Klasse ChildForm und übergibt dieses als Parameter an die Methode dockManag.Contents.Add. Damit wird ein neues ContentObjekt erzeugt. (Jedes vom DockingManager verwaltete Subfenster wird über ein Content-Objekt angesprochen.) Als zweiter Parameter wird der gewünschte Fenstertitel übergeben. Mit zwei weiteren optionalen Para-
WINFORM.fm Seite 103 Dienstag, 20. August 2002 3:48 15
Docking mit der Magic-Bibliothek
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
meter kann ein ImageList-Steuerelement und die Nummer einer dort enthaltenen Bitmap übergeben werden. Das Bild wird dann in manchen Darstellungsformen neben dem Titel angezeigt (siehe Abbildung 6.4). Damit das neue Fenster sichtbar wird, muss das Content-Objekt an die Methode dockManag.ShowContent übergeben werden. ' neues Subfenster erzeugen und anzeigen Private Sub MenuWindowNew_Click(...) _ Handles MenuWindowNew.Click Dim c As Docking.Content Dim f As New ChildForm() Static i As Integer i += 1 f.TextBox1.Text = _ "Text von Fenster " + i.ToString c = dockManag.Contents.Add(f, _ "Fenster " + i.ToString, ImageList1, 0) dockManag.ShowContent(c) End Sub
Beachten Sie, dass das Aussehen der Docking-Fenster – also der Titel, der Fensterrahmen etc. – durch den DockingManager bestimmt wird, nicht durch die Eigenschaften des ChildForm-Objekts! Beachten Sie auch, dass das Programm – trotz optischer Ähnlichkeiten – intern nichts mit einem MDI-Programm zu tun hat. Weder beim Haupt- noch bei den Subfenstern wurden irgendwelche MDI-Eigenschaften verändert.
Docking ist nicht MDI!
Subfenster Die Verwaltung der Docking-Fenster erfolgt also durch Objekte der Content-Klasse. Beim Erzeugen eines solchen Objekts kann ein einfaches Steuerelement (z.B. ein Textfeld oder eine PictureBox) übergeben werden. Wenn Sie innerhalb der Subfenster mehrere Steuerelemente anzeigen bzw. beim Design der Subfenster wie gewohnt auf die Entwicklungsumgebung zurückgreifen möchten, können Sie innerhalb eines Content-Objekts auch ein ganzes Formular darstellen (wie dies bereits im Einführungsbeispiel der Fall war). Dabei müssen Sie aber bedenken, dass nur das Innere des Fensters dargestellt wird.
Content-Klasse
Für den Fensterrahmen und seine Beschriftung ist die Content-Klasse verantwortlich. Deswegen sollten Sie beim Erzeugen eines ContentObjekts einen Fenstertitel und eventuell auch einen Verweis auf ein ImageList-Steuerelement und eine Indexnummer angeben. Damit wird neben dem Titel auch die entsprechende Bitmap angezeigt. (Bei der Magic-Version 1.6 ist die Bitmap erforderlich, damit bei automatisch ausgeblendeten Fenstern die Beschriftung richtig angezeigt wird.)
103
WINFORM.fm Seite 104 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Die folgenden Zeilen erzeugen ein neues Content-Objekt, das nur ein RichTextBox-Steuerelement enthält. Dim c As New Docking.Content( _ dockManag, New RichTextBox(), "Fenstertitel")) dockManag.Contents.Add(c)
Alternativ ist auch die folgende Schreibweise zulässig: Dim c As Docking.Content c = dockManag.Contents.Add( _ New RichTextBox(), "Fenstertitel")
Mit der Methode ShowContent machen Sie das neue Content-Objekt sichtbar: dockManag.ShowContent(c)
Docking-Position auswählen
Dadurch wird das Objekt per Default in einem neuen, 150 Pixel breiten Fenster links angedockt. Wenn Sie eine andere Docking-Position wünschen, müssen Sie die Methode AddContentWithState verwenden und im zweiten Parameter den gewünschten Rand angeben (DockLeft, DockTop etc.). Auch eine ungedockte Darstellung ist möglich (Floating). Durch DisplaySize und DisplayLocation können Sie im Voraus die Größe und die Position in absoluten Bildschirmkoordinaten angeben. Die Methode PointToScreen hilft bei der Umrechnung zwischen den lokalen Koordinaten des Hauptfensters und den absoluten Bildschirmkoordinaten. c = dockManag.Contents.Add( _ New RichTextBox(), "Fenstertitel") ' Subfenster frei positionieren c.DisplaySize = New Size(250, 150) c.DisplayLocation = Me.PointToScreen( _ New Point(10, 10)) dockManag.AddContentWithState( _ c, Docking.State.Floating) dockManag.ShowContent(c) 'c anzeigen
Subfenster wie Dialogblätter darstellen WindowContent-Klasse
104
Jedes Content-Objekt befindet sich Magic-intern innerhalb eines Window Content-Objekts. Per Default wird in jedem WindowContent nur ein einziges Fenster angezeigt. Wenn Sie Subfenster aber ineinander verschieben, befinden sich mehrere Content-Objekte in einem WindowContent-Objekt. Das Objekt kümmert sich dann automatisch um die Darstellung von Dialogblättern (wie bei den Fenstern 5 und 6 in Abbildung 6.4).
WINFORM.fm Seite 105 Dienstag, 20. August 2002 3:48 15
Docking mit der Magic-Bibliothek
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Per Code können Sie mit der Eigenschaft ParentWindowContent das WindowContent-Objekt ermitteln, in dem sich ein bestimmtes ContentObjekt befindet. Mit der Methode AddContentToWindowContent können Sie in das damit bekannte WindowContent-Objekt weitere Subfenster einfügen. Die folgenden Zeilen demonstrieren die Vorgehensweise. Statt jedes Content-Objekt einzeln sichtbar zu machen, werden alle Subfenster mit ShowAllContents angezeigt. ' zwei Subfenster als Dialogblätter anzeigen Dim c1, c2 As Docking.Content Dim wc As Docking.WindowContent c1 = dockManag.Contents.Add( _ New RichTextBox(), "abc") c2 = dockManag.Contents.Add( _ New RichTextBox(), "def") dockManag.ShowContent(c1) wc = c1.ParentWindowContent dockManag.AddContentToWindowContent(c2, wc) dockManag.ShowAllContents() 'alles anzeigen
Subfenster neben- oder untereinander anordnen Die Verschachtelung der Klassen geht noch eine Stufe weiter: Intern wird jedes WindowContent-Objekt in einem Zone-Objekt angeordnet, wobei sich per Default immer nur ein WindowContent-Objekt in einem Zone-Objekt befindet. Die Aufgabe der Zone-Klasse besteht darin, mehrere Subfenster neben- oder untereinander darzustellen (je nachdem, an welchem Rand das Zone-Objekt angedockt ist).
Zone-Klasse
Um mehrere Subfenster neben- oder untereinander einzufügen, ermitteln Sie mit der Eigenschaft ParentZone das zu einem WindowContent gehörende Zone-Objekt und fügen die weiteren Content-Objekte mit AddContentToZone ein. Dabei müssen Sie als dritten Parameter die gewünschte Position innerhalb der Zone angeben. (0 bedeutet ganz oben bzw. ganz links.) Die folgenden Zeilen demonstrieren die Vorgehensweise, Abbildung 6.5 zeigt das Ergebnis. ' drei Subfenster am unteren Fensterrand ' nebeneinander anordnen Dim c1, c2, c3 As Docking.Content Dim wc As Docking.WindowContent Dim z As Docking.Zone c1 = dockManag.Contents.Add( _ New RichTextBox(), "abc") c2 = dockManag.Contents.Add( _ New RichTextBox(), "def") c3 = dockManag.Contents.Add( _ New RichTextBox(), "ghi") dockManag.AddContentWithState( _
105
WINFORM.fm Seite 106 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
c1, Docking.State.DockBottom) wc = c1.ParentWindowContent z = wc.ParentZone dockManag.AddContentToZone(c2, z, 1) dockManag.AddContentToZone(c3, z, 2) dockManag.ShowAllContents() 'alles anzeigen
Abbildung 6.5: Drei RichTextBoxes in einer Reihe
Beachten Sie, dass es nicht möglich ist, eine ganze Zone mit der Maus zu verschieben oder auch nur einzelne Subfenster nebeneinander in eine Zone einzufügen. Bei Verschiebeoperationen mit der Maus werden die Subfenster unweigerlich wieder zu Dialogblättern.
Docking einschränken OuterControl-Eigenschaft
Wenn das Hauptfenster (mit der Ausnahme eines Menüs) leer ist, funktioniert das Docking problemlos. Wenn sich im Hauptfenster aber weitere Steuerelemente befinden (z.B. eine oben angedockte ToolBar oder eine unten angedockte StatusBar), dann kann es passieren, dass der DockingManager auch diese Steuerelemente verschiebt. Um das zu vermeiden, muss die Eigenschaft OuterControl auf das erste Steuerelement dieser Gruppe verweisen. Der erforderliche Code in New (innerhalb des Codes des Form Designer) sieht so aus: Public Sub New() MyBase.New() InitializeComponent() dockManag = New Docking.DockingManager( _ Me, Common.VisualStyle.IDE) dockManag.OuterControl = Me.Controls(0) End Sub
InnerControl-Eigenschaft
106
Komplizierter wird es, wenn es innerhalb des Hauptfensters nicht nur seitlich gedockte Steuerelemente gibt, sondern außerdem noch ein Steuerelement mit Dock=Fill, das den gesamten freien Fensterraum füllt, der nicht durch gedockte Steuerelemente bzw. Subfenster bedeckt
WINFORM.fm Seite 107 Dienstag, 20. August 2002 3:48 15
Docking mit der Magic-Bibliothek
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
ist. (Üblicherweise handelt es sich dabei um ein Panel oder um ein Dokumentenfenster, z.B. ein Textfeld.) Damit die Position und Größe dieses Steuerelements erst dann berechnet wird, wenn alle anderen gedockten Steuerelemente und Subfenster positioniert wurden, muss die Eigenschaft InnerControl darauf verweisen. Public Sub New() MyBase.New() InitializeComponent() dockManag = New Docking.DockingManager( _ Me, Common.VisualStyle.IDE) dockManag.OuterControl = Me.Controls(0) ' für RichTextBox gilt Dock=Fill; es füllt den ' gesamten freien Raum innerhalb des ' Hauptfensters dockManag.InnerControl = RichTextBox1 End Sub
Wenn Sie sich die Dokumentation zu Inner- und OuterControl durchlesen, werden Sie feststellen, dass das Ganze noch ein bisschen komplizierter ist. Entscheidend ist nämlich auch die Reihenfolge der Steuerelemente innerhalb des Formulars, die bei der automatischen Platzierung der Steuerelemente berücksichtigt wird. Damit das oben beschriebene Szenario funktioniert, muss das InnerControl-Steuerelement das erste Steuerelement innerhalb des Formulars sein. Das können Sie im Form Designer erreichen, indem Sie per Kontextmenü IN DEN VORDERGRUND ausführen. Wenn Sie das InnerControl-Steuerelement per Code erzeugen, müssen Sie die übrigen, angedockten Steuerelemente (z.B. die Statusbar) auch per Code erzeugen, und zwar nach dem InnerControl-Steuerelement. Andernfalls funktioniert das automatische Ausblenden angedockter Fenster nicht korrekt. Insgesamt ist es leider ein recht mühsamer und eher experimenteller Vorgang, die für den DockingManager richtige Reihenfolge bei der Erzeugung der Steuerelemente und bei der Einstellung von Inner- und OuterControl zu finden.
Subfenster schließen Jedes Subfenster ist mit einem kleinen X-Buttons ausgestattet, mit dem das Fenster geschlossen wird. Allerdings gibt es hier Unterschiede im Vergleich zu normalen Fenstern: Das Subfenster wird nämlich nur ausgeblendet. Es steht weiterhin zur Verfügung und kann z.B. mit der Methode dockManag.ShowAllContents() oder über das im Titelbereich eines noch sichtbaren Fensters zugängliche Kontextmenü wieder eingeblendet werden.
Per Default werden Subfenster nur ausgeblendet
Diese Vorgehensweise ist für Dialoge, die der Steuerung eines Programms dienen, durchaus praktisch. Wenn der DockingManager aber
107
WINFORM.fm Seite 108 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
dazu verwendet wird, Dokumentenfenster zu verwalten, dann erwartet der Benutzer zumeist, dass das Fenster durch den X-Button endgültig geschlossen wird und dass gegebenenfalls vorher eine Rückfrage erscheint, ob der Inhalt des Fensters gespeichert werden soll. ContentHiding-Ereignis
Um ein derartiges Verhalten zu erreichen, muss das ContentHidingEreignis ausgewertet werden. Dieses Ereignis tritt auf, bevor ein Subfenster ausgeblendet wird. Die folgenden Zeilen testen, ob das auszublendende Content-Objekt auf ein Form2-Objekt verweisen (das z.B. ein Textfeld enthält). Wenn das der Fall ist, wird versucht, dieses Fenster durch Close zu schließen. Wenn dieser Versuch gelingt, wird das Fenster anschließend durch Dispose aus dem Speicher entfernt und das dazugehörende Content-Menü aus der Contents-Liste des DockingManager entfernt. Wenn das Fenster dagegen nicht geschlossen werden konnte (z.B. weil der Vorgang in der Closing-Prozedur von Form2 abgebrochen wurde), wird auch das Ausblenden durch cea.Cancel=True abgebrochen. Private Sub dockManag_ContentHiding( _ ByVal c As Docking.Content, ByVal cea _ As System.ComponentModel.CancelEventArgs) _ Handles dockManag.ContentHiding Dim f As Form2 If TypeOf c.Control Is Form2 Then f = CType(c.Control, Form2) f.Close() If f.Visible = True Then ' Close hatte keine Wirkung, ' also Vorgang abbrechen cea.Cancel = True Else ' Fenster und Content-Objekt aus dem ' Speicher entfernen f.Dispose() dockManag.Contents.Remove(c) End If End If End Sub
Die dazugehörende Form2.Closing-Prozedur könnte z.B. so aussehen: Private Sub Form2_Closing( _ ByVal sender As Object, ByVal e _ As System.ComponentModel.CancelEventArgs) _ Handles MyBase.Closing Dim result As MsgBoxResult result = MsgBox("Soll das Fenster " + _ "wirklich geschlossen werden?", _ MsgBoxStyle.YesNo) If result = MsgBoxResult.No Then
108
WINFORM.fm Seite 109 Dienstag, 20. August 2002 3:48 15
Dialogblätter mit der Magic-Bibliothek
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
e.Cancel = True End If End Sub
Einschränkungen und Probleme Bei den Experimenten mit den Docking-Funktionen der Magic-Bibliothek sind leider auch ein paar (nicht allzu gravierende) Probleme aufgetreten:
왘 Wenn Subfenster als eigenständige Objekte angezeigt werden (also ohne Docking), kann das Menü nicht per Tastatur bedient werden. Weder (Alt)+Anfangsbuchstabe noch (F10) zur Aktivierung des Menüs funktionieren. 왘 Wenn das Hauptfenster verschoben wird, werden eigenständige Subfenster nicht mit verschoben. Dieses Verhalten entspricht der Visual Studio .NET-Entwicklungsumgebung, es ist aber ungewohnt, wenn Sie es mit MDI-Programmen vergleichen. 왘 Generell ist kein Tastenkürzel für einen Fokuswechsel zwischen den Fenstern vorgesehen. Jeder Wechsel des aktiven Fensters muss per Maus erfolgen.
6.5 Dialogblätter mit der Magic-Bibliothek Die Windows.Forms-Bibliothek stellt zur Gestaltung von mehrblättrigen Dialogen das TabControl-Steuerelement zur Verfügung. Die Anwendung dieses Steuerelements zur Gestaltung von Dialogen ist vollkommen unkompliziert (so dass in diesem Buch auf eine Beschreibung verzichtet wird).
TabControl-Steuerelement von Windows.Forms
Das TabControl-Steuerelement hat aber den Nachteil, dass die Gestaltungsmöglichkeiten ziemlich eingeschränkt sind: Die Tabellenreiter (also die Beschriftungstexte für die einzelnen Blätter) müssen immer am oberen Fensterrand dargestellt werden, es ist unmöglich, einen X-Button zum Schließen einzelner Blätter anzuzeigen etc. Damit eignet sich das Steuerelement zwar gut zur Gestaltung von mehrblättrigen Dialogen, es ist aber nur schlecht geeignet, um Dokumentenfenster einer Docking-Anwendung darzustellen (in der Art und Weise, wie die Code- und Formularfenster innerhalb der Visual Studio .NET-Entwicklungsumgebung angezeigt werden). Wesentlich mehr Gestaltungsmöglichkeiten bietet das TabControlSteuerelement aus der Magic-Bibliothek. Allerdings kann der Dialogentwurf nun nicht mehr in der Entwicklungsumgebung erfolgen. Das ist aber keine große Einschränkung, wenn das Steuerelement in einer Docking-Anwendung als Container für Dokumentenfenster dienen
TabControl-Steuerelement der MagicBibliothek
109
WINFORM.fm Seite 110 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
soll: in diesem Fall werden die im TabControl einzufügenden Fenster oder Dialoge ohnedies per Code erzeugt. Das Aussehen des TabControl-Steuerelements wird primär durch die Eigenschaft Appearance gesteuert. Hier sind drei Einstellungen möglich: MultiBox, MultiDocument und MultiForm. Daneben gibt es eine Reihe weiterer Eigenschaften wie PositionTop, ShowClose, ShowArrows etc., die verschiedene optische Details bestimmen. Die Eigenschaften können am einfachsten mit dem mit der Magic-Bibliothek mitgelieferten TabControl ausprobiert werden. TabPage-Klasse
Die TabPages-Aufzählung des Steuerelements verwaltet die einzelnen Dialogseiten. Beim Erzeugen eines neuen TabPage-Objekts müssen als Parameter ein Steuerelement oder Formular sowie optional ein Beschriftungstext, ein Verweis auf ein ImageList-Steuerelement sowie eine Indexnummer für das Icon neben dem Text angegeben werden. Die Vorgehensweise entspricht der beim Erzeugen eines neuen ContentObjekts für den DockingManager. Nachdem eine neue TabPage mit TabPages.Add eingefügt wurde, muss diese noch durch die Einstellung der SelectedTab-Eigenschaft ausgewählt werden. (Diese Eigenschaft verweist auf das gerade aktive Dialogblatt.)
ClosedPress-Ereignis
Das wichtigste Ereignis des TabControl-Steuerelements lautet ClosedPressed. Es wird ausgelöst, wenn der Anwender den X-Button zum Schließen eines Dialogblatts anklickt. In der Ereignisprozedur sollten Sie dann mit Remove die betreffende Dialogseite aus dem Steuerelement entfernen.
Beispiel Durch die folgenden Zeilen wird in einem ansonsten leeren Fenster ein neues TabControl-Steuerelement mit Dock=Fill eingefügt. Durch das Menükommando FENSTER|NEU wird in MenuWindowNew_Click ein neues RichTextBox-Steuerelement erzeugt und in einem TabPage-Objekt in das TabControl-Steuerelement eingefügt. tabCtrl_ClosePressed löscht das gerade aktive Dialogblatt. ' Beispiel mdi\magic-tabcontrol Friend WithEvents tabCtrl As Controls.TabControl Private Sub Form1_Load(...) Handles MyBase.Load ' TabControl einfügen tabCtrl = New Controls.TabControl() tabCtrl.Dock = DockStyle.Fill tabCtrl.Appearance = Crownwood.Magic. _ Controls.TabControl.VisualAppearance. _ MultiDocument Me.Controls.Add(tabCtrl) End Sub
110
WINFORM.fm Seite 111 Dienstag, 20. August 2002 3:48 15
Dialogblätter mit der Magic-Bibliothek
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Private Sub MenuWindowNew_Click(...) _ Handles MenuWindowNew.Click Static i As Integer Dim rtf As New RichTextBox() Dim tp As Controls.TabPage i += 1 rtf.Text = "Text in Fenster " + i.ToString tp = New Controls.TabPage("Fenster " + _ i.ToString, rtf) tabCtrl.TabPages.Add(tp) tabCtrl.SelectedTab = tp End Sub Private Sub tabCtrl_ClosePressed(...) _ Handles tabCtrl.ClosePressed Dim tp As Controls.TabPage tp = tabCtrl.SelectedTab If IsNothing(tp) Then Exit Sub tabCtrl.TabPages.Remove(tp) End Sub
Abbildung 6.6: Im Magic-TabControl werden mehrere Texte angezeigt.
Probleme Grundsätzlich ist es möglich, in einer TabPage ein ganzes Fenster (also ein in der Visual Studio .NET-Benutzeroberfläche zusammengestelltes Formular) anzuzeigen. Wenn nun aber versucht wird, in der Closed-Prozedur des TabControl-Steuerelements das dargestellte Fenster durch dessen Close-Methode zu schließen, kommt die Windows.Forms-interne Fensterverwaltung durcheinander. Es handelt sich dabei nicht um einen Fehler des TabControl-Steuerelements, sondern um einen Fehler in der Windows.Forms-Bibliothek: Wenn ein Fenster gelöscht wird, das noch den Fokus enthält, kann das Programm anschließend nicht beendet werden. Es tritt zwar kein unmittelbarer Fehler auf, aber in der Folge reagiert das Hauptfenster weder auf ein Anklicken des X-Buttons noch auf die Methode Me.Close. Sie kön-
111
WINFORM.fm Seite 112 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
nen den Fehler mit dem Beispielprogramm mdi\magic-tabcontrol-error selbst ausprobieren. Das Beispielprogramm enthält auch Beispielcode, wie dieser Fehler umgangen werden kann (indem der Fokus vorübergehend in einen unsichtbares Button gesetzt wird).
6.6 RichText-Docking-Editor Das in diesem Abschnitt vorgestellte Beispielprogramm zeigt die Realisierung einer Docking-Benutzeroberfläche für einen relativ simplen Texteditor (siehe Abbildung 6.7). Mit dem Programm können Sie beliebig viele RTF-Textdateien laden und speichern, den darin enthaltenen Text vorwärts und rückwärts durchsuchen sowie die Schriftart und -farbe einstellen. Die Textfenster werden in einem TabControl-Steuerelement angezeigt, die restlichen Bedienungselemente in Docking-Fenstern, die nach Belieben ein- und ausgeblendet werden können. In der Statuszeile werden die aktuelle Cursorposition und Fehlermeldungen angezeigt.
Abbildung 6.7: Texteditor mit Docking-Benutzeroberfläche
Einschränkungen Das Beispielprogramm stellt natürlich keine ausgereifte Anwendung dar. Es fehlen zahllose Funktionen eines typischen Textverarbeitungsprogramms, etwa zur Absatzformatierung, zur Steuerung der Zwischenablage etc. Das Programm sieht zwar hübsch aus, die Bedienung ist aber keineswegs besonders effizient; insbesondere ist eine Tastatursteuerung unmöglich.
112
WINFORM.fm Seite 113 Dienstag, 20. August 2002 3:48 15
RichText-Docking-Editor
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Das Programm leidet aber auch unter den Mängeln der RichTextBox aus der Windows.Form-Bibliothek. Das Steuerelement macht leider einen ähnlich unausgereiften Eindruck wie die erste COM-Version dieses Steuerelements (VB4). Um ein paar Beispiele zu nennen:
.NET
Essentials
Probleme des RichTextBox-Steuerelements
왘 Das SelectionFont-Objekt liefert die Schriftart des markierten Texts. Wenn ein Textbereich markiert ist, in dem gleichzeitig mehrere Schriftattribute, -größen oder -familien vorkommen, liefert SelectionFont manchmal Nothing, manchmal aber auch ein Font-Objekt mit falschen Angaben (z.B. einer vollkommen falschen Größe, meist 13). 왘 Generell ist es nicht möglich, bei einem markierten Textbereich nur ein Schriftattribut (z.B. fett oder kursiv) zu verändern, ohne auch alle anderen Schriftattribute zu verändern. 왘 Das Steuerelement kommt (wie generell alle Klassen aus den Windows.Forms- und Drawing-Bibliotheken) nur mit TrueType-Schriften zurecht, nicht aber mit PostScript-Schriften. 왘 Es gibt keine einfache Möglichkeit, den Text auszudrucken. (Der beste Weg besteht darin, den Text in die Zwischenablage zu kopieren, Microsoft Word zu starten, den Text dort einzufügen und auszudrucken.) Da diese Einschränkungen nur schwer zu umgehen sind, lohnt die Entwicklung eines professionellen Texteditors auf der Basis der RichTextBox wohl kaum.
Programmaufbau Das Programm besteht aus fünf Formularen:
왘 MainForm ist das Hauptfenster der Benutzeroberfläche. Das Formular gilt auch als Startobjekt des Programms. 왘 TextForm stellt die RichTextBox für die Texteingabe zur Verfügung. 왘 FontForm hilft bei der Einstellung der Schriftarten. 왘 ColorForm stellt eine Palette von 125 Farben zur Verfügung. 왘 SearchForm ermöglicht die Suche im Text. Die TextForm-Fenster werden innerhalb eines TabControl-Steuerelements aus der Magic-Bibliothek dargestellt, die restlichen Fenster als DockingObjekte. Im Folgenden werden aus Platzgründen nur die interessantesten Codepassagen der fünf Fenster vorgestellt.
113
WINFORM.fm Seite 114 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
MainForm Im Form Designer wurde das Formular lediglich mit einem Menü, einer ImageList mit einigen kleinen Bitmaps sowie einem OpenFileDialog ausgestattet. Alle weiteren Steuerelemente werden in MainForm_Load per Code erzeugt. Zur Verwaltung dieser Steuerelemente und der DockingObjekte gibt es einige Klassenvariablen: ' Beispiel mdi\magic-richtext ' Datei MainForm.vb ' Steuerelemente, Docking-Manager Friend StatusBar1 As StatusBar Friend WithEvents dockManag _ As Docking.DockingManager Friend WithEvents tabCtrl As Controls.TabControl ' Content-Objekte Friend searchCont, fontCont, colorCont _ As Docking.Content ' andere Fenster Friend searchFrm As SearchForm Friend fontFrm As FontForm Friend colorFrm As ColorForm ' sonstiges Private windowcounter As Integer
Initialisierung
Die Load-Ereignisprozedur fällt ziemlich umfangreich aus. Als Erstes werden das TabControl-Steuerelement und der DockingManager eingerichtet. Das TabControl-Steuerelement gilt für den DockingManager als InnerControl, d.h., es füllt den gesamten freien Raum aus, der nicht von den Docking-Fenstern beansprucht wird. Damit nach dem Programmstart sofort mit Texteingaben begonnen werden kann, wird gleich ein leeres Dokument erzeugt. Private Sub MainForm_Load(...) _ Handles MyBase.Load Dim sz As Size Dim zn As Docking.Zone Dim sbPanel As StatusBarPanel ' TabControl für die RTF-Dokumente tabCtrl = New Controls.TabControl() tabCtrl.Dock = DockStyle.Fill tabCtrl.Appearance = Crownwood.Magic. _ Controls.TabControl.VisualAppearance. _ MultiDocument Me.Controls.Add(tabCtrl) tabCtrl.BringToFront() Me.Controls(0).SendToBack() ' ein leeres Dokument anzeigen CreateNewText()
114
WINFORM.fm Seite 115 Dienstag, 20. August 2002 3:48 15
RichText-Docking-Editor
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Docking Manager einrichten dockManag = New Docking.DockingManager( _ Me, Common.VisualStyle.IDE) dockManag.InnerControl = tabCtrl
Die StatusBar muss nach dem TabControl erzeugt werden, damit die Positionierung der Docking-Fenster funktioniert. (Aus diesem Grund wird das Steuerelement per Code und nicht durch den Form Designer erzeugt.) Innerhalb der Statusbar wird ein Panel angezeigt, das die gesamte Statusbar ausfüllt. Damit der DockingManager die Statusbar nicht verschiebt, wird die Eigenschaft OuterControl entsprechend eingerichtet. ' Statusbar nach dem tabCtrl erzeugen! StatusBar1 = New StatusBar() StatusBar1.Dock = DockStyle.Bottom sbPanel = New StatusBarPanel() sbPanel.AutoSize = _ StatusBarPanelAutoSize.Spring StatusBar1.Panels.Add(sbPanel) StatusBar1.ShowPanels = True Me.Controls.Add(StatusBar1) dockManag.OuterControl = StatusBar1
Alle Menüeinträge werden über die rekursive Prozedur AddHandlerFor AllMenuItems mit einer zentralen Ereignisprozedur verbunden. Die Eigenschaften InitialDirectory, Filter und DefaultExt des OpenFileDialog werden so initialisiert, dass nur RTF-Dateien geladen werden können und als Defaultverzeichnis das persönliche Datenverzeichnis des Benutzers verwendet wird (Methode GetFolderPath).
Persönliches Verzeichnis ermitteln
AddHandlerForAllMenuItems(MainMenu1.MenuItems) ' OpenFileDialog initialisieren openDlg.InitialDirectory = _ Environment.GetFolderPath( _ Environment.SpecialFolder.Personal) openDlg.Filter = "RichText-Datei|*.rtf" openDlg.DefaultExt = "rtf"
Die drei folgenden Codeblöcke dienen dazu, die drei Bedienungsfenster als Docking-Objekte anzuzeigen. Die ersten zwei Fenster werden links angedockt, das Farbauswahlfenster unten. ' Suchformular anzeigen searchFrm = New SearchForm() searchFrm.mainFrm = Me sz = searchFrm.ClientSize searchCont = New Docking.Content( _ dockManag, searchFrm, "Suchen", _
115
WINFORM.fm Seite 116 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
ImageList1, 1) searchCont.DisplaySize = sz dockManag.Contents.Add(searchCont) dockManag.ShowContent(searchCont) ' Fontformular in derselben Docking.Zone fontFrm = New FontForm() fontFrm.mainFrm = Me sz = fontFrm.ClientSize fontCont = New Docking.Content( _ dockManag, fontFrm, "Schriftart", _ ImageList1, 2) fontCont.DisplaySize = sz zn = searchCont.ParentWindowContent. _ ParentZone dockManag.Contents.Add(fontCont) dockManag.AddContentToZone(fontCont, zn, 1) dockManag.ShowContent(fontCont) ' Color-Formular anzeigen colorFrm = New ColorForm() colorFrm.mainfrm = Me colorCont = New Docking.Content( _ dockManag, colorFrm, "Farben", _ ImageList1, 3) colorCont.DisplaySize = New Size(400, 130) dockManag.Contents.Add(colorCont) dockManag.AddContentWithState( _ colorCont, Docking.State.DockBottom) dockManag.ShowContent(colorCont) End Sub CreateNewText wird durch den Menüeintrag DATEI|NEU ausgeführt. (Der Aufruf erfolgt in der aus Platzgründen nicht abgedruckten Prozedur MenuItems_Click.) CreateNewText erzeugt ein neues TextForm-Objekt und macht dieses innerhalb einer TabPage sichtbar. Bis der Anwender den Text zum ersten Mal speichert, wird die TabPage mit Text 1, 2 etc. beschriftet. Damit im TextForm-Code zurück auf das TabPage-Objekt und auf das Hauptfenster verwiesen werden kann, werden die TextFormKlassenvariablen tp und mainFrm initialisiert. Private Sub CreateNewText() Dim txtfrm As New TextForm() Dim tp As Controls.TabPage windowcounter += 1 tp = New Controls.TabPage("Text " + _ windowcounter.ToString, txtfrm, _ ImageList1, 0) tabCtrl.TabPages.Add(tp) tabCtrl.SelectedTab = tp
116
WINFORM.fm Seite 117 Dienstag, 20. August 2002 3:48 15
RichText-Docking-Editor
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
txtfrm.RTBox1.Focus() ' Rückverweise txtfrm.tp = tp txtfrm.mainFrm = Me End Sub OpenFile wird durch DATEI|ÖFFNEN ausgeführt. Die Prozedur zeigt die
Anwendung des OpenFileDialog (Variable openDlg) zur Dateiauswahl: Der Dialog wird mit ShowDialog geöffnet. Wenn diese Methode nicht Cancel als Ergebnis liefert, kann der Dateiname aus der Eigenschaft FileName entnommen werden. Damit bei der nächsten Dateiauswahl das soeben ausgewählte Verzeichnis wieder verwendet wird, wird die Eigenschaft InitialDirectory verändert. Wenn beim Laden ein Fehler auftritt, wird die Fehlermeldung in der Statuszeile angezeigt.
OpenFileDialogSteuerelement
' vorhandene RichText-Datei öffnen Private Sub OpenFile() Dim result As DialogResult Dim filename As String Dim txtFrm As TextForm ' Datei auswählen openDlg.FileName = "" result = openDlg.ShowDialog() If result = DialogResult.Cancel Then Exit Sub End If filename = openDlg.FileName openDlg.InitialDirectory = _ IO.Path.GetFullPath(filename) ' neues Textfenster erzeugen, Datei laden CreateNewText() txtFrm = GetActiveTextForm() Try txtFrm.RTBox1.LoadFile(filename) txtFrm.RTBox1.Modified = False Catch e As Exception SetStatusText("Fehler beim Laden der " + _ "Datei: " + e.Message) End Try txtFrm.tp.Title = _ IO.Path.GetFileNameWithoutExtension( _ filename) End Sub SetStatusText ist eine Hilfsprozedur, die die Veränderung der TextEigenschaft des ersten Panel der Statuszeile erleichtert.
117
WINFORM.fm Seite 118 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Friend Sub SetStatusText(ByVal s As String) StatusBar1.Panels(0).Text = s End Sub
Eine weitere nützliche Hilfsfunktion ist GetActiveTextForm. Die Funktion liefert entweder einen Verweis auf das gerade aktive TextForm-Objekt oder Nothing. ' TextForm-Objekt des aktiven Fensters ermitteln Friend Function GetActiveTextForm() As TextForm Dim f As TextForm Dim tp As Controls.TabPage = _ tabCtrl.SelectedTab If IsNothing(tp) Then Return Nothing Return CType(tp.Control, TextForm) End Function
Die nicht abgedruckte Prozedur CloseFile dient dazu, ein Textfenster durch txtFrm.Close zu schließen. Die Prozedur ist deswegen ziemlich kompliziert, weil darin ein Windows.Form-Fehler umgangen werden muss. Dazu wird ein winziger Button erzeugt und dorthin vorübergehend der Tastaturfokus gesetzt.
TextForm Modified-Eigenschaft
Das TextForm-Formular enthält lediglich zwei Steuerelemente, die RichTextBox RTBox1 und den SaveFileDialog saveDlg, sowie drei Prozedu-
ren. TextForm_Closing wird ausgeführt, bevor das Fenster geschlossen wird. Wenn die Modified-Eigenschaft der RichTextBox den Wert True enthält, der Text also seit dem letzten Speichern geändert wurde, erfolgt vor dem Schließen eine Sicherheitsabfrage. Damit kann das Schließen abgebrochen oder der Text gespeichert werden (Prozedur SaveFile, Dateiauswahl mit einem SaveFileDialog). Wesentlich interessanter ist die Prozedur RTBox1_SelectionChanged. Das SelectionChanged-Ereignis tritt immer dann auf, wenn sich die Cursorposition innerhalb der RichTextBox ändert. Die Prozedur ändert daraufhin den Inhalt der Statusbar. Falls das FontForm-Fenster sichtbar ist, werden dort entsprechend der Textformatierung an der Cursorposition die Listenfelder und Auswahlkästchen entsprechend eingestellt. Das FontForm-Fenster zeigt damit immer die Textformatierung an, die an der Cursorposition gilt. Die Klassenvariable ignorEvents verhindert, dass das durch die Veränderung der Listenfelder bzw. Auswahlkästchen ausgelöste Changed-Ereignis zu einer Neuformatierung des Texts führt (siehe die Prozedur fntControls_Changed etwas weiter unten). Fehlerabsicherung
118
Die Fehlerabsicherung wurde eingebaut, damit es bei eventuellen Problemen mit SelectionFont zu keiner Fehlermeldung kommt. Wie oben bereits erwähnt, liefert diese Eigenschaft bisweilen widersprüchliche
WINFORM.fm Seite 119 Dienstag, 20. August 2002 3:48 15
RichText-Docking-Editor
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Ergebnisse, wenn in der RichTextBox ein Text markiert ist, dessen Teile unterschiedlich formatiert sind. Wahrscheinlich ist diese Absicherung nicht notwendig; SelectionFont liefert zwar falsche Ergebnisse, es tritt allerdings keine Exception auf. Aber sicher ist sicher! ' Beispiel mdi\magic-richtext ' Datei TextForm.vb Friend filename As String 'Dateiname ' Verweis auf die TabPage Friend tp As Controls.TabPage ' Verweis auf das Hauptfenster Friend mainFrm As MainForm Private Sub RTBox1_SelectionChanged(...) _ Handles RTBox1.SelectionChanged Dim fontFrm As FontForm = mainFrm.fontFrm Dim fnt As Font Try mainFrm.SetStatusText( _ String.Format("Zeichen {0} von {1}", _ RTBox1.SelectionStart, RTBox1.TextLength)) fnt = RTBox1.SelectionFont If fontFrm.Visible And _ Not IsNothing(fnt) Then fontFrm.ignoreEvents = True fontFrm.chkBold.Checked = fnt.Bold fontFrm.chkItalic.Checked = fnt.Italic fontFrm.chkUnder.Checked = fnt.Underline fontFrm.chkStrike.Checked = fnt.Strikeout fontFrm.cmbSize.Text = _ RTBox1.SelectionFont.Size.ToString fontFrm.cmbFont.Text = _ RTBox1.SelectionFont.FontFamily.Name fontFrm.ignoreEvents = False End If Catch ex As Exception mainFrm.SetStatusText( _ "Fehler: " + ex.Message) fontFrm.ignoreEvents = False End Try End Sub
FontForm Das FontForm-Formular zeigt die aktuelle Textformatierung an. Wenn eines der zwei Listenfelder (cmbFont oder cmbSize) bzw. eines der vier Kontrollkästchen (chkBold, chkItalic, chkStrike oder chkUnder) verändert wird, wird die Textformatierung des markierten Texts in der gerade aktiven RichTextBox geändert.
119
WINFORM.fm Seite 120 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
In FontForm_Load werden die beiden Listenfelder initialisiert. Die Liste der am Rechner verfügbaren TrueType-Schriftfamilien wird der Aufzählung FontFamily.Families entnommen. ' Beispiel mdi\magic-richtext ' Datei FontForm.vb Friend mainFrm As MainForm ' Changed- Ereignisse ignorieren Friend ignoreEvents As Boolean = False Private Sub FontForm_Load(...) _ Handles MyBase.Load ' Font-Combo initialisieren Dim ff As FontFamily For Each ff In FontFamily.Families cmbFont.Items.Add(ff.Name) ff.Dispose() Next cmbFont.Text = "" ' Size-Combo initialisieren Dim fntsizes() As Integer = _ {6, 8, 9, 10, 12, 14, 16, 18, _ 20, 24, 28, 32, 36, 48, 72} Dim i As Integer For Each i In fntsizes cmbSize.Items.Add(i.ToString) Next cmbSize.Text = "" End Sub
Eine Ereignisprozedur für sechs Steuerelemente
Wenn eines der sechs Steuerelemente durch den Anwender verändert wird, wird die zentrale Ereignisprozedur ausgeführt (beachten Sie die sechsteilige Handles-Liste!). In der Prozedur werden die sechs Steuerelemente ausgewertet und darauf basierend ein neues Font-Objekt erzeugt. Dieses wird der Eigenschaft SelectionFont der aktiven RichTextBox zugewiesen. Durch Focus wird der Cursor vom FontForm-Fenster zurück in die RichTextBox gesetzt, so dass dort weitergearbeitet werden kann. Private Sub fntControls_Changed(...) _ Handles chkBold.CheckedChanged, _ chkItalic.CheckedChanged, _ chkStrike.CheckedChanged, _ chkUnder.CheckedChanged, _ cmbSize.SelectedIndexChanged, _ cmbFont.SelectedIndexChanged Dim txtFrm As TextForm = _ mainFrm.GetActiveTextForm() Dim oldfnt As Font
120
WINFORM.fm Seite 121 Dienstag, 20. August 2002 3:48 15
RichText-Docking-Editor
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Dim fntStyle As FontStyle Dim fntSize As Single If ignoreEvents Then Exit Sub If IsNothing(txtFrm) Then Exit Sub If chkBold.Checked Then _ fntStyle = fntStyle Or FontStyle.Bold If chkItalic.Checked Then _ fntStyle = fntStyle Or FontStyle.Italic If chkUnder.Checked Then _ fntStyle = fntStyle Or FontStyle.Underline If chkStrike.Checked Then _ fntStyle = fntStyle Or FontStyle.Strikeout fntSize = Convert.ToSingle(cmbSize.Text) Try txtFrm.RTBox1.SelectionFont = _ New Font(cmbFont.Text, fntSize, fntStyle) Catch ex As Exception mainFrm.SetStatusText("Fehler: " + _ ex.Message) End Try txtFrm.RTBox1.Focus() End Sub
ColorForm ColorForm ist an sich ein leeres Fenster ohne Steuerelemente. Erst beim ersten Aufruf von ColorForm_Resize werden 125 kleine PictureBoxSteuerelemente erzeugt, mit AddRange möglichst effizient in das Fenster eingefügt und in der Resize-Prozedur entsprechend der aktuellen Fenstergröße auf 5 bis 25 Zeilen verteilt. Wenn eines der PictureBox-Steuerelemente angeklickt wird, kommt es zum Aufruf von pic_Click, wo die Farbe des markierten Texts in der RichTextBox geändert wird. Die folgenden Prozeduren sind ein gutes Beispiel für die Möglichkeiten, die dynamisch erzeugte Steuerelemente bieten! ' Beispiel mdi\magic-richtext ' Datei ColorForm.vb Const picSize As Integer = 12 Const picBorder As Integer = 6 Private init As Boolean = False Private pic(5 * 5 * 5 - 1) As PictureBox Friend mainfrm As MainForm
' 125 PictureBoxes zur Farbauswahl erzeugen Private Sub CreatePictureBoxes() Dim r, g, b, i As Integer Dim n() As Integer = {255, 192, 128, 64, 0} For r = 0 To 4
121
WINFORM.fm Seite 122 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
For g = 0 To 4 For b = 0 To 4 i = b + g * 5 + r * 25 pic(i) = New PictureBox() pic(i).Size = New Size(picSize, picSize) pic(i).BorderStyle = _ BorderStyle.FixedSingle pic(i).BackColor = _ Color.FromArgb(n(r), n(g), n(b)) AddHandler pic(i).Click, _ AddressOf pic_Click Next Next Next Me.Controls.AddRange(pic) init = True End Sub ' die PictureBoxes entsprechend der ' Fenstergröße anordnen Private Sub ColorForm_Resize(...) _ Handles MyBase.Resize Dim r, g, b As Integer Dim i, row, col As Integer If init = False Then CreatePictureBoxes() For r = 0 To 4 col = 0 For g = 0 To 4 For b = 0 To 4 i = b + g * 5 + r * 25 If picBorder + (picBorder + picSize) _ * (col + 1) > Me.ClientSize.Width _ And col > 4 Then col = 0 row += 1 End If pic(i).Location = New Point( _ picBorder + _ (picBorder + picSize) * col, _ picBorder + _ (picBorder + picSize) * row) col += 1 Next Next row += 1 Next End Sub
122
WINFORM.fm Seite 123 Dienstag, 20. August 2002 3:48 15
RichText-Docking-Editor
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' RTF-Text neu einfärben Private Sub pic_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Dim txtFrm As TextForm = _ mainfrm.GetActiveTextForm() If IsNothing(txtFrm) Then Exit Sub txtFrm.RTBox1.SelectionColor = _ CType(sender, PictureBox).BackColor txtFrm.Focus() End Sub
SearchForm Das SearchForm-Fenster besteht aus einem Textfeld und zwei Buttons. Die Click-Prozeduren der Buttons suchen in der aktiven RichTextBox nach dem Suchtext aus dem Textfeld. Die Prozeduren demonstrieren in erster Linie die korrekte Anwendung der Find-Methode der RichTextBox.
Find-Methode
Gerade bei der Rückwärtssuche ist das deswegen nicht ganz trivial, weil die Methode per Default immer vom Ende des Texts (und nicht von der aktuellen Cursorposition) an sucht. Daher muss nicht nur der Startpunkt, sondern auch der Endpunkt der Suche durch die optionalen Find-Parameter angegeben werden. Wenn der Suchtext gefunden wird, wird der Text innerhalb der RichTextBox markiert und der Fokus wieder in die RichTextBox gesetzt.
Rückwärtssuche
' Beispiel mdi\magic-richtext ' Datei SearchForm.vb Private Sub btnSearchForward_Click(...) _ Handles btnSearchForward.Click Dim txtFrm As TextForm = _ mainFrm.GetActiveTextForm() Dim rtf As RichTextBox Dim pos As Integer If IsNothing(txtFrm) Then Exit Sub If txtSearch.Text = "" Then Exit Sub rtf = txtFrm.RTBox1 pos = rtf.SelectionStart + 1 If pos > rtf.TextLength Then _ pos = rtf.TextLength – 1 pos = rtf.Find(txtSearch.Text, pos, _ RichTextBoxFinds.None) If pos > 0 Then rtf.Select(pos, Len(txtSearch.Text)) Else mainFrm.SetStatusText("Text nicht gefunden") End If rtf.Focus() End Sub
123
WINFORM.fm Seite 124 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
6 MDI- und Docking-Anwendungen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Private Sub btnSearchBackward_Click(...) ... ... wie oben pos = rtf.SelectionStart – 1 If pos < 0 Then pos = 0 pos = rtf.Find(txtSearch.Text, _ 0, pos, RichTextBoxFinds.Reverse) If pos > 0 Then ... wie oben End Sub
124
WINFORM.fm Seite 125 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7
Multithreading
Durch Multithreading können Sie mehrere Teile Ihres Programms quasi parallel ausführen. Bei Windows-Anwendungen wird Multithreading vor allem dazu eingesetzt, um aufwändige Operationen im Hintergrund durchzuführen, ohne die Benutzeroberfläche des Programms zu blockieren. Dieses Kapitel beschreibt kurz einige allgemeine Grundlagen der Multithreading-Programmierung, geht dann auf die Besonderheiten ein, die speziell bei Windows.Forms-Anwendungen gelten, und gibt schließlich zwei konkrete Beispiele.
7.1 Grundlagen Ein Thread ist ein Teilprozess eines Programms. Ein gewöhnliches Programm besteht aus nur einem einzigen Thread zur Ausführung des Programms. Multithreading bedeutet, die Programmausführung auf mehrere Threads zu verteilen; diese Threads werden dann quasi parallel ausgeführt. Wie parallel die Ausführung wirklich ist, hängt auch von Ihrer Hardware ab. Auf einem gewöhnlichen PC mit einer CPU kann immer nur ein Thread ausgeführt werden. Bei einem Multithreading-Programm wechselt das Betriebssystem aber automatisch alle paar Millisekunden zwischen den Threads, so dass der Eindruck der Gleichzeitigkeit entsteht. Nur auf einem Rechner mit mehreren CPUs ist es theoretisch möglich, dass zwei Threads wirklich gleichzeitig ausgeführt werden. Neben den von Ihnen verwalteten Threads gibt es noch weitere Threads, die von den .NET-Bibliotheken benötigt werden. Diese dienen unter anderem dazu, regelmäßig den Speicher von nicht mehr benötigten Objekten durch eine garbage collection freizugeben. Auf die durch .NET vorgegebenen Threads haben Sie im Regelfall keinen direkten Einfluss. Wenn in diesem Abschnitt von Threads die Rede ist, sind deshalb nur die vom Hauptprogramm verwalteten Threads gemeint.
.NET-interne Threads
Neue Threads starten Fast alle Klassen zur Verwaltung von Multithreading-Anwendungen befinden sich im Namensraum System.Threading der Standardbibliothek mscorlib.dll. Um eine Prozedur in einem neuen Thread auszuführen, müssen Sie zuerst ein neues Thread-Objekt erzeugen. Dabei geben Sie als Parameter die Adresse der auszuführenden Prozedur oder Methode an. Die Prozedur (hier myprocedure) darf keine Parameter haben. Funktionen bzw. Methoden mit einem Rückgabewert sind ebenfalls nicht zulässig. Anschließend führen Sie für das Objekt die Methode Start aus.
Thread-Klasse
125
WINFORM.fm Seite 126 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Dim mythread As _ New Threading.Thread(AddressOf myprocedure) mythread.Start() Start übergibt den neuen Thread an das Betriebssystem. Dieses
bestimmt, wann der Thread tatsächlich gestartet wird. (Das ist meist nicht sofort der Fall, sondern erst nach ein paar Millisekunden.) Der neue Thread läuft nun parallel zum Hauptprogramm, d.h., die Programmausführung wechselt alle paar Millisekunden zwischen dem Hauptprogramm und der Prozedur myprocedure. Damit verlangsamt sich natürlich sowohl das Hauptprogramm als auch die Prozedur, weil die beiden Programmteile sich ja nun die verfügbare CPU-Zeit teilen müssen. Wenn einer der beiden Programmteile vorübergehend blockiert ist (etwa weil er auf das Öffnen einer Datei, die Herstellung einer Datenbankverbindung oder eine Benutzereingabe wartet), wird der andere Programmteil während dieser Zeit fast ungehindert ausgeführt.
Periodischer Prozeduraufruf in einem eigenen Thread TimerCallback-Klasse
Anstatt eine Prozedur einmal in einem eigenen Thread zu starten, besteht auch die Möglichkeit, dies periodisch zu tun. Das ist vor allem dann sinnvoll, wenn die Prozedur eine Kontroll- oder Protokollierungsaufgabe übernehmen soll, die sehr rasch erledigt werden kann. Dazu müssen Sie zuerst ein Objekt der Delegate-Klasse TimerCallback erzeugen und dabei die Adresse der aufzurufenden Prozedur oder Methode angeben. Dim mytimerDelegate As _ New Threading.TimerCallback(AddressOf method1)
Timer-Klasse
Um die periodischen Aufrufe zu starten, erzeugen Sie ein Objekt der Timer-Klasse. Mit state wird ein beliebiges Objekt (oder Nothing) ange-
geben, das bei jedem Aufruf an die Prozedur oder Methode übergeben wird. n1 gibt an, nach wie vielen Millisekunden die Prozedur zum ersten Mal ausgeführt werden soll. (0 bedeutet, so schnell wie möglich.) n2 gibt das Intervall an, alle wie viel Millisekunden die Prozedur aufgerufen werden soll. Die Einstellungen n1 und n2 können später durch die Change-Methode des Timer-Objekts verändert werden. Dim tm As _ New Threading.Timer( _ mytimerDelegate, state, n1, n2)
Die aufzurufende Methode muss folgendermaßen deklariert werden. (Achten Sie auf die Parameterliste! Anders als bei einfachen ThreadAufrufen ist hier die Übergabe eines Objekts zwingend vorgesehen.)
126
WINFORM.fm Seite 127 Dienstag, 20. August 2002 3:48 15
Grundlagen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Sub method1(ByVal state As Object)
Kommunikation zwischen Hauptprogramm und Thread An die mit mythread.Start() gestartete Prozedur oder Methode können keine Parameter übergeben werden. Ebenso wenig kann die Prozedur nicht (wie eine Funktion) ein Ergebnis an das Hauptprogramm zurückliefern. Daher müssen andere Wege zur Kommunikation gesucht werden. Der einfachste Weg, an einen Thread Daten zu übergeben und später im Hauptprogramm Ergebnisse zu empfangen, bietet die Definition einer eigenen Klasse: Um eine Operation in einem neuen Thread zu starten, erzeugen Sie zuerst ein Objekt dieser Klasse und übergeben die Startdaten mittels Klassenvariablen oder Eigenschaften und Methoden. Dann verwenden Sie den neuen Thread, um eine Methode der Klasse auszuführen. Am Ende der Methode erfolgt die Rückmeldung an das Hauptprogramm über eine Ereignisprozedur.
Thread-Ausführung vorübergehend unterbrechen Wenn Sie den aktuellen Thread für einige Zeit unterbrechen möchten, können Sie dazu die Sleep-Methode verwenden. Als Parameter geben Sie die gewünschte Zeit in Millisekunden an.
Sleep-Methode
' 5 Sekunden warten Threading.Thread.Sleep(5000) Sleep kann auch in gewöhnlichen (Single-Threaded-)Programmen ver-
wendet werden und bewirkt dann, dass das gesamte Programm während der angegebenen Zeit ruht. Sleep ist auf jeden Fall einer Warteschleifen vorzuziehen, weil es keine CPU-Zeit verbraucht und stattdessen die Rechenzeit anderen Threads bzw. anderen Programmen des Computers zur Verfügung stellt. Beachten Sie, dass Sleep immer für den gerade aktiven Thread ausgeführt wird, unabhängig davon, auf welches Objekt die Sleep-Methode angewendet wird!
Auf das Ende eines anderen Threads warten Wenn Sie im aktuellen Code darauf warten möchten oder müssen, bis ein anderer Thread mythread zu Ende ist, führen Sie mythread.Join() aus.
Join-Methode
Threads beenden oder abbrechen Ein Thread endet automatisch, wenn das Ende der gestarteten Prozedur oder Methode erreicht wird. Um den aktuell laufenden Thread zu beenden, müssen Sie also nur Exit Sub ausführen.
Abort-Methode
127
WINFORM.fm Seite 128 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
ThreadAbort Exception
Von außen kann ein Thread durch mythread.Abort() beendet werden. Abort bewirkt, dass im betroffenen Thread eine ThreadAbortException
ausgelöst wird. An die Abort-Methode kann optional ein beliebiges Objekt übergeben werden, das bei der Auswertung des ThreadAbortException-Objekts aus der Eigenschaft ExceptionState entnommen werden kann. Das Objekt kann beispielsweise dazu dienen, den Thread über die Gründe des Abbruchs zu informieren. Die Reaktion eines Threads auf eine ThreadAbortException ist aus mehreren Gründen ungewöhnlich:
왘 Wenn der Code des Threads nicht abgesichert ist (wenn es also keine Try-Catch-Konstruktion gibt), wird der Thread ohne Fehlermeldung beendet. Das Gesamtprogramm wird fortgesetzt (sofern es noch andere Vordergrund-Threads gibt). 왘 Die ThreadAbortException kann wie jede andere Exception durch TryCatch abgefangen werden. Am Ende der Try-Konstruktion wird der Fehler aber neuerlich ausgelöst und der Thread somit trotz der TryKonstruktion beendet. Sie können also ein geordnetes Ende der Prozedur erreichen, offene Ressourcen schließen etc., aber im Gegensatz zu gewöhnlichen Exceptions gilt der Fehler durch die TryKonstruktion nicht als behoben. Wenn Sie einen Thread trotz dieses merkwürdigen Verhaltens nach einer ThreadAbortException fortsetzen möchten, müssen Sie innerhalb des Catch-Blocks die Methode Threading.Thread.CurrentThread.Reset Abort() ausführen.
왘 Der Finally-Block einer Try-Konstruktion wird auf jeden Fall ausgeführt, selbst dann, wenn die ThreadAbortException nicht durch Catch abgefangen wird (d.h., wenn es in der Try-Konstruktion gar keinen Catch-Block gibt oder wenn es nur Catch-Blöcke für andere Fehler gibt). Beachten Sie, dass ResetAbort im Finally-Block nicht mehr wirksam ist. Wenn der Code eines Threads Dispose-Objekte erzeugt, Datenbankverbindungen herstellt, Dateien öffnet etc., sollte (zumindest) eine Fehlerabsicherung in der folgenden Form vorliegen: Sub method1() Try ... der eigentliche Code Finally ... Aufräumarbeiten, die in jedem Fall ausgeführt werden (auch dann, wenn eine ThreadAbortException auftritt) End Try End Sub
128
WINFORM.fm Seite 129 Dienstag, 20. August 2002 3:48 15
Grundlagen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Synchronisierung von Threads Wenn mehrere Threads auf gemeinsame Daten zugreifen müssen und zumindest einer der Threads die Daten auch verändern muss, sind Probleme im wörtlichen Sinne vorprogrammiert. Es kann vorkommen, dass ein Thread unterbrochen wird, während er die Daten ändert und nun ein anderer Thread die unvollständig veränderten Daten zu lesen versucht: Das Ergebnis sind korrupte Daten (die meist unmittelbar zu Fehlern führen) oder falsche Daten (was viel schlimmer ist, weil dieser Fall oft lange unbemerkt bleibt). Beachten Sie, dass der Thread-Wechsel vom Betriebssystem durchgeführt wird und zu jedem Zeitpunkt erfolgen kann, selbst während einer ganz elementaren Operation (z.B. x += 1)! Um derartige Probleme zu vermeiden, müssen die Threads synchronisiert werden. Das bedeutet, dass ein Thread mit dem Zugriff auf gemeinsame Daten warten muss, bis die anderen Threads fertig sind. Den einfachsten Weg bietet hierfür das Visual Basic .NET-Konstrukt SyncLock: Damit wird ein Objekt angegeben, auf das ein Programmteil den alleinigen Zugriff beansprucht. Wenn das Objekt bei der Ausführung von SyncLock bereits durch einen anderen Thread blockiert ist, muss der aktuelle Thread warten, bis das Objekt wieder freigegeben wird. Sobald das Objekt frei ist, erhält der aktuelle Thread den alleinigen Zugriff auf das Objekt. Nun müssen also alle anderen Threads warten.
SyncLock
SyncLock data ... Code, der das Objekt data verändert End SyncLock
Mit SyncLock muss ein Objekt eines Referenztyps angegeben werden. ValueType-Variablen für elementare Datentypen wie Integer oder für Strukturen sind also ungeeignet. String-Variablen sind nur dann geeignet, wenn die Zeichenkette nicht geändert wird. (Bei jeder Veränderung wird ein neues String-Objekt erzeugt, womit der Schutz hinfällig wird.) Mit SyncLock kann nur ein Objekt angegeben werden. Wenn innerhalb der SyncLock-Codes mehrere Objekte bearbeitet bzw. verändert werden, dann müssen sich alle Codeteile auf ein gemeinsames Objekt für Sync Lock einigen. (SyncLock ist ja kein Schutz gegen Veränderungen, sondern ein Schutz gegen die gleichzeitige Ausführung von Code. Insofern ist es ganz egal, welches Objekt mit SyncLock angegeben wird, solange es nur für alle Threads dasselbe ist.)
129
WINFORM.fm Seite 130 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
7.2 Windows-Multithreading Sonderregeln bei Windows-Programmen
Multithreading in Windows.Forms-Anwendungen ist leider ein wenig komplizierter als bei Konsolenanwendungen oder bei der Programmierung einer allgemeinen Klasse. Das Problem besteht darin, dass ein Form-Objekt (also das Fenster) mit allen darin enthaltenen Steuerelementen in einem STA-Thread läuft (singlethread apartment). Für diesen Thread gelten spezielle Regeln:
왘 Methoden und Eigenschaften des Form-Objekts und aller darin enthaltenen Steuerelemente dürfen ausschließlich von dem Thread verwendet werden, in dem das Fenster erzeugt wurde (nicht von selbst gestarteten Threads). 왘 SyncLock darf nicht dazu verwendet werden, um den Windows.FormsThread zu synchronisieren, weil daraus deadlocks resultieren können. Sie dürfen SyncLock also insbesondere nicht auf Steuerelemente oder Formulare anwenden. Um es ganz klar zu machen: Sie dürfen in Ihren selbst gestarteten (zusätzlichen) Threads keine Eigenschaften oder Methoden von Formularen oder Steuerelementen direkt nutzen! Wenn Sie es dennoch tun, kommt es bei manchen Steuerelementen (z.B. TreeView) sofort zu einem Fehler. Bei den meisten anderen Steuerelemente tritt ein Fehler aber nur dann auf, wenn die beiden Threads (also der Windows.Forms-Thread und Ihr eigener Thread) zugleich auf gemeinsame Daten zugreifen. Je nach Anwendung tritt dieser Fall sehr selten auf, was die Sache aber nur noch unangenehmer macht: Sie erhalten ein Programm, das bei Ihren eigenen Tests vielleicht gut funktioniert, das beim Anwender aber hin und wieder mit merkwürdigen Fehlermeldungen abstürzt.
Zugriff auf Methoden, Eigenschaften und Ereignisse des Formulars Invoke-Methoden
Die einzige Möglichkeit, Methoden oder Eigenschaften eines Formulars oder Steuerelements innerhalb eines eigenen Threads zu nutzen, geben die Methoden Invoke bzw. BeginInvoke. Die Methoden stehen für die Form-Klasse und für alle Steuerelemente zur Verfügung. Sie starten synchron (Invoke) bzw. asynchron (BeginInvoke) Prozeduren, die dann innerhalb des Windows.Forms-Threads ausgeführt werden. Soweit der Thread Ereignisse ausgelöst, in deren Ereignisprozeduren auf das Fenster oder dessen Steuerelemente zugegriffen werden soll, müssen auch diese Ereignisse mit [Begin]Invoke ausgelöst werden! Intern kommt bei der Ausführung von [Begin]Invoke der so genannte marshalling-Mechanismus zur Anwendung, der es ermöglicht, Methoden anderer Prozesse oder Threads aufzurufen und dabei alle erforderlichen Daten zwischen den Prozessen oder Threads zu übertragen.
130
WINFORM.fm Seite 131 Dienstag, 20. August 2002 3:48 15
Windows-Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Die Verwendung von [Begin]Invoke ist umständlich, langsam (vor allem, wenn die Methode wiederholt ausgeführt wird) und kann in ungünstigen Fällen zu so genannten deadlocks führen. Das bedeutet, dass sich mehrere Threads gegenseitig blockieren und keiner mehr fortgesetzt werden kann. Aus diesem Grund eignet sich Multithreading vor allem für solche Szenarien, in denen ein eigener Thread eine relativ zeitaufwändige Aufgabe vollkommen losgelöst von der Benutzeroberfläche durchführen kann; erst wenn die Aufgabe erledigt ist, wird das Ergebnis im Fenster bzw. in einem seiner Steuerelemente angezeigt. Schlecht geeignet sind dagegen Aufgaben, bei denen der Thread ununterbrochen (z.B. in einer Schleife) auf die Eigenschaften oder Methoden von Steuerelementen zugreifen muss. In solchen Fällen ist es meist zielführender, die Aufgabe in herkömmlicher Weise im aktuellen Thread auszuführen und durch regelmäßige DoEvents eine Unterbrechung zuzulassen. Manchmal besteht auch die Möglichkeit, den Code für die skizzierten Anforderungen zu optimieren: Beispielsweise ist es besser, zuerst eine Sammlung neuer Listeneinträge zu bilden und diese dann alle gemeinsam mit AddRange in ein Listenfeld einzufügen, als jeden Eintrag gesondert mit Add einzufügen (was nur durch zahllose Invoke-Aufrufe möglich ist).
Invoke Mit der Methode Invoke können Sie eine beliebige Prozedur oder Methode aufrufen. Der Code wird innerhalb des Threads des Formulars ausgeführt, d.h., zur Ausführung muss ein Thread-Wechsel durchgeführt werden. Beachten Sie, dass die Ausführung der Methode im Windows-Thread erst dann erfolgt, wenn dieser dazu Zeit hat, also nicht mit der Abarbeitung bereits laufender (Ereignis-)Prozeduren beschäftigt ist! Diese durch den Code des Hauptprogramms verursachten Verzögerungen können den Multithreading-Code sehr ineffizient machen und in ungünstigen Fällen zu Deadlocks führen.
Achtung – Deadlocks!
Um es nochmals klarzustellen: Die mit Invoke ausgeführte Prozedur wird nicht im neuen Thread, sondern im Thread des Hauptprogramms (also im Windows.Forms-Thread ausgeführt). Da der Sinn von Multithreading im Regelfall darin besteht, Code möglichst unabhängig vom Haupt-Thread auszuführen, sollte die durch Invoke ausgeführte Prozedur möglichst wenig Code enthalten und möglichst rasch beendet werden! An Invoke muss ein Delegate-Objekt übergeben werden. (Eine direkte Adresseangabe der Prozedur ist leider nicht möglich.) Delegates sind – vereinfacht ausgedrückt – Klassen, deren Objekte Zeiger auf Methoden
131
WINFORM.fm Seite 132 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
oder Prozeduren enthalten. Im Folgenden veranschaulichen drei Muster die Vorgehensweise zur Deklaration und Initialisierung des Delegate-Objekts und des nachfolgenden Aufrufs der Prozedur oder Methode. Beispielprogramm
Ein hier aus Platzgründen nicht abgedrucktes Beispielprogramm zur Demonstration der unterschiedlichen [Begin]Invoke-Varianten finden Sie im Verzeichnis multithread\invoke-test.
Invoke-Aufruf einer Prozedur ohne Parameter Delegate-Klasse für Prozeduren ohne Parameter
Dazu müssen Sie mit Delegate eine neue Delegate-Klasse für Methoden ohne Parameter definieren. (Diese Definition muss auf Datei-, Moduloder Klassenebene erfolgen. Delegate kann nicht innerhalb einer Prozedur verwendet werden.) Als Nächstes müssen Sie ein Objekt der Delegate-Klasse mit der Adresse der aufzurufenden Methode bzw. Prozedur initialisieren (mymethod0 im folgenden Codegerüst). Dieses Objekt können Sie nun an Invoke übergeben. Invoke kann auf ein beliebiges Steuerelement oder Formular angewendet werden. Fall sich Invoke im Code der Formularklasse befindet, muss gar kein Objekt angegeben werden. (Invoke bezieht sich dann automatisch auf Me.) Die auszuführende Prozedur (do_it0 im Codegerüst) kann beliebigen Code zur Bearbeitung aller Steuerelemente enthalten, die sich im selben Thread befinden wie das Objekt, auf das Invoke angewendet wurde. Wenn Invoke also auf ein Steuerelement oder direkt auf ein Formular angewendet wird, können in der Prozedur alle Steuerelemente des Fensters angesprochen werden – und in der Regel auch alle Steuerelemente aller anderen Fenster des Programms (es sei denn, die Fenster wurden in unterschiedlichen Threads geöffnet). ' Deklaration einer Delegate-Klasse für ' Methoden ohne Parameter Delegate Sub mydelegate0() ' Aufruf einer Methode ohne Parameter Dim mymethod0 As _ New mydelegate0(AddressOf do_it0) myform.Invoke(mymethod0) ' die aufzurufende Prozedur oder Methode Sub do_it0() ... Code zur Bearbeitung aller Steuerelemente des Formulars End Sub
132
WINFORM.fm Seite 133 Dienstag, 20. August 2002 3:48 15
Windows-Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Invoke-Aufruf einer Funktion mit Rückgabewert Wenn Sie eine Methode mit Rückgabewert aufrufen möchten, müssen Sie die Delegate-Klasse als Function deklarieren. Invoke liefert den Rückgabewert der aufgerufenen Funktion als Object zurück, d.h., Sie müssen anschließend in der Regel noch eine Typumwandlung durchführen (CStr, CType etc.). Die folgenden Zeilen geben ein Muster:
Delegate-Klasse für Rückgabewert
' Deklaration einer Delegate-Klasse für ' Funktionen ohne Parameter Delegate Function mydelegate1() As String ' Aufruf einer Funktion mit String-Rückgabe Dim s As String Dim mymethod1 As _ New mydelegate0(AddressOf do_it1) s = CStr(myform.Invoke(mymethod1)) die aufzurufende Prozedur oder Methode unction do_it1() As String ... Code zur Bearbeitung aller Steuerelemente des Formulars Return "ein Ergebnis" End Sub
Invoke-Aufruf einer Prozedur mit Parametern Ein bisschen komplizierter wird es, wenn Sie an die Prozedur Parameter übergeben möchten. Die erste Voraussetzung hierfür besteht darin, dass Sie in der Delegate-Klasse die erforderlichen Parameter angeben. An die Invoke-Methode müssen Sie alle Parameter in Form eines ObjectFelds übergeben. Invoke übergibt das erste Feldelement an den ersten Parameter der Prozedur, das zweite an den zweiten Parameter etc. Dabei erfolgt eine Typkontrolle, d.h., wenn die Daten im Feld nicht mit den Parametertypen laut Delegate übereinstimmen, kommt es zu einer Fehlermeldung. Außerdem muss die Größe des Felds mit der Anzahl der Parameter übereinstimmen.
Delegate-Klasse für Parameter
Im folgenden Codegerüst wird ein Delegate für eine Prozedur definiert, die einen Integer- und einen String-Parameter erwartet. Die Übergabe der Parameter erfolgt durch das Feld obj, dessen beiden Elemente vor dem Aufruf initialisiert werden. Dieses Feld wird dann als zweiter Parameter an die Invoke-Methode übergeben, die sich darum kümmert, den Inhalt an die aufzurufende Prozedur do_it2 weiterzugeben.
133
WINFORM.fm Seite 134 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
' Deklaration einer Delegate-Klasse für Methoden ' mit einem Integer- und einem String-Parameter Delegate Sub mydelegate2(ByVal n As Integer, _ ByVal s As String) ' Aufruf einer Prozedur mit zwei Parametern Dim mymethod2 As _ New mydelegate2(AddressOf do_it2) Dim obj(1) As Object 'Übergabe der Parameter obj(0) = 123 obj(1) = "abc" Invoke(mymethod2, obj) ' die aufzurufende Prozedur oder Methode Sub do_it2(ByVal n As Integer, _ ByVal s As String) ... Code zur Bearbeitung aller Steuerelemente des Formulars End Sub
BeginInvoke und EndInvoke Asynchroner Aufruf
Invoke wird (wie alle gewöhnlichen Methoden oder Eigenschaften) synchron ausgeführt. Das bedeutet, dass die nächste Anweisung nach Invoke erst nach dem Abschluss der durch Invoke aufgerufenen Prozedur ausgeführt wird. Je nachdem, ob der Haupt-Thread des WindowsProgramms gerade beschäftigt ist, kann es daher eine Weile dauern, bis Ihr eigener Thread fortgesetzt werden kann.
IAsyncResultSchnittstelle
Um diese Wartezeit zu vermeiden und eine möglichst hohe Parallelität des Codes zu erzielen (was ja oft die Motiviation einer MultithreadingAnwendung ist), können Sie die Prozedur mit BeginInvoke auch asynchron starten. BeginInvoke liefert sofort ein Objekt der Schnittstelle IAsyncResult zurück. Über dieses Objekt können Sie feststellen (Eigenschaft IsCompleted), ob die aufgerufene Prozedur schon abgeschlossen ist. Um den Programmfluss wieder zu synchronisieren, führen Sie EndInvoke aus, wobei Sie das IAsyncResult-Objekt übergeben. EndInvoke wartet, bis die Prozedur tatsächlich zu Ende ist und liefert bei Funktionen den Rückgabewert. Formal erfolgt der Aufruf von BeginInvoke genau gleich wie bei Invoke, d.h., Sie müssen zuerst eine Delegate-Klasse deklarieren und dann ein Objekt dieser Klasse mit der aufzurufenden Prozedur initialisieren. Dieses Objekt (eventuell samt einem Object-Feld für die Parameter) übergeben Sie an BeginInvoke. Die folgenden Zeilen zeigen die Anwendung von Begin- und EndInvoke für eine Prozedur ohne Parameter und ohne Rückgabewert.
134
WINFORM.fm Seite 135 Dienstag, 20. August 2002 3:48 15
Windows-Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Deklaration einer Delegate-Klasse für Methoden ' ohne Parameter Delegate Sub mydelegate3() ' Aufruf einer Methode ohne Parameter Dim result As IasyncResult Dim mymethod3 As _ New mydelegate0(AddressOf do_it3) result = mycontrol.BeginInvoke(mymethod3) ... ' beliebiger Code, der parallel zu mymethod3 ' ausgeführt werden kann mycontrol.EndInvoke(result) ' die aufzurufende Prozedur oder Methode Sub do_it3() ... Code zur Bearbeitung aller Steuerelemente des Formulars End Sub
Programmende Ein Windows-Programm endet automatisch, wenn das Startfenster geschlossen wird. Das bedeutet aber nicht, dass auch ein von diesem Fenster aus gestarteter Thread endet! Dieser läuft im Gegenteil unbegrenzt weiter, auch wenn es die Benutzeroberfläche gar nicht mehr gibt. Nur wenn der Thread auf ein Element des Fensters zuzugreifen versucht (über Invoke), tritt ein Fehler auf. Um zu vermeiden, dass einzelne Threads länger laufen als das Hauptprogramm, sollten Sie diese Threads unbedingt explizit in der ClosingEreignisprozedur des Fensters durch Abort und Join beenden. (Beachten Sie, dass die Abort-Methode nur zulässig ist, wenn das Programm in der höchsten .NET-Sicherheitsstufe läuft, also direkt von der lokalen Festplatte gestartet wurde. Sichern Sie den Aufruf entsprechend ab!) Private Sub Form1_Closing(...) _ Handles MyBase.Closing If mythread.IsAlive Then mythread.Abort() 'Thread zum Beenden 'auffordern mythread.Join() 'warten, bis der Thread 'wirklich beendet wurde End If End Sub
Die andere Variante besteht darin, den Thread beim Erzeugen als Hintergrund-Thread zu kennzeichnen (mythread.IsBackground = True). Damit wird der Thread beim Programmende automatisch beendet. (Beachten Sie aber, dass es dennoch zu einer Fehlermeldung kommen kann, wenn der Thread gerade in der Phase, in der das Formular aus dem Speicher entfernt wird, der Fenster-Thread aber noch läuft, Invoke ausführt.)
135
WINFORM.fm Seite 136 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
7.3 Verzeichniseigenschaften in einem eigenen Thread ermitteln Bei dem in Abbildung 7.1 dargestellten Beispielprogramm können Sie im hierarchischen Listenfeld ein Verzeichnis auswählen. Wenn Sie anschließend den Button EIGENSCHAFTEN anklicken, wird in einem neuen Thread die Anzahl aller Dateien und Unterverzeichnisse und der gesamte Platzbedarf ermittelt. Während diese Daten ermittelt werden, wird der Button grau angezeigt (Enabled=False). Sobald die Endergebnisse vorliegen, verschwindet der Button ganz. Das Programm merkt sich die Ergebnisse für das betroffene Verzeichnisse, so dass diese bei einem nochmaligen Auswählen des Verzeichnisses sofort zur Verfügung stehen. Da die Verzeichniseigenschaften in einem eigenen Thread ermittelt werden, kann das Programm uneingeschränkt weiterverwendet werden, etwa um ein anderes Verzeichnis im Verzeichnisbaum auszuwählen. Wenn die Eigenschaften eines neuen Verzeichnisses ermittelt werden sollen, bevor die Berechnung der Eigenschaften des vorigen Verzeichnisses abgeschlossen ist, wird der noch laufende Thread abgebrochen.
Abbildung 7.1: Die Verzeichniseigenschaften werden in einem eigenen Thread ermittelt.
Zur Ermittlung der Verzeichniseigenschaften wird die im Programm definierte Klasse dirinfo eingesetzt. Deren Methode GetProperties wird in einem eigenen Thread ausgeführt. Die Klasse kennt zwei Ereignisse: update wird regelmäßig aufgerufen und ermöglicht eine Aktualisierung des Fensters, während das Verzeichnis durchlaufen wird. done gibt an, dass die endgültigen Daten vorliegen. (Damit demonstiert dieses Beispiel also auch den Aufruf von Ereignissen aus einem eigenen Thread.)
136
WINFORM.fm Seite 137 Dienstag, 20. August 2002 3:48 15
Verzeichniseigenschaften in einem eigenen Thread ermitteln
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Programmcode des Hauptfensters Das TreeView-Steuerelement wird in Form1_Load mit den Laufwerksbuchstaben initialisiert. Bei einem Doppelklick innerhalb des Steuerelements kommt es in TreeView1_DoubleClick zu einem Aufruf von ReadDirectories, um die Verzeichnishierarchie um einige Unterverzeichnisse zu ergänzen. Der Code entspricht dem Beispiel aus Abschnitt 3.3. Auf Formularebene sind zwei Klassenvariablen definiert: details_tn zeigt auf das TreeNode-Objekt, dessen Eigenschaften momentan im rechten Teil des Fensters angezeigt werden. dinfothread verweist auf den Thread zur Ausführung der dirinfo.GetProperties-Methode. ' Beispiel multithread\dir-properties Dim details_tn As TreeNode Dim dinfothread As Threading.Thread
Die Ereignisprozedur TreeView1_AfterSelect wird immer dann aufgerufen, wenn im Listenfeld durch einen einfachen Mausklick ein Verzeichnis ausgewählt wurde. Die Prozedur speichert dann das ausgewählte TreeNode-Element in details_tn und zeigt die verfügbaren Eigenschaften an. (Bereits zu einem früheren Zeitpunkt ermittelte Eigenschaften können mit tn.Tag aus einem dirinfo-Objekt gelesen werden.) Private Sub TreeView1_AfterSelect(...) _ Handles TreeView1.AfterSelect Dim tn As TreeNode = TreeView1.SelectedNode ' falls gültige Auswahl If Not IsNothing(tn) Then details_tn = tn lblDirName.Text = "Verzeichnis: " + _ Replace(tn.FullPath, "\\", "\") If IsNothing(tn.Tag) Then lblFiles.Text = "Dateien: ???" lblDirectories.Text = "Verzeichnisse: ???" lblSize.Text = "Platzbedarf: ???" btnProperties.Visible = True btnProperties.Enabled = True Else ShowTreenodeDetails(tn) End If End If End Sub Private Sub ShowTreenodeDetails( _ ByVal tn As TreeNode) Dim di As dirinfo = CType(tn.Tag, dirinfo) lblFiles.Text = "Dateien: " + _ di.files.ToString lblDirectories.Text = "Verzeichnisse: " + _
137
WINFORM.fm Seite 138 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
di.directories.ToString lblSize.Text = "Platzbedarf: " + _ (di.size \ 1024 \ 1024).ToString + " MBytes" btnProperties.Visible = False End Sub
Neuen Thread starten
Die Ermittlung der Verzeichniseigenschaften beginnt durch einen Klick auf den EIGENSCHAFTEN-Button. In btnProperties_Click wird zuerst ein eventuell noch laufender Thread beendet. Anschließend wird ein neues dirinfo-Objekt erzeugt. An den Konstruktor werden zwei Parameter übergeben: ein Verweis auf das Formular sowie das TreeNode-Objekt des Verzeichnisses. Für die Ereignisse Done und Update werden Ereignisprozeduren eingerichtet. Anschließend wird di.GetProperties in einem neuen Thread gestartet.
Ereignisse zur Kommunikation zwischen dem neuen Thread und dem Hauptprogramm
In den update- und done-Ereignisprozeduren wird der rechte Fensterbereich aktualisiert, falls details_tn immer noch auf dasselbe TreeNodeObjekt zeigt, dessen Eigenschaften ermittelt werden. (Vielleicht hat der Anwender in der Zwischenzeit bereits ein anderes Verzeichnis ausgewählt. In diesem Fall läuft der Thread weiter, die Anzeige wird aber nicht aktualisiert.) Sobald das Endergebnis vorliegt (done-Ereignis), wird das dirinfo-Objekt in der Tag-Eigenschaft des TreeNode-Elements gespeichert. ' Eigenschaften zum Verzeichnis ermitteln Private Sub btnProperties_Click(...) _ Handles btnProperties.Click Dim di As dirinfo If IsNothing(details_tn) Then Exit Sub stop_dirinfo_thread() 'eventuell laufenden 'Thread abbrechen 'neues dirinfo-Objekt erzeugen und dessen 'Methode GetProperties in neuem Thread 'starten btnProperties.Enabled = False di = New dirinfo(Me, details_tn) AddHandler di.Done, AddressOf dirinfo_done AddHandler di.Update, AddressOf dirinfo_update dinfothread = New Threading.Thread( _ AddressOf di.GetProperties) dinfothread.Name = "dinfothread" dinfothread.Start() End Sub
138
WINFORM.fm Seite 139 Dienstag, 20. August 2002 3:48 15
Verzeichniseigenschaften in einem eigenen Thread ermitteln
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Ereignisprozedur, wird aufgerufen, ' um die Verzeichnisinfos zu aktualisieren Private Sub dirinfo_update(ByVal di As dirinfo) If di.tn Is details_tn Then lblFiles.Text = "Dateien: " + _ di.files.ToString lblDirectories.Text = "Verzeichnisse: " + _ di.directories.ToString lblSize.Text = "Platzbedarf: " + _ (di.size \ 1024 \ 1024).ToString + _ " MBytes" btnProperties.Enabled = False End If End Sub ' Ereignisprozedur, wird aufgerufen, wenn ' dinfothread fertig ist Private Sub dirinfo_done(ByVal di As dirinfo) di.tn.Tag = di If di.tn Is details_tn Then ShowTreenodeDetails(di.tn) End If End Sub ' Prozedur, um einen eventuell noch laufenden ' Thread zu stoppen Private Sub stop_dirinfo_thread() If (Not IsNothing(dinfothread)) AndAlso _ dinfothread.IsAlive Then Try dinfothread.Abort() Catch MsgBox("Fehlermeldung, bitte warten ...") End Try dinfothread.Join() End If End Sub ' bei Programmende eventuell noch laufenden ' Thread stoppen Private Sub Form1_Closing(...) _ Handles MyBase.Closing stop_dirinfo_thread() End Sub
Programmcode der dirinfo-Klasse Bei der dirinfo-Klasse ist eigentlich nur der Aufruf der Ereignisse Done und Update durch Invoke bzw. BeginInvoke bemerkenswert. [Begin]Invoke ist deswegen erforderlich, weil in diesen Ereignisprozeduren der Fens-
Ereignisse mit Invoke auslösen
139
WINFORM.fm Seite 140 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
terinhalt aktualisiert wird. Deswegen muss sichergestellt sein, dass die Ereignisprozeduren im Thread des Fensters ausgeführt werden. ReadDirectory ist zweistufig gegen jede Art von Zugriffsfehler auf Verzeichnisse oder einzelne Dateien abgesichert. Der Grund besteht darin, dass das Programm – wenn es nicht von einem Benutzer mit Administrator-Rechten ausgeführt wird – nicht alle Verzeichnisse lesen darf. Derartige Fehler führen dazu, dass ein Fehlerzähler um eins erhöht wird. Es kommt aber zu keiner Fehlermeldung. ' Beispiel multithread\dir-properties Class dirinfo 'Aktualisierungsinvervall in ms Const interval As Integer = 300 Public files, directories As Long Public size, errors As Long Public tn As TreeNode Public path As String Private frm As Form Private nextupdate As Date ' Ereignisse Public Event Done(ByVal di As dirinfo) Public Event Update(ByVal di As dirinfo) ' Delegate Private Delegate Sub sub_nopara() ' Konstruktor Public Sub New(ByVal frm As Form, _ ByVal tn As TreeNode) Me.frm = frm Me.path = tn.FullPath Me.tn = tn End Sub ' Methoden Public Sub GetProperties() 'erstes Update nach 100 ms nextupdate = Now.AddMilliseconds(100) ReadDirectory(path) frm.Invoke(New sub_nopara( _ AddressOf raiseDoneEvent)) End Sub ' rekursiv die Verzeichniseigenschaften ' ermitteln Private Sub ReadDirectory(ByVal p As String) Dim dir As New IO.DirectoryInfo(p) Dim subdir As IO.DirectoryInfo
140
WINFORM.fm Seite 141 Dienstag, 20. August 2002 3:48 15
Mehrere Fenster in eigenen Threads öffnen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Dim file As IO.FileInfo Try For Each file In dir.GetFiles ' Update-Event auslösen If Now > nextupdate Then frm.BeginInvoke(New sub_nopara( _ AddressOf raiseUpdateEvent)) nextupdate = _ Now.AddMilliseconds(interval) End If ' Dateien und Größe aufsummieren files += 1 Try size += file.Length Catch 'Fehler ignorieren errors += 1 End Try Next For Each subdir In dir.GetDirectories directories += 1 ReadDirectory(subdir.FullName) Next Catch 'Fehler ignorieren errors += 1 End Try End Sub ' xxxEvent() werden mit Invoke aufgerufen Sub raiseDoneEvent() RaiseEvent Done(Me) End Sub Sub raiseUpdateEvent() RaiseEvent Update(Me) End Sub End Class
7.4 Mehrere Fenster in eigenen Threads öffnen Das in diesem Abschnitt vorgestellte Beispiel (siehe Abbildung 7.2) greift nochmals die zentrale Frage aus Abschnitt 7.2 auf: Wie können mehrere Fenster geöffnet werden, die wirklich gleichberechtigt sind, die sich also auch bei aufwändigen Berechnungen nicht gegenseitig blockieren? Die Antwort ist eigentlich ganz einfach: Jedes Fenster wird in einem eigenen Thread gestartet und jeder Thread erhält seine eigene Nachrichtenschleife zur Verarbeitung der Ereignisse.
141
WINFORM.fm Seite 142 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Sie können sich selbst davon überzeugen, wie gut das funktioniert: starten Sie das Programm, öffnen Sie einige Fenster und klicken Sie dann in allen Fenstern den Button MUSTER ZEICHNEN an. Dabei sollten Ihnen zwei Dinge auffallen: Erstens sind die einzelnen Fenster nicht dadurch blockiert, dass gerade in einem Fenster eine zeitaufwändige Ereignisprozedur läuft. Und zweitens werden die Muster nun wirklich fast gleichzeitig gezeichnet. (Sie sehen am Bildschirm sogar durch ein wahrnehmbares Flackern, wann gerade ein Thread-Wechsel stattfindet.) Beachten Sie, dass das Programm wegen der Verwendung der Application.Exit-Methode nur dann ohne Fehler läuft, wenn es in der höchsten
.NET-Sicherheitsstufe ausgeführt wird (Start von der lokalen Festplatte).
Abbildung 7.2: Jedes Fenster läuft in einem eigenen Thread (Beispiel multi4).
Code in Module1 Fenster in einem neuen Thread öffnen
Die Programmausführung beginnt in Module1.Main. Dort wird mit OpenWindow ein neues Fenster erzeugt. Bemerkenswert ist, dass OpenWindow die
Methode Application.Run(frm) für jedes Fenster in einem eigenen Thread ausführt. Das Form-Objekt wird in die myForms-Aufzählung eingefügt, so dass im Code jedes Fensters auf alle Fenster zugreifen kann, und damit das Programmende durch Application.Exit ausgelöst werden kann, wenn es keine offenen Fenster mehr gibt. Der neue Thread beginnt in der Prozedur NewWindowThread, wobei newfrm als Zwischenspeicher für das Formularobjekt dient. ' Beispiel multithread\multi4 Module Module1 Public myForms As New Collections.ArrayList() Dim newfrm As Form Sub Main() ShowWindow(New Form1()) End Sub Public Sub ShowWindow(ByVal frm As Form) Dim winThread As New Threading.Thread( _ AddressOf NewWindowThread) newfrm = frm
142
WINFORM.fm Seite 143 Dienstag, 20. August 2002 3:48 15
Mehrere Fenster in eigenen Threads öffnen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
myForms.Add(frm) ' erleichtert die Fehlersuche winThread.Name = "winthread, started = " + _ Now.ToLongTimeString winThread.Start() End Sub Private Sub NewWindowThread() Application.Run(newfrm) End Sub End Module
Formularcode Im Vergleich zum Beispielprogramm multi3 (siehe Abschnitt 6.2) sind am Code in Form1 nur kosmetische Änderungen erforderlich. Eine betrifft die Schleife, um alle Fenster mit Close zu schließen. Da alle Threads gleichzeitig auf myForms zugreifen können, kann es passieren, dass sich der Inhalt von myForms während der Ausführung der Schleife verändert. Deswegen wird der Inhalt in ein lokales Feld kopiert, anschließend werden alle Fenster – abgesichert durch Try-Catch – geschlossen. Die Close-Methode wird per Invoke ausgeführt. Die Application.Exit-Anweisung am Ende der Prozedur sollte eigentlich nicht notwendig sein, weil das Programm beim Schließen des letzten Fensters in Form1_Closed beendet wird.
Fenster in unterschiedlichen Threads schließen
' Beispiel multithread\multi4 Public Class Form1 [Vom Windows Form Designer generierter Code] ' zum Aufruf der Close-Methode Private Delegate Sub sub_formpara( _ ByVal f As Form) Private Sub btnClose_Click(...) _ Handles btnClose.Click Dim i As Integer Dim f As Form Dim frmCount As Integer = myForms.Count Dim frms(frmCount - 1) As Object Dim obj(0) As Object Dim closeform_dlg As sub_formpara myForms.CopyTo(frms, 0) For i = 0 To frmCount – 1 Try f = CType(frms(i), Form) obj(0) = f closeform_dlg = New sub_formpara( _ AddressOf CloseForm) f.Invoke(closeform_dlg, obj) Catch ex As Exception Debug.WriteLine( _
143
WINFORM.fm Seite 144 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
7 Multithreading
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
"Fehler in btnClose_Click(): " + _ ex.Message) End Try Next 'sicher ist sicher Application.Exit() End Sub ' Prozedur, um Close für ein Formular ' innerhalb von dessen Thread auszuführen Private Sub CloseForm(ByVal f As Form) f.Close() End Sub ' beim Schließen des Fensters gegebenenfalls ' auch Programm beenden Private Sub Form1_Closed(...) _ Handles MyBase.Closed myForms.Remove(Me) If myForms.Count = 0 Then Application.Exit() End Sub End Class Application.Exit hat trotz des Multithreading-Ansatzes dieselbe Wirkung wie bisher: Es beendet das Programm, weil es alle Fenster des laufenden Prozesses schließt. Wenn Sie nur den laufenden Thread beenden möchten, können Sie Me.Close oder Application.ExitThread ausführen.
Verhalten bei einem nicht behandelten Fehler Wenn in einem Fenster ein nicht behandelter Fehler auftritt, wird nur der Thread dieses einen Fensters beendet. Die restlichen Fenster laufen unbeeinflusst weiter. Das Problem besteht aber darin, dass myForms nun auf ein Fenster verweist, dass es gar nicht mehr gibt. Das kann in der Folge alle möglichen Probleme verursachen. (btnClose_Click wurde auch im Hinblick auf dieses Szenario so wasserdicht wie möglich formuliert.)
144
WINFORM.fm Seite 145 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
8
Optische Effekte
In diesem kurzen Kapitel geht es weniger um Details bei der Steuerung von Fenstern und Steuerelementen, sondern darum, das optische Erscheinungsbild von Programmen ein wenig zu verbessern. Das Kapitel beantwortet zwei Fragen:
왘 Wie können .NET-Programme die moderne Optik von Windows XP nutzen? 왘 Wie kann bei Programmen, deren Start länger als ein bis zwei Sekunden dauert, während der Startphase eine so genannte SplashBitmap eingeblendet werden?
8.1 Windows-XP-Optik Per Default haben .NET-Programme das Aussehen von Windows-95/ 98/ME/2000-Programmen, auch dann, wenn sie unter Windows XP ausgeführt werden (siehe Abbildung 8.1, Mitte). Mit Windows XP hat Microsoft die Optik von Fenstern, Buttons etc. aber modernisiert und veränderbar gemacht. Dieses Merkmal von Windows XP wird als themes bezeichnet. Man kann zwar darüber streiten, ob die neue Optik wirklich einen Fortschritt darstellt oder eher als Modetrend zu bezeichnen ist, aber auf jeden Fall besteht häufig der Wunsch, auch Visual Basic .NET-Programme mit Windows-XP-Look-and-Feel zu gestalten. Damit .NET-Programme im modernen Chic von Windows XP glänzen können, müssen drei Voraussetzungen erfüllt sein:
Voraussetzungen
왘 Die neue XP-Optik kann nur genutzt werden, wenn das Betriebssystem, unter dem das Programm ausgeführt wird, themes unterstützt. Ob das am aktuellen Rechner der Fall ist, können Sie per Code feststellen: If OSFeature.Feature.IsPresent( _ OSFeature.Themes) Then ...
왘 Bei allen Steuerelementen, die die Eigenschaft FlatStyle kennen, muss diese auf System gestellt werden. Das gilt insbesondere für die Steuerelemente Button, RadioButton, CheckBox und GroupBox. (Per Default lautet die Einstellung Standard.) 왘 Das Programm (die *.exe-Datei) muss mit Version 6 der Bibliothek comctl32.dll verbunden werden. (Diese Bibliothek steht zurzeit nur unter Windows XP in Version 6 zur Verfügung.)
145
WINFORM.fm Seite 146 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
8 Optische Effekte
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
FlatStyle einstellen FlatStyle-Eigenschaft
FlatStyle können Sie natürlich für jedes einzelne Steuerelement im Eigenschaftsfenster einstellen. Komfortabler ist es, diese Aktion nur bei Bedarf per Code durchzuführen. Dazu rufen Sie in Form1_Load die Prozedur SetFlatStyle auf, die die Eigenschaft bei allen Steuerelementen ändert. SetFlatStyle ruft sich selbst rekursiv auf, damit auch in Panel oder GroupBox eingebettete Steuerelemente erfasst werden (siehe auch Abschnitt 4.3).
Durch FlatStyle=System verlieren Sie übrigens einige Einstellmöglichkeiten. (Das ist wohl auch der Grund, warum FlatStyle = Standard als Defaulteinstellung gilt.) Sie können jetzt beispielsweise weder eine eigene Hintergrundfarbe noch eine Hintergrundbitmap angeben. Diese Merkmale werden nun durch die themes vorgegeben. ' Beispiel effekte\xp-optik Private Sub Form1_Load(...) Handles MyBase.Load If OSFeature.Feature.IsPresent( _ OSFeature.Themes) Then SetFlatStyle(Me.Controls) End If End Sub ' rekursiv alle Steuerelemente durchlaufen Private Sub SetFlatStyle( _ ByVal ctrls As Control.ControlCollection) Dim c As Control For Each c In ctrls If TypeOf c Is ButtonBase Then CType(c, ButtonBase).FlatStyle = _ FlatStyle.System ElseIf TypeOf c Is GroupBox Then CType(c, GroupBox).FlatStyle = _ FlatStyle.System End If SetFlatStyle(c.Controls) Next End Sub
Manifest-Datei Damit das Programm die Version 6 der comctl32.dll-Bibliothek verwendet (sofern sie am Rechner verfügbar ist), liefern Sie zusammen mit der *.exe-Datei eine so genannte Manifest-Datei aus. (Eine Manifest-Datei ist eine XML-Datei, die Abhängigkeiten des Programms von einzelnen Bibliotheken beschreibt.) Diese Datei muss den Namen <programmname>.manifest haben. (Wenn Ihr Programm also hellow.exe heißt, muss die Manifest-Datei hellow.exe.manifest heißen.)
146
WINFORM.fm Seite 147 Dienstag, 20. August 2002 3:48 15
Begrüßungsbild (Splash-Bitmap)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Die Manifest-Datei muss den folgenden Inhalt haben. (Sie müssen diese Zeilen nicht abschreiben, sondern finden die Datei unter den Beispieldateien zum Buch.)
<dependency> <dependentAssembly>
Abbildung 8.1: Links das Programm unter Windows 2000, in der Mitte unter Windows XP ohne Manifest-Datei, rechts ebenfalls unter Windows XP, aber mit Manifest-Datei
8.2 Begrüßungsbild (Splash-Bitmap) Wenn Sie große Programme wie den Internet Explorer, Mozilla, Word, Excel etc. starten, dauert es je nach Hardware einige Sekunden, bis das Bedienungsfenster endlich erscheint. Um in der Zwischenzeit ein Feedback zu geben, dass das Programm tatsächlich gestartet wurde, zeigen diese Programme sofort ein buntes Begrüßungsbild an. Dieses Bild wird oft als Splash-Bitmap bezeichnet. In .NET-Programmen tritt dieselbe Verzögerung auf, wenn zum Programmstart (üblicherweise in Form1_Load) eine zeitaufwändige Initialisierung durchgeführt wird. Wenn Sie dort eine Datenbankverbindung
147
WINFORM.fm Seite 148 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
8 Optische Effekte
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
herstellen und dann einige Daten einlesen etc., wird das Fenster erst sichtbar, wenn Form1_Load abgeschlossen ist.
Splash-Formular Es ist nicht schwierig, auch in einem .NET-Programm ein SplashBitmap zu realisieren. Dazu fügen Sie Ihrem Projekt ein neues Formular hinzu, fügen dort ein PictureBox-Steuerelement ein und laden in dieses Steuerelement die gewünschte Bitmap. Durch AutoSize = True erreichen Sie, dass die PictureBox genauso groß ist ist wie die Bitmap. Um das Programm durch die Bitmap nicht allzu sehr aufzublähen, sollten Sie die Bitmap in einem platzsparenden Format speichern (z.B. *.png oder *.gif, nicht *.bmp!). Fenster ohne Rahmen
Bei den Formulareigenschaften stellen Sie FormBorderStyle = None ein. Damit wird das Fenster ohne jeden Rahmen angezeigt. Des weiteren stellen Sie StartPosition auf Manual. Damit das Fenster beim Erscheinen genau so groß wie die PictureBox ist und außerdem am Bildschirm zentriert wird, sind die folgenden Zeilen in der Load-Prozedur erforderlich: ' Beispiel effekte\splash ' Formular Form2 (Splash-Fenster) ' Fenster so groß wie die Bitmap machen; ' Fenster am Bildschirm zentrieren Private Sub Form2_Load(...) Handles MyBase.Load Dim x, y As Integer x = (SystemInformation.WorkingArea.Width - _ PictureBox1.Size.Width) \ 2 y = (SystemInformation.WorkingArea.Height - _ PictureBox1.Size.Height) \ 2 Me.Location = New Point(x, y) Me.ClientSize = PictureBox1.Size End Sub
Splash-Formular anzeigen DoEvents-Methode
Als Startobjekt für Ihr Programm gilt weiterhin Form1 (oder wie immer Ihr Hauptfenster heißt). Dort erzeugen Sie am Beginn von Form1_Load das Splash-Fenster und zeigen es an. Anschließend müssen Sie Application.DoEvents ausführen, damit das Fenster tatsächlich sichtbar wird. (DoEvents bewirkt, dass Form1_Load kurz unterbrochen wird, um andere Ereignisse – inklusive der Initialisierung von Form2 – zu verarbeiten.) Wenn das Erzeugen der Steuerelemente des Hauptfensters sehr aufwändig ist, können Sie das Splash-Fenster auch schon im Vom Windows Form Designer generierten Code in der Prozedur New (unmittelbar nach der Zeile MyBase.New()) erzeugen. Damit erreichen Sie, dass das SplashFenster ein bisschen schneller erscheint.
148
WINFORM.fm Seite 149 Dienstag, 20. August 2002 3:48 15
Begrüßungsbild (Splash-Bitmap)
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Beispiel effekte\splash ' Formular Form1 (Hauptfenster) Dim splash As Form2 Private Sub Form1_Load(...) Handles MyBase.Load Dim i As Integer ' Splash-Fenster anzeigen splash = New Form2() splash.Show() Application.DoEvents() ' zeitaufwändige Initialisierung For i = 1 To 100000 ListBox1.Items.Add(i.ToString) Next ' Splash-Fenster schließen splash.Close() End Sub
Splash-Fenster langsam ausblenden Nachdem es in diesem Kapitel um optische Effekte (man könnte auch sagen: Spielereien) geht, stelle ich Ihnen hier noch eine Methode vor, wie Sie das Splash-Fenster nicht plötzlich verschwinden lassen, sondern langsam ausblenden. (Das ist nur dann sinnvoll, wenn das Hauptfenster nach dem Start das Splash-Fenster nicht vollkommen bedeckt!)
Fenster ausblenden
Dazu statten Sie das Splash-Fenster mit einem Timer-Steuerelement aus und stellen Interval=50 ein. Das Steuerelement löst damit nach seiner Aktivierung alle 50 ms ein Tick-Ereignis aus. Die Aktivierung erfolgt in der Closing-Ereignisprozedur, wenn diese Prozedur zum ersten Mal aufgerufen wird und wenn das Betriebssystem, unter dem das Programm läuft, Spezialeffekte wie das Ausblenden unterstützt. (Das kann mit der Methode OSFeature.Feature.IsPresent festgestellt werden.) Das Schließen des Fenster wird durch e.Cancel = True vorerst verhindert.
Timer-Steuerelement
In der Tick-Ereignisprozedur wird nun die Deckungskraft des Fensters durch eine schrittweise Verkleinerung von Opacity so lange vermindert, bis das Fenster schließlich unsichtbar ist. Anschließend wird es geschlossen.
Opacity-Eigenschaft
' Beispiel effekte\splash ' Formular Form2 (Splash-Fenster) Dim first_close_attempt As Boolean = True ' Ausblendvorgang starten Private Sub Form2_Closing( _ ByVal sender As Object, ByVal e As _ System.ComponentModel.CancelEventArgs) _
149
WINFORM.fm Seite 150 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
8 Optische Effekte
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Handles MyBase.Closing If first_close_attempt And _ OSFeature.Feature.IsPresent( _ OSFeature.LayeredWindows) Then first_close_attempt = False Timer1.Enabled = True e.Cancel = True End If End Sub ' Fenster ausblenden und schließen Private Sub Timer1_Tick(...) Handles Timer1.Tick Me.Opacity -= 0.15 If Me.Opacity <= 0 Then Timer1.Enabled = False Me.Close() End If End Sub
150
WINFORM.fm Seite 151 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9
Zwischenablage und Drag&Drop
Damit Ihr Programm mit anderen Windows-Programmen kommunizieren kann, sollte es in der Lage sein, Daten über die Zwischenablage sowie über Drag&Drop auszutauschen. Dieser Abschnitt beschreibt die grundlegenden Programmiertechniken. Es ist übrigens kein Zufall, dass die beiden Themen hier in einem Kapitel behandelt werden: Der Datenaustausch erfolgt in beiden Fällen über die IDataObject-Schnittstelle. Daher gibt es bei der Unterstützung der beiden Mechanismen einige Ähnlichkeiten.
9.1 Zwischenablage nutzen Der Inhalt der Zwischenablage wird über die Windows.Forms-Klasse Clipboard angesprochen. Die Klasse kennt nur zwei Methoden: GetDataObject, um den Inhalt der Zwischenablage auszulesen, und SetDataObject, um die Zwischenablage zu verändern. Eine ganze Reihe von .NET-Steuerelementen kommuniziert selbstständig mit der Zwischenablage. Beispielsweise funktionieren in Textfeldern die Tastenkürzel (Strg)+(C), (Strg)+(X) und (Strg)+(V) bzw. (Shift)+(Einfg) und (Shift)+(Entf) automatisch, ohne dass Sie eine Zeile Code schreiben müssen. Wenn Sie Programme zur Manipulation der Zwischenablage schreiben, ist das Programm c:\windows\system32\clipbrd.exe ein nützliches Hilfsmittel. Es zeigt die gerade in der Zwischenablage enthaltenen Daten in beliebigen Formaten an.
Zwischenablage lesen GetDataObject liefert ein Objekt der Schnittstelle IDataObject.
IDataObject-Schnittstelle
Dim ido As IDataObject ido = Clipboard.GetDataObject()
Der erste Schritt zur Verarbeitung der Daten besteht darin, ihren Typ festzustellen. Dazu führen Sie ido.GetFormats aus. Diese Methode liefert ein String-Feld mit den Namen von sämtlichen in der Zwischenablage enthaltenen Formaten (bzw. Formaten, in die die Daten umgewandelt werden können). In diesem Zusammenhang ist es wichtig zu wissen, dass die Zwischenablage Daten in mehreren Formaten gleichzeitig enthalten kann. Wenn Sie z.B. in Microsoft Word 2000 einen Textabsatz
Format feststellen
151
WINFORM.fm Seite 152 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
kopieren, dann wird dieser Absatz unter anderem als ASCII-Text, Unicode-Text, RichText (RTF), im HTML-Format sowie als Windows-Metafile-Grafikobjekt in die Zwischenablage eingefügt. Oft sind Sie gar nicht an allen Formaten interessiert, sondern wollen nur wissen, ob die Zwischenablage Daten in einem bestimmten Format enthält, das Ihr Programm weiterverarbeiten kann. Dazu verwenden Sie die Methode GetDataPresent, an die Sie wahlweise eine Zeichenkette oder ein System.Type-Objekt zur Beschreibung des Formats übergeben können. Die Klasse DataFormats enthält eine Aufzählung mit den wichtigsten System.Type-Objekten, z.B. Bitmap, CommaSeparatedValue, EnhancedMetafile, Rtf und Text. If ido.GetDataPresent("HTML Format") Then ... If ido.GetDataPresent(DataFormats.Html) _ Then ...
Beliebige andere Datentypen können Sie angeben, indem Sie auf ein entsprechendes Objekt die Methode GetType anwenden. (GetType funktioniert allerdings nicht, wenn Sie nur den Klassennamen angeben. Bitmap.GetType() liefert daher lediglich eine Fehlermeldung.) Wenn Sie Daten mit externen Programmen austauschen möchten, die Zwischenablage also nicht nur zur Kommunikation innerhalb eines oder mehrerer selbst erstellter Programme einsetzen, sollten Sie nach Möglichkeit die vordefinierten DataFormats-Elemente verwenden. If ido.GetDataPresent(bitmapobj.GetType()) _ Then ...
Daten auslesen
Um die Daten in einem bestimmten Format auszulesen, verwenden Sie schließlich die Methode GetData, wobei Sie wie bei GetDataPresent das gewünschte Datenformat angeben müssen. GetData liefert den Datentyp Object, den Sie mit CType in den von Ihnen gewünschten Typ umwandeln müssen. (Diese Umwandlung klappt natürlich nur, wenn eine Umwandlung möglich ist. Sie können also kein Grafikobjekt in den Datentyp String umwandeln.) Die folgende Anweisung fügt Textdaten aus der Zwischenablage in ein Textfeld ein. TextBox1.Text = CType(ido.GetData( _ DataFormats.Text), String)
Zwischenablage ändern SetData-Methode
152
Mit ClipBoard.SetData(obj) fügen Sie einen Verweis auf ein beliebiges Objekt in die Zwischenablage ein. (Wenn sich obj später ändert, ändert sich somit auch der Inhalt der Zwischenablage.) Daten, die sich bereits in der Zwischenablage befinden, werden dadurch automatisch überschrieben. Grundsätzlich gibt es keine Einschränkungen bezüglich des
WINFORM.fm Seite 153 Dienstag, 20. August 2002 3:48 15
Zwischenablage nutzen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Objekttyps. Andere Programme werden aber nur dann etwas mit den Daten anfangen können, wenn Sie Ihre Daten in einem der allgemein akzeptierten Formate übergeben. An SetData können Sie im optionalen zweiten Parameter True übergeben: Damit erreichen Sie, dass die Daten in die Zwischenablage kopiert werden. Die Daten stehen dann in der Zwischenablage auch dann noch zur Verfügung, wenn sie in Ihrem Programm durch Dispose gelöscht werden oder wenn Ihr Programm endet. In die Zwischenablage eingefügte Daten stehen meist automatisch in mehreren Formaten zur Verfügung. Wenn Sie beispielsweise Text einfügen, liegt dieser in den Formaten "System.String", "Text" und "UnicodeText" vor. Es gibt aber leider keine Möglichkeit, selbst Daten in mehreren Formaten einzufügen.
Beispielprogramm Das Beispielprogramm (siehe Abbildung 9.1) demonstriert einige einfache Anwendungen der Zwischenablage. Im linken oberen Eck des Beispielprogramms wird ein Listenfeld mit allen in der Zwischenablage enthaltenen Datenformaten angezeigt. Das Listenfeld wird bei jeder Änderung der Zwischenablage aktualisiert. (Die Aktualisierung erfolgt genau genommen in einer Timer-Ereignisprozedur, die vier Mal pro Sekunde aufgerufen wird und überprüft, ob sich die Datenformate in der Zwischenzeit geändert haben.) Mit drei Buttons können Sie Textoder Bitmap-Daten in die Zwischenablage einfügen bzw. von dort lesen.
Abbildung 9.1: Datenaustausch mit der Zwischenablage ' Beispiel clip-dragndrop\clipboard-test ' Datenformate der Zwischenablage in Listenfeld ' anzeigen Private Sub Timer1_Tick(...) Handles Timer1.Tick Dim ido As IdataObject Dim tmp, frmt As String Static last_formats As String
153
WINFORM.fm Seite 154 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
ido = Clipboard.GetDataObject() tmp = Join(ido.GetFormats) If tmp <> last_formats Then last_formats = tmp ListBox1.Items.Clear() For Each frmt In ido.GetFormats() ListBox1.Items.Add(frmt) Next End If Button1.Enabled = _ ido.GetDataPresent(DataFormats.Text) Button2.Enabled = _ (TextBox1.SelectionLength > 0) End Sub ' Textinhalt der Zwischenablage in TextBox1 ' einfügen Private Sub Button1_Click(...) _ Handles Button1.Click Dim ido As IdataObject ido = Clipboard.GetDataObject() If ido.GetDataPresent(DataFormats.Text) Then TextBox1.SelectedText = _ CType(ido.GetData(DataFormats.Text), _ String) End If End Sub ' markierten Text aus TextBox1 in die ' Zwischenablage kopieren Private Sub Button2_Click(...) _ Handles Button2.Click If TextBox1.SelectionLength > 0 Then Clipboard.SetDataObject( _ TextBox1.SelectedText) End If End Sub
Beachten Sie, dass es in Button3_Click zwei mögliche Vorgehensweisen gibt: Die eine besteht darin, dass Sie SetDataObject(bm) ausführen – dann dürfen Sie die Bitmap aber nicht mit Dispose aus dem Speicher löschen. Da die Zwischenablage nur einen Verweis auf die Bitmap enthält, ändern sich die über die Zwischenablage zugänglichen Daten, wenn Sie die Bitmap durch weitere Grafikkommandos verändern. Die andere, im Beispielprogramm gewählte Variante besteht darin, SetDataObject(bm, True) zu verwenden und so eine Kopie der Bitmap in die Zwischenablage einzufügen. Damit können Sie mit der Bitmap anschließend tun und lassen, was Sie wollen – die sich in der Zwischenablage befindenden Daten werden dadurch nicht beeinträchtigt.
154
WINFORM.fm Seite 155 Dienstag, 20. August 2002 3:48 15
Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' selbst gezeichnete Bitmap in die ' Zwischenablage einfügen Private Sub Button3_Click(...) _ Handles Button3.Click Dim bm As New Bitmap(100, 100) Dim gr As Graphics = Graphics.FromImage(bm) gr.Clear(Color.White) gr.FillEllipse(Brushes.Blue, 0, 0, 100, 100) gr.Dispose() Clipboard.SetDataObject(bm, True) bm.Dispose() End Sub
9.2 Drag&Drop Der Begriff Drag&Drop bezeichnet das Verschieben von Objekten an eine neue Position (im selben oder in einem anderen Fenster, das auch von einem anderen Programm stammen kann). .NET-Programme können sowohl Auslöser als auch Empfänger von Drag&Drop-Ereignissen sein (aber es ist nicht erforderlich, dass ein Programm immer beide Funktionen realisiert).
왘 Um eine Drag&Drop-Operation in einem .NET-Programm auszulösen, führen Sie die Methode DoDragDrop aus (üblicherweise in einer MouseDown- oder MouseMove-Ereignisprozedur). 왘 Wenn Sie dagegen Drag&Drop-Operationen verarbeiten (empfangen) möchten, müssen Sie für das betreffende Steuerelement oder Formular die Eigenschaft AllowDrop auf True stellen und zumindest die zwei Ereignisprozeduren DragEnter und DragDrop programmieren.
Drag&Drop-Operation initiieren Eine Drag&Drop-Operation beginnt mit der Methode DoDragDrop. Im ersten Parameter übergeben Sie die Daten, die verschoben oder kopiert werden sollen. (Dabei kann ein beliebiges Objekt angegeben werden.)
DoDragDrop-Methode
Im zweiten Parameter geben Sie an, welche Operationen zulässig sind. Zur Auswahl stehen die Elemente der DragDropEffects-Aufzählung (oder eine beliebige Or-Kombination): Copy, Move, Scroll, Link oder All. Bei der Programmierung der Beispiele zu diesem Abschnitt hat sich herausgestellt, dass das Auslösen einer Drag&Drop-Operation viel schwieriger ist als das Empfangen und Verarbeiten solcher Aktionen. Das Problem besteht darin, dass Drag&Drop-Operationen üblicherweise mit dem Drücken der linken Maustaste beginnen (MouseDownEreignis). Ein erster Ansatz zum Start einer Verschiebeoperation sieht daher meist folgendermaßen aus:
155
WINFORM.fm Seite 156 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Private Sub steuerelement_MouseDown(...) _ Handles ToolBar1.MouseDown If e.Button = MouseButtons.Left Then steuerelement.DoDragDrop(daten, _ DragDropEffects.Move) End If End Sub
Ganz so einfach funktioniert es aber leider selten: Das Drücken der linken Maustaste dient ja oft zur Markierung eines Objekts (oder Listeneintrags). Es muss also zwischen einem gewöhnlichen Mausklick und dem Beginn einer Drag&Drop-Operation unterschieden werden. Viele Steuerelemente bieten dazu leider keine ausreichenden Möglichkeiten, so dass alle möglichen Umwege begangen werden müssen. Zu allem Überfluss kommen sich bei manchen Steuerelementen (speziell bei gewöhnlichen Listenfeldern) die interne Logik zur automatischen Verarbeitung von Mausklicks und Ihr eigener Code zum Start einer Drag&Drop-Aktion in die Quere. DragItem-Ereignis
Eine positive Ausnahme dieser Misere stellen die beiden Steuerelemente TreeView und ListView dar. Dort tritt nämlich das DragItem-Ereignis auf, wenn der Anwender versucht, zuvor markierte Einträge zu verschieben. In der Ereignisprozedur können Sie dann mühelos DoDragDrop ausführen.
Rückgabewert von DoDragDrop
Die Methode DoDragDrop liefert zum Ende der Drag&Drop-Operation als Ergebnis ein Element der DragDropEffects-Aufzählung. Dieser Rückgabewert gibt Aufschluss darüber, ob tatsächlich eine Verschiebeoperation durchgeführt wurde, und wenn ja, welcher Art die Operation war. Damit können Sie beispielsweise nach eine Move-Operation die verschobenen Objekte löschen. (Das ListView-Beispiel in Abschnitt 9.4 demonstriert diese Vorgehensweise.)
Drag&Drop-Ereignisse zulassen AllowDrop-Eigenschaft
Damit ein Steuerelement bzw. ein Formular als Empfänger für Drag&Drop-Ereignisse dienen und die im Folgenden beschriebenen DragXxx-Ereignisse empfangen kann, muss die AllowDrop-Eigenschaft auf True gestellt werden. Üblicherweise erfolgt das bereits bei der Programmentwicklung im Eigenschaftsfenster. (Achtung, die Defaulteinstellung für diese Eigenschaft lautet False!) Die AllowDrop-Eigenschaft kann grundsätzlich für jedes Steuerelement individuell eingestellt werden. Bei Containern (und insbesondere bei Formularen) vererbt sich die Einstellung AllowDrop=True auf alle enthaltenen Steuerelemente, auch wenn für diese AllowDrop= False gilt. In diesem Fall treten die DragXxx-Ereignisse nicht für das Steuerelement, sondern nur für den Container auf.
156
WINFORM.fm Seite 157 Dienstag, 20. August 2002 3:48 15
Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Im Regelfall ist dieses Verhalten praktisch: Wenn beispielsweise das gesamte Fenster als Drag&Drop-Empfänger gelten soll, müssen Sie lediglich für das Formular AllowDrop auf True setzen und DragXxx-Ereignisprozeduren für das Formular entwickeln. Um die im Formular enthaltenen Steuerelemente brauchen Sie sich nicht zu kümmern – Drag&Drop-Operationen sind auch dort möglich und werden von den DragXxx-Ereignisprozeduren des Formulares verwaltet. Wenn Sie freilich möchten, dass ein Steuerelement innerhalb eines Drag&Drop-tauglichen Fensters explizit nicht als Empfänger dienen soll, müssen Sie AllowDrop=True einstellen. Das klingt zunächst wahrscheinlich widersinnig. Sie erreichen dadurch aber, dass die DragXxxEreignisprozeduren des Steuerelements aktiv werden. Wenn Sie dort keinen Code vorsehen, ist das Steuerelement zwar theoretisch ein Drag&Drop-Empfänger, verweigert tatsächlich aber die Annahme jeglicher Drag&Drop-Objekte. Damit haben Sie erreicht, was Sie möchten.
Drag&Drop-Ereignisse Für den Empfänger einer Drag&Drop-Operation (also für Steuerelemente bzw. das Formular mit AllowDrop=True) treten die folgenden Ereignisse auf:
왘 DragEnter tritt auf, wenn die Maus mit einem Drag-Objekt über ein Steuerelement bewegt wird. Mit e.Data können die Daten ermittelt werden, die beim Loslassen übergeben werden.
Maus in ein Steuerelement bewegen
In der Prozedur wird üblicherweise der Objekttyp der Daten getestet. Wenn das Steuerelement mit den Daten zurechtkommt, wird durch e.Effect das Aussehen des Mauscursors verändert. (Per Default sieht die Maus wie die Verkehrstafel Einfahrt verboten aus.)
왘 DragOver tritt kontinuierlich während der Bewegung der Maus auf. Bei manchen Drag&Drop-Anwendungen können Sie hier eine Art Vorschau realisieren, die anzeigt, was passiert, wenn die Maustaste jetzt losgelassen wird.
Maus bewegen
Eine andere Funktion der DragOver-Ereignisprozedur könnte darin bestehen, im TreeView-Steuerelement die hierarchische Liste auseinander zu klappen (wie dies auch im Windows-Explorer der Fall ist, wenn Sie Dateien in ein anderes Verzeichnis verschieben oder kopieren möchten). Je nachdem, ob Zustandstasten wie (Shift) oder (Strg) gedrückt sind, können Sie via e.Effect auch hier das Aussehen der Maus verändern, um so optisch zwischen einer Verschiebe- und einer Kopieroperation zu unterscheiden.
왘 DragLeave tritt analog zu DragEnter auf, wenn die Maus das Steuerelement wieder verlässt. Im Regelfall ist es nicht notwendig, eine Prozedur zu diesem Ereignis zu schreiben.
Maus herausbewegen
157
WINFORM.fm Seite 158 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Maustaste loslassen
왘 DragDrop tritt auf, wenn der Anwender die Maustaste loslässt, die Daten also fallen gelassen werden.
Ereignisse für den Drag&Drop-Initiator
Für das Steuerelement, das die Drag&Drop-Operation initiiert hat (gewissermaßen der Sender), tritt das Ereignis QueryContinueDrag kontinuierlich während der Drag&Drop-Operation auf. In der Prozedur kann die Operation je nach Tastatureingabe abgebrochen (e.Action = Drag-Action.Cancel) oder aber vorzeitig abgeschlossen werden (e.Action = DragAction.Drop). Normalerweise brauchen Sie keine QueryContinueDrag-Ereignisprozedur zu schreiben, weil die Drag&Drop-Operation durch (Esc) ohnedies automatisch beendet wird und andere Aktionen selten erforderlich sind.
Parameter der Ereignisprozeduren DragEnter, -Over und -Leave An die Ereignisprozeduren zu DragEnter, DragOver und DragLeave wird mit sender ein Verweis auf das Steuerelement übergeben, das die Daten empfängt (!) und mit e ein Objekt der Klasse DragEventArgs. Tabelle Tabelle 9.1 fasst die Eigenschaften dieser Klasse zusammen. e.AllowedEffect
gibt an, welche Drag&Drop-Operationen zulässig sind. Diese Eigenschaft enthält die DragDropEffects-Kombination, die bei DoDragDrop im zweiten Parameter angegebenen wurde.
e.Data
verweist auf ein IDataObject mit den Daten, die durch die Drag&Drop-Operation übertragen werden. Die Auswertung der Daten ist im vorigen Abschnitt beschrieben. (Die Zwischenablage überträgt Daten ebenfalls als IDataObject.)
e.Effect
gibt das Aussehen der Maus an. e.Effect muss insbesondere in der DragEnter-Prozedur eingestellt werden, andernfalls wird der Mauscursor als Eintritt verboten dargestellt und es kann kein DragDrop-Ereignis auftreten. Bei der Einstellung von e.Effect sind nur die Werte zulässig, die mit e.AllowedEffect angegeben wurden. Falls je nach dem Zustand von (Strg), (Shift) oder (Alt) unterschiedliche Drag&Drop-Operationen möglich sind, sollte in der DragOver-Prozedur e.KeyState ausgewertet werden und e.Effect entsprechend eingestellt werden.
e.KeyState
enthält den Zustand der Tasten (Strg), (Shift) oder (Alt) sowie der Maustasten. Aus unerfindlichen Gründen gilt KeyState als Integer-Wert, d.h., es gibt keine Aufzählung (Enum) mit den zulässigen KeyState-Zuständen. Stattdessen enthält die Online-Hilfe die folgende Tabelle:
Tabelle 9.1: Eigenschaften der DragEventArgs-Klasse
158
WINFORM.fm Seite 159 Dienstag, 20. August 2002 3:48 15
Symbolleiste verschieben
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
1 linke Maustaste 2 rechte Maustaste 4 (Shift)-Taste 8 (Strg)-Taste 16 mittlere Maustaste 32 (Alt)-Taste Da beliebige Kombinationen dieser Werte zulässig sind, müssen Sie zur Auswertung And verwenden. Die folgende Zeile stellt fest, ob die mittlere Maustaste gedrückt ist: If (e.KeyState And 16) = 16 Then ... Einen besser lesbaren Code erhalten Sie, wenn Sie e.KeyState gleich ganz ignorieren und die Informationen über den Zustand der Tasten aus MouseButtons bzw. ModfiersKeys entnehmen. e.X und e.Y
enthält die absoluten Mauskoordinaten, die Ihnen aber wahrscheinlich nicht viel helfen werden. Zur Ermittlung der relativen Mauskoordinaten für das Steuerelement ctrl können Sie die folgende Anweisung verwenden: Dim pt As Point = _ ctrl.PointToClient(Me.MousePosition)
Tabelle 9.1: Eigenschaften der DragEventArgs-Klasse (Forts.)
9.3 Symbolleiste verschieben Bei dem in Abbildung 9.2 dargestellten Beispielprogramm können Sie die Symbolleiste per Drag&Drop an einen der vier Fensterränder verschieben. Das Beispiel demonstiert also das Verschieben eines Steuerelements durch Drag&Drop. Das Fenster besteht aus einem ToolBar-Steuerelement (per Default mit Dock=Top) und einem Panel-Steuerelement mit Dock=Fill, das zwei Buttons enthält. Beim Verschieben der Symbolleiste wird die Dock-Eigenschaft geändert. Die Größe des Panel-Steuerelements passt sich automatisch an (zumindest meistens, siehe unten).
Dock-Eigenschaft
Abbildung 9.2: Die Symbolleiste kann mit der Maus verschoben werden.
159
WINFORM.fm Seite 160 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Drag&Drop initiieren Das erste Problem tritt bei der Initialisierung der Drag&Drop-Operation auf. In den MouseDown- bzw. MouseMove-Ereignisprozeduren lässt sich nämlich nicht feststellen, ob die Maus gedrückt wurde, um einen Button der Symbolleiste anzuklicken oder um die Symbolleiste zu verschieben (üblicherweise durch einen Klick außerhalb der Buttons). Die hier gewählte Vorgehensweise ist ein Kompromiss: Indem DoDragDrop in MouseMove erst frühestens 250 ms nach dem erstmaligen Drücken der linken Maustaste ausgeführt wird, sollte die Drag&DropOperation gewöhnlichen Button-Klicks nicht in die Quere kommen. Ärgerlich ist aber, dass eine Drag&Drop-Operation auch über einem Button initiiert werden kann. Das sieht für den Anwender so aus, als würde er nur den Button und nicht die ganze Symbolleiste verschieben. Ich habe aber leider keine Lösung für das Problem gefunden. Der Toolbar-Klasse fehlt leider eine Methode, um anhand der Mausposition den darunter befindlichen Button zu ermitteln. (Die von Control vererbte Methode GetChildAtPoint kann nicht angewandt werden, weil ToolBarButtons nicht als Steuerelemente, sondern nur als Komponenten gelten.) ' Beispiel clip-dragndrop\drag-and-drop-toolbar Dim mouseDownTime As Date = Now ' Toolbar verschieben (nur wenn kein Button ' angeklickt wurde) Private Sub ToolBar1_MouseDown(...) _ Handles ToolBar1.MouseDown If e.Button = MouseButtons.Left Then _ mouseDownTime = Now End Sub Private Sub ToolBar1_MouseMove(...) _ Handles ToolBar1.MouseMove If e.Button = MouseButtons.Left And _ (Now > mouseDownTime.AddMilliseconds(250)) _ Then ToolBar1.DoDragDrop(ToolBar1, _ DragDropEffects.Move) End If End Sub
Drop-Ereignisse verarbeiten In der DragEnter-Ereignisprozedur wird getestet, ob es sich bei den Drag&Drop-Daten um ein ToolBar-Objekt handelt. In diesem Fall wird der Mauscursor mit e.Effect=Move auf das Verschiebesymbol eingestellt.
160
WINFORM.fm Seite 161 Dienstag, 20. August 2002 3:48 15
Symbolleiste verschieben
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' visuelles Feedback Private Sub Panel1_DragEnter( _ ByVal sender As Object, _ ByVal e As Windows.Forms.DragEventArgs) _ Handles Panel1.DragEnter If e.Data.GetDataPresent(ToolBar1.GetType()) _ Then e.Effect = DragDropEffects.Move End Sub
Bei den DragOver- und DragDrop-Ereignissen wird jeweils derselbe Code ausgeführt (beachten Sie, dass mit Handles zwei Ereignisse angegeben sind!): Falls sich die Maus im linken, rechten, oberen oder unteren Viertel des Panels befindet, wird die Toolbar durch eine Veränderung der DockEigenschaft dorthin verschoben. (Die Reaktion auf die Drag&Drop-Operation wird hier also sofort sichtbar, nicht erst beim Loslassen der Maustaste.) Durch die Veränderung von ToolBar1.Dock wird die Größe des Panels (Dock=Fill) normalerweise automatisch angepasst. Wenn die Toolbar von einer Seite nach unten verschoben wird, funktioniert diese automatische Anpassung aus unerklärlichen Gründen aber nicht. (Es handelt sich hier wohl um einen Fehler in der Dock-Logik.) Um den Fehler zu umgehen, wird die Symbolleiste vorrübergehend am oberen Fensterende angedockt. ' Drag&Drop-Operation bereits hier ausführen Private Sub Panel1_DragOverDrop( _ ByVal sender As Object, _ ByVal e As Windows.Forms.DragEventArgs) _ Handles Panel1.DragOver, Panel1.DragDrop Dim pt As Point = _ Panel1.PointToClient(Me.MousePosition) ' wenn es mehrere ToolBars gibt, muss mit ' e.Data.GetData(...) differenziert werden, ' welche Toolbar verschoben wird If e.Data.GetDataPresent(ToolBar1.GetType()) _ Then If pt.Y < Panel1.ClientSize.Height / 4 Then ToolBar1.Dock = DockStyle.Top ElseIf pt.Y > _ (Panel1.ClientSize.Height * 0.75) Then ' umgeht einen Fehler bei der Logik von ' Panel.Dock=Fill If ToolBar1.Dock = DockStyle.Left Or _ ToolBar1.Dock = DockStyle.Right Then ToolBar1.Dock = DockStyle.Top End If ToolBar1.Dock = DockStyle.Bottom ElseIf pt.X < Panel1.ClientSize.Width / 4 _
161
WINFORM.fm Seite 162 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Then ToolBar1.Dock = DockStyle.Left ElseIf pt.X > _ (Panel1.ClientSize.Width * 0.75) Then ToolBar1.Dock = DockStyle.Right End If End If End Sub
9.4 Drag&Drop zwischen ListViewSteuerelementen Abbildung 9.3 zeigt ein Beispielprogramm, bei dem Sie Listeneinträge zwischen zwei ListView-Steuerelementen hin und her verschieben und kopieren können. Dabei gelten folgende Regeln:
왘 Wenn Einträge von einem Listenfeld zum anderen verschoben werden, werden die Einträge je nach Zustand der (Strg)-Taste kopiert oder verschoben. 왘 Innerhalb desselben Listenfelds werden Einträge automatisch kopiert. (Ein Verschieben ist hier ja nicht sinnvoll.) 왘 Das Verschieben und Kopieren funktioniert auch zwischen verschiedenen Instanzen des Programms (wenn das Programm mehrfach gestartet wird). Beim Programmstart wird das erste Listenfeld mit einer Liste aller vordefinierten Farben initialisiert (Prozedur Form1_Load).
Abbildung 9.3: Drag&Drop zwischen zwei ListView-Steuerelementen
162
WINFORM.fm Seite 163 Dienstag, 20. August 2002 3:48 15
Drag&Drop zwischen ListView-Steuerelementen
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Drag&Drop initiieren Die Drag&Drop-Operation wird für beide Steuerelemente in der ItemDrag-Ereignisprozedur gestartet. An DoDragDrop wird ein String-Feld mit den Texten der zu kopierenden Einträge übergeben. Die Variable dragdropsource enthält während der Drag&Drop-Operation einen Verweis auf das Steuerelement, das die Operation inititiert hat. Diese Information ist erforderlich, damit später in den Drop-Ereignisprozeduren festgestellt werden kann, ob die Drag&Drop-Operation innerhalb desselben oder zwischen zwei unterschiedlichen ListView-Steuerelementen erfolgt.
ItemDrag-Ereignis
Eine Besonderheit des Beispielprogramms besteht darin, dass der Rückgabewert von DoDragDrop ausgewertet wird: Wenn die Drag&DropOperation als Verschiebeoperation durchgeführt wurde (DragDropEffects.Move), dann werden die in ein anderes Steuerelement kopierten Listeneinträge aus dem Startlistenfeld entfernt.
Verschiebeoperation
' Beispiel clip-dragndrop\drag-and-drop-listview ' merkt sich die Quelle der Drag&Drop-Operation Dim dragdropsource As ListView ' Drag&Drop inititieren Private Sub ListViews_ItemDrag( _ ByVal sender As Object, _ ByVal e As Windows.Forms.ItemDragEventArgs) _ Handles ListView1.ItemDrag, ListView2.ItemDrag Dim Dim Dim Dim
i As Integer lv As ListView = CType(sender, ListView) lvitem As ListViewItem result As DragDropEffects
' Listeneinträge in ein String-Feld kopieren Dim items(lv.SelectedItems.Count - 1) _ As String For Each lvitem In lv.SelectedItems items(i) = lvitem.Text i += 1 Next ' Quelle der Drag&Drop-Operation speichern dragdropsource = lv ' Drag&Drop starten result = lv.DoDragDrop(items, _ DragDropEffects.Copy Or _ DragDropEffects.Move)
163
WINFORM.fm Seite 164 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
' bei Verschiebeoperation: verschobene Objekte 'löschen If result = DragDropEffects.Move Then For Each lvitem In lv.SelectedItems lv.Items.Remove(lvitem) Next End If ' Quelle der Drag&Drop-Operation wieder ' löschen dragdropsource = Nothing End Sub
Drop-Ereignisse verarbeiten Eine Ereignisprozedur für vier Ereignisse
Die Ereignisse DragEnter und DragOver werden für beide Listenfelder in einer einzigen Prozedur verarbeitet (siehe die vierteilige Handles-Liste!). Die Prozedur testet zuerst, ob die Daten als String-Feld vorliegen. (Wenn das nicht der Fall ist, handelt es sich um eine andere Drag&Drop-Operation, die vom Programm nicht verarbeitet werden kann.) Je nachdem, ob (Strg) gedrückt wird und ob das Ausgangslistenfeld (dragdropsource) und das Ziellistenfeld (sender) unterschiedlich sind, wird als Effekt Move oder Copy eingestellt. ' Feedback Private Sub ListViews_DragEnterOver( _ ByVal sender As Object, _ ByVal e As Windows.Forms.DragEventArgs) _ Handles ListView1.DragEnter, _ ListView2.DragEnter, ListView1.DragOver, _ ListView2.DragOver ' falls die richtigen Daten enthalten sind If e.Data.GetDataPresent( _ GetType(String())) Then 'falls Strg nicht gedrückt ist und 'Quelle und Ziel unterschiedlich sind: 'Move() If (Me.ModifierKeys And Keys.Control) = _ Keys.None And _ Not (sender Is dragdropsource) Then e.Effect = DragDropEffects.Move Else e.Effect = DragDropEffects.Copy End If End If End Sub
In DragDrop werden die übergebenen Daten dann in das entsprechende Listenfeld kopiert.
164
WINFORM.fm Seite 165 Dienstag, 20. August 2002 3:48 15
Datei-Drop aus dem Windows-Explorer
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
' Drag&Drop-Daten kopieren Private Sub ListViews_Dragdrop( _ ByVal sender As Object, _ ByVal e As Windows.Forms.DragEventArgs) _ Handles ListView1.DragDrop, ListView2.DragDrop Dim s, items() As String Dim lvi As ListViewItem Dim lv As ListView = CType(sender, ListView) If e.Data.GetDataPresent( _ GetType(String())) Then ' Daten kopieren items = CType(e.Data.GetData( _ GetType(String())), String()) For Each s In items lv.Items.Add(s) Next End If End Sub
9.5 Datei-Drop aus dem Windows-Explorer Im Beispielprogramm können zuvor im Windows-Explorer markierte Dateien per Drag&Drop abgelegt werden. Die Dateinamen werden im Listenfeld im oberen Teil des Fensters angezeigt. Wenn Sie einen Dateinamen anklicken, wird der Inhalt der Datei im unteren Fensterabschnitt angezeigt. (Das funktioniert nur dann zufriedenstellend, wenn es sich um UTF8-Textdateien handelt.)
Abbildung 9.4: Drag&Drop aus dem Windows-Explorer
Bei Datei-Drag&Drop-Ereignissen enthält e.Data ein Objekt des Typs DataFormats.FileDrop. Wenn derartige Daten in der DragEnter-Ereignisprozedur festgestellt werden, wird e.Effect = Link ausgeführt. (Die Maus wird dann mit einem runden Pfeil dargestellt.)
165
WINFORM.fm Seite 166 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
9 Zwischenablage und Drag&Drop
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
An die DragDrop-Ereignisprozedur wird mit e.Data ein String-Feld übergeben. Da GetData ein Object zurückgibt, muss mit CType eine Umwandlung (casting) in String() durchgeführt werden. Die im Feld enthaltenen Namen werden in das Listenfeld eingetragen. Sobald ein Eintrag des Listenfelds ausgewählt wird, versucht die SelectedIndexChanged-Ereignisprozedur die Datei in das Textfeld zu laden. ' Beispiel clip-dragndrop\drag-and-drop-explorer ' Feedback für FileDrop-Ereignisse Private Sub Form1_DragEnter( _ ByVal sender As Object, _ ByVal e As Windows.Forms.DragEventArgs) _ Handles MyBase.DragEnter If e.Data.GetDataPresent( _ DataFormats.FileDrop) Then e.Effect = DragDropEffects.Link End If End Sub ' Dateinamen des FileDrop-Ereignisses in ListBox ' einfügen Private Sub Form1_DragDrop( _ ByVal sender As Object, _ ByVal e As Windows.Forms.DragEventArgs) _ Handles MyBase.DragDrop Dim filenames(), s As String If e.Data.GetDataPresent( _ DataFormats.FileDrop) Then ListBox1.Items.Clear() filenames = CType( _ e.Data.GetData(DataFormats.FileDrop), _ String()) For Each s In filenames ListBox1.Items.Add(s) Next End If End Sub ' die in der Listbox angezeigte Datei im ' Textformat anzeigen Private Sub ListBox1_SelectedIndexChanged(...) _ Handles ListBox1.SelectedIndexChanged If IsNothing(ListBox1.SelectedItem) Then Exit Sub End If Dim sr As IO.StreamReader Try sr = New IO.StreamReader( _ ListBox1.SelectedItem.ToString) TextBox1.Text = sr.ReadToEnd()
166
WINFORM.fm Seite 167 Dienstag, 20. August 2002 3:48 15
Datei-Drop aus dem Windows-Explorer
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
sr.Close() Catch If Not IsNothing(sr) Then sr.Close() End Try End Sub
167
WINFORM.fm Seite 168 Dienstag, 20. August 2002 3:48 15
winSIX.fm Seite 169 Dienstag, 20. August 2002 3:48 15
Stichwortverzeichnis
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
.NET
Essentials
Stichwortverzeichnis A Abort-Methode 128 ActivateMdiChild-Methode 95 ActiveControl-Eigenschaft 95 ActiveMdiChild-Eigenschaft 95 AddContentToWindowContentMethode 105 AddContentToZone-Methode 105 AddContentWithState-Methode 104 AddHandler 57 Beispiel (dynamische Steuerelemente) 62 Steuerelemente 60 Add-Methode, Controls-Aufzählung 61 AddRange-Methode 51, 121 ListView-Steuerelement 26 AddressOf-Operator 60 Beispiel (dynamische Steuerelemente) 62 AfterSelect-Ereignis 30 AllowColumnReorder-Eigenschaft 26 AllowDrop-Eigenschaft 156 AllowedEffect-Eigenschaft, DragEventArgs-Klasse 158 Andocken von Steuerelementen 15 Application-Klasse 44 Asynchroner Methodenaufruf (BeginInvoke) 134 Aufzählung 14 AutoScaleBaseSize-Eigenschaft 55 AutoScale-Eigenschaft 54 AutoScroll-Eigenschaft 38, 63 AutoSize-Konstante 37 B BeginInvoke-Methode 134 BeginUpdate-Methode, ListViewSteuerelement 26 Begrüßungsbild (Splash-Bitmap) 147 Bildschirmauflösung, DPI (Windows.Forms) 53 Bitmap-Konstante 152 Bitmaps anzeigen 21, 37 BorderStyle-Eigenschaft 17 Bounds-Eigenschaft, Screen-Klasse 84 Brush-Klasse 69
ButtonClick-Ereignis (ToolBarButton) 40 Button-Steuerelement, grafisch gestalten 68
C Catch 24 CenterParent-Konstante 83 CheckBox-Steuerelement, Beispiel (Controls-Schleife) 65 Checked-Eigenschaft, MenuItem-Klasse 91 Click-Ereignis MDI-Hauptfenster 95 MenuItem-Klasse 92 Clipboard-Klasse 151 ClosedPressed-Ereignis 110 Close-Methode, MDI-Anwendungen 97 Closing-Ereignis 118, 149 MDI-Anwendungen 97 ColumnClick-Ereignisprozedur 28 Columns-Eigenschaft 25 comctl32.dll 145 CommaSeparatedValue-Konstante 152 Compare-Methode 33 Content-Klasse 102 ContextMenu-Eigenschaft 93 ContextMenu-Klasse 93 Control.ControlCollection-Klasse 65 Controls-Eigenschaft 146 Formular/Container 65 Copy-Konstante 155 CType-Funktion, dynamische Steuerelemente 62 D DataFormats-Aufzählung 152, 165 Dateiauswahldialog 117 DebuggerStepThrough-Attribut 51 DefaultExt-Eigenschaft 115 Delegate-Klasse 126, 131 Detailansicht 25 Details-Konstante 24 Dialogblätter 109 Dialoge bei Mausposition anzeigen 83 selbst verwalten 81 teilen (Splitter) 16
169
winSIX.fm Seite 170 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
Stichwortverzeichnis
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
DialogResult-Aufzählung 81 DirectoryInfo-Klasse 23 DisplayLocation-Eigenschaft 104 DisplaySize-Eigenschaft 104 Dispose-Methode, Form-Klasse 48, 82 Dock-Eigenschaft 15, 159 MDI 99 Splitter-Steuerelement 16 Docking in MDI-Anwendungen 99 DockManager-Klasse 102 DoDragDrop-Methode 155 DoEvents-Methode 46 DPI-Einstellung, Windows.Forms 53 DpiX-Eigenschaft 56 DpiY-Eigenschaft 56 Drag&Drop 155 ListView-Steuerelement 162 Symbolleiste verschieben 159 Windows-Explorer 165 DragDropEffects-Aufzählung 155 DragDrop-Ereignis 157 DragEnter-Ereignis 157 DragEventArgs-Klasse 158 DragItem-Ereignis 156 DragLeave-Ereignis 157 DragOver-Ereignis 157 DrawBackGround-Methode 77 DrawEllipse-Methode 68 DrawFocusRectangle-Methode 78 Drawing-Bibliothek 67 DrawItem-Ereignis 70 MenuItem-Klasse 74 DrawItem-Ereignisprozedur 39 DrawItemEventArgs-Klasse 70 DrawMode-Eigenschaft 70 DrawString-Methode 70
E Editor 112 Effect-Eigenschaft 158 Ellipse zeichnen 68 Enabled-Eigenschaft 47 MenuItem-Klasse 90 End, MDI-Anwendungen 97 EndInvoke-Methode 134 EndUpdate-Methode, ListViewSteuerelement 26 EnhancedMetafile-Konstante 152 Enum 14 Environment-Klasse 22
170
Ereignisprozeduren 59 Ereignisreihenfolge 44 Ereignisse C# 61 message loop (Windows.Forms) 44f. ExceptionState-Eigenschaft 128 ExitThread-Methode 144
F Families-Eigenschaft 72, 120 Feature-Eigenschaft 145 Fehlerabsicherung Beispiel (Font) 72 Multithreading 128 Fenster anordnen (MDI) 97 ausblenden 149 dynamisch erzeugen 57 erzeugen 44 Interna 43 MDI-Anwendungen 89 mehrere Fenster verwalten 81 modal anzeigen 81 ohne Rahmen 148 Programmstart (Interna) 44 teilen 16 Windows-XP-Optik 145 FileDrop-Konstante 165 FillEllipse-Methode 69 Filter-Eigenschaft 115 Find-Methode 123 FirstNode-Eigenschaft 21 FlatStyle-Eigenschaft 145 Focus-Methode 120 FontFamily-Klasse 72, 120 Font-Klasse 70 Form Designer 48 FormBorderStyle-Eigenschaft 148 FormStartPosition-Aufzählung 83 Formulare dynamisch erzeugen 57 Interna 43 Steuerelemente dynamisch einfügen 61 Windows-XP-Optik 145 Friend, Modifier-Eigenschaft 50 G GDI+ 68 GetData-Methode 152 GetDataObject-Methode 151
winSIX.fm Seite 171 Dienstag, 20. August 2002 3:48 15
Stichwortverzeichnis
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
GetDataPresent-Methode 152 GetDirectories-Methode 23 GetFiles-Methode 30 GetFolderPath-Methode 115 GetFormats-Methode 151 GetLogicalDrives-Methode 22 GetScaleBaseSize-Methode 55 Grafik 68 Graphics-Klasse 68
H Handles 59 eine Prozedur für mehrere Ereignisse 60 Hierarchisches Listenfeld 21 Hintergrundberechnung, DoEventsMethode 46 I IAsyncResult-Schnittstelle 134 IComparer-Schnittstelle 27, 33 IDataObject-Schnittstelle 151 Image-Eigenschaft 37 ImageIndex-Eigenschaft 21, 27 ImageList-Steuerelement 21, 27 Inherits 28 InitialDirectory-Eigenschaft 115 InitializeComponent-Prozedur 51 DPI-Anpassung 55 InnerControl-Eigenschaft 106 Interval-Eigenschaft 149 Invoke-Methode, WindowsProgrammierung 131 IsMdiContainer-Eigenschaft 94 IsPresent-Methode 145, 149 ItemDrag-Ereignis 162f. ItemHeight-Eigenschaft 70 MeasureItemEventArgs-Klasse 71 Items-Eigenschaft 25 ItemWidth-Eigenschaft 71 J Join-Methode 127 K KeyState-Eigenschaft 158 Kontextmenü 93 Koordinatenumrechnung 104
.NET
Essentials
L LargeIcon-Konstante 24 LargeImageList-Eigenschaft 27 LastNode-Eigenschaft 21 LayeredWindows-Konstante 149 Layout-Ereignis 64 LayoutMdi-Methode 97 LinearGradientBrush-Klasse 69 Link-Konstante 155 ListBox-Steuerelement 165 owner drawn 70 Listenfeld 24, 165 hierarchische Listen 21 ListViewItem-Klasse 25 ListViewItemSorter-Eigenschaft 27, 33 ListView-Steuerelement 24 Drag&Drop-Beispiel 162 DragItem-Ereignis 156 ListViewSubItem-Klasse 26 Load-Ereignis 22, 45 M Magic-Bibliothek 99 MainMenu-Klasse 90 Manifest-Datei 146 marshalling 130 Maus Aussehen (Drag&Drop) 157 Drag&Drop 155 MDI-Anwendungen 89 Docking 99 Menü 96 Programmende 97 MdiChildActivate-Ereignis 95 MdiChildren-Eigenschaft 95 MDI-Hauptfenster 94 MdiList-Eigenschaft 96 MdiParent-Eigenschaft 95 MDI-Subfenster 95 MeasureItem-Ereignis ListBox-Steuerelement 71 MenuItem-Klasse 74 MeasureItemEventArgs-Klasse 71 Mehrblättrige Dialoge 109 Menu.MenuItemCollection-Klasse 90 MenuButtonSize-Eigenschaft 76 Menü 89 Kontextmenü 93 MDI-Anwendung 96
171
winSIX.fm Seite 172 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
Stichwortverzeichnis
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
MenuFont-Eigenschaft 76, 91 MenuHeight-Eigenschaft 76 MenuItem-Klasse 90 MenuItems-Eigenschaft 90 MergedMenu-Eigenschaft 97 message loop 44f. MinExtra-Eigenschaft 17 MinSize-Eigenschaft 17 Modaler Dialog 81 Modified-Eigenschaft 118 MouseDown-Ereignis 155 MDI-Hauptfenster 95 Move-Konstante 155 Multithreading 125 Grundlagen 125 mehrere Fenster 141 Synchronisierung 129 Verzeichniseigenschaften 136 Windows-Nachrichtenschleife 44
N Nachrichtenschleife 44f. Name-Eigenschaft, fehlt bei MenuItemKlasse 90 New Form-Klasse 48 Steuerelemente 61 NextNode-Eigenschaft 21 Nodes-Eigenschaft 21 O Opacity-Eigenschaft 149 OpenFileDialog-Steuerelement 115, 117 Option Strict 13 OSFeature-Klasse 145, 149 OuterControl-Eigenschaft 106 OwnerDraw-Eigenschaft 74 OwnerDrawFixed-Konstante 70 Owner-drawn-ListBox 70 Owner-drawn-StatusBar 39 Owner-drawn-Steuerelemente 67 OwnerDrawVariable-Konstante 70 P Paint-Ereignis 68 Panels-Eigenschaft 38 Panel-Steuerelement, Scrollbalken anzeigen 38 Parent-Eigenschaft 21
172
ParentWindowContent-Eigenschaft 105 ParentZone-Eigenschaft 105 Pen-Klasse 68 Persönliches Verzeichnis ermitteln 115 PictureBox-Steuerelement 37 dynamisch erzeugen 121 PointToScreen-Methode 104 Popup-Ereignis ContextMenu-Klasse 94 MenuItem-Klasse 93 Popup-Menü 93 PrevNode-Eigenschaft 21 Programm, starten (Windows.Forms) 44 Programmende MDI-Anwendungen 97 Multithreading-WindowsAnwendung 135 Windows-Anwendungen 44, 84
Q QueryContinueDrag-Ereignis 158 R RadioChecked-Eigenschaft 91 Refresh-Methode 39 Resize-Ereignis 20, 121 ResumeLayout-Methode 64 RichTextBox-Steuerelement, Editor-Beispiel 112 RotateTransform-Methode 70 Rtf-Konstante 152 Run-Methode 44 S Screen-Klasse 84 Scroll-Konstante 155 SDI (Single Document Interface) 89 SelectedImageIndex-Eigenschaft 21 SelectedItems-Eigenschaft 25 SelectedNode-Eigenschaft 21 SelectionChanged-Ereignis 118 SelectionFont-Eigenschaft 120 sender-Parameter 62 SetData-Methode 152 SetDataObject-Methode 151 ShortCut-Eigenschaft 90 ShowAllContents-Methode 105 ShowContent-Methode 102 ShowDialog-Methode 81
winSIX.fm Seite 173 Dienstag, 20. August 2002 3:48 15
Stichwortverzeichnis
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Show-Methode 84 ContextMenu-Klasse 94 ShowPanels-Eigenschaft 38 ShowShortCut-Eigenschaft 90 SizeMode-Eigenschaft 37 Sleep-Methode 46, 127 SmallImageList-Eigenschaft 27 SmoothingMode-Eigenschaft 68 Sorted-Eigenschaft 21 Sortieren, ListView-Steuerelement 27, 33 Sorting-Eigenschaft 27 Sort-Methode, ListView-Steuerelement 28 Splash-Bitmap 147 Splitter-Steuerelement 16 STA (singlethread apartment) 130 Start-Methode 125 StartPosition-Eigenschaft 83 State-Eigenschaft 70 StatusBarPanel-Klasse 38 StatusBar-Steuerelement 38, 115 Magic-Docking 106 Statusbar-Steuerelement, Owner-drawnPanel 39 Statusleiste 38 Steuerelement andocken 15 dynamisch einfügen 61 dynamisch verwalten 59 Ereignisse 59 mit Drag&Drop verschieben 159 owner-drawn 67 rekursiv durchlaufen 66 Windows-XP-Optik 145 StretchImage-Konstante 37 StringFormat-Klasse 76 Style-Eigenschaft 39 ToolBarButton-Klasse 40 SubItems-Eigenschaft 26 SuspendLayout-Methode 51, 64 Symbolleiste 40 mit Drag&Drop verschieben 159 Synchronisierung, Threads 129 SyncLock 129 System.Drawing-Bibliothek 67 System.Threading-Namensraum 125 SystemInformation-Klasse 76, 91
.NET
Essentials
T TabControl-Steuerelement 109 Tag-Eigenschaft, fehlt bei MenuItemKlasse 90 Teilbares Fenster 16 Textausgabe 70 Texteditor 112 Text-Konstante 152 TextRenderingHint-Eigenschaft 70 Themes-Konstante 145 Thread 125 abbrechen 127 Ende abwarten 127 synchronisieren 129 vorübergehend unterbrechen 127 ThreadAbortException-Fehler 128 Threading-Namensraum 125 Thread-Klasse 125 Tick-Ereignis 149 TimerCallback-Klasse 126 Timer-Klasse (ThreadingNamensraum) 126 Timer-Steuerelement 149 ToolBar-Steuerelement 40 Magic-Docking 106 mit Drag&Drop verschieben 159 TopLevel-Eigenschaft 99 TreeView-Steuerelement 21 DragItem-Ereignis 156 Try 24 U Unicode in Steuerelementen 35 V Vererbung 28 Verschiebeoperationen, siehe Drag&Drop 155 Verzeichniseigenschaften ermitteln 136 Verzeichnisse anzeigen 21 View-Eigenschaft 24 W WindowContent-Klasse 104 Windows Form Designer 48 Code 48
173
winSIX.fm Seite 174 Dienstag, 20. August 2002 3:48 15
.NET
Essentials
Stichwortverzeichnis
●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
Windows XP Optik 145 WindowsDefaultLocation-Konstante 83 Windows-Explorer (Drag&Drop) 165 WithEvents 50, 59
174
Z Zeichenqualität 68 Zone-Klasse 105 Zwischenablage 151
T H E
S I G N
O F
E X C E L L E N C E
Visual Basic .NET Grundlagen, Programmiertechniken, Windows-Anwendungen Michael Kofler
Sind Sie professioneller Programmierer? Suchen Sie nach einem soliden Fundament für die VB.NET-Programmierung im bewährten und preisgekrönten Kofler-Schreibstil? Möchten Sie eine tiefgehende Einführung in die wichtigsten .NET-Bibliotheken? Dann ist das das richtige Buch für Sie: Schwerpunkte sind die Neuerungen gegenüber VB6, Variablen- und Objektverwaltung, objektorientierte Programmierung, Umgang mit Dateien, Windows-Programmierung, Windows.Forms sowie Grafik und Drucken (GDI+). Praxisnahe Beispiele veranschaulichen die Entwicklung von .NET-Projekten. Programmer’s Choice 1076 Seiten, 1 CD-ROM € 49,95 [D] / € 51,40 [A] ISBN 3-8273-1982-X