How to Set the Background Color of the Microsoft Access Application Window with VBA

A series of Windows API calls will "repaint" the Microsoft Access canvas with the color of your choice. It is a bit rough around the edges, though...

How to Set the Background Color of the Microsoft Access Application Window with VBA

Several years ago, I had a client ask me to change the application background color from its default gray.

As I recall, the application included a feature to switch the data source between current data and read-only archived data.  She wanted the application background to change when viewing the archive data.  To be clear, she was not asking to change the background color of the forms, but of the Access application window itself.

I spent more time than I care to admit trying and failing to make this work.  

I searched online for a solution, but I came up empty-handed.

Many years later–with a deeper understanding of Windows API programming and the help of ChatGPT–I managed to write the surprisingly simple subroutine that eluded me back then: SetCanvasBgColor().

The Access Canvas

Known to some as the MDI (multi-document interface) client area, the Access canvas is the space that surrounds forms and reports within the Access window.

GetInnerAccessHwnd(): Retrieve the Window Handle to the Microsoft Access Canvas
This function returns the window handle to the Microsoft Access “canvas”, the inner portion of the application window where forms and reports are drawn.

It's the area in teal in the screenshots below:

In fact, I used the SetCanvasBgColor() function to create the effect in these screenshots.

The Approach

The SetCanvasBgColor() function leverages the Windows API to interact with the Access application at a level deeper than what's typically exposed through VBA.

By obtaining the window handle of the Access canvas, we can directly manipulate its properties. The function uses a series of API calls to create a solid brush with the desired color and then fills the canvas area with that color.

It's a clever workaround to an otherwise static aspect of Access's UI.

The Algorithm

Here's a breakdown of the SetCanvasBgColor() function in plain English:

  1. Obtain the Canvas Handle: Using the GetInnerAccessHwnd() function, we retrieve the handle to the Access canvas, which is the drawable area minus any docked elements like menus or toolbars.
  2. Get the Device Context: With the canvas handle, we call GetDC() to get the device context, which is necessary for any drawing operations.
  3. Create the Brush: We invoke CreateSolidBrush() with the color passed to the function to create a brush of the desired color.
  4. Determine the Area to Fill: The GetCanvas() function is called to get the coordinates of the canvas area.
  5. Fill the Area: We use FillRect() to fill the canvas area with the solid brush created earlier.
  6. Clean Up: Finally, we release the device context with ReleaseDC() to avoid resource leaks.

The Function

Here is the SetCanvasBgColor() function on its own without its required dependencies:

Sub SetCanvasBgColor(RgbColor As Long)
    Dim hWnd As LongPtr
    hWnd = GetInnerAccessHwnd

    Dim hdc As LongPtr
    hdc = GetDC(hWnd)
       
    Dim hBrush As LongPtr
    hBrush = CreateSolidBrush(RgbColor)
    
    Dim Rectangle As Rect
    Rectangle = GetCanvas
    
    FillRect hdc, Rectangle, hBrush

    ReleaseDC hWnd, hdc
End Sub

The Full Code

The code below includes required helper functions that I've written about in the past:

Option Compare Database
Option Explicit

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

' --== GetInnerAccessHwnd() declarations ==--
Private Declare PtrSafe Function apiFindWindowEx _
    Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As LongPtr, _
                                        ByVal hWnd2 As LongPtr, _
                                        ByVal lpsz1 As String, _
                                        ByVal lpsz2 As String) As LongPtr

Private Declare PtrSafe Function apiGetParent _
    Lib "user32" Alias "GetParent" (ByVal hWnd As LongPtr) As LongPtr


' --== GetCanvas() declarations ==--
Private Declare PtrSafe Function apiGetClientRect _
    Lib "user32" Alias "GetClientRect" (ByVal hWnd As LongPtr, _
                                        lpRect As Rect) As Long


' --== SetCanvasBgColor() declarations ==--
Private Declare PtrSafe Function GetDC _
    Lib "user32" (ByVal hWnd As LongPtr) As LongPtr

