Return index of selected series - excel

is there a simple way to get the index of a selected line in a Excel chart using VBA? I have a chart, where the user selects a series. Then a macro should do some thing. I'm looking for something like idx = Selection.getIndex.
I need this idx to call other functions which do some stuff with the series using the index to select the specific series (e.g. FullSeriesCollection(idx).DataLabels.labelPos=...)

Three ideas for you:
1st- use object variable instead of index reference:
Dim SER As Series
Set SER = Selection
SER.anyproperty.anymethod... 'do your action here
2nd- use plot order to get...index (but not sure if this always match)?
Dim inxSer As Integer
inxSer = Selection.PlotOrder
3rd- read last parameter of series formula
Dim inxFromFormula As Integer
Dim tmpSerFormula As String
tmpSerFormula = Selection.Formula
tmpSerFormula = Mid(tmpSerFormula, InStrRev(tmpSerFormula, ",") + 1)
inxFromFormula = Left(tmpSerFormula, Len(tmpSerFormula) - 1)

You can utilize Chart object events to achieve that. If the Chart is embedded in a sheet, you need to declare it as 'WithEvents' variable first as described here:
Using Events with Embedded Charts
After that you can define SeriesChange handler with the following parameters:
Private Sub myChartClass_SeriesChange(ByVal SeriesIndex As Long, ByVal PointIndex As Long)
End Sub
EDIT: The above event is fired when user changes point value, you should use Select event instead.
myChartClass_Select(ByVal ElementID As Long, ByVal Arg1 As Long, ByVal Arg2 As Long)
Parameter description (the same as BeforeDoubleClick)

Related

Does MyRange.Cells(i,j) truly return an Excel.Range?

First things first, I emphasis I am under a VSTO / .Net environment, using Visual Studio 2017. No VBA here.
You have a MyRange as Excel.Range = something object and you want to refer to the top left cell of that range. Two options:
MyRange.Range("A1")
MyRange.Cells(1,1)
But then, although the documentation indicates .Cells() returns an Excel.Range object, Intellisense will not pick it up as such. For example, MyRange.Cells(1,1).Value2 is not readily available from the Intellisense drop down list. However, if you do manually type in .Value2, it will work no problem.
My question is: Is this some limitation from Visual Studio, or does this have some implication at compilation and subsequently at runtime?
First I started to use .Range("A1"), but that becomes quite unconvenient when dealing with dynamic references, such as an equivalent to .Cells(i,j).
So instead, I created my own Extension, which relies on implicit conversion. Is that ok? (second question)
Module RangeExtensions
''' <summary>
''' Returns a Range object that represents the cell in the specified range.
''' </summary>
<System.Runtime.CompilerServices.Extension()>
Public Function Cell(ByVal MyRange As Excel.Range, ByVal RowIndex As Long, ByVal ColumnIndex As Long) As Excel.Range
Return MyRange.Cells(RowIndex, ColumnIndex)
End Function
''' <summary>
''' Returns a Range object that represents the cell in the specified worksheet.
''' </summary>
<System.Runtime.CompilerServices.Extension()>
Public Function Cell(ByVal MySheet As Excel.Worksheet, ByVal RowIndex As Long, ByVal ColumnIndex As Long) As Excel.Range
Return MySheet.Cells(RowIndex, ColumnIndex)
End Function
End Module
This is no limitation of Visual Studio, but rather one of the properties on the System.Object type.
rangeReference.Cells is a property on the Range type that returns an Excel.Range object.
rangeReference.Cells(1,1) is a shortcut way of writing rangeReference.Cells.Item(1,1). Item is the default property on the Range object. Unfortunately, Item is defined as a Variant type within Excel and .Net represents the Variant type using the System.Object type. For Intellisense to recognize Item as a Range, it needs to be cast to a Range type.
Example:
Dim rng As Excel.Range = Sheet1.Range("A1:B4")
Dim rngCells As Excel.Range = rng.Cells
Dim specificCell As Object
specificCell = rngCells(1, 1)
' or
specificCell = rngCells.Item(1, 1)
Dim specificCellRange As Excel.Range = CType(specificCell, Excel.Range)
However, if you do manually type in .Value2, it will work no problem.
This implies that you are working with Option Strict Off which allows late-binding; the property is discovered at run-time. Late-binding does impose a performance hit in that Value2 must be discovered and and then retrieved. This is done via extra code that the compiler inserts to support the property retrieval.

How to call a user defined function in vba code

