Как стать автором
Обновить

Как хранить Config в Excel

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров5.3K
Изображение отсюда: https://www.freepik.com/free-vector/settings-concept-illustration_9793179.htm#query=settings&position=2&from_view=search
Изображение отсюда: https://www.freepik.com/free-vector/settings-concept-illustration_9793179.htm#query=settings&position=2&from_view=search

Я часто пользуюсь конфигурацией при написании VBA макросов в Excel. Иногда она нужна в формах для сохранения настроек, иногда для сохранения каких-то получаемых в процессе выполнения макроса данных.

Долгое время я искал информацию о том как работать с конфигом в VBA.
Есть множество различных вариантов, от хранения конфигурации в коде, до выноса ее в отдельный файл.

Сегодня я хотел бы пошагово рассказать о том как я храню и обрабатываю конфигурацию макроса в книге Excel.

Создаем лист ConfigSheet

У меня под рукой был Excel 2010, но в данном случае версия не имеет значения.

Для начала создаем отдельный лист. Я назвал его config, но это не принципиально. Что же действительно принципиально, так это CodeName листа:

CodeName листа
CodeName листа

Если вы вдруг не знали, листы документа Excel в VBA – это, ни что иное, как объект класса Worksheet. Обращаемся к справке и видим у объекта Worksheet необходимое свойство (перезаписать его программно, несмотря на Read-only, можно, но об этом в другой раз):

Чтобы было проще обращаться к нашему Config листу, меняем ему значение поля (Name) в свойствах (если у вас их нет, нажмите F4 или View -> Properties Window, а если у вас нет структуры с проектом, нажмите Ctrl+R или View -> Project Explorer).

Таким образом, мы дополнительно защищаемся от ошибки в случае изменения обычного имени листа, а так же получаем автокомплит.

Вызываем автокомплит с помощью Ctrl+Space
Вызываем автокомплит с помощью Ctrl+Space

Кстати, так как лист – это объект, мы можем обращаться так же к его методам, полям и свойствам через точку, как обычно:

Вызываем методы и свойства
Вызываем методы и свойства

Этим мы и воспользуемся, но об этом чуть позже.

Создаем 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

Ну а дальше этот лист можно спрятать от глаз пользователей, защитить книгу и структуру, и спокойно работать с конфигом.

Уверен, что можно изменить подход и написать код иначе. Возможно у вас есть предложения или дополнения по статье. Буду рад любому отзыву. 🙂

Теги:
Хабы:
Всего голосов 20: ↑19 и ↓1+18
Комментарии30

Публикации

Истории

Ближайшие события