API таймеры в VBA - как сделать безопасным

Я читал в разных местах, что таймеры API рискованны в VBA, что если вы редактируете ячейку во время работы таймера, это приведет к сбою Excel. Но этот код отhttp://optionexplicitvba.wordpress.com из-за Иордании Goldmeier, похоже, не имеет этой проблемы. Он исчезает всплывающее окно с помощью таймера, и в то время как его исчезает, я могу щелкнуть и ввести текст в ячейки и строку формул без каких-либо проблем.

когда таймер API безопасен, а когда нет? Есть некоторые конкретные принципы, которые помогут мне понять? и что такое механизм аварии: что происходит именно, чтобы сделать Excel аварии?

Option Explicit
Public Declare Function SetTimer Lib "user32" ( _
    ByVal HWnd As Long, _
    ByVal nIDEvent As Long, _
    ByVal uElapse As Long, _
    ByVal lpTimerFunc As Long) As Long

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

Public TimerID As Long
Public TimerSeconds As Single
Public bTimerEnabled As Boolean
Public iCounter As Integer
Public bComplete As Boolean

Public EventType As Integer

Public Sub Reset()
    With Sheet1.Shapes("MyLabel")
        .Fill.Transparency = 0
        .Line.Transparency = 0
        .TextFrame2.TextRange.Font.Fill.ForeColor.RGB = RGB(0, 0, 0)
    End With
    Sheet1.Shapes("MyLabel").Visible = msoTrue
End Sub
Sub StartTimer()
    iCounter = 1
    Reset
    TimerID = SetTimer(0&, 0&, 0.05 * 1000&, AddressOf TimerProc)
End Sub

Sub EndTimer()
    KillTimer 0&, TimerID
    bTimerEnabled = False
    bComplete = True
End Sub

Sub TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, _
    ByVal nIDEvent As Long, ByVal dwTimer As Long)

    On Error Resume Next

    Debug.Print iCounter
    If iCounter > 50 Then
        With Sheet1.Shapes("MyLabel")
            .Fill.Transparency = (iCounter - 50) / 50
            .Line.Transparency = (iCounter - 50) / 50
            .TextFrame2.TextRange.Font.Fill.ForeColor.RGB = _
                RGB((iCounter - 50) / 50 * 224, _
                     (iCounter - 50) / 50 * 224, _
                     (iCounter - 50) / 50 * 224)
        End With
    End If

    If iCounter > 100 Then
        Sheet1.Shapes("MyLabel").Visible = msoFalse
        EndTimer
    End If

    iCounter = iCounter + 1
End Sub


Public Function ShowPopup(index As Integer)


    Sheet1.Range("Hotzone.Index").Value = index

    iCounter = 1

    If bTimerEnabled = False Then
        StartTimer
        bTimerEnabled = True
        Reset
    Else
        Reset
    End If

    With Sheet1.Shapes("MyLabel")
        .Left = Sheet1.Range("Hotzones").Cells(index, 1).Left + _
            Sheet1.Range("Hotzones").Cells(index, 1).Width
        .Top = Sheet1.Range("Hotzones").Cells(index, 1).Top - _
                (.Height / 2)
    End With
    Sheet1.Range("a4:a6").Cells(index, 1).Value = index


End Function

5 ответов


@CoolBlue: и каков механизм сбоя: что именно происходит, чтобы сделать Excel аварии?

Я могу дать вам расширение ответа Сиддарта Раута, но не полное объяснение.

вызовы API не являются VBA: они существуют вне обработчиков ошибок VBA, и когда что-то пойдет не так, они либо ничего не сделают, либо вызовут ресурс в памяти, который не существует, или попытаются прочитать (или написать!) в память, которая находится вне назначенной памяти пространство для Excel.exe

когда это произойдет, операционная система вмешается и выключит ваше приложение. Раньше мы называли это "общей ошибкой защиты", и это все еще полезное описание процесса.

теперь для некоторых деталей.

когда вы вызываете функцию в VBA, вы просто напишите имя-назовем его "CheckMyFile ()" - и это все, что вам нужно знать в VBA. Если нет ничего под названием "CheckMyFile", чтобы позвонить, или это объявленный там, где ваш вызов не может его видеть, компилятор или механизм выполнения вызовет ошибку в виде точки останова или предупреждения перед компиляцией и запуском.

за кулисами есть числовой адрес, связанный со строкой "CheckMyFile": я немного упрощаю, но мы ссылаемся на этот адрес как Указатель На Функцию - следуйте этому адресу, и мы попадем в структурированный блок памяти, который хранит определения параметров функции, пространство для их сохраненные значения и, за этим, адреса, направляющие эти параметры в функциональные структуры, созданные для выполнения VBA и возвращающие значения в адрес вывода функции.

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

Если вы дадите указатель этой функции на что - то, что не является VBA-внешним приложением или (скажем) вызов таймера API - ваша функция все еще может быть позвонил, он еще может работать, и все будет работать.

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

