Заставить функции Excel влиять на «другие» ячейки - PullRequest
4 голосов
/ 14 октября 2008

Допустим, я создаю Sub (не функцию), чья миссия в жизни состоит в том, чтобы взять активную ячейку (т.е. выделение) и установить для соседней ячейки какое-то значение. Это отлично работает.

Когда вы пытаетесь преобразовать этот Sub в функцию и пытаетесь оценить его по электронной таблице (то есть задаете ее формулу "= MyFunction ()"), Excel будет лаять на тот факт, что вы пытаетесь повлиять на значение неактивная ячейка, и просто заставьте функцию возвращать #VALUE, не касаясь соседней ячейки.

Возможно ли отключить это защитное поведение? Если нет, какой хороший способ обойти это? Я ищу что-то, что компетентный разработчик может выполнить за 1-2 недели, если это возможно.

С уважением, Алан.

Примечание: я использую 2002, поэтому я бы предпочел решение, которое будет работать для этой версии. Учитывая это, если будущие версии сделают это значительно проще, я бы тоже хотел об этом знать.

Ответы [ 6 ]

9 голосов
/ 14 октября 2008

Это невозможно сделать, что имеет смысл, потому что:

  • Когда вызывается функция листа, ячейка, содержащая функцию, не обязательно является активной ячейкой. Таким образом, вы не можете найти соседнюю ячейку надежно.

  • Когда Excel пересчитывает лист, он должен поддерживать зависимости между ячейками. Поэтому он не может позволить функциям таблицы произвольно изменять другие ячейки.

Лучшее, что вы можете сделать, это одно из:

  • Обработка события SheetChange. Если ячейка, содержащая вашу функцию, изменяется, измените соседнюю ячейку.

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

Обновление

По поводу комментария: «Я бы хотел, чтобы эта функция работала с« пустой »электронной таблицей, поэтому я не могу полагаться на событие SelectionChange электронных таблиц, которые еще не существуют, но должны будут вызывать эту функцию» :

  • Можете ли вы поместить свою функцию в надстройку XLA? Тогда ваша надстройка XLA может обрабатывать событие Application SheetChange (*) для всех книг, которые открываются в этом экземпляре Excel?

Относительно комментария: «Тем не менее, если вы сохраните Excel в CalculationMode = xlManual и заполните только значения, у вас все будет в порядке»

  • Даже если для CalculationMode задано значение xlManual, Excel необходимо поддерживать дерево зависимостей ссылок между ячейками, чтобы он мог вычислять в правильном порядке. И если одна из функций может обновить произвольную ячейку, это испортит порядок. По-видимому, именно поэтому Excel накладывает это ограничение.

(*) Первоначально я написал SelectionChange выше, исправил сейчас - конечно, правильным событием является SheetChange для объектов Workbook или Application или Change для объекта Worksheet.

Обновление 2 Несколько замечаний по поводу поста AlanR , описывающего, как «заставить» работать, используя таймер:

  • Непонятно, как функция таймера ("Woohoo") узнает, какие ячейки обновлять. У вас нет информации о том, какая ячейка содержит формулу, которая запустила таймер.

  • Если формула существует более чем в одной ячейке (в одной и той же или разных книгах), то UDF будет вызываться несколько раз во время пересчета, перезаписывая timerId. В результате вам не удастся надежно уничтожить таймер и произойдет утечка ресурсов Windows.

2 голосов
/ 14 октября 2008

Согласно Как создать пользовательские функции Excel, определенные пользователем :

Ограничения UDF's

  • Невозможно поместить значение в ячейку, отличную от ячейки (или диапазона), содержащей формула. Другими словами, UDF предназначен для использования в качестве "формул", а не обязательно "макросы".

Итак, похоже, что это невозможно сделать.

2 голосов
/ 14 октября 2008

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

Это вид функционального программирования, без побочных эффектов. Если бы вы могли просто изменить другие ячейки внутри функции (используемой на рабочем листе), то у Excel не было бы возможности узнать порядок и что пересчитать в случае изменения ячейки.

Эта статья также содержит много информации о том, как Excel выполняет пересчет. Но никогда не говорится, что другие клетки заморожены.

Я не знаю, что вы пытаетесь сделать, но почему бы вам просто не поместить другую функцию в соседнюю ячейку, которая принимает в качестве параметра первую ячейку?

Пример:

