How to automatically arrange shapes automatically drawn by loop? - vba

I have a loop that generates rectangles automatically on a visio drawing but I need to have the script to arrange them automatically right know I recorded a macro while I rearranged the rectangles manually under the title box. But my rectangle count change constantly because the results from my if statement change constantly because my data continuously changes. I need for my loop to start drawing them under the title box in columns of six or seven rectangles.
For I = 1 To WS_Count
Set vsoShape =
Application.ActiveWindow.Page.Drop(Application.DefaultRectangleDataObject,
aoffset, boffset)
vsoShape.Text = ActiveWorkbook.Worksheets(I).Name
aoffset = aoffset
boffset = boffset + 0.75
Dev_Count = Dev_Count + 1
ActiveDocument.DiagramServicesEnabled = DiagramServices
Next I
I need to be able to set a starting position to begin dropping the rectangles below the title rectangle creating a new column every six to seven rectangles. Thanks

Increment aOffset every time I is divisible by the number of shapes you want horizontally...
You can do this with the Mod Operator If (iterator Mod runEveryXIterations = 0) Then ...
The example below should clarify the idea, the code is not exactly what you need but you should be able to grasp the idea:
Option Explicit
Public Sub printXY()
xyDistribute 10, 3, 0, 0, 0.75, 1.5
End Sub
Private Function xyDistribute(ByRef iterations As Long, _
ByRef newColAfter As Long, _
ByRef xPosInitial As Double, _
ByRef yPosInitial As Double, _
ByRef xStep As Double, _
ByRef yStep As Double)
Dim iter As Long
Dim xPos As Double
Dim yPos As Double
yPos = yPosInitial
xPos = xPosInitial
Debug.Print "xPos", "yPos"
For iter = 1 To iterations
Debug.Print xPos, yPos
' your code goes here
If (iter Mod newColAfter = 0) Then
yPos = yPos + yStep
xPos = xPosInitial
Else
xPos = xPos + xStep
End If
Next iter
End Function

Related

Chart not plotting data correctly (Visual Basic)

I am having problems with a project which should show the acceleration of a simple pendulum swinging over time. It should model a negative cosine curve, and I have the X and Y axis set so that the cosine curve will peak at the top of the chart area. However, it does not do this. For example, the maximum acceleration is calculated to be 17.746 m/s^2, however on the chart it says the maximum is around 1.71 m/s^2. Screenshot attached below.
https://drive.google.com/file/d/1AcmIDXsJIbzihH8Nq4mgSn1CofJCkQ6T/view?usp=sharing
I have wondered whether my maths are wrong or whether the problem lies somewhere in the chart. I have two other charts working, displacement and velocity of the pendulum, so I am confused as to why this will not work, especially as the coding behind each different chart is almost identical. I will attach all necessary code beneath.
Private Function CalculateAcceleration(ByVal amplitude As Double, ByVal frequency As Double, ByVal time As Double) As Double
Dim acceleration, maxAcc As Double
maxAcc = sq((2 * PI * frequency)) * amplitude
acceleration = -(maxAcc * Cos(2 * PI * frequency * time))
Return acceleration
End Function
Private Sub CreateAccelerationGraph(ByVal angularSpeed As Double, ByVal amplitude As Double, ByVal timePeriod As Double, ByVal frequency As Double)
If AccelerationChart.Series("Acceleration").Points.Count <> 0 Then
AccelerationChart.Series("Acceleration").Points.Clear()
End If
Dim maxAcceleration As Double = sq(angularSpeed) * amplitude
With AccelerationChart.ChartAreas("Default")
.AxisX.Minimum = 0
.AxisX.Maximum = (1 * timePeriod)
.AxisY.Minimum = -(maxAcceleration)
.AxisY.Maximum = maxAcceleration
End With
Dim increment As Double = (1 * timePeriod) / 100
For time As Double = 0 To (1 * timePeriod) Step increment
Dim xPos, yPos As Double
yPos = CalculateAcceleration(amplitude, frequency, time)
xPos = time
AccelerationChart.Series("Acceleration").Points.AddXY(xPos, yPos)
Dim label As New CalloutAnnotation
With label
AccelerationChart.Annotations.Add(label)
End With
Next
End Sub
Private Sub SetUpAccelerationGraph()
Dim innerPlotPosition As ElementPosition = New ElementPosition(10, 10, 80, 80)
AccelerationChart.Series.Add("Acceleration")
AccelerationChart.ChartAreas.Add("Default")
With AccelerationChart.Series("Acceleration")
.Color = Color.BlueViolet
.BorderWidth() = 5
.ChartType = DataVisualization.Charting.SeriesChartType.Spline
End With
With AccelerationChart.ChartAreas("Default")
.IsSameFontSizeForAllAxes = True
.InnerPlotPosition = innerPlotPosition ' sets the inner plot position
End With
AccelerationChart.ChartAreas.FirstOrDefault.AxisX.LabelStyle.Format = "{0:0.0}" 'sets the format of labels to 1dp
AccelerationChart.ChartAreas.FirstOrDefault.AxisY.LabelStyle.Format = "{0:0.000}"
End Sub