I created a Public function in Module two called "t_value". I now want to use this function in the VBA code for a userform, which uses the input from the userform.
This is the function:
Public Function t_value(theta As Variant)
Dim theta_1 As Integer, theta_2 As Integer
Dim A As Variant, B As Variant, s As Variant
theta_1 = Application.WorksheetFunction.Floor(theta, 5)
theta_2 = Application.WorksheetFunction.Ceiling(theta, 5)
A = theta - theta_1
B = theta_2 - theta_1
s = A / B
t_value = s
End Function
Here is the code I would like to use the function above in:
Private Sub Submit_Click()
Dim theta As Variant, alpha As Variant, t As Variant, u As Variant
theta = UserForm1.theta_input.Value
alpha = UserForm1.alpha_input.Value
t = Application.WorksheetFunction.t_value(theta)
End Sub
Normally "Application.WorksheetFunction.[function]" works, but it wouldn't work for me in this situation - I thought it may be due to the fact I created the formula. Would it be easier to just put the formula into the Sub? I was worried about runtime. I'm rather new, so I'm not completely familiar with VBA syntax.
Application.WorksheetFunction is a class defined in the Excel library; you can find it in the Object Browser (F2):
A public Function in a standard module is just a function that can be invoked from a worksheet cell (provided it doesn't have side-effects), just as well as from anywhere in the workbook's VBA project: you can't write any VBA code that "becomes a member" of a class that's defined in a library you're referencing.
So if you have a function called MyFunction in a module called Module1, you can invoke it like this:
foo = MyFunction(args)
Or like this:
foo = Module1.MyFunction(args)
So in this case:
t = t_value(theta)
Would it be easier to just put the formula into the Sub?
Nope, because a Sub won't return a value (however, you can pass variables ByRef):
Sub t_value(theta as variant, ByRef t as Variant)
Dim theta_1 As Integer, theta_2 As Integer
Dim A As Variant, B As Variant, s As Variant
theta_1 = Application.WorksheetFunction.Floor(theta, 5)
theta_2 = Application.WorksheetFunction.Ceiling(theta, 5)
A = theta - theta_1
B = theta_2 - theta_1
s = A / B
t = s '## Assign the value to the ByRef 't' variable and it should retain its value in the calling procedure
End Sub
Whether you choose to put this function in a module (Public) or in the user form module is a design decision that depends on whether you want the function to be generally available outside of the form instance(s). Whether you choose to make this function a sub is a bit different -- I'd probably recommend against it following the general best practice that Functions should return values and Subroutines should just perform actions and/or manipulate objects.
Directly use
t = t_value(theta)
, instead of
t = Application.WorksheetFunction.t_value(theta)

Passing arrays from VBA to VB.NET

I am working on a vb.net COM interop to work in Microsoft Excel and I am having trouble passing arrays from vb to vb.net. I have a PointPairs property in the vb.net code that I need to set from vb and I am having trouble passing the 2 dimensional array. I have tried both setting the property explicitly with a 2D array as well as passing two 1D arrays into a Sub to try and set the property in vb.net, but nothing I have tried seems to work.
vb.net code:
Public Property PointPairs() As Double(,)
Get
...
Return array
End Get
Set(ByVal Value(,) As Double)
...
End Set
End Property
Public Sub SetPointPairs(ByRef spline As Spline, ByRef xValues() As Double, _
ByRef yValues() As Double)
Dim Value(,) As Double
ReDim Value(1, UBound(xValues, 1))
For i As Integer = 0 To UBound(xValues, 1)
Value(0, i) = xValues(i)
Value(1, i) = yValues(i)
Next
spline.PointPairs = Value
End Sub
vb code:
Dim spline1 As New Spline
Dim points(), xValues(), yValues() As Double
'read input from excel cells into points() array/add x and y values to respective arrays
spline1.PointPairs = points 'first method (doesn't work)
Call SetPointPairs(spline1, xValues, yValues) 'second method (doesn't work)
Everything is being exported correctly by vb.net and the properties/subs/functions are visible in the Object Browser in vba, however when I try to pass arrays in these two approaches I get error messages Function or interfaces markes as restricted, or the function uses an automation type not supported in Visual Basic or Sub or Function not defined. I have also tried using <MarshalAs()> but I have never used it before and can't find much documentation on how to use it for passing arrays between vb and vb.net.
Thanks in advance for any suggestions or solutions
For anyone interested in the solution, I found this article that was exactly what I needed.
http://www.codeproject.com/Articles/12386/Sending-an-array-of-doubles-from-Excel-VBA-to-C-us?fid=247508&select=2478365&tid=2478365
I had to break up the 2D array into two 1D arrays of Doubles in VBA and pass them into vb.net as objects and modify them as outlined in the article. I changed the SetPointPairs Sub as follows and added this Private Function to convert from Object to Array in the .net code
Sub SetPointPairs(ByRef spline As CubicSpline, ByRef xValues As Object, ByRef yValues As Object) Implements iCubicSpline.SetPointPairs
Dim xDbls(), yDbls(), pointDbls(,) As Double
xDbls = ComObjectToDoubleArray(xValues)
yDbls = ComObjectToDoubleArray(yValues)
ReDim pointDbls(1, UBound(xDbls, 1))
For i As Integer = 0 To UBound(pointDbls, 2)
pointDbls(0, i) = xDbls(i)
pointDbls(1, i) = yDbls(i)
Next
spline.PointPairs = pointDbls
End Sub
Private Function ComObjectToDoubleArray(ByVal comObject As Object) As Double()
Dim thisType As Type = comObject.GetType
Dim dblType As Type = Type.GetType("System.Double[]")
Dim dblArray(0) As Double
If thisType Is dblType Then
Dim args(0) As Object
Dim numEntries As Integer = CInt(thisType.InvokeMember("Length", BindingFlags.GetProperty, _
Nothing, comObject, Nothing))
ReDim dblArray(numEntries - 1)
For j As Integer = 0 To numEntries - 1
args(0) = j
dblArray(j) = CDbl(thisType.InvokeMember("GetValue", BindingFlags.InvokeMethod, _
Nothing, comObject, args))
Next
End If
Return dblArray
End Function

