Prevent a TreeView from firing events in VB6? - events

In some VB6 code, I have a handler for a TreeView's Collapse event:
Private Sub MyTree_Collapse(ByVal Node as MSComCtlLib.Node)
This is called whenever a node in the tree is collapsed, whether by the user or programmatically. As it turns out, through some roundabout execution, it may happen that this handler will wind up telling a node to collapse, leading to infinite recursion.
I can think of multiple ways to skin this cat, but what seems simplest to me is to tell the TreeView not to raise events for some period of time. I can't find a simple call to let me do this, though. Has anyone successfully done this, or do I need to keep track of state in some other manner so I can respond appropriately when recursive events come along?

#Phil - I came to the same conclusion. My implementation of MyTree_Collapse now looks something like this (where m_bHandlingCallback is a member variable):
Private Sub MyTree_Collapse(ByVal Node as MSComCtlLib.Node)
If m_bHandlingCallback Then Exit Sub
m_bHandlingCallback = True
DoSomeStuff
m_bHandlingCallback = False
End Sub

Another way in VB6 is to have an alternate WithEvents reference to the control:
Private WithEvents alt as TreeView
and in Form_Load:
Private Sub Form_Load()
Set alt = MyTree
End Sub
Now alt will receive lots of events like this:
Private Sub alt_Collapse(ByVal Node as MSComCtlLib.Node)
Set alt = Nothing
'DoSomeStuff'
Set alt = MyTree
End Sub
But, during the DoSomeStuff, the events are unhooked - which also applies to all other event Subs for alt without the need for them to have intrusive changes.

I think that like many events in VB, it can't be switched off.
Just set a boolean flag as you've suggested.

I would declare the flag variable as STATIC in the Sub. This avoids making the variable global and makes it keep its value between calls.

Related

How to delay the LostFocus Event in VB6

I'm having an issue with a process that involves the LostFocus event.
When the cursor loses focus from a particular textbox, I'm simply putting the focus back into that box.
My issue is removing focus long enough for the user to click a log out button. Is there a way to intercept the LostFocus event long enough to allow the user to click the log out button?
Obviously I don't know the big picture here. But keeping only with what you said, the following does the trick. Effectively the event is delayed briefly, allowing the button to be clicked:
Option Explicit
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Sub Text1_LostFocus()
Sleep 100
DoEvents
Text1.SetFocus
End Sub
With a combination of a Timer and another control that is outside of the borders of your form, you can achieve this.
Private Sub Text1_LostFocus()
Combo1.SetFocus
ReturnFocusTimer.Enabled = True
End Sub
Private Sub ReturnFocusTimer_Timer()
ReturnFocusTimer.Enabled = False
Text1.SetFocus
End Sub
In this example Combo1 is positioned beyond the bottom of the form. You can control the ReturnFocusTimer interval to however long you need.

How can I capture key ups/downs no matter what control on my form is the target?

I want to capture ctrl/alt/etc key ups and downs, no matter which control on my form gets the keyup or keydown event. Since I have about 100 controls on my form, it would be really ugly if I were to add code to each individual control. How can I accomplish this without having to do that?
PS: What's the difference between SetWindowsHook and SetWindowsHookEx?
You need to set the KeyPreview property of each Form to True. Subsequently, you can catch the keyboard events at the form level, in addition to the individual control level:
Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer)
Debug.Print "Form_KeyDown"
End Sub
Private Sub Form_KeyPress(KeyAscii As Integer)
Debug.Print "Form_KeyPress"
End Sub
Private Sub Form_KeyUp(KeyCode As Integer, Shift As Integer)
Debug.Print "Form_KeyUp"
End Sub
Essentially, the form gets a "preview" of each keyboard event before the control, e.g.
Form_KeyDown
Control_KeyDown
Form_KeyUp
Control_KeyUp
As for SetWindowsHook & SetWindowsHookEx, the former is the original Win16 API call, and the latter is the Win32 and Win64 API call. SetWindowsHook is deprecated, and isn't in the current MSDN library, as far as I know.

Powerpoint VBA App_SlideShowBegin

