Как изящно выйти из середины вложенной подпрограммы, когда пользователь отменяет?

(Я использую VB6, но думаю, что это встречается на большинстве других языков.)

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

Я использовал статическую переменную, чтобы это работало очень хорошо (см. Код ниже), но я очищаю проект и хочу поместить цикл For / Next в его собственную функцию, поскольку он требуется в нескольких разных местах проекта. .

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

Итак, в основном мой вопрос заключается в том, как мне воспроизвести это:

Private Sub DoSomething_Click()

  Static ExitThisSub As Boolean ' Needed for graceful exit

  If DoSomething.Caption = "Click To Stop Doing Something" Then
    ExitThisSub = False ' this is the first time we've entered this sub
  Else ' We've re-entered this routine (user clicked on button to stop it)
    ExitThisSub = True ' Set this so we'll see it when we exit this re-entry
    Exit Sub '
  End If


  DoSomething.Caption = "Click To Stop Doing Something"

  For i = 0 To ReallyBigNumber
    Call DoingSomethingSomewhatTimeConsuming
    If ExitThisSub = True Then GoTo ExitThisSubNow
    DoEvents
  Next

  ' The next line was missing from my original example,
  ' prompting appropriate comments
  DoSomething.Caption = "Click To Do Something"

  Exit Sub

ExitThisSubNow:

  ExitThisSub = False ' clear this so we can reenter later
  DoSomething.Caption = "Click To Do Something"

End Sub

Когда я перемещаю цикл for / next к его собственной функции?

Я думаю, что заменю ExitThisSub на общедоступную переменную QuitDoingSoManyLongCalculations, которая таким же образом выйдет из новой подпрограммы for / next, а затем из DoSomething_Click.

Но когда я использую глобальные переменные, я всегда чувствую себя любителем (каковым я являюсь) - есть ли более элегантное решение?

Ответов (6)

Решение

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

Но будьте осторожны с DoEvents. Это в основном означает, что цикл сообщений Windows может обрабатывать сообщения. Это означает, что пользователь может не только снова щелкнуть вашу кнопку, но и закрыть форму и заняться другими делами. Поэтому, когда вы находитесь в этом цикле, вам все равно нужно будет установить переменную уровня модуля, поскольку вам нужно будет проверить ее в QueryUnload формы и в любых обработчиках событий.

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

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

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

Option Explicit

Private Enum StopFlag
   NotSet = 0
   StopNow = 1
   StopExit = 2
End Enum

Private m_lngStopFlag As StopFlag
Private m_blnProcessing As Boolean

Private Sub cmdGo_Click()

   Dim lngIndex As Long
   Dim strTemp As String

   m_lngStopFlag = StopFlag.NotSet
   m_blnProcessing = True

   cmdStop.Visible = True
   cmdGo.Visible = False

   For lngIndex = 1 To 99999999

      ' check stop flag
      Select Case m_lngStopFlag

         Case StopFlag.StopNow

            MsgBox "Stopping - Last Number Was " & strTemp
            Exit For

         Case StopFlag.StopExit

            m_blnProcessing = False
            End

      End Select

      ' do your processing
      strTemp = CStr(lngIndex)

      ' let message loop process messages
      DoEvents

   Next lngIndex

   m_lngStopFlag = StopFlag.NotSet
   m_blnProcessing = False
   cmdGo.Visible = True
   cmdStop.Visible = False

End Sub

Private Sub cmdStop_Click()

   m_lngStopFlag = StopFlag.StopNow

End Sub

Private Sub Form_Load()

   m_blnProcessing = False

End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)

   Select Case UnloadMode

      Case vbFormControlMenu, vbFormCode

         If m_blnProcessing Then

            Cancel = True

            If MsgBox("Unload Attempted - Cancel Running Process?", vbOKCancel + vbDefaultButton1 + vbQuestion, "Test") = vbOK Then

               m_lngStopFlag = StopFlag.StopExit

            End If

         End If

      Case Else

         m_lngStopFlag = StopFlag.StopExit
         Cancel = True

   End Select

End Sub

Работает до тех пор, пока вам не понадобится выполнить локализацию или что-то еще, что требует изменения пользовательского интерфейса независимо от логики. Я бы использовал свойство Tag или переменную уровня частного модуля. Затем вы можете изменять заголовок независимо от логики.

Если бы это был я, я бы использовал 2 кнопки - одну для GO и одну для STOP. Кнопка STOP становится видимой, когда вы нажимаете GO. Событие Click для STOP просто скрывается - вот и все.

Затем ваш цикл может просто проверить, видна ли еще кнопка STOP. Если это не так, это означает, что он был нажат, и вы должны вырваться.

Ваши элементы управления - это статические объекты с областью действия формы ...

Одна из возможных альтернатив - переложить тяжелую работу на новый поток. Затем вы можете либо напрямую убить этот поток, если пользователь хочет отменить, либо вы можете отправить сообщение в поток.

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

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

Моя основная проблема с образцом кода, который вы опубликовали, не имеет ничего общего с отменой пользователем, а скорее со всем остальным: вы проверяете свое рабочее состояние, читая текст кнопки, вместо того, чтобы делать это наоборот (установите текст кнопки из-за текущее состояние, которое следует сохранить в переменной); вы используете GOTO для выхода из цикла for вместо разрыва (есть ли у VB разрывы?), и вы помещаете свой код очистки вне обычного потока, когда мне кажется, что он может быть запущен независимо от того, отменил пользователь или нет.

Я всегда использовал глобальную логическую переменную, такую ​​как bUserPressedCancel, вместе с DoEvents внутри цикла. Элегантно, смело, работает.

Я согласен с мистером Шини в том, что проверка ценности подписи - не лучшая идея. Если вы измените текст на кнопке в дизайнере, вы нарушите код. Лучше не полагаться на формулировку текста, чтобы ваш код работал.