Quellcode des .NET Frameworks Microsoft Reference Source .NET Framework


VS2022 Single File Applikation| VS2022 Single File Applikation

Damit das ewige Suchen ein Ende hat, hier ist es erklärt: Veröffentlichen einer Einzeldatei-App.


Zifferneingabe-TextBox| Zifferneingabe-TextBox

Um die Eingabe in ein Steuerelementen auf Ziffern zu beschränken, muss das KeyPress-Ereignis des Steuerelements ausgewertet werden:

Private Sub TextBox1_KeyPress(sender As Object, e As KeyPressEventArgs) Handles TextBox1.KeyPress
   Select Case e.KeyChar
      Case "0"c To "9"c, Chr(8), Chr(26), Chr(3), Chr(1) ' Ziffern, Backspace, Ctrl-Z, Ctrl-C, Ctrl-A
         ' Nichts zu tun, an die interne Verarbeitung weitergeben
      Case Chr(22) ' Einfügen per Ctrl-V
         Try
            Dim Input As String
            Input = My.Computer.Clipboard.GetText().Trim ' Versuchen 'Text' aus dem Clippboard zu lesen
            Integer.Parse(Input)                         ' Repräsentiert der Text eine Ganzzahl?
            CType(sender, TextBox).SelectedText = Input  ' oder ToolStripTextBox bei TextBox im Contextmenu
         Finally
            e.Handled = True
         End Try
      Case Else ' Restliche Zeichen nicht verarbeiten
         e.Handled = True
   End Select
End Sub

Applikationseinstellungen benutzerdefiniert speichern| Applikationseinstellungen benutzerdefiniert speichern

Der Standard-Mechanismus den Visual Studio für das Speichern von benutzerspezifischen Anwendungseinstellungen anbietet, speichert diese in den Tiefen von "<user>/AppData". Dabei hinterlässt er ein ziemliches Chaos an Dateien, weil u.a. für jede Version ein eigenes Verzeichnis angelegt wird.

AppDate-Chaos

Mit folgendem einfachen Prinzip kann man hier ebenso komfortabel unter eigener Kontrolle durchführen.

Imports System.IO
Imports System.Xml.Serialization

