Being a self-taught VBA programmer I usually search the internet until I find a satisfactory solution for the problems (and the limitations of VBA) I stumble upon. I do not simply copy code of others, I really try to understand it as well, so I can learn from it. Long story short: I used code I found for dragging data from one listbox to another. Originally, the code (for 2 listboxes) was simply put in the form's code module but I wanted to use it in a class module so I don't have to copy/paste the same code for each and every d&d listbox I use on a form. The code I use:
(Code module of the form; just the listbox part)
Option Explicit
Private collection_ListBox As New collection
Private collection_ComboBox As New collection
Private collection_Textbox As New collection
Private Sub UserForm_Initialize()
Dim frm_control As Control
Set collection_ListBox = New collection
Set collection_ComboBox = New collection
Set collection_Textbox = New collection
For Each frm_control In Me.Controls
Select Case TypeName(frm_control)
Case "ListBox"
Dim obj As CfrwxDragDropList: Set obj = New CfrwxDragDropList
Set obj.FRWX_Control = frm_control: obj.Initialize
collection_ListBox.Add obj
Case "ComboBox"
Case "TextBox"
End Select
Next frm_control
'***TEMP for testing purposes***
ListBox1.List = Array("Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7")
End Sub
(Code module of the class)
Option Explicit
Private WithEvents FRWX_DragDrop As msforms.ListBox
Private Item_Source As msforms.ListBox
Public Property Get FRWX_Control() As msforms.ListBox
Set FRWX_Control = FRWX_DragDrop
End Property
Public Property Set FRWX_Control(reg_Control As msforms.ListBox)
Set FRWX_DragDrop = reg_Control
End Property
Public Sub Initialize()
'Nothing here yet!
End Sub
Private Sub FRWX_DragDrop_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, _
ByVal X As Single, ByVal Y As Single)
If FRWX_Control.ListIndex < 0 Then Exit Sub
If Button = 1 Then
Call SetDraggedItem(FRWX_Control)
End If
End Sub
Private Sub FRWX_DragDrop_BeforeDragOver(ByVal Cancel As msforms.ReturnBoolean, _
ByVal Data As msforms.DataObject, ByVal X As Single, ByVal Y As Single, _
ByVal DragState As Long, ByVal Effect As msforms.ReturnEffect, _
ByVal Shift As Integer)
Cancel = True
Effect = fmDropEffectMove
End Sub
Private Sub FRWX_DragDrop_BeforeDropOrPaste(ByVal Cancel As msforms.ReturnBoolean, _
ByVal Action As msforms.fmAction, _
ByVal Data As msforms.DataObject, ByVal X As Single, ByVal Y As Single, _
ByVal Effect As msforms.ReturnEffect, ByVal Shift As Integer)
Cancel = True
Effect = fmDropEffectMove
Call AddDroppedItem(FRWX_Control, Data, Y)
Call DeleteDraggedItem(Item_Source)
End Sub
Private Sub SetDraggedItem(lb As msforms.ListBox)
Set Item_Source = lb
Dim dataObj As New DataObject
dataObj.SetText lb.Text
Call dataObj.StartDrag(fmDropEffectMove)
End Sub
Private Sub AddDroppedItem(lb As msforms.ListBox, dataObj As DataObject, Y As Single)
lb.AddItem dataObj.GetText, FixDropIndex(lb, Y)
End Sub
Private Sub DeleteDraggedItem(lb As msforms.ListBox)
Dim selIndex As Long
With lb
selIndex = .ListIndex
.Selected(selIndex) = False
.RemoveItem selIndex
End With
Set Item_Source = Nothing
End Sub
Private Function FixDropIndex(lb As msforms.ListBox, Y As Single) As Long
Dim toIndex As Long
With lb
toIndex = .TopIndex + Int(Y * 0.85 / .Font.Size)
If toIndex < 0 Then toIndex = 0
If toIndex >= .ListCount Then toIndex = .ListCount
End With
FixDropIndex = toIndex
End Function
So far so good; everything works fine except for one tiny little thing: I get an error on
Call DeleteDraggedItem(Item_Source)
in the sub FRWX_DragDrop_BeforeDropOrPaste. I know WHY I get this error: when I drop the DataObject in ListBox2, Item_Source in the corresponding instance of the class will be empty for it was filled in the ListBox1 instance of the class. So I need a way to let ListBox2 know the source of the dropped text. I can think of 2 ways of fixing this.
The first one sends shivers down my spine only thinking of it, for it feels almost like blasphemy: I could send it with the DataObject along with the text itself by 'expanding' lb.Text with "|" followed by lb.Name and split the string in the receiving instance. It will work, but I don't like this kind of solutions.
I could pass the name of the source ListBox to the parent (being the form itself) from instance 1, so instance 2 can ask for it there. I haven't tried that solution yet, but I'm sure I'll get it to work.
So now for my questions:
1. is solution 2 a/the right way to go?
2. are there other/better solutions I haven't thought of yet?
Any help will be highly appreciated!
***** UPDATE *****
As mentioned below, I found another (I think better) fix myself. The events are still triggered from the class instances for each listbox, but I used a separate, single instance of a class that performs the actual actions attached to them. Here's the updated code:
(Code module of the form; just the listbox part)
Option Explicit
Private collection_ListBox As New collection
Private collection_ComboBox As New collection
Private collection_Textbox As New collection
Private Sub UserForm_Initialize()
Dim frm_control As Control
Set collection_ListBox = New collection
Set collection_ComboBox = New collection
Set collection_Textbox = New collection
Dim handler As CfrwxDragDropList_EventHandler: Set handler = New CfrwxDragDropList_EventHandler
For Each frm_control In Me.Controls
Select Case TypeName(frm_control)
Case "ListBox"
Dim obj As CfrwxDragDropList: Set obj = New CfrwxDragDropList
Set obj.FRWX_Control = frm_control: obj.Initialize
Set obj.FRWX_EventHandler = handler
collection_ListBox.Add obj
Case "ComboBox"
Case "TextBox"
End Select
Next frm_control
'***TEMP for testing purposes***
ListBox1.List = Array("Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7")
End Sub
(Code module of the listbox class "CfrwxDragDropList"
Option Explicit
Private WithEvents FRWX_DragDrop As MSForms.ListBox
Private FRWX_DragDrop_Handler As CfrwxDragDropList_EventHandler
Private Item_Source As MSForms.ListBox
Public Property Get FRWX_Control() As MSForms.ListBox
Set FRWX_Control = FRWX_DragDrop
End Property
Public Property Set FRWX_Control(reg_Control As MSForms.ListBox)
Set FRWX_DragDrop = reg_Control
End Property
Public Property Get FRWX_EventHandler() As CfrwxDragDropList_EventHandler
Set FRWX_EventHandler = FRWX_DragDrop_Handler
End Property
Public Property Set FRWX_EventHandler(handler As CfrwxDragDropList_EventHandler)
Set FRWX_DragDrop_Handler = handler
End Property
Public Sub Initialize()
'Nothing here yet!
End Sub
Private Sub FRWX_DragDrop_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, _
ByVal X As Single, ByVal Y As Single)
If FRWX_Control.ListIndex < 0 Then Exit Sub
If Button = 1 Then
Call FRWX_DragDrop_Handler.SetDraggedItem(FRWX_Control)
End If
End Sub
Private Sub FRWX_DragDrop_BeforeDragOver(ByVal Cancel As MSForms.ReturnBoolean, _
ByVal Data As MSForms.DataObject, ByVal X As Single, ByVal Y As Single, _
ByVal DragState As Long, ByVal Effect As MSForms.ReturnEffect, _
ByVal Shift As Integer)
Cancel = True
Effect = fmDropEffectMove
End Sub
Private Sub FRWX_DragDrop_BeforeDropOrPaste(ByVal Cancel As MSForms.ReturnBoolean, _
ByVal Action As MSForms.fmAction, _
ByVal Data As MSForms.DataObject, ByVal X As Single, ByVal Y As Single, _
ByVal Effect As MSForms.ReturnEffect, ByVal Shift As Integer)
Cancel = True
Effect = fmDropEffectMove
Call FRWX_DragDrop_Handler.AddDroppedItem(FRWX_Control, Data, Y)
Call FRWX_DragDrop_Handler.DeleteDraggedItem
End Sub
(Code module of the eventhandler class "CfrwxDragDropList_EventHandler"
Option Explicit
Private Item_Source As MSForms.ListBox
Public Sub SetDraggedItem(lb As MSForms.ListBox)
Set Item_Source = lb
Dim dataObj As New DataObject
dataObj.SetText lb.Text
Call dataObj.StartDrag(fmDropEffectMove)
End Sub
Public Sub AddDroppedItem(lb As MSForms.ListBox, dataObj As DataObject, Y As Single)
lb.AddItem dataObj.GetText, FixDropIndex(lb, Y)
End Sub
Public Sub DeleteDraggedItem()
Dim selIndex As Long
With Item_Source
selIndex = .ListIndex
.Selected(selIndex) = False
.RemoveItem selIndex
End With
Set Item_Source = Nothing
End Sub
Private Function FixDropIndex(lb As MSForms.ListBox, Y As Single) As Long
Dim toIndex As Long
With lb
toIndex = .TopIndex + Int(Y * 0.85 / .Font.Size)
If toIndex < 0 Then toIndex = 0
If toIndex >= .ListCount Then toIndex = .ListCount
End With
FixDropIndex = toIndex
End Function
That's it! It works between 2 listboxes, but if you want to use more it'll work as well. You can move items between listboxes, but also change the order of items within a listbox.
Related
I have a collection of Toggle Buttons along with the object name of the toggle button, as is shown in the watch screen shot
I have tried to reference the sName for the second item in the collection with this line itbcollection.Item(2).sName but it gives me Object does not support this property or method error. What is the appropriate code to get to the sName?
There is a class that is used to capture Mouse Down events, assigned to several ToggleButtons.
Option Explicit
Private WithEvents cTB As MSForms.ToggleButton
Private sName As String
Public Property Let aTB(iTB As ToggleButton)
Set cTB = iTB
sName = cTB.Name
End Property
Private Sub cTB_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal x As Single, ByVal Y As Single)
If Button = 1 Then Exit Sub
Module1.RightClickDay cTB.Caption
End Sub
and at form load a loop finds all ToggleButtons and loads the collection
Dim tbEvent As cObjectArray
Dim TB(42) As Object
x = 0
For Each o In UFShows.Controls
If o.Tag = "T" Then
Set TB(x) = o
x = x + 1
o.Visible = False
Set tbEvent = New cObjectArray
tbEvent.aTB = o
iTBcollection.Add tbEvent
End If
Next o
Please, use the next adapted class code and name it "TGButClass":
Option Explicit
Public WithEvents cTB As MSForms.ToggleButton 'if not Public, it will not be exposed!
Public sName As String 'if not Public, it will not be exposed!
Public Property Set aTB(iTB As MSForms.ToggleButton)'you need Set here (nothing to be extracted, you need to Set)
'and As TooggleButtin means a sheet control
Set cTB = iTB
sName = cTB.Name
End Property
Private Sub cTB_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal x As Single, ByVal Y As Single)
If Button = 1 Then Exit Sub
Module1.RightClickDay cTB.Caption
End Sub
A testing called sub RightClickDay should look like:
Sub RightClickDay(strName As String)
MsgBox strName
End Sub
Then, in the form code module you should paste the next declarations and UserForm_Initializeevent:
Option Explicit
Private objMyEventClass As New Collection
Public tbEvents As TGButClass
Private Sub UserForm_Initialize()
Dim o As Control, x As Long
For Each o In Me.Controls
If o.Tag = "T" Then
x = x + 1
Set tbEvents = New TGButClass
Set tbEvents.aTB = o
objMyEventClass.Add tbEvents
Debug.Print objMyEventClass(x).sName 'to see the (now) exposed sName
End If
Next o
End Sub
Please, send some feedback after testing it.
If something not clear enough, do not hesitate to ask for clarifications.
A similar result can be obtained using an array instead of a collection, too.
I have this UserForm (Image 1) and I'm trying to apply some customization through Class Module. So, my first goal was to modify the label format when it was clicked (Image 2). So far so good, I've accomplished this through the Class Module "cLabels". Now, my second goal is (this is the one I'm stuck) to apply some other color to the aforementioned Label. The point is, I don't know how to accomplish this.
I tried to create other class module called "cUserForm", but I don't how to pass the label modified to the cUserForm Class Module and use its MouseMove Event. I know I could apply the modification through the standard UserForm Module using the MouseMove Event, but the thing is, I don't want any code like that in my UserForm Module, I want the class module doing the "dirty" work. Do guys have any ideas how can I circumvent the problem?
Additional information (but not important to solve the problem): My final goal is to make "Buttons" like this https://drive.google.com/file/d/1ev_LNgxPqjMv0dtzlF7GSo7SOq0wDbR2/view?usp=sharing with some effects such as MouseHover, TabPress and so on. VBA buttons are very ugly. Just for the record, I've already done all this in a standard UserForm module (If anyone wants the workbook to see what I'm talking about, I have it), but the final result was just a mess, so many code (and It was just the code to modify the appearance of the UserForm, imagine when I put some code to do certain action, omg).
Image 1
Image 2
Here is what I have so far:
UserForm Module
Option Explicit
Private ObjLabel As cLabels
Private ObjUserForm As cUserForm
Private Sub UserForm_Initialize()
Set ObjLabel = New cLabels
ObjLabel.CallClasse Me
Set ObjUserForm = New cUserForm
Set ObjUserForm.UserFormValue = Me
End Sub
cLabels
Option Explicit
'## Events/Variables/Collections
Private WithEvents clsLabel As MSForms.Label
Private ClasseObject As cLabels
Private LabelCollection As New Collection
'## Properties
Public Property Get ActiveLabel() As MSForms.Label
Set ActiveLabel = clsLabel
End Property
Public Property Set ActiveLabel(Value As MSForms.Label)
Set clsLabel = Value
End Property
'## Procedures/Methods
Private Sub clsLabel_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
LabelHovered
End Sub
Public Sub CallClasse(MainObject As MSForms.UserForm)
Dim ctrl As MSForms.Control
For Each ctrl In MainObject.Controls
If TypeOf ctrl Is MSForms.Label Then
Set ClasseObject = New cLabels
Set ClasseObject.ActiveLabel = ctrl
LabelCollection.Add ClasseObject
End If
Next ctrl
End Sub
Private Sub LabelHovered()
ActiveLabel.BackColor = vbYellow
End Sub
cUserForm
Option Explicit
'## Events/Variables/Collections
Private WithEvents clsUserForm As MSForms.UserForm
Private mActiveLabel As MSForms.Label
Private ObjLabel As New cLabels
'## Properties
Public Property Get UserFormValue() As MSForms.UserForm
Set UserFormValue = clsUserForm
End Property
Public Property Set UserFormValue(Value As MSForms.UserForm)
Set clsUserForm = Value
End Property
Public Property Get ActiveLabel() As MSForms.Label
Set ActiveLabel = mActiveLabel
End Property
Public Property Set ActiveLabel(Value As MSForms.Label)
Set mActiveLabel = Value
End Property
'## Procedures
Private Sub clsUserForm_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
'MsgBox ObjLabel.ActiveLabel.BackColor 'Got an error
End Sub
Workbook:
https://drive.google.com/file/d/1cLG4pLmC-jDaysjd_dK0EFuJ_LqYqJ-u/view?usp=sharing
I found your question very interesting and I've got a bit of a different, more object oriented take on how you might do this. I tried implementing an Observer Pattern to get the described effect. (As a side note, normally I would generalize a solution a bit more using Interfaces, but for this quick demo, I will show a couple of tightly coupled classes that get the job done)
Allow me to first introduce all my components:
Classes:
LabelObserver
Option Explicit
Private WithEvents mInteralObj As MSForms.label
Private mBackGroundColor As Long
Private mMouseOverColor As Long
Private Const clGREY As Long = &H8000000F
'// "Constructor"
Public Sub Init(label As MSForms.label, _
Optional mouseOverColor As Long = clGREY, _
Optional backGroundColor As Long = clGREY)
Set mInteralObj = label
mBackGroundColor = backGroundColor
mMouseOverColor = mouseOverColor
End Sub
Private Sub Class_Terminate()
Set mInteralObj = Nothing
End Sub
Public Sub MouseLeft()
'//Remove Highlight
mInteralObj.BackColor = mBackGroundColor
End Sub
Private Sub mInteralObj_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
'//Highlight
mInteralObj.BackColor = mMouseOverColor
End Sub
LabelNotifier
Option Explicit
Private observersCollection As Collection
Private Sub Class_Initialize()
Set observersCollection = New Collection
End Sub
Public Sub AddObserver(observer As LabelObserver)
observersCollection.Add observer
End Sub
Public Sub RemoveObserver(observer As LabelObserver)
Dim i As Long
'// We have to search through the collection to find the observer to remove
For i = 1 To observersCollection.Count
If observersCollection(i) Is observer Then
observersCollection.Remove i
Exit Sub
End If
Next i
End Sub
Public Function ObserverCount() As Integer
ObserverCount = observersCollection.Count
End Function
Public Sub Notify()
Dim obs As LabelObserver
If Me.ObserverCount > 0 Then
For Each obs In observersCollection
'//call each observer's MouseLeft method
obs.MouseLeft
Next obs
End If
End Sub
Private Sub Class_Terminate()
Set observersCollection = Nothing
End Sub
Module:
LabelObserverFactory (this is kinda optional - it simply provides a nice streamlined way of creating valid LabelObservers)
Option Explicit
Public Function NewYellowHighlightCustomLabel(label As MSForms.label) As LabelObserver
Dim product As New LabelObserver
product.Init label, vbYellow
Set NewYellowHighlightCustomLabel = product
End Function
Public Function NewRedHighlightCustomLabel(label As MSForms.label) As LabelObserver
Dim product As New LabelObserver
product.Init label, vbRed
Set NewRedHighlightCustomLabel = product
End Function
UserForm
MyForm (note that this form has three labels with default names placed on it for the purposes of this demo)
Option Explicit
Private notifier As LabelNotifier
Private Sub UserForm_Initialize()
Set notifier = New LabelNotifier
'//add controls to be notified
notifier.AddObserver LabelObserverFactory.NewYellowHighlightCustomLabel(Me.Label1)
notifier.AddObserver LabelObserverFactory.NewRedHighlightCustomLabel(Me.Label2)
notifier.AddObserver LabelObserverFactory.NewYellowHighlightCustomLabel(Me.Label3)
End Sub
Private Sub UserForm_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
'//Notify labels that mouse has left them
notifier.Notify
End Sub
Private Sub UserForm_Terminate()
Set notifier = Nothing
End Sub
Now, to explain what's going on here:
The form has a LabelNotifier object, which gets established when the form initializes, that it will use to notify our labels that the mouse has moved away from them. We do this by listening for the form's MouseMove event. (I know you are trying to avoid using this, but hopefully the fact that ours will just have one line of code, no matter how many labels you are impacting, will satisfy the desire to encapsulate logic elsewhere.) When we get a mouse move, we will have the notifier do its only job, to send a message to all the labels we added to it.
The LabelObserver is the counter part of the LabelNotifier. A label observer is responsible for telling the labels to change color and which colors to use.
Even if you don't like this implementation, I had fun making it. :-)
You don't need to create a separate class module to change things in the form. Just add event-handling methods in the code behind for the form. (In the form editor, right click on the form and select "View code".)
You can use the MouseMove event for the button to change its colour, and then use the MouseMove event for the form to reset the button colour, like so:
Private Sub UserForm_MouseMove(ByVal Button As Integer, _
ByVal Shift As Integer, _
ByVal X As Single, _
ByVal Y As Single)
CommandButton1.BackColor = &H8000000F
End Sub
Private Sub CommandButton1_MouseMove(ByVal Button As Integer, _
ByVal Shift As Integer, _
ByVal X As Single, _
ByVal Y As Single)
CommandButton1.BackColor = vbYellow
End Sub
Apologies for the awful title, its not the easiest thing to describe. I currently have a form with 3 listbox and adjacent to each 2 command buttons which allows the selected item in the listbox to move up or down. It all works fine but I would like to reduce the number of command buttons to a single pair and operate on the listbox with the currently selected item but I do not know if its possible. The code I have is:
Form declarations(3 instances for each pair of up/down buttons):
Private m_clsListMoveUpDown1 As CListbox_UpDown
Private m_clsListMoveUpDown2 As CListbox_UpDown
Private m_clsListMoveUpDown3 As CListbox_UpDown
and then instantiating the 3 instances of the class (within the form) via:
Set m_clsListMoveUpDown1 = New CListbox_UpDown
With m_clsListMoveUpDown1
Set .MoveDownButton = Me.Btn_MoveDown1
Set .MoveUpButton = Me.Btn_MoveUp1
Set .UpDownList = Me.LB_Sheet1
End With
Set m_clsListMoveUpDown2 = New CListbox_UpDown
With m_clsListMoveUpDown2
Set .MoveDownButton = Me.Btn_MoveDown2
Set .MoveUpButton = Me.Btn_MoveUp2
Set .UpDownList = Me.LB_Sheet2
End With
Set m_clsListMoveUpDown3 = New CListbox_UpDown
With m_clsListMoveUpDown3
Set .MoveDownButton = Me.Btn_MoveDown3
Set .MoveUpButton = Me.Btn_MoveUp3
Set .UpDownList = Me.LB_Sheet3
End With
Finally, some class declarations:
Public WithEvents MoveUpButton As MSForms.CommandButton
Public WithEvents MoveDownButton As MSForms.CommandButton
Public UpDownList As MSForms.ListBox
I haven't included either of the 2No. subroutines (moving the selected item up or down) within the class but can do if it helps. In a nutshell they contain:
With Me.UpDownList
'lines of code using arrays to move items up or down
End With
I completely understand the reason why a single pair of up/down buttons currently only operates on the listbox specified by e.g. Set .UpDownList = Me.LB_Sheet1 but I do not know how to associate the other 2 listboxes so that if either one has a selected item, have the buttons operate on that listbox.
I attempted to use the Set .UpDownList = Me.LB_Sheet1 or Me.LB_Sheet2 or Me.LB_Sheet2 but the gives a type mismatch error and I'm not sure what else to try.
I used a class like so
Private frmParent As MSForms.UserForm
Private lstListBoxUnderControl As MSForms.ListBox
Public WithEvents UP As MSForms.CommandButton
Public WithEvents DOWN As MSForms.CommandButton
Public Property Set PARENT_FORM(frm As MSForms.UserForm)
Set frmParent = frm
End Property
Public Property Set LISTBOX_CONTROLLING(lb As MSForms.ListBox)
Set lstListBoxUnderControl = lb
End Property
Private Sub Class_Initialize()
'
End Sub
Private Sub DOWN_Click()
' Down Code
lstListBoxUnderControl.BackColor = vbRed
End Sub
Private Sub UP_Click()
' Up Code
lstListBoxUnderControl.BackColor = vbGreen
End Sub
Setting it up in the form like so
Dim c As New clsUpdateController
Private Sub ListBox1_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Set c.LISTBOX_CONTROLLING = Me.ActiveControl
End Sub
Private Sub ListBox2_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Set c.LISTBOX_CONTROLLING = Me.ActiveControl
End Sub
Private Sub ListBox3_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Set c.LISTBOX_CONTROLLING = Me.ActiveControl
End Sub
Private Sub ListBox4_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Set c.LISTBOX_CONTROLLING = Me.ActiveControl
End Sub
Private Sub ListBox5_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Set c.LISTBOX_CONTROLLING = Me.ActiveControl
End Sub
Private Sub UserForm_Initialize()
Set c.PARENT_FORM = Me
Set c.DOWN = Me.cmdDown
Set c.UP = Me.cmdUP
Me.ListBox1.AddItem "test"
Me.ListBox2.AddItem "test"
Me.ListBox3.AddItem "test"
Me.ListBox4.AddItem "test"
End Sub
I'm re-visiting a tool that I wrote in VB.Net for my helpdesk team a while back and want to add a couple of checkboxes to replicate the same function that Windows uses to show hidden files and folders / re-hide, as well as protected operating system files.
I know I can do this by editing a registry entry and restarting explorer.exe, but that closes all open Explorer Windows and I don't want that.
Does anyone know how Windows is able to do this by a simple click of a checkbox and how I may be able to code it in VB.net?
Any input on this is greatly appreciated in advance.
EDIT: So it looks like I have found a refresh method that works to refresh Windows Explorer / File Explorer which can be applied to Drarig's answer below but I am having trouble converting it to VB.net as the original example is in C#.
'Original at http://stackoverflow.com/questions/2488727/refresh-windows-explorer-in-win7
Private Sub refreshExplorer(ByVal explorerType As String)
Dim CLSID_ShellApplication As Guid = Guid.Parse("13709620-C279-11CE-A49E-444553540000")
Dim shellApplicationType As Type = Type.GetTypeFromCLSID(CLSID_ShellApplication, True)
Dim shellApplication As Object = Activator.CreateInstance(shellApplicationType)
Dim windows As Object = shellApplicationType.InvokeMember("Windows", Reflection.BindingFlags.InvokeMethod, Nothing, shellApplication, New Object() {})
Dim windowsType As Type = windows.GetType()
Dim count As Object = windowsType.InvokeMember("Count", Reflection.BindingFlags.GetProperty, Nothing, windows, Nothing)
For i As Integer = 0 To CType(count, Integer)
Dim item As Object = windowsType.InvokeMember("Item", Reflection.BindingFlags.InvokeMethod, Nothing, windows, New Object() {i})
Dim itemType As Type = item.GetType()
'Only fresh Windows explorer Windows
Dim itemName As String = CType(itemType.InvokeMember("Name", Reflection.BindingFlags.GetProperty, Nothing, item, Nothing), String)
If itemName = explorerType Then
itemType.InvokeMember("Refresh", Reflection.BindingFlags.InvokeMethod, Nothing, item, Nothing)
End If
Next
End Sub
I am getting an exception Object reference not set to an instance of an object when I set itemType as Type = item.GetType() above. I can't figure out which object isn't being created. When I step through the code it looks like windowsType contains an object for windows. Does anyone have any idea on this? Once this is worked out I can then apply it to Drarig's solution below.
Alright I wish I could have got this to you sooner, but busy lately at work. I took a little time today to figure this out as I love digging into something I have not done before. This is the whole class from a new project; didn't have time to wrap it up in a separate class. I am sure this will get you what you need. It was a little harder than I thought as getting the correct handle and then send the command, but I got it. I hope you find it useful.
P.S. Some of the things you can leave out, specifically the boolean used for loading, this was so I can pull the current value back on load and either check/uncheck the CheckBox.
Note: This is tried and tested on Windows 7, 8 and 10
Imports Microsoft.Win32
Imports System.Reflection
Imports System.Runtime.InteropServices
Public Class Form1
<Flags()> _
Public Enum KeyboardFlag As UInteger
KEYBOARDF_5 = &H74
End Enum
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function GetWindow(ByVal hl As Long, ByVal vm As Long) As IntPtr
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function PostMessage(ByVal hWnd As IntPtr, ByVal Msg As UInteger, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Boolean
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindow(ByVal lpClassName As String, ByVal lpWindowName As String) As IntPtr
End Function
Private blnLoading As Boolean = False
Private Sub CheckBox1_CheckedChanged(sender As Object, e As EventArgs) Handles CheckBox1.CheckedChanged
Form1.HideFilesExtension(Me.CheckBox1.Checked)
If Not blnLoading Then NotifyFileAssociationChanged()
RefreshExplorer()
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim name As String = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(name, False)
blnLoading = True
Me.CheckBox1.Checked = CBool(key.GetValue("Hidden"))
key.Close()
blnLoading = False
End Sub
Private Shared Sub HideFilesExtension(ByVal Hide As Boolean)
Dim name As String = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(name, True)
key.SetValue("Hidden", If(Hide, 1, 0))
key.Close()
End Sub
Public Shared Sub RefreshExplorer()
Dim clsid As New Guid("13709620-C279-11CE-A49E-444553540000")
Dim typeFromCLSID As Type = Type.GetTypeFromCLSID(clsid, True)
Dim objectValue As Object = Activator.CreateInstance(typeFromCLSID)
Dim obj4 As Object = typeFromCLSID.InvokeMember("Windows", BindingFlags.InvokeMethod, Nothing, objectValue, New Object(0 - 1) {})
Dim type1 As Type = obj4.GetType
Dim obj2 As Object = type1.InvokeMember("Count", BindingFlags.GetProperty, Nothing, obj4, Nothing)
If (CInt(obj2) <> 0) Then
Dim num2 As Integer = (CInt(obj2) - 1)
Dim i As Integer = 0
Do While (i <= num2)
Dim obj5 As Object = type1.InvokeMember("Item", BindingFlags.InvokeMethod, Nothing, obj4, New Object() {i})
Dim type3 As Type = obj5.GetType
Dim str As String = CStr(type3.InvokeMember("Name", BindingFlags.GetProperty, Nothing, obj5, Nothing))
If (str = "File Explorer") Then
type3.InvokeMember("Refresh", BindingFlags.InvokeMethod, Nothing, obj5, Nothing)
End If
i += 1
Loop
End If
End Sub
Public Shared Sub NotifyFileAssociationChanged()
'Find the actual window...
Dim hwnd As IntPtr = FindWindow("Progman", "Program Manager")
'Get the window handle and refresh option...
Dim j = GetWindow(hwnd, 3)
'Finally post the message...
PostMessage(j, 256, KeyboardFlag.KEYBOARDF_5, 3)
End Sub
End Class
Here's a solution for everything excepting the refreshing of the explorer.
I've translated the code, but I'm unable to find how to refresh the explorer/desktop without restarting it.
Const keyName As String = "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
Const Hidden As String = "Hidden"
Const SHidden As String = "ShowSuperHidden"
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim St As Integer = GetRegValue(Hidden)
If St = 2 Then
SetRegValue(Hidden, 1)
SetRegValue(SHidden, 1)
Else
SetRegValue(Hidden, 2)
SetRegValue(SHidden, 0)
End If
End Sub
Private Function GetRegValue(valueName As String) As Integer
Return CInt(My.Computer.Registry.GetValue(keyName, valueName, 0))
End Function
Private Sub SetRegValue(valueName As String, value As Integer)
My.Computer.Registry.SetValue(keyName, valueName, value, Microsoft.Win32.RegistryValueKind.DWord)
End Sub
I have a few ideas to refresh the desktop :
Send a key to a running process. I tried this (source) :
Dim pp As Process() = Process.GetProcessesByName("explorer")
If pp.Length > 0 Then
For Each p In pp
AppActivate(p.Id)
SendKeys.SendWait("{F5}")
Next
End If
Refresh using SHChangeNotify (source),
Refresh broadcasting a WM_SETTINGCHANGE message (source),
etc.
I think you'll be forced to manually refresh or restart the explorer.
I have a simple subroutine that loads a list from a database. I would like to be able to use the same code to load a ListBox and a ComboBox by defining the list type as the common abstract base class ListControl, and see no reason why I can't - except that VB.NET doesn't expose/implement/whatever the Items collection in ListControl. I note with frustration that this is not the case in ASP.NET. At moment my code is ugly because I have to check what type of list control I have passed in, in order to cast it to a type that has an Items collection. (My code may be ugly for numerous other reasons too, but it is beautiful to me). Is there a way to rewrite the code to avoid having to go through the testing and casting nonsense? (I've stripped it down somewhat so that all that remains is where the problem lies).
Sub loadList(ByVal db As SqlDatabase, ByVal strCommandText As String, lstHost As ListControl, Optional bClearList As Boolean = True, Optional bIsListBox As Boolean = True)
If bClearList Then
If bIsListBox Then
CType(lstHost, ListBox).Items.Clear()
Else
CType(lstHost, ComboBox).Items.Clear()
End If
End If
Dim dt As DataTable = db.ExecuteDataSet(db.GetSqlStringCommand(strCommandText)).Tables(0)
For i = 0 To dt.Rows.Count - 1
If bIsListBox Then
CType(lstHost, ListBox).Items.Add(dt.Rows(i)(0).ToString)
Else
CType(lstHost, ComboBox).Items.Add(dt.Rows(i)(0).ToString)
End If
Next
End Sub
This is because in winforms a ListBox object collection is different from a ComboBox object collection. The simplest way I can think of to tidy this is to make a helper class like
Public Class ListHelper
Public Shared Sub Clear(ByRef lst As ListControl)
If TypeOf lst Is ListBox Then
CType(lst, ListBox).Items.Clear()
Else
CType(lst, ComboBox).Items.Clear()
End If
End Sub
Public Shared Sub Add(ByRef lst As ListControl, ByVal itm As Object)
If TypeOf lst Is ListBox Then
CType(lst, ListBox).Items.Add(itm)
Else
CType(lst, ComboBox).Items.Add(itm)
End If
End Sub
End Class
Then in your code you can just do :
Sub loadList(ByVal db As SqlDatabase, ByVal strCommandText As String, _
ByVal lstHost As ListControl, Optional ByVal bClearList As Boolean = True)
If bClearList Then
ListHelper.Clear(lstHost)
End If
Dim dt As DataTable = _
db.ExecuteDataSet(db.GetSqlStringCommand(strCommandText)).Tables(0)
For i = 0 To dt.Rows.Count - 1
ListHelper.Add(lstHost, dt.Rows(i)(0).ToString)
Next
End Sub
EDIT :
Another (probably better) way to do this is using extension methods (add a new module and ) :
Imports System.Runtime.CompilerServices
Module ListExtensions
<Extension()> _
Public Sub AddToItems(ByRef lc As ListControl, ByVal itm As Object)
If TypeOf lc Is ListBox Then
CType(lc, ListBox).Items.Add(itm)
ElseIf TypeOf lc is ComboBox then
CType(lc, ComboBox).Items.Add(itm)
Else
'handle abuse
End If
End Sub
<Extension()> _
Public Sub ClearItems(ByRef lc As ListControl)
If TypeOf lc Is ListBox Then
CType(lc, ListBox).Items.Clear()
ElseIf TypeOf lc is ComboBox Then
CType(lc, ComboBox).Items.Clear()
Else
'handle abuse
End If
End Sub
End Module
Which ends up being even a bit neater in your code :
Sub loadList(ByVal db As SqlDatabase, ByVal strCommandText As String, _
ByVal lstHost As ListControl, Optional ByVal bClearList As Boolean = True)
If bClearList Then
lstHost.ClearItems()
End If
Dim dt As DataTable = _
db.ExecuteDataSet(db.GetSqlStringCommand(strCommandText)).Tables(0)
For i = 0 To dt.Rows.Count - 1
lstHost.AddToItems(dt.Rows(i)(0).ToString)
Next
End Sub
Here I've called these ClearItems and AddToItems to avoid ambiguity with instance methods. ListControl doesn't have .Clear() or .Add() itself but for the sake of being explicit it's probably best to have a unique nomenclature for extensions.