VBA Gantt Chart, Parent-activities dictionary - excel

I am brand new to VBA and would need your help on a personal project.
I want to create a custom Gantt chart on Excel VBA.
Excel version is Microsoft® Excel® for Microsoft 365 MSO (Version 2208 Build 16.0.15601.20446) 64-bit
One aspect of the project I am struggling with is to manage "Parent-activities". Lets say that I have an activity identified as "Child", that can not start before other activities identified as "Parents" finish.
I would like to build a Public dictionary, each entry/key being a "Child", each child having a collection of "Parents".
My issue is that my code is very buggy, sometimes it works, sometimes not and I can not tell why...
Here is what I tried. I would really appreciate if you could flag any issue.
The Parent List is buggy (sometimes works, sometimes not).
The Parent Count function does not work, I guess I dont have the right syntax.
Public Parents_Dict As New Scripting.Dictionary 'I want this dictionary accessible from any Sub and I want to keep its content throughout the project
Public Child As Variant 'This will store the ID of the Child depending on the context
Public Parent As Variant 'This will store the ID of the Parent depending on context
'I have a button "Manage Parent" that triggers a user form and save the Child ID
Sub Open_Parents_Manager() 'We want to open the user form to manage parent activities
'We find the activity ID of the cell that was active when we opened the form
'We store this ID as the Variable "Child" that is recognize in this whole module
Child = CStr(ActiveSheet.Cells(ActiveCell.Row, 1).Value)
Manage_Parents.Show vbModeless 'VbModeless allow to interact with the speadsheet while the user form is still open
End Sub
'In this user form I have a Listbox that should list the Parents of the selected Child
Sub UserForm_Activate()
'We want the title of the box to mention Child ID to help the user confirm they are working on the right activity
Me.Caption = "Manage parent activities of " & Child 'Mention the ID of the Child as a title of the window
'We want to display the list of the current parents of this activity
Call Display_Parents_List
'We want to display the number of parents of this activity
Call Count_Parents
End Sub
Sub Display_Parents_List()
UserForm.Parents_ListBox.Clear
Dim Val As Variant
On Error Resume Next 'Override error if no dictionary exists
For Each Val In Parents_Dict(Child)
UserForm.Parents_ListBox.AddItem Val 'We add each item in the collection in our ListBox
Next Val
End Sub
'In this user form I also have a label that should return the number of parents for the selected child
Sub Count_Parents() 'We inform the user of how many parents this Child has
If Parents_Dict.Exists(Child) Then
UserForm.Label1.Caption = Parents_Dict(Child).Count 'This one does not work
Else
UserForm.Label1.Caption = "Currently, this activity has no parent"
End If
End Sub
'Finally I have button to add parents.
Sub Add_Parent()
Parent = "TP9" 'for testing I change the values manually here
If Parents_Dict.Exists(Child) Then
On Error Resume Next
Parents_Dict(Child).Add Item:=Parent, Key:=Parent 'Collection can have multiple times the same item but not under the same Key, Using Parent as Item & Key avoids duplicates
On Error GoTo 0
Else
Dim newList As Collection
Set newList = New Collection
newList.Add Item:=Parent, Key:=Parent
Parents_Dict.Add Key:=Child, Item:=newList
End If
End Sub

Related

How to add (or copy) a new userform in VBA using code inside a Module (not Insert Button)

I need to be able to have identical userforms open simultaneously as a user needs to access info from a dataset to update entries.
Each userform will have the same text boxes, command boxes, etc.
So the userform needs to be of structure userform(i).
How do I create the userforms dynamically? The user selects ID# and new form is created.
I can handle the loops and passing the name into the form to update all the references, but it’s the making a dynamic copy in code that has me completely stumped.
I was thinking the code would be something like:
Dim frm As UserForm
Set frm = UserForms.Add
frm.Name = "NewName_i"
I would then have a loop that would look for how many "NewName_i" exists using Forms.Count and Forms.Visible and then would just add one more as it is needed.
But I cannot get past the creating of a new form dynamically with code.
If you need to show the same form (e.g.) 5 times then you can do something like this:
Dim colForms As Collection 'holds references to the opened forms
Sub TestForms()
Dim i As Long
Set colForms = New Collection
For i = 1 To 5
Dim f As frmTestx
Set f = New frmTestx
f.Show vbModeless 'if not modeless then code stops here...
colForms.Add f
Next i
End Sub