Если нет, внешнее приложение вызовет свои собственные обработчики ошибок, и они не будут такими прощающими, как VBA.

Он может просто отбросить вызов и ничего не делать, если Excel и VBA находятся в состоянии "занят" или иным образом недоступны, когда он пытается использовать этот указатель функции: вам может повезти, только один раз. Но это может вызвать гнев операционной системы на Excel.процесс exe.

Если вызов приводит к ошибке, и эта ошибка не обрабатывается вашим кодом, VBA поднимет ошибку вызывающему абоненту - и, поскольку вызывающий абонент не является VBA, у него, вероятно, не будет способа обработки этого: и он вызовет "помощь" из операционной системы.

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

эти предположения:

  1. определенно будет действительная функция за этим указателем;
  2. он определенно будет доступен, когда он называется;
  3. ...И это не вызовет ошибок у вызывающего.

с вызовом API, caller is операционная система, и ее ответ на обнаружение ошибки будет вас закрыть.

Так что это очень простой план процесса - "почему" вместо того, чтобы объяснять "что".

полное объяснение, без излишних упрощений, для разработчиков C++. Если вы действительно хотите получить подробный ответ, вы должны научиться программировать с помощью указателей; и вы должны свободно владеть концепциями и практикой выделения памяти, исключениями, последствиями плохого указателя и механизмами, используемыми операционной системой для управления запущенными приложениями и обнаружения недопустимой операции.

VBA существует, чтобы защитить вас от этого знания и упрощается задача написания приложений.


Я читал в разных местах, что таймеры API рискованны в VBA

ну заявление должно быть I read in various places that API timers are risky? И причина, по которой я говорю это, заключается в том, что эти API можно использовать в VB6/VBA/VB.Net etc..

так они рискованные? Да они а то так ходьба по канату. Одно неверное движение-и все. И это не просто SetTimer API но почти с любым API.

Я создал пример еще в 2009 году, что использует SetTimer API для создания заставок в Excel. Вот это ссылке.

теперь, если вы извлекаете файлы и непосредственно открыть файл excel, то вы увидите, что Excel аварийно завершает работу. Чтобы заставить его работать, нажмите SHIFT ключ, а затем откройте Excel, чтобы макросы не запускались. Затем измените путь изображений. Новый путь будет путем изображений, извлеченных из zip-файла. как только вы измените путь, просто сохраните и закройте файл. В следующий раз когда вы запустите его, Excel не аварийно завершит работу.

вот код в файле Excel

Public Declare Function SetTimer Lib "user32" ( _
ByVal HWnd As Long, ByVal nIDEvent As Long, _
ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

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

Public TimerID As Long, TimerSeconds As Single, tim As Boolean
Dim Counter As Long
Sub StartTimer()
    '~~ Set the timer.
    TimerSeconds = 1
    TimerID = SetTimer(0&, 0&, TimerSeconds * 1000&, AddressOf TimerProc)
End Sub

Sub EndTimer()
    On Error Resume Next
    KillTimer 0&, TimerID
End Sub

Sub TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, _
ByVal nIDEvent As Long, ByVal dwTimer As Long)
    If tim = False Then
        UserForm1.Image1.Picture = LoadPicture("C:\temp.bmp")
        tim = True
    Else
        UserForm1.Image1.Picture = LoadPicture("C:\temp.bmp")
        tim = False
    End If
    Counter = Counter + 1
    If Counter = 10 Then
        EndTimer
        Unload UserForm1
    End If
End Sub

когда таймер API безопасен, а когда нет? Есть ли какие-то общие принципы, которые помогут мне понять?

Итак, все сводится к одному факту. насколько надежен ваш код. Если ваш код обрабатывает каждый сценарий, то SetTimer API или по сути любой API не подведет.


указатель-Безопасные и 64-разрядные объявления для Windows Timer API в VBA:

как и было обещано, вот 32-разрядные и 64-разрядные объявления API для API таймера, используя LongLong и безопасный тип указателя:

Option Explicit
Option Private Module
#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows ' Use LongLong and LongPtr
Private Declare PtrSafe Function SetTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr, _ ByVal uElapse As LongLong, _ ByVal lpTimerFunc As LongPtr _ ) As LongLong
Public Declare PtrSafe Function KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr _ ) As LongLong Public TimerID As LongPtr

#ElseIf VBA7 Then ' 64 bit Excel in all environments ' Use LongPtr only, LongLong is not available
Private Declare PtrSafe Function SetTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As Long, _ ByVal uElapse As Long, _ ByVal lpTimerFunc As LongPtr) As LongPtr
Private Declare PtrSafe Function KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As Long) As Long
Public TimerID As LongPtr
#Else ' 32 bit Excel
Private Declare Function SetTimer Lib "user32" _ (ByVal hwnd As Long, _ ByVal nIDEvent As Long, _ ByVal uElapse As Long, _ ByVal lpTimerFunc As Long) As Long
Public Declare Function KillTimer Lib "user32" _ (ByVal hwnd As Long, _ ByVal nIDEvent As Long) As Long
Public TimerID As Long
#End If