Private Declare PtrSafe Function CreateSolidBrush _
    Lib "gdi32" (ByVal crColor As Long) As LongPtr
    
Private Declare PtrSafe Function FillRect _
    Lib "user32" (ByVal hdc As LongPtr, _
                  lpRect As Rect, _
                  ByVal hBrush As LongPtr) As Long
                  
Private Declare PtrSafe Function ReleaseDC _
    Lib "user32" (ByVal hWnd As LongPtr, _
                  ByVal hdc As LongPtr) As Long


' ----------------------------------------------------------------
' Procedure : SetCanvasBgColor
' Date      : 12/12/2023
' Author    : Mike Wolfe
' Source    : https://nolongerset.com/setcanvasbgcolor/
' Purpose   : Sets the background color of the Microsoft Access canvas.
' Notes     - Background color reverts to default whenever a form
'               or report is closed or moved on the screen.
'           - Thus, this must be called in every form and report's
'               unload and resize events (and even that is not enough
'               because there is no form or report "move" event) OR
'               it must be called on a Timer (which presents its own
'               set of problems).
'           - Use the RGB() function to create the value to be passed
'               in the RgbColor argument.
' Sample Usage (e.g., in hidden global form):
'
'   Private Sub Form_Timer()
'       SetCanvasBgColor RGB(255, 0, 0)   'Set canvas background to Red
'   End Sub
' ----------------------------------------------------------------
Sub SetCanvasBgColor(RgbColor As Long)
    Dim hWnd As LongPtr
    hWnd = GetInnerAccessHwnd

    ' Get the device context
    Dim hdc As LongPtr
    hdc = GetDC(hWnd)
       
    ' Create a solid brush
    Dim hBrush As LongPtr
    hBrush = CreateSolidBrush(RgbColor)
    
    ' Get the area to fill
    Dim Rectangle As Rect
    Rectangle = GetCanvas
    
    ' Fill the rectangle area with the brush
    FillRect hdc, Rectangle, hBrush

    ' Release the device context
    ReleaseDC hWnd, hdc
End Sub



'---------------------------------------------------------------------------------------
' Procedure : GetInnerAccessHwnd
' Date      : 11/26/2014 - 12/12/2023
' Author    : Mike Wolfe
' Source    : https://nolongerset.com/getinneraccesshwnd/
' Purpose   : Gets the window handle of the Access "canvas".
' Notes     - We don't use hWndAccessApp here because that's the window handle for the
'               full application; what we really want is the handle for the "inner container";
'               i.e., the Access window minus the docked menus, docked toolbars, and status bar
'---------------------------------------------------------------------------------------
'
Function GetInnerAccessHwnd(Optional ByVal ChildHWnd As LongPtr = 0) As LongPtr
    
    GetInnerAccessHwnd = apiFindWindowEx(hWndAccessApp, ByVal 0&, "MDIClient", vbNullString)
    If GetInnerAccessHwnd <> 0 Then Exit Function

    On Error Resume Next
    If ChildHWnd = 0 Then ChildHWnd = Screen.ActiveForm.hWnd
    If ChildHWnd = 0 Then ChildHWnd = Screen.ActiveReport.hWnd
    If ChildHWnd = 0 Then ChildHWnd = Screen.ActiveDatasheet.hWnd
    GetInnerAccessHwnd = apiGetParent(ChildHWnd)
End Function

'---------------------------------------------------------------------------------------
' Procedure : GetCanvas
' Date      : 11/18/2011 - 12/14/2023
' Author    : Mike Wolfe
' Source    : https://nolongerset.com/getcanvas/
' Purpose   : Returns the Left, Top, Right, and Bottom coordinates of the Access canvas.
' Notes     - Convenience function normally used in conjunction with other API calls.
'---------------------------------------------------------------------------------------
'
Function GetCanvas(Optional ByVal ChildHWnd As LongPtr) As Rect
    Dim CanvasHwnd As LongPtr
    CanvasHwnd = GetInnerAccessHwnd(ChildHWnd)
    apiGetClientRect CanvasHwnd, GetCanvas