In order to use the SlideShowBegin event in Powerpoint, you have to have a Class Module configured the following way:
Public WithEvents App As Application
Private Sub App_SlideShowBegin(ByVal Wn As SlideShowWindow)
MsgBox "SlideShowBegin"
End Sub
Then, inside of a non-class module, you have to create an object of that type and set the App to Application.
Dim X As New Class1
Sub InitializeApp()
Set X.App = Application
End Sub
Now, the only issue I have is, if you don't manually called InitializeApp with the Macro Menu in Powerpoint, the events don't work. You have to call this sub before anything can called at the beginning of a slideshow INCLUDING this sub.
How can I go about calling this sub before running my powerpoint? Is there a better way to do this?
EDIT:
I've tried using Class_Initialize but it only gets called once it is first used or you make a statement like Dim X as Class1; X = new Class1
Usually event handlers are installed as part of an add-in, where you'd initialize the class in the Auto_Open subroutine, which always runs when the add-in loads. If you want to include an event handler in a single presentation, one way to cause it to init is to include a shape that, when moused over or clicked fires a macro, which inits your event handler and goes to the next slide.
Answering to an old question, but I hope my solution might helpt somebody ending up at this question.
The general advice for this issue is using a plug-in or placing some element on the slide and when that is clicked or hovered perform the initialization. Both are not always desired so I have the following approach:
In some module:
Dim slideShowRunning As Boolean
-----------------------------
Sub SlideShowBegin(ByVal Wn As SlideShowWindow)
' Your code for start-up
End Sub
-----------------------------
Public Sub OnSlideShowPageChange(ByVal Wn As SlideShowWindow)
If TypeName(slideShowRunning) = "Empty" Or slideShowRunning = False Then
slideShowRunning = True
SlideShowBegin Wn
End If
End Sub
----------------------------
Public Sub OnSlideShowTerminate(ByVal Wn As SlideShowWindow)
slideShowRunning = False
End Sub
For me this works perfectly. NOTE I am by no means a vba expert, actually I might have less than 50 hours of vba programming (maybe only 8 in powerpoint). So this might be an horrible solution. I don't know, but for me it works so I liked to share.
In fact, OnSlideShowPageChange runs when slideshow begins. Just make sure it doesn't work in the subsequent page changes if not needed using a global variable. See answer from C. Binair for details.

GUI freezing after datagridviews have been updated using backgroundworkers

I have made an application which runs three backgroundworkers simultaneously. It basically updates three datagridviews, which it is doing spot on. My problem is if I press maximizebox button or anywhere in any of the datagridview the program hangs for quite a long time. I am able to use the horizontal scroll but not vertical scrolls. I have tried Backgroundworker_runworkercompleted and it fires as required after threads have updated their respective datagridviews. Is it a normal behaviour or am i doing something wrong any suggestions would be helpful.
P.S: I have run the whole program using step method and their is no infinite loop in the code.
Thanks in advance
Jhon
Place this line of code just before when you are calling Backgroundworker1.RunWorkerAsync() to temporarily disable your datagridview scroll bars.
DataGridView1.ScrollBars = ScrollBars.None
Now Re-enable scroll bars of your datagridview by adding this line of code in BackgroundWorker1_RunWorkerCompleted event.
DataGridView1.ScrollBars = ScrollBars.Both
This will never make your application unresponding.
It sounds like you are still blocking the UI thread somehow. It may be helpful for you to post some code snippets. Also, what is the CPU utilization of your process? If the cpu usage is high, you may be starving the UI thread somehow.
Okay I have found the solution to my problem, while working out sequential elimination of the perceived trouble spots, I called my datagridview outside of the backgroundworker.dowork event and voila that solved the problem.
Moral of the story "NEVER UPDATE A DATA GRID VIEW FROM WITHIN THE BACKGROUNDWORKER THREAD" specially when you don't know what are you doing wrong :). I hope it will help someone in future.
You have to make sure that you are updating the datagridviews from the UI thread.
I had the same problem. When my dataGridViews were being updated from the UI thread, they worked fine. When I tried to add a series of rows to them from the backgroundWorker, they became unresponsive.
To correct this, I added the rows to a dataTable instead. When I kick off the backgroundWorker, I set the .datasource property of the dataGridView to nothing. When the worker completes, I set it back to the dataTable again. This both forces the dataGridView to update itself, and severs the synchronous connection between what it happening in the worker thread, and what is being displayed in the UI, which seems to alleviate the unresponsiveness when the worker completes.
Sample usage:
Private Sub button_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn1.Click
If bgWorker1.IsBusy <> True Then
dataGridView1.DataSource = Nothing
bgWorker1.RunWorkerAsync()
End If
End Sub
Private Sub bgWorker_DoWork(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bgWorker1.DoWork
dataTable1.Rows.Add("data")
End Sub
Private Sub bgWorker_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As RunWorkerCompletedEventArgs) Handles bgWorker1.RunWorkerCompleted
dataGridView1.DataSource = dataTable1
dataGridView1.Refresh()
End Sub
It's too late to answer to #Jhon but it seems like it might help other people.
In my case, I was using a BindingSource and the UI only froze if there where so many records that scrollbars had to appear.
When you use a background worker, task or thread to do the work, you have to update your UI controls on the main UI thread.
To do that you can Invoke a method in the corresponding thread.
A simple example:
myDataGridView.Invoke((MethodInvoker)delegate { myBindingSource.DataSource = myData; });
But i prefer this solution:
InvokeIfRequired