Is there a way to put manydifferent folders into a treeview

I am new with vba, and I want to fill a huge "microsoft treeview control, version 6.0" with 15000 different folders in excel on this format:
/folderOne
/folderOne/subfolderOne
/folderOne/subfolderTwo
/folderTwo/subfolderOne
I have used the Pradeep Kumar's solution, but it makes excel crash if I try to make it with too many rows (ok with 1000 rows, but not with 2000 rows) which is as follow:
Sub Button1_Click()
LoadTreeView1 TreeView1, 1, 1000
End Sub
Private Sub LoadTreeView1(TV As TreeView, min As Integer, max As Integer)
Dim i As Integer, RootNode As Node
TV.Nodes.Clear
Set RootNode = TV.Nodes.Add(, , "ROOT", "ROOT")
RootNode.Expanded = True
For i = min To max
AddNode TV, RootNode, Cells(i, 1)
Next
End Sub
Private Sub AddNode(TV As TreeView, RootNode As Node, Path As String)
Dim ParentNode As Node, NodeKey As String
Dim PathNodes() As String
On Error GoTo ErrH
PathNodes = Split(Path, "/")
NodeKey = RootNode.Key
For i = 1 To UBound(PathNodes)
Set ParentNode = TV.Nodes(NodeKey)
NodeKey = NodeKey & "/" & PathNodes(i)
TV.Nodes.Add ParentNode, tvwChild, NodeKey, PathNodes(i)
ParentNode.Expanded = True
Next
Exit Sub
ErrH:
If Err.Number = 35601 Then
Set ParentNode = RootNode
Resume
End If
Resume Next
End Sub
My treeview looks good on excel, similar to the one below but my issue is that i can't put enough data in it.
I have exported my list into access, but the process is different, and I am a bit lost because as I said, I am new with vba.
Thank you for your help
Main strategy in using treeview controls is to upload data of expanded rows on demand.
firstly, you're loading just high-level treeview elements (clienti list)
after user clicks the node, the method of filling corresponding node with child elements is called
Idea is that nobody would ever scroll through all list of elements in treeview.
These details could be difficult if you're new in VBA, so may be it is better to use linked lists. E.g.
list1: contains clienti list
list2: contains dates
after selecting element in list1, list2 recordsource is dynamically changed in order to provide list of only and only elements, that correspond to list1
This solution exceeds limitations of memory for treeview control, and suitable if you have definite depth of taxonomy of your objects.

How to retrieve name from dynamically created frames

I'm fairly new to VBA and I'm in over my head this time...
I'm building an order form with several dynamically created frames, each of which contains one or more dynamically created comboboxes. What I eventually hope to achieve, is for every combobox to have a change event.
When certain conditions are met, two textboxes have to appear
The first of these textboxes needs to have a change event as well
When certain conditions are met, a new combobox needs to be created right beneath the first combobox, with all the same values except the value of the first combobox
If this happens, the height of the parent frame needs to be adjusted and all subsequent frames have to be lowered accordingly
I'm not expecting a full solution, but some help with the first step would be appreciated. Specifically, how do I make sure the new textboxes are created in the same frame as the combobox?
I've puzzled together some stuff from other threads, and came up with the code below. It breaks down at Set Included, apparently on the subgroup. Yet subgroup is a frame with the correct name. If I change "subgroup" into "Subgroep- " & aantalframes it does work.
Option Explicit
Public Included As MSForms.textbox
Public subgroup As MSForms.frame
Public dinbegrepen As Long
Public dmaximum As Long
Public aantalframes As Long
Public WithEvents ChkEvents As MSForms.combobox
Private Sub ChkEvents_Change()
If dmaximum > 1 Then
Set subgroup = ConfigureProduct.Controls("Subgroep- " & aantalframes)
Set Included =
ConfigureProduct.Controls("subgroup").Add("Forms.TextBox.1", "included")
With Included
.value = dinbegrepen
.Left = 305
.Width = 25
End With
End If
End Sub

Calling a VBA form from a button causes UserForm_Initialize to run twice, breaking my code?

Hello wonderful VBA community,
I'm still really new to vba and am trying to learn a lot. Thank you in advance for looking through my code and my description of the issue I'm facing.
I have a button on a page that calls a new Userform.
CODE SNIPPET 1:
Sub btnShowDetails_Click()
Call frmShowDeets.ShowDeets
End Sub
... which calls the next bit of code in the 'frmShowDeets' UserForm:
CODE SNIPPET 2:
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
... triggering:
CODE SNIPPET 3:
Private Sub UserForm_Initialize()
Dim comboBoxItem As Range
For Each comboBoxItem In ContactList.Range("tblContactList[CompanyName]")
'^refers to unique values in a named range
With Me.boxCompanySelection
.AddItem comboBoxItem.Value
End With
Next comboBoxItem
End Sub
So at this point, the form I want to display has values loaded in its one combobox for user selection. The user selects a company and the Combobox_Change event triggers other routines that pull information for that company.
CODE SNIPPET 4:
Public Sub boxCompanySelection_Change()
Call frmShowDeets.PullData
End Sub
Sub PullData()
Dim numCompanies As Long
numCompanies = ContactList.Range("B6").Value 'this holds a count of the rows in the named range
Dim FoundCell As Range
Set FoundCell = ContactList.Range("tblContactList[Company Name]").Find(What:=boxCompanySelection.Text, LookIn:=xlValues, LookAt:=xlWhole)
Dim CompanyRow As Long
CompanyRow = FoundCell.Row
With ContactList
'pull a bunch of the company's details
End With
End Sub
Here is where it gets weird... Once the form is shown and the user selects one of the combo box items, triggering the Combobox_Change event the code breaks because the 'What:=boxCompanySelection.Text' part of the Range().Find method reads as "" empty (even though Code Snippet 3 is meant to load in company names and Code Snippet 4 is only triggered when the user selects one of those company names from the combobox) and I shouldn't need to build something to handle 'not found' exceptions since the only possible values should be the ones pulled in from my named range.
From stepping through the code, I have determined that for some reason, Code Snippets 2 and 3 run TWICE before Snippet 4 is run. Does anyone know what about my code is causing this to happen? I'm thinking there's a disconnect between the form that is shown and loaded with combobox values and whatever Code Snippet 4 is reading data from.
What is weirder is that if I run the code starting from Code Snippet 2 (ignoring the button call in Code Snippet 1), the form works as intended and from what I can tell 2 and 3 are only run once.
The problem is probably something simple I'm overlooking but I just cannot figure out what it is. Thanks again!
You have to understand that a form is an object - exactly as any other class module, except a form happens to have a designer and a base class, so UserForm1 inherits the members of the UserForm class.
A form also has a default instance, and a lot of tutorials just happily skip over that very important but rather technical bit, which takes us exactly here on Stack Overflow, with a bug involving global state accidentally stored on the default instance.
Call frmShowDeets.ShowDeets
Assuming frmShowDeets is the name of the form class, and assuming this is the first reference to that form that gets to run, then the UserForm_Initialize handler of the default instance runs when the . dot operator executes and dereferences the object. Then the ShowDeets method runs.
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
That line triggers UserForm_Initialize on the local instance named frm - which is an entirely separate object, of the same class. The Initialize handler runs whenever an instance of a class is, well, initialized, i.e. created. The Terminate handler runs when that instance is destroyed.
So ShowDeets is acting as some kind of "factory method" that creates & shows a new instance of the frmShowDeets class/form - in other words whatever happened on the default instance is irrelevant beyond that point: the object you're working with exists in the ShowDeets scope, is named frm, and gets destroyed as soon as it goes out of scope.
Remove the ShowDeets method altogether. Replace this:
Call frmShowDeets.ShowDeets
With this:
With New frmShowDeets
.Show
End With
Now the Initialize handler no longer runs on the default instance.
What you want, is to avoid using the default instance at all. Replace all frmShowDeets in the form's code-behind, with Me (see Understanding 'Me' (no flowers, no bees)), so that no state ever accidentally gets stored in the default instance.
Call frmShowDeets.PullData
Becomes simply:
Call Me.PullData
Or even:
PullData
Since Call is never required anywhere, and the Me qualifier is always implicit when you make a member call in a class module's code.

VBA best practices for modules relative to modeless userforms

