Ini Datei lesen und schreiben in VB.NET – Einstellungen speichern / laden

Ini Datei lesen und schreiben in VB NET – Einstellungen speichern und laden
Ini Datei lesen und schreiben VB.NET – Einstellungen speichern und laden

Inhaltsverzeichnis

Ini Datei lesen und schreiben

In diesem Beispiel zeige ich, wie man in VB.NET eine eine Ini Datei lesen und schreiben kann. Dadurch können wir im Anschluss Einstellungen für das Programm speichern und wieder laden. Ferner werden dazu Bestandteile aus der .NET API importiert und letztendlich verwendet, Diese sind allerdings optional und nur als weiteres Beispiel zu sehen.

warningDu hast keine Lust alles selbst zu coden? Kein Problem, verwende einfach mein kostenfreies INI-Dateien NuGet-Paket (engl. Dokumentation), Welches Dir die ganze INI-Arbeit abnimmt – einfach und komfortabel.

Trotz des alten Flers haben Konfigurationsdateien auch heutzutage durchaus noch ihren Sinn, bzw. Ihre Daseinsberechtigung. Es ist zum Beispiel nicht immer gegeben, dass eine Datenbank, oder ähnliche Sachen zur Verfügung stehen.

Video auf YouTube ansehen

Hier findest du mein YouTube-Video zum Thema Einstellungen speichern und laden via Konfigurationsdatei in VB.NET.

Wie Du schnell starten kannst!

Willst Du nicht viel fummeln und einfach sofort starten können? Dann verwende mein NuGet-Paket, Welches Dir jegliche INI-Arbeit abnimmt – Beitrag besuchen, installieren, loslegen.

Möchtest Du alles selbst machen und einfach, schnell und objektorientiert arbeiten:

  • Lade mein Beispielprojekt herunter
  • Kopiere die drei Klassen Ini, Section, Entry in dein Projekt
  • Lade eine Ini-Objekt mit der statischen (shared) „Ini.Load„-Methode
  • Füge Sektionen und Einträge mit den Methoden „AddSection“ & „AddEntry“ hinzu
  • Speichere die Ini-Datei mit der „Save„-Methode der Ini-Klasse
  • Gehe für mehr Beispiele einfach nach unten

Auch hier setzt Du alles selbst um und kannst die Windows-API Funktionen verwenden, gehe wie folgt vor:

Wozu Einstellungen verwalten?

Spätestens nachdem sich ein Benutzer an eine Software einigermaßen gewöhnt hat, wird es nicht mehr allzu lang dauern, bis er Einstellungen speichern und laden möchte.

Der Mensch – Ein Gewohnheitstier

Gewöhnung ist hier das richtige Triggerwort! Wer sich einmal an etwas gewöhnt hat, möchte dies meist nicht mehr missen – und das aus gutem Grund.

Wer sich in einer Umgebung heimisch fühlt, kann effizienter und vor Allem auch entspannter arbeiten.

Oder wie würdest Du es finden, wenn Deine Desktop-Icons jeden Tag an einer anderen Stelle wären 😉!

Die Initialisierungsdatei – kurz Ini

Die Initialisierungsdatei kurz Ini – findet Ihren Ursprung in der Zeit vor der Windows-Registry.

Bis zur Einführung der Registrierungsdatenbank mit Microsoft Windows NT 3.1 war das INI-Format das einzige Dateiformat zur Speicherung von Programm-Konfigurationen, das durch die WinAPI unterstützt wurde.

Wikipedia – Initialisierungsdatei

Aufbau der Ini

Der Aufbau der Initialisierungsdatei ist ein relativ einfacher Zusammenbau aus Schlüssel- und Wert-Paaren (engl. Key-Value Pairs).

Die Schlüssel-Wert-Kombinationen werden durch sogenannte Sektionen unterteilt.

Durch den einfachen Aufbau wurde ein Ziel der Ini-Datei erreicht: Sie sollten von Menschen einfach zu lesen sein und betriebssystemübergreifend verwendet werden können.

Ini-Datei Beispiel

Dem o. g. Wikipedia-Artikel können wir folgendes Beispiel zum Aufbau einer Ini-Datei entnehmen:

[Sektion1]
Schlüssel=Wert
[Sektion2]
Schlüssel=Wert
Schlüssel2=Wert

Dort sehen wir einmal den Abschnitt, bzw. die Sektion Nummer 1 mit einem Schlüssel-Wert-Paar.

Und die zweite Sektion mit letztendlich 2 Schlüssel-Wert-Paaren.

.NET API-Funktionen deklarieren

Damit wir von einer Ini Datei lesen, bzw. in eine Ini Datei schreiben können, müssen wir zuerst einmal die passenden .NET API-Funktionen deklarieren.

ByVal – Häufig in Online-Beispielen

