5

Intro:

I've run into an issue when I tried to position Visio-UserForms relative to the calling Visio application window, as it is possible in other MS Office applications.
Normally I would use calling code like in the first block (Excel) to open a UserForm in a relative position to the application window.
The important properties for this uqestion are .Left and .Top, which return the offset of the window compared to the screen.

If I try the same in Visio (code block 2) I ran into the following Issue: The application object of a the Visio application (vsApp) does not support the .Top an .Left properties, so obviously I get the standart Run.time error "438": “Object doesn't support this property or method”

Question:

My question is if there is an alternative relatively clean method to get the window position of the calling application (maybe even application-agnostic). When looking around there are a multitude of solutions for Excel, but none for Visio as far as I can tell.

This is my first question here, so please if I submitted something wrong or missed a rule/guideline please let me know.

Code:

In both cases the FooUserForm is a simple UserForm with a single button that hides the form with Me.Hide. The code below resides in a standard module

Code in Excel:

Option Explicit

Sub openFooUserForm()

    Dim fooUF As FooUserForm
    Set fooUF = New FooUserForm

    Dim exApp As Excel.Application
    Set exApp = ThisWorkbook.Application

    fooUF.StartUpPosition = 0
    fooUF.Top = exApp.Top + 25
    fooUF.Left = exApp.Left + 25

    fooUF.Show

    Set fooUF = Nothing

End Sub

Code in Visio:

Option Explicit

Sub openFooUserForm()

    Dim fooUF As FooUserForm
    Set fooUF = New FooUserForm

    Dim vsApp As Visio.Application
    Set vsApp = ThisDocument.Application

    fooUF.StartUpPosition = 0
    fooUF.Top = vsApp.Top + 25
    fooUF.Left = vsApp.Left + 25

    fooUF.Show

    Set fooUF = Nothing

End Sub
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
L8n
  • 728
  • 1
  • 5
  • 15
  • 4
    You need to go lower level than that. Look for a `hWnd` property on `Visio.Application`; that's a *window handle* (pointer) you can try to use with the [GetWindowRect](https://msdn.microsoft.com/en-us/library/windows/desktop/ms633519(v=vs.85).aspx) Win32 API function. Try setting up your `Declare` statements and the `Type` structures you need for them (you'll need to know if you're on a 32 or 64-bit host, and if your code needs to run in both, you'll need `#If...#Then...#End If` precompiler directives) - then you'll have a starting point. – Mathieu Guindon Aug 14 '18 at 13:57
  • 2
    @MathieuGuindon Perfect, I'm on the right track now. Visio.Application did not have the`hWnd` Property, instead I had to use `WindowHandle32` I put the whole code into it's own class and added some extras like output in Points (UserForms do not like Pixels). For now it's 32-bit only, but I'll post it as an answer as soon as the class works properly. – L8n Aug 14 '18 at 16:03
  • 1
    Your diligence is refreshing here haha – Marcucciboy2 Aug 14 '18 at 18:01
  • I added the class as an answer, if no others show up I will accept it as answer. Is this already useful enough to be posted to CodeReview? – L8n Aug 22 '18 at 13:00

2 Answers2

2

Since I assume to use this in many other project, I created a class containing all the code. The class works in 32-bit for now, mostly because I couldn't find a way to get the 64-bit handle from the Visio Application Object.

The code itself is prepared 64-bit thanks to the use of the LongPtr type. More Info here: https://codekabinett.com/rdumps.php?Lang=2&targetDoc=windows-api-declaration-vba-64-bit
The declarations should work since they were recreated in the 64-bit environment.

The class exposes 13 properties, 12 of these are Window positions and sizes and one is the Handle, this allows the user to target a different window instead of the application. This could be used to position a Userform in relation to a window opened inside the "Main" application.

Office UserForms (for some reason) use Points instead of Pixels to position themselves on the screen, to help with this I also built a conversion into the class.

There are still some things open that I want to change, like adding proper Error Handling and maybe giving the class a default Instance, but for now this is usable.


Resources

http://officeoneonline.com/vba/positioning_using_pixels.html

http://www.vbforums.com/showthread.php?436888-Get-Set-Window-Size-Position


Explanation

What happens in this Module/Class?

  • The class handles the interaction with the Windows API
  • It creates a Private Type Rect, which is used by the GetWindowRect function.
  • It declares the GetWindowRect function, wich takes the window handle of a window (obviously) and returns the position of the "Outline" in pixels
  • When the object is initialized it automatically stores the window handle of the Application in which it was called in this.Handle
  • When getting one of the px__ properties it simply updates the window position this.rc and returns the desired value.
  • When getting on of the pt__ properties it updates the window position and calculates the equivalent in points, this is usefull since VBA Userforms actually use points for positioning. The conversion is described here.
  • The windows handle can be changed by setting the Handle Property, this provides some more flexibility, for example when a multiple windows of the same application are opened.

Code

aModule (Module)

Sub openFooUserForm()
    
    Dim winPo As WindowPositioner
    Set winPo = New WindowPositioner
    
    Dim fooUF As FooUserForm
    Set fooUF = New FooUserForm
    
    fooUF.StartUpPosition = 0
    fooUF.Top = winPo.ptTop + 100
    fooUF.Left = winPo.ptLeft + 50
    
    fooUF.Show
    
    Set fooUF = Nothing

