INI-файлы в VB.NET: чтение и запись через GetPrivateProfileString

РазделVB.NET и WinFormsУровеньСредний Чтение8 минОпубликовано ТегиVB.NETINIWindows APIНастройки

В .NET нет встроенного класса для INI-файлов, но Windows умеет работать с ними сама. Две API-функции и небольшая обёртка закрывают задачу настроек для WinForms-приложения.

Главное. INI-файл из VB.NET читается и пишется двумя функциями kernel32 с DllImport и CharSet.Unicode. Обязательно передавайте полный путь, держите изменяемый файл в папке данных приложения, а не рядом с exe.

Зачем INI-файл, если есть app.config

Файл app.config и класс My.Settings хорошо подходят для настроек, которые разработчик задаёт заранее. Но в реальных проектах часто встречается другое: приложение переносят с VB6, где все параметры жили в INI, а рядом работают старые утилиты, читающие тот же файл. Переписывать формат ради красоты нет смысла, проще научить новый код читать и писать старый файл.

У INI есть и честные плюсы. Его правит администратор в Блокноте, его легко скопировать на другой компьютер, в нём видно секции и ключи без знания XML. Для небольшого набора параметров (путь к базе, имя сервера, последний открытый файл) это вполне рабочий выбор.

  • секция задаётся строкой в квадратных скобках, например [settings];
  • ключ и значение разделены знаком равенства, пробелы вокруг значения Windows обрезает;
  • строки, начинающиеся с точки с запятой, считаются комментариями.

Объявляем функции Windows API

Работу с INI в Windows выполняют функции GetPrivateProfileString и WritePrivateProfileString из kernel32.dll. В VB.NET их удобно объявить через атрибут DllImport с CharSet.Unicode: тогда вызываются W-версии функций, и кириллица в значениях не превращается в знаки вопроса.

Для чтения нужен изменяемый буфер. В VB6 для этого создавали строку из пробелов через Space, в .NET правильнее передать StringBuilder заданной ёмкости. Функция вернёт количество символов, реально записанных в буфер.

IniApi.vbVB.NET
Imports System.Runtime.InteropServices
Imports System.Text

Friend Module IniApi
    ' Чтение строки из INI; возвращает число символов в буфере
    <DllImport("kernel32.dll", CharSet:=CharSet.Unicode)>
    Friend Function GetPrivateProfileString(section As String, key As String,
        def As String, buffer As StringBuilder, size As Integer,
        fileName As String) As Integer
    End Function

    ' Запись строки; Nothing в value удаляет ключ, в key удаляет секцию
    <DllImport("kernel32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)>
    Friend Function WritePrivateProfileString(section As String, key As String,
        value As String, fileName As String) As Boolean
    End Function
End Module

Класс-обёртка для чтения и записи

Вызывать API напрямую из формы неудобно: каждый раз нужно помнить про буфер, полный путь и значение по умолчанию. Соберём небольшой класс IniFile, который получает путь в конструкторе и предлагает два понятных метода.

Путь обязательно должен быть полным. Если передать только имя файла, Windows будет искать его в системной папке Windows, а не рядом с программой. Это одна из самых частых причин ситуации «настройки не сохраняются».

IniFile.vbVB.NET
Imports System.IO
Imports System.Text

Public Class IniFile
    Private ReadOnly _path As String

    Public Sub New(path As String)
        _path = System.IO.Path.GetFullPath(path) ' только полный путь
    End Sub

    Public Function Read(key As String, Optional section As String = "settings",
                         Optional def As String = "") As String
        Dim sb As New StringBuilder(1024)
        IniApi.GetPrivateProfileString(section, key, def, sb, sb.Capacity, _path)
        Return sb.ToString()
    End Function

    Public Sub Write(key As String, value As String,
                     Optional section As String = "settings")
        If Not IniApi.WritePrivateProfileString(section, key, value, _path) Then
            Throw New IOException("Не удалось записать " & _path,
                New ComponentModel.Win32Exception())
        End If
    End Sub
End Class
Исходный вариант через On Error GoTo работает, но в VB.NET естественнее проверить результат функции и бросить исключение с кодом ошибки Windows. Так в журнале сразу видно, что именно пошло не так.

Списки секций и ключей

Иногда нужно не прочитать конкретный ключ, а узнать, что вообще лежит в файле: например, показать в настройках список подключений, каждое из которых хранится в своей секции. Для этого в GetPrivateProfileString передают Nothing вместо имени секции или ключа.

В ответ функция кладёт в буфер имена, разделённые нулевыми символами, а в конце ставит двойной ноль. Остаётся разрезать строку по символу с кодом 0 и отбросить пустые элементы. Буфер стоит брать с запасом: если он мал, список молча обрежется.

IniFile.Lists.vbVB.NET
' Добавьте в класс IniFile
Public Function GetSections() As String()
    Return ReadList(Nothing, Nothing)
End Function

Public Function GetKeys(section As String) As String()
    Return ReadList(section, Nothing)
End Function

Private Function ReadList(section As String, key As String) As String()
    Dim sb As New StringBuilder(32768)
    Dim n = IniApi.GetPrivateProfileString(section, key, "", sb, sb.Capacity, _path)
    ' имена разделены символом с кодом 0
    Return sb.ToString(0, n).Split(New Char() {ChrW(0)},
        StringSplitOptions.RemoveEmptyEntries)
End Function

Кодировка и права на запись

Windows определяет кодировку INI по самому файлу. Если файл в ANSI, W-функции пересчитают текст через текущую кодовую страницу, и кириллица сохранится только на системах с русской локалью. Если файл начинается с метки UTF-16 LE, Windows будет читать и писать его в Юникоде. Простой способ: при первом запуске создать пустой файл через File.WriteAllText с Encoding.Unicode, а дальше работать через API.

Вторая ловушка связана с правами. Начиная с Windows Vista обычный пользователь не может писать в Program Files. Старые программы, хранящие INI рядом с exe, получают виртуализацию: запись молча уходит в профиль пользователя, а другой пользователь видит старые значения. Храните изменяемый файл в папке данных приложения.

  • общие настройки для всех пользователей: Environment.SpecialFolder.CommonApplicationData;
  • личные настройки пользователя: Environment.SpecialFolder.ApplicationData;
  • рядом с exe оставляйте только файл, который программа читает и никогда не пишет.

Что учесть при сопровождении

API для INI считается устаревшим и оставлен ради совместимости, но работает во всех версиях Windows. Он кэширует файл и сам сериализует обращения из разных процессов, поэтому две программы, пишущие в один INI, не испортят его. А вот частая запись в цикле заметно медленнее, чем работа со словарём в памяти: если параметров десятки, прочитайте их при старте и запишите при выходе.

Значение ограничено размером буфера, а длинные строки и многострочный текст в INI хранить неудобно. Если настройки разрослись до вложенных структур и списков, это сигнал переходить на XML или JSON, оставив INI только для обратной совместимости.

Как это делают сегодня

В современных .NET-приложениях настройки читают через Microsoft.Extensions.Configuration: пакет Microsoft.Extensions.Configuration.Ini подключает INI как источник наравне с appsettings.json и переменными окружения, без P/Invoke и с привязкой к типизированным классам. Для записи INI без Windows API есть библиотеки вроде ini-parser, но чаще проще перевести изменяемые параметры в JSON и сохранять их через System.Text.Json.

Вопросы и ответы

Почему значение не читается, хотя ключ в файле есть?

Чаще всего передан относительный путь, и Windows ищет файл в папке Windows. Второй вариант: ключ лежит в другой секции. Проверьте полный путь через Path.GetFullPath и имя секции.

Как удалить ключ или целую секцию?

Вызовите WritePrivateProfileString с Nothing вместо значения, чтобы удалить ключ. Если передать Nothing вместо имени ключа, будет удалена вся секция.

Работают ли эти функции в .NET на Linux?

Нет, это функции kernel32 и они есть только в Windows. Для кроссплатформенного кода используйте Microsoft.Extensions.Configuration.Ini или собственный разбор файла построчно.

Читайте также