Kontrollierter Abbruch einer laufenden VB Prozedur mit ESCAPE

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 24 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

    Kontrollierter Abbruch einer laufenden VB Prozedur mit ESCAPE

    Hi,

    ich habe eine VB Anwendung, die recht lange läuft. Manchmal möchte ich die laufende Prozedur mit ESCAPE abbrechen (etwa eine Prozedur, die mit Button_Click gestartet wurde) ... aber ohne das Programm zu beenden.

    Insbesondere möchte ich bestimmen, an welchen Stellen das Programm angehalten werden darf. Beispielsweise möchte ich während des Schreibens eines Files nicht so gern abbrechen, sondern erst abwarten, bis das Schreiben zu einem normalen Ende gekommen ist und dann zuschlagen.

    Das Problem dürfte ich nicht allein haben. Ich könnte mir vorstellen, dass es dazu einige Lösungsansätze gibt. Hoffentlich kann mir jemand da ein bissl weiterhelfen.

    Wie immer hoffe ich, dass ich mich verständlich ausgedrückt habe. :)

    LG
    Peter
    Die laufende Prozedur mit async / await aufrufen, dann einfach KeyPress abonnieren, und dabei ein flag oder so setzen, der in der Prozedur dann ab und zu abgefragt wird. Wenn die Form dabei auch den Focus verlieren darf, aber trotzdem reagieren soll, dann mit Polling und GetAsyncKeyState, siehe PInvoke.
    »There's no need to "teach" atheism. It's the natural result of education without indoctrination.« — Ricky Gervais
    @Peter329 Mit GetAsyncKeyState(), gugst Du hier, musst nur Deine Taste einsetzen.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Danke erst mal für die Ratschläge. Ich habe jetzt das Coding von RFG ausprobiert.

    Das Sample läuft (erwartungsgemäß) hervorragend.

    Aber bei mir klappt das nicht:

    VB.NET-Quellcode

    1. Private Function ScanTreeProcess(RealPath As String,
    2. intLevel As Integer,
    3. strFunction As String) As Integer
    4. 'Check programm intercepted
    5. Debug.Print(DateTime.Now.ToString & ": " & Expose(RealPath))
    6. If (GetAsyncKeyState(Keys.Space) And &H8000) <> 0 Then
    7. If MessageBox.Show("Programm interupted" & NewLine2 &
    8. "Do want to exit?",
    9. "Program interrupt",
    10. MessageBoxButtons.YesNo,
    11. MessageBoxIcon.Warning) = DialogResult.Yes Then Return Retv.Cancel
    12. End If


    Mein Programm ist halt ein bissl komplexer. Mein Button_Click ruft eine Init Prozedur, die ruft dann die ScanTreeProcess Prozedur ... und die wiederum ruft sich rekursiv selbst auf. Am Ende springe ich in die Button_Click Routine zurück und dort rufe ich ein Ergebnis Window (DisplayLog) auf.

    In diesem Umfeld greift die GetAsyncState Routine offensichtlich nicht. Die "If MessageBox.Show ..." Anweisung wird einfach nicht angesprungen. Die Eingabe, die ich während der ScanTreeProcess Prozedur gemacht habe - egal ob space oder sonstwas - landen im ersten Eingabefeld des DisplayLog Window.

    Ich weiß, dass ist so einfach nicht nachzustellen. Aber vielleicht habt ihr ja eine Idee was da bei mir falsch laufen könnte.

    LG
    Peter

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Peter329“ ()

    @Peter329 Setz mal auf das If einen Haltepunmkt, ob er da ühaupt vorbeikommt.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Jau, die If Abfrage wird durchlaufen. Das war aber klar, weil die davor stehende Debug.Print Anweisung ja auch durchlaufen wurde, die mir anzeigt, was mein Programm gerade macht.

    Ich habe versucht die Funktion selbst mit Debug zu tracen ... ist ja eigentlich eine nette Idee. Aber leider klappt das nicht:

    VB.NET-Quellcode

    1. <DllImport("user32.dll")>
    2. Private Shared Function GetAsyncKeyState(ByVal vKey As Keys) As Int16
    3. Debug.Print("GetAsyncKeyState")
    4. End Function


    liefert diverse Fehlermeldungen:

    Quellcode

    1. Warnung BC42353 Die Funktion "GetAsyncKeyState" gibt nicht für alle Codepfade einen Wert zurück. Fehlt eine Return-Anweisung?
    2. Fehler BC31522 System.Runtime.InteropServices.DllImportAttribute kann nicht auf "Sub", "Function" oder "Operator" angewendet werden, die Text enthalten.

    @Peter329 So geht das nicht.
    Wenn Du das so testen willst, musst Du eine Wrapper-Funktion drumherum schreiben:

    VB.NET-Quellcode

    1. Private Shared Function GetAsyncKeyStateNet(ByVal vKey As Keys) As Boolean
    2. Debug.Print("GetAsyncKeyState")
    3. Return (GetAsyncKeyState(vKey) And &H8000) <> 0
    4. End Function
    Und bedenke, dass diese Funktion genau eine Taste abfragt, nicht aber eine andere als die übergebene.
    Wenn Du eine andere drückst, kommt nix.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Okie ... also die Funktion wird angesprungen ... aber sie liefert einen anderen Rückgabewert als erwartet:

    Quellcode

    1. GetAsyncKeyState=00 00 00 00 (ohne Interrupt)
    2. GetAsyncKeyState=00 00 00 01 (mit Interrupt)


    Mit folgendem Coding klappt dann die Sache:

    VB.NET-Quellcode

    1. 'Check programm intercepted
    2. Debug.Print(DateTime.Now.ToString & ": " & Expose(RealPath))
    3. Dim intKeystate As Integer = GetAsyncKeyState(Keys.Space)
    4. Debug.Print("GetAsyncKeyState=" & IntegerToHex(intKeystate))
    5. If (intKeystate And &H0001) <> 0 Then
    6. If MessageBox.Show("Programm interupted" & NewLine2 &


    Warum auch immer das Bit jetzt nicht mehr am Anfang sondern am Ende steht.

    Jetzt hab ich nur noch ein Problemchen:

    Wenn der Anwender ungeduldig ist und mehrfach Space drückt, dann wird das erste Space vom GetAsyncKeyState entgegen genommen. Die nächsten Leerzeichen werden aber im nachfolgenden Window übernommen. Ich müsste also in der Load Routine dieses Window alle gepufferten Eingaben löschen. Das müsste doch zu machen sein ...

    LG
    Peter
    Na, solange ich nicht die Space-Taste drücke ist das ohne Interrupt ... Sobald ich die Space Taste aber gedrückt habe, habe ich einen Interrupt:

    so sieht mein Debugging aus:

    Quellcode

    1. GetAsyncKeyState=00 00 00 00
    2. GetAsyncKeyState=00 00 00 00
    3. GetAsyncKeyState=00 00 00 00
    4. GetAsyncKeyState=00 00 00 00
    5. GetAsyncKeyState=00 00 00 00 <-- danach habe ich die Space Taste gedrückt
    6. GetAsyncKeyState=FF FF 80 01


    Jetzt hab ich mal zur Abwechslung statt 00 00 00 01 den Wert FF FF 80 01 erhalten.

    Die Frage ist halt, wie die Bits des Rückgabewerts belegt sind.
    @Peter329 GetAsyncKeyState() gibt ein Int16 (Short) zurück, da kannst Du die führenden beide FF eh ignorieren.
    Gugst Du MSDN.
    Gugst Du PInvoke.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Jeheyyyy ... jetzt hats geschnackelt.

    Also, GetAsyncKeyState liefert 16 Bits zurück. Das linke (highorder) Bit gibt an, ob die Taste gerade gedrückt ist ... das rechte (loworder) Bit gibt an, ob die Taste gedrückt ist oder irgendwann vorher gedrückt wurde. Dieses Bit ist nicht verlässlich (weil andere parallel laufende Anwendungen es "klauen" können) und wird nur aus Gründen der Kompatibilität mitgeführt. Es sollte deshalb nicht mehr verwendet werden.

    Wenn man also auf &H8000 abfragt, dann greift das nur, wenn die Taste zu diesem Zeitpunkt auch gedrückt ist. In deinem Sample ist das kein Problem, weil der Zähler affig schnell rennt. In meinem Programm ist das sehr wohl ein Problem, weil meine Schleifen mehrere Sekunden laufen!

    Jetzt ist auch klar, wieso ich meistens 00 00 00 01 zurück erhalte, nachdem ich die Interrupt Taste gedrückt habe ... die Taste ist zum Abfragezeitpunkt nicht mehr "down" aber wurde vorher gedrückt. Ab und zu erhalte ich FF FF 80 01 zurück. Da hatte ich gerade Glück (oder Pech) und die Taste war zum Zeitpunkt der Abfrage doch gerade mal gedrückt. Das FF FF zu Beginn entsteht einfach durch die Konvertierung von Short nach Integer, da wird das Vorzeichen (in diesem Fall eine binäre 1) repliziert.

    Um meine Schleife also abzubrechen muss ich die Taste solange gedrückt halten, bis die MessageBox aufspringt. Blöde ist halt, dass die meisten Tasten ein AutoRepeat haben und das schlägt dann im Folgefenster zu Buche. Ich nehme also eine Taste, die kein AutoRepeat hat, z.B. die linke CTRL Taste. Und schon klappt die Sache:

    VB.NET-Quellcode

    1. 'Check programm intercepted
    2. Debug.Print(DateTime.Now.ToString & ": " & Expose(RealPath))
    3. Dim VK_LCONTROL As Keys = CType(&HA2, Keys)
    4. Dim shortKeystate As Short = GetAsyncKeyState(VK_LCONTROL)
    5. Dim hexKeystate As String = ShortToHex(shortKeystate, "D"c)
    6. Debug.Print("GetAsyncKeyState=" & hexKeystate)
    7. If (shortKeystate And &H8000) <> 0 Then ...


    Danke für deine unermessliche Geduld ... du hast meinen Tag gerettet! :)

    LG
    Peter

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Peter329“ ()

    Jetzt hab ich doch noch eine Frage.

    Ich bin dabei meine Interrupt Steuerung in verschiedene Forms einer VB Anwendung einzubauen.

    If CheckInterrupt() Then ...

    CheckInterupt ist wie folgt definiert.

    VB.NET-Quellcode

    1. <DllImport("user32.dll")>
    2. Public Shared Function GetAsyncKeyState(ByVal vKey As Keys) As Int16
    3. End Function
    4. <DebuggerStepThrough()>
    5. Public Function CheckInterrupt() As Boolean
    6. If (GetAsyncKeyState(VK_LCONTROL) And &H8000) <> 0 Then
    7. If MessageBox.Show("Programm interupted" & NewLine2 &
    8. "Do want to exit?",
    9. "Program interrupt",
    10. MessageBoxButtons.YesNo,
    11. MessageBoxIcon.Warning) = DialogResult.Yes Then
    12. Return True
    13. End If
    14. End If
    15. Return False
    16. End Function


    Bisher steht diese Deklaration in meiner Start Form. Nun würde ich diese Deklaration gern in ein zentrales Modul einbauen, damit es alle Forms verwenden können.

    Das klappt aber leider nicht. Die Anweisung <DllImport("user32.dll")> liefert einen Compile Fehler, weil das nicht in einem Modul stehen darf.

    Wie kann man denn diese Sache "global" also für alle Form deklarieren?

    LG
    Peter

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Peter329“ ()

    Peter329 schrieb:

    VB.NET-Quellcode

    1. Public Function CheckInterrupt() As Boolean
    Machst Du

    VB.NET-Quellcode

    1. Public Shared Function CheckInterrupt() As Boolean
    und kannst von überall ohne Instanz, jedoch mit dem Klassennamen davor, drauf zugreifen:

    VB.NET-Quellcode

    1. If Form1.CheckInterrupt() Then

    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Okie dokie ... so funktioniert das ...

    Aber natürlich ist das nicht so ganz befriedigend, dass ich eine Routine etwa in frmHugo deklariere, die ich dann in frmHugoline mit entsprechendem Präfix aufrufe.

    Also meine gestrengen Lehrmeister der grenzenlosen objektorientierten und typkonformen Programmierung haben mir da eigentlich nachdrücklich beigebracht, solch zweifelhafte Konstrukte wie der Teufel das Weihwasser zu meiden.

    Leben kann ich damit zwar. Aber ich möchte doch gern sicherstellen, dass es nicht anders geht: ist das wirklich das letzte Wort in dieser Angelegenheit ?

    LG
    Peter
    @Peter329 In C# gibt es eine static class, deren VB-Pendant das Modul ist.
    Offensichtlich hast Du da eines der wenigen Gegenbeispiele erwischt.
    Mach Dir eine solche Klasse:

    VB.NET-Quellcode

    1. Public NotInheritable Class StaticClass
    2. Private Sub New()
    3. ' damit keine Instanz erstellt werden kann
    4. End Sub
    5. Public Shared Function CheckInterrupt() As Boolean
    6. ' ...
    7. End Function
    8. End Class
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Mich wundert es, dass noch keiner den BackgroundWorker erwähnt hat.
    Der ist exakt dafür gemacht:
    • nebenläufigen Code ausführen
    • cancel wird auch unterstützt (CancelAsync)
    Multithreading mit BackgroundWorker

    Das mit dem GetAsyncKeyState brauchst du wirklich nur dann, wenn du deine Anwendung steuern willst auch wenn sie nicht den Fokus hat.
    Das bedeutet auch, dass du die Escape taste gar nicht mehr in anderen Anwednungen benutzen kannst, ohne gleichzeitig deinen Code zu beenden.
    Bist du sicher, dass du das willst?
    @RFG

    Also, ich habe versucht das jetzt mal umzusetzen und habe die folgenden Anweisungen in mein Modul Modul1 ans Ende eingefügt.

    VB.NET-Quellcode

    1. Public NotInheritable Class StaticClass
    2. Private Sub New()
    3. ' damit keine Instanz erstellt werden kann
    4. End Sub
    5. Public Shared Function CheckInterrupt() As Boolean


    Damit erhalte ich den Compiler Error: Fehler BC30451 "GetAsyncKeyState" wurde nicht deklariert. Aufgrund der Schutzstufe ist unter Umständen kein Zugriff möglich.

    Iss ja auch klar .. den Namen kennt er nicht.

    Jetzt versuche ich die Deklaration hinzu zu fügen:

    VB.NET-Quellcode

    1. Public NotInheritable Class StaticClass
    2. <DllImport("user32.dll")>
    3. Public Shared Function GetAsyncKeyState(ByVal vKey As Keys) As Int16
    4. End Function
    5. Private Sub New()
    6. ' damit keine Instanz erstellt werden kann
    7. End Sub
    8. Public Shared Function CheckInterrupt() As Boolean


    Das liefert den Compiler Fehler: Fehler BC30002 Der Typ "DllImport" ist nicht definiert.

    Also irgendwie komme ich mit deinem Rat nicht klar.

    @Marcus.Obi

    Jau, möglicherweise gibt es noch andere Lösungsansätze. Aber ich möchte erst mal EINEN Ansatz zu verfolgen ... denn wenn man sich alle Nasen lang auf jede neue Idee einlässt, dann kommt man nie ans Ziel. Das ist nicht nur hier im VB so ... sondern ganz allgemein wenn man versucht Probleme zu lösen. Trotzdem vielen Dank, dass du dich mich meinem Problem befasst hast!

    LG
    Peter