mouse controlled by an IMU

I am using pitch and yaw angle from an IMU to move a mouse pointer for PC in vb.net.
Unfortunately it doesn't work steady.
First I set the max angles which should be performed
LOAD-Event
'set max angles in degree
maxAngle_YawRight = 30
maxAngle_YawLeft = -30
maxAngle_PitchUp = 20
maxAngle_PitchDown = -20
calculateCurParameter(maxAngle_YawRight, Panel_Right.Location.X, maxAngle_YawLeft, Panel_Left.Location.X + Panel_Left.Width, maxAngle_PitchUp, Panel_Top.Location.Y+Panel_Top.Height, maxAngle_NickDown, Panel_Bottom.Location.Y)
I use four these max angles (pitch_up, pitch_down, yaw_right, yaw_left) to assign them to a part of my form, to get 4 points, e.g. (pitch_up,panel_2.location.Y)
pitch_up and pitch_down are used for the y coordinate of the mouse pointer and yaw_right and yaw_left are used for the x coordinate. This is done by function calculateCurParameter.
Then I use this four points to calculate parameters for an linear equation:
CursorPos=m*angle+b
So you get for every new angle a new Cursor Position.
Private mX As Double
Private bX As Double
Private mY As Double
Private bY As Double
...
Private Sub calculateCurParameter(ByVal maxYawRight As Double, ByVal rightBorder As Double, ByVal maxYawLeft As Double, ByVal leftBorder As Double, ByVal maxPitchUp As Double, ByVal topBorder As Double, ByVal maxPitchDown As Double, ByVal bottomBorder As Double)
'this function calculates the parameter for Ycur=m*Xangle+b for each Pitch and Yaw
'calculate the parameter for the CursorPosition X
dim deltaX= maxYawLeft-maxYawRight
dim deltaY=leftBorder-rightBorder
mX=deltaY/deltaX
bX=rightBorder-mX*maxYawRight
'calculate the parameter for the CursorPosition Y
deltaX=bottomBorder-topBorder
deltaY=maxPitchDown-maxPitchUp
mY=deltaY/deltaX
bY=topBorder-mY*maxPitchUp
end Sub
After that calculation, I use a quadratic weighted moving average (QWMA) as a low pass filter cursor outputs calculated by the function y=m*x+b.
For that moving average I use the last 50 samples of each angle.
After that I put the two angles in each linear equation(cursorPos=m*angle+b) as described.
'Timer-Event which is triggered each 8ms
' yaw and pitch are in degrees and are updated in this timer event before
Dim xCoordCur = Math.Round(mX * yaw + bX)
Dim yCoordCur = Math.Round(mY * pitch + bY)
arrayCurY(cursorCounter) = yCoordCur
arrayCurX(cursorCounter) = xCoordCur
If cursorCounter =50 Then 'need 50 samples to do the QWMA
Dim aqwmY = qwma_calculating(arrayCurY)'function to calculate the QWMA, seems to be working
Dim aqwmX = qwma_calculating(arrayCurX)
'the mouse pointer should not leave the form
If aqwmX > Panel_Right.Location.X Then
aqwmX = Panel_Right.Location.X
ElseIf aqwmX < (Panel_Left.Location.X + Panel_Left.Width) Then
aqwmX = Panel_Left.Location.X + Panel_Left.Width
End If
If aqwmY > Panel_Bottom.Location.Y Then
aqwmY = Panel_Bottom.Location.Y
ElseIf aqwmY < (Panel_Top.Location.Y + Panel_Top.Height) Then
aqwmY = Panel_Top.Location.Y + Panel_Top.Height
End If
'Set the new Cursor Position
Cursor.Position = New Point(aqwmX, aqwmY)
arrayValuesMoving(arrayCurX)'function to move the values one index forward
arrayValuesMoving(arrayCurY)
Else
cursorCounter += 1
End If
In the my last step I set the new Cursor-Position
Cursor.Position=New Point(xCoor,yCoor)
Now I can control the mouse pointer by moving the IMU, but it is very unstable.
For example the mouse pointer still moves although the IMU doesn't move.
It's even impossible to hover over some form elements.
What did I do wrong?
Thanks in advance!
I added the follwoing code:
If xCoor > Cursor.Position.X + 3 Or xCoor < Cursor.Position.X - 3 Then
istxCoor = True
Else
istxCoor = False
End If
If yCoor > Cursor.Position.Y + 3 Or yCoor < Cursor.Position.Y - 3 Then
istyCoor = True
Else
istyCoor = False
End If
If istxCoor And istyCoor Then
SetCursorPos(xCoor, yCoor)
ElseIf istxCoor Then
SetCursorPos(xCoor, Cursor.Position.Y)
ElseIf istyCoor Then
SetCursorPos(Cursor.Position.X, yCoor)
End If
it is now more steady but it has now a time lag.