''' <summary>
''' Muster-Klasse für das Speichern und Laden von benutzerdefinierten Anwendungseinstellungen 
''' mit individueller Festlegung des Speicherorts.
''' </summary>
Public Class AnyAppSettings
   ''' <summary>
   ''' Dummy-Property, automatisch implementierte Eigenschaft.
   ''' </summary>
   ''' <value>Irgendeine Farbe.</value>
   Public Property AnyColor As Color = Color.DarkRed 'Standardwert ist DarkRed

   Private _AnyIndex As Integer = 0
   ''' <summary>
   ''' Dummy-Property, Get + Set
   ''' </summary>
   ''' <value>Irgendein Integer.</value>
   Public Property AnyIndex As Integer
      Get
         Return _AnyIndex
      End Get
      Set(value As Integer)
         If _AnyIndex < 0 Then
            Throw New ArgumentOutOfRangeException("AnyIndex", "AnyIndex must not be negative.")
         End If
         _AnyIndex = value
      End Set
   End Property

   ''' <summary>
   ''' Speichert die öffentlichen Eigenschaften in die angegebene Datei.
   ''' </summary>
   ''' <param name="Path">Dateipfad.</param>
   ''' <remarks>Die Datei wird überschrieben.</remarks>
   Public Sub Save(Path As String)
      Dim Writer As New StreamWriter(Path, False)
      Dim ser As XmlSerializer = New XmlSerializer(GetType(AnyAppSettings))
      ser.Serialize(Writer, Me)
      Writer.Close()
   End Sub

   ''' <summary>
   ''' Liest die öffentlichen Eigenschaften aus der angegebenen Datei.
   ''' </summary>
   ''' <param name="Path">Dateipfad.</param>
   ''' <returns>Eine neue Instanz der <see cref="AnyAppSettings"/>-Klasse mit den einmgelesenen Werten.</returns>
   ''' <remarks>Schlägt das Lesen fehl, wird eine Instanz mit Standardwerten zurück geliefert.</remarks>
   Public Shared Function LoadFromFile(Path As String) As AnyAppSettings
      Dim Reader As StreamReader = Nothing
      Try
         Reader = New StreamReader(Path)
         Dim Serializer As XmlSerializer = New XmlSerializer(GetType(AnyAppSettings))
         Return Serializer.Deserialize(Reader)
      Catch
         Return New AnyAppSettings
      Finally
         If Not Reader Is Nothing Then
            Reader.Close()
         End If
      End Try
   End Function
End Class

Man legt eine Klasse an, in der alle notwendigen Einstellungen als öffentliche Properties hinterlegt sind, entweder als automatisch implementierte Properties oder mit einem expliziten Getter & Setter. Diesen Eigenschaften sollte man Standardwerte (Defaults) zuweisen. Über diese Properties könne dann die Einstellungen abgerufen oder gesetzt werden.

Zusätzlich werden zwei Methoden eingefügt, die das Speichern und zurückladen der Daten mit Hilfe eines XmlSerializer erledigen. Beim Einladen werden gespeicherte, aber aktuell nicht mehr vorhandene Eigenschaften ignoriert. Noch nicht in der Datei enthaltene Eigenschaften bekommen ihren Standardwert zugewiesen.

Will man dann später wieder doch wieder in AppData speichern, geht dies über Application.LocalUserAppDataPath. Dieser Pfad verweist auf den Ordner <user>/AppData/local/<app-name>/<app-version>/.


Application-Properties (Pfade, etc.)| Application-Properties (Pfade, etc.)

So löst Visual Studio die Application-Eigenschaften unter Windows 10 auf. Die Applikation heißt "AppDisplay". Das Programm wurde im Debug-Modus gestartet, der Solution-Ordner heißt "AppDisplaySln" der Projekt-Ordner heißt "AppDisplaySln".

Application …
.CommonAppDataPath C:\ProgramData\AppDisplay\AppDisplay\1.0.0.0
.ExecutablePath <user>\AppData\Local\Temporary Projects\AppDisplay\bin\Debug\AppDisplay.EXE
(wenn das Projekt noch nicht gespeichert wurde)
<user>\Documents\Visual Studio 2015\Projects\AppDisplaySln\AppDisplayPrj\bin\Debug\AppDisplay.EXE
.LocalUserAppDataPath <user>\AppData\Local\AppDisplay\AppDisplay\1.0.0.0
.StartupPath <user>\AppData\Local\Temporary Projects\AppDisplay\bin\Debug
(wenn das Projekt noch nicht gespeichert wurde)
  <user>\Documents\Visual Studio 2015\Projects\AppDisplaySln\AppDisplaySln\bin\Debug
.UserAppDataPath <user>\AppData\Roaming\AppDisplay\AppDisplay\1.0.0.0
.CommonAppDataRegistry HKEY_LOCAL_MACHINE\Software\AppDisplay\AppDisplay\1.0.0.0
.UserAppDataRegistry HKEY_CURRENT_USER\Software\AppDisplay\AppDisplay\1.0.0.0
.SafeTopLevelCaptionFormat {1} - {0} - {2}

Label-Größe an Text anpassen | Label-Größe an Text anpassen

Mithilfe der AutoSize-Eigenschaft eines Label-Steuerelements, dass dieses die passende Größe erhält. Standardmäßig wird die Breite (width-Eigenschaft) des Labels passend berechnet.
AutoSize

Will man jedoch erreichen, dass die Breite des Labels eine bestimmte Größe nicht überschreitet muss man die width-Komponente der MaxSize-Eigenschaft entsprechend einstellen. Die BorderStyle-Eigenschaft muss auf None stehen, sonst klappt es nicht. Der waagerechte Strich ist in Realität ein Label mit den gleichen Eigenschaften, außer der BorderStyle-Eigenschaft, wie das links daneben.
Autosize, Breite beschränkt

Achtung aufpassen: in der Entwicklungsumgebung erscheint das Label jedoch korrekt:
Abweichung im Designer

Die Size-Eigenschaft wird dann automatisch entsprechend der Vorgaben angepasst.
Automatische Einstellung der Size-Eigenschaft

Je nach dem, wie der Text umgebrochen werden kann, weicht die berechnete Breite von der vorgegeben ab. Im Beispiel ist das 1 Pixel. Dieser Effekt kann u.U. störend sein, z.B. wenn das Label eingefärbt werden soll und an ein anderes Steuerelement angrenzt. Dann gibt es je nach Text unterschiedlich große Lücken zwischen dem Label und dem anderen Steuerelement. Hier hilft folgender Trick.

Das sichtbare Label soll exakt die Breite W haben, die Höhe soll aber so gewählt werden, dass der zugewiesene Text vollständig angezeigt wird. Man nehme ein zusätzliches, unsichtbares Label zu Hilfe. Diesem weist man die MaxSize-Eigenschaft

HiddenLabel.MaxSize = New Size(W, 0)

und danach den umzubrechenden Text

HiddenLabel.Text = "Dies ist ein langer Text"

Danach kann man das sichtbare Label passend einstellen:

VisibleLabel.Size = New Size(W, HiddenLabel.Height)

Fenster in den Vordergrund bringen| Fenster in den Vordergrund bringen

Das geht recht einfach:

Public Declare Function SetForegroundWindow Lib "user32" (ByVal hwnd As IntPtr) As Boolean
...
SetForegroundWindow(Handle)

TextBox scrollen | TextBox scrollen / Textbox ans Ende scrollen

Ans Ende scrollen geht so:

Public Const WM_VSCROLL As Integer = &H115
Public Const SB_BOTTOM As Integer = 7
Public Declare Auto Function SendMessage Lib "user32" (hWnd As IntPtr, 
                                                       wMsg As Integer, wParam As IntPtr, lParam As IntPtr) As Integer

...
SendMessage(TextBox1.Handle, WM_VSCROLL, CType(SB_BOTTOM, IntPtr), IntPtr.Zero)

Im Internet wird häufig diese Methode empfohlen

  •  die aktuellen Selection-Eigenschaften speichern
  • die Selection ans Textende setzen
  • mit  ScrollToCaret zur Selection scrollen
  • die Selection-Eigenschaften wieder zurücksetzen.

 Diese Methode hat den Nachteil, dass je nach Art der TextBox (z.B. RichTextBox) der markierte Text unangenehm flackert. Die oben präsentierte Methode vermeidet diesen Nachteil.

Weitere Richtungen kann man der folgenden Aufstellung entnehmen:

Enum VScrollCommands
   SB_LINEUP = 0
   SB_LINEDOWN = 1
   SB_PAGEUP = 2
   SB_PAGEDOWN = 3
   SB_THUMBPOSITION = 4
   SB_THUMBTRACK = 5
   SB_TOP = 6
   SB_BOTTOM = 7
End Enum

WM_VSCROLL message auf MSDN.


RichTextBox SuspendLayout| RichTextBox SuspendLayout

RichTextBox.SuspendLayout hat keine Wirkung (Framework-Version 4.6.1, Windows 10). Dieser Code funktioniert:

Private Const WM_USER As Integer = &H400
Private Const EM_SETEVENTMASK As Integer = (WM_USER + 69)
Private Const WM_SETREDRAW As Integer = &HB
Private OldEventMask As IntPtr

<DllImport("user32.dll", CharSet:=CharSet.Auto)>
Private Shared Function SendMessage(hWnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
End Function

Public Sub BeginUpdate()
   SendMessage(tbOutput.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero)
   OldEventMask = SendMessage(tbOutput.Handle, EM_SETEVENTMASK, IntPtr.Zero, IntPtr.Zero)
End Sub

Public Sub EndUpdate()
   SendMessage(tbOutput.Handle, WM_SETREDRAW, CType(1, IntPtr), IntPtr.Zero)
   SendMessage(tbOutput.Handle, EM_SETEVENTMASK, IntPtr.Zero, OldEventMask)
End Sub

Gefunden auf hier auf stack overflow

Wenn man mit geschachtelten Aufrufen zu rechnen hat, ist diese Implementierung geeigneter:

Private UpdateCount As Integer = 0 ' Zählt die Schachtelung von Aufrufen mit.
''' <summary>
''' Unterbricht vorübergehend die Layoutlogik für das Steuerelement.
''' </summary>
Public Sub BeginUpdate()
   UpdateCount += 1
   If UpdateCount = 1 Then
      SendMessage(tbOutput.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero)
      OldEventMask = SendMessage(tbOutput.Handle, EM_SETEVENTMASK, IntPtr.Zero, IntPtr.Zero)
   End If