End Function

The code can be copied and pasted into a blank standard module for a fully-working solution that can be easily integrated into your projects.

Some Very Important Caveats

This function only sets a custom canvas background color; it does not keep a custom canvas background color.  This has some important consequences.

Works Best With "Overlapping Windows"

Modern versions of Access provide two options for displaying document windows (i.e., forms and reports):

  • Overlapping Windows
  • Tabbed Documents

With "Tabbed Documents", every open form and report completely covers the Access canvas.  The only time you would see it is if there were no visible forms or reports at all (or every form/report was a popup).  The code will still work with the "Tabbed Documents" option, it just won't be of much practical value.

Access Redraws Its Canvas...A LOT

Every time Access "redraws" all or part of its canvas, the default background color is restored and you will have to call the

  • Resizing a form will reset the entire canvas back to its default color
  • Restoring a minimized form will reset the entire canvas back to its default color
  • Moving or closing a form will restore the default color of the canvas background for any portion of the canvas that the form was previously covering

There are three ways you can account for this:

  • Intercept the WM_PAINT message at the operating system level and call SetCanvasBgColor every time Access redraws the canvas (that approach is WAY beyond the scope of this article)
  • Add calls to SetCanvasBgColor in every form's Resize and Close events (besides requiring a lot of code, this approach also does not account for a user moving a form without resizing it, as there is no Form_Move() event you can handle)
  • Add a Timer event to an always-open form, such as a hidden global form and set the Timer to run at least once per second

Of these approaches, the Timer event is the easiest to implement, but it comes with a couple of major caveats of its own.

Timer Event Messes With the VBA Editor

Every time the Timer executes during development, VBA performs a just-in-time compile of the project.  One of the side effects of compiling is that trailing whitespace is trimmed from all lines of code...including the one you may be editing.  This happens regardless of the "Compile On Demand" and "Background Compile" settings in the VBE Options dialog.

So, if you take this approach, you'll want to keep that form closed (or otherwise disable the Timer event) while you are writing code, or it will drive you absolutely crazy.

Frequent Timer Events Can Hurt Performance

For the best visual experience, you'll want to set a pretty short timer (around 100ms or so).  Unfortunately, the overhead of raising a VBA event that frequently could easily have a noticeable impact on your application's performance.  

In my original example, I would disable the timer and live with the default background color when the users were working with current data.  Only when they switched to archived data would I enable the timer.

Sample Usage

Imagine you have a database application that needs to reflect your company's branding, which includes a specific shade of blue as a background color. Here's how you might use the SetCanvasBgColor() function in a hidden global form that's always open:

Private Sub Form_Timer()
    SetCanvasBgColor RGB(0, 122, 204)   ' Set canvas background to Brand Blue
End Sub

This form could have a timer event that periodically calls SetCanvasBgColor() to ensure that the canvas color remains consistent, even after other forms or reports are closed or moved.  Be sure to set the form's Timer Interval property to a suitable number of milliseconds, such as 100 (to re-apply the background color every tenth of a second).  To temporarily disable the timer, you can set the Timer Interval to zero.

One Final Note of Caution

Be sure to save your project often when working with this code, especially if you run it using a short timer.  

Windows API calls in general will crash your application if you're not careful, but API calls that deal with low-level drawing commands are especially finicky.

Furthermore, this code is brand new to me.  Unlike many of the functions I post on this site–which have been in production use for a long time (and so have had lots of time to accumulate little bug fixes here and there), this is a very fresh function.  There could easily be edge and corner cases that cause problems.  

Your mileage may vary.

Acknowledgements
  • Portions of this article's body generated with the help of ChatGPT

All original code samples by Mike Wolfe are licensed under CC BY 4.0