Powerpoint layout algorithm for box-in-box view - vba

I am trying to write some code in VBA and PowerPoint to automatically create a poster (basically a box in box view) however, its proving to be a little more challenging than my skills can cope.
looking to get some guidance on how best to structure the code (and patterns)
at the moment:
Im reading the data (see table) from csv, and placing them into 4 in memory arrays (cat, sub-cat, column, intersection) - this is probably bad - but dont know of anything better.
my code tries to process the spacing logic and adjusts the x-and-y pos of each intersection block (grows the category and sub-category blocks when needed) within the arrays, then finally draws/places the blocks on the powerpoint canvas.
You will see in the graphic "initial selected block" at the moment thats the context from which i run -- the idea is to use that block size as the template for the dimensions
Im not worried about the placement, its more the algorithm to determine the spacing (or block size of each of the components
below is a sample of the data that I read:
and once the logic is generated, this is how the "poster" would look
Im happy to share code, its just getting a bit big now (and I feel im missing a trick in terms of simplicity)
code sample included below:
Const defaultObjectWidth = 95
Const defaultObjectHeight = 50
Const defaultCategoryWidth = 90
Const defaultSubCatWidth = 90
Const defaultBuffer = 3
Const blocksPerCol = 4
Const myYAxisWidth = 100
Sub insertShape()
On Error GoTo Err_Handler
Dim SlideHeight As Long, SlideWidth As Long
Dim ContainerHeight As Long, ContainerWidth As Long
Dim ObjectHeight As Long, ObjectWidth As Long
Dim x As Long, y As Long
Dim shp As Object
Dim colWidth As Integer
Dim blockSize As Integer
Dim IntersectCalcComplete As Boolean
IntersectCalcComplete = False
' create the Arrays for storing the data
Dim CategorysArr() As String
Dim SubCatsArr() As String
Dim YAxissArr() As String
Dim IntersectsArr() As String
Dim tmpIntersects() As String
Call LoadSubCats(SubCatsArr)
Call LoadCategorys(CategorysArr)
Call LoadYAxiss(YAxissArr)
Call LoadIntersects(IntersectsArr)
ReDim tmpIntersects(0 To 2, 0 To 0)
If ActiveWindow.Selection.Type = ppSelectionNone Then
MsgBox "Please select object", vbExclamation, "Make Selection"
Else
Set shp = ActiveWindow.Selection.ShapeRange(1)
SlideHeight = ActivePresentation.PageSetup.SlideHeight 'get slide vertical height
SlideWidth = ActivePresentation.PageSetup.SlideWidth 'get slide horizontal width
'Main Content Container width and height
ContainerHeight = shp.Height
ContainerWidth = shp.Width
' calculate the container column sizes.
'
colWidth = calcColumnSizes(shp.Width, CategorysArr, SubCatsArr, YAxissArr)
' calculate the blockwidth based on the density factor parameter
blockWidth = calcBlockWidth(colWidth, blocksPerCol)
' Iterate through the Categorys, SubCats and Intersects in order to calculate the spacing of the Intersects.
' first sort the matricies by a specific column.
' CategorysArr = QuickSortArray(CategorysArr, -1, -1, 4)
' Call multiSortArray(1, CategorysArr, arrFlds(CategorysArr), "2 Asc 5 Asc")
' -- ------------------------------------------------------------------------------------------------------------------------------------------------
Dim SubCatID As Integer
'Dim myYAxisWidth As Integer
Dim SubCatHeight, CategoryHeight, currentX, currentY, n, t As Integer
SubCatHeight = 0
CategoryHeight = 0
currentX = 0
currentY = 0
currentBlockUsed = 0
For i = LBound(SubCatsArr, 2) To UBound(SubCatsArr, 2) ' For each SubCat in the SubCats Array
t = t + 1
'Debug.Print (" processing SubCat: " & t & " called: " & SubCatsArr(1, i))
' Debug.Print (SubCatsArr(i, 1)) ' print the name
SubCatID = SubCatsArr(0, i)
If Not IsNull(SubCatID) Then
' with each SubCat - find all related Intersects and fit them into the canvas
' 1: Filter the Intersects by SubCat
Call getIntersectBySubCat(SubCatID, IntersectsArr, tmpIntersects)
'printArray (tmpIntersects)
' 2: re-structure the Intersects position to fit into the YAxiss allocated
' myYAxisWidth = 200 ' -- get YAxis width TODO: get the real width
' -- we already know what the blockWidth is (above)
SubCatHeight = defaultObjectHeight + defaultBuffer ' set the SubCatheight on the first pass.
For j = LBound(tmpIntersects, 2) To UBound(tmpIntersects, 2) ' Loop through tmp Intersects to add placement
If LBound(tmpIntersects, 2) = -1 Then ' SubCat has no Intersects
' Debug.Print ("SubCat ID:" & SubCatID & " has no Intersects")
Else
If myYAxisWidth < (currentX + blockWidth + (defaultBuffer * 2)) Then ' If there is not enough remaining space in YAxis
currentY = currentY + defaultBuffer + defaultObjectHeight
currentX = 0
SubCatHeight = SubCatHeight + defaultObjectHeight + defaultBuffer
End If
n = findArrStr(IntersectsArr, 0, str(tmpIntersects(0, j)))
IntersectsArr(6, n) = str(currentX + defaultBuffer) ' X CoOrdinate
IntersectsArr(7, n) = str(currentY + defaultBuffer) ' Y CoOrdinate
currentX = currentX + defaultBuffer + defaultObjectWidth
End If
Next j 'filtered Intersect
Else
' Debug.Print ("empty record in Array Detected")
End If
Call setSubCatHeight(SubCatHeight, SubCatID, SubCatsArr, CategorysArr)
SubCatHeight = 0
Next i 'SubCat
' -- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
' -- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
' ADDING OF THE SHAPES TO THE VIEW
Call addCategorys(CategorysArr, shp)
Call addSubCats(SubCatsArr, shp)
Call addIntersects(IntersectsArr, shp)
' x = (SlideWidth - ObjectWidth) / 2 'calculate horizontal position
' y = (SlideHeight - ObjectHeight) / 2 'calculate vertical position
' shp.Left = x 'move object horizontal
' shp.Top = y 'move object vertical
End If
Exit_Label:
On Error Resume Next
Set shp = Nothing
Exit Sub
Err_Handler:
MsgBox Err.Description, vbCritical
Resume Exit_Label
End Sub
Function getIntersectBySubCat(SubCatID As Integer, ByRef IntersectsArr As Variant, ByRef tmpIntersects As Variant)
' Filters the Intersects to the specific SubCat
Dim i, j As Integer
j = -1
Erase tmpIntersects
ReDim Preserve tmpIntersects(0 To 2, -1 To 0)
For i = LBound(IntersectsArr, 2) To UBound(IntersectsArr, 2)
If IntersectsArr(2, i) = SubCatID Then 'if the Intersect relates to the current SubCat
j = j + 1
ReDim Preserve tmpIntersects(0 To 2, 0 To j) ' Redimension:
tmpIntersects(0, j) = IntersectsArr(0, i) ' Intersect Code
tmpIntersects(1, j) = IntersectsArr(3, i) ' get the YAxis
tmpIntersects(2, j) = IntersectsArr(9, i) ' get the Order
'Debug.Print ("RESULT ARRAY" & tmpIntersects(i + 1, 0) & "_" & tmpIntersects(i + 1, 1) & "_" & tmpIntersects(i + 1, 2) & "_")
End If
Next i 'Intersect
' sort the array by the Intersect order column (2)
' Call QuickSortArray(tmpIntersects, , , 2)
End Function
Function calcColumnSizes(lenContainer As Integer, ByRef CategorysArr As Variant, ByRef SubCatsArr As Variant, ByRef YAxissArr As Variant) As Integer
Dim lenCategory, lenSubCat, cntQtr As Integer
Dim lenWorkArea, columnSize As Integer
cntQtr = UBound(YAxissArr) + 1
lenWorkArea = lenContainer - defaultCategoryWidth - defaultSubCatWidth - (2 * defaultBuffer)
lenWorkArea = lenWorkArea - ((2 * defaultBuffer) + (cntQtr * defaultBuffer))
columnSize = lenWorkArea / cntQtr
' now propogate the columnSize into the YAxiss data
For i = LBound(YAxissArr, 1) To UBound(YAxissArr, 1)
YAxissArr(2, i) = str(Round(columnSize, 1)) ' Width
Next i
calcColumnSizes = columnSize
End Function
Function calcBlockWidth(columnSize As Integer, density As Integer) As Integer
calcBlockWidth = columnSize / density
End Function
Function setSubCatHeight(SubCatYPos, ID As Integer, ByRef SubCatsArr As Variant, ByRef CategorysArr As Variant)
Dim curSubCat As Integer
curSubCat = findArrStr(SubCatsArr, 0, str(ID))
If SubCatsArr(3, curSubCat) < SubCatYPos Then
SubCatsArr(3, curSubCat) = SubCatYPos
Call setCategoryHeight(SubCatYPos, CInt(SubCatsArr(2, curSubCat)), CategorysArr)
End If
End Function
Function setCategoryHeight(CategoryYPos, ID As Integer, ByRef CategorysArr As Variant)
Dim curCategory As Integer
curCategory = findArrStr(CategorysArr, 0, str(ID))
If CategorysArr(2, curCategory) < CategoryYPos Then
CategorysArr(2, curCategory) = CategoryYPos
End If
End Function
Function addCategorys(ByRef CategorysArr As Variant, masterShp As Shape)
Dim i, j As Integer
For i = LBound(CategorysArr, 2) To UBound(CategorysArr, 2)
Call addBlockContents(CInt(CategorysArr(2, i)), CInt(CategorysArr(3, i)), CStr(CategorysArr(1, i)), masterShp) ' X, Y, Name, Shape
Next i
End Function
Function addSubCats(ByRef SubCatsArr As Variant, masterShp As Shape)
Dim i, j As Integer
For i = LBound(SubCatsArr, 2) To UBound(SubCatsArr, 2)
Call addBlockContents(CInt(SubCatsArr(3, i)), CInt(SubCatsArr(4, i)), CStr(SubCatsArr(1, i)), masterShp)
Next i
End Function
Function addIntersects(ByRef IntersectsArr As Variant, masterShp As Shape)
Dim i, j As Integer
Dim myX, myY As Integer
myX = 0
myY = 0
For i = LBound(IntersectsArr, 2) To UBound(IntersectsArr, 2)
' adjust the placement of the shape relative to the masterShape
myX = masterShp.Left + CInt(IntersectsArr(6, i)) + defaultCategoryWidth + defaultSubCatWidth + (defaultBuffer * 2)
myY = masterShp.Top + CInt(IntersectsArr(7, i))
Call addBlockContents(myX, myY, CStr(IntersectsArr(1, i)), masterShp)
'For j = LBound(COAArray, 2) To UBound(COAArray, 2)
'Next j
Next i
End Function
Function addBlockContents(x, y, IntersectName As String, myShp As Shape)
Dim oSh As Shape
Set oSh = ActiveWindow.Selection.SlideRange.Shapes.AddShape(msoShapeRectangle, x, y, defaultObjectWidth, defaultObjectHeight)
With oSh
With .Fill
.Visible = msoTrue
.ForeColor.ObjectThemeColor = msoThemeColorText2
.ForeColor.TintAndShade = 0
.ForeColor.Brightness = 0.8000000119
.Transparency = 0
.Solid
End With 'fill
With .TextFrame
.MarginLeft = 0
.MarginRight = 0
.MarginTop = 0
.MarginBottom = 0
.AutoSize = False
With .TextRange
.Text = IntersectName
With .Font
.Name = "Ariel"
.Size = 6.4
.Bold = msoFalse
.Italic = msoFalse
.Underline = msoFalse
.Shadow = msoFalse
.Emboss = msoFalse
.BaselineOffset = 0
.AutoRotateNumbers = msoFalse
.Color.SchemeColor = ppForeground
End With ' Font
End With ' TextRange
End With ' TextFrame
End With ' oSh, the shape itself
'Set addBlockContents = oSh
End Function
any help will be awesome!

The easiest way to get started here is to use tables. It will help you with a lot of the layout issues.
As a first step, create a table manually. Insert a table and remove headings and other automatic coloring. Set the border line to maximum width and to match the background color before applying it to all cells.
Now you can merge cells to create larger boxes (like Cars in your example). Deleting cells isn't possible, but you can set the background color.
If this is good enough, then you have something to automate.
If not, then you can perhaps use this to find coordinates for boxes to create.

Related

Align shapes flush/stacked/touching

I'm trying to get a selection of shapes in order from right to left. I found a routine by John Wilson on vbaexpress on which I based my code.
The sorting works perfectly when I select item by item by clicking on the shapes but it doesn't respect the "visible order" of shapes if I select them by "lassoing" with my mouse.
In case of dragging my mouse over the shapes to select them, the routine should respect the visible order of shapes.
Thanks in advance.
Sub AlignFlush()
Dim oshpR As ShapeRange
Dim oshp As Shape
Dim L As Long
Dim rayPOS() As Single
Set oshpR = ActiveWindow.Selection.ShapeRange
ReDim rayPOS(1 To oshpR.Count)
'add to array
For L = 1 To oshpR.Count
rayPOS(L) = oshpR(L).Left
Next L
'sort
Call sortray(rayPOS)
'apply
For L = 1 To oshpR.Count
If L = 1 Then
Set oshp = Windows(1).Selection.ShapeRange(1)
PosTop = oshp.Top
PosNext = oshp.Left + oshp.Width
Else
Set oshp = Windows(1).Selection.ShapeRange(L)
oshp.Top = PosTop
oshp.Left = PosNext
PosNext = oshp.Left + oshp.Width
End If
Next L
End Sub
Sub sortray(ArrayIn As Variant)
Dim b_Cont As Boolean
Dim lngCount As Long
Dim vSwap As Long
Do
b_Cont = False
For lngCount = LBound(ArrayIn) To UBound(ArrayIn) - 1
If ArrayIn(lngCount) > ArrayIn(lngCount + 1) Then
vSwap = ArrayIn(lngCount)
ArrayIn(lngCount) = ArrayIn(lngCount + 1)
ArrayIn(lngCount + 1) = vSwap
b_Cont = True
End If
Next lngCount
Loop Until Not b_Cont
End Sub
Some comments on your existing code:
Array counts always start at 0 unless you use the Option Base statement to set it to a different number.
When you use ReDim, most of the time, you want to use the Preserve keyword, or the ReDim obliterates the existing array contents. But in this case, we know the array size ahead of time, so Preserve is not necessary.
You call sortray, but didn't include it in your listing. I've added a sorting routine.
But then you make no use of the sorted array in the section where you position the shapes.
Working macro (based on your description of what you mean by "visible order" being the left-to-right sequence):
Since you use the left position of the leftmost shape to apply to the others, here's a simpler way to do that:
Sub AlignFlushLeftWithLeftmostShape()
Dim ShpCount As Long
Dim oshpR As ShapeRange
Dim L As Long
Dim rayPOS() As Single
Set oshpR = ActiveWindow.Selection.ShapeRange
ShpCount = oshpR.Count
ReDim rayPOS(ShpCount - 1)
For L = 0 To ShpCount - 1
rayPOS(L) = oshpR(L + 1).Left
Next L
Call BubbleSort(rayPOS)
For x = 1 To ShpCount
oshpR(x).Left = rayPOS(0)
Next x
End Sub
Sub BubbleSort(arr)
Dim lTemp As Long
Dim i As Long
Dim j As Long
Dim lngMin As Long
Dim lngMax As Long
lngMin = LBound(arr)
lngMax = UBound(arr)
For i = lngMin To lngMax - 1
For j = i + 1 To lngMax
If arr(i) > arr(j) Then
lTemp = arr(i)
arr(i) = arr(j)
arr(j) = lTemp
End If
Next j
Next i
End Sub

How to convert array from (x,y)(z) dimensions into (x,y) dimensions?

I am working with Bloomberg's API in VBA and I want to be able to take in the arrays that the API gives out from requesting historical data and put it into a table that has field names. However, the array that the API gives me is given in this format: (x,y)(Z) but I cannot use that for inserting into a table. I also want to be able to add another piece of data into the array while I convert from one form to another
I have tried just going through the Bloomberg array and replacing each element in a different array, but the main issues I have are not being able to know how big I need the array to be and how I am going to loop through the bloomberg API without going out of index and getting an error. I have tried using Ubound, but it does not work the way I have intended.
This is the code I have tried using to convert my array and then insert it. It just puts in blank values and does not put in anything into the table
Sub mWriteToTable(vTableName As String, ByVal vArray As Variant, vCUSIPS As Variant, vFields As Variant)
On Error GoTo ErrorHandler
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim x As Long, y As Long
Dim TEST As String
Dim DataArray() As Variant
Set db = CurrentDb
Set rs = db.OpenRecordset(vTableName, dbOpenDynaset, dbSeeChanges)
TEST = ""
Dim xBound As Integer, yBound As Integer, ThirdBound As Integer, fieldcount As Integer, NewBoundY As Integer, Z As Integer
Dim Boundarynum As Integer
Boundarynum = 0
Dim Boundarynum1 As Integer
Boundarynum1 = 0
fieldcount = UBound(vFields, 1) + 1
xBound = UBound(vArray, 1)
yBound = UBound(vArray, 2)
NewBoundY = fieldcount * (fieldcount + 1)
ReDim DataArray(0 To 20, 0 To (xBound + 1))
'using a static size for the array for now. Will try and make it the same size as the bloomberg array
'TRANSFORMING ARRAY FROM BLOOMBERG
For x = 0 To xBound
For y = 0 To NewBoundY
For Boundarynum1 = 0 To yBound
On Error Resume Next
DataArray(Boundarynum, Boundarynum1) = vArray(x, y)(Boundarynum1)
Next
Boundarynum = Boundarynum + 1
Next
Next
'TRANSFORMING ARRAY FROM BLOOMBERG
'set CUSIP in array
y = 0
Dim counter As Integer
counter = 0
For Z = 0 To 20
If DataArray(Z, 0) = "" Then
Debug.Print ("")
counter = 1
ElseIf counter = 1 And DataArray(Z, 0) <> "" Then
y = y + 1
DataArray(Z, 3) = vCUSIPS(y)
counter = 0
Else
DataArray(Z, 3) = vCUSIPS(y)
End If
Next
'set CUSIP in array
For x = 0 To 20
With rs
.AddNew
For y = 0 To yBound
' On Error GoTo Line1
' If vArray(x, y) = "NA" Then
' TEST = "This is a test"
' End If
'Line1:
.fields(y) = DataArray(x, y)
Next
.Update
End With
Next
'Call fImmediateWindow(vArray)
ErrorHandler:
If Err.Number <> 0 Then
Dim vMsg As String
vMsg = "Error # " & Str(Err.Number) & " was generated by " & Err.Source & Chr(13) & "Error Line: " & Erl & Chr(13) & Err.Description
MsgBox vMsg, , "Error", Err.HelpFile, Err.HelpContext
End If
rs.Close
Set rs = Nothing
db.Close
Set db = Nothing
End Sub
'''
This is the way the Bloomberg Array looks when I get it. I am unsure of how to really work around this. The array from the program above just becomes blank.
Each element of the Bloomberg array is returning 2 sets of data. The key is to have your array have double the number of elements of the top level Bloomberg array.
Sub ConvertBloombergTestData()
Dim r As Variant
r = getBloombergTestData
Dim Values As Variant
Dim n As Long
Dim j As Long
Dim Item
ReDim Values(1 To (UBound(r) + 1) * 2, 1 To 2)
For n = LBound(r) To UBound(r)
j = j + 1
Item = r(n, 0)
Values(j, 1) = Item(0)
Values(j, 2) = Item(1)
Item = r(n, 1)
j = j + 1
Values(j, 1) = Item(0)
Values(j, 2) = Item(1)
Next
End Sub
Not knowing the the array nesting but knowing that we are returning pairs of data, we could add all the data to a collection and create our array bu iterating over the collection.
Sub Test()
Dim r As Variant, Values As Variant
r = getBloombergTestData
Values = ConvertBloombergArrayTo2d(r)
End Sub
Function ConvertBloombergArrayTo2d(BloombergArray)
Dim Map As New Collection
FlattenArray Map, BloombergArray
Dim Results As Variant
ReDim Results(1 To Map.Count / 2, 1 To 2)
Dim n As Long, j As Long
For n = 1 To Map.Count Step 2
j = j + 1
Results(j, 1) = Map.Item(n)
Results(j, 2) = Map.Item(n + 1)
Next
ConvertBloombergArrayTo2d = Results
End Function
Sub FlattenArray(Map As Collection, Element As Variant)
If Right(TypeName(Element), 2) = "()" Then
Dim Item
For Each Item In Element
FlattenArray Map, Item
Next
Else
Map.Add Element
End If
End Sub

Show dimensions of Excel shape in column widths and row heights vba

I have a spreadsheet that involves the user resizing some rectangular shapes, which are set on a background of an Excel grid with column width = row height = 10pixels. The purpose of this background is to give a scale to the plan, which is made by the shapes; in this case, one column or row represents 10cm - there is a thick border after every 10 cells to represent a metre:
When the user resizes the rectangle, I would like the text inside the rectangle to display the dimensions, according to the scale of the plan. I have read many articles about how the shapes dimensions are provided in points, and the columns and rows in pixels (or a unit based on the font), and have found the conversion function between them, but it does not seem to give the results I would expect - the values for the width and height depend on the level of zoom, giving smaller and smaller results as I zoom out, even though the displayed pixel width remains the same.
Is there a way to consistently convert the pixel units of the grid to the points unit of the shapes such that I can essentially count how many column widths and row heights comprise the shape dimensions? Here is the macro I have written so far:
Option Explicit
Dim sh As Shape
Dim dbPx_Per_Unit As Double
Dim strUnit As String
Dim UserSelection As Variant
Dim strText As String
Dim strWidth As String
Dim strHeight As String
Sub LabelShapeSize()
Set UserSelection = ActiveWindow.Selection
'is selection a shape?
On Error GoTo NoShapeSelected
Set sh = ActiveSheet.Shapes(UserSelection.Name)
On Error Resume Next
'pixels are the units for the columns and rows
'dbPx_Per_Unit = InputBox("there are this many pixels per unit:", "Conversion Rate", 10)
dbPx_Per_Unit = 100
'strUnit = InputBox("Unit Name:", "Units", "M")
strUnit = "M"
With sh
'Width and length is measured in points, so we need to convert the points to pixels to get the actual size
strWidth = Format(Application.ActiveWindow.PointsToScreenPixelsX(.Width) / dbPx_Per_Unit, "#,##0.0")
strHeight = Format(Application.ActiveWindow.PointsToScreenPixelsY(.Height) / dbPx_Per_Unit, "#,##0.0")
'this is our message that will be in the shape
strText = strWidth & strUnit & " x " & strHeight & strUnit
With .TextFrame2
.VerticalAnchor = msoAnchorMiddle
With .TextRange.Characters
.ParagraphFormat.FirstLineIndent = 0
.ParagraphFormat.Alignment = msoAlignCenter
.Text = strText
'I'll sort something out for dark shapes at some point, but for now let's just write in black ink
With .Font
.Fill.Visible = msoTrue
.Fill.ForeColor.ObjectThemeColor = msoThemeColorDark1
.Fill.Solid
.Size = 10
End With
End With
End With
End With
Exit Sub
'No shape error
NoShapeSelected:
MsgBox "You must select a shape to calculate dimensions!", vbCritical, "Object not set to an instance of a Nobject"
End Sub
****** for completeness, here is the final script I wrote implementing the solution in the answer below ******
Option Explicit
Dim sh As Shape
Dim db_Cols_Per_Unit As Double
Dim strUnit As String
Dim strText As String
Dim userSelection As Variant
Dim ws As Worksheet
Dim clrBackground As Long
Dim leftCol As Integer
Dim colWidth As Integer
Dim topRow As Integer
Dim rowHeight As Integer
Sub LabelShapeSize()
Set userSelection = ActiveWindow.Selection
Set ws = ActiveSheet
db_Cols_Per_Unit = 10
strUnit = "M"
'is selection a shape?
On Error GoTo NoShapeSelected
Set sh = ActiveSheet.Shapes(userSelection.Name)
On Error Resume Next
topRow = 1
rowHeight = 0
leftCol = 1
colWidth = 0
With sh
While ws.Cells(1, leftCol).Left <= .Left 'Move left until we find the first column the shape lies within
leftCol = leftCol + 1
Wend
While ws.Cells(1, leftCol + colWidth).Left <= .Left + .Width 'Continue moving left until we find the first column the shape does not lie within
colWidth = colWidth + 1
Wend
While ws.Cells(topRow, 1).Top <= .Top 'Move down until we find the first row the shape lies within
topRow = topRow + 1
Wend
While ws.Cells(topRow + rowHeight, 1).Top <= .Top + .Height 'Continue moving down until we find the first row the shape does not lie within
rowHeight = rowHeight + 1
Wend
'this is our message that will be in the shape
strText = Format(colWidth / db_Cols_Per_Unit & strUnit, "#,##0.0") & " x " & rowHeight / Format(db_Cols_Per_Unit, "#,##0.0") & strUnit
clrBackground = .Fill.ForeColor.RGB
With .TextFrame2
.VerticalAnchor = msoAnchorMiddle
With .TextRange.Characters
.ParagraphFormat.FirstLineIndent = 0
.ParagraphFormat.Alignment = msoAlignCenter
.Text = strText
With .Font
.Fill.Visible = msoTrue
.Fill.ForeColor.RGB = ContrastColor(clrBackground)
.Fill.Solid
.Size = 10
End With
End With
End With
End With
Exit Sub
'No shape error
NoShapeSelected:
MsgBox "You must select a shape to calculate dimensions!", vbCritical, "Object not set to an instance of a Nobject"
End Sub
Function ContrastColor(clrBackground As Long) As Long
Dim brightness As Integer
Dim luminance As Double
Dim r As Integer
Dim g As Integer
Dim b As Integer
r = clrBackground Mod 256
g = (clrBackground \ 256) Mod 256
b = (clrBackground \ 65536) Mod 256
luminance = ((0.199 * r) + (0.587 * g) + (0.114 * b)) / 255
If luminance > 0.5 Then
brightness = 0
Else
brightness = 255
End If
ContrastColor = RGB(brightness, brightness, brightness)
End Function
thanks to #Gacek answer in this question for the luminance function.
I believe your best bet would be to make use of the Left, Top, Width, and Height cell properties. They'll tell you the value in Excel's weird format (the same units as used by the shapes), so you won't need to do any conversions.
The downside is that as far as I know of, there's no way to get the row/column that exists at a given top/left value, so you need to search through all the rows/columns until you find the one that matches your shape's boundaries.
Here's a quick example (there's probably an off-by-one error in here somewhere)
Dim UserSelection As Variant
Dim ws As Worksheet
Dim sh As Shape
Dim leftCol As Integer
Dim colWidth As Integer
Dim topRow As Integer
Dim rowHeight As Integer
Set ws = ActiveSheet
Set UserSelection = ActiveWindow.Selection
Set sh = ActiveSheet.Shapes(UserSelection.Name)
leftCol = 1
colWidth = 0
While ws.Cells(1, leftCol).Left <= sh.Left 'Move left until we find the first column the shape lies within
leftCol = leftCol + 1
Wend
While ws.Cells(1, leftCol + colWidth).Left <= sh.Left + sh.width 'Continue moving left until we find the first column the shape does not lie within
colWidth = colWidth + 1
Wend
topRow = 1
rowHeight = 0
While ws.Cells(topRow, 1).Top <= sh.Top 'Move down until we find the first row the shape lies within
topRow = topRow + 1
Wend
While ws.Cells(topRow + rowHeight, 1).Top <= sh.Top + sh.height 'Continue moving down until we find the first row the shape does not lie within
rowHeight = rowHeight + 1
Wend
MsgBox "Shape is " & colWidth & " columns wide by " & rowHeight & " rows high"

chartArea position properties don't work in vba Excel 2013

I'm noob in vba (Excel macros). I need to add somes charts automatically in the same WorkSheet. This is my code:
Sub runChart()
Dim xchart As Chart
Dim nameSheet As String
nameSheet = ActiveSheet.Name
Dim x As Integer
Dim firstIndex As Integer
Dim firstValue As Integer
Dim actualValue As Integer
Dim actualIndex As Integer
Dim rChart1 As Range
Dim rChart2 As Range
MsgBox nameSheet
firstIndex = 2
actualIndex = 2
firstValue = Cells(2, 1)
actualValue = Cells(2, 1)
Do
Do
actualIndex = actualIndex + 1
actualValue = Sheets(nameSheet).Cells(actualIndex, 1)
Loop Until firstValue <> actualValue
Set rChart1 = Range(Sheets(nameSheet).Cells(firstIndex, "E"), Sheets(nameSheet).Cells(actualIndex - 1, "E"))
Set rChart1 = Union(rChart1, Range(Sheets(nameSheet).Cells(firstIndex, "J"), Sheets(nameSheet).Cells(actualIndex - 1, "J")))
Dim nameChart As String
nameChart = CStr(Sheets(nameSheet).Cells(firstIndex, 5)) & " - " & Sheets(nameSheet).Cells(actualIndex, 5) & " " & CStr(Sheets(nameSheet).Cells(firstIndex, 1))
Set xchart = Charts.Add
With xchart
.Name = nameChart
.ChartType = xlColumnClustered
.SetSourceData rChart1
.Location Where:=xlLocationAsObject, Name:=nameSheet
'position and size chart
.ChartArea.Top = 10 'this position is a example
.ChartArea.Left = 1700 'this position is a example
.ChartArea.Height = 400 'this size is a example
.ChartArea.Width = 750 'this size is a example
End With
firstValue = Sheets(nameSheet).Cells(actualIndex, 1)
firstIndex = actualIndex
Loop Until (Sheets(nameSheet).Cells(actualIndex, 1) = vbNullString)
End Sub
So, my problem happens is in .ChartArea.left = 1700. The program says :
The specified dimension is not valid for the current chart type
anyone has any idea what 's happening? Thanks for your time :)
The ChartArea is the overall rectangle containing the chart within its parent ChartObject (the shape that contains the embedded chart). The position and size of the ChartArea are read only. But that's okay, you want to position and resize the ChartObject, which is the chart's .Parent.
With xchart
'position and size chart
.Parent.Top = 10 'this position is a example
.Parent.Left = 1700 'this position is a example
.Parent.Height = 400 'this size is a example
.Parent.Width = 750 'this size is a example
End With

Easy generic print Dictionary to Excel Worksheet using VBA

I'm working with really intricate data. Because of this I wrote this really nice function to print data to the debug area - the imediate window you can reach with Ctrl + G on the VBA, inside Excel. I need a similar function to print this generic data (that has numbers, strings, dictionarys and arrays) to a worksheet.
'call using: Call PrintDict(data)
' Where data can be a number, a string, a dictionary or an Array,
' with any of these inside.
Sub PrintDict(ByVal dicttoprint As Variant, Optional indent As Integer = 0, Optional wasdict As Boolean = False)
Dim i As Long
Dim j As Long
Dim indentStr As String
indentStr = ""
i = 0
Do While i < indent
indentStr = indentStr + " "
i = i + 1
Loop
Dim key
If (TypeName(dicttoprint) = "Dictionary") Then
If (wasdict = True) Then
Debug.Print vbNewLine;
End If
For Each key In dicttoprint.Keys:
Debug.Print indentStr & key & " ";
Call PrintDict(dicttoprint.Item(key), indent + 2, True)
Next
ElseIf (TypeName(dicttoprint) = "Variant()") Then
If (wasdict = True) Then
Debug.Print vbNewLine;
End If
For j = LBound(dicttoprint) To UBound(dicttoprint)
Call PrintDict(dicttoprint(j), indent + 2)
Next j
Else
Debug.Print indentStr & dicttoprint & " "
End If
End Sub
Edit1:
Ok, been thinking about, I have an idea, but can't solve some corner cases...
Example expected output below:
key1:____|__________|__________|__________|_________|
_________|key1.1:___|_numvalue_|__________|_________|
_________|__________|_numvalue_|__________|_________|
_________|__________|_arr1Indx1|_numvalue_|_________|
_________|__________|_arr1Indx2|_numvalue_|_________|
_________|__________|_arr1Indx3|_numvalue_|_________|
_________|key1.2:___|_numvalue_|__________|_________|
_________|__________|_numvalue_|__________|_________|
key2:____|_numvalue_|__________|__________|_________|
key3:____|__________|__________|__________|_________|
_________|_arr2Indx1|keyA.1:___|_numvalue_|_________|
_________|__________|keyA.2:___|_strvalue_|_________|
_________|_arr2Indx2|_numvalue_|__________|_________|
Ok, I think now this output solves some corner cases. Any ideas on how to implement it?
I'm thinking on having the function be able to pass X,Y parameters, that are optional and to return last Y. When working with text, the cursor naturally goes down, I don't know how to do this through recursion in a worksheet.
Edit 2:
Ok, this is pseudo code idea - is almost VBA, but I don't know how to make this work...
Function PrintToWS(ByVal data As Variant, _
Optional rowi As Integer = 0, _
Optional coli As Integer = 0) As Integer
If (TypeName(data) = "Dictionary") Then
For Each key In data.Keys:
Cells(rowi, coli).Value = key
coli = coli + PrintToWS(data.Item(key), rowi+1, coli)
Next
ElseIf (TypeName(data) = "Variant()") Then
For j = LBound(data) To UBound(data)
coli = coli + PrintToWS(data(j), rowi+1, coli)
Next j
Else
Cells(rowi, coli).Value = data
coli = coli + 1
End If
PrintToWS = coli
End Function
Edit2:
Added it in a gist here
Solved. Code is below:
'usage: PrintToWS(yourdata)
' Optional parameters are to be used internally by the function,
'leave optional parameters blank.
Function PrintToWS(ByVal data As Variant, _
Optional rowi As Integer = 1, _
Optional coli As Integer = 1, _
Optional wasdict As Integer = 0) As Integer
Dim key
Dim j As Integer
If (TypeName(data) = "Dictionary") Then
For Each key In data.Keys:
Cells(rowi + wasdict, coli).Value = key
rowi = PrintToWS(data.Item(key), rowi + wasdict, coli + 1, 1)
wasdict = 0
Next
ElseIf (TypeName(data) = "Variant()") Then
For j = LBound(data) To UBound(data)
rowi = PrintToWS(data(j), rowi, coli + 1)
Next j
Else
Cells(rowi, coli).Value = data
rowi = rowi + 1
End If
PrintToWS = rowi
End Function