Version 1.3 First Published 20 Oct 2023 Last Updated 24 Oct 2023 Approx 0.9 MB (zipped)
I am very grateful to fellow Access developer Xavier Batlle for suggesting the code used in this example application and further refining it following discussions to broaden its scope. It is one of several very interesting ideas that I plan to publish (with Xavier's permission) in the near future.
Normally the Access application interface is limited to the physical dimensions of a single monitor.
However, using API calls, it is possible to extend the Access interface across two or more monitors arranged horizontally or vertically.
The form used to control the Access application interface is very simple with just two buttons to click:
1. Extend ACCESS window across all screens - or use the keyboard shortcut Ctrl+E
2. Restore ACCESS to a single screen - or use the shortcut Ctrl+Shift+E
You may be wondering when/why extending the application interface could ever be useful? Here are several possible ways of using this setup:
a) Display several objects such as forms/reports simultaneously without overlaps.
b) Display a complete datasheet form which is too wide to fit on a single screen across 2 or more monitors horizontally.
c) Display a complete datasheet form which is too tall to fit on a single screen across 2 or more monitors vertically.
d) Simulate how your application will look for users with very large, high resolution monitors.
e) Test the effects of using automatic form resizing with your applications for users with very large, high resolution monitors.
f) Plan for Access forms wider than the current limit of 22.75 inches (57.79 cm). Microsoft have announced wider forms will be supported in a forthcoming release.
g) Display a very large number of items across multiple monitors on the Quick Access Toolbar (QAT).
For example, in the screenshot below Access has been extended across two monitors arranged horizontally. The left monitor is showing two forms and the right monitor shows a report with no overlapping objects.
Click the image to open a larger version on a new tab
The Windows display settings in this case are:
Alternatively, the 2 monitors can be arranged vertically in Windows settings:
In this case, extending Access across both screens will look similar to this:
The primary monitor can be arranged on the left/right/top or bottom.
Using three or more monitors, the possible arrangements are even greater.
The screenshot below shows a datasheet form with over 50 fields (some hidden), extended horizontally across two monitors.
Click the image to open a larger version on a new tab
The above datasheet form also has over 2.6 million records so arranging two or more monitors vertically would allow more records to be viewed without scrolling.
The next screenshot shows a quick access toolbar with many items filling two monitors horizontally.
Click the image to open a larger version on a new tab
NOTE:
For best results, all the monitors should be similar sizes and resolutions.
Code
The API code used to extend the interface across multiple monitors is contained in the module modExtendAccess.
The module code works in both 32-bit and 64-bit Access.
It is fairly complex but you do not need to understand it fully in order to use it in your own apps.
Option Compare Database
Option Explicit
'Code compiled by Xavier Batlle from various online sources and including code by:
'Mike Wolfe - https://nolongerset.com
'Daniel Pineault - https://devhut.net
'Colin Riddington - https://isladogs.co.uk
'==================================================================================
'GetSystemMetrics32 info: http://msdn.microsoft.com/en-us/library/ms724385(VS.85).aspx
#If VBA7 Then
Declare PtrSafe Function GetSystemMetrics32 Lib "user32" Alias "GetSystemMetrics" (ByVal nIndex As Long) As Long
Declare PtrSafe Function IsZoomed Lib "user32" (ByVal hWnd As LongPtr) As Long
Declare PtrSafe Function IsIconic Lib "user32" (ByVal hWnd As LongPtr) As Long
Declare PtrSafe Function SetWindowPos Lib "user32" ( _
ByVal hWnd As LongPtr, _
ByVal hWndInsertAfter As LongPtr, _
ByVal X As Long, _
ByVal Y As Long, _
ByVal cx As Long, _
ByVal cy As Long, _
ByVal wFlags As Long) As Long
#Else
Declare Function GetSystemMetrics32 Lib "user32" Alias "GetSystemMetrics" (ByVal nIndex As Long) As Long
Declare Function IsZoomed Lib "user32" (ByVal hWnd As Long) As Long
Declare Function IsIconic Lib "user32" (ByVal hWnd As Long) As Long
Declare Function SetWindowPos Lib "user32" (ByVal hWnd As Long, _
ByVal hWndInsertAfter As Long, _
ByVal X As Long, _
ByVal Y As Long, _
ByVal cx As Long, _
ByVal cy As Long, _
ByVal wFlags As Long) As Long
#End If
Public Const P2T = 15 'pixels to twips
#If VBA7 Then
Dim hWnd As LongPtr
#Else
Dim hWnd As Long
#End If
'=======================================
Sub ExtendAccessScreens()
'extend Access interface across multiple monitors
Dim X As Long
Dim Y As Long
Dim cx As Long
Dim cy As Long
'https://www.devhut.net/ms-access-vba-minimizemaximize-access-application/
'DoCmd.RunCommand acCmdAppMinimize 'Minimize the MS Access Application
'DoCmd.RunCommand acCmdAppMaximize 'Maximize the MS Access Application
' If Access Windows is maximized then this code doesn't work.
' Get the handle of the MS Access window
hWnd = Application.hWndAccessApp
If IsMaximized(hWnd) Or IsMinimized(hWnd) Then
DoCmd.RunCommand acCmdAppRestore 'Restore the MS Access Application
End If
' Get the screen dimensions
X = GetLeftVIRTUALSCREEN
Y = GetTopVIRTUALSCREEN
cx = GetVirtualHorizontalResolution
cy = GetVirtualVerticalResolution
' Debug.Print X, Y, cx, cy
' Set the window position and size to cover both screens
SetWindowPos hWnd, 0, X, Y, cx, cy, 0
End Sub
'=======================================
Function RestoreSingleScreen()
'Colin Riddington 20/10/2023
Dim x As Long
Dim y As Long
Dim cx As Long
Dim cy As Long
  'https://www.devhut.net/ms-access-vba-minimizemaximize-access-application/
  'DoCmd.RunCommand acCmdAppMinimize 'Minimize the MS Access Application
  'DoCmd.RunCommand acCmdAppMaximize 'Maximize the MS Access Application
  ' If Access Windows is maximized then it doesn't work.
  ' Get the handle of the MS Access window