I came across this similar issue and read the replies: Modeless form that still pauses code execution
I have been attempting to apply in my own situation the suggestion provided by David Zemens. In my situation, I cannot seem to find an approach that incorporates Mr. Zemen's suggestion without also utilizing a GoTo.
I am wondering if there is a better or more elegant solution.
Here is an outline of what I am doing:
I have a UserForm with a Command Button that begins the code execution that will perform several actions on multiple Excel workbooks. As such, there are a number of blocks of code and the successful completion of one block of code allows for the execution of the subsequent block of code.
At a certain point, depending on the situation, the code might require User input; in other situations, the needed data is obtainable from an Excel. If input is needed from the User, another UserForm is displayed.
The User may need to view several different Excel sheets before entering the input, so the UserForm is modeless. So the code comes to a stop until the User enters the needed input and clicks another Command Button.
It is at this point I am having trouble: how to resume the program flow. Is the only way to 'pick-up where it left-off' is by using a GoTo statement? Or is there some way to organize the modules so there is a single consistent program flow, defined in one spot and not duplicated from the point at which User input might be needed?
Here is my take on the problem . Hope I understood the problem correctly.
Assumptions:
There are two user forms.
UserForm1 with a button to start the processing.
UserForm2 with a button to supply intermediate input.
A sub inside a module to start/ launch UserForm1.
VBA Code (for the sub routine)
Sub LaunchUserForm1()
Dim frm As New UserForm1
'/ Launch the main userform.
frm.Show vbModeless
End Sub
VBA Code (for UserForm1)
Private Sub cmdStart_Click()
Dim i As Long
Dim linc As Long
Dim bCancel As Boolean
Dim frm As UserForm2
'/ Prints 1 to 5 plus the value returned from UserForm2.
For i = 1 To 5
If i = 2 Then
Set frm = New UserForm2
'/ Launch supplementary form.
frm.Show vbModeless
'<< This is just a PoC. If you have large number of inputs, better way will be
' to create another prop such as Waiting(Boolean Type) and then manipulate it as and when User
' supplies valid input. Then validate the same in While loop>>
'/ Wait till we get the value from UserForm2.
'/ Or the User Cancels the Form with out any input.
Do While linc < 1 And (linc < 1 And bCancel = False)
linc = frm.Prop1
bCancel = frm.Cancel
DoEvents
Loop
Set frm = Nothing
End If
Debug.Print i + linc
Next
MsgBox "User Form1's ops finished."
End Sub
VBA Code (for UserForm2)
Dim m_Cancel As Boolean
Dim m_prop1 As Long
Public Property Let Prop1(lVal As Long)
m_prop1 = lVal
End Property
Public Property Get Prop1() As Long
Prop1 = m_prop1
End Property
Public Property Let Cancel(bVal As Boolean)
m_Cancel = bVal
End Property
Public Property Get Cancel() As Boolean
Cancel = m_Cancel
End Property
Private Sub cmdlinc_Click()
'/Set the Property Value to 10
Me.Prop1 = 10
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'/ Diasble X button
Me.Cancel = True
Me.Hide
Cancel = True
End Sub
OK so here are my thoughts.
You have a userform frmSelectUpdateSheet which you wish to use in order to allow the user to select the sheet, when the sheet can't be determined programmatically. The problem is that if you do .Show vbModeless (which allows the user to navigate the worksheet/s), then code continues to execute which either leads to errors or otherwise undesired output.
I think it's possible to adapt the method I described in the previous answer. However, that's out of the question here unless you're paying me to reverse engineer all of your code :P
Assuming you have a Worksheet object variable (or a string representing the sheet name, etc.) which needs to be assigned at this point (and that this variable is Public in scope), just use the CommandButton on the form to assign this based on the selected item in the frmSelectUpdateSheet list box.
This is probably a superior approach for a number of reasons (not the least of which is trying to avoid application redesign for this sort of fringe case), such as:
This keeps your form vbModal, and does prevent the user from inadvertently tampering with the worksheet during the process, etc.
Using this approach, the thread remains with the vbModal displayed frmSelectUpdateSheet, and you rely on the form's event procedures for control of process flow/code execution.
It should be easier (and hence, cheaper) to implement; whether you're doing it yourself or outsourcing it.
It should be easier (and hence, cheaper) to maintain.
NOW, on closer inspection, it looks like you're already doing this sort of approach with the cmdbtnSelect_Click event handler, which leads me to believe there's a related/follow-up problem:
The sheet names (in listbox) are not sufficient for user to identify the correct worksheet. So if the user needs the ability to "scroll" the sheet (e.g., to review data which does not fit in the window, etc.), then add some spinner buttons or other form controls to allow them to navigate the sheet.

Resources