Страницы: 1
RSS
Изменение значений в именованных диапазонах, Это скорее интересная тема, чем вопрос.
 

Ночные приключения с макросами: или как простая задача вылилась в ночной марафон из VBA

Привет всем! Поделюсь историей, как я, в который раз, провёл ночь, сражаясь с Excel. Обычная задача раздвинуть диапазоны в файле. Вроде ничего особенного, не правда ли? Зашёл в диспетчер имен, поправил пару диапазонов — и готово. Ха! Ага, сейчас...

Оказалось, что в файле именованных диапазонов столько, что вручную это править — можно успеть выучить китайский язык, а потом всё равно не закончить. Ну ладно, думаю, не беда, надо просто быстренько макрос накатать. Простой, как дверь, макрос, который возьмёт и поменяет диапазоны с одного столбца на другой. Пять минут делов. Кто бы сомневался, да?

Код
Sub UpdateNamedRanges()
    Dim nName As Name
    Dim oldAddress As String
    Dim newAddress As String
    
    For Each nName In ThisWorkbook.Names
        oldAddress = nName.RefersTo
        
        If InStr(1, oldAddress, "$EB$", vbTextCompare) > 0 Then
            newAddress = Replace(oldAddress, "$EB$", "$EC$")
            
            nName.RefersTo = newAddress
            
            Debug.Print "Имя: " & nName.Name & " изменено с " & oldAddress & " на " & newAddress
        End If
    Next nName

    MsgBox "Обновление диапазонов завершено!", vbInformation
End Sub

Пишу макрос, и тут начинается... Какой диапазон менять? На что менять? Давай прогресс-бар добавим, чтобы было видно, как он этот диапазон величественно раздвигает. Ну и, конечно, не забываем о всплывающем окне, которое спросит у пользователя, что вообще менять. Потому что, ну, вы же понимаете — без этого никуда. И так уже два часа ночи, а я всё ещё пишу этот "простой" макрос, который, очевидно, должен был быть готов уже три чашки кофе назад.

Но самое интересное начинается, когда вдруг вспоминаешь, что макрос-то у тебя в "Книга1", а вот работаешь ты в "Книга2". Конечно, Excel должен был догадаться сам и всё сделать правильно, но нет. Так что сидишь и дописываешь, чтобы он работал в активной книге, потому что, естественно, Excel не в состоянии сам понять, что от него хотят. Простая задача, думал я. Пара минут, говорил я.

И вот, когда наконец это всё заработало, чувствую себя настоящим героем. Макрос теперь такой умный, что сам всё заменяет, прогресс показывает, а я уже не помню, зачем вообще это делал. Но результат есть, и теперь могу раздвигать эти диапазоны, как будто это какой-то важный навык в жизни. 🙈

Код
Sub UpdateNamedRangesWithProgressBar()
    Dim nName As Name
    Dim oldAddress As String
    Dim newAddress As String
    Dim totalNames As Long
    Dim currentIndex As Long
    Dim searchValue As String
    Dim replaceValue As String
    Dim activeWorkbook As Workbook

    Set activeWorkbook = Application.ActiveWorkbook

    searchValue = InputBox("Введите значение, которое нужно заменить (например, $EB$):", "Значение для замены")
    If searchValue = "" Then Exit Sub ' Отмена, если ничего не введено

    replaceValue = InputBox("Введите новое значение, на которое нужно заменить (например, $EC$):", "Новое значение")
    If replaceValue = "" Then Exit Sub ' Отмена, если ничего не введено

    totalNames = activeWorkbook.Names.Count
    currentIndex = 0

    'прогресс-бар
    Set ProgressForm = VBA.UserForms.Add("ProgressForm")
    ProgressForm.ProgressBar.Width = 0
    ProgressForm.ProgressLabel.Caption = "Обновление диапазонов..."
    ProgressForm.Show vbModeless

    For Each nName In activeWorkbook.Names
        currentIndex = currentIndex + 1
        UpdateProgressBar currentIndex, totalNames
        oldAddress = nName.RefersTo

    ' Проверяем, содержит ли диапазон искомое значение и меняем его на новое
        If InStr(1, oldAddress, searchValue, vbTextCompare) > 0 Then
            newAddress = Replace(oldAddress, searchValue, replaceValue)

            nName.RefersTo = newAddress

            Debug.Print "Имя: " & nName.Name & " изменено с " & oldAddress & " на " & newAddress
        End If
    Next nName

    ' Закрываем форму прогресс-бара после завершения
    Unload ProgressForm
    MsgBox "Обновление диапазонов завершено!", vbInformation