Allow vertical scrolling in listbox, when it is disabled (VB6)

I need to allow the vertical scrollbar in a multiselect listbox (VB6) however, when the control is disabled, I can't scroll.
I would think there is an API to allow this, but my favorite VB6 site (MVPS VB.NET) does not have a way.
I toyed with pretending it was disabled, and ignore the clicks... but to do that with VB6 code is really ugly... so if this is a solution, I need an API to ignore the clicks.
Thanks for your help.
I came up with the following code, which hides all of the gnarly details behind a class. Basically, I implemented greg's idea of using overlaying another scrollbar on top of the disabled list box's scrollbar. In my code, I dynamically create another ListBox control (resized so that only its scrollbar is visible), and use its scrollbar to scroll the actual ListBox. I also specifically avoided using the Windows API (except for the call to GetSystemMetrics that I used to figure how how wide a scroll bar is on the system). The nice thing about using another ListBox's scrollbar is that it will be themed properly (a ListBox uses the OS's theme when it displays it's scrollbar, but a VB.Scrollbar doesn't, so it would look out-of-place). Another advantage of using a second ListBox to scroll the first list box is that it's really easy to implement the scrolling logic (just set the first ListBox's TopIndex property to the second ListBox's TopIndex property whenever the second one is scrolled).
I also set it up to be as low-impact as possible (you only have to call a single function in your Form_Load event to make it work).
Usage
Add CustomScrollingSupport.cls and ListBoxExtras.bas to your project.
In your form's Form_Load event, add the following line:
AddCustomListBoxScrolling Me
This will make every VB.ListBox on the form support scrolling even while they are disabled. If you only want to add this functionality to a select number of ListBox's, you can call AddCustomScrollingSupport instead, passing in a specific ListBox control.
Interesting Note
In an older version of this code, I wasn't calling the ZOrder method on the second listbox (the one that provides the scrollbar) to make sure it would appear on top of the first listbox. This meant the second listbox was actually behind the first listbox; the interesting thing is that the scrolling on the second ListBox still worked when the first ListBox was disabled! Apparently, when the first ListBox is disabled, any mouse and keyboard events that would have gone to that ListBox "bleed through" to the second ListBox, so scrolling support still does work. I'm not sure if this is a bug or by design (I mean, you could argue that it makes sense that controls behind a disabled control would be able to receive events...). However, I found the scrolling to be slightly jerky at times, so I decided to add .ZOrder 0 to make the second listbox render on top of the first one. This has the drawback that you see the frame border for the second listbox (to the left of the scroll bar), which you wouldn't see if it was hidden behind the first listbox, but the scrolling is smoother.
CustomScrollingSupport.cls
This class wraps up the logic necessary to add "custom scrolling support" (for lack of a better name) to a VB.ListBox control. It should not be used directly, instead use the one of the Add* methods in the ListBoxExtras.bas module (I'll provide the code for that module later in the post).
Option Explicit
Private Declare Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
Private Const SM_CXVSCROLL = 2
Private Const SM_CXFRAME = 32
Private m_runningScrollers As Collection
Private WithEvents m_list As VB.listbox
Private WithEvents m_listScroller As VB.listbox
'--------------------------------------------------------------'
' Bind '
' '
' Adds custom scrolling support to a ListBox control. '
' Specifically, it allows the ListBox to be '
' scrolled even when it is disabled. '
' '
' Parameters: '
' '
' + list '
' the ListBox control to add custom scrolling support to '
' '
' + runningScrollers '
' a Collection of CustomScrollingSupport objects. Passed '
' in so that this object can remove itself from the list '
' when it is terminated. '
' '
'--------------------------------------------------------------'
Public Sub Bind(ByVal list As VB.listbox, runningScrollers As Collection)
Set m_list = list
Set m_runningScrollers = runningScrollers
'Create another ListBox loaded with the same number of entries as the real listbox'
Set m_listScroller = m_list.Container.Controls.Add("VB.ListBox", list.Name & "_scroller")
LoadScrollerList
Dim nScrollbarWidth As Long
nScrollbarWidth = GetSystemMetricScaled(SM_CXVSCROLL, m_list) + _
GetSystemMetricScaled(SM_CXFRAME, m_list)
'Display the other listbox (the "scroller"), just wide enough so that only its scrollbar is visible'
'and place it over the real listboxs scroll bar'
With m_listScroller
.Left = m_list.Left + m_list.Width - nScrollbarWidth
.Top = m_list.Top
.Height = m_list.Height
.Width = nScrollbarWidth
.Enabled = True
.Visible = True
.ZOrder 0
End With
End Sub
Private Sub m_listScroller_Scroll()
'If the master list has changed, need to reload scrollers list'
'(not ideal, but there is no ItemAdded event that we could use to keep the lists in sync)'
If m_list.ListCount <> m_listScroller.ListCount Then
LoadScrollerList
End If
'Make any scrolling done on the scroller listbox occur in the real listbox'
m_list.TopIndex = m_listScroller.TopIndex
End Sub
Private Sub Class_Terminate()
Dim scroller As CustomScrollingSupport
Dim nCurrIndex As Long
If m_runningScrollers Is Nothing Then
Exit Sub
End If
'Remove ourselves from the list of running scrollers'
For Each scroller In m_runningScrollers
nCurrIndex = nCurrIndex + 1
If scroller Is Me Then
m_runningScrollers.Remove nCurrIndex
Debug.Print m_runningScrollers.Count & " scrollers are running"
Exit Sub
End If
Next
End Sub
Private Sub LoadScrollerList()
Dim i As Long
m_listScroller.Clear
For i = 1 To m_list.ListCount
m_listScroller.AddItem ""
Next
End Sub
Private Function GetSystemMetricScaled(ByVal nIndex As Long, ByVal ctrl As Control)
GetSystemMetricScaled = ctrl.Container.ScaleX(GetSystemMetrics(nIndex), vbPixels, ctrl.Container.ScaleMode)
End Function
ListBoxExtras.bas
This module contains two utility methods:
AddCustomScrollingSupport adds custom scrolling functionality
to an individual VB.ListBox
control
AddCustomListBoxScrolling adds custom scrolling
functionality to every VB.ListBox
control on a given Form
Option Explicit
Public Sub AddCustomScrollingSupport(ByVal list As VB.listbox)
Static runningScrollers As New Collection
Dim newScroller As CustomScrollingSupport
Set newScroller = New CustomScrollingSupport
runningScrollers.Add newScroller
newScroller.Bind list, runningScrollers
End Sub
Public Sub AddCustomListBoxScrolling(ByVal frm As Form)
Dim ctrl As Control
For Each ctrl In frm.Controls
If TypeOf ctrl Is VB.listbox Then
AddCustomScrollingSupport ctrl
End If
Next
End Sub
Rather than looking for the API to ignore clicks, can't you just ignore the events? (i.e. just don't do when the user clicks/selects something).
And, I think there is a SelectionMode property to disable multiselect and make it single select.
If you don't want the user to be able to select anything at all, you can try hooking into the SelectionIndexChanged event and set the SelectionIndex to -1.
My VB6 is a bit rustly, so sorry if the event/property name don't match exactly.
Speaking of hacks, what if you enable the scrollbar when the mouse is moving over the scroll bar?
Or maybe ... place another scroll bar over the ListBox's SB, and use APIs to scoll the disabled LB.
Enabling just the scrollbar on a disabled listbox is possible (I think), but you'd have to dig into the Windows API and do SendMessage and some other grisly stuff.
I just checked it out, and when a listbox is disabled you can still scroll it up and down programatically by changing the control's ListIndex property. So you could do something like what greg suggests, and "float" an enabled vertical scroll bar over the one on the list box, and use this scrollbar's Value_Changed event (I think that's what it's called) to change the listbox's ListIndex property.
This is a total VB hack, but I think you can leave the listbox enabled, and then drag a transparent label (with blank text) over all of the listbox except the scrollbar. The label will intercept any mouse clicks (although this won't affect keystrokes).
This will only work if the label is transparent like I remember (it may be the image control - without a loaded image - that works like this in VB).

Resources