End Sub

''' <summary>
''' Nimmt die übliche Layoutlogik wieder auf und erzwingt optional ein sofortiges ausführen der 
''' ausstehenden Layoutanforderungen.
''' </summary>
''' <param name="performLayout">True, um ausstehende Layoutanforderungen auszuführen, andernfalls False.
''' Der Strandardwert ist True.</param>
Public Sub EndUpdate(Optional performLayout As Boolean = True)
   UpdateCount -= 1
   If UpdateCount <= 0 Then
      UpdateCount = 0
      SendMessage(tbOutput.Handle, WM_SETREDRAW, CType(1, IntPtr), IntPtr.Zero)
      SendMessage(tbOutput.Handle, EM_SETEVENTMASK, IntPtr.Zero, OldEventMask)
      If performLayout Then
         tbOutput.Refresh()
      End If
   End If
End Sub

UTC-Timestamp => Date und zurück| UTC-Timestamp => Date und zurück

Erstaunlich einfach:

Const UnixStartDate As DateTime = #1/1/1970#
''' <summary> 
''' Konvertiert ein Unix-Timestamp (Sekunden seit 1.1.1970 in Zeitzone UTC) 
''' in ein <see cref="Date"/>-Objekt mit der lokalen Ortszeit. 
''' </summary> 
''' <param name="UtcTimestamp">Ein Unix-Timestamp (Sekunden seit 1.1.1970 in Zeitzone UTC).</param> 
''' <returns>Ein <see cref="Date"/>-Objekt mit der lokalen Ortszeit.</returns> 
''' <remarks>Entspricht i.W. der C-Funktion char* ctime (const time_t * timer);</remarks>
Public Function VbCTime(ByVal UtcTimestamp As UInt32) As Date
   Return DateAdd("s", UtcTimestamp, UnixStartDate).ToLocalTime
End Function

''' <summary>
''' Konvertiert ein <see cref="Date"/>-Objekt in der lokalen Ortszeit
''' in ein Unix-Timestamp (Sekunden seit 1.1.1970 in Zeitzone UTC).
''' </summary>
''' <param name="NetDate">Das <see cref="Date"/>-Objekt, das konvertiert werden soll.</param>
''' <returns>Ein Unix-Timestamp, das dem <see cref="Date"/>-Objekt entspricht.</returns>
Public Function VbUnixTime(NetDate As Date) As UInt32
   Return CUInt((NetDate.ToUniversalTime - UnixStartDate).TotalSeconds)
End Function

Soll's bei UTC als Zeitzone bleiben: ToLocalTime entfernen.


Unerwünschte Eigenschaften in abgeleiteten Controls verbergen| Unerwünschte Eigenschaften in abgeleiteten Controls verbergen

Will man Standard-Eigenschaften in abgeleiteten Steuerelementen verbergen, muss man die Eigenschaft mit entsprechenden Attributen versehen. Im folgenden Beispiel wird die Eigenschaft MinimumSize verborgen:

<System.ComponentModel.Browsable(False),
 System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never),
 System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden),
 System.ComponentModel.Bindable(False)>
Public Overrides Property MinimumSize As Size

Die Methode String.Split() unterteilt eine Zeichenfolge anhand von anzugebenden Separatoren in ein Array von Teilzeichenfolgen. Dabei ist zu beachten, dass, wenn ein Separator am Ende oder am Anfang des zu splittenden Strings steht, implizit ein Leerstring vor- bzw. nachher angefügt wird. D.h. die Länge des zurückgegebenen Arrays ist immer Anzahl der enthaltenen Separatoren + 1. Hier ein paar Beispiele; der Separator ist "X".

Ausgangsstring Array-Größe Array-Elemente
"" 1 ""
"X" 2 "", ""
"XX" 3 "", "", ""
"abcd" 1 "abcd"
"Xabcd" 2 "", "abcd"
"abXcd" 2 "ab", "cd"
"abXXcd" 3 "ab", "", "cd"
"XabXcd" 3 "", "ab", "cd"
"abcdX" 2 "abcd", ""
"XabcdX" 3 "", "abcd", ""
"XabXcdX" 4 "", "ab", "cd", ""
"XXabXXcdXX" 7 "", "", "ab", "", "cd", "", ""

Wird der Parameter StringSplitOptions mit RemoveEmptyEntries belegt, werden alle Array-Elemente entfernt, die einen Leerstring enthalten.


Lokale IP-Adressen ermitteln| Lokale IP-Adressen ermitteln

Typischerweise besitzt ein PC mehrere lokale IP-Adressen. Dies ändert sich je nach dem, welche Hard- und Software man installiert hat. Eine Übersicht erhält man über den Befehl

ipconfig /all

in der Eingabeaufforderung:

ipconfig Output

Unter .NET kommt man mit der Methode NetworkInterface.GetAllNetworkInterfaces() weiter. Diese Methode liefert alle Netzwerk-Schnittstellen des PCs. Bei mir sind dies acht an der Zahl. Um eine bestimmte zu finden, benutzt man am besten die Name-Eigenschaft zur Selektion. Die Namen der Netzwerkverbindungen erhält man über die Systemsteuerung:

Netzwerkverbindungen

Will man Netzwerk-Schnittstellen eines bestimmten Typs auswählen, kann man die anderen Eigenschaften der ermittelten Schnittstellen-Objekte auswerten. Z.B. liefert eine Selektion des NetworkInterfaceType.Wireless80211 alle WLAN-Adapter. Aber Vorsicht, hier werden auch virtuelle Adapter ausgewählt, z.B. der  "Microsoft Hosted Network Virtual Adapter". Aktive Interfaces selektiert über die Eigenschaft OperationalStatus mit dem Wert OperationalStatus.Up.

Die folgende Grafik zeigt die von NetworkInterface.GetAllNetworkInterfaces() gelieferten Objekte. Das Objekt, dass zum WLAN gehört, ist aufgeblättert.

NetworkInterface.GetAllNetworkInterfaces()


Anmeldung an eine UDP-Muliticast-Gruppe| Anmeldung an eine UDP-Muliticast-Gruppe|

Will man sich an einer UDP-Multicast-Gruppe anmelden, muss man berücksichtigen, dass der PC i.d.R. mehr als ein Netzwerk-Interface besitzt (s. oben: Lokale IP-Adressen ermitteln). Deshalb muss man anderweitig dafür sorgen, dass das richtige Interface genutzt werden, oder die Methoden nutzen, die die lokale IP-Adresse mitgeben.

UdpClient.JoinMulticastGroup(GroupIP, LocalIp)

ist solch eine Methode.


VB.Net-Klassen aus XML- oder JSON-Dateien | VB.Net-Klassen aus XML- oder JSON-Dateien|

Visual Studio (VS2017) bietet eine einfache Möglichkeit aus einem XML- oder ein JSON-Dokument ein passende Klassenstruktur zu generieren. Dazu kopiert man den Inhalt eines Muster-Dokuments in den Zwischenspeicher, positioniert den Cursor einer Code-Datei in VS an die Stelle, an die die Klassenstruktur eingefügt werden soll und lässt dann mit dem Menü-Punkt "Bearbeiten"->"Inhalte einfügen" die Klassenstruktur generieren.

Menü

Die Datei lässt sich dann einfach einlesen. Hier ein Bespiel bei dem eine GPX-Datei als Basis genommen wurde. Der umhüllende Typ (Klasse) hat den Namen gpx. Eingelesen wird in das Objekt gpxDoc.

Dim gpxDoc As gpx
Dim reader As New System.Xml.Serialization.XmlSerializer(GetType(gpx))
Dim file As New System.IO.StreamReader(filePath)
gpxDoc = CType(reader.Deserialize(file), gpx)

Standard-Dialoge positionieren| Standard-Dialoge positionieren

Die Standard-Dialoge in .NET, z.B. der ColorDialog zur Auswahl von Farben, erleichtern das Erstellen von Anwendungen enorm. Leider lassen sich nicht alle Eigenschaften über das Standard-API einstellen. Die Dialoge werden z.B. immer in der Bildschirmmitte platziert. Auf die hier beschriebene Weise erhält man Zugriff auf weitere Merkmale der Dialoge.

Alle Standard-Dialoge sind von der Klasse CommonDialog abgeleitet. CommonDialog definiert die geschützte Methode HookProc. die überschrieben werden kann, um einem Standarddialogfeld spezifische Funktionen hinzuzufügen. Sie entspricht i.W. der WndProc für Fenster.

In der Version 4.5 des .NET Frameworks besitzt die Methode folgenden Code

Protected Overridable Function HookProc(ByVal hWnd As IntPtr, ByVal msg As Integer, 
                                        ByVal wparam As IntPtr, ByVal lparam As IntPtr) As IntPtr
   If (msg = &H110) Then ' WM_INITDIALOG
      CommonDialog.MoveToScreenCenter(hWnd) ' ===== Positioniert das Fenster =====
      Me.defaultControlHwnd = wparam
      UnsafeNativeMethods.SetFocus(New HandleRef(Nothing, wparam))
   ElseIf (msg = 7) Then ' WM_SETFOCUS 
      UnsafeNativeMethods.PostMessage(New HandleRef(Nothing, hWnd), &H451, 0, 0)
   ElseIf (msg = &H451) Then
      UnsafeNativeMethods.SetFocus(New HandleRef(Me, Me.defaultControlHwnd))
   End If
   Return IntPtr.Zero
End Function

In Zeile 4 steht der Aufruf der Methode MoveToScreenCenter, die den Dialog auf die Bildschirmmitte platziert. MoveToScreenCenter ruft SetWindowPos zum Positionieren auf:

SetWindowPos(hWnd, 0, x, y, 0, 0, &H15)

Der Wert &H15 für den Wert von Flags ergibt sich aus der Kombination von SWP_NOACTIVATE, SWP_NOZORDER und SWP_NOSIZE. Die Anweisung wirkt sich also nur auf die Position des Fensters aus.

Wenn man den Standard-Dialog ableitet, kann man auf HookProc überschreiben und dem Dialogfenster eine neue Position geben:

Public Class LocatableColorDialog
   Inherits ColorDialog

   Private Location As Point

   Protected Overrides Function HookProc(ByVal hWnd As IntPtr, ByVal msg As Integer, 
                                          ByVal wparam As IntPtr, ByVal lparam As IntPtr) As IntPtr
      MyBase.HookProc(hWnd, msg, wparam, lparam)
      If (msg = &H110) Then 'WM_INITDIALOG 
         SetWindowPos(hWnd, IntPtr.Zero, Location.X, Location.Y, 0, 0, &H15)
      End If
      Return IntPtr.Zero
   End Function

   Public Shadows Function ShowDialog(Location As Point) As DialogResult
      Me.Location = Location
      Return MyBase.ShowDialog
   End Function
End Class

SetWindowPos hat folgende Signatur:

Private Declare Function SetWindowPos Lib "user32.dll" (
   ByVal hWnd As IntPtr,
   ByVal hWndInsertAfter As IntPtr,
   ByVal X As Integer,
   ByVal Y As Integer,
   ByVal cx As Integer,
   ByVal cy As Integer,
   ByVal uFlags As Integer) As Boolean