End Sub

Sub UpdateProgressBar(currentIndex As Long, totalNames As Long)
    ' Обновление ширины прогресс-бара и текста на форме
    If Not ProgressForm Is Nothing Then
        With ProgressForm
            .ProgressBar.Width = (.ProgressFrame.Width - 2) * (currentIndex / totalNames)
            .ProgressLabel.Caption = "Обновление: " & currentIndex & " из " & totalNames & _
                                     " (" & Format(currentIndex / totalNames, "0%") & ")"
            DoEvents ' Обновление интерфейса
        End With
    End If
End Sub

Чтобы это всё заработало, нужно создать форму --> еще небольшая инструкция:


   
Скрытый текст
Изменено: Михаил - 18.09.2024 10:41:36
 
Михаил, ну, тогда уж и код процедуры UpdateProgressBar выкладывайте, раз решили свой код выложить
Изменено: New - 18.09.2024 03:25:38
 
Цитата
New написал:
Михаил , ну, тогда уж и код процедуры UpdateProgressBar выкладывайте, раз решили свой код выложить
а намой взгляд как код так и сам прогрессбар не нужен. Процедура должна занять мгновение, даже если диапазонов много, Правда скорее всего нужно отключить пересчет. Если не прав, то пример с диапазонами для тестов в студию.

' Проверяем, содержит ли диапазон искомое значение и меняем его на новое
       If InStr(1, oldAddress, searchValue, vbTextCompare) > 0 Then
нужно только для вывода DEBUG , а оно нужно на постоянной основе?
По вопросам из тем форума, личку не читаю.
 
Для тех, кто не сильно понимает в кодах, но захочет воспользоваться решением - небольшая ложка дегтя :)
Код будет корректно работать только если все именованные диапазоны закреплены(т.е. со знаком доллара перед именем столбца и перед номером строки). С незакрепленными диапазонами будут косяки.
Например, если надо заменить "F1:" на "F2:" в диапазонах вида "F1:F100","F1:G100", "F1:AA100" - замены будут произведены так же и в диапазоне вида "AF1:AF100"(он станет выглядеть как "AF2:AF100"). А это не всегда желательно, если предполагались замены только диапазонов столбца F.
А такие диапазоны могут быть, особенно, если имен много. И часто имена вообще вычисляются всякими функциями с кучей по разному написанных диапазонов.
В общем там полно нюансов :)

Понятно, что решение выложено как есть и в нем не все может быть гладко - поэтому и написал, чтобы у не сильно вникающих в решение и нюансы было понимание таких вот замен.
Изменено: Дмитрий(The_Prist) Щербаков - 18.09.2024 09:19:43
Даже самый простой вопрос можно превратить в огромную проблему. Достаточно не уметь формулировать вопросы...
 
Цитата
написал:
Михаил , ну, тогда уж и код процедуры UpdateProgressBar выкладывайте, раз решили свой код выложить
Ох, совсем забыл пока пост писал :-) отредактировал первое сообщение, ниже под основным кодом и прогресс бар теперь есть :-)  
 
Цитата
написал:
Например, если надо заменить "F1:" на "F2:" в диапазонах вида "F1:F100","F1:G100", "F1:AA100" - замены будут произведены так же и в диапазоне вида "AF1:AF100"(он станет выглядеть как "AF2:AF100"). А это не всегда желательно, если предполагались замены только диапазонов столбца F.
это так и будет да, тут надо пользоваться этим как функцией "замена" в екселе... надо подумать прежде чем менять :-)
А теперь надо подумать еще как докрутить так, чтобы менялось именно то, что надо... и особенно интересно как сработает с учетом знаков " * " и " ? " ... хм. Хорошо что ночей впереди полно...
 