hwnd = Application.hWndAccessApp
If IsMaximized(hwnd) Or IsMinimized(hwnd) Then
DoCmd.RunCommand acCmdAppRestore 'Restore the MS Access Application
End If
  ' Get the screen dimensions in pixels
x = GetPrimaryMonitorLeft 'normally zero
y = GetPrimaryMonitorTop 'normally zero
cx = GetPrimaryMonitorRight 'normally same as primary horizontal resolution
cy = GetPrimaryMonitorBottom 'normally same as primary vertical resolution
  'Debug.Print x, y, cx, cy
  'Set the window position and size to cover just the primary monitor screen
SetWindowPos hwnd, 0, x, y, cx, cy, 0
End Function
'=======================================
Public Function CountMonitors() As Long
'count the number of monitors
Const SM_CMONITORS = 80
CountMonitors = GetSystemMetrics32(SM_CMONITORS)
End Function
'=======================================
Public Function GetVirtualHorizontalResolution() As Long
'The total width of the virtual screen, in pixels.
'The virtual screen is the bounding rectangle of all display monitors.
'The SM_XVIRTUALSCREEN metric is the coordinates for the left side of the virtual screen.
Const SM_CXVIRTUALSCREEN = 78
GetVirtualHorizontalResolution = GetSystemMetrics32(SM_CXVIRTUALSCREEN)
End Function
'=======================================
Public Function GetVirtualVerticalResolution() As Long
'The total height of the virtual screen, in pixels.
'The virtual screen is the bounding rectangle of all display monitors.
'The SM_YVIRTUALSCREEN metric is the coordinates for the top of the virtual screen.
Const SM_CYVIRTUALSCREEN = 79
GetVirtualVerticalResolution = GetSystemMetrics32(SM_CYVIRTUALSCREEN)
End Function
'=======================================
'===== PRIMARY MONITOR =====
Public Function GetPrimaryMonitorTop() As Long
'The top edge of the screen of the primary display monitor, in pixels - normally zero
GetPrimaryMonitorTop = DLookup("Top", "tblMonitors", "PrimaryMonitor = True")
End Function
'=======================================
Public Function GetPrimaryMonitorLeft() As Long
'The left edge of the screen of the primary display monitor, in pixels - normally zero
GetPrimaryMonitorLeft = DLookup("Left", "tblMonitors", "PrimaryMonitor = True")
End Function
'=======================================
Public Function GetPrimaryMonitorRight() As Long
'The right edge of the screen of the primary display monitor, in pixels - normally same as primary horizontal resolution
GetPrimaryMonitorRight = DLookup("Right", "tblMonitors", "PrimaryMonitor = True")
End Function
'=======================================
Public Function GetPrimaryMonitorBottom() As Long
'The bottom position of the screen of the primary display monitor, in pixels - normally same as primary vertical resolution
GetPrimaryMonitorBottom = DLookup("Bottom", "tblMonitors", "PrimaryMonitor = True")
End Function
'=======================================
Public Function GetLeftVIRTUALSCREEN() As Long
'The coordinates for the left side of the virtual screen.
Const SM_XVIRTUALSCREEN = 76
GetLeftVIRTUALSCREEN = GetSystemMetrics32(SM_XVIRTUALSCREEN)
End Function
'=======================================
Public Function GetTopVIRTUALSCREEN() As Long
'The coordinates for the top of the virtual screen.
Const SM_YVIRTUALSCREEN = 77
GetTopVIRTUALSCREEN = GetSystemMetrics32(SM_YVIRTUALSCREEN)
End Function
'=======================================
'Next two items from https://nolongerset.com/get-a-handle-on-window-state/
'Modified 21/10/2023 by Colin Riddington for 64-bit
#If VBA7 Then
Function IsMaximized(hwnd As LongPtr) As Boolean
#Else
Function IsMaximized(hwnd As Long) As Boolean
#End If
IsMaximized = IsZoomed(hWnd) * -1
End Function
'=======================================
#If VBA7 Then
Function IsMimimized(hwnd As LongPtr) As Boolean
#Else
Function IsMinimized(hwnd As Long) As Boolean
#End If
IsMinimized = IsIconic(hWnd) * -1
End Function
Additional code in module modMonitorInfo is used to determine info about each monitor and populate the table tblMonitors.
This code is also very complex but once again can just be used 'as is' without fully understanding how it works.
Option Compare Database
Option Explicit
'########################################
'used to collect monitor info
'Colin Riddington - last updated 18/10/2023
'########################################
#If VBA7 Then 'CR - updated 27/11/2022
Declare PtrSafe Function GetActiveWindow Lib "user32" () As LongPtr
Declare PtrSafe Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
Declare PtrSafe Function MonitorFromWindow Lib "user32" (ByVal hwnd As LongPtr, ByVal dwFlags As Long) As LongPtr
Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hdc As LongPtr, ByVal nIndex As Long) As Long
Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hdc As LongPtr) As Long
Declare PtrSafe Function CreateDC Lib "gdi32" Alias "CreateDCA" _
(ByVal lpDriverName As String, ByVal lpDeviceName As String, ByVal lpOutput As String, lpInitData As LongPtr) As LongPtr
Declare PtrSafe Function DeleteDC Lib "gdi32" (ByVal hdc As LongPtr) As Long
Declare PtrSafe Function LoadLibraryEx Lib "kernel32.dll" Alias "LoadLibraryExA" (ByVal lpFileName As String, ByVal hFile As LongPtr, ByVal dwFlags As Long) As LongPtr
Declare PtrSafe Function GetProcAddress Lib "kernel32.dll" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
Declare PtrSafe Function GetModuleHandle Lib "kernel32.dll" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As LongPtr
Declare PtrSafe Function FreeLibrary Lib "kernel32.dll" (ByVal hLibModule As LongPtr) As Long
Declare PtrSafe Function EnumDisplayMonitors Lib "user32.dll" (ByVal hdc As LongPtr, ByRef lprcClip As Any, ByVal lpfnEnum As LongPtr, ByVal dwData As Long) As Boolean
Declare PtrSafe Function GetMonitorInfo Lib "user32.dll" Alias "GetMonitorInfoA" (ByVal hMonitor As LongPtr, ByRef lpMI As MONITORINFOEX) As Boolean
Declare PtrSafe Function ShellExecute Lib "Shell32.dll" Alias "ShellExecuteA" (ByVal hwnd As LongPtr, ByVal lpOperation As String, _
ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As LongPtr
#Else
Declare Function GetActiveWindow Lib "user32" () As Long
Declare Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
Declare Function MonitorFromWindow Lib "user32" (ByVal hwnd As Long, ByVal dwFlags As Long) As Long
Declare Function GetDeviceCaps Lib "gdi32" (ByVal hdc As Long, ByVal nIndex As Long) As Long
Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hdc As Long) As Long
Declare Function CreateDC Lib "gdi32" Alias "CreateDCA" _
(ByVal lpDriverName As String, ByVal lpDeviceName As String, ByVal lpOutput As String, lpInitData As Long) As Long
Declare Function DeleteDC Lib "gdi32" (ByVal hdc As Long) As Long
Declare Function LoadLibraryEx Lib "kernel32.dll" Alias "LoadLibraryExA" (ByVal lpFileName As String, ByVal hFile As Long, ByVal dwFlags As Long) As Long
Declare Function GetProcAddress Lib "kernel32.dll" (ByVal hModule As Long, ByVal lpProcName As String) As Long
Declare Function GetModuleHandle Lib "kernel32.dll" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As Long
Declare Function FreeLibrary Lib "kernel32.dll" (ByVal hLibModule As Long) As Long
Declare Function EnumDisplayMonitors Lib "user32.dll" (ByVal hdc As Long, ByRef lprcClip As Any, ByVal lpfnEnum As Long, ByVal dwData As Long) As Boolean
Declare Function GetMonitorInfo Lib "user32.dll" Alias "GetMonitorInfoA" (ByVal hMonitor As Long, ByRef lpMI As MONITORINFOEX) As Boolean
Declare Function ShellExecute Lib "Shell32.dll" Alias "ShellExecuteA" (ByVal hwnd As Long, ByVal lpOperation As String, _
ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long
#End If
Public Const SM_CMONITORS As Long = 80 ' number of display monitors
Public Const MONITOR_CCHDEVICENAME As Long = 32 ' device name fixed length
Private Const MONITOR_PRIMARY As Long = 1
Private Const MONITOR_DEFAULTTONULL As Long = 0
Private Const MONITOR_DEFAULTTOPRIMARY As Long = 1
Private Const MONITOR_DEFAULTTONEAREST As Long = 2
Public Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
Public Type MONITORINFOEX
cbSize As Long
rcMonitor As RECT
rcWork As RECT
dwFlags As Long
szDevice As String * MONITOR_CCHDEVICENAME
End Type
Public Enum DevCap ' GetDeviceCaps nIndex (video displays)
HORZSIZE = 4 ' width in millimeters
VERTSIZE = 6 ' height in millimeters
HORZRES = 8 ' width in pixels
VERTRES = 10 ' height in pixels
BITSPIXEL = 12 ' color bits per pixel
LOGPIXELSX = 88 ' horizontal DPI (assumed by Windows)
LOGPIXELSY = 90 ' vertical DPI (assumed by Windows)
COLORRES = 108 ' actual color resolution (bits per pixel)
VREFRESH = 116 ' vertical refresh rate (Hz)
End Enum
Dim MonitorID() As String, I As Integer
#If VBA7 Then
Dim hHandle As LongPtr, hwnd As LongPtr, hdc As LongPtr, hMonitor As LongPtr
#Else
Dim hHandle As Long, hwnd As Long, hdc As Long, hMonitor As Long
#End If
'items to get the mouse cursor position
Private Type POINTAPI
x As Long
y As Long
End Type
Private tPos As POINTAPI
#If VBA7 Then
Private Declare PtrSafe Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
#Else
Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
#End If
'=======================================
'Get mouse cursor position
Public Function GetXCursorPos() As Long
Dim pt As POINTAPI
GetCursorPos pt
GetXCursorPos = pt.x
End Function
'=======================================
Public Function GetYCursorPos() As Long
Dim pt As POINTAPI
GetCursorPos pt
GetYCursorPos = pt.y
End Function
'=======================================
Public Function GetCurrentMonitorID() As Integer
GetCurrentMonitorID = Nz(DLookup("MonitorID", "tblMonitors", "CurrentMonitor=True"), 1)
End Function
'=======================================
Public Sub CheckMonitorInfo()
'---------------------------------------------------------------------------------------
' Procedure: CheckMonitorInfo
' DateTime: 18/11/2022
' Author: Colin Riddington
' Purpose: Used to detect position, size and resolution of all connected monitors
' Data stored in tblMonitors and called in form load of startup form
'---------------------------------------------------------------------------------------
Dim bytNumOfMonitors As Byte
'clear & repopulate tblMonitors
CurrentDb.Execute "DELETE * FROM tblMonitors", dbFailOnError
DoEvents
bytNumOfMonitors = CountMonitorId
For I = 1 To UBound(MonitorID)
Call SaveMonitorInfo(MonitorID(I), bytNumOfMonitors)
Next I
End Sub
'=======================================
Public Function CountMonitorId()
ReDim MonitorID(0)
' Of course dual screen systems are not available on all Win versions.
If FunctionExist("user32.dll", "EnumDisplayMonitors") = True Then
If EnumDisplayMonitors(&H0, ByVal &H0, AddressOf MonitorEnumProc, &H0) = False Then
Failed "EnumDisplayMonitors"
End If
End If
CountMonitorId = UBound(MonitorID)
' Debug.Print CountMonitorId
End Function
'=======================================
Private Sub SaveMonitorInfo(ForMonitorID As String, bytNoMonitors As Byte)
'updated 03/03/2023
'Reason - fix to replace boolean varaibles with byte for non-English versions of Office
'Thanks to Jacinto Trilio for alerting me to this issue
'adds info to tblMonitors
Dim MONITORINFOEX As MONITORINFOEX
Dim bytPrimary As Byte, strDevice As String, strName As String, bytCurrent As Byte, strPos As String
Dim lngLeft As Long, lngTop As Long, lngRight As Long, lngBottom As Long, lngHRes As Long, lngVRes As Long
Dim lngDPI As Long
MONITORINFOEX.cbSize = Len(MONITORINFOEX)
If GetMonitorInfo(CLng(ForMonitorID), MONITORINFOEX) = 0 Then Failed "GetMonitorInfo"
With MONITORINFOEX
If .dwFlags And MONITOR_PRIMARY Then
bytPrimary = 1 'True
Else
bytPrimary = 0 'False
End If
With .rcMonitor
lngLeft = .Left
lngTop = .Top
lngRight = .Right
lngBottom = .Bottom
End With
lngHRes = lngRight - lngLeft
lngVRes = lngBottom - lngTop
End With
'check which monitor is current
bytCurrent = IIf(GetXCursorPos >= lngLeft And GetXCursorPos <= lngRight And GetYCursorPos >= lngTop And GetYCursorPos <= lngBottom, 1, 0)
'check monitor position
Dim strSQL As String
strSQL = "INSERT INTO tblMonitors ( MonitorID, PrimaryMonitor, MonitorPosition, [Left], [Top], [Right], Bottom, HRes, VRes, CurrentMonitor )" & _
" SELECT " & I & " AS MonitorID, " & bytPrimary & " AS PrimaryMonitor, '' AS MonitorPosition," & _
" " & lngLeft & " AS [Left], " & lngTop & " AS [Top], " & lngRight & " AS [Right], " & lngBottom & " AS Bottom," & _
" " & lngHRes & " AS HRes, " & lngVRes & " AS VRes, " & bytCurrent & " AS CurrentMonitor;"
'Debug.Print strSQL
CurrentDb.Execute strSQL, dbFailOnError
End Sub
By comparison, the code in frmMain is very simple:
Private Sub Form_Load()
'check the monitor arrangement and populate tblMonitorInfo
CheckMonitorInfo
blnExtend = False
cmdRestore.Enabled = False
cmdExtend.Enabled = True
End Sub
'=======================================
Private Sub cmdExtend_Click()
'recheck the monitor arrangement and update tblMonitorInfo
CheckMonitorInfo
'extend Access interface across all screens
ExtendAccessScreens
blnExtend = True
cmdExtend.Enabled = False
cmdRestore.Enabled = True
End Sub
'=======================================
Private Sub cmdRestore_Click()
'recheck the monitor arrangement and update tblMonitorInfo
CheckMonitorInfo
'restores Access interface to fill the primary monitor only
RestoreSingleScreen
blnExtend = False
cmdRestore.Enabled = False
cmdExtend.Enabled = True
End Sub
Finally, the keyboard shortcuts are done using an Autokeys macro:
NOTE:
1. This code has been extensively tested in both 32-bit & 64-bit Access with two monitors in all 8 possible arrangements.
I would be very grateful for feedback from anyone able to test this code using three or more monitors.
If possible, please supply screenshots showing how the code works in these arrangements.
2. A follow up article Auto position objects in extended Access interface demonstrates the automatic placement of different objects such as two forms on the
primary and secondary monitors no matter what arrangement is used for the monitor display.
Version History
Version Date Notes
v1.1 20/10/2023 Initial release
v1.3 22/10/2023 Fixed issues with 64-bit code; fixed bug when restoring to single screen; added keyboard shortcuts
Download
Click to download: Extend Access Window v1.3 Approx 0.9 MB (zipped)
Acknowledgements
Many thanks to Xavier Batlle for suggesting this approach and providing part of the original API code used in the example application.
Feedback
Please use the contact form below to let me know whether you found this article interesting/useful or if you have any questions/comments.
Please also consider making a donation towards the costs of maintaining this website. Thank you.
Colin Riddington Mendip Data Systems Last Updated 24 Oct 2023
Return to Example Databases Page
|
Return to Top
|