Vermutlich wirst Du in Codes die du online findest häufig das „ByVal„-Schlüsselwort sehen.

Was das Schlüsselwort bewirkt, bzw. wofür es ist, findet sicher an einer anderen Stelle seinen Platz, hier würde es den Rahmen sprengen bzw. thematisch nicht passen.

Für unsere Deklarationen ist es nicht notwendig, da „primitive“ Parameter – wenn nicht anders angegeben – sowieso standardmäßig By Value“ übergeben werden.

GetPrivateProfileString API-Funktion

Als erstes wäre da die „GetPrivateProfileString„-Funktion, Welche dafür verantwortlich ist, Informationen aus einer INI-Datei zu lesen.

<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function GetPrivateProfileString(
                        lpAppName As String,
                        lpKeyName As String,
                        lpDefault As String,
                        lpReturnedString As StringBuilder,
                        nSize As Integer,
                        lpFileName As String) As Integer
End Function

WritePrivateProfileString API-Funktion

Dann wäre die „WritePrivateProfileString„-Funktion an der Reihe, Welche dafür verantwortlich ist, Informationen in eine INI-Datei zu schreiben.

<DllImport("kernel32.dll", SetLastError:=True)>
Private Shared Function WritePrivateProfileString(
                    lpAppName As String,
                    lpKeyName As String,
                    lpString As String,
                    lpFileName As String) As Boolean
End Function

Der erste Code im Einsatz – Rohe API

Ini Datei lesen und schreiben mit rohen Win-API Funktionen
Ini Datei lesen und schreiben mit rohen Win-API Funktionen

Mit diesen beiden Windows API-Funktionen können wir eigentlich schon alles Notwendige erledigen.

Wir können Einstellungen aus einer Sektion Laden und Einstellungen in eine Sektion speichern.

Schön sieht das Ganze bisher aber noch nicht so ganz aus, daher werde ich das später noch in eine andere Hilfsfunktion wrappen.

Einstellung speichern/schreiben

Mit diesem kleinen Code-Snippet schreiben wir im aktuellen Verzeichnis der Anwendung eine „config.ini“ Datei.

Dort wird unter der Sektion namens „Sektion1ein Schlüssel namens „Schlüsselangelegt, bzw. überschrieben.

In diesem Schlüssel speichern wir den Wert „Wert“ ab.

Dim wroteSuccessfully = WritePrivateProfileString("Sektion1", "Schlüssel", "Wert", ".\config.ini")

Einstellung laden/lesen

Etwas komplizierter sieht es dann beim Lese-Beispiel aus:

Dim bufferLength = 1024
Dim stringBuilder = New StringBuilder(bufferLength)
Dim length = GetPrivateProfileString("Sektion1", "Schlüssel", "Standardwert", stringBuilder, bufferLength, ".\config.ini")
Dim readValue = stringBuilder.ToString().Substring(0, length)
' do something with readValue

Vereinfachung durch Klasse – Ini Datei lesen und schreiben

In den nächsten Zeilen habe ich mal den ersten Versuch einer kleinen Ini-Klasse dargestellt.

Diese soll den Zugriff auf die Ini-Funktionalitäten kapseln, vereinfachen und natürlich auch verständlicher machen. Dadurch können wir dann ganz einfach eine Ini Datei lesen und schreiben.

Zu einem späteren Zeitpunkt werde ich in diesem Beitrag ggf. auch eine komplett objektorientierte Variante erstellen.

Die erste Ini-Klasse

Hier siehst Du die erste kleine Ini-Klasse, dadurch werden wir den Zugriff auf die API-Funktionen vereinfachen.

Ich habe die „rohenAPI-Funktionen beabsichtigt als „Public“ drin gelassen, um auch einen direkten Zugriff zu ermöglichen.

Auch wenn die Klasse zuerst rudimentär zu sehen ist, hilft Sie uns dennoch eine Ini Datei lesen und schreiben zu können.

Imports System.Runtime.InteropServices
Imports System.Text