Цитата
написал:
нужно только для вывода DEBUG , а оно нужно на постоянной основе?
Если вкратце, то можно и без него

Эта строчка используется только для Debug.Print и если вам не нужно это условие на постоянной основе, то его можно убрать. Это условие проверяет, содержит ли именованный диапазон искомое значение перед тем, как выполнить замену.

Если убрать это строчку, то можно просто выполнять замену без проверки. Тогда макрос заменит значение, даже если искомое значение не было найдено (просто ничего не изменится в таком случае).
 
Цитата
Михаил написал:
Эта строчка используется только для Debug.Print
Это вы кому поясняете?  :D
По вопросам из тем форума, личку не читаю.
 
Михаил, может проще макросом вывести списком всех на лист, там вручную под контролем поменять любым способом, затем макросом всё залить назад.
Не так конечно красиво, зато функционально.
 
Цитата
БМВ написал:
Это вы кому поясняете?
добрее надо быть
 
Михаил, Вот эта строка лишняя: Set activeWorkbook = Application.ActiveWorkbook
Так как вот эта строка totalNames = ActiveWorkbook.Names.Count - сработает у вас и без строки, которую я указал выше, т.к. в VBA уже есть объект ActiveWorkbook со всеми свойствами Активной книги
Изменено: New - 18.09.2024 22:16:49
 
тогда и эту строку надо будет убрать
Цитата
Михаил написал:
Dim activeWorkbook As Workbook
иначе конфликт будет и activeWorkbook(без отсылки к Application) будет равна nothing.
Т.е. если выполнить этот код:
Код
    Dim ActiveWorkbook As Workbook
    
    Debug.Print Application.ActiveWorkbook.Name
    Debug.Print ActiveWorkbook.Name

то первый Debug.Print выведет имя активной книги, а на втором получим ошибку.
Изменено: Дмитрий(The_Prist) Щербаков - 19.09.2024 08:13:20
Даже самый простой вопрос можно превратить в огромную проблему. Достаточно не уметь формулировать вопросы...
 
Цитата
написал:
вывести списком всех на лист, там вручную под контролем поменять любым способом, затем макросом всё залить назад.
А я уже реализовал такой вариант на следующий день после того как прочёл сообщение от Дмитрий(The_Prist) Щербаков, задумался как контролировать процесс удобнее и надёжнее в плане результата. Т.е. сейчас есть вариант с выводом на отдельный лист всех имен с диапазонами и сохранением после изменений отдельным макросом.
 
И все ж меня сильно интересует сколько ж имен в книге? Просто на достаточно старом ноутбуке 1000 обрабатывается за 3 секунды. Несмотря на то что количество ограничено объемом оперативки, все ж слабо представляю потребность в тысячах имен, даже если десяток другой на листе и сотня листов, то это менее 10К , От сюда и вопрос, а нужен ли прогрессбар?
По вопросам из тем форума, личку не читаю.
 
Цитата
БМВ написал:
А я уже реализовал такой вариант
Извините, а можно на него взглянуть?
 
Цитата
Мистер Экселистор написал:
Извините,
Извиняю, но я не писал такого.
По вопросам из тем форума, личку не читаю.
 
Цитата
написал:
И все ж меня сильно интересует сколько ж имен в книге?
Имен в книге около 500, я сожалению отправить рабочий файл со структурой не могу из-за NDA даже не структуру файла, там очень много расчетов на нескольких листах около 2700 строк и столбцы от A до EB, за счёт чего долго и происходит замена имен с включенным пересчетом я думаю.
А прогресс бар не то чтобы нужен :-) я просто его сделал. Достаточно было бы строки состояния стандартной.
 
