The new vbAccelerator Site - more VB and .NET Code and Controls
Source Code
3 Code Libraries Source Code &nbsp


 NOTE: this code has been superceded by the version at the new site.



&nbsp

Centre a Common Dialog - The Easy and the Hard Way

Download the Easy Common Dialog Centre project files (10kb)
Download the "Hard" Common Dialog Centre project files (54kb)

&nbsp This sample requires the CommonDialog/Direct DLL component. Make sure you have loaded and registered this before trying the project.

Updated 27 September 1998
The previous release of CommonDialog/Direct did not fully support hooks. The new version has much improved hook support and the sample has been updated. Also, there was a foolish bug in the code which caused the Dialog box to disappear when you tried to centre to the screen. This has now been fixed, and the code also uses a neater method using SystemParametersInfo to take account of the taskbar(s)

Introduction
By default, a Common Dialog appears aligned to the top left position of the form which owns the common dialog. Often, you want to centre the common dialog to a particular form, or control on the form, or to the screen. VB offers you no way to do it directly, but there are ways of doing it. In fact there are two ways - the easy, VB type way, and the hard, API subclassing way. This article will show you how to do both.

The Easy Way
Since a common dialog is aligned to the top/left of a form, the easy way is this:

  • Add a new form to your project, and put a Common Dialog on it.
  • Rather than show the Common Dialog from a particular form, instead use the one on the new form
  • Before showing your Common Dialog, load the new form and move it so the top/left of the new form appear where the top,left of the Common Dialog should appear to centre it to the object you want to centre to.
It turns out it doesn't matter whether the form containing the Common Dialog is visible or not, so you can keep the new form hidden the whole time.

Simple! Here is sample code to put into the form containing the Common Dialog to help automate this process:

Option Explicit
Private m_oThis As Object
Public Enum ECentreCDlgTypes
&nbsp &nbsp eCDLFile = 0
&nbsp &nbsp eCDLPrint = 1
&nbsp &nbsp eCDLFont = 2
&nbsp &nbsp eCDLColor = 3
End Enum
Private m_lCLO As Long
Private m_lCTO As Long
Private m_lCW As Long
Private m_lCH As Long

Public Property Let Owner(ByRef oThis As Object)
&nbsp &nbsp Set m_oThis = oThis
End Property
Public Property Let DialogType(ByRef eType As ECentreCDlgTypes)
&nbsp &nbsp Select Case eType
&nbsp &nbsp Case eCDLFile
&nbsp &nbsp &nbsp &nbsp m_lCLO = 0
&nbsp &nbsp &nbsp &nbsp m_lCTO = 0
&nbsp &nbsp &nbsp &nbsp m_lCW = 427 * Screen.TwipsPerPixelX
&nbsp &nbsp &nbsp &nbsp m_lCH = 287 * Screen.TwipsPerPixelY
&nbsp &nbsp Case eCDLPrint
&nbsp &nbsp &nbsp &nbsp m_lCLO = 48 * Screen.TwipsPerPixelX
&nbsp &nbsp &nbsp &nbsp m_lCTO = 48 * Screen.TwipsPerPixelY
&nbsp &nbsp &nbsp &nbsp m_lCW = 439 * Screen.TwipsPerPixelX
&nbsp &nbsp &nbsp &nbsp m_lCH = 328 * Screen.TwipsPerPixelY
&nbsp &nbsp Case eCDLFont
&nbsp &nbsp &nbsp &nbsp m_lCLO = 20 * Screen.TwipsPerPixelX
&nbsp &nbsp &nbsp &nbsp m_lCTO = 88 * Screen.TwipsPerPixelY
&nbsp &nbsp &nbsp &nbsp m_lCW = 402 * Screen.TwipsPerPixelX
&nbsp &nbsp &nbsp &nbsp m_lCH = 345 * Screen.TwipsPerPixelY
&nbsp &nbsp Case eCDLColor
&nbsp &nbsp &nbsp &nbsp m_lCLO = 0
&nbsp &nbsp &nbsp &nbsp m_lCTO = 0
&nbsp &nbsp &nbsp &nbsp m_lCW = 455 * Screen.TwipsPerPixelX
&nbsp &nbsp &nbsp &nbsp m_lCH = 326 * Screen.TwipsPerPixelY
&nbsp &nbsp
&nbsp &nbsp End Select
End Property
Public Property Get cdlg() As CommonDialog
Dim lL As Long
Dim lT As Long
Dim lW As Long
Dim lH As Long
&nbsp &nbsp On Error Resume Next
&nbsp &nbsp lL = m_oThis.Left
&nbsp &nbsp lT = m_oThis.Top
&nbsp &nbsp lW = m_oThis.Width
&nbsp &nbsp lH = m_oThis.Height
&nbsp &nbsp Me.Move lL - m_lCLO + (lW - m_lCW) \ 2, lT - m_lCTO + (lH - m_lCH) \ 2, 1, 1
&nbsp &nbsp On Error GoTo 0
&nbsp &nbsp Set cdlg = cdlMain
End Property

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
&nbsp &nbsp Set m_oThis = Nothing
End Sub