Public Class Ini

    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function GetPrivateProfileString(lpAppName As String,
                        lpKeyName As String,
                        lpDefault As String,
                        lpReturnedString As StringBuilder,
                        nSize As Integer,
                        lpFileName As String) As Integer
    End Function

    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function WritePrivateProfileString(lpAppName As String,
                        lpKeyName As String,
                        lpString As String,
                        lpFileName As String) As Boolean
    End Function


    Public Property Sections As List(Of Section)

    ''' <summary>
    ''' Helper to retrieve the section of the given index
    ''' </summary>
    ''' <exception cref="IndexOutOfRangeException"></exception>
    Default Public ReadOnly Property Section(index As Integer) As Section
        Get
            Dim foundSection As Section = Sections.ElementAtOrDefault(index)
            If foundSection Is Nothing Then
                Throw New IndexOutOfRangeException($"No section at the given index of {index} found")
            End If
            Return foundSection
        End Get
    End Property

    ''' <summary>
    ''' Helper to retrieve the entry of the given key
    ''' </summary>
    ''' <exception cref="IndexOutOfRangeException"></exception>
    Default Public ReadOnly Property Section(name As String) As Section
        Get
            Dim foundSection As Section = Sections.SingleOrDefault(Function(x) x.Name.ToLower() = name)
            If foundSection Is Nothing Then
                Throw New IndexOutOfRangeException($"No section for the name {name} found")
            End If
            Return foundSection
        End Get
    End Property

    Sub New()
        Sections = New List(Of Section)
    End Sub

    ''' <summary>
    ''' Helper function to read single values if needed
    ''' </summary>
    Public Shared Function ReadValue(section As String, key As String, [default] As String, file As String) As String
        Dim bufferLength = 1024
        Dim stringBuilder = New StringBuilder(bufferLength)
        Dim length = GetPrivateProfileString(section, key, [default], stringBuilder, bufferLength, file)
        Dim bufferedValue = stringBuilder.ToString()
        Dim value = bufferedValue.Substring(0, length)
        Return value
    End Function

    ''' <summary>
    ''' Helper function to write single values if needed
    ''' </summary>
    Public Shared Function WriteValue(section As String, key As String, value As String, file As String) As Boolean
        Dim wroteSuccessfully = WritePrivateProfileString(section, key, value, file)
        Return wroteSuccessfully
    End Function

    Public Shared Function Load(contents As String) As Ini
        Dim lines = LoadIniLinesIgnoringEmpty(contents)
        Dim ini = New Ini()
        Dim lastSection As Section = Nothing
        For Each line In lines
            Dim isSection = line.StartsWith("[")
            If isSection Then
                Dim sectionName = Section.GetNameFromLine(line)
                ini.AddSection(sectionName)
                lastSection = ini.Sections.Last()
            Else
                Dim entry = IniFileExample.Entry.FromLine(line)
                lastSection.AddEntry(entry)
            End If
        Next
        Return ini
    End Function

    ''' <summary>
    ''' Adds a section by name
    ''' </summary>
    ''' <param name="name">The name for the section to add</param>
    ''' <exception cref="SectionAlreadyExistsException"></exception>
    Public Sub AddSection(name As String)
        ThrowOnDuplicateSectionName(name)
        Dim section = New Section(name)
        Sections.Add(section)
    End Sub

    ''' <summary>
    ''' Adds a section by instance
    ''' </summary>
    ''' <param name="section">The section to add</param>
    ''' <exception cref="SectionAlreadyExistsException"></exception>
    Public Sub AddSection(section As Section)
        ThrowOnDuplicateSectionName(section.Name)
        Sections.Add(section)
    End Sub

    ''' <summary>
    ''' Helper function to avoid duplicated code
    ''' </summary>
    ''' <param name="name">The <see cref="Section"/>-Name to check for</param>
    ''' <exception cref="SectionAlreadyExistsException"></exception>
    Private Sub ThrowOnDuplicateSectionName(name As String)
        Dim existentSection = Sections.SingleOrDefault(Function(x) x.Name.ToLower() = name.ToLower())
        If existentSection IsNot Nothing Then
            Throw New SectionAlreadyExistsException("A section with the name " & name & " already exists")
        End If
    End Sub

    Private Shared Function LoadIniLinesIgnoringEmpty(contents As String) As List(Of String)
        Dim lines = contents.Split(New String() {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
        Return lines.ToList()
    End Function

    ''' <summary>
    ''' Loads the ini instance from the given filepath
    ''' </summary>
    ''' <param name="filepath">The filepath to the ini file</param>
    ''' <param name="encoding">The encoding to use - defaults to UTF8</param>
    ''' <returns>The parsed ini file instance</returns>
    Public Shared Async Function Load(filepath As String, Optional encoding As Encoding = Nothing) As Task(Of Ini)
        If encoding Is Nothing Then
            encoding = Encoding.UTF8
        End If
        Dim contents = Await IO.File.ReadAllTextAsync(filepath, encoding)
        Dim ini = Load(contents)
        Return ini
    End Function

    ''' <summary>
    ''' Saves the ini instance to the given location
    ''' </summary>
    ''' <param name="filepath">The filepath to save the ini file to</param>
    ''' <param name="encoding">The encoding to use - defaults to UTF8</param>
    Public Async Function Save(filepath As String, Optional encoding As Encoding = Nothing) As Task
        If encoding Is Nothing Then
            encoding = Encoding.UTF8
        End If
        Dim iniString = ToIniString()
        Await IO.File.WriteAllTextAsync(filepath, iniString, encoding)
    End Function

    ''' <summary>
    ''' Creates a string representing the ini file
    ''' </summary>
    Public Function ToIniString() As String
        Dim sectionStrings = New List(Of String)
        For Each section In Sections
            sectionStrings.Add(section.ToIniString())
        Next
        Return String.Join(Environment.NewLine, sectionStrings)
    End Function

    Public Overrides Function ToString() As String
        Return $"Ini Sections: {Sections.Count}, Entries: {Sections.SelectMany(Function(x) x.Entries).Count()}"
    End Function

End Class

Wert aus Ini-Datei lesen

Die Funktion ReadValuevereinfacht den Zugriff auf die „GetPrivateProfileString“ API-Funktion.

Du musst dich nun nicht länger um den Buffer, oder den StringBuilder kümmern.

ReadValue ist bewusst als Shared gekennzeichnet, damit keine Instanz der Ini-Klasse von Nöten ist.

Dim value = Ini.ReadValue("Sektion1", "Schlüssel", "Standardwert", ".\config.ini")

Wert in Ini-Datei schreiben

Die Funktion WriteValuevereinfacht uns den Zugriff auf die dahinter befindliche „WritePrivateProfileStringAPI-Funktion nur mäßig, aber um einheitlich zu bleiben, lassen wir Sie drin.

So schreiben wir mit der neuen Klasse einen Wert in unsere Ini-Datei:

Dim wroteSuccessfully = Ini.WriteValue("Sektion1", "Schlüssel", "Wert", ".\config.ini")

Vollständiger Code 1 – Wrapper Methoden

Anbei ist der vollständige Code mit kleinen Wrappern um die nativen API-Funktionen herum.

Diesen kannst Du einfach in eine separate Datei innerhalb deines Projekts anlegen.

Benenne die Datei z. B. als „Ini.vbund verwende je nach Bedarf & Stil ggf. noch einen sauberen Namespace wie „Utils“.

Imports System.Runtime.InteropServices
Imports System.Text

Public Class Ini

    Public Const INI_WRITE_ERROR = 0
    Public Const INI_WRITE_SUCCESS = 1

    Public Shared Function ReadValue(section As String, key As String, [default] As String, file As String) As String
        Dim bufferLength = 1024
        Dim stringBuilder = New StringBuilder(bufferLength)
        Dim length = GetPrivateProfileString(section, key, [default], stringBuilder, bufferLength, file)
        Dim bufferedValue = stringBuilder.ToString()
        Dim value = bufferedValue.Substring(0, length)
        Return value
    End Function

    Public Shared Function WriteValue(section As String, key As String, value As String, file As String) As Boolean
        Dim resultCode = WritePrivateProfileString(section, key, value, file)
        Dim successfulWrite = resultCode = INI_WRITE_SUCCESS
        Return successfulWrite
    End Function

    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function GetPrivateProfileString(lpAppName As String,
                        lpKeyName As String,
                        lpDefault As String,
                        lpReturnedString As StringBuilder,
                        nSize As Integer,
                        lpFileName As String) As Integer
    End Function

    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function WritePrivateProfileString(lpAppName As String,
                        lpKeyName As String,
                        lpString As String,
                        lpFileName As String) As Boolean
    End Function

End Class

Vollständiger Code 2 – Starke Objektorientierung

Ini Datei lesen und schreiben mit Objektorientierung
Ini Datei lesen und schreiben mit Objektorientierung

Im zweiten Beispiel des vollständigen Codes habe ich versucht, die Ini-Datei so objektorientiert wie möglich darzustellen.

Sicherlich könnte man hier und da ggf. noch etwas vereinfachen, oder optimieren, ich denke allerdings, dass das ein guter Anfang ist.

Hierfür habe ich mehrere Dateien angelegt, die ja jeder so namespacen etc. kann, wie er es für richtig hält.

Ebenso habe ich alle Funktionen beschrieben, was die Benutzung dementsprechend einfach gestalten sollte.

Die Entry-Klasse

Jede Instanz der Entry-Klasse stellt praktisch eine Zeile aus einer Ini-Datei, bzw. genauer genommen einer Sektion dar.

Eigenschaften / Properties

Key

Repräsentiert den Schlüssel des Eintrags

Value

Steht für den Wert des Eintrags

Konstruktor

New(key)

Um einen Eintrag nur mit einem Schlüssel und leerem String als Wert zu erstellen.

New(key, value)

Erstellt einen Eintrag mit Schlüssel und zugehörigem Wert.

Methoden

FromLine

Die „SharedFromLine-Funktion soll es uns vereinfachen einen Eintrag aus einer Ini-Datei-Zeile zu verarbeiten. Dabei sucht die Funktion das erste Vorkommen eines Gleichzeichens und verarbeitet den Rest als Wert.

ToIniString

Eine „ToIniString„-Funktion soll es vereinfachen, den Eintrag wieder in eine Ini-Zeile umzuwandeln. Achtung: Ohne Zeilenumbruch!

ToString

Letztlich soll die „ToString()„-Funktion für ein anschaulicheres Ergebnis durch standardmäßiges Umwandeln in einen String sorgen.

Public Class Entry

    Public Property Key As String

    Public Property Value As String

    Sub New(key As String)
        Me.Key = key
        Me.Value = ""
    End Sub

    Sub New(key As String, value As String)
        Me.Key = key
        Me.Value = value
    End Sub

    ''' <summary>
    ''' Parses an <see cref="Entry"/> from the given ini file line
    ''' </summary>
    ''' <param name="line"></param>
    ''' <returns>The parsed Entry</returns>
    ''' <exception cref="ArgumentException">If no key value separator could be found</exception>
    Public Shared Function FromLine(line As String) As Entry
        Dim firstIndexOfEqualSign = line.IndexOf("=")
        If firstIndexOfEqualSign = -1 Then
            Throw New ArgumentException("The equal separation sign could not be found", NameOf(line))
        End If
        Dim parts = line.Split("=")
        Dim key = parts(0)
        Dim value = parts(1)
        Dim entry = New Entry(key, value)
        Return entry
    End Function

    ''' <summary>
    ''' Returns the line like it would be represented in an ini file
    ''' No Appended newline!
    ''' </summary>
    Public Function ToIniString() As String
        Return $"{Key}={Value}"
    End Function

    Public Overrides Function ToString() As String
        Return $"Ini-Entry Key: {Key}, Value: {Value}"
    End Function

End Class

Die Section-Klasse

Wie der Name schon verrät, spiegelt diese Klasse eine jeweilige Sektion aus der Ini-Datei wieder.

Eigenschaften / Properties

Name

Jede Sektion muss einen Namen besitzen, weil die Regeln für Ini-Dateien das so vorschreiben.

Entries

Jede Sektion kann eine beliebige Anzahl an Einträgen haben.

Indexer

Vor dem Konstruktor der Klasse habe ich zwei Indexer für die Sektionen geschrieben, da es so auch aus .NET Standardklassen kennt – und das mag ich!

Mit dem einen können wir über den tatsächlichen Index und mit dem anderen über den Schlüssel an den jeweiligen Eintrag kommen.

Konstruktor

Der Konstruktor braucht zumindest einen Namen für die jeweilige Sektion und die Liste der Einträge wird automatisch instanziiert.

Methoden

GetNameFromLine

Diese Funktion soll den Namen einer Sektion aus einer Ini-Zeile extrahieren können. Da Diese ja mit „[Sektionsname]“ angegeben werden, entfernt die Funktion für uns die Eckigen Klammern und eventuelle Leerzeichen.

GetIniNameLine

Macht praktisch genau das Gegenteil der vorherigen Funktion: Sie wandelt den Namen wieder in einen für eine Ini brauchbaren Namen a la „[Sektionsname]“ um.

AddEntry(key, value)

Fügt der Sektion einen Eintrag anhand des übergebenen Schlüssels und Wertes hinzu. Falls der Schlüssel bereits existiert, wird der Wert darin überschrieben.

AddEntry(entry)

Wie die Methode oben drüber, nur mit einem übergebenen Objekt sozusagen als „Container“.

ToIniString

Gibt die Sektion als String wieder, so wie Sie in der Ini-Datei aussehen würde. Achtung! Kein hinten anstehender Zeilenumbruch!

ToString

Stellt die Informationen über die Sektion als lesbaren String dar, also wie im .NET Framework üblich.

Public Class Section

    Public Property Name As String

    Public Property Entries As List(Of Entry)

    ''' <summary>
    ''' Helper to retrieve the entry of the given index
    ''' </summary>
    ''' <exception cref="IndexOutOfRangeException"></exception>
    Default Public ReadOnly Property GetEntry(index As Integer) As Entry
        Get
            Dim foundEntry As Entry = Entries.ElementAtOrDefault(index)
            If foundEntry Is Nothing Then
                Throw New IndexOutOfRangeException($"No entry at the given index of {index} found")
            End If
            Return foundEntry
        End Get
    End Property

    ''' <summary>
    ''' Helper to retrieve the entry of the given key
    ''' </summary>
    ''' <exception cref="IndexOutOfRangeException"></exception>
    Default Public ReadOnly Property GetEntry(key As String) As Entry
        Get
            Dim foundEntry As Entry = Entries.SingleOrDefault(Function(x) x.Key.ToLower() = key.ToLower())
            If foundEntry Is Nothing Then
                Throw New IndexOutOfRangeException($"No entry for the key {key} found")
            End If
            Return foundEntry
        End Get
    End Property

    Sub New(name As String)
        Entries = New List(Of Entry)
        Me.Name = name
    End Sub

    ''' <summary>
    ''' Retrieves the section name from a given ini file line
    ''' </summary>
    ''' <param name="line">The ini line to retrieve from</param>
    Public Shared Function GetNameFromLine(line As String) As String
        Return line.Replace("[", "").Replace("]", "").Replace(" ", "")
    End Function

    ''' <summary>
    ''' Retrieves the section name like it would be represented in an ini file
    ''' No appended newline!
    ''' </summary>
    Public Function GetIniNameLine() As String
        Return $"[{Name}]"
    End Function

    ' TODO - Maybe validate given key and value..
    ''' <summary>
    ''' Adds an <see cref="Entry"/> by key and value
    ''' If the key already exists, the value will be overriden
    ''' </summary>
    ''' <param name="entry">The key of the entry</param>
    ''' <param name="value">The value of the entry</param>
    ''' <returns>The <see cref="Section"/> to chain multiple calls</returns>
    Public Function AddEntry(key As String, value As String) As Section
        Dim existentEntry = Entries.SingleOrDefault(Function(x) x.Key.ToLower() = key.ToLower())
        If existentEntry IsNot Nothing Then
            existentEntry.Value = value
            Return Me
        End If
        Dim entry = New Entry(key, value)
        Entries.Add(entry)
        Return Me
    End Function

    ' TODO - Maybe validate given entries key and value..
    ''' <summary>
    ''' Adds a given <see cref="Entry"/>
    ''' If the key already exists, the value will be overriden
    ''' </summary>
    ''' <param name="entry">The Entry to add</param>
    ''' <returns>The <see cref="Section"/> to chain multiple calls</returns>
    Public Function AddEntry(entry As Entry) As Section
        Dim existentEntry = Entries.SingleOrDefault(Function(x) x.Key.ToLower() = entry.Key.ToLower())
        If existentEntry IsNot Nothing Then
            existentEntry.Value = entry.Value
            Return Me
        End If
        Entries.Add(entry)
        Return Me
    End Function

    ''' <summary>
    ''' Returns the string representing the section inside an ini file
    ''' Without appended newline!
    ''' </summary>
    Public Function ToIniString() As String
        Dim lines = New List(Of String)
        lines.Add(GetIniNameLine())
        For Each entry In Entries
            lines.Add(entry.ToIniString())
        Next
        Dim iniString = String.Join(Environment.NewLine, lines)
        Return iniString
    End Function

    Public Overrides Function ToString() As String
        Return $"Section {Name}, Entries: {Entries.Count}"
    End Function

