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).
Related
I'm making a program in vb6 that requires me to be able to scroll a certain area of the screen to the right. It consists solely of lines and picture boxes, is there a way to only scroll that area? Thanks!
You can take advantage from the fact that some visual controls can act as container of other visual controls.
Just an example:
In the VBIDE, place a Frame over a VB Form. Then - inside this Frame place a PictureBox. Pay attention that the PictureBox shall be fully contained inside this Frame.
Now, if you drag the Frame around the Form, you will see that the PictureBox inside is moving together, while keeping the position inside the container Frame, i.e. it will keep the original top & left coordinates relative to the container control.
To find out which visual controls have this capability, simply retry the test. You will see, for example, that a Label can't act as a container.
That said, you need following:
one visual control (preferably a PictureBox) which act as container (the viewport)
one visual control (preferably a PictureBox) which act as scrollable area
a HScrollBar (and, optionally, a VScrollBar) to scroll the
view-able area
Now, inside the second PictureBox (the scrollable area) you can place your controls, the Lines and PictureBoxes you mentioned in your question.
Why is a PictureBox preferable? Because you can profit from the ScaleMode property, set it to 3-Pixeland use pixel-exact scrolling. With Frames you can't do that, you are limited to just Twips.
By using a contained control you have two advantages:
you can visually place and reposition the controls ypu need inside the IDE
you need to scroll just only one control - all other hosted controls will
move together
The boring thing you must code is the synchronization of the container with the ScrollBars.
But luckily, as VB6 has been going a long way, you will find enough cut-and-paste code examples of such a task, one of which is on VBForums: Scroll bar in picturebox
Some final notes:
PictureBoxes in VB6 are constrained to a maximum size of 16,383 x 16,383 pixels. If your scrollable area should be bigger, you may implement a kind of custom "infinite scroller", and manage the position of your controls by grouping them, and you will need some additional coding.
ScrollBars in VB6 can range from a minimum value of -32,768 to a maximum value of 32,767. If you need more, you will end up with some other additional coding tasks.
If you stick to Twips, you can have a bigger logical area available - for example: until 245,745 with the typical 15 TwipsPerPixel - but you can't use such a big value with ScrollBars.
Here is a simple example illustrating what you requested. The key is that the scrollable area must be a container control hosting the controls you wish to scroll.
Option Explicit
Private oldPos As Integer
Private Sub Form_Load()
HScroll1.Min = 0
HScroll1.Max = 1000
HScroll1.SmallChange = Screen.TwipsPerPixelX * 10
HScroll1.LargeChange = HScroll1.SmallChange
End Sub
Private Sub HScroll1_Change()
ScrollPictureBox
End Sub
Private Sub HScroll1_Scroll()
ScrollPictureBox
End Sub
Private Sub ScrollPictureBox()
Dim c As Control
For Each c In Me.Controls
If c.Container.Name = "Picture1" And Not TypeOf c Is HScrollBar Then
c.Left = c.Left - (oldPos - HScroll1.Value)
End If
Next
oldPos = HScroll1.Value
End Sub
In this code, Picture1 is a PictureBox (the scrollable area) containing HScroll1 (a horizontal scrollbar) and the other controls you wish to scroll.
How I can get access to the nested controls of a control?
I have serveral frames on my user interface and every frame contains serveral other controls (like labels, buttons, ...). I have to iterate over the frames and change content of the children of a specific frame (e.g. set another text in a label).
So far I iterate over all controls of my frame and I check if the control in the loop control variable is the frame where the changes should be.
Dim cntrl As Control
For Each cntrl In Controls
'Debug.Print cntrl.Name // here I get all controls on the form
If cntrl.Name = "Frame_Name" Then
If cntrl.Index = index Then
Debug.Print "true" ' here the caption of nested components should be changed
End If
End If
Next
Now I have the frame in the control variable but the problem is that I get no access to the nested label to change the label's caption. What can I do?
You need to look at the Container property of each control. The following code should give you the idea:
Dim cntrl As Control
For Each cntrl In Controls
If cntrl.Container.Name = "Frame_Name" Then
Debug.Print cntrl.Name & " is nested in the specified frame"
End If
Next
I'm trying to get a button to change from the "Arrow Left" to "Arrow Right" picture when I click it, but I'm not sure how to assign the images through VBA. I tried Me!btnCollapseUnscheduled.Picture = "Arrow Left", but I get an error message this way.
In Access 2010 or later, you can store images in the MSysResources system table. Then choose Shared for your command button's Picture Type property, and select one of those shared images.
Afterward, it's easy to toggle between the shared images from the command buttons's click event. I named my command button "cmdArrow" and the shared images "Arrow Left" and "Arrow Right" ...
Option Compare Database
Option Explicit
Private Sub cmdArrow_Click()
Dim strDirection As String
If Me!cmdArrow.Picture Like "*Left" Then
strDirection = "Right"
Else
strDirection = "Left"
End If
Me!cmdArrow.Picture = "Arrow " & strDirection
Me!txtImage.Value = Me!cmdArrow.Picture
End Sub
Private Sub Form_Load()
Me!txtImage.Value = Me!cmdArrow.Picture
End Sub
Screenshots:
The biggest challenge was loading the images into MSysResources. I created a throwaway form and added a command button. Next I chose Embedded as the button's Picture Type property and selected Arrow Left from the available choices. Then I changed Picture Type from Embedded to Shared, which added a row to MSysResources with the image data in an attachment field and my command button's name in the Name field. So I opened the table, changed Name to "Arrow Left" and closed the table. Note MSysResources will not be visible in the Navigation pane unless you enable "Show System Objects" in the Navigation Options.
I repeated those steps for "Arrow Right". It may sound fiddly, but it's not really a huge hurdle.
The trick is to use the full file path of your icon.
MS-Access might be using some icon map for its native icons, so you might not have much luck getting a path to one of those.
If you definitely can't find the path to the native MS-Access icons, you could always just do a google image search for arrow icons (specify exact size of 16 x 16 pixels).
You can then just save these icons to a folder of your choosing and then use those paths in the following illustrations.
If you just want the button to change its image on the first click and stay that way thereafter:
Make sure the button does not have a picture attached in Design View.
Assign the default image using its full path to your command button using the form's Open event:
Private Sub Form_Open(Cancel As Integer)
Me.cmdButton.Picture = "C:\myFolder\myDefaultIcon.png"
End Sub
Then in the button's click event, assign the .picture property to the icon path you want to change it to:
Private Sub cmdButton_Click()
Me.cmdButton.Picture = "C:\myFolder\myChangedIcon.png"
End Sub
If you just want the button to toggle between 2 images on each click:
Use a toggle button control instead of a command button control
Make sure the button does not have a picture attached in Design View.
Write a sub that will handle the different on/off states of your toggle button:
Public Sub togImg()
If _
Me.togButton = -1 _
Then
Me.togButton.Picture = "C:\myFolder\myChangedIcon.png"
Else
Me.togButton.Picture = "C:\myFolder\myDefaultIcon.png"
End If
End Sub
Call your sub from both the form's Open event and the toggle button's Click event:
Private Sub Form_Open(Cancel As Integer)
togImg
End Sub
Private Sub togButton_Click()
togImg
End Sub
You could create two buttons where only one is visible at a time, both calling the same code.
When one is clicked, unhide the other, set focus to this, then hide the first button. And vice-versa.
I'm facing a weird case in VB6, which is, when I show a form in respect with another OwnerForm, if I did this two/three times for example, when I close all the child forms, suddenly the OwnerForm disappear on background (lose focus and the previous application will be on top), and I will have to click on it on the task bar to show the form again!
A quick sample will be something like that:
Private Sub Command1_Click()
Command1.Enabled = False
Dim frm As New Form1
frm.Show 0, Form1
End Sub
Is there any solution for this?
Thanks.
You're showing the child form on top non-modally, ie, you can have any number of child forms of that type open. When you close them, sometimes the form / app that spawned them will lose focus and disappear behind other applications - as you are experiencing.
You might be able to hide the form being closed (use the _QueryUnload event and cancel the unload), hide, then set the ZOrder on the parent form (to bring it to the front), then carry on unloading the form that is being closed.
Let me know how you get on.
Is there a method such that a user can click on the form itself, and in doing so remove focus from whatever object (textbox, combobox, etc) currently has it? Basically, can focus be uniformly removed from everything at once?
Setting the focus to the form itself does not work.
I thought about doing the old "hide a placeholder button behind another object" trick, but I'm really not a fan of that.
Thanks!
In VB6 a PictureBox can get focus, even if it does not contain any control.
In your case you can put a PictureBox with TabStop false, BorderStyle set to 0, TabIndex set to 0 behind every other control but not containing any focusable control and stretch it to ScaleWidth by ScaleHeight at run-time.
You have to put the labels and any windowless control in this background PictureBox too.
This way when the user clicks "on the form" the focus will "go away". With "no focus" Tab key will focus first control (the one with TabIndex set to 1).
When a form is active, something generally HAS to have focus. It sounds like you're just wanting to not "show" that a particular control has focus.
If that's the case, it's going to depend on the controls. Some have properties that control whether or not the specific control "indicates" its focus in some way.
But the built in Windows controls will always show their focus state unless you subclass them
Given this problem. I'd probably put a button on the form , then move it offscreen when the form loads. Make sure it's not a tab stop, but then when you want to hide focus, set focus specifically to the button, make sure the button is STILL in the tab order, even though it's not a tab stop, so the user can press tab while on the button and end up somewhere logical.
Don't have VB handy, but could you simply remove TabStop?
for x = 1 to me.Controls.count
me.Controls(x).TabStop = 0
next
I have a picturebox and a control on a form.
Private Sub cmdButton_Click
PictureBox.setFocus
Exit sub
End sub
The control doesn't change its appearance, nor does the picturebox.
Of course you'll need to add an If-Then clause if you want the control to respond normally sometimes.