Here is the code you can then use to produce a centred File Open dialog (assuming the form with the Common Dialog control on is called fCdlg):

&nbsp &nbsp fCdlg.Owner = Me
&nbsp &nbsp fCdlg.DialogType = eCDLFile
&nbsp &nbsp With fCdlg.cdlg
&nbsp &nbsp &nbsp &nbsp .DialogTitle = "Choose a File to Open"
&nbsp &nbsp &nbsp &nbsp .Filter = "Text Files (*.TXT)|*.TXT|All Files (*.*)|*.*"
&nbsp &nbsp &nbsp &nbsp .FilterIndex = 1
&nbsp &nbsp &nbsp &nbsp .Flags = cdlOFNFileMustExist Or cdlOFNPathMustExist
&nbsp &nbsp &nbsp &nbsp .ShowOpen
&nbsp &nbsp End With

And by the way, remember to include Unload fCDlg when your application ends, otherwise the invisible form will stop your project from terminating.

Whilst this works just fine, the problem with it is we must hardcode the size and offsets from the left and top for each of the Common Dialogs. Can you be sure the dialog is the same size on all systems? What if the user has larger fonts set up in the title bar? Even the above code is not complete. For example, on a color dialog, you can set a flag saying whether the whole dialog appears including colour customisation, or whether just the standard colours appear. Clearly, the dialog has a different size depending on the setting of this flag, but I haven't coded it in.

What we really want to have is an event which occurs when the dialog opens, saying what size its going to be and requesting a Left and Top position. This is possible, but to do it you need to delve into Common Dialog hooks and subclassing. And to do this, you need to create a Common Dialog box using the API rather than using the VB one.

The Hard Way
The real method to achieve a centred Common Dialog is to utilise a Hook function. When Common Dialogs are created via the COMDLG32.DLL API, each dialog provides a method to provide the address of a function in your code. The Common Dialog can then call this function within your code to notify your application of any actions you may wish to respond to.

Taking a sample of the File Open Common Dialog, here is how it works. To create a File Open box through the API, you fill in an OPENFILENAME structure and then call the GetOpenFileName API function exposed by COMDLG32.DLL with the structure as a parameter. One of the fields of the OPENFILENAME structure is lpfnHook. This takes a long pointer to a function, which must be constructed in accordance with the function COMDLG32 expects to call. In VB, you can pass a pointer to a function by using AddressOf, provided the function itself lives within a module (- most irritatingly!).

So, to take advantage of a hook, you do the following:

Set Up a Hook Function
The code for the hook function in VB is as follows:

&nbsp &nbsp Public Function DialogHookFunction( _
&nbsp &nbsp &nbsp &nbsp ByVal hDlg As Long, _
&nbsp &nbsp &nbsp &nbsp ByVal msg As Long, _
&nbsp &nbsp &nbsp &nbsp ByVal wParam As Long, _
&nbsp &nbsp &nbsp &nbsp ByVal lParam As Long) As Long

&nbsp &nbsp &nbsp &nbsp ' Do any processing here

&nbsp &nbsp End Function

Tell the Common Dialog the address of the Hook Function
Assuming you have an OPENFILENAME structure called opfile, you want to set the opfile.lpfnHook property. Essentially what you want to say is:

&nbsp &nbsp opfile.lpfnHook = AddressOf DialogHookFunction

However, VB seems to think this is a syntax error. Instead you have to write a function like this:

&nbsp &nbsp Private Function lHookAddress(lPtr As Long) As Long
&nbsp &nbsp &nbsp &nbsp lHookAddress = lPtr
&nbsp &nbsp End Function