Add hover labels to a scatter chart that has it's data range updated dynamically in Excel 2007

Hi I want to add labels to the plotted points on a scatter chart in Excel, however my charts data set range changes whenever my macro updates it... so my first question is:
Is there a way to set the data range of an Add-in such as the one below "Chart Hover Label" in VBA?
Recording a macro did nothing (my fingers were crossed to begin with).
Here is a list of other chart add-ins I know of, from what I know only 1 of these allows you to show ONLY the label when you hover over the plotted point.. I have also not seen one that allows you to show the data range on click of the point.
This is the add-in that allows allows you to show only on the hover:
http://www.tushar-mehta.com/excel/software/chart_hover_label/index.html
These are the other 2 I know of:
http://www.appspro.com/Utilities/ChartLabeler.htm
http://spreadsheetpage.com/index.php/file/j_walk_chart_tools_add_in/
Does anyone know of any other chart add-ins for Excel (preferably free) that give more options? and can be updated via VBA?
Thanks for any help.
I don't know about the add-ins but alot can be done in VBA with chart interactions. Just insert a chart sheet and enter the below code into that sheet in VBA.
Here is an example I have in a working graph of mine. When I click on a series it will create a text box and populate it with text in a cell that is updated in the code below. Its just for the series name, but you can add more functionality to it.
Private Sub Chart_MouseDown(ByVal Button As Long, ByVal Shift As Long, ByVal x As Long, ByVal y As Long)
Dim ElementID As Long, Arg1 As Long, Arg2 As Long
Dim chart_data As Variant, chart_label As Variant
Dim last_bar As Long, chrt As Chart
Dim ser As Series, Txt As String
On Error Resume Next 'Sorry for this line of code, I haven't had the chance to look into why it was needed.
Me.GetChartElement x, y, ElementID, Arg1, Arg2
Set chrt = ActiveChart
Set ser = ActiveChart.SeriesCollection(1)
chart_data = ser.Values
chart_label = ser.XValues
Set txtbox = ActiveSheet.Shapes("hover") 'I suspect in the error statement is needed for this.
If ElementID = xlSeries Then
txtbox.Delete
Sheet1.Range("Ch_Series").Value = Arg1
Txt = Sheet1.Range("CH_Text").Value
Set txtbox = ActiveSheet.Shapes.AddTextbox _
(msoTextOrientationHorizontal, x - 150, y - 150, 150, 40)
txtbox.Name = "hover"
txtbox.Fill.Solid
txtbox.Fill.ForeColor.SchemeColor = 9
txtbox.Line.DashStyle = msoLineSolid
chrt.Shapes("hover").TextFrame.Characters.Text = Txt
With chrt.Shapes("hover").TextFrame.Characters.Font
.Name = "Arial"
.Size = 12
.ColorIndex = 16
End With
ser.Points(Arg2).Interior.ColorIndex = 44
txtbox.Left = x - 150
txtbox.Top = y - 150
Else
txtbox.Delete
ser.Interior.ColorIndex = 16
End If
End Sub
But you can also do the below for a hover function.
Private Sub Chart_MouseMove(ByVal Button As Long, ByVal Shift As Long, ByVal x As Long, ByVal y As Long)
Remember the code needs to be inserted into the Chart sheet and not in a module.
As for your data range fitting the graph, have you tried dynamic named ranges and then set the graph to reference the named range?
You could set the MouseMove function to display what you want, then on MouseDown it can navigate to the selected series data range.
Hope this helps.

Required field handling Excel VBA

I have an application I have created in Excel VBA. This application requires a lot of entries from the user. All the fields/cells are required to have values before submission. What will be the best way to check that the users entered values in every single field/cell. I could do simple IF satatements, but that will take forever to write the tons of IFs that I will need. Is there a more efficient way to do it?
You will need to use if statements. You can speed the process up by creating a helper function. This one checks for empty and numeric data only. It can be modified to look for dates and text easily
Function ValidCell(RowPos As Long, ColPos As Long) As Boolean
Dim ws As Worksheet
Dim v As Variant
Set ws = ActiveSheet
v = ws.Cells(RowPos, Colpos).Value
If IsEmpty(v) Or Not IsNumeric(v) Then
ValidCell = False
Else
ValidCell = True
End If
End Function

Resources