Цитата
написал:
Извините, а можно на него взглянуть?
Строго не судите, я как смог сделал. Тут 2 макроса, один экспортирует всё на отдельный лист, второй меняет данные. Там был ряд проблем с вытаскиванием диапазонов (пришлось чуть проверок добавить) и примечания по-моему я так и не доделал, но оно работает. Комментарии в коде где посчитал важным написал.
Код
Sub ExportNamedRangesToSheet()
    Dim ws As Worksheet
    Dim nName As Name
    Dim i As Integer
    Dim rng As Range
    Dim refersToValue As String

    On Error Resume Next
    Set ws = ThisWorkbook.Sheets("NamedRanges")
    If ws Is Nothing Then
        Set ws = ThisWorkbook.Sheets.Add
        ws.Name = "NamedRanges"
    End If
    On Error GoTo 0

    ws.Cells.Clear

    ws.Cells(1, 1).Value = "Имя"
    ws.Cells(1, 2).Value = "Значение"
    ws.Cells(1, 3).Value = "Диапазон"
    ws.Cells(1, 4).Value = "Область"
    ws.Cells(1, 5).Value = "Примечание"

    totalNames = ThisWorkbook.Names.Count

    ' Заполняем таблицу именованными диапазонами
    i = 2
    currentIndex = 0 ' Счётчик для прогресса
    For Each nName In ThisWorkbook.Names
        currentIndex = currentIndex + 1 ' Обновляем прогресс

        Application.StatusBar = "Экспорт именованных диапазонов: " & currentIndex & " из " & totalNames & " (" & Format(currentIndex / totalNames, "0%") & ")"

        ws.Cells(i, 1).Value = nName.Name ' Имя

        On Error Resume Next
        refersToValue = "'" & nName.RefersTo ' Преобразуем в строку, добавляя апостроф для безопасности
        On Error GoTo 0

        If Len(refersToValue) > 0 Then
            ws.Cells(i, 2).Value = refersToValue
        Else
            ws.Cells(i, 2).Value = "" ' Оставляем пустым, если ошибка
        End If

        On Error Resume Next
        Set rng = Nothing
        Set rng = nName.RefersToRange
        On Error GoTo 0
        
        If Not rng Is Nothing Then
            ws.Cells(i, 3).Value = rng.Worksheet.Name & "!" & rng.Address ' Включаем имя листа
        Else
            ws.Cells(i, 3).Value = "Не применимо" ' Если это не диапазон
        End If

        ' Определяем, глобальный ли это диапазон или на уровне листа
        If nName.Parent Is ThisWorkbook Then
            ws.Cells(i, 4).Value = "Глобальный (Workbook)"
        Else
            ws.Cells(i, 4).Value = nName.Parent.Name ' Лист, если диапазон на уровне листа
        End If
        
        ws.Cells(i, 5).Value = "" ' Примечание
        i = i + 1
    Next nName

    Application.StatusBar = False

    MsgBox "Именованные диапазоны экспортированы на лист 'NamedRanges'.", vbInformation
End Sub

Код
Sub UpdateNamedRangesFromSheet()
    Dim ws As Worksheet
    Dim nName As Name
    Dim lastRow As Long
    Dim i As Long
    Dim newRange As String
    Dim oldName As String
    Dim totalRows As Long
    Dim currentIndex As Long

    Set ws = ThisWorkbook.Sheets("NamedRanges")

    lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
    totalRows = lastRow - 1
    currentIndex = 0

    ' Проходим по каждому имени и обновляем диапазон (можно оставить только те строки которые необходимо изменить)
    For i = 2 To lastRow
        currentIndex = currentIndex + 1

        Application.StatusBar = "Обновление именованных диапазонов: " & currentIndex & " из " & totalRows & " (" & Format(currentIndex / totalRows, "0%") & ")"

        oldName = ws.Cells(i, 1).Value
        newRange = ws.Cells(i, 3).Value

        ' Ищем соответствующее имя и обновляем его
        For Each nName In ThisWorkbook.Names
            If nName.Name = oldName Then
                On Error Resume Next
                nName.RefersTo = "=" & newRange ' Обновляем диапазон
                On Error GoTo 0
                Exit For
            End If
        Next nName
    Next i

    Application.StatusBar = False

    MsgBox "Именованные диапазоны обновлены на основе изменений на листе.", vbInformation
End Sub
Страницы: 1
Наверх