Creating a repeating line through looping by giving the x and y coordinates

I am experimenting on the paint event of VB.Net, and for this experiment I would like to create a repeating horizontal or vertical (depending on the parameter that I inputted) line and loop through until it meets the corresponding end point x and y.
Something like this:
What I'm trying to achieve is given the x and y start point and x and y end point the function should create either vertical or horizontal line that starts with the given start point until it reaches the given end point.
I can create curveline and straightline using the paintevent, but right now I don't have any idea on how to perform looping in the given x and y start point and end point.
You just need to use a For loop to iterate the x/y coordinates. Here's an example:
Public Class Form1
Private Enum Orientation
Vertical
Horizontal
End Enum
Protected Overrides Sub OnPaint(e As PaintEventArgs)
Dim orient As Orientation = Orientation.Vertical
Dim x As Integer = 100 'X Coord
Dim y As Integer = 100 'Y Coord
Dim count As Integer = 10 'Number of Lines to draw
Dim spacing As Integer = 5 'Spacing between lines in pixels
Dim length As Integer = 20 'Length of each line in pixels
Dim thickness As Integer = 3 'Thickness of each line in pixels
drawLines(x, y, orient, count, spacing, length, thickness, e.Graphics)
End Sub
Private Sub drawLines(x As Integer, y As Integer, orient As Orientation, count As Integer, spacing As Integer, length As Integer, thickness As Integer, g As Graphics)
'Create the Pen in a using block so it will be disposed.
'The code uses a red pen, you can use whatever color you want
Using p As New Pen(Brushes.Red, CSng(thickness))
'Here we iterate either the x or y coordinate to draw each
'small segment.
For i As Integer = 0 To count - 1
If orient = Orientation.Horizontal Then
g.DrawLine(p, x + ((thickness + spacing) * i), y, x + ((thickness + spacing) * i), y + length)
Else
g.DrawLine(p, x, y + ((thickness + spacing) * i), x + length, y + ((thickness + spacing) * i))
End If
Next
End Using
End Sub
End Class
Have you tried something like:
For x = xstart to xend Step Spacing
Next
Where:
xstart = your start point
xend = your end point
Spacing = distance between lines

Excel VBA: what is the transforming formula given {X;Y} pair of Chart.MouseDown event to a {Category;Value} pair?