End Class

Die Ini-Klasse

Repräsentiert die Initialisierungsdatei mit all Ihren Eigenschaften und Methoden selbst, daher hilft sie noch einmal durch starke Objektorientierung, eine Ini Datei lesen und schreiben zu können.

Eigenschaften / Properties

Sections

Repräsentiert die einzelnen Sektionen der Ini-Datei.

Indexer

Vor dem Konstruktor der Klasse habe ich zwei Indexer für die Ini geschrieben, so kennt man es auch aus .NET Standardklassen – und das mag ich!

Mit dem einen können wir über den tatsächlichen Index und mit dem anderen über den Namen an die jeweilige Sektion kommen.

Konstruktor

Der Konstruktor braucht bisher keinen Parameter und die Liste der Sektionen wird automatisch instanziiert.

Methoden

ReadValue

Die Funktion haben wir weiter oben schon besprochen, daher entfällt die Erklärung hier.

WriteValue

Haben wir ebenfalls schon weiter oben besprochen, insofern entfällt die Erklärung hier.

Load(contents)

Lädt eine Ini-Instanz anhand ihres String-Inhalts, indem es die Teile mit Hilfsfunktionen verarbeitet.

Load(filepath, encoding)

Lädt die Inhalte einer Textdatei mit dem angegebenen Encoding (sonst UTF8) und wandelt Sie mit der anderen Überladung in eine Ini-Datei um.