End Sub

WindowPositioner (Class)

Option Explicit

Private Type RECT
    Left    As Long
    Top     As Long
    Right   As Long
    Bottom  As Long
End Type

Private Type TWindowPositioner
    Handle As LongPtr
    rc As RECT
End Type

Private this As TWindowPositioner

Const LOGPIXELSX = 88
Const LOGPIXELSY = 90
Const TWIPSPERINCH = 1440

Private Declare Function GetWindowRect Lib "user32" (ByVal hWnd As LongPtr, lpRect As RECT) As Long
Private Declare Function GetDC Lib "user32" (ByVal hWnd As LongPtr) As Long
Private Declare Function ReleaseDC Lib "user32" (ByVal hWnd As LongPtr, ByVal hDC As LongPtr) As Long
Private Declare Function GetDeviceCaps Lib "gdi32" (ByVal hDC As LongPtr, ByVal nIndex As Long) As Long


Private Sub Class_Initialize()
#If WIN64 THEN
   'this.Handle = 'Method to get the 64-bit Handle of the Application Object
#Else
    this.Handle = ThisDocument.Application.WindowHandle32
#End If
    this.rc.Left = 0
    this.rc.Top = 0
    this.rc.Right = 0
    this.rc.Bottom = 0
End Sub

Public Property Get Handle() As LongPtr
    Handle = this.Handle
End Property

Public Property Let Handle(val As LongPtr)
    this.Handle = val
End Property



Public Property Get pxTop() As Long
    UpdatePosition
    pxTop = this.rc.Top
End Property

Public Property Get pxLeft() As Long
    UpdatePosition
    pxLeft = this.rc.Left
End Property

Public Property Get pxBottom() As Long
    UpdatePosition
    pxBottom = this.rc.Bottom
End Property

Public Property Get pxRight() As Long
    UpdatePosition
    pxRight = this.rc.Right
End Property

Public Property Get pxHeight() As Long
    UpdatePosition
    pxHeight = this.rc.Bottom - this.rc.Top
End Property

Public Property Get pxWidth() As Long
    UpdatePosition
    pxWidth = this.rc.Left - this.rc.Right
End Property


Public Property Get ptTop() As Long
    ptTop = CPxToPtY(pxTop)
End Property

Public Property Get ptLeft() As Long
    ptLeft = CPxToPtX(pxLeft)
End Property

Public Property Get ptBottom() As Long
    ptBottom = CPxToPtY(pxBottom)
End Property

Public Property Get ptRight() As Long
    ptRight = CPxToPtX(pxRight)
End Property

Public Property Get ptHeight() As Long
    ptHeight = CPxToPtY(pxBottom) - CPxToPtY(pxTop)
End Property

Public Property Get ptWidth() As Long
    ptWidth = CPxToPtX(pxRight) - CPxToPtX(pxLeft)
End Property



Private Sub UpdatePosition()
    GetWindowRect this.Handle, this.rc
End Sub

Private Function CPxToPtX(ByRef val As Long) As Long
    Dim hDC As LongPtr
    Dim RetVal As Long
    Dim XPixelsPerInch As Long

    hDC = GetDC(0)
    XPixelsPerInch = GetDeviceCaps(hDC, LOGPIXELSX)
    RetVal = ReleaseDC(0, hDC)
    
    CPxToPtX = CLng(val * TWIPSPERINCH / 20 / XPixelsPerInch)
End Function

Private Function CPxToPtY(ByRef val As Long) As Long
    Dim hDC As LongPtr
    Dim RetVal As Long
    Dim YPixelsPerInch As Long

    hDC = GetDC(0)
    YPixelsPerInch = GetDeviceCaps(hDC, LOGPIXELSY)
    RetVal = ReleaseDC(0, hDC)
    
    CPxToPtY = CLng(val * TWIPSPERINCH / 20 / YPixelsPerInch)
End Function
Community
  • 1
  • 1
L8n
  • 728
  • 1
  • 5
  • 15
  • Please check my comment. Your code is working, but it might be just too bulky compared to the single line solution. – Nikolay Aug 25 '18 at 22:29
1

You simply need to use Application.Window.GetWindowRect instead of Application.Top and Application.Left in Visio to get the main window coordinates (for historical reasons - when Visio became part of the Microsoft Office some 20 years ago, this API already existed, and it was different from other office apps you are referring to). Anyways, the subject can be done easier than in the accepted answer:

Set vsApp = ThisDocument.Application

'''' here we go
Dim left As Long, top As Long, width As Long, height As Long
vsApp.Window.GetWindowRect left, top, width, height

fooUF.StartUpPosition = 0
fooUF.Top = top + 25
fooUF.Left = left + 25
Nikolay
  • 10,752
  • 2
  • 23
  • 51
  • I'm away from my PC for some time, I'll try it when I get back – L8n Sep 01 '18 at 17:21
  • The method you show works, although it is not just a simple replacement since you have to handle similar to the API call (instead of just using a return value). Also the problem with the Pixels and Points still exists, the return value of your function is in pixel, while the UserForm expects Points. – L8n Sep 18 '18 at 15:44