I am trying to find formula for transforming X and Y coordinates of MouseDown/MouseUp event of Chart to Axes's coordinate system, i.e to a pair {Category;Value}. Excel Object Model Reference for Chart.MouseDown event says:
The X coordinate of the mouse pointer in chart object client coordinates.
I've found useful methods and properties for the task:
Axis: Left, Width
MouseDown/MouseUp: x, y
ChartObject: Left, Width
Application: CentimetersToPoints, InchesToPoints, MeasurementUnit
Window: ActivePane, PointsToScreenPixelsX, PointsToScreenPixelsY
Pane: PointsToScreenPixelsX, PointsToScreenPixelsY
I've also found that X an Y values of a "mouse" event are in pixels -- see test:
Private Sub m_target_MouseDown(ByVal Button As Long, ByVal Shift As Long, ByVal x As Long, ByVal y As Long)
Dim v_l As Long, v_r As Long
'X and Y are in pixels!
With m_target
v_l = ActiveWindow.ActivePane.PointsToScreenPixelsX(.Parent.Left)
v_r = ActiveWindow.ActivePane.PointsToScreenPixelsX(.Parent.Left + .Parent.Width)
Debug.Print "X="; x; "Y="; y; "Xc="; x / 12 * 9; "Yc="; y / 12 * 9; VBA.chr$(13); _
"xlCategory: Left="; .Axes(xlCategory).Left; " Top="; .Axes(xlCategory).Top; " Width="; .Axes(xlCategory).Width; VBA.chr$(13); _
"xlValue: Left="; .Axes(xlValue).Left; " Top="; .Axes(xlValue).Top; " Width="; .Axes(xlValue).Width; VBA.chr$(13); _
"PlotArea: Left="; .PlotArea.Left; " Top="; .PlotArea.Top; " Width="; .PlotArea.Width; " Height="; .PlotArea.Height; VBA.chr$(13); _
"PlotArea: InsideLeft="; .PlotArea.InsideLeft; " InsideTop="; .PlotArea.InsideTop; " InsideWidth="; .PlotArea.InsideWidth; " InsideHeight="; .PlotArea.InsideHeight; VBA.chr$(13); _
"ChartObject: Left="; .Parent.Left; " Top="; .Parent.Top; ", Width="; .Parent.Width; " Height="; .Parent.Height; VBA.chr$(13); _
"ActiveWindow: Caption="; ActiveWindow.Caption; " Left="; ActiveWindow.Left; " UsableWidth="; ActiveWindow.UsableWidth; " Width="; ActiveWindow.Width; VBA.chr$(13); _
"ChartObject: Left_px="; ActiveWindow.ActivePane.PointsToScreenPixelsX(.Parent.Left); " Width_px="; v_r - v_l
End With
End Sub
The ratio 9/12 was found in "Excel VBA: Why is so big difference between values returned by PointsToScreenPixelsX for Window and for Pane?"
Clicking right border of a chart gives the following output:
X= 1838 Y= 220 Xc= 1378.5 Yc= 165
xlCategory: Left= 56 Top= 186 Width= 1286
xlValue: Left= 35 Top= 2 Width= 21
PlotArea: Left= 34.7092125984252 Top=-4 Width= 1320.84480314961 Height= 210
PlotArea: InsideLeft= 55.6250393700787 InsideTop= 2.10251968503937 InsideWidth= 1286.32645669291 InsideHeight= 183.845826771654
ChartObject: Left= 132.374954223633 Top= 0 , Width= 1377.72351074219 Height= 210
ActiveWindow: Caption=Q99708-VSSPVFGATSSIK-DC-1147-DC-r0001-dn.xlsx Left=-2 UsableWidth= 1279.5 Width= 1298.25
ChartObject: Left_px=-310 Width_px= 1837
Width_px is close to X and calculated as v_r - v_l using converted left (v_l) and right (v_r) border to pixels by ActiveWindow.ActivePane.PointsToScreenPixelsX.
But how to find exact formula to get {Category;Value} from {X;Y} which should work for any resolution (pixels per inch), MeasurementUnit, Pane.Left/Width, Chart.Left/Width, PlotArea.Left/Width and Axis.Left/Width?
I've found the formula. The following surprises considerably complicated this:
ChartArea.Width <> ChartObject.Width and ChartArea.Height <> ChartObject.Height!
It is possible to move a shape in chart coordinate system out of positive coordinates.
I've created a line shape in a chart and moved line to the left constraint of the chart. And line’s left was -4! Then I've tried to set the Shape.Left property to -4. This does not work: its value becomes 0 immediately after assignment. But the Shape.IncrementLeft method successfully moved line to the left constraint of a chart (-4). Similarly the Shape.IncrementTop method changes a Shape.Top property value. As a result a transformation from pixels to points was found.
Then with experiments I've found that PloatArea.Inside<Left,Top,Width,Height> must be used instead of corresponding properties of Axis to transform {X;Y} pair in points to a {Category;Value} pair.
First, find shift of ChartArea object in a ChartObject using movement of line shape to left and top constraints:
Sub extract_transform(ByVal p_chart As Chart, ByRef p_sh_x As Double, ByRef p_sh_y As Double)
Dim v_ScreenUpdating As Boolean, v_sh As Double
With p_chart.Shapes.AddShape(msoLine, 0, 0, 20, 0)
p_sh_x = 0: .IncrementLeft -10
While p_sh_x <> .Left
p_sh_x = .Left
.IncrementLeft -10 'Move to the left constaint
Wend
p_sh_y = 0: .IncrementTop -10
While p_sh_y <> .Top
p_sh_y = .Top
.IncrementTop -10 'Move to the top constaint
Wend
.delete 'Collecting garbage
End With
End Sub
Using found {left;top} of ChartArea object in Chart coordinate system a transformation of points to pixels is performed as follows:
Sub pixels2points(ByVal p_pane As Pane, ByVal x_pi As Long, ByVal y_pi As Long, ByRef x_pt As Double, ByRef y_pt As Double)
Dim v_sc As Double
With p_pane
v_sc = (.PointsToScreenPixelsX(1000) - .PointsToScreenPixelsX(0)) / 1000
x_pt = m_sh_x + x_pi / v_sc
v_sc = (.PointsToScreenPixelsY(1000) - .PointsToScreenPixelsY(0)) / 1000
y_pt = m_sh_y + y_pi / v_sc
End With
End Sub
The pixels2points assumes x_pi and y_pi are in the chart coordinate system in pixels.
m_sh_x and m_sh_y are of type Double and are members of a class c_report.
p_pane is a key object: only Pane.PointsToScreenPixels<X,Y> methods transform points to pixels and it depends on scaling factor of a window. Also the same sheet may appear in many windows, hence the corresponding Pane object must be used.
To transform {X;Y} pair in points to a {Category;Value} pair a chart wrapper class has the following method:
Sub pt2axes(ByVal p_axes As Axes, ByVal p_pa As PlotArea, ByVal x_pt As Double, ByVal y_pt As Double, ByRef p_cat As Double, ByRef p_val As Double)
With p_axes(xlCategory)
p_cat = (x_pt - p_pa.InsideLeft) / p_pa.InsideWidth * (.MaximumScale - .MinimumScale) + .MinimumScale
End With
With p_axes(xlValue)
p_val = ((p_pa.InsideTop + p_pa.InsideHeight) - y_pt) / p_pa.InsideHeight * (.MaximumScale - .MinimumScale) + .MinimumScale
End With
End Sub
Transformation formula for Y assumes that the origin of coordinates is at the bottom of a chart.
And now the application of these methods in handling the Chart.MouseDown event:
Private Sub m_target_MouseDown(ByVal Button As Long, ByVal Shift As Long, ByVal x As Long, ByVal y As Long)
Dim v_x As Double, v_y As Double, v_cat As Double, v_val as Double
With m_target
Debug.Print "X="; x; "Y="; y
m_report.pixels2points ActiveWindow.ActivePane, x, y, v_x, v_y
pt2axes .Axes, .PlotArea, v_x, v_y, v_cat, v_val
Debug.Print "cat="; v_cat; "val="; v_val
End With
End Sub
m_target class member is of type Chart. m_report class member is of type c_report and serves as a reference to a parent object which has the pixels2points method.
To test formulas a wide chart, frozen panes and scrolls at scale 400% were used and points at crossing grid lines with visible coordinates {270;2} and {10;6} were clicked. As a result the output of the Immediate window is:
X= 6980 Y= 849
cat= 270.030767164932 val= 2.00522623783961
X= 564 Y= 522
cat= 9.97421024128619 val= 6.00822155418118
I suppose the difference +/-0.03 for Category axis occurred because of pixels are discrete and the vertical grid line step had not integer value in pixels.
Below is some code I created starting with code from bettersolutions.com
It is an approach I just developed today using .GetChartElement to identify the pixel coordinates where the horizontal and vertical axes are located. Once those are found, the rest is fairly straightforward. In my testing it is accurate, on both chart sheets and embedded charts, and does in fact find the pixels coincident with the axes. The key is to work from the inside out to find each axis.
First code block goes in a Class module named EventClass.
Second code block goes in a standard module.
Public WithEvents ExcelChartEvents As Excel.Chart
Private Sub ExcelChartEvents_MouseMove( _
ByVal Button As Long, _
ByVal Shift As Long, _
ByVal x As Long, _
ByVal y As Long)
Dim oAxis As Excel.Axis
Dim chtX As Double
Dim chtY As Double
Dim pixAtVertAxis As Long, pixAtHorzAxis As Long
Dim pts2pix#, pix2pts#, pts2chtX#, pts2chtY#
pix2pts = PointsPerPixel() / (ActiveWindow.Zoom / 100)
pts2pix = 1 / pix2pts
GetPixAtHorizontalAndVerticalAxes pixAtVertAxis, pixAtHorzAxis
Set oAxis = ActiveChart.Axes(xlCategory)
pts2chtX = (oAxis.MaximumScale - oAxis.MinimumScale) / ActiveChart.PlotArea.InsideWidth
chtX = oAxis.MinimumScale + (x - pixAtVertAxis) * pix2pts * pts2chtX
Set oAxis = ActiveChart.Axes(xlValue)
pts2chtY = (oAxis.MaximumScale - oAxis.MinimumScale) / ActiveChart.PlotArea.InsideHeight
chtY = oAxis.MinimumScale - (y - pixAtHorzAxis) * pix2pts * pts2chtY
Application.StatusBar = "(" & Format(chtX, "#.000") & " , " & Format(chtY, "#.000") & ")"
If (Shift = 1) Then
'do this when shift key is held down
Dim chtID&, arg1&, arg2&
ActiveChart.GetChartElement x, y, chtID, arg1, arg2
Application.StatusBar = "(pxX=" & Format(x, "000") & " , pxY=" & Format(y, "000") & ")" _
& " (chtX=" & Format(chtX, "#.000") & " , chtY=" & Format(chtY, "#.000") & ")" _
& " chartID=" & chtID
End If
End Sub
second code block:
Private Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function GetDeviceCaps Lib "gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
Private Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hDC As Long) As Long
Private Const LOGPIXELSX = 88 'Pixels/inch in X
'A point is defined as 1/72 inches
Private Const POINTS_PER_INCH As Long = 72
Public oChart As EventClass
Public Sub StartCursorReadout()
'run this to start readout of the mouse location in chart coordinates
If ActiveChart Is Nothing Then MsgBox "First select a Chart or go to a ChartSheet.": Exit Sub
Set oChart = Nothing
Set oChart = New EventClass
Set oChart.ExcelChartEvents = ActiveChart
End Sub
Public Function PointsPerPixel() As Double
'Return the number of Excel points equal to one pixels. Usually this is 0.6
Dim hDC As Long
Dim lDotsPerInch As Long
hDC = GetDC(0)
lDotsPerInch = GetDeviceCaps(hDC, LOGPIXELSX)
PointsPerPixel = POINTS_PER_INCH / lDotsPerInch
ReleaseDC 0, hDC
End Function
Public Sub GetPixAtHorizontalAndVerticalAxes(ByRef pixAtVertAxis As Long, ByRef pixAtHorzAxis As Long)
Dim x&, y&, chtID&, arg1&, arg2&, lastchtID&
Dim pts2pix As Double, success As Boolean
pts2pix = 1 / (PointsPerPixel() / (ActiveWindow.Zoom / 100))
With ActiveChart
success = False: lastchtID = 0
For y = Int((.PlotArea.InsideTop + .PlotArea.InsideHeight) * pts2pix) - 20 To Int((.PlotArea.InsideTop + .PlotArea.InsideHeight) * pts2pix) - 200 Step -10
For x = Int(.PlotArea.InsideLeft * pts2pix) + 20 To Int(.PlotArea.InsideLeft * pts2pix) Step -1
.GetChartElement x, y, chtID, arg1, arg2
If chtID = xlAxis Then success = True: Exit For
If chtID = xlChartArea And lastchtID = xlPlotArea Then
success = True: x = x + 1: Exit For
End If
lastchtID = chtID
Next x
If success Then pixAtVertAxis = x: Exit For
Next y
success = False: lastchtID = 0
For x = Int(.PlotArea.InsideLeft * pts2pix) + 50 To Int(.PlotArea.InsideLeft * pts2pix) + 200 Step 10
lastchtID = 0
For y = Int((.PlotArea.InsideTop + .PlotArea.InsideHeight) * pts2pix) - 20 To Int((.PlotArea.InsideTop + .PlotArea.InsideHeight) * pts2pix) + 20 Step 1
.GetChartElement x, y, chtID, arg1, arg2
If chtID = xlAxis Then success = True: Exit For
If chtID = xlChartArea And lastchtID = xlPlotArea Then
success = True: y = y - 1: Exit For
End If
lastchtID = chtID
Next y
If success Then pixAtHorzAxis = y: Exit For
Next x
End With
End Sub

