I have create some shapes in Excel, group them together and i can not find how to apply fill color on the whole group. Not on the shapes but on the background.
Any help?
In short, this isn't possible on a shape group.
Detailed explanation:
From https://learn.microsoft.com/en-us/office/vba/api/excel.groupshapes
The GroupedShapes object "Represents the individual shapes within a
grouped shape."
Because the group itself only represents the shapes within it, it doesn't have its own fill colour as such, only the fill colours of the shapes within it.
So, as I guess you've discovered, if you do something like this:
With Sheet1.Shapes(1).GroupItems.Parent
.Fill.ForeColor.RGB = RGB(255, 0, 0)
End With
Then you are telling the group to apply the Forecolor to every shape within the group, and not the group object itself.
If you wish to have a background colour to the group, then a workaround would be to create a rectangle at the back, and set the colour of that instead.
Update: Workaround Example
If you wish to implement my suggested workaround, then the following will get you there. You will need to adjust for colours / workbook / worksheet / shape group name etc. There might be a prettier way, but I have this working...
Const shapeGroupName As String = "ShapeGroup"
Const shapeGroupBGName As String = "ShapeGroupBG"
Const shapeGroupMargin As Single = 5
Dim x As Integer
Dim y As Integer
Dim h As Integer
Dim w As Integer
Dim shapeCount As Integer
Dim shapeCol() As String
With Sheet1.shapes.Range(Array(shapeGroupName))
ReDim Preserve shapeCol(.GroupItems.Count)
For shapeCount = 1 To .GroupItems.Count
shapeCol(shapeCount) = .GroupItems.Item(shapeCount).Name
Next
y = .Top - shapeGroupMargin
x = .Left - shapeGroupMargin
h = .Height + shapeGroupMargin * 2
w = .Width + shapeGroupMargin * 2
.Ungroup
End With
shapeCol(0) = shapeGroupBGName
With Sheet1.shapes.AddShape(msoShapeRectangle, x, y, w, h)
.Name = shapeGroupBGName
.Fill.ForeColor.RGB = RGB(255, 0, 0)
.Line.DashStyle = msoLineDashDot
.ZOrder msoSendToBack
End With
With Sheet1.shapes.Range(shapeCol).Group
.Name = shapeGroupName
End With
This works by getting the dimensions of the group, and the names of the shapes within it. Next it ungroups the shapes, adds a rectangle behind the existing shapes, and then regroups accordingly.
A couple of notes:
I have created an arbitrary margin size, as the dimensions of the group are actually the top left and bottom right of the objects within it. In excel, the GUI adds a nice bit extra on to this, so its up to you to set this to what you want.
You can also implement a different shape (i.e. a rounded rectangle instead if desired etc.).
If you have multiple shape groups, then the above could be modified easily to accommodate this.
Related
The problem is: column.width, row.height and chart.width, chart.height use different units. Also it seems it might depend on the os, application and display. Is it even possible to fit a chart inside a single cell so it gonna be rendered the same everywhere?
UPD: The code looks like
width = 26
height = 78
for c in range(ws.min_column, ws.max_column + 1):
ws.column_dimensions[get_column_letter(c)].width = width
for r in range(ws.min_row, 10):
ws.row_dimensions[r+1].height = height
chart.width = width / 5.425
chart.height = height / 29
"One unit of column width is equal to the width of one character in the Normal style. For proportional fonts, the width of the character 0 (zero) is used."
So ColumnWidth in Excel is measured as a number of "0" characters which fits in a column. How can this value be converted into pixels and vice versa?
As already mentioned ColumnWidth value in Excel depends on default font of a Workbook which can be obtained via Workbook.Styles("Normal").Font. Also it depends on current screen DPI.
After carrying out some research for different fonts and sizes in Excel 2013 I've found out that we have 2 linear functions (Arial cannot be seen because it overlaps with Tahoma.):
As it can be seen in the picture the function for ColumnWidth < 1 is different from the major part of the line chart. It's calculated as a number of pixels in a column / number of pixels needed to fit one "0" character in a column.
Now let's see what a typical cell width consists of.
A - "0" character width in the Normal Style
B - left and right padding
C - 1px right margin
A can be calculated with GetTextExtentPoint32 Windows API function, but font size should be a little bit bigger. By experiment I chose +0.3pt which worked for me for different fonts with 8-48pt base size. B is (A + 1) / 4 rounded to integer using "round half up". Also screen DPI will be needed here (see Python 3 implementation below)
Here are equations for character-pixel conversion and their implementation in Python 3:
import win32print, win32gui
from math import floor
def get_screen_dpi():
dc = win32gui.GetDC(0)
LOGPIXELSX, LOGPIXELSY = 88, 90
dpi = [win32print.GetDeviceCaps(dc, i) for i in (LOGPIXELSX,
LOGPIXELSY)]
win32gui.ReleaseDC(0, dc)
return dpi
def get_text_metrics(fontname, fontsize):
"Measures '0' char size for the specified font name and size in pt"
dc = win32gui.GetDC(0)
font = win32gui.LOGFONT()
font.lfFaceName = fontname
font.lfHeight = -fontsize * dpi[1] / 72
hfont = win32gui.CreateFontIndirect(font)
win32gui.SelectObject(dc, hfont)
metrics = win32gui.GetTextExtentPoint32(dc, "0")
win32gui.ReleaseDC(0, dc)
return metrics
def ch_px(v, unit="ch"):
"""
Convert between Excel character width and pixel width.
`unit` - unit to convert from: 'ch' (default) or 'px'
"""
rd = lambda x: floor(x + 0.5) # round half up
# pad = left cell padding + right cell padding + cell border(1)
pad = rd((z + 1) / 4) * 2 + 1
z_p = z + pad # space (px) for "0" character with padding
if unit == "ch":
return v * z_p if v < 1 else v * z + pad
else:
return v / z_p if v < z_p else (v - pad) / z
font = "Calibri", 11
dpi = get_screen_dpi()
z = get_text_metrics(font[0], font[1] + 0.3)[0] # "0" char width in px
px = ch_px(30, "ch")
ch = ch_px(px, "px")
print("Characters:", ch, "Pixels:", px, "for", font)
2022 and still the same Problem... Found threads going back to 2010 having the issue...
To start of: Pixel != Points
Points are defined as 72points/inch: https://learn.microsoft.com/en-us/office/vba/language/glossary/vbe-glossary#point
Though that definition seems stupid, as a shape with a fixed width of 100points, would display the exact same size in inch on every monitor independent of monitor configuration, which is not the case.
Characters is a unit that is defined to the number of 0 characters of the default text format. A cell set to a width of 10 characters, can fit 10 "0" characters, when the cell content is formatted to the default format.
My case is that I need to place pictures into the document and place text into cells next to it. But pictures hover over the document and cells are hidden below it. Depending on the size of the Picture, more or less cells are hidden. Thus, I can't just say I place text 5 cells to the left of the picture. Autosizing a column to the contents of the cells of the column, does not account for the hovering picture.
A picture is bound to the cell that is below the top left corner of the picture. I need to set the size of that cell to the size of the picture to solve the issue.
A Picture is a Shape. A Shape returns its width as Points (Shape.Width).
A Range can be set to a cell like Worksheet.Range["A1"]. From a Range you can get the width in Characters (Range.ColumnWidth) or in Points (Range.Width). But you can only set the width of a Range in Characters (Range.ColumnWidth).
So we can retrieve the size of the Picture (Shape) in Points and need to convert them to Characters to set the cell to the correct width...
Some research showed that the Points size of a cell contains a constant for spacing (padding before and after the cell content) and probably the seperator lines between cells.
On my system:
A cell set to a width of 1 **Characters** = 9 **Points**
A cell set to a width of 2 **Characters** = 14.25 **Points**
A cell set to a width of 3 **Characters** = 19.5 **Points**
As I said, there is a constant within the Points. Thus going from 1 Characters, to 2 Characters, the difference is only the size of the letter.
SizeOfLetter = 14.25 Points - 9 Points = 5.25 Points
we can then subtract that SizeOfLetter from the Points for 1 Characters and get the Points constant.
PointsConstant = 9 Points - 5.25 Points = 3.75 Points
Verify:
Points size for a cell containing 3 "0" letters = 3SizeOfLetter + PointsConstant = 35.25 Points + 3.75 Points = 19.5 Points
As the values depend on your system, YOU CAN'T USE THOSE VALUES!
Best way is to use code to calculate it for your system:
C# code:
Excel.Application excelApp = new Excel.Application();
Excel.Workbook workbook1 = excelApp.Workbooks.Add();
Excel.Worksheet sheet1 = (Excel.Worksheet)workbook1.ActiveSheet;
// Evaluate the Points data for the document
double previousColumnWidth = (double)sheet1.Range["A1"].ColumnWidth;
sheet1.Range["A1"].ColumnWidth = 1; // Make the cell fit 1 character
double points1 = (double)sheet1.Range["A1"].Width;
sheet1.Range["A1"].ColumnWidth = 2; // Make the cell fit 2 characters
double points2 = (double)sheet1.Range["A1"].Width;
double SizeOfLetter = points2 - points1;
double PointsConstant = points1 - pointsPerCharater;
// Reset the column width
sheet1.Range["A1"].ColumnWidth = previousColumnWidth;
// Create a function for the conversion
Func<double, double> PointsToCharacters = (double points) => (points - PointsConstant ) / SizeOfLetter ;
I am drawing circles in excel with a number in them, but as soon as that number hits a double digit, the second digit is not visible.
I would like the textsize to fit the shape.
I cannot change the size of the circle, because this is based on the opportunity value.
I am working on a business funnel, the relevant input of the funnel is:
- ticket number
- value of opportunity (potential revenue): used to size the circle
The circles are drawn in the funnel, which exists of four stages that are depicted as four shapes.
I have tried:
Public Function DrawCircle(x As Integer, y As Integer, tickets As Collection, ticketIndex As Integer)
Dim myCircle As Shape
Dim circleSize As Integer
circleSize = GetTicketSize(tickets, ticketIndex)
Dim name As String
name = "" & tickets.Item(ticketIndex).id
Set myCircle = ThisWorkbook.Sheets("Graphic funnel").Shapes.AddShape(msoShapeOval, x, y, circleSize, circleSize)
myCircle.Select
Selection.ShapeRange.Fill.Visible = msoTrue
Selection.ShapeRange.TextFrame.Characters.Text = name
Selection.ShapeRange.Left = Selection.ShapeRange.Left
Selection.ShapeRange.Top = Selection.ShapeRange.Top - circleSize / 2
With myCircle
.DeleteText
.wrapformat = msoWrapFormat
.WordWrap = True
.AutoSize = msoAutoSizeTextToFitShape
End With
But it returns: "object doesn't support this property or method"
I need to create shapes labelled with a number but the shape doesn't display the whole number.
For example, where the number is 1 to 9 it displays, but for 10 to 19 it shows just 1.
I don't want to change the radius it have to be the same.
Sheets(xRiS).Select
Dim textRectangles As Shape
Dim radius As Integer
radius = 15
Set textRectangles = ActiveSheet.Shapes.AddShape(msoShapeOval, (Range("D13").Left - radius / 2), _
Range("D13").Top - radius / 2, radius, radius)
textRectangles.Name = "Groups " & i
textRectangles.TextFrame.Characters.Text = i
textRectangles.TextFrame.VerticalAlignment = xlVAlignCenter
textRectangles.TextFrame.HorizontalAlignment = xlHAlignCenter
textRectangles.TextFrame.Characters.Font.Size = 9
textRectangles.TextFrame.Characters.Font.Name = "Georgia"
textRectangles.Fill.ForeColor.RGB = RGB(220, 105, 0)
textRectangles.Fill.Transparency = 0.5
This is the current result:
This is the result I want:
How can I configure the shape to display the number where the number is 10-19?
There are a couple of theoretical options, but neither seem like they are going to work with radius = 15 and textRectangles.TextFrame.Characters.Font.Size = 9.
First is textRectangles.TextFrame.AutoSize = True which resizes the shape to fix the text - however, this is going to expand the radius significantly.
Second is textRectangles.TextFrame2.AutoSize = msoAutoSizeTextToFitShape - however, this only works if the minimum value is radius = 33.
You may have to consider increasing the radius.
I have some code that looks at the position of a line that a user can edit. The purpose of editing this line is to get rough measurements from a plot plan which works so far. I'm working on making it much more interactive by recording measured positions as well.
Here's the issue. When this shape (a connector elbow) is on the cabinet and the access point it records its position in points using shape.top and shape.left (code below). When I resize this line to put the access point end on another access point, the cabinet position changes as well even though it didn't move. I did notice that the scale height and width changed on the drawing but I can't figure out why that would affect the initial point.
It's worth noting that as you rotate the elbow connector the value of width and height rotate. That means sometimes height is up and down and sometimes its the value you'd expect width to be. Still the left position only stays constant when the connector is rotated 180 degrees.
Is there a relation between scale height/width and the top/left value?
Sub Measure()
Set sp = ActiveSheet.Shapes("Measurement")
Msgbox(sp.Top & "//" & sp.Left)
end sub
Edit: So I realize I mentioned a lot about the program, the real problem is why do the top left measurements change despite the top left staying stationary on the screen? And only in the 270/90 rotation (happens automatically depending how you drag the line)
Add Screen Shots (Measurements are Top then left)
This is the first screen shot with a 270 rotation
This is the second, notice the top left stayed stationary but the points changed
This next group is with a 180/0 rotation (shape auto rotates, otherwise I would just lock it and be done).
Rotated 180 first screen shot
Rotated 180 second screen shot
I sympathized with your problem recognition and I did not understand this phenomenon, but after testing, it seems that the original coordinates are preserved even if the figure is rotated. This is because even if the rotation is performed again, the rotation is performed by the original coordinate value. If you artificially align the position, the coordinates appear to move accordingly. I tested it as follows.
Sub Measure()
Dim rngT As Range
Dim sp As Shape
Set sp = ActiveSheet.Shapes("Measurement")
'MsgBox (sp.Top & "//" & sp.Left)
Set rngT = Range("k" & Rows.Count).End(xlUp).Offset(1, 0)
rngT = sp.Top
rngT.Offset(, 1) = sp.Left
End Sub
Sub test()
Dim sp As Shape
Dim Ws As Worksheet
Dim vDB
Dim i As Integer, t As Single, l As Single
Dim r As Integer
vDB = Range("k2", Range("L" & Rows.Count).End(xlUp))
Set Ws = ActiveSheet
r = UBound(vDB, 1)
For i = 1 To r
t = vDB(i, 1)
l = vDB(i, 2)
Set sp = Ws.Shapes.AddShape(msoShapeRectangle, l, t, 10, 10)
Next i
End Sub
I figured it out guys.
So in orientation 0 or 180 the Shape.top and Shape.Left give accurate consistent measurements.
In the 90 or 270 the top and left have to be adjusted to give accurate consistent measurements.
The code is Shape.Top-((Shape.Width-Shape.Height)/2) and Shape.Left+((Shape.Width-Shape.Height)/2)
(NOTE: THIS ADJUSTMENT ONLY APPLIES WHEN OBJECT IS ROTATED 90 or 270//Shape.Rotation)
Also, the shape used here is an elbow connector