Как изящно выйти из середины вложенной подпрограммы, когда пользователь отменяет?
(Я использую 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)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
Если бы это был я, я бы использовал 2 кнопки - одну для GO и одну для STOP. Кнопка STOP становится видимой, когда вы нажимаете GO. Событие Click для STOP просто скрывается - вот и все.
Затем ваш цикл может просто проверить, видна ли еще кнопка STOP. Если это не так, это означает, что он был нажат, и вы должны вырваться.
Ваши элементы управления - это статические объекты с областью действия формы ...
Одна из возможных альтернатив - переложить тяжелую работу на новый поток. Затем вы можете либо напрямую убить этот поток, если пользователь хочет отменить, либо вы можете отправить сообщение в поток.
Переключение с помощью имени кнопки, как вы это делали выше, - довольно часто встречающийся трюк и довольно безопасный, если у вас есть только несколько состояний кнопки.
Вам нужна какая-то общая переменная, чтобы ваш цикл for и ваша кнопка могли взаимодействовать. Я бы поместил цикл for (и связанный с ним код) в командный объект. Мой VB заржавел, но я думаю, что вы можете объявлять модули с их собственными «глобальными» переменными и функциями. Вы можете переместить весь код в модуль и просто проверить глобальную переменную, как вы это делаете сейчас.
Моя основная проблема с образцом кода, который вы опубликовали, не имеет ничего общего с отменой пользователем, а скорее со всем остальным: вы проверяете свое рабочее состояние, читая текст кнопки, вместо того, чтобы делать это наоборот (установите текст кнопки из-за текущее состояние, которое следует сохранить в переменной); вы используете GOTO для выхода из цикла for вместо разрыва (есть ли у VB разрывы?), и вы помещаете свой код очистки вне обычного потока, когда мне кажется, что он может быть запущен независимо от того, отменил пользователь или нет.
Я всегда использовал глобальную логическую переменную, такую как bUserPressedCancel, вместе с DoEvents внутри цикла. Элегантно, смело, работает.
Я согласен с мистером Шини в том, что проверка ценности подписи - не лучшая идея. Если вы измените текст на кнопке в дизайнере, вы нарушите код. Лучше не полагаться на формулировку текста, чтобы ваш код работал.