How to Stop A Form From Behaving Modally When Using Specific Controls

vb6 Ethiopia
  • 9 years ago

     

    In an application written in VB6 (SP6), when a user uses the elevator (scrollbar) control in a combo dropdown, the application hangs.  The form on which the control appears behaves modally.

     

    This occurs in non-modal forms.

     

    This happens as long as the user holds the mouse down on the elevator.  As soon as the user releases the mouse, the application resumes.

     

    Is there any secret API call or control property which circumvents this annoying and application-killing behavior?

     

    Please advise.  Thank you.

     

  • 9 years ago

    I experimented with VB2008, so not sure if this helps you in VB6.

    I found that code in a loop with DoEvents() in Form1 is stopped as soon as the user starts moving the scroll bar in a drop down Combo in Form2. I guess this is the Modal behaviour that bothered you.

    However, if you have a Timer object in Form1, this continues to fire while the user scrolls up and down the combo without lifting the mouse button.

    The code in Form1 is below. Form2 has no code, just has a combo box with a long list to provide the scrolling behaviour. The debug output shows ticks from the Do..Loop in Button1_Click and from Timer1 events, until the user starts sliding up and down the combo drop-down. Then the loop in Button1_Click stops, while the Timer1 ticks continue to fire. When the user releases the slider, the Do..Loop resumes.

    Hope that helps.

    Public Class Form1
    
        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
            Form2.Show()
            Dim tt As String = "", tt2 As String = ""
            Dim count As Long = 0
    
            Do
    
                tt2 = Format(Now(), "hhmmss")
                If tt2 <> tt Then
                    count = count + 1
                    Debug.Print("Loop: " & count & " at " & tt2)
                    tt = tt2
                End If
                Application.DoEvents()
            Loop
        End Sub
    
        Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
            Static tt As String = ""
            Dim tt2 As String = ""
            Static count As Long = 0
    
            tt2 = Format(Now(), "hhmmss")
            If tt2 <> tt Then
                count = count + 1
                Debug.Print("Timer: " & count & " at " & tt2)
                tt = tt2
            End If
        End Sub
    End Class
    
  • 9 years ago

    Thank you for taking the time to look at this.

     I agree completely with your analysis.

     Using a timer to execute code in case a user clicks on a interfering control may be a workaround in some cases.  Unfortunately, in our case it is not, primarily because the timer resolution is too coarse.

    I guess two questions remain: 1) What is Microsoft's rationale for implementing this behavior (they may not be aware it exists)? 2) Since not all controls exhibit this behavior, what is the setting/property/method workaround?

     Thank you again for looking at this.
     

  • 9 years ago

    Actually, the behaviour seems to be consistent - that is, this happens whenever there is an action by the user that involves click-drag with the mouse. It happens with the scroll bar of a list box and with moving or resizing a form as well as with the combo drop-down.

    I think the basic reason is that VB is single-threaded. Whenever you want to get apparent mutithreaded behaviour, you have to use DoEvents() to pass the execution thread elsewhere, but this is just a procedure call and always stops the current stream of execution in its tracks - the multi-thread illusion relies on control coming back pretty quickly. In the situation that you have, as soon as the user clicks and drags, this probably starts an implicit loop with a DoEvents() call within it. That means that any new events can be handled (i.e the Timer ticks), but anything that was running and issued the DoEvents() call at the time of the mouse click remains blocked until the mouse button is released, becuase it is higher up the call stack.

    Are you sure the timer tick firing is not fast enough? Setting the interval to 1 generates around 100 events per second on my laptop and probably much more on a fast machine.

    I have implemented some simulated real-time multithreaded applications in VB in the past and for these I have had to use a single Timer as the driving mechanism and subdivide my logic into small parcels that placed themselves in a queue waiting patiently for their turn to be called from the timer. This was to get round the problem of the blocks that arise from DoEvents().

    However, if you really want to implement a multithreaded application, you would probably do better to use a language that supports threads. :¬(

  • 9 years ago

    Again, I agree almost entirely with everything you've written.  It's great to be involved in a conversation with somebody who understands these types of problems.

    For our application, we need a resolution of no greater than one-tenth that the typical VB timer control provides.  Also, when one does some research, the timer control interval actually varies from machine to machine (the same type of machine delivers the same interval, but a Dell M90 is different from an HP DC7700, for example).

    Our application uses the Windows QueryProgramCounter API for timing.  It runs in a polling mode and does not use timer "interrupts".  In this way we can get the resolution we need in Windows.  And just as an editorial comment, yes, it is possible to have such a high-performance application run successfully in VB, but it requires a higher than normal level of consciousness.

    Your comment about threading is right on.  We have a C++ version of the application which runs under QNX.  As you suppose, this problem does not exist there.

    However, I am uncertain that your theory about the single-threading explains the cause.  One does not see this behavior, for example, if one holds the mouse down on a VB command button.

    Thank you again for your involvement and support.
     

     

  • 9 years ago

    Well, that's mighty civil of you. I just happen to have mucked around with computers for a while now and have picked up a few things. 

    Yes, I agree you cannot rely on the interval on a timer control as firing accurately, the only way to measure true elapsed time is, as you say, to get at the system clock. I used the Timer function in VB which returns milliseconds - I guess the QueryProgramCounter API may give you a finer resolution?

    I am pretty sure that single threading is actually the cause of your problem. I think, when you hold down a command button, there is no "drag" element to the interaction so it doesn't set up a loop, so that's why it doesn't cause a problem. But in a single-threaded system, any loop by definition holds up any other pre-exisitng loop, because there is only one call stack.

    I can demonstrate the single-threading nature of VB, by adapting the previous code as in the sample below. In the adaptation the application has a single form with just two buttons. When the button1 is pressed, the code enters a loop that only ends when you click on button2 - the Button1 loop waits for a semaphore set by Button2. The Button1 loop must contain DoEvents() to allow the Button2 event to register.

    Now, here comes the interesting bit. As there is a DoEvents() call in the Button1 loop, what happens if instead of pressing Button2, you press on Button1 again? Well, that event is processed just like any other event, so it starts a completely new Button1 loop, also waiting for the flag to be set by Button2. Effectively, the DoEvents() call in the Button1 code sends the executing thread recursively back into the same Button1 code. You can press Button1 as many times as you like, and each time you get another recursive call and a new loop in the stack. None of the earlier loops in the stack can continue until all later loops have exited. So now, when you press on Button2, the final loop exits back to the previous loop, which also exits (because the flag was set by Button 2). The whole call stack unwinds in a rush, LIFO style. You could modify it to unwind just one loop at a time by resetting the semaphore at the exit point from the Button1 code.

    Public Class Form1
        Private bStopLoop As Boolean = False
        Private iLoopCount As Long = 0
    
    
        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    
            Dim tt As String = "", tt2 As String = ""
            Dim count As Long = 0
            bStopLoop = False
            iLoopCount = iLoopCount + 1
    
            Dim MyLoopNumber As Long = iLoopCount
    
            Do
    
                tt2 = Format(Now(), "hhmmss")
                If tt2 <> tt Then
                    count = count + 1
                    Debug.Print("Loop " & MyLoopNumber & ": " & count & " at " & tt2)
                    tt = tt2
    
                End If
                Application.DoEvents()
    
            Loop Until bStopLoop = True
    
            Debug.Print("Loop " & MyLoopNumber & " ended: " & count)
        End Sub
    
        Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
            'Stop loop button
            bStopLoop = True
        End Sub
    End Class
    

     

Post a reply

Enter your message below

Sign in or Join us (it's free).

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“To iterate is human, to recurse divine” - L. Peter Deutsch