Public Function Bar(r As Range) As Integer
  If r.Value = 2 Then
    Bar = 0
  Else
    Bar = 128
  End If
End Function
1 голос
/ 07 января 2009

Вот простой обходной путь VBA, который работает. В этом примере откройте новую книгу Excel и скопируйте следующий код в область кода для Sheet1 (не ThisWorkbook или VBA Module). Затем перейдите в Sheet1 и поместите что-нибудь в одну из верхних левых ячеек рабочего листа. Если вы наберете число и нажмете Enter, то ячейка справа будет обновлена ​​в 4 раза больше, и фон ячейки станет голубым. Любое другое значение вызывает очистку следующей ячейки. Вот код:

Dim busy As Boolean
Private Sub Worksheet_Change(ByVal Target As Range)
  If busy Then Exit Sub
  busy = True
  If Target.Row <= 10 And Target.Column <= 10 Then
    With Target.Offset(0, 1)
      If IsNumeric(Target) Then
        .Value = Target * 4
        .Interior.Color = RGB(212, 212, 255)
      Else
        .Value = Empty
        .Interior.ColorIndex = xlColorIndexNone
      End If
    End With
  End If
  busy = False
End Sub

Подпрограмма фиксирует все события изменения ячейки на листе. Если строка и столбец оба <= 10, то ячейка справа устанавливается в 4 раза больше измененной ячейки, если значение является числовым; в противном случае ячейка справа очищается. </p>

1 голос
/ 27 ноября 2008

Хотя вы не можете сделать это в Excel, это возможно в Resolver One (хотя это все еще довольно странная вещь).

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

В качестве примера того, что вы спрашиваете, вы можете определить функцию safeDivide, которая (вместо вызова ZeroDivisionError) сообщала вам о проблеме, закрашивая ячейку знаменателя и помещая сообщение об ошибке рядом Это. Вы можете определить это так:

def safeDivide(numerator, cellRange):
    if not isinstance(cellRange, CellRange):
        raise ValueError('denominator must be a cell range')
    denominator = cellRange.Value
    if denominator == 0:
        cell = cellRange.TopLeft
        cell.BackColor = Color.Red
        cell.Offset(1, 0).Value = 'Tried to divide by zero'
        return 0
    return numerator / denominator

Есть дополнительная складка: функции, которые передают переданные ячейки, просто передают значение ячейки, поэтому, чтобы обойти это, мы настаиваем на том, чтобы передать знаменатель для одной ячейки.

Если вы пытаетесь делать необычные вещи с электронными таблицами, которые не совсем вписываются в Excel, или вы заинтересованы в использовании возможностей Python для работы с данными электронных таблиц, стоит взглянуть на Resolver One.

1 голос
/ 16 октября 2008

Спасибо всем за отклик. Это возможно сделать! Своего рода. Я говорю «вроде», потому что технически говоря «функция» не влияет на клетки вокруг нее. Однако на практике ни один пользователь не может заметить разницу.

Хитрость заключается в том, чтобы использовать Win32 API для запуска таймера, и как только он отключается, вы делаете то, что хотите, в любую ячейку и выключаете таймер.

Теперь я не эксперт в том, как работает COM-потоки (хотя я знаю, что VBA - это однокомпонентный поток), но будьте осторожны с тем, что ваш таймер убегает с процессом Excel и приводит к его сбою. Это действительно не то, что я бы предложил в качестве решения для любой другой таблицы.

Просто создайте модуль со следующим содержимым:

Option Explicit

Declare Function SetTimer Lib "user32" (ByVal HWnd As Long, _
  ByVal IDEvent As Long, ByVal mSec As Long, _
  ByVal CallFunc As Long) As Long

Declare Function KillTimer Lib "user32" (ByVal HWnd As Long, _
  ByVal timerId As Long) As Long

Private timerId As Long

Private wb As Workbook
Private rangeName As String
Private blnFinished As Boolean

Public Sub RunTimer()

    timerId = SetTimer(0, 0, 10, AddressOf Woohoo)


End Sub


Public Sub Woohoo()

    Dim i As Integer

'    For i = 0 To ThisWorkbook.Names.Count - 1
'        ThisWorkbook.Names(i).Delete
'    Next

     ThisWorkbook.Worksheets("Sheet1").Range("D8").Value = "Woohoo"

     KillTimer 0, timerId

End Sub
...