Grouping and naming shapes in Excel with vba

In Excel vba, I am creating two shapes in excel using vba. An arrow, which I name "aro" + i, and a textbox, which I name "text" + i, where i is a number indicating the number of a photograph.
So, say for photograph 3 I will creat arrow "aro3" and textbox "text3".
I then want to group them and rename that group "arotext" + i, so "arotext3" in this instance.
So far I have been doing the grouping and renaming like this:
targetSheet.shapes.Range(Array(Arrow.Name, textBox.Name)).Select
Selection.group
Selection.Name = "AroTxt" & Number
which works splendidly in a sub, but now I want to change this into a function and return the named group, so I tried something like this:
Dim arrowBoxGroup as Object
set arrowBoxGroup = targetSheet.shapes.Range(Array(Arrow.Name, textBox.Name))
arrowBoxGroup.group
arrowBoxGroup.Name = "AroTxt" & Number
I run into problems when I create a new group which has the same name as one which has already been created. So, if I create a second "aro3" and "text3" and then try to group them and rename the group to "arotext3" I get an error because a group with the same name is already present.
The thing I don't understand is that when I did this using the method referring to the selection, I could rename every group with the same name if I wanted and wouldn't get an error. Why does it work when referring to the Selection object, but fails when trying to use an assigned object?
UPDATE:
Since somebody asked, the code I have so far is below. arrow and textbox are an arrow and a textbox which point into a direction arbitrarily defined by the user using a form.
This then creates an arrow at the correct angle on the target worksheet and places a textbox with the specified number (also through the form) at the end of the arrow, so that it effectively forms a callout. I know that there are callouts, but they don't do what I want so I had to make my own.
I have to group the textbox and arrow because 1) they belong together, 2) I keep track of which callouts have already been placed using the group's name as a reference, 3) the user has to place the callout in the right location on a map which is embedded in the worksheet.
So far I have managed to make this into a function by making the return value a GroupObject. But this still relies on Sheet.Shapes.range().Select, which in my opinion is a very bad way of doing this. I am looking for a way which does not rely on the selection object.
And I would like to understand why this works when using selection, but fails when using strong typed variables to hold the objects.
Public Function MakeArrow(ByVal No As Integer, ByVal angle As Double, ByVal size As ArrowSize, ByVal ArrowX As Double, ByVal ArrowY As Double, ByVal TargetInternalAngle As Double, ByRef targetSheet As Worksheet) As GroupObject
Dim Number As String
Dim fontSize As Integer
Dim textboxwidth As Integer
Dim textboxheight As Integer
Dim arrowScale As Double
Dim X1 As Double
Dim Y1 As Double
Dim X2 As Double
Dim Y2 As Double
Dim xBox As Double
Dim yBox As Double
Dim testRange As Range
Dim arrow As Shape
Dim textBox As Shape
' Dim arrowTextbox As ShapeRange
' Dim arrowTextboxGroup As Variant
Select Case size
Case ArrowSize.normal
fontSize = fontSizeNormal
arrowScale = arrowScaleNormal
Case ArrowSize.small
fontSize = fontSizeSmall
arrowScale = arrowScaleSmall
Case ArrowSize.smaller
fontSize = fontSizeSmaller
arrowScale = arrowScaleSmaller
End Select
arrowScale = baseArrowLength * arrowScale
'Estimate required text box width
Number = Trim(CStr(No))
Set testRange = shtTextWidth.Range("A1")
testRange.value = Number
testRange.Font.Name = "MS P明朝"
testRange.Font.size = fontSize
shtTextWidth.Columns(testRange.Column).EntireColumn.AutoFit
shtTextWidth.Columns(testRange.row).EntireRow.AutoFit
textboxwidth = testRange.Width * 0.8
textboxheight = testRange.Height * 0.9
testRange.Clear
'Make arrow
X1 = ArrowX
Y1 = ArrowY
X2 = X1 + arrowScale * Cos(angle)
Y2 = Y1 - arrowScale * Sin(angle)
Set arrow = AddArrow(X1, Y1, X2, Y2, Number, targetSheet)
'Make text box
Set textBox = Addtextbox(angle, Number, fontSize, X2, Y2, textboxwidth, textboxheight, TargetInternalAngle, targetSheet)
'Group arrow and test box
targetSheet.shapes.Range(Array(arrow.Name, textBox.Name)).group.Select
Selection.Name = "AroTxt" & Number
Set MakeArrow = Selection
' Set arrowTextbox = targetSheet.shapes.Range(Array(arrow.Name, textBox.Name))
' Set arrowTextboxGroup = arrowTextbox.group
' arrowTextboxGroup.Name = "AroTxt" & Number
'
' Set MakeArrow = arrowTextboxGroup
End Function
Private Function AddArrow(ByVal StartX As Double, ByVal StartY As Double, ByVal EndX As Double, ByVal EndY As Double, ByVal Number As String, ByRef targetSheet As Worksheet) As Shape
Set AddArrow = targetSheet.shapes.AddLine(StartX, StartY, EndX, EndY)
With AddArrow
.Name = "Aro" & Number
With .Line
.BeginArrowheadStyle = msoArrowheadTriangle
.BeginArrowheadLength = msoArrowheadLengthMedium
.BeginArrowheadWidth = msoArrowheadWidthMedium
.ForeColor.RGB = RGB(0, 0, 255)
End With
End With
End Function
Private Function Addtextbox(ByVal angle As Double, ByVal Number As String, ByVal fontSize As Integer, ByVal arrowEndX As Double, ByVal arrowEndY As Double, ByVal Width As Integer, ByVal Height As Integer, ByVal LimitAngle As Double, ByRef targetSheet As Worksheet) As Shape
Dim xBox, yBox As Integer
Dim PI As Double
Dim horizontalAlignment As eTextBoxHorizontalAlignment
Dim verticalAlignment As eTextBoxVerticalAlignment
PI = 4 * Atn(1)
If LimitAngle = 0 Then
LimitAngle = PI / 4
End If
Select Case angle
'Right
Case 0 To LimitAngle, 2 * PI - LimitAngle To 2 * PI
xBox = arrowEndX
yBox = arrowEndY - Height / 2
horizontalAlignment = eTextBoxHorizontalAlignment.left
verticalAlignment = eTextBoxVerticalAlignment.Center
'Top
Case LimitAngle To PI - LimitAngle
xBox = arrowEndX - Width / 2
yBox = arrowEndY - Height
horizontalAlignment = eTextBoxHorizontalAlignment.Middle
verticalAlignment = eTextBoxVerticalAlignment.Bottom
'Left
Case PI - LimitAngle To PI + LimitAngle
xBox = arrowEndX - Width
yBox = arrowEndY - Height / 2
horizontalAlignment = eTextBoxHorizontalAlignment.Right
verticalAlignment = eTextBoxVerticalAlignment.Center
'Bottom
Case PI + LimitAngle To 2 * PI - LimitAngle
xBox = arrowEndX - Width / 2
yBox = arrowEndY
horizontalAlignment = eTextBoxHorizontalAlignment.Middle
verticalAlignment = eTextBoxVerticalAlignment.top
End Select
Set Addtextbox = targetSheet.shapes.Addtextbox(msoTextOrientationHorizontal, xBox, yBox, Width, Height)
With Addtextbox
.Name = "Txt" & Number
With .TextFrame
.AutoMargins = False
.AutoSize = False
.MarginLeft = 0#
.MarginRight = 0#
.MarginTop = 0#
.MarginBottom = 0#
Select Case verticalAlignment
Case eTextBoxVerticalAlignment.Bottom
.verticalAlignment = xlVAlignBottom
Case eTextBoxVerticalAlignment.Center
.verticalAlignment = xlVAlignCenter
Case eTextBoxVerticalAlignment.top
.verticalAlignment = xlVAlignTop
End Select
Select Case horizontalAlignment
Case eTextBoxHorizontalAlignment.left
.horizontalAlignment = xlHAlignLeft
Case eTextBoxHorizontalAlignment.Middle
.horizontalAlignment = xlHAlignCenter
Case eTextBoxHorizontalAlignment.Right
.horizontalAlignment = xlHAlignRight
End Select
With .Characters
.Text = Number
With .Font
.Name = "MS P明朝"
.FontStyle = "標準"
.size = fontSize
.Strikethrough = False
.Superscript = False
.Subscript = False
.OutlineFont = False
.Shadow = False
.Underline = xlUnderlineStyleNone
.ColorIndex = xlAutomatic
End With
End With
End With
.Fill.Visible = msoFalse
.Fill.Solid
.Fill.Transparency = 1#
With .Line
.Weight = 0.75
.DashStyle = msoLineSolid
.style = msoLineSingle
.Transparency = 0#
.Visible = msoFalse
End With
End With
End Function
Range.Group returns a value. You might try:
Set arrowBoxRange = targetSheet.shapes.Range(Array(Arrow.Name, textBox.Name))
Set arrowBoxGroup = arrowBoxRange.Group
arrowBoxGroup.Name = "AroTxt" & Number
I suspect that the current Selection gets updated as if the following in your earlier work:
Set Selection = Selection.Group 'it's as if this is done for you when you create the group.
which is causing the difference.
FYI, I'm using Excel 2010 and cannot duplicate the original code snippet based on Selection (I get an error doing "Selection.Name = ", which gives object does not support property.)
Ok, I can get this to work:
Selection.Group.Select
Selection.Name = "AroTxt"
Of course, like the other snippet I suggest, this reassigns the group's return value, so that Selection in Selection.Group and Selection.Name are referring to different objects, which I think is what you want.
It is because you are storing the new groups as an object manually now that this error has appeared. You probably are not able to do anything with the multiple instances of "AroTxt" & Number that you have created. As excel wouldn't be able to decide which group you mean.
Excel shouldn't allow this but it doesn't always warn that this has happened but will error if you try to select a group that has a duplicate name.
Even if this isn't the case, it isn't good practice to have duplicate variable names. Would it not be better to add the extra Arrow's and textBox's to the group?
So to solve your problem you will have to check to see if the group already exists before you save it. Maybe delete it if exists or add to the group.
Hope this helps
Edit: As it always seems to go, the error started popping up after I clicked submit. I'll tinker around a bit more, but will echo #royka in wondering if you really do need to give the same name to multiple shapes.
The below code seems to do what you're looking for (create the shapes, give them names and then group). In the grouping function, I left the "AroText" number the same just to see if an error would happen (it did not). It seems that both shapes have the same name, but what differentiates them is their Shape.ID. From what I can tell, if you say ActiveSheet.Shapes("My Group").Select, it will select the element with that name with the lowest ID (as to why it lets you name two things the same name, no clue :) ).
It isn't quite an answer to your question of "why" (I wasn't able to replicate the error), but this will hopefully give you one way "how".
Sub SOTest()
Dim Arrow As Shape
Dim TextBox As Shape
Dim i as Integer
Dim Grouper As Variant
Dim ws As Worksheet
Set ws = ActiveSheet
' Make two shapes and group, naming the group the same in both cases
For i = 1 To 2
' Create arrow with name "Aro" & i
Set Arrow = ws.Shapes.AddShape(msoShapeRightArrow, 10, 50, 30, 30)
Arrow.Name = "Aro" & i
' Create text box with name "Text" & i
Set TextBox = ws.Shapes.AddTextbox(msoTextOrientationHorizontal, 50, 50, 40, 40)
TextBox.Name = "Text" & i
' Use a group function to rename the shapes
Set Grouper = CreateGroup(ws, Arrow, TextBox, i)
' See the identical names but differing IDs
Debug.Print "Name: " & Grouper.Name & " | ID: " & Grouper.ID
Next
End Sub
Function CreateGroup(ws As Worksheet, Arrow As Shape, TextBox As Shape, Number As Integer) As Variant
Dim arrowBoxGroup As Variant
' Group the provided shapes and change the name
Set arrowBoxGroup = ws.Shapes.Range(Array(Arrow.Name, TextBox.Name)).Group
arrowBoxGroup.Name = "AroTxt" & Number
' Return the grouped object
Set CreateGroup = arrowBoxGroup
End Function