I am running a code, that changes the format of the task's row, based on a value in Text1 field.
If I change the Duration, or Finish or some other values when I update the schedule, the value of Text1 (customized field) is modified as well. As a result of that value, I am formatting the background color and the font color.
Once I run this code, I can't use the regular "Undo" so I can't get the values to return to their previous state.
How do I create the "Custom Undo"?
ThisProject Code
Private Sub Project_Change(ByVal pj As Project)
' enable class to modify the Task format on Project change (when a task is changed)
StatusRYGFieldUpdate
End Sub
Regular Code Module
Option Explicit
Public StatusRYGView As New clsTskUpdate
Public UpdateViewFlag As Boolean
Public TskIDChanged As Long
Sub StatusRYGFieldUpdate()
' this Sub is triggered once a task is modified
' if the Field being modifed is related to "Text1"
Dim CurTskID As Long
Set StatusRYGView.ProjApp = Application
Application.Calculation = pjManual
Application.ScreenUpdating = False
If UpdateViewFlag Then
CurTskID = TskIDChanged ' save Row ID
FormatTask (TskIDChanged) ' call the Sub that formats the cell (send the taskId)
End If
Application.Calculation = pjAutomatic
Application.ScreenUpdating = False
End Sub
'===========================================================
Sub FormatTask(TskID)
Dim Tsk As Task
If UpdateViewFlag Then
SelectTaskField TskID, "Text1", False
Set Tsk = ActiveCell.Task ' set the Task to current cell's Task
SelectRow Row:=TskID, RowRelative:=False
' format entire row first
Select Case Tsk.Text1 ' Get the Field's used field, not name
Case "R"
FontEx CellColor:=7, Color:=0
FontEx Italic:=False
Case "Complete"
FontEx Italic:=True
FontEx CellColor:=15, Color:=14 ' Background Silver ; font Gray
End Select
' format "Status" field
SelectTaskField TskID, "Text1", False
Select Case Tsk.Text1 ' Get the Field's used field, not name
Case "R"
' Font Color:=pjWhite ' Font White
FontEx Italic:=False
FontEx CellColor:=1, Color:=7 ' Background Red ; font White
Case "Complete"
FontEx Italic:=True ' Font Italic
FontEx CellColor:=15, Color:=14 ' Background Silver ; font Gray
End Select
End If ' UpdateViewFlag is True
End Sub
clsTskUpdate Class Module
Option Explicit
Public WithEvents ProjApp As Application
Private Sub ProjApp_ProjectBeforeTaskChange(ByVal Tsk As Task, ByVal Field As PjField, ByVal NewVal As Variant, Cancel As Boolean)
' Sub (in "clsTskUpdate" Class) is triggered once a task is modified
' if the Field being modifed is related to "Text1"
' then the UpdateViewFlag is being raised, and the Tsk.ID (task's row) is saved to TskIDChanged variable
UpdateViewFlag = False
TskIDChanged = 0
Select Case Field
Case pjTaskActualFinish
If Not NewVal Like Format(Tsk.ActualFinish, myDateFormat) Then ' need to modify date format to "dd/mm/yy"
LastValue = Tsk.ActualFinish
UpdateViewFlag = True
TskIDChanged = Tsk.ID
End If
Case pjTaskStart
If Not NewVal Like Format(Tsk.Start, myDateFormat) Then ' need to modify date format to "dd/mm/yy"
LastValue = Tsk.Start
UpdateViewFlag = True
TskIDChanged = Tsk.ID
End If
Case pjTaskDuration
If Not NewVal Like (Tsk.Duration / 480) & "*" Then ' need to divide by 480 (in minutes) and add `*` wild-card for "days"
LastValue = Tsk.Duration / 480
UpdateViewFlag = True
TskIDChanged = Tsk.ID
End If
Case pjTaskPercentComplete
If Not NewVal Like Tsk.PercentComplete Then
LastValue = Tsk.PercentComplete
UpdateViewFlag = True
TskIDChanged = Tsk.ID
End If
' other possible Case Scenarios in the future
End Select
End Sub
Microsoft Project 2007 added a pair of methods, OpenUndoTransaction and CloseUndoTransaction, that create a single undo entry for the user to undo an entire macro.
Add these methods to procedure StatusRYGFieldUpdate like this:
Sub StatusRYGFieldUpdate()
Dim CurTskID As Long
Set StatusRYGView.ProjApp = Application
Application.OpenUndoTransaction "Status RYG Field Update"
Application.Calculation = pjManual
Application.ScreenUpdating = False
If UpdateViewFlag Then
CurTskID = TskIDChanged ' save Row ID
FormatTask (TskIDChanged) ' call the Sub that formats the cell (send the taskId)
End If
Application.Calculation = pjAutomatic
Application.ScreenUpdating = True
Application.CloseUndoTransaction
End Sub
Related
I am currently trying to use a ProjectBeforeAssignmentChange application in a class module to pull resource and assignment costs into a task field. I am doing this because I want to group resource loads on a task into labor and material buckets. I have received some great guidance on how to use a ProjectBeforeAssignmentChange application to isolate an assignment change and react to it. I am now trying to make a link between this application and the ability to parse and sum data in a task.Cost# field.
Currently, the class module prints a lot of helpful information into the immediate window. Can someone please recommend how to sum Cost = Assgn.Cost as it loops and route it to tsk.Cost5 (for my purposes)? I have tested multiple approaches to this problem and all failed. It appears that the assignment change event does not acknowledge task objects.
ThisProject:
Private Sub Project_Open(ByVal pj As Project)
Call m_Events.StartEvents
End Sub
m_Event as a regular module:
Public oMSPEvents As New cm_Events
Public EnableEvents As Boolean
Sub StartEvents()
Set oMSPEvents.MyMSPApp = Application ' MSProject.Application
EnableEvents = True
End Sub
cm_Events Class Module:
Private Sub MyMSPApp_ProjectBeforeAssignmentChange(ByVal Assgn As Assignment, ByVal Field As PjAssignmentField, ByVal NewVal As Variant, Cancel As Boolean)
'
'Dim tsk As task
If Assgn.Resource.Text2 = "Labor" Then
Cost = Assgn.Cost
Debug.Print Now, Assgn.ResourceName, Assgn.Resource.Text2, NewVal, "$" & Cost, Assgn
End If
If EnableEvents Then
EnableEvents = False
' tsk.Cost5 = 10
'
' Dim Assgn As Assignment
' For Each Assgn In tsk.Assignments
' If Assgn.Resource.Text1 = "Labor" Then
' tsk.Cost5 = tsk.Cost5 + Assgn.Cost
' Assgn.Cost5 = Assgn.Cost5 + Assgn.Cost
' End If
' Next Assgn
'
EnableEvents = True
End If
End Sub
Goal: Capture changes to assignment-level cost changes and roll them into the task's Cost5 field
Issue: While the event, ProjectBeforeAssignmentChange, can be used to react to assignment-level changes, the Cost field is calculated so changes to it are done indirectly, for example, by changing the amount of work. Therefore, the event handler needs to react to changes to any field that would change the assignment cost.
Here is an example of how to use the ProjectBeforeAssignmentChange event to monitor changes to the assignment work to update the task's Cost5 field:
Private Sub MyMSPApp_ProjectBeforeAssignmentChange(ByVal asg As Assignment, ByVal Field As PjAssignmentField, ByVal NewVal As Variant, Cancel As Boolean)
If EnableEvents And Field = pjAssignmentWork Then
EnableEvents = False
Dim tsk As Task
Set tsk = asg.Task
Dim resRate As Double
resRate = CDbl(Mid(asg.Resource.StandardRate, 2, InStr(asg.Resource.StandardRate, "/") - 2))
Dim newAsnCost As Variant
newAsnCost = NewVal * resRate
If asg.Resource.Text1 = "Labor" Then
tsk.Cost5 = newAsnCost
Else
tsk.Cost5 = 0
End If
Dim Assgn As Assignment
For Each Assgn In tsk.Assignments
If Assgn.Resource.Text1 = "Labor" And Assgn <> asg Then
tsk.Cost5 = tsk.Cost5 + Assgn.Cost
End If
Next Assgn
EnableEvents = True
End If
End Sub
Note: the resRate calculation is based on the Standard Rate having format like "$123.45/hr"; adjust as necessary.
Take a look at this Wiki article. It will show you how to do it.
https://social.technet.microsoft.com/wiki/contents/articles/32051.ms-project-extra-fields-in-views.aspx
I'm having trouble getting my pivot table to select multiple values for the B3 filter. When I enter the criteria as an array, it recognizes it as null - "Cannot enter a null value as an item or field name in a PivotTable report." Is there a straightforward way to do this? I couldn't find a solution from Google.
Sub Button5_Click()
Dim docworksheet As Worksheet
Dim docworkbook As Workbook
Set docworkbook = ThisWorkbook
Set docworksheet = docworkbook.Sheets("Analysis")
docworksheet.Activate
ActiveSheet.PivotTables("PivotTable2").ManualUpdate = False
ActiveSheet.Range("B4").Value = "(blank)"
ActiveSheet.Range("B5").Value = "(All)"
ActiveSheet.Range("B2").Value = "(All)"
ActiveSheet.Range("B3").Value = Array("A", "B", "C")
ActiveSheet.Range("B7").Value = "L"
End Sub
Go hit "Record Macro", then carry out your action, then hit "Stop Recording".
You should have something like this:
With ActiveSheet.PivotTables("PivotTable2").PivotFields("Field Name Here")
.PivotItems("First Item Name").Visible = True
.PivotItems("Second Item Name").Visible = False
.PivotItems("Third Item Name").Visible = True
End With
This goes through and sets each item one-by-one.
So loop through all of the PivotItems in your PivotField, and compare to the values in your Array (e.g. with the Filter function) - for something like this:
Public Sub FilterPivotByArray(Target As PivotField, Values() As String)
Dim piTMP As PivotItem, bManualUpdate As Boolean
With Target.Parent
bManualUpdate = .ManualUpdate 'Store setting for later
.ManualUpdate = True 'Turn on Manual Update to speed things up
End With
Target.ClearAllFilters
For Each piTMP In Target.PivotItems
'Visible if Value is in Array
'Not Visible if Value is Not in Array
piTMP.Visible = (UBound(Filter(Values, piTMP.Value)) >= 0)
Next piTMP
With Target.Parent
.Update
.ManualUpdate = bManualUpdate 'restore setting
End With
End Sub
I have created a UserForm that has two ListBoxes, one populated from a dictionary that contains excess items from a company report, and the other from a dictionary that contains excess items from a bank report. The first ListBox is a fmMultiSelectMulti, allowing the user to select multiple items to get the sum of the selected items (which change the value of a TextBox).
My issue is when I select the first item in the ListBox, the ListBox_Change() event fires twice. The sum variable is public since it is referenced in other methods, but upon the double-fire, it doubles the real value.
Here's the code for the change event:
Private Sub GPListBox_Change()
For lItem = 0 To GPListBox.ListCount - 1
If GPListBox.Selected(lItem) = True Then
gpTotal = gpTotal + GPListBox.List(lItem, 1)
Debug.Print GPListBox.List(lItem, 1)
End If
Next
GPTotalTextBox.Value = Format(gpTotal, "$#,##0.00")
End Sub
The other method that removes the (multiple) selected variables, and references the sum variable:
Private Sub RemoveButton1_Click()
For lItem = GPListBox.ListCount - 1 To 0 Step -1
If GPListBox.Selected(lItem) Then
gpTotal = gpTotal - GPListBox.List(lItem, 1)
'GPListBox.RemoveItem GPListBox.ListIndex
GPListBox.RemoveItem lItem
GPTotalTextBox.Value = gpTotal
End If
Next
End Sub
This is the UserForm after I selected the first item, which automatically deselected and left the sum present:
.
My Question: How do I prevent this from double-firing every time the first selection occurs?
I have got around it like this in the past. Something like this.
Use a global boolean at the top of you code. Above all subs and functions.
Dim bFire as Boolean
A boolean is false by default so you will have to set the boolean to true somewhere outside of you subs such as form UserForm_Initialize event or something. If you don't have a place to do that, switch the true/false usage in the subs (Benno Grimm Elaborated on this below in comments).
Private Sub UserForm_Initialize()
bFire = True
End Sub
Then use the boolean in the subs.
Private Sub GPListBox_Change()
'Check the status and get out if you have set it to not fire.
If bFire = false then
Exit Sub
End If
For lItem = 0 To GPListBox.ListCount - 1
If GPListBox.Selected(lItem) = True Then
gpTotal = gpTotal + GPListBox.List(lItem, 1)
Debug.Print GPListBox.List(lItem, 1)
End If
Next
GPTotalTextBox.Value = Format(gpTotal, "$#,##0.00")
End Sub
In the button that modifies it, set the boolean false at the start and true at the end.
Private Sub RemoveButton1_Click()
'Set it false
bFire = false
For lItem = GPListBox.ListCount - 1 To 0 Step -1
If GPListBox.Selected(lItem) Then
gpTotal = gpTotal - GPListBox.List(lItem, 1)
'GPListBox.RemoveItem GPListBox.ListIndex
GPListBox.RemoveItem lItem
GPTotalTextBox.Value = gpTotal
End If
Next
'After you have modified the control set it to true
bFire = True
End Sub
I am implementing a Userform and wish to include some checks on the input data prior to running the Userform. In particular, check all inputs into the Userform textboxs are numerical, although it is valid a textbox is blank or Null. I have tried implementing the following:
Select Case KeyAscii
Case 0, 46, 48 To 57
Case Else
MsgBox "Only numbers allowed"
End Select
But this does not work.
Please, ideas?
Thank you very much!!!!!!!!!
Maybe bit long winded - I usually use a class module and the tag property on the control to decide what can be entered in a textbox.
Create a form with four text boxes.
Give the text boxes these tags:
1;CDBL
2;CINT
3;CSTR
4;CSENTENCE
The numbers are the columns to paste the values into when the form is saved (I haven't described that bit here).
The text describes what can be entered in the textbox - CDBL is numeric with 2 decimal places, CINT is numeric with 0 decimal places, CSTR is for Proper text and CSENTENCE is for sentence text.
Create a class module called clsControlText.
Add this code to the class module:
Public WithEvents txtBox As MSForms.TextBox
Private Sub txtBox_Change()
Static LastText As String
Static SecondTime As Boolean
Const MaxDecimal As Integer = 2
Const MaxWhole As Integer = 1
With txtBox
If InStr(.Tag, ";") > 0 Then
Select Case Split(.Tag, ";")(1)
Case "CDBL", "CCur"
'Allow only numbers with <=2 decimal places
If Not SecondTime Then
If .Text Like "[!0-9.-]*" Or Val(.Text) < -1 Or _
.Text Like "*.*.*" Or .Text Like "*." & String$(1 + MaxDecimal, "#") Or _
.Text Like "?*[!0-9.]*" Then
Beep
SecondTime = True
.Text = LastText
Else
LastText = .Text
End If
End If
SecondTime = False
Case "CINT"
'Allow only whole numbers.
If .Text Like "[!0-9]" Or Val(.Text) < -1 Or .Text Like "?*[!0-9]*" Then
Beep
.Text = LastText
Else
LastText = .Text
End If
Case "CSTR"
'Convert text to proper case.
.Text = StrConv(.Text, vbProperCase)
Case "CSENTENCE"
'Convert text to sentence case (capital after full-stop).
.Text = ProperCaps(.Text)
Case Else
'Allow anything.
End Select
End If
End With
End Sub
Private Function ProperCaps(strIn As String) As String
Dim objRegex As Object
Dim objRegMC As Object
Dim objRegM As Object
Set objRegex = CreateObject("vbscript.regexp")
strIn = LCase$(strIn)
With objRegex
.Global = True
.ignoreCase = True
.Pattern = "(^|[\.\?\!\r\t]\s?)([a-z])"
If .Test(strIn) Then
Set objRegMC = .Execute(strIn)
For Each objRegM In objRegMC
Mid$(strIn, objRegM.firstindex + 1, objRegM.Length) = UCase$(objRegM)
Next
End If
ProperCaps = strIn
End With
End Function
Add this code to the user form:
Private colTextBoxes As Collection
Private Sub UserForm_Initialize()
Dim ctrlSelect As clsControlText
Dim ctrl As Control
Me.Caption = ThisWorkbook.Name
Set colTextBoxes = New Collection
For Each ctrl In Me.Controls
Select Case TypeName(ctrl)
Case "TextBox"
Set ctrlSelect = New clsControlText
Set ctrlSelect.txtBox = ctrl
colTextBoxes.Add ctrlSelect
End Select
Next ctrl
End Sub
NB: Not all this code is mine. I found ProperCaps and the code for CDBL elsewhere on this site - or maybe MrExcel.
You could use a basic LIKE or Regexp
Sub Test()
Debug.Print StrCheck("")
Debug.Print StrCheck("hello kitty")
Debug.Print StrCheck("4156")
End Sub
function
Function StrCheck(strIn As String) As String
Dim objRegex As Object
Set objRegex = CreateObject("vbscript.regexp")
objRegex.Pattern = "\d+"
'vaidate empty string
If Len(Trim(strIn)) = 0 Then
StrCheck = True
Else
'validate whether non-empty string is numeric
StrCheck = objRegex.Test(strIn)
End If
End Function
I have in my Excel userform several checkboxes (in total 10) which all should be have the same behavior. It means, when I reload the form the value of the checkbox should change, depending on values from the spreadsheet with the actual selected row.
The following code is working properly for one checkbox:
If Sheet1.Cells(iRow, Sheets("aux").Range("B21")) = "X" Then
cbERW.Value = True
ElseIf Sheet1.Cells(iRow, Sheets("aux").Range("B21")) = "?" Then
cbERW.Value = Null
Else
cbERW.Value = False
End If
How can I do this in the easiest way for multiple checkboxes? But for every checkbox the referring row and column could change. For example, the next one look like this (cbHSAW instead of cbERW, Column B22 instead of B21):
If Sheet1.Cells(iRow, Sheets("aux").Range("B22")) = "X" Then
cbHSAW.Value = True
ElseIf Sheet1.Cells(iRow, Sheets("aux").Range("B22")) = "?" Then
cbHSAW.Value = Null
Else
cbHSAW.Value = False
End If
Anybody know how to do this easily?
You can wrap your test statement in a function and pass the range for the specific checkbox. Then set the return result can be as the value for the checkbox. The below needs to be modified for your form and ranges but will give the desired result.
'Function to test the value of the range
Private Function SetCheckBox(ByVal rng As Range)
If rng.Value = "X" Then
SetCheckBox = True
ElseIf rng.Value = "?" Then
SetCheckBox = Null
Else
SetCheckBox = False
End If
End Function
Private Sub UserForm_Initialize()
'Set value for each checkbox by passing its range to the function
CheckBox1.Value = SetCheckBox(Sheets("aux").Range("A1"))
CheckBox2.Value = SetCheckBox(Sheets("aux").Range("A2"))
CheckBox3.Value = SetCheckBox(Sheets("aux").Range("A3"))
CheckBox4.Value = SetCheckBox(Sheets("aux").Range("A4"))
End Sub