Я часто пользуюсь конфигурацией при написании VBA макросов в Excel. Иногда она нужна в формах для сохранения настроек, иногда для сохранения каких-то получаемых в процессе выполнения макроса данных.
Долгое время я искал информацию о том как работать с конфигом в VBA.
Есть множество различных вариантов, от хранения конфигурации в коде, до выноса ее в отдельный файл.
Сегодня я хотел бы пошагово рассказать о том как я храню и обрабатываю конфигурацию макроса в книге Excel.
Создаем лист ConfigSheet
У меня под рукой был Excel 2010, но в данном случае версия не имеет значения.
Для начала создаем отдельный лист. Я назвал его config, но это не принципиально. Что же действительно принципиально, так это CodeName
листа:
Если вы вдруг не знали, листы документа Excel в VBA – это, ни что иное, как объект класса Worksheet
. Обращаемся к справке и видим у объекта Worksheet
необходимое свойство (перезаписать его программно, несмотря на Read-only, можно, но об этом в другой раз):
Чтобы было проще обращаться к нашему Config листу, меняем ему значение поля (Name) в свойствах (если у вас их нет, нажмите F4 или View -> Properties Window, а если у вас нет структуры с проектом, нажмите Ctrl+R или View -> Project Explorer).
Таким образом, мы дополнительно защищаемся от ошибки в случае изменения обычного имени листа, а так же получаем автокомплит.
Кстати, так как лист – это объект, мы можем обращаться так же к его методам, полям и свойствам через точку, как обычно:
Этим мы и воспользуемся, но об этом чуть позже.
Создаем ListObject
Итак, как вы уже догадались, всю информацию мы будем сохранять в таблицу, а именно в объект ListObject
.
Для этого на нашем листе создаем пустую таблицу с двумя столбцами Key
и Value
:
Теперь осталось в Конструкторе задать нашей таблице имя, и основа для хранения готова:
Получаем объект таблицы
Переходим к самому интересному. Писать код будем в модуле листа ConfigSheet
.
Для начала создадим необходимые гетеры:
Public Property Get Table() As ListObject
' Свойство Read-Only для объекта таблицы.
Set Table = Me.ListObjects("configTable")
End Property
Public Property Get Keys() As Range
' Свойство Read-Only для столбца ключей.
Set Keys = Me.Table.ListColumns(KeyColumn).DataBodyRange
End Property
Public Property Get Values() As Range
' Свойство Read-Only для столбца значений.
Set Values = Me.Table.ListColumns(ValueColumn).DataBodyRange
End Property
В свойство Table
помещаем нашу таблицу, в Keys
– столбец ключей, в Values
– столбец значений.
Для удобства обращения к столбцам (и чтобы не хардкодить), прописываем Enum
на уровне модуля:
Private Enum EConfigColumns
KeyColumn = 1
ValueColumn
End Enum
Сказал "не хардкодить" и захардкодил название таблицы ?. Исправляюсь:
Option Explicit
Private Enum EConfigColumns
KeyColumn = 1
ValueColumn
End Enum
Private Const ConfigTable As String = "configTable"
Прописываем свойство Get Config
Осталось создать свойство Config
, через которое будем записывать, получать и обновлять значения в таблице.
Для начала прописываем получение значения по ключу:
Public Property Get Config(ByVal Key As Variant) As Variant
Dim i As Long
For i = 1 To Me.Keys.Rows.Count
If Key <> Me.Keys(i).Value Then GoTo NextKey
Config = Me.Values(i).Value: Exit Property
NextKey:
Next
End Property
Здесь все довольно просто – пробегаем циклом по ключам и сравниваем их с параметром Key
, передаваемым пользователем. Как только находим нужный ключ, возвращаем соответствующее значение.
Так как мы работаем напрямую с объектом Range
, это уже несколько замедляет работу макроса. Плюс ко всему, во время цикла макрос будет постоянно вызывать свойства Keys
и Values
чтобы получить их значения, что еще сильнее замедлит скорость выполнения.
Чтобы немного ускорить процесс и избежать постоянное обращение, можно создать переменные и передавать в них свойства:
Public Property Get Config(ByVal Key As Variant) As Variant
' Переменные, для хранения свойств.
Dim Keys As Range: Set Keys = Me.Keys
Dim Values As Range: Set Values = Me.Values
Dim i As Long
For i = 1 To Me.Keys.Rows.Count
If Key <> Me.Keys(i).Value Then GoTo NextKey
Config = Me.Values(i).Value: Exit Property
NextKey:
Next
End Property
Но это несколько загромождает код (а ведь у нас еще будет свойство Let
), поэтому воспользуемся UDT
(user defined type) и процедурой, которая будет его инициализировать.
Создаем тип TConfig
в который помещаем все наши ранее созданные свойства (кроме, собственно, Config
), а так же создаем приватную переменную This
на уровне модуля:
Option Explicit
Private Enum EConfigColumns
KeyColumn = 1
ValueColumn
End Enum
Private Const ConfigTable As String = "configTable"
Private Type TConfig
Table As ListObject
Keys As Range
Values As Range
End Type
Private This As TConfig
Очень важно чтобы и Type TConfig
и переменная This
были приватными, иначе на этапе компиляции возникнет ошибка.
Далее, прописываем небольшую процедуру InitThis
, для присваивания значений нашему типу:
Public Sub InitThis()
Set This.Table = Me.Table
Set This.Keys = Me.Keys
Set This.Values = Me.Values
End Sub
Теперь поправим свойство Config
:
Public Property Get Config(ByVal Key As Variant) As Variant
Me.InitThis
Dim i As Long
For i = 1 To This.Keys.Rows.Count
If Key <> This.Keys(i).Value Then GoTo NextKey
Config = This.Values(i).Value: Exit Property
NextKey:
Next
End Property
Лаконично, не так ли?
Прописываем свойство Let Config
С установлением значений чуть иначе:
Public Property Let Config(ByVal Key As Variant, ByVal RHS As Variant)
Me.InitThis
If This.Keys Is Nothing Then This.Table.ListRows.Add: Me.InitThis
Dim i As Long
Do Until Key = This.Keys(i).Value
i = i + 1
If i > This.Keys.Rows.Count Then This.Table.ListRows.Add: Exit Do
Loop
This.Keys(i).Value = Key
This.Values(i).Value = RHS
End Property
В параметры принимаем Key
и RHS
(Right Hand Side – по правую руку), для того чтобы можно было прописывать такую конструкцию:
ConfigSheet.Config("Key") = "Value"
В самом начале проверяем This.Keys
на Nothing
, т.к. если в таблице еще совсем нет значений, при попытке пробежаться циклом по столбцам выскочит ошибка.
Чтобы этого избежать, после проверки добавляем в таблицу пустую строку и заново инициализировать This
. Только после этого можно будет свободно проходить по столбцам циклом.
Подобную проверку добавляем и в Get
, но вместо добавления строки просто возвращаем сообщение "Нет данных в таблице конфигурации":
Public Property Get Config(ByVal Key As Variant) As Variant
Me.InitThis
If This.Keys Is Nothing Then Config = "Нет данных в таблице конфигурации": Exit Property
Dim i As Long
For i = 1 To This.Keys.Rows.Count
If Key <> This.Keys(i).Value Then GoTo NextKey
Config = This.Values(i).Value: Exit Property
NextKey:
Next
End Property
Далее, так же как и в Get
части, циклом, только теперь Do Until
, пробегаем по ключам конфига. При достижении максимального индекса – добавляем в конце новую строку и выходим из цикла. В конце присваиваем ключ и значение в соответствующие ячейки.
Удаляем пустые строки
При первом обращении к таблице у нас появится пустая строка. Чтобы ее убрать (и вообще, на всякий случай, каждый раз проверять, нет ли пустых строк в таблице), пропишем небольшую процедуру:
Public Sub DeleteEmptyRows()
Me.InitThis
Dim i As Long
For i = This.Keys.Count To 1 Step -1
If (IsEmpty(This.Keys(i).Value) And IsEmpty(This.Values(i).Value)) _
Or (This.Keys(i).Value = vbNullString And This.Values(i).Value = vbNullString) Then This.Table.ListRows(i).Delete
Next
End Sub
и добавим ее в уже написанную Let
часть:
Public Property Let Config(ByVal Key As Variant, ByVal RHS As Variant)
Me.InitThis
If This.Keys Is Nothing Then This.Table.ListRows.Add: Me.InitThis
Dim i As Long
Do Until Key = This.Keys(i).Value
i = i + 1
If i > This.Keys.Rows.Count Then This.Table.ListRows.Add: Exit Do
Loop
This.Keys(i).Value = Key
This.Values(i).Value = RHS
Me.DeleteEmptyRows ' Проверяем на пустые строки.
End Property
Итоговый код
Исправил запись в Property Get Config
, спасибо за наводку @qyix7z.
Option Explicit
Private Enum EConfigColumns
KeyColumn = 1
ValueColumn
End Enum
Private Const ConfigTable As String = "configTable"
Private Type TConfig
Table As ListObject
Keys As Range
Values As Range
End Type
Private This As TConfig
Public Sub InitThis()
Set This.Table = Me.Table
Set This.Keys = Me.Keys
Set This.Values = Me.Values
End Sub
Public Property Get Table() As ListObject
' Свойство Read-Only для объекта таблицы.
Set Table = Me.ListObjects(ConfigTable)
End Property
Public Property Get Keys() As Range
' Свойство Read-Only для столбца ключей.
Set Keys = Me.Table.ListColumns(KeyColumn).DataBodyRange
End Property
Public Property Get Values() As Range
' Свойство Read-Only для столбца значений.
Set Values = Me.Table.ListColumns(ValueColumn).DataBodyRange
End Property
Public Property Get Config(ByVal Key As Variant) As Variant
Me.InitThis
If This.Keys Is Nothing Then Config = "Нет данных в таблице конфигурации": Exit Property
Dim i As Long
For i = 1 To This.Keys.Rows.Count
If Key = This.Keys(i).Value Then Config = This.Values(i).Value: Exit Property
Next
End Property
Public Property Let Config(ByVal Key As Variant, ByVal RHS As Variant)
Me.InitThis
If This.Keys Is Nothing Then This.Table.ListRows.Add: Me.InitThis
Dim i As Long
Do Until Key = This.Keys(i).Value
i = i + 1
If i > This.Keys.Rows.Count Then This.Table.ListRows.Add: Exit Do
Loop
This.Keys(i).Value = Key
This.Values(i).Value = RHS
Me.DeleteEmptyRows ' Проверяем на пустые строки.
End Property
Public Sub DeleteEmptyRows()
Me.InitThis
Dim i As Long
For i = This.Keys.Count To 1 Step -1
If (IsEmpty(This.Keys(i).Value) And IsEmpty(This.Values(i).Value)) _
Or (This.Keys(i).Value = vbNullString And This.Values(i).Value = vbNullString) Then This.Table.ListRows(i).Delete
Next
End Sub
Проверяем результат
Ну и наконец проверяем получившийся результат.
Записываем значение в конфиг:
Sub Test()
' Значение "Дневник VBAшника" записано в таблицу с ключом "ChanelName"
ConfigSheet.Config("ChanelName") = "Дневник VBAшника"
End Sub
Считываем значение:
Sub Test()
' Распечатает: "Дневник VBAшника"
Debug.Print ConfigSheet.Config("ChanelName")
End Sub
Меняем и считываем еще раз:
Sub Test()
ConfigSheet.Config("ChanelName") = "https://t.me/VBAn_Diary"
' Распечатает: "https://t.me/VBAn_Diary"
Debug.Print ConfigSheet.Config("ChanelName")
End Sub
Ну а дальше этот лист можно спрятать от глаз пользователей, защитить книгу и структуру, и спокойно работать с конфигом.
Уверен, что можно изменить подход и написать код иначе. Возможно у вас есть предложения или дополнения по статье. Буду рад любому отзыву. ?