' Call the timer as: ' SetTimer 0&, 0&, lngMilliseconds, AddressOf TimerProc

#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows ' Use LongLong and LongPtr ' Note that wMsg is always the WM_TIMER message, which actually fits in a Long
Public Sub TimerProc(ByVal hwnd As LongPtr, _ ByVal wMsg As LongLong, _ ByVal idEvent As LongPtr, _ ByVal dwTime As LongLong) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****

End Sub

#ElseIf VBA7 Then ' 64 bit Excel in all environments
' Use LongPtr only
Public Sub TimerProc(ByVal hwnd As LongPtr, _ ByVal wMsg As Long, _ ByVal idEvent As LongPtr, _ ByVal dwTime As Long) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****

End Sub

#Else ' 32 bit Excel
Public Sub TimerProcInputBox(ByVal hwnd As Long, _ ByVal wMsg As Long, _ ByVal idEvent As Long, _ ByVal dwTime As Long) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****
End Sub

#End If

параметр hwnd установлен в ноль в приведенном выше примере кода и всегда будет равен нулю, если вы вызываете это из VBA вместо того, чтобы связывать вызов с (скажем) InputBox или формой.

A полностью отработанный пример этого API таймера, включая использование параметра hwnd для окна, доступен на веб-сайте Excellerando:

использование VBA InputBox для паролей и скрытие ввода клавиатуры пользователя звездочками.




Примечание:

Это было опубликовано как отдельный ответ на мое объяснение системных ошибок, связанных с вызовом API таймера без тщательного обработка ошибок: это отдельная тема, и StackOverflow выиграет от отдельного и доступного для поиска ответа с безопасными указателями и 64-разрядными объявлениями для API таймера Windows.

в интернете есть плохие примеры объявлений API; и есть очень мало примеров для общего случая VBA7 (который поддерживает тип безопасного указателя), установленного в 32-разрядной среде Windows (которая не поддерживает 64-разрядное целое число "LongLong").


@CoolBlue я написал код, который вы выложили выше. Это правда, что API может действовать непредсказуемо, по крайней мере, по сравнению с обычным кодом. Однако, если ваш код достаточно надежен (следуя комментариям @Siddharth Rout сверху), то это больше не предсказание. На самом деле эта непредсказуемость возникает в процессе развития.

например, в моей первой итерации всплывающего окна опрокидывания, созданного выше, я случайно набрал KillTimer в операторе IF. В основном, где EndTimer существует сейчас Я написал KillTimer. Я сделал это не подумав. Я знал, что у меня есть процедура, которая закончит таймер, но я на мгновение перепутал EndTimer с KillTimer.

Итак, вот почему я поднимаю этот вопрос: обычно, когда вы делаете этот тип ошибки в Excel, вы получите ошибку во время выполнения. Однако, поскольку вы работаете с API, вы просто получаете незаконную ошибку, и все приложение Excel перестает отвечать и завершает работу. Итак, если вы не сохранили перед запуском таймера, вы проиграете все (что, по сути, произошло со мной в первый раз). Хуже того, поскольку вы не получаете ошибку времени выполнения, вы не сразу узнаете, какая строка вызвала ошибку. В таком проекте вы должны ожидать нескольких незаконных ошибок (и последующей перезагрузки Excel) для диагностики ошибки. Иногда это может быть болезненным процессом. Но это типичная ситуация отладки, которая происходит при работе с API. Что ошибки не выделяются напрямую - и незаконные ошибки кажутся случайными - вот почему многие описывают API как непредсказуемые и рискованные.

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

и просто чтобы дать вам некоторые конкретные рекомендации, чтобы избежать ошибок:

  • если вы начинаете таймер, убедись, что убьешь его позже. Если у вас есть ошибка выполнения Excel до того, как таймер будет убит, он может продолжаться вечно и съесть вашу память. Используйте консоль (Debug.Print) для записи строки при каждом вызове TimerProc. Если он продолжает тикать в консоли даже после выполнения кода, у вас есть таймер побега. Закройте Excel и вернитесь, когда это произойдет.
  • не используйте несколько таймеров. Используйте один таймер для обработки нескольких элементов синхронизации.
  • не запускайте новый таймер, не убивая старый.
  • самое главное: проверьте на компьютере вашего друга, чтобы убедиться, что он работает на разных платформах.

кроме того, просто для ясности:нет проблем с использованием таймера API и редактированием ячейки одновременно. нет ничего о таймерах, которые будут препятствовать вашей способности редактировать что-либо на листе.


Я также столкнулся с тем, что Excel аварийно завершает работу при вводе значения и нашел этот вклад. Здорово! Моя проблема была решена, как только я добавил эту строку:

On Error Resume Next 

в "TimerProc".