AddSection(name)

Fügt eine Sektion anhand eines Namens hinzu. Wirft eine SectionAlreadyExistsException, falls die übergebene Sektion bereits existiert.

AddSection(section)

Macht das Gleiche wie die Funktion hier drüber, jedoch mit einer Instanz.

ThrowOnDuplicateSectionName

Hilfs-Methode um „Duplicate-Content“ zu vermeiden. Checkt ob eine Sektion mit dem gegebenen Namen schon existiert und schmeißt eine SectionAlreadyExistsException wenn ja.

LoadIniLinesIgnoringEmpty

Lädt die Zeilen einer Ini-Datei in eine String-Liste und überspringt leere Zeilen.

Save

Speichert die Ini als Datei unter dem angegebenen Dateinamen und mit dem Encoding (sonst UTF8) im System ab.

ToIniString

Wandelt die Ini-Instanz in einen speicherbaren String um.

ToString

Zeigt Informationen über die Ini in lesbarer Form an.

Imports System.Runtime.InteropServices
Imports System.Text

Public Class Ini

    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function GetPrivateProfileString(lpAppName As String,
                        lpKeyName As String,
                        lpDefault As String,
                        lpReturnedString As StringBuilder,
                        nSize As Integer,
                        lpFileName As String) As Integer
    End Function

    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function WritePrivateProfileString(lpAppName As String,
                        lpKeyName As String,
                        lpString As String,
                        lpFileName As String) As Boolean
    End Function


    Public Property Sections As List(Of Section)

    ''' <summary>
    ''' Helper to retrieve the section of the given index
    ''' </summary>
    ''' <exception cref="IndexOutOfRangeException"></exception>
    Default Public ReadOnly Property GetSection(index As Integer) As Section
        Get
            Dim foundSection As Section = Sections.ElementAtOrDefault(index)
            If foundSection Is Nothing Then
                Throw New IndexOutOfRangeException($"No section at the given index of {index} found")
            End If
            Return foundSection
        End Get
    End Property

    ''' <summary>
    ''' Helper to retrieve the entry of the given name
    ''' </summary>
    ''' <exception cref="IndexOutOfRangeException"></exception>
    Default Public ReadOnly Property GetSection(name As String) As Section
        Get
            Dim foundSection As Section = Sections.SingleOrDefault(Function(x) x.Name.ToLower() = name.ToLower())
            If foundSection Is Nothing Then
                Throw New IndexOutOfRangeException($"No section for the name {name} found")
            End If
            Return foundSection
        End Get
    End Property

    Sub New()
        Sections = New List(Of Section)
    End Sub

    ''' <summary>
    ''' Helper function to read single values if needed
    ''' </summary>
    Public Shared Function ReadValue(section As String, key As String, [default] As String, file As String) As String
        Dim bufferLength = 1024
        Dim stringBuilder = New StringBuilder(bufferLength)
        Dim length = GetPrivateProfileString(section, key, [default], stringBuilder, bufferLength, file)
        Dim bufferedValue = stringBuilder.ToString()
        Dim value = bufferedValue.Substring(0, length)
        Return value
    End Function

    ''' <summary>
    ''' Helper function to write single values if needed
    ''' </summary>
    Public Shared Function WriteValue(section As String, key As String, value As String, file As String) As Boolean
        Dim wroteSuccessfully = WritePrivateProfileString(section, key, value, file)
        Return wroteSuccessfully
    End Function

    ''' <summary>
    ''' Adds a section by name
    ''' </summary>
    ''' <param name="name">The name for the section to add</param>
    ''' <exception cref="SectionAlreadyExistsException"></exception>
    Public Sub AddSection(name As String)
        ThrowOnDuplicateSectionName(name)
        Dim section = New Section(name)
        Sections.Add(section)
    End Sub

    ''' <summary>
    ''' Adds a section by instance
    ''' </summary>
    ''' <param name="section">The section to add</param>
    ''' <exception cref="SectionAlreadyExistsException"></exception>
    Public Sub AddSection(section As Section)
        ThrowOnDuplicateSectionName(section.Name)
        Sections.Add(section)
    End Sub

    ''' <summary>
    ''' Helper function to avoid duplicated code
    ''' </summary>
    ''' <param name="name">The <see cref="Section"/>-Name to check for</param>
    ''' <exception cref="SectionAlreadyExistsException"></exception>
    Private Sub ThrowOnDuplicateSectionName(name As String)
        Dim existentSection = Sections.SingleOrDefault(Function(x) x.Name.ToLower() = name.ToLower())
        If existentSection IsNot Nothing Then
            Throw New SectionAlreadyExistsException("A section with the name " & name & " already exists")
        End If
    End Sub

    Private Shared Function LoadIniLinesIgnoringEmpty(contents As String) As List(Of String)
        Dim lines = contents.Split(New String() {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
        Return lines.ToList()
    End Function

    Public Shared Function Load(contents As String) As Ini
        Dim lines = LoadIniLinesIgnoringEmpty(contents)
        Dim ini = New Ini()
        Dim lastSection As Section = Nothing
        For Each line In lines
            Dim isSection = line.StartsWith("[")
            If isSection Then
                Dim sectionName = Section.GetNameFromLine(line)
                ini.AddSection(sectionName)
                lastSection = ini.Sections.Last()
            Else
                Dim theEntry= IniFileExample.Entry.FromLine(line)
                lastSection.AddEntry(theEntry)
            End If
        Next
        Return ini
    End Function

    ''' <summary>
    ''' Loads the ini instance from the given filepath
    ''' </summary>
    ''' <param name="filepath">The filepath to the ini file</param>
    ''' <param name="encoding">The encoding to use - defaults to UTF8</param>
    ''' <returns>The parsed ini file instance</returns>
    Public Shared Async Function Load(filepath As String, Optional encoding As Encoding = Nothing) As Task(Of Ini)
        If encoding Is Nothing Then
            encoding = Encoding.UTF8
        End If
        Dim contents = Await IO.File.ReadAllTextAsync(filepath, encoding)
        Dim ini = Load(contents)
        Return ini
    End Function

    ''' <summary>
    ''' Saves the ini instance to the given location
    ''' </summary>
    ''' <param name="filepath">The filepath to save the ini file to</param>
    ''' <param name="encoding">The encoding to use - defaults to UTF8</param>
    Public Async Function Save(filepath As String, Optional encoding As Encoding = Nothing) As Task
        If encoding Is Nothing Then
            encoding = Encoding.UTF8
        End If
        Dim iniString = ToIniString()
        Await IO.File.WriteAllTextAsync(filepath, iniString, encoding)
    End Function

    ''' <summary>
    ''' Creates a string representing the ini file
    ''' </summary>
    Public Function ToIniString() As String
        Dim sectionStrings = New List(Of String)
        For Each section In Sections
            sectionStrings.Add(section.ToIniString())
        Next
        Return String.Join(Environment.NewLine, sectionStrings)
    End Function

    Public Overrides Function ToString() As String
        Return $"Ini Sections: {Sections.Count}, Entries: {Sections.SelectMany(Function(x) x.Entries).Count()}"
    End Function

End Class

Eine vorhandene Ini-Datei lesen

Hier ein kleines Beispiel, um mit unserem vollständigen Satz an Klassen ob eine vorhandene Ini-Datei zu lesen.

Beachte, dass natürlich eine Datei namens „config.ini“ tatsächlich in deinem Anwendungs-Pfad existieren muss.

Ebenfalls muss die Datei die Sektion „Sektion1“ und einen passenden Schlüssel „Key“ beinhalten.

Falls Du mein Beispielprojekt heruntergeladen hast, befindet diese Datei sich dort bereits!

Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Dim config = Await Ini.Load(".\config.ini", Encoding.UTF8)
    Dim section = config.GetSection("Sektion1")
    Dim entry = section("Key")
    Dim value = entry.Value
    MessageBox.Show(value)
End Sub

Eine Ini-Datei erstellen

Hier kommt ein schnelles Beispiel, um eine neue Ini-Datei zu erstellen.

(Achtung, überschreibt die vorhandene Ini-Datei durch die „Save“-Methode!).

Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Dim config = New Ini()
    config.AddSection("Sektion2")
    Dim section = config("Sektion2")
    section.AddEntry("Key1", "Value1")
    section.AddEntry("Key2", "Value2")
    Await config.Save(".\config.ini", Encoding.UTF8)
End Sub

Die vorhandene Ini-Datei „bearbeiten“

Nun der letzte Schritt, wo wir eine vorhandene Datei bearbeiten, bzw. verändern.

Achte hierbei je nach Vorgehensweise darauf, dass eine Sektion noch nicht existiert.

Private Async Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Dim config = Await Ini.Load(".\config.ini", Encoding.UTF8)
    Dim section = config.GetSection("Sektion2")
    Dim entry = section("Key2")
    entry.Value = "NewValue!"
    Await config.Save(".\config.ini", Encoding.UTF8)
End Sub

Fazit

Ini Datei lesen und schreiben – Das Fazit
Ini Datei lesen und schreiben – Das Fazit

Am Ende des Beitrag angekommen, wiederholen wir nochmal kurz die wichtigsten Dinge.

Ini-Dateien sind wohl der gute alte Weg schlechthin, um Einstellungen zu speichern.

Trotz ihres langen Bestehens sind Sie auch heute noch ein gerne genutztes Werkzeug.

Durch den einfachen Aufbau und den Verzicht auf kryptische Zeichen, sind Sie von Menschen einfach zu lesen.

Das hat viele Nutzer der Ini-Dateien in Vergangenheit das ein oder andere Mal gerettet, wenn es hieß: „Kannst Du mal eben die Einstellung anpassen?“.

In diesem Beitrag habe ich Dir gezeigt, wie Du mit Hilfe von Visual Basic NET eigene Ini-Dateien verwalten kannst.

Das bezog sich einerseits auf das typische Lesen, aber natürlich auch auf das Setzen von Einstellungen.

Dabei gab es von der rohen „API-Variante„, bis zu Hilfs-Methoden und der vollständigen Objektorientierung alles.

Ich würde Dir natürlich die vollständige Objektorientierung ans Herz legen.

Einfache Bedienbarkeit, saubere Zugriffe, Erweiterbarkeit und mehr liegen dann ganz weit vorn.

Downloads

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert