
INI-файлы в VB.NET: чтение и запись через GetPrivateProfileString
В .NET нет встроенного класса для INI-файлов, но Windows умеет работать с ними сама. Две API-функции и небольшая обёртка закрывают задачу настроек для WinForms-приложения.
Зачем 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 заданной ёмкости. Функция вернёт количество символов, реально записанных в буфер.
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, а не рядом с программой. Это одна из самых частых причин ситуации «настройки не сохраняются».
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Списки секций и ключей
Иногда нужно не прочитать конкретный ключ, а узнать, что вообще лежит в файле: например, показать в настройках список подключений, каждое из которых хранится в своей секции. Для этого в GetPrivateProfileString передают Nothing вместо имени секции или ключа.
В ответ функция кладёт в буфер имена, разделённые нулевыми символами, а в конце ставит двойной ноль. Остаётся разрезать строку по символу с кодом 0 и отбросить пустые элементы. Буфер стоит брать с запасом: если он мал, список молча обрежется.
' Добавьте в класс 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 только для обратной совместимости.
Как это делают сегодня
Вопросы и ответы
Почему значение не читается, хотя ключ в файле есть?
Чаще всего передан относительный путь, и Windows ищет файл в папке Windows. Второй вариант: ключ лежит в другой секции. Проверьте полный путь через Path.GetFullPath и имя секции.
Как удалить ключ или целую секцию?
Вызовите WritePrivateProfileString с Nothing вместо значения, чтобы удалить ключ. Если передать Nothing вместо имени ключа, будет удалена вся секция.
Работают ли эти функции в .NET на Linux?
Нет, это функции kernel32 и они есть только в Windows. Для кроссплатформенного кода используйте Microsoft.Extensions.Configuration.Ini или собственный разбор файла построчно.