PHV.FSM
XLDnaute Nouveau
Dans le cadre de développement d'application, il y a des objets que l'on souhaite positionner par rapport à une cellule mais qui sont positionnés par rapport à l'écran et dont les coordonnées sont souvent exprimées en pixel. Par exemple les UserForms ou le curseur de la souris. J'ai cru que la solution était simple et j'ai trouvé des solutions sur le net qui s'avéraient au final très spécifiques à une configuration particulière et qui ne donnaient pas un résultat universel.
Voici le code préalable nécessaire pour utiliser la procédure principale : GetGridPos.
Ainsi que le code suivant pour définir la taille du séparateur entre les panneaux :
Et enfin il faut inclure dans sa procédure d'initialisation le code suivant :
Si vous utilisé la fonction GetGridPos avec l'indexe de panneau actif numéro 1, les entêtes d'affichage sont prisent en compte par les fonctions PointsToScreenPixelsX et PointsToScreenPixelsY mais sinon ce n'est pas le cas, la largeur de ces entêtes est variable, et je n'ai pas trouver de moyen d' interroger leur dimension.
Du coup, il est plus simple de ne pas afficher les entêtes ... (jusqu'à trouver un code alternatif).
Sauf exception, si vous n'utilisé pas de panneau.
Il est tout d'abord nécessaire de pouvoir convertir les grandeurs physiques exprimées en point, en pixel et cette valeur que je nomme PxToPt (Single) doit être calculé au travers une procédure d'initialisation 'GetScreenData'; contrairement à ce que j'avais lu sur une solution ou il s'agissait d'une valeur constante car cela change en fonction de l'écran ou de la résolution.
Il existe la fonction suivante pour calculer la position sur l'écran d'une position ou grandeur sur la grille : Application.ActiveWindow.PointsToScreenPixelsX(x) ou PointsToScreenPixelsY(y)
x => Largeur ou abscisse
y => Hauteur ou ordonnée
Le problème c'est que cette fonction ne tient pas compte du zoom et que le calcul d'une largeur ou hauteur est approximatif dès que le zoom n'est pas à 100% dû au arrondis nécessaire pour passer d'une grandeur en point (valeur réelle) à une en pixel (valeur entière).
Voici la fonction pour calculer la distance entre deux rangées (Row ou Column) de la grille.
Ensuite l'utilisation de panneaux complique énormément la calcul de la position :
J'utilise la fonction PointsToScreenPixels{X ou Y} avec 0 en paramètre qui me donne la position du panneau ACTIF !
Il faut ajouter :
Classeur avec un exemple d'utilisation proposée :
Pour pouvoir exploiter ou tester la procédure 'GetGridPos' j'ai pris le cas de l'utilisation d'un menu contextuel pour simuler une ListBox à partir de cellule réagissant à la sélection ce qui est plus facile à visualiser et tester.
Classeur 'ScreenPosListBox' testé sous excel 2003 et 2016 mais qui devrait fonctionner sur toutes les versions depuis office 2003.
Remarque : j'utilise la propriété ID des cellules sur lesquelles je simule un Listbox. Cette propriété est initialisée à l'ouverture du classeur et persiste durant toute la session sauf si on efface la cellule.
C'est pour cela que j'ai ajouté une protection empêchant l'effacement de la propriété.
Voici le code préalable nécessaire pour utiliser la procédure principale : GetGridPos.
VB:
#If VBA7 Then
Private Declare PtrSafe Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
Private Declare PtrSafe Function apiCreateIC Lib "gdi32" Alias "CreateICA" (ByVal lpDriverName As String, ByVal lpDeviceName As String, ByVal lpOutput As String, lpInitData As Any) As Long
Private Declare PtrSafe Function apiGetDeviceCaps Lib "gdi32" Alias "GetDeviceCaps" (ByVal hdc As Long, ByVal nIndex As Long) As Long
Private Declare PtrSafe Function apiDeleteDC Lib "gdi32" Alias "DeleteDC" (ByVal hdc As Long) As Long
#Else
Private Declare Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
Private Declare Function apiCreateIC Lib "gdi32" Alias "CreateICA" (ByVal lpDriverName As String, ByVal lpDeviceName As String, ByVal lpOutput As String, lpInitData As Any) As Long
Private Declare Function apiGetDeviceCaps Lib "gdi32" Alias "GetDeviceCaps" (ByVal hdc As Long, ByVal nIndex As Long) As Long
Private Declare Function apiDeleteDC Lib "gdi32" Alias "DeleteDC" (ByVal hdc As Long) As Long
#End If
Private Const SM_CXSCREEN = 0
Private Const SM_CYSCREEN = 1
Private Const LOGPIXELSX = 88 ' Logical pixels/inch in X
'*** Public data initiated with 'GetScreenData'
Public PxToPt As Single 'Parameter to convert a step in pixel in point
VB:
'Pane Separator Size in Point
#If VBA7 Then
Private Const SEP_PIXSIZE = 3 '=>Estimé entre {2.7 & 3.3}
#Else
Private Const SEP_PIXSIZE = 6 '=>Estimé entre {5.7 & 6.3}
#End If
Private SeparatorSize As Long
Sub GetGridData()
SeparatorSize = Round(SEP_PIXSIZE / PxToPt)
End Sub
VB:
' In user's module
Sub Init()
'(...)
Call GetScreenData: Call GetGridData
Application.ActiveWindow.DisplayHeadings = False
'(...)
End Sub
' In module 'ThisWorkbook' :
Private Sub Workbook_BeforeClose(Cancel As Boolean)
'(...)
Application.ActiveWindow.DisplayHeadings = True
'(...)
End Sub
Du coup, il est plus simple de ne pas afficher les entêtes ... (jusqu'à trouver un code alternatif).
Sauf exception, si vous n'utilisé pas de panneau.
Il est tout d'abord nécessaire de pouvoir convertir les grandeurs physiques exprimées en point, en pixel et cette valeur que je nomme PxToPt (Single) doit être calculé au travers une procédure d'initialisation 'GetScreenData'; contrairement à ce que j'avais lu sur une solution ou il s'agissait d'une valeur constante car cela change en fonction de l'écran ou de la résolution.
VB:
Sub GetScreenData()
Dim twipsPerPixelX As Long '?? ...
Dim lngIC As Long
Dim lngRet As Long
lngIC = apiCreateIC("DISPLAY", vbNullString, vbNullString, vbNullString)
If lngIC <> 0 Then
twipsPerPixelX = 1440 \ apiGetDeviceCaps(lngIC, LOGPIXELSX) 'Get the Screen X resolution
apiDeleteDC (lngIC) 'Release the information context.
Else 'Error : assume an average value.
lngRet = MsgBox("Error..invalid Display Device Context..Exiting", vbOKOnly)
Exit Sub
End If
ScreenWidth = GetSystemMetrics(SM_CXSCREEN)
ScreenHeight = GetSystemMetrics(SM_CYSCREEN)
PxToPt = (twipsPerPixelX / 20) 'Ratio convert twips to Pixel per Point
End Sub
Il existe la fonction suivante pour calculer la position sur l'écran d'une position ou grandeur sur la grille : Application.ActiveWindow.PointsToScreenPixelsX(x) ou PointsToScreenPixelsY(y)
x => Largeur ou abscisse
y => Hauteur ou ordonnée
Le problème c'est que cette fonction ne tient pas compte du zoom et que le calcul d'une largeur ou hauteur est approximatif dès que le zoom n'est pas à 100% dû au arrondis nécessaire pour passer d'une grandeur en point (valeur réelle) à une en pixel (valeur entière).
Voici la fonction pour calculer la distance entre deux rangées (Row ou Column) de la grille.
VB:
'(Use Running Constant PxToPt, ratio to convert point'size in pixel'size)
'Give pixel'size between two lines
Function GetGapSize(sht As Worksheet, startLine As Long, endLine As Long, isWidth As Boolean, zoomRatio As Single) As Long
Dim rgArea As Range, rgLine As Range
Dim lnPx As Single ' Long
Set rgArea = sht.Range(Cells(IIf(isWidth, 1, startLine), IIf(isWidth, startLine, 1)), _
Cells(IIf(isWidth, 1, startLine), IIf(isWidth, startLine, 1)))
If isWidth Then Set rgArea = sht.Range(sht.Cells(1, startLine), sht.Cells(1, endLine)) Else _
Set rgArea = sht.Range(sht.Cells(startLine, 1), sht.Cells(endLine, 1))
If zoomRatio = 1 Then
GetGapSize = IIf(isWidth, rgArea.Width, rgArea.Height) / PxToPt
Else
Set rgArea = IIf(isWidth, rgArea.Columns, rgArea.Rows)
For Each rgLine In rgArea
lnPx = IIf(isWidth, rgLine.Width, rgLine.Height)
If lnPx > 0 Then
lnPx = Round(lnPx * zoomRatio / PxToPt + 0.01) '0.01 fix round pb with xx.5
GetGapSize = GetGapSize + lnPx
End If
Next rgLine
End If
End Function
J'utilise la fonction PointsToScreenPixels{X ou Y} avec 0 en paramètre qui me donne la position du panneau ACTIF !
Il faut ajouter :
- Les rangées 'cachées' : ce sont les rangées situées entre la 1ère rangée et la rangée précédent la 1ère rangée visible dans le plan ACTIF.
- Les rangées sur le plan ou la cellule se situe de la 1ère rangée visible à la rangée précédente de la cellule.
- Les rangées visibles du plan actif si différent de celui de la cellule et si précédent celle-ci.
VB:
' Calcul the screen's coordinate of the top-left corner's cell
' cellTopLeft : grid's corner top-left point (CellTL)
' celPanIdx : Pane's index where CellTL is display
' Return : lXGrid, lYGrid position on screen (in Pixel)
Sub GetGridPos(ByRef lXGrid As Long, ByRef lYGrid As Long, cellTopLeft As Range, Optional celPanIdx As Integer = 1)
Dim sht As Worksheet 'Active Sheet in Active Workbook
Dim rgCrt As Range 'Current Area
Dim ratioZoom As Single 'Grid's ratio Zoom
'Pos Grid Var
Dim hideRows As Long, hideCols As Long 'Total of Hide Row and Column between Cells(1,1) and the beginning of the active's Area
Dim begRowAct As Long, endRowAct As Long, begColAct As Long, endColAct As Long 'Final active area's intervals
Dim begRowCel As Long, endRowCel As Long, begColCel As Long, endColCel As Long 'Final CellTL area's intervals
Dim rc1 As Long, rc2 As Long, cc1 As Long, cc2 As Long 'Row and Column interval of CellTL's Area between Start Area and Previous Cell's CellTL
Dim rz1 As Long, rz2 As Long, cz1 As Long, cz2 As Long 'Row and Column interval of Area(1) : (rz1,rz2) and (cz1,cz2)
Dim isOK As Boolean, isPanVert2 As Boolean, isPanHor2 As Boolean
Dim typPan As Integer 'Total Pane => 0 : 1, 1 : 2 Verticals, 2 : 2 Horizontals, 3 : 4
Dim actPanIdx As Integer 'Index of actif pane
Set sht = ActiveWorkbook.ActiveSheet
With Application
With .ActiveWindow
lXGrid = .PointsToScreenPixelsX(0)
lYGrid = .PointsToScreenPixelsY(0)
If (celPanIdx > 1) And Not (.FreezePanes) Then
.SplitRow = .SplitRow: .SplitColumn = .SplitColumn 'Force separators position (between 2 range)
isOK = True
End If
typPan = -(.SplitColumn > 0) - 2 * (.SplitRow > 0)
isPanVert2 = ((celPanIdx - 1) And typPan) And 1 'True if Cell Pane is on right
isPanHor2 = (celPanIdx - 1) And (typPan - 1) 'True if Cell Pane is on bottom
If isOK Then 'Add pane's Sep width if panes are free
If isPanVert2 Then lXGrid = lXGrid + SeparatorSize
If isPanHor2 Then lYGrid = lYGrid + SeparatorSize
End If
ratioZoom = .Zoom / 100 'Theoretical zoom ratio
Set rgCrt = .ActivePane.VisibleRange
hideRows = rgCrt.Row - 1: hideCols = rgCrt.Column - 1 'Total hide line
actPanIdx = .ActivePane.Index
Set rgCrt = .Panes(celPanIdx).VisibleRange 'Get CellTL Intervals (rc1 to rc2, cc1 to cc2)
rc1 = rgCrt.Row: rc2 = cellTopLeft.Row - 1: If rc1 > rc2 Then rc1 = 0
cc1 = rgCrt.Column: cc2 = cellTopLeft.Column - 1: If cc1 > cc2 Then cc1 = 0
Set rgCrt = .Panes(1).VisibleRange 'Get Pane(1) Intervals (rz1 to rz2, cz1 to cz2)
If isPanHor2 Then
rz1 = rgCrt.Row: rz2 = rz1 + rgCrt.Rows.Count - 1
End If
If isPanVert2 Then
cz1 = rgCrt.Column: cz2 = cz1 + rgCrt.Columns.Count - 1
End If
If celPanIdx = actPanIdx Then 'if CellTL is on active pane
begRowAct = rc1: begColAct = cc1: endRowAct = rc2: endColAct = cc2
begRowCel = rz1: begColCel = cz1: endRowCel = rz2: endColCel = cz2
Else
If ((celPanIdx + 1) Xor (actPanIdx + 1)) Or typPan = 2 Then
begRowCel = rc1: endRowCel = rc2: begRowAct = rz1: endRowAct = rz2
Else
begRowCel = rz1: endRowCel = rz2: begRowAct = rc1: endRowAct = rc2
End If
If (typPan And 1) And ((celPanIdx Xor actPanIdx) And 1) Then
begColCel = cc1: endColCel = cc2: begColAct = cz1: endColAct = cz2
Else
begColCel = cz1: endColCel = cz2: begColAct = cc1: endColAct = cc2
End If
End If
'Add the CellTL offset to coordinates
lXGrid = lXGrid + CalCelOff(sht, True, ratioZoom, hideCols, begColCel, endColCel, begColAct, endColAct)
lYGrid = lYGrid + CalCelOff(sht, False, ratioZoom, hideRows, begRowCel, endRowCel, begRowAct, endRowAct)
End With
End With
End Sub
'Calcul CellTL offset : interval's sum of the 'Hide Area'(ha), the 'CellTL Area' and the active Area
'st : the Active Sheet
'iW : true if is a width interval else is a height interval
'zR : ratio zoom of the active sheet
'ha : Total of Hide line(s) between Cells(1,1) and the beginning of the active Area
'c1, c2 : interval(c1,c2) of the 'CellTL' Area
'a1, a2 : interval(a1,a2) of the active Area
Private Function CalCelOff(st As Worksheet, iW As Boolean, zR As Single, ha As Long, c1 As Long, c2 As Long, a1 As Long, a2 As Long) As Long
Dim r As Long
If ha = 0 Then
If a1 > 0 Then
If c1 > 0 Then CalCelOff = CalCelOff2(st, iW, zR, c1, c2, a1, a2) Else CalCelOff = GetGapSize(st, a1, a2, iW, zR)
Else
If c1 > 0 Then CalCelOff = GetGapSize(st, c1, c2, iW, zR)
End If
Else
If a1 > 0 Then
If ha = a1 - 1 Then a1 = 1 Else r = GetGapSize(st, 1, ha, iW, zR)
If c1 > 0 Then CalCelOff = r + CalCelOff2(st, iW, zR, a1, a2, c1, c2) Else CalCelOff = r + GetGapSize(st, a1, a2, iW, zR)
Else
If c1 > 0 Then CalCelOff = CalCelOff2(st, iW, zR, 1, ha, c1, c2) Else CalCelOff = GetGapSize(st, 1, ha, iW, zR)
End If
End If
End Function
'Calcul 2 interval's sum
'st : the Active Sheet
'iW : true if is a width interval else is a height interval
'zR : ratio zoom of the active sheet
'(a,b) and (c,d) : intervals
Private Function CalCelOff2(st As Worksheet, iW As Boolean, zR As Single, a As Long, b As Long, c As Long, d As Long) As Long
Dim r As Long, e As Long, f As Long
e = c
If c < a Then
f = d: c = a: d = b: a = e: b = f: e = c
End If
If c > b Then
If b = c - 1 Then CalCelOff2 = GetGapSize(st, a, d, iW, zR) Else CalCelOff2 = GetGapSize(st, a, b, iW, zR) + GetGapSize(st, c, d, iW, zR)
Else
If c > a Then r = GetGapSize(st, a, c - 1, iW, zR)
If b > d Then
f = d: r = r + GetGapSize(st, d + 1, b, iW, zR)
Else
f = b: If d > b Then r = r + GetGapSize(st, b + 1, d, iW, zR)
End If
CalCelOff2 = r + 2 * GetGapSize(st, e, f, iW, zR)
End If
End Function
Classeur avec un exemple d'utilisation proposée :
Pour pouvoir exploiter ou tester la procédure 'GetGridPos' j'ai pris le cas de l'utilisation d'un menu contextuel pour simuler une ListBox à partir de cellule réagissant à la sélection ce qui est plus facile à visualiser et tester.
Classeur 'ScreenPosListBox' testé sous excel 2003 et 2016 mais qui devrait fonctionner sur toutes les versions depuis office 2003.
Remarque : j'utilise la propriété ID des cellules sur lesquelles je simule un Listbox. Cette propriété est initialisée à l'ouverture du classeur et persiste durant toute la session sauf si on efface la cellule.
C'est pour cela que j'ai ajouté une protection empêchant l'effacement de la propriété.
Pièces jointes
Dernière édition: