How do I get the coordinates of the cursor position relative to a rectangle (the one I use to call the macro)?
Here what I got this far:
First: I use the function:
Declare PtrSafe Function GetCursorPos Lib "user32" (Point As POINTAPI) As Long
Type POINTAPI
X As Long
Y As Long
End Type
to get the coordinates of the cursor on the screen. Those values are returned by:
Point.X 'pixels to the left of the screen
Point.Y 'pixels to the top of the screen
Second: I created a rectangle like this:
and set the following macro to it:
Sub SH03G13()
Dim Point As POINTAPI: GetCursorPos Point
Dim rectang As Shape: Set rectang = ThisWorkbook.Sheets("Sheet1").Shapes("SH03G13BACK")
Dim ABCISSA As Long: ABCISSA = Point.X - rectang.Left
Dim ORDENAD As Long: ORDENAD = Point.Y - rectang.Top
MsgBox ABCISSA & " " & ORDENAD
End Sub
On my mind, when I did this, I was positive I was getting the coordinates of the cursor inside the green rectangle. However, when I clicked on the black spot on the next image:
the coordinates that my plan returned weren't the expected near 0 coordinates I thought:
Then I realized that the GetCursorPos were returning the position of the cursor relative to the screen while the rectang.Left and rectang.Top commands on my script were returning the position of the rectangle relative to the spreadsheet. So, the lines Point.X - rectang.Left and Point.X - rectang.Left couldn't possibly be right.
Any ideas how I could get the correct coordinates? i.e How can I get the right coordinates near 0 by clicking on the black spot?
Any help will be very appreciated. And, as always, thank you all in advance.
As I told, I got what I want after exploring an idea gived to me by #Luuklag (by aligning the rectangle with a range of cells).
First I put the next code on a different module (just for a well organized code matter):
Option Explicit
Type RECT
Left As Long: Top As Long: Right As Long: Bottom As Long
End Type
Type POINTAPI
X As Long: Y As Long
End Type
Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hDC As Long) As Long
Declare PtrSafe Function GetSystemMetrics Lib "user32.dll" (ByVal nIndex As Long) As Long
Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long
Declare PtrSafe Function GetCursorPos Lib "user32" (Point As POINTAPI) As Long
Function ScreenDPI(bVert As Boolean) As Long
Static lDPI&(1), lDC&
If lDPI(0) = 0 Then
lDC = GetDC(0)
lDPI(0) = GetDeviceCaps(lDC, 88&)
lDPI(1) = GetDeviceCaps(lDC, 90&)
lDC = ReleaseDC(0, lDC)
End If
ScreenDPI = lDPI(Abs(bVert))
End Function
Function PTtoPX(Points As Single, bVert As Boolean) As Long
PTtoPX = Points * ScreenDPI(bVert) / 72
End Function
Sub GetRangeRect(ByVal rng As Range, ByRef rc As RECT)
Dim wnd As Window: Set wnd = rng.Parent.Parent.Windows(1)
With rng
rc.Left = PTtoPX(.Left * wnd.Zoom / 100, 0) + wnd.PointsToScreenPixelsX(0)
rc.Top = PTtoPX(.Top * wnd.Zoom / 100, 1) + wnd.PointsToScreenPixelsY(0)
rc.Right = PTtoPX(.Width * wnd.Zoom / 100, 0) + rc.Left
rc.Bottom = PTtoPX(.Height * wnd.Zoom / 100, 1) + rc.Top
End With
End Sub
After this, I set the rectangle with the next macro:
Sub SH03G13()
With ThisWorkbook.Sheets("Sheet1")
Dim AreaRng As Range: Set AreaRng = .Range(.Cells(2, 2), .Cells(13, 10))
Dim rectang As Shape: Set rectang = .Shapes("SH03G13BACK")
rectang.Height = AreaRng.Height
rectang.Width = AreaRng.Width
rectang.Top = AreaRng.Top
rectang.Left = AreaRng.Left
DoEvents
Dim Point As POINTAPI: GetCursorPos Point
Dim rc As RECT: Call GetRangeRect(.Cells(2, 2), rc)
Dim ABCISSA As Long: ABCISSA = Point.X - rc.Left
Dim ORDENAD As Long: ORDENAD = Point.Y - rc.Top
End With
MsgBox "x: " & ABCISSA & ", y: " & ORDENAD
End Sub
The previous macro places and adjusts the rectangle SH03G13BACK to the .Cells(2, 2), .Cells(13, 10) range. Once this is done, the Point.X - rc.Left and Point.Y - rc.Top commands gave me the exact coordinates inside the rectangle (and relative to it), regardless the maximized/minimized state of the excel window, the zoom value, the size/contents of the excel command ribbon or the size/resolution of the screen itself. It's perfect:
I realize this is a little cheating (I know that the GetRangeRect subroutine gives the coordinates relative to the .Cells(2, 2) position. However, for this matter, the trick works like a charm.
Your first problem is Points.X & Points.Y are not relative to the document or the clients individual monitor setup, forget about multi-monitor setups. For example, if the cursor pos = (1000,500) but the application isn't full screen, you have to take into account Application.Left / Application.Top values.
Even so, this isn't a true depiction of where your shape is. rectang.Left / rectang.Top are not relative to the spreadsheet as you mention, they are relative to the spreadsheet object, or window if you will. Meaning, if you were to move the rectangle all the way to the left and top of the spreadsheet it would be (0,0). As show below:
Now, lets say we remove the column headings as well as the formula bar from the ActiveWindow object, the coordinates maintain their position, as shown below:
Clearly they Application environment size has changed, not the rectang.Left position. With that being said, a cursor position of Application.Top + rectang.Top will never be a true representation of where the top of the rectangle is, unless you account for all these run-time circumstances.
Let's say you do take these into account, you do have access to some settings with the ActiveWindow object, like Application.ActiveWindow.DisplayHeadings, and you do make sure you do your best to omit these concerns. You still have a bunch of user preferences to consider, i.e, displayed scrollbars to account for, tabs, the actual ribbon, which may or may not be the same size across clients, minimized or maximized, page layouts, what the current zoom level is alone will cause conflicts, and don't forget about content panes. Let's take, for example, the format shape window pane, moving it to the left of the application and resizing it to an obnoxious width defined by a user:
The coordinates still maintain their relative position, which will not correlate to the cursor position regardless of what properties you have access to, because it will always depend on the user's environment settings.
At this time, my answer would be to say there is no reasonable 'out-of-the-box' method to accomplish this, also for another simple reason that Shape Objects in Excel do not have event handlers for things like onclick or otherwise, in addition to Worksheet.SelectionChange does not fire for selection of Shapes afaik. You could potentially find a "hacky" way by running a loop to continually check for current selection etc, but naturally this is not desired for performance reasons.
As a built in means of accomplishing this, until there are event handlers added for Shape Objects, your best bet might be to port this over to a COM AddIn or populate some kind of VBA Windows Form in the Worksheet where you have more control over client positions, do all your shape manipulation in the form, then add the final product to the spreadsheet when the user is done.
This solution generates the Shape Screen coordinates, following this steps:
Ensures the shape worksheet is active (application.WindowState could be either xlNormal or xlMaximized)
Set the shape object
Sets the shape range screen coordinates
Sets the shape screen coordinates by scanning shape range screen coordinates
This solutions does not need the shape to be aligned to the cells.
Tested successfully for the following situations:
a) Excel window in laptop screen, WindowState =xlNormal
b) Excel window in laptop screen, WindowState =xlMaximized
c) Excel window in alternate screen, WindowState =xlNormal
d) Excel window in alternate screen, WindowState =xlMaximized
These are the procedures:
Option Explicit
Public Type RgCrds
Top As Long
Left As Long
Right As Long
Bottom As Long
End Type
Public Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Public Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hDC As Long) As Long
Public Declare Function SetCursorPos Lib "user32" (ByVal x As Long, ByVal y As Long) As Long
Public Declare Function GetDeviceCaps Lib "gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
Public Function Shape_ƒCoordinates_Get(uSpCrds As RgCrds, sp As Shape) As Boolean
Dim wd As Window, rg As Range, oj As Object
Dim uSpOutput As RgCrds, uRgCrds As RgCrds
Dim lX As Long, lY As Long
Dim blX As Boolean, blY As Boolean
Dim b As Byte
On Error GoTo Exit_Err
Rem Set Shape Worksheet Window
sp.TopLeftCell.Worksheet.Activate
Set wd = ActiveWindow
Application.EnableEvents = False
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Rem Set Shape Range
Set rg = Range(sp.TopLeftCell, sp.BottomRightCell)
Rem Get Shape Range Coordinates
Call Range_ScreenCoordinates_Get(uRgCrds, rg)
Rem Set Shape Coordinates Limites
With uSpOutput
.Top = uRgCrds.Bottom
.Left = uRgCrds.Right
.Right = uRgCrds.Left
.Bottom = uRgCrds.Top
End With
Rem Scan Shape Range to Get Shape Coordinates - [TopLeft Corner]
blX = False: blY = False
For lX = uRgCrds.Left To uRgCrds.Right
For lY = uRgCrds.Top To uRgCrds.Bottom
Set oj = wd.RangeFromPoint(lX, lY)
If TypeName(oj) <> "Range" Then
If oj.ShapeRange.Type = sp.Type And oj.Name = sp.Name Then
Shape_ƒCoordinates_Get = True
With uSpOutput
If lY < .Top Then .Top = lY Else blX = True
If lX < .Left Then .Left = lX Else blY = True
If blX And blY Then Exit For
End With: End If: End If: Next: Next
Rem Scan Shape Range to Get Shape Coordinates [BottomRight Corner]
blX = False: blY = False
For lX = uRgCrds.Right To uRgCrds.Left Step -1
For lY = uRgCrds.Bottom To uRgCrds.Top Step -1
Set oj = wd.RangeFromPoint(lX, lY)
If TypeName(oj) <> "Range" Then
If oj.ShapeRange.Type = sp.Type And oj.Name = sp.Name Then
Shape_ƒCoordinates_Get = True
With uSpOutput
If lX > .Right Then .Right = lX Else: blX = True
If lY > .Bottom Then .Bottom = lY Else: blY = True
If blX And blY Then Exit For
End With: End If: End If: Next: Next
Rem Coordinates Fine-Tuning
' The RangeFromPoint Method recognizes the Shapes,
' as soon as any part of the cursor is over the shape,
' therefore some fine-tuning is required in order
' to place the entire mouse inside the Shape's body
b = 15 'change as required
With uSpOutput
.Top = .Top + b
.Left = .Left + b
.Right = .Right - b
.Bottom = .Bottom - b
End With
Rem Set Results
uSpCrds = uSpOutput
Shape_ƒCoordinates_Get = True
Exit_Err:
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.EnableEvents = True
End Function
Public Sub Range_ScreenCoordinates_Get(uOutput As RgCrds, ByVal rg As Range)
Dim wd As Window
With rg
Rem Activate range's worksheet window
.Worksheet.Activate
Application.Goto .Worksheet.Cells(1), 1
Set wd = ActiveWindow
Rem Set Range Screen Coordinates
uOutput.Top = Points_ƒToPixels(.Top * wd.Zoom / 100, 1) + wd.PointsToScreenPixelsY(0)
uOutput.Left = Points_ƒToPixels(.Left * wd.Zoom / 100, 0) + wd.PointsToScreenPixelsX(0)
uOutput.Right = Points_ƒToPixels(.Width * wd.Zoom / 100, 0) + uOutput.Left
uOutput.Bottom = Points_ƒToPixels(.Height * wd.Zoom / 100, 1) + uOutput.Top
End With
End Sub
Private Function Points_ƒToPixels(sgPoints As Single, blVert As Boolean) As Long
Points_ƒToPixels = sgPoints * Screen_ƒDPI(blVert) / 72
End Function
Private Function Screen_ƒDPI(blVert As Boolean) As Long
Static lDPI(0 To 1) As Long, lDC As Long
If lDPI(0) = 0 Then
lDC = GetDC(0)
lDPI(0) = GetDeviceCaps(lDC, 88&)
lDPI(1) = GetDeviceCaps(lDC, 90&)
lDC = ReleaseDC(0, lDC)
End If
Screen_ƒDPI = lDPI(Abs(blVert))
End Function
Copy the procedures above in a standard module then copy this procedure in a separated module
Option Explicit
Sub Shape_Coordinates_Get_TEST()
Dim ws As Worksheet
Dim sp As Shape
Dim uSpCrds As RgCrds
Rem Set Target Worksheet Active Window
Set ws = ThisWorkbook.Worksheets("SO_Q50293831") 'replace as required
With ws
.Activate
Set sp = .Shapes("SH03G13BACK")
End With
Rem Get Shape Coordinates
If Not (Shape_ƒCoordinates_Get(uSpCrds, sp)) Then Exit Sub 'might want to add a message
Rem Apply Shape Coordinates
With uSpCrds
SetCursorPos .Left, .Top: Stop ' Mouse is now at the Shape's TopLeft corner
SetCursorPos .Left, .Bottom: Stop ' Mouse is now at the Shape's LeftBottom corner
SetCursorPos .Right, .Top: Stop ' Mouse is now at the Shape's RightTop corner
SetCursorPos .Right, .Bottom: Stop ' Mouse is now at the Shape's BottomRigh corner
End With
End Sub
For additional information about the resources used visit these pages:
GetDeviceCaps function
GetDC function
ReleaseDC function
Visual Basic Procedure to Get/Set Cursor Position
NEW EDITED VERSION
Take a look at the following code. The core idea is to use RangeFromPoint, that returns the Shape or Range object that is positioned at the specified pair of screen coordinates.
There logical steps are:
1) get the clicking position and screen dimensions (in pixels).
2) get the first two cells in the visible range that belongs to different row/column, and get their 'excel' position as well as their pixel position.
3) Calculate the relation between 'Excel units' and pixels.
4) scan all shapes in worksheet, take their excel position and calculate their pixel position.
Although a little verbose (not too long, if you delete all lines for writing variables to sheet), I think the code is rather straight, without the need for positioning shapes along cells or checking zoom or similar. You can have many shapes in the sheet, and assign the code to all of them.
The only requirement is that the four cell on the top-left corner of the visible window must not be covered by a shape.
The below code is writing the different variables in the sheet, just for clarity.
Private Declare Function GetCursorPos Lib "user32" (ByRef lpPoint As POINT) As Long
Private Type POINT
x As Long
y As Long
End Type
Public Declare Function GetSystemMetrics Lib "user32.dll" (ByVal index As Long) As Long
Public Const SM_CXSCREEN = 0
Public Const SM_CYSCREEN = 1
Sub GetPixelsFromImageBorder()
Dim pLocation As POINT
Dim objShape As Object
Dim ScreenWidth As Integer
Dim ScreenHeight As Integer
Dim xPix As Integer, yPix As Integer
Dim Cell_1_X As Double, Cell_1_Y As Double
Dim Cell_2_X As Double, Cell_2_Y As Double
Dim Cell_1_Row As Integer, Cell_1_Col As Integer
Dim Cell_2_Row As Integer, Cell_2_Col As Integer
Dim Cell_1_X_Pix As Double, Cell_1_Y_Pix As Double
Dim Cell_2_X_Pix As Double, Cell_2_Y_Pix As Double
Dim Y0 As Double, X0 As Double
Dim SlopeX As Double, SlopeY As Double
Dim flg1 As Boolean, flg2 As Boolean, flg3 As Boolean
Dim WhichWS As Worksheet
Dim w As Window, r As Range, cll As Range
Dim Shp As Shape
Call GetCursorPos(pLocation)
Set WhichWS = Worksheets("Sheet1")
WhichWS.Range("A1:H20").ClearContents
ScreenWidth = GetSystemMetrics(SM_CXSCREEN)
ScreenHeight = GetSystemMetrics(SM_CYSCREEN)
ClickX = pLocation.x
ClickY = pLocation.y
WhichWS.Cells(3, 1) = "Variable"
WhichWS.Cells(3, 1).Font.Bold = True
WhichWS.Cells(3, 2) = "X"
WhichWS.Cells(3, 2).Font.Bold = True
WhichWS.Cells(3, 3) = "Y"
WhichWS.Cells(3, 3).Font.Bold = True
WhichWS.Cells(4, 1) = "Screen (in pixels): "
WhichWS.Cells(4, 2) = ScreenWidth
WhichWS.Cells(4, 3) = ScreenHeight
WhichWS.Cells(5, 1) = "Mouse clicked on (in pixels): "
WhichWS.Cells(5, 2) = ClickX
WhichWS.Cells(5, 3) = ClickY
Set w = ActiveWindow
Set r = w.VisibleRange
i = 1
For Each cll In r.Cells
If i = 1 Then
'get top and right pos (in excel units) of first cell in visible range
'also get row and column of that cell
Cell_1_Y = cll.Top
Cell_1_X = cll.Left
Cell_1_Row = cll.Row
Cell_1_Col = cll.Column
i = i + 1
ElseIf cll.Row > Cell_1_Row And cll.Column > Cell_1_Col Then
'get top and right pos (in excel units) of second cell in visible range
'also get row and column of that cell
Cell_2_Y = cll.Top
Cell_2_X = cll.Left
Cell_2_Row = cll.Row
Cell_2_Col = cll.Column
Exit For
End If
Next
On Error Resume Next
flg1 = False
flg2 = False
flg3 = False
For yPix = 1 To ScreenHeight
For xPix = 1 To ScreenWidth
Set objShape = ActiveWindow.RangeFromPoint(xPix, yPix)
If Not objShape Is Nothing Then
If TypeName(objShape) = "Range" Then
If objShape.Column = Cell_1_Col And objShape.Row = Cell_1_Row Then
'get top and right pos (in pix) of first cell in visible range
If flg2 = False Then
Cell_1_X_Pix = xPix
Cell_1_Y_Pix = yPix
flg2 = True
End If
ElseIf objShape.Column = Cell_2_Col And objShape.Row = Cell_2_Row Then
'get top and right pos (in pix) of second cell in visible range
If flg3 = False Then
Cell_2_X_Pix = xPix
Cell_2_Y_Pix = yPix
flg3 = True
flg1 = True 'exit of outer loop
Exit For 'exit inner loop (this)
End If
End If
End If
End If
Next
If flg1 = True Then Exit For
Next
'Calculate the relation between pixels and 'excel position'
SlopeY = (Cell_2_Y_Pix - Cell_1_Y_Pix) / (Cell_2_Y - Cell_1_Y)
Y0 = Cell_1_Y_Pix - SlopeY * Cell_1_Y
SlopeX = (Cell_2_X_Pix - Cell_1_X_Pix) / (Cell_2_X - Cell_1_X)
X0 = Cell_1_X_Pix - SlopeX * Cell_1_X
'print some variables in sheet
WhichWS.Cells(6, 1) = "Variable"
WhichWS.Cells(6, 1).Font.Bold = True
WhichWS.Cells(6, 2) = "X Pos (excel units)"
WhichWS.Cells(6, 2).Font.Bold = True
WhichWS.Cells(6, 3) = "Y Pos (excel units)"
WhichWS.Cells(6, 3).Font.Bold = True
WhichWS.Cells(6, 4) = "X Pos (pixels)"
WhichWS.Cells(6, 4).Font.Bold = True
WhichWS.Cells(6, 5) = "Y Pos (pixels)"
WhichWS.Cells(6, 5).Font.Bold = True
WhichWS.Cells(6, 6) = "X Dist. from click (pixels)"
WhichWS.Cells(6, 6).Font.Bold = True
WhichWS.Cells(6, 7) = "Y Dist. from click (pixels)"
WhichWS.Cells(6, 7).Font.Bold = True
i = 7
For Each Shp In WhichWS.Shapes
WhichWS.Cells(i, 1) = Shp.Name
WhichWS.Cells(i, 2) = Shp.Left
WhichWS.Cells(i, 3) = Shp.Top
PosInPixX = X0 + Shp.Left * SlopeX
PosInPixY = Y0 + Shp.Top * SlopeY
DistFromClickX = ClickX - PosInPixX
DistFromClickY = ClickY - PosInPixY
WhichWS.Cells(i, 4) = Round(PosInPixX, 2)
WhichWS.Cells(i, 5) = Round(PosInPixY, 2)
WhichWS.Cells(i, 6) = DistFromClickX
WhichWS.Cells(i, 7) = DistFromClickY
i = i + 1
Next Shp
End Sub
You are nearly there with your code. However Excel App has ribbon that takes some space. In this case, ActiveWindow.PointsToScreenPixelsX(0) and ActiveWindow.PointsToScreenPixelsY(0) will return your worksheet starting pixels relative to the screen.
Now the (mousePos) - (worksheet position) - (left and top of the shapeIn Pixel) will give you mouse position relative to your shape.
Try this code:
Public Function SH03G13()
Dim point As POINTAPI: GetCursorPos point
Dim rectang As Shape: Set rectang = ThisWorkbook.Sheets("Sheet1").Shapes("SH03G13BACK")
Debug.Print "Mouse pointer relative to screen:", point.X, point.Y
Debug.Print "Mouse pointer relative to app:", (point.X - ActiveWindow.PointsToScreenPixelsX(0)), (point.Y - ActiveWindow.PointsToScreenPixelsY(0))
Debug.Print "Mouse pointer relative to shape:", ((point.X - ActiveWindow.PointsToScreenPixelsX(0)) - PointToPixel(rectang.Left)), ((point.Y - ActiveWindow.PointsToScreenPixelsY(0)) - PointToPixel(rectang.Top))
Dim ABCISSA As Long: ABCISSA = point.X - rectang.Left
Dim ORDENAD As Long: ORDENAD = point.Y - rectang.Top
'Debug.Print ABCISSA & " " & ORDENAD
End Function
Public Function PointToPixel(point As Double) As Double
'Converts points to pixel
If point > 0 Then PointToPixel = Round((1.33333333333333 * point), 2) Else PointToPixel = 0
End Function
results in your immediate window will be:
Mouse pointer relative to screen: 410 356
Mouse pointer relative to app: 384 313
Mouse pointer relative to shape: 0 0
Note: You might get -1 coordinates, that's because the on click event fires even if you click slightly away from the shape. You can catch this easily in your function.
Related
I'm flying a drone, and would like to be able to point out things on a picture by adding an arrow with a single click. By clicking an arrow will be added and the end will point towards the closest corner. When clicking on a picture the macro draw arrow will be run. The code is able to run (most times), however I do have some problems.
Picture of the final result, where the tip of the arrow is inserted at the mouse location when clicked at the picture
The first one is that for embed the arrow in the original picture I select both pictures and do a copy paste operation. Then I delete the old picture and the arrow. There is probably a smarter way of doing it. Sometimes in the operation of pasting the picture in to a cell a error happens : Error 1004, Microsoft Excel cannot insert the data.
The problem occurs in the Sub SaveFigure where a picture is moved to a chart and is saved external and in the main sub drawArrow at the line "ActiveSheet.Pictures.Paste.Select".
Another problem that I have, is I have big difficulties to avoid using .select. I have tried to set the combined picture = an object. I do not know how to insert it in the sheet agian. Do anyone know how to do that?
I have inserted the whole code below, if other are facing the same problem of embed figures in picture.
Edit: I had forgot some data types and functions. They are in the code now.
Type RECT
Left As Long: Top As Long: Right As Long: Bottom As Long
End Type
Type POINTAPI
Xcoord As Long: Ycoord As Long
End Type
Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hDC As Long) As Long
Declare PtrSafe Function GetSystemMetrics Lib "user32.dll" (ByVal nIndex As Long) As Long
Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long
Declare PtrSafe Function GetCursorPos Lib "user32" (Point As POINTAPI) As Long
Sub drawArrow()
Dim PictureName As String, ArrowName As String, TempName As String
Dim CellLoacation As Collection
Dim Pointlocation As Collection
Dim CellX As Integer, CellY As Integer, CompensateX As Integer, CompensateY As Integer
Dim PicX As Single, PicY As Single, MouseX As Single, MouseY As Single, PicHeight As Single, PicWidth As Single
Dim Arrow As Shape, EditedShape As Shape
Dim strImageName As String
PictureName = shapename
Set CellLoacation = PictureLocatedInCell(PictureName)
CellX = CellLoacation.Item(1) ' the cells x position
CellY = CellLoacation.Item(2) ' the cells y position
PicX = CellLoacation.Item(3) ' the pictures x position
PicY = CellLoacation.Item(4) ' the pictures x position
PicWidth = CellLoacation.Item(5) 'width of the picture
PicHeight = CellLoacation.Item(6) 'Height of the picture
Set Pointlocation = SH03G13(PictureName, CellX, CellY)
MouseX = Pointlocation.Item(1) 'Where the mouse is located at x in pt
MouseY = Pointlocation.Item(2) 'Where the mouse is located at y in pt
CompensateX = ArrowXEndPoint(MouseX, PicWidth) ' Taking zoom into account
CompensateY = ArrowYEndPoint(MouseY, PicHeight) ' Taking zoom into account
Set Arrow = ActiveSheet.Shapes.AddConnector(msoConnectorStraight, PicX + CompensateX, PicY + CompensateY, PicX + MouseX, PicY + MouseY + Round(CellX / 7, 0))
ArrowName = Arrow.name
Arrow.Line.EndArrowheadStyle = msoArrowheadTriangle
Arrow.ShapeStyle = msoLineStylePreset1
With Arrow.Line
.Visible = msoTrue
.ForeColor.RGB = RGB(255, 0, 0)
.Transparency = 0
End With
ActiveSheet.Shapes.Range(Array(ArrowName, _
PictureName)).Select ' select both arrow and picture
Selection.CopyPicture Appearance:=xlScreen, Format:=xlPicture
ActiveSheet.Pictures.Paste.Select ' insert picture in sheet - this is where it fails
ActiveSheet.Pictures.OnAction = "drawArrow" ' enable macro
TempName = Selection.name
ActiveSheet.Shapes.Range(Array(ArrowName, _
PictureName)).Select
Selection.Delete' delete old picture and arrow
ActiveSheet.Shapes(TempName).Left = Sheets("Input").Cells(CellX, CellY).Left
ActiveSheet.Shapes(TempName).Top = Sheets("Input").Cells(CellX, CellY).Top
SaveFigure TempName, CellX
End Sub
Sub SaveFigure(TempName, CellX) 'gemmer figuren i en undermappe til hovedmappen
Dim chtObj As ChartObject
S_PATH = Sheets("Data").Range("E1").Value
With ThisWorkbook.Worksheets("Input")
.Activate
Set chtObj = .ChartObjects.add(0, 0, .Shapes(TempName).Width, .Shapes(TempName).Height)
chtObj.name = "TemporaryPictureChart"
'ActiveSheet.Shapes.Range(Array(TempName)).Copy
ActiveSheet.Shapes.Range(Array(TempName)).Select
Selection.Copy
ActiveSheet.ChartObjects("TemporaryPictureChart").Activate
ActiveChart.Paste
ActiveChart.Export fileName:=S_PATH & "\arrowPics\" & Sheets("Input").Cells(CellX, 1), FilterName:="jpg"
chtObj.Delete
End With
End Sub
Function ArrowYEndPoint(MouseY, PicHeight) As Integer ' where to set the end point of the arrow
If (MouseY < PicHeight / 2) Then
If (MouseY < 100) Then
ArrowYEndPoint = 0
Else
ArrowYEndPoint = MouseY - 75
End If
ElseIf (MouseY > PicHeight / 2) Then
If (MouseY > PicHeight - 100) Then
ArrowYEndPoint = PicHeight
Else
ArrowYEndPoint = MouseY + 75
End If
End If
End Function
Function ArrowXEndPoint(MouseX, PicWidth) As Integer where to set the end point of the arrow
If (MouseX < PicWidth / 2) Then
If (MouseX < 100) Then
ArrowXEndPoint = 0
Else
ArrowXEndPoint = MouseX - 75
End If
ElseIf (MouseX > PicWidth / 2) Then
If (MouseX > PicWidth - 100) Then
ArrowXEndPoint = PicWidth
Else
ArrowXEndPoint = MouseX + 75
End If
End If
End Function
Function PictureLocatedInCell(PictureName As String) As Collection ' find picture based on name
Dim PictureToChange As Shape: Set PictureToChange = Sheets("Input").Shapes(shapename)
Dim var As Collection
Set var = New Collection
var.add FindCellBasedOnTop(PictureToChange.Top, PictureToChange.Left)
var.add FindCellBasedOnLeft(PictureToChange.Top, PictureToChange.Left)
var.add PictureToChange.Left
var.add PictureToChange.Top
var.add PictureToChange.Width
var.add PictureToChange.Height
Set PictureLocatedInCell = var
End Function
Function ScreenDPI(bVert As Boolean) As Long
Static lDPI&(1), lDC&
If lDPI(0) = 0 Then
lDC = GetDC(0)
lDPI(0) = GetDeviceCaps(lDC, 88&)
lDPI(1) = GetDeviceCaps(lDC, 90&)
lDC = ReleaseDC(0, lDC)
End If
ScreenDPI = lDPI(Abs(bVert))
End Function
Function PTtoPX(Points As Single, bVert As Boolean) As Long
PTtoPX = Points * ScreenDPI(bVert) / 72
End Function
Function PXtoPT(Pixels As Long, bVert As Boolean) As Single
PXtoPT = Pixels / (ScreenDPI(bVert) / 72)
End Function
Sub GetRangeRect(ByVal rng As Range, ByRef rc As RECT)
Dim wnd As Window: Set wnd = rng.Parent.Parent.Windows(1)
With rng
rc.Left = PTtoPX(.Left * wnd.Zoom / 100, 0) + wnd.PointsToScreenPixelsX(0)
rc.Top = PTtoPX(.Top * wnd.Zoom / 100, 1) + wnd.PointsToScreenPixelsY(0)
rc.Right = PTtoPX(.Width * wnd.Zoom / 100, 0) + rc.Left
rc.Bottom = PTtoPX(.Height * wnd.Zoom / 100, 1) + rc.Top
End With
End Sub
Function SH03G13(shapename, CellX, CellY) As Collection
Dim wnd As Window
Dim var As Collection
Set var = New Collection
With ThisWorkbook.Sheets("Input")
Dim AreaRng As Range: Set AreaRng = .Range(.Cells(CellX, CellY), .Cells(CellX, CellY))
Dim rectang As Shape: Set rectang = .Shapes(shapename)
'rectang.Height = AreaRng.Height
'rectang.Width = AreaRng.Width
'rectang.Top = AreaRng.Top
'rectang.Left = AreaRng.Left
DoEvents
Dim Point As POINTAPI: GetCursorPos Point
Dim rc As RECT: Call GetRangeRect(.Cells(CellX, CellY), rc)
Dim ABCISSA As Long: ABCISSA = Point.Xcoord - rc.Left
Dim ORDENAD As Long: ORDENAD = Point.Ycoord - rc.Top
End With
'MsgBox "x: " & ABCISSA & ", y: " & ORDENAD
Set wnd = Cells(CellX, CellY).Parent.Parent.Windows(1)
'Debug.Print "Zoom " & wnd.Zoom / 100
var.add PXtoPT(ABCISSA / (wnd.Zoom / 100), 0)
var.add PXtoPT(ORDENAD / (wnd.Zoom / 100), 0)
Set SH03G13 = var
End Function
Function FindCellBasedOnTop(Top, Left) As Integer
FindCellBasedOnTop = Round((Top - Sheets("Input").Rows("1:1").RowHeight) / Sheets("Input").Rows("2:2").RowHeight, 0) + 2
End Function
Function FindCellBasedOnLeft(Top, Left) As Integer
FindCellBasedOnLeft = Round((Left - Sheets("Input").Columns("A").ColumnWidth) / Sheets("Input").Columns("B").ColumnWidth, 0) + 1
End Function
Public Function shapename() As String
Dim ActiveShape As Shape
Dim ButtonName As String 'Get Name of Shape that initiated this macro
ButtonName = Application.Caller
'Set variable to active shape
Set ActiveShape = ActiveSheet.Shapes(ButtonName)
shapename = ActiveShape.name
End Function
If there is something which is unclear please tell me so.
Help will greatly be appreciated
The problem is that you are copy and pasting right after each other. If you have a break in between the kode will be able to run.
Selection.CopyPicture Appearance:=xlScreen, Format:=xlPicture
Sleep 500
ActiveSheet.Pictures.Paste.Select ' insert picture in sheet - this is where it fails
And the add this in the top of the sheet.
If VBA7 Then ' Excel 2010 or later
Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal Milliseconds As LongPtr)
Else ' Excel 2007 or earlier
Public Declare Sub Sleep Lib "kernel32" (ByVal Milliseconds As Long)
End If
BR
I am in the process of building an Excel based Application that builds itself dynamically at run-time based on external data.
Here is the empty userform:
Code within UserForm_Activate()
Private Sub UserForm_Activate()
Dim f As Control, i As Integer
mdMenuItems.BuildMenuItems
mdTheme.GetTheme
For Each f In Me.Controls
If TypeName(f) = "Frame" Then
i = i + 1
ReDim Preserve fra(1 To i)
Set fra(i).fraEvent1 = f
End If
Next f
End Sub
mdMenuItems.BuildMenuItems dynamically builds a series of menu items based on external data...
Code within mdMenuItems module
Option Explicit
Dim lbl() As New cMenuItem
Public myFileData As String
Public myFileValue As String
Public frmTheme As String
Sub BuildMenuItems()
Dim FileNum As Integer, i As Integer
Dim WrdArray() As String
Dim lblMenuIcon As MSForms.Label, lblMenuText As MSForms.Label, lblMenuBackground As MSForms.Label
FileNum = FreeFile()
Open Application.ThisWorkbook.Path & "\Data\MenuItems.csv" For Input As #FileNum
Do While Not EOF(FileNum)
i = i + 1
Line Input #FileNum, myFileData ' read in data 1 line at a time
WrdArray() = Split(myFileData, ",")
Set lblMenuBackground = frmTest.frmMenuBackground.Controls.Add("Forms.Label.1", "lblMenuBackground_" & i)
Set lblMenuIcon = frmTest.frmMenuBackground.Controls.Add("Forms.Label.1", "lblMenuIcon_" & i)
Set lblMenuText = frmTest.frmMenuBackground.Controls.Add("Forms.Label.1", "lblMenuText_" & i)
With lblMenuBackground
.top = 30 * i
.left = 0
.Width = 170
.Height = 30
.BackColor = RGB(255, 255, 255)
.BackStyle = fmBackStyleOpaque
.MousePointer = fmMousePointerCustom
.MouseIcon = LoadPicture(Application.ThisWorkbook.Path & "\Creative\Other\Hand.cur")
.Tag = "_006"
End With
ReDim Preserve lbl(1 To i)
Set lbl(i).lblEvent1 = lblMenuBackground
With lblMenuIcon
.Caption = Sheets("FontAwesome").Cells(WrdArray(0), 1)
.top = (30 * i) + 9
.left = 0
.Width = 30
.Height = 20
.ForeColor = RGB(0, 0, 0)
.BackStyle = fmBackStyleTransparent
.Font.Name = "FontAwesome"
.Font.Size = 14
.TextAlign = fmTextAlignCenter
.MousePointer = fmMousePointerCustom
.MouseIcon = LoadPicture(Application.ThisWorkbook.Path & "\Creative\Other\Hand.cur")
.Tag = "-021"
End With
With lblMenuText
.Caption = WrdArray(1)
.top = (30 * i) + 8
.left = 30
.Width = 90
.Height = 20
.ForeColor = RGB(0, 0, 0)
.BackStyle = fmBackStyleTransparent
.Font.Size = 12
.MousePointer = fmMousePointerCustom
.MouseIcon = LoadPicture(Application.ThisWorkbook.Path & "\Creative\Other\Hand.cur")
.Tag = "-021"
End With
Loop
Close #FileNum
End Sub
Ok, so a brief overview of whats happeing here...
I open a data file MenuItems.csv for input. I assign each line within this file to i. I then Set three individual MSForms.Label(s):
lblMenuBackground
lblMenuIcon
lblMenuText
...and build them asynchronously.
You will notice that after building the first label (lblMenuBackground), I assign a custom class event lbl(i).lblEvent1 = lblMenuBackground.
(It is important that I use ReDim Preserve correctly here so that each sequential menu item gains this custom class, and not just the last one.)
Code within cMenuItem class module
Public WithEvents lblEvent1 As MSForms.Label
Private Sub lblEvent1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Dim ctl As Control
For Each ctl In frmTest.frmMenuBackground.Controls
If TypeName(ctl) = "Label" Then
If Not ctl.BackColor = RGB(GetB(mdTheme.frmThemeID6), GetG(mdTheme.frmThemeID6), GetR(mdTheme.frmThemeID6)) Then ctl.BackColor = RGB(GetB(mdTheme.frmThemeID6), GetG(mdTheme.frmThemeID6), GetR(mdTheme.frmThemeID6))
End If
Next ctl
Me.lblEvent1.BackColor = RGB(GetB(mdTheme.frmThemeID2), GetG(mdTheme.frmThemeID2), GetR(mdTheme.frmThemeID2))
End Sub
(Please ignore the .BackColor property complexity here as it could get even more confusing, and is un-related to this question.)
After UserForm_Activate, here is the updated form:
(You may notice the use of FontAwesome icons here.)
Because I have added a custom MouseOver class event to each lblMenuBackground label, mousing over causes the .BackColor to change:
Here is my issue...
This mouse over effect is only triggered when the cursor passes over one of the three labels that make up each menu item.
lblMenuBackground
Why?
I only know how to affect the called control's properties.
Or rather...
I don't know how to affect un-called control properties from within the called control's event.
Here is the structure of each menu item:
Here is my question...
How can I affect the .BackColor of the same control from the MouseOver events of all three individual controls which make up each menu item?
Moves cursor over icon = Background colour changes
Moves cursor over text = Background colour changes
Moves cursor over background = Background colour changes
The class event needs to be assigned at build time...
ReDim Preserve lbl(1 To i)
Set lbl(i).lblEvent1 = lblMenuBackground
...for each menu item.
EndSubQuestion
__________
This logic will fundamentally lay the foundations for my interface.
For those of you who made it this far - thank you for reading!
Any help is appreciated.
Thanks,
Mr. J
You are on hooking into the events for lblMenuBackground
lbl(i).lblEvent1 = lblMenuBackground
Modify BuildMenuItems
Change
Set lbl(i).lblEvent1 = lblMenuBackground
to
Set lbl(i) = New cMenuItem
lbl(i).setControls lblMenuBackground, lblMenuIcon, lblMenuText
Modify CMenuItem Class
Public WithEvents m_lblMenuBackground As MSForms.Label
Public WithEvents m_lblMenuIcon As MSForms.Label
Public WithEvents m_lblMenuText As MSForms.Label
Public Sub setControls(lblMenuBackground As MSForms.Label, lblMenuIcon As MSForms.Label, lblMenuText As MSForms.Label)
Set m_lblMenuBackground = lblMenuBackground
Set m_lblMenuIcon = lblMenuIcon
Set m_lblMenuText = lblMenuText
End Sub
Private Sub m_lblMenuBackground_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Update
End Sub
Private Sub m_lblMenuIcon_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Update
End Sub
Private Sub m_lblMenuText_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Update
End Sub
Private Sub Update()
Dim ctl As Control
For Each ctl In frmTest.frmMenuBackground.Controls
If TypeName(ctl) = "Label" Then
If Not ctl.BackColor = RGB(GetB(mdTheme.frmThemeID6), GetG(mdTheme.frmThemeID6), GetR(mdTheme.frmThemeID6)) Then ctl.BackColor = RGB(GetB(mdTheme.frmThemeID6), GetG(mdTheme.frmThemeID6), GetR(mdTheme.frmThemeID6))
End If
Next ctl
Me.m_lblMenuBackground.BackColor = RGB(GetB(mdTheme.frmThemeID2), GetG(mdTheme.frmThemeID2), GetR(mdTheme.frmThemeID2))
End Sub
I was researching different components of VBA and APIs and came across a website that proposes a personalized API that runs the game Pong within a spreadsheet that contains certain subs and functions. The instance used was with Windows/Excel version 97 and is stated to not be compatible with the 2000 version (and, I am assuming, the current version). I was wondering if any VBA-savvy individuals who thought this was worth doing could let me know whether this is a function capable of implementing under the current iteration of Excel, and if so, what the workaround would be?
When I use the following code below, I get a run-time error declaring that
vba332.dll is missing
the debugger highlights the 9th line of the Public Function AddrOf which states:Call GetCurrentVbaProject (hProject) is the error line and (hProject) when hovered on is 0, which I am assuming is also a problem as it is supposed to be getting a value other than 0 to move forward...
According to some reading that I have found, the newer version of the referenced .dll would be something like:
vbe7.dll
But when I have substituted that line within this string of codes, it still does not return any data for the Declare Function.
This seemed like a fun API to play around with, but I could not devise a way to upgrade it to the current Excel version. The code:
Option Explicit
Private Declare Function GetCurrentVbaProject _
Lib "vba332.dll" Alias "EbGetExecutingProj" _
(hProject As Long) As Long
Private Declare Function GetFuncID Lib "vba332.dll" Alias "TipGetFunctionId" _
(ByVal hProject As Long, ByVal strFunctionName As String, _
ByRef strFunctionId As String) As Long
Private Declare Function GetAddr _
Lib "vba332.dll" Alias "TipGetLpfnOfFunctionId" _
(ByVal hProject As Long, ByVal strFunctionId As String, _
ByRef lpfn As Long) As Long
Private Declare Function SetTimer Lib "user32" _
(ByVal hwnd As Long, ByVal nIDEvent As Long, _
ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32" _
(ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Private lngTimerId As Long
Dim Paddle As Shape
Dim Ball As Shape
Dim nVertical As Integer
Dim nHorizontal As Integer
Dim nSpeed As Integer
Sub Auto_Open()
Application.OnKey "{F12}", "StartPong"
End Sub
Sub Auto_Close()
Timer_Terminate
On Error Resume Next
Paddle.Delete
Ball.Delete
End Sub
Sub StartPong()
Dim nLeft As Integer
Dim nTop As Integer
Dim nWidth As Integer
Dim nHeight As Integer
'Draw the paddle
nLeft = ActiveWindow.UsableWidth - 100
nTop = ActiveWindow.UsableHeight - 30
nWidth = 50
nHeight = 10
Set Paddle = ActiveSheet.Shapes.AddShape(1, nLeft, nTop, nWidth, nHeight)
Paddle.Fill.ForeColor.SchemeColor = 8
'Draw the ball
nLeft = CInt(ActiveWindow.UsableWidth / 2) - 20
nTop = 0
nWidth = 15
nHeight = 15
Set Ball = ActiveSheet.Shapes.AddShape(9, nLeft, nTop, nWidth, nHeight)
Ball.Fill.ForeColor.SchemeColor = 8
'Define keys
Application.OnKey "{ESC}", "EndPong"
Application.OnKey "{RIGHT}", "MoveRight"
Application.OnKey "{LEFT}", "MoveLeft"
Application.OnKey "{F12}"
'Set speed
nVertical = 10 'Ball Vertical
nHorizontal = 10 'Ball Horizontal
nSpeed = 18 'Paddle Horizontal
'Start the ball movement timer
Timer_Initialize (15) 'Ball will be moved every 15 milliseconds
'Now we wait for events to move things
End Sub
Sub MoveBall()
Dim nLeft As Integer
Dim nTop As Integer
With Ball
'Move Horizontal
.Left = .Left + nHorizontal
'Move vertical
.Top = .Top + nVertical
'Bounce horizontal
nLeft = .Left
If nLeft > (ActiveWindow.UsableWidth - 50) Then
nHorizontal = -1 * Abs((nHorizontal))
End If
If nLeft < 20 Then
nHorizontal = Abs(nHorizontal)
End If
'Bounce vertical
nTop = .Top
If nTop > (ActiveWindow.UsableHeight - 50) Then
nVertical = -1 * (Abs(nVertical))
'Did Paddle hit it?
If (.Left + (.Width / 2)) > Paddle.Left And _
(.Left + (.Width / 2)) < (Paddle.Left + Paddle.Width) Then
'Paddle hit the ball
If (.Left + (.Width / 2)) < (Paddle.Left + (Paddle.Width / 3)) Then
'Ball hit paddle on left third; apply english
nHorizontal = nHorizontal - 5
If nHorizontal < -15 Then nHorizontal = -15
End If
If (.Left + (.Width / 2)) > (Paddle.Left + (2 * Paddle.Width / 3)) Then
'Ball hit paddle on right third
nHorizontal = nHorizontal + 5
If nHorizontal > 15 Then nHorizontal = 15
End If
Else
Beep 'missed
'Move the paddle in case window was resized
Paddle.Top = ActiveWindow.UsableHeight - 30
End If
End If
If nTop < 20 Then
nVertical = Abs(nVertical)
End If
End With
End Sub
Sub EndPong()
Timer_Terminate
Application.OnKey "{ESC}"
Application.OnKey "{RIGHT}"
Application.OnKey "{LEFT}"
Application.OnKey "{F12}", "StartPong"
Paddle.Delete
Ball.Delete
End Sub
Sub MoveRight()
Paddle.Left = Paddle.Left + nSpeed
If Paddle.Left > (Application.UsableWidth - 30 - Paddle.Width) Then
Paddle.Left = Application.UsableWidth - 30 - Paddle.Width
End If
End Sub
Sub MoveLeft()
Paddle.Left = Paddle.Left - nSpeed
If Paddle.Left < 0 Then
Paddle.Left = 0
End If
End Sub
Public Function AddrOf(strFuncName As String) As Long
'Returns a function pointer of a VBA public function given its name.
'AddrOf code from Microsoft Office Developer magazine
'http://www.informant.com/mod/index.htm
Dim hProject As Long
Dim lngResult As Long
Dim strID As String
Dim lpfn As Long
Dim strFuncNameUnicode As String
Const NO_ERROR = 0
' The function name must be in Unicode, so convert it.
strFuncNameUnicode = StrConv(strFuncName, vbUnicode)
' Get the current VBA project
Call GetCurrentVbaProject(hProject)
' Make sure we got a project handle
If hProject <> 0 Then
' Get the VBA function ID
lngResult = GetFuncID(hProject, strFuncNameUnicode, strID)
If lngResult = NO_ERROR Then
' Get the function pointer.
lngResult = GetAddr(hProject, strID, lpfn)
If lngResult = NO_ERROR Then
AddrOf = lpfn
End If
End If
End If
End Function
Private Sub TimerProc(ByVal hwnd&, ByVal lngMsg&, ByVal lngTimerId&, ByVal
lngTime&)
Call MoveBall
End Sub
Sub Timer_Initialize(Optional vInterval As Variant)
Dim lngInterval As Long
lngInterval = CLng(vInterval)
If lngInterval = 0 Then lngInterval = 60 '60 milliseconds just a bit longer
than a "tick"
lngTimerId = SetTimer(0, 0, lngInterval, AddrOf("TimerProc"))
If lngTimerId = 0 Then
MsgBox "Unable to initialize a new timer!"
End If
End Sub
Sub Timer_Terminate()
If lngTimerId <> 0 Then
Call KillTimer(0, lngTimerId)
End If
End Sub
Thanks!
In later versions of Office, "TipGetLpfnOfFunctionId" is exposed as AddressOf.
And since you can use AddressOf directly to get the address of a function, you don't need "TipGetFunctionId" nor all the "addrof" code.
Sub Timer_Initialize(Optional vInterval As Variant)
Dim lngInterval As Long
lngInterval = CLng(vInterval)
If lngInterval = 0 Then lngInterval = 60
lngTimerId = SetTimer(0, 0, lngInterval, AddressOf TimerProc)
If lngTimerId = 0 Then
MsgBox "Unable to initialize a new timer!"
End If
End Sub
Note the unique syntax of the "AddressOf" operator: it's not a function.
Private Sub framePDF_MouseMove(ByVal... )
framePDF.BackColor = &H80000012&
So, the frame's color is changing.
I can't find the event to return the color back - when the cursor is away from the frame ?
On a Userform? The Userform also has a MouseMove event that doesn't fire when you're in the Frame.
Private Sub Frame1_MouseMove(ByVal ...)
Me.Frame1.BackColor = vbRed
End Sub
Private Sub UserForm_MouseMove(ByVal ...)
Me.Frame1.BackColor = vbWhite
End Sub
will turn the frame red when you're over it and white when you're not. These events fire constantly, so use them judiciously.
In vba and VB6 there is no MouseLeave event.
The best way to achieve this is to start a timer when the mouse enters the frame.
Then in the timer code check to see if the mouse pointer is still within the bounds of the frame. If not change the colour back and stop the timer
Put this code in a module:
Public Declare Function GetCursorPos Lib "user32" (lpPoint As _
POINTAPI) As Long
Public Type POINTAPI
x As Long
y As Long
End Type
Create a timer on your form, set interval =10 Enbaled = False
Then the code looks something like this:
Private Sub frameTest_MouseMove(Button As Integer, Shift As Integer, x As Single, y As Single)
frameTest.BackColor = vbRed
tmrMouseLeave.Enabled = True
End Sub
Private Sub tmrMouseLeave_Timer()
Dim pt As POINTAPI
Call GetCursorPos(pt)
Dim xValue As Long, yValue As Long
xValue = pt.x * Screen.TwipsPerPixelX
yValue = pt.y * Screen.TwipsPerPixelY
If (xValue > (Me.Left + frameTest.Left)) And _
(xValue < (Me.Left + frameTest.Left + frameTest.width)) And _
(yValue > (Me.Top + frameTest.Top)) And _
(yValue < (Me.Top + frameTest.Top + frameTest.height)) Then
'we are still inside the frame
Else
'mouse is outside the frame
frameTest.BackColor = vbBlue
tmrMouseLeave.Enabled = False
End If
End Sub
Easier way: in your MouseMove event, test the X and Y arguments against the control's width and height (minus a margin, say 5) - if the mouse is in the margin, consider it a "Mouse out" and change the control's colours accordingly. No need for concurrent buttons, z-order manipulation, frames, etc.
See image for clarity.
I have 5 variables (A, B, C, D and E), each of which can range from 0-100. I need the sum of all these variables to be 100 at all times, not more, not less. However, the way it is set up currently, if I change variable A from 21 to, say, 51, the total becomes 130.
How could I set this up such that if I change one variable, the others could automatically compensate for that increase or decrease, such that the total is always 100?
Use the Slider Change events, so that when one slider changes value the others are scaled so values sum to 100
Example code, using 3 sliders - you can scale it to allow for as many sliders as you want
Private UpdateSlider As Boolean
Private Sub ScaleSliders(slA As Double, ByRef slB As Double, ByRef slC As Double)
Dim ScaleFactor As Double
If (slB + slC) = 0 Then
ScaleFactor = (100# - slA)
slB = ScaleFactor / 2
slC = ScaleFactor / 2
Else
ScaleFactor = (100# - slA) / (slB + slC)
slB = slB * ScaleFactor
slC = slC * ScaleFactor
End If
End Sub
Private Sub ScrollBar1_Change()
Dim slB As Double, slC As Double
' UpdateSlider = False
If Not UpdateSlider Then
slB = ScrollBar2.Value
slC = ScrollBar3.Value
ScaleSliders ScrollBar1.Value, slB, slC
UpdateSlider = True
ScrollBar2.Value = slB
ScrollBar3.Value = slC
UpdateSlider = False
End If
End Sub
Private Sub ScrollBar2_Change()
Dim slB As Double, slC As Double
If Not UpdateSlider Then
slB = ScrollBar1.Value
slC = ScrollBar3.Value
ScaleSliders ScrollBar2.Value, slB, slC
UpdateSlider = True
ScrollBar1.Value = slB
ScrollBar3.Value = slC
UpdateSlider = False
End If
End Sub
Private Sub ScrollBar3_Change()
Dim slB As Double, slC As Double
If Not UpdateSlider Then
slB = ScrollBar1.Value
slC = ScrollBar2.Value
ScaleSliders ScrollBar1.Value, slB, slC
UpdateSlider = True
ScrollBar1.Value = slB
ScrollBar2.Value = slC
UpdateSlider = False
End If
End Sub
Note that sliders data type in integer, so you may need to allow for rounding not summing to exactly 100
Thx Chris for posting your solution. To scale it to six, I've made this. I'm no VBA expert, this code is not yet really clean or great. but it might help someone.
Private UpdateSlider As Boolean
Private Sub ScaleSliders_arr(slider_value As Double, ByRef other_sliders() As Double)
Dim scale_factor As Double
Dim total_other_sliders As Double
Dim element As Variant
Dim i As Integer
Dim other_sliders_arr_length As Long
For Each element In other_sliders
total_other_sliders = total_other_sliders + element
Debug.Print total_other_sliders
Next element
' when all other values are 0
If total_other_sliders = 0 Then
ScaleFactor = (100# - slider_value)
other_sliders_arr_length = ArrayLength(other_sliders)
i = 0
For Each element In other_sliders
other_sliders(i) = ScaleFactor / other_sliders_arr_length
i = i + 1
Next element
Debug.Print other_sliders_arr_length
' When other sliders have >0 as a total sum
Else
ScaleFactor = (100# - slider_value) / total_other_sliders
' Adjust other sliders according to current value
i = 0
For Each element In other_sliders
other_sliders(i) = other_sliders(i) * ScaleFactor
i = i + 1
Next element
End If
End Sub
Private Sub AdjustSliderByMagic(this_slider As Variant)
Dim slider_value As Double
Dim other_sliders() As Double
Dim cell_locations() As Variant
Dim other_sliders_arr_size As Integer
Dim value As Variant
Dim i As Integer
Dim k As Integer
' which cells contain the values - this also determines number of rows
cell_locations = Array("HiddenTable!B2", "HiddenTable!B3", "HiddenTable!B4", "HiddenTable!B5", "HiddenTable!B6", "HiddenTable!B7")
' size of the others is minus 2 because A) counting starts at 0 B) one slider is the current one which is not the other
other_sliders_arr_size = ArrayLength(cell_locations) - 2
' need to size the other sliders array
ReDim other_sliders(other_sliders_arr_size)
' start loops with 0's
i = 0
k = 0
' Determine the value of this slider and of the other sliders
For Each value In cell_locations
If this_slider = cell_locations(i) Then
slider_value = Range(cell_locations(i)).value
Else
other_sliders(k) = Range(cell_locations(i)).value
k = k + 1
End If
i = i + 1
Next value
' use function to determine slider values
ScaleSliders_arr slider_value, other_sliders
UpdateSlider = True
' start loops with 0's
i = 0
k = 0
' change the values of the other sliders
For Each value In cell_locations
If this_slider = cell_locations(i) Then
'do nothing
Else
Range(cell_locations(i)).value = other_sliders(k)
k = k + 1
End If
i = i + 1
Next value
End Sub
Private Sub ScrollBar1_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B2"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar2_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B3"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar3_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B4"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar4_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B5"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar5_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B6"
AdjustSliderByMagic (this_slider)
End Sub
Private Sub ScrollBar6_Change()
Dim this_slider As Variant
' what is the connected field of this slider
this_slider = "HiddenTable!B7"
AdjustSliderByMagic (this_slider)
End Sub
Function ArrayLength(arr As Variant) As Long
On Error GoTo eh
' Loop is used for multidimensional arrays. The Loop will terminate when a
' "Subscript out of Range" error occurs i.e. there are no more dimensions.
Dim i As Long, length As Long
length = 1
' Loop until no more dimensions
Do While True
i = i + 1
' If the array has no items then this line will throw an error
length = length * (UBound(arr, i) - LBound(arr, i) + 1)
' Set ArrayLength here to avoid returing 1 for an empty array
ArrayLength = length
Loop
Done:
Exit Function
eh:
If Err.Number = 13 Then ' Type Mismatch Error
Err.Raise vbObjectError, "ArrayLength" _
, "The argument passed to the ArrayLength function is not an array."
End If
End Function