&nbsp &nbsp opfile.lpfnHook = lHookAddress(AddressOf DialogHookFunction)

You also need to tell the dialog to use the lpfnHook value. This is done by setting a flag to include the OFN_ENABLEHOOK function:

&nbsp &nbsp opfile.flags = opfile.flags Or OFN_ENABLEHOOK Or OFN_EXPLORER

Respond to Dialog Initialisation Messages
With this done, you will find that when the file open dialog is opened, your DialogHookFunction is called with the msg parameter set to WM_INITDIALOG. To centre the file open dialog then, all you need to do is find out the size of the dialog box and then move it to the appropriate point. This is done with a few API functions (GetParent, GetWindowRect, SystemParametersInfo and MoveWindow) :

Public Sub CentreDialog(ByVal hDlg As Long, ByRef oCentreTo As Object)
Dim lHwnd As Long
Dim tWR As RECT, tDR As RECT
Dim tp As POINTAPI
Dim lHwndCentreTo As Long
Dim lL As Long
Dim lT As Long
Dim lR As Long

&nbsp &nbsp
' If we're showing a file dialog, then the rectangle is the
&nbsp &nbsp ' parent of the dialog itself:
&nbsp &nbsp If (m_bFileDialog) Then
&nbsp &nbsp &nbsp &nbsp lHwnd = GetParent(hDlg)
&nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp lHwnd = hDlg
&nbsp &nbsp End If
&nbsp &nbsp GetWindowRect lHwnd, tDR
&nbsp &nbsp Debug.Print tDR.Right - tDR.Left, tDR.Bottom - tDR.Top
&nbsp &nbsp
&nbsp &nbsp On Error Resume Next
&nbsp &nbsp lHwndCentreTo = oCentreTo.hwnd
&nbsp &nbsp If (Err.Number = 0) Then
&nbsp &nbsp &nbsp &nbsp GetWindowRect lHwndCentreTo, tWR
&nbsp &nbsp Else
&nbsp &nbsp
' Assume the screen object:
&nbsp &nbsp lR = SystemParametersInfo(SPI_GETWORKAREA, 0, tWR, 0)
&nbsp &nbsp If (lR = 0) Then
&nbsp &nbsp &nbsp &nbsp
' Call failed - just use standard screen:
&nbsp &nbsp &nbsp &nbsp tWR.Left = 0
&nbsp &nbsp &nbsp &nbsp tWR.Top = 0
&nbsp &nbsp &nbsp &nbsp tWR.Right = Screen.Width \ Screen.TwipsPerPixelX
&nbsp &nbsp &nbsp &nbsp tWR.Bottom = Screen.Height \ Screen.TwipsPerPixelY
&nbsp &nbsp End If
&nbsp &nbsp End If
&nbsp &nbsp On Error GoTo 0
&nbsp &nbsp If (tWR.Right > 0) And (tWR.Bottom > 0) Then
&nbsp &nbsp &nbsp &nbsp lL = tWR.Left + (((tWR.Right - tWR.Left) - (tDR.Right - tDR.Left)) \ 2)
&nbsp &nbsp &nbsp &nbsp lT = tWR.Top + (((tWR.Bottom - tWR.Top) - (tDR.Bottom - tDR.Top)) \ 2)
&nbsp &nbsp &nbsp &nbsp Debug.Print tDR.Right - tDR.Left, tDR.Bottom - tDR.Top
&nbsp &nbsp &nbsp &nbsp MoveWindow lHwnd, lL, lT, (tDR.Right - tDR.Left), (tDR.Bottom - tDR.Top), 1
&nbsp &nbsp End If
End Sub



See It Working
The sample code in the download is small because it utilises my Common Dialog/Direct class library. This provides full support for hooks via a simple interface: simply set the Hooked property to True, and then the class will raise an InitDialog event. You can centre the common dialog by calling the CentreDialog method, which uses the code shown above.

If you want to see the whole implementation, download the Common Dialog/Direct source code and demo application project.



TopBack to top
Source Code - What We're About!Back to Code Libraries
Source Code - What We're About!Back to Source Code Overview

&nbsp
 

About  Contribute  Send Feedback  Privacy

Copyright © 1998-1999, Steve McMahon ( steve@vbaccelerator.com). All Rights Reserved.
Last updated: 27 September 1998