34

I want to create a modeless popup dialog in VBA 7.0. So far the most promising route seems to be CreateDialog.

First I tried CreateDialogW and received Entry point not found for CreateDialogW in DLL.
After opening the DLL, I verified this function was not listed. The MSDN reference linked above shows User32 as the DLL for this function and lists function names CreateDialogW and CreateDialogA (Unicode/ansi respectively), but they are not listed in this DLL on my computer (Win 7 professional, 64bit).

So, looking at the list of functions that are in the DLL, I saw the CreateDialogParam and CreateDialogIndirectParam functions (Ansi and Unicode versions of each).

I've been trying to follow the MSDN and convert the C examples to VB, but I'm missing something somewhere and I'm kind of stuck as I don't know what I'm doing wrong. The code compiles and runs without errors, but nothing happens at the API call - it executes but nothing happens.

If anyone could give me some pointers in the right direction I would greatly appreciate it. My current workaround sucks, and I'd really like to button this project up.

Option Explicit

'Reference conversion of C to VB type declarations here
'http://msdn.microsoft.com/en-us/library/aa261773(v=vs.60).aspx

'Declare function to Win API CreateDialog function
'http://msdn.microsoft.com/en-us/library/ms645434(v=vs.85).aspx
Private Declare PtrSafe Function CreateDialog Lib "User32.dll" Alias "CreateDialogParamW" _
                                (ByVal lpTemplateName As LongPtr, _
                                 ByRef lpDialogFunc As DIALOGPROC, _
                                 ByVal dwInitParam As Long, _
                                 Optional ByVal hInstance As Long, _
                                 Optional ByVal hWndParent As Long) _
                                As Long

'Windows Style Constants
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx
Public Const WS_BORDER As Long = &H800000
Public Const WS_CAPTION As Long = &HC00000
Public Const WS_CHILD As Long = &H40000000
Public Const WS_CHILDWINDOW As Long = &H40000000
Public Const WS_CLIPCHILDREN As Long = &H2000000
Public Const WS_CLIPSIBLINGS As Long = &H4000000
Public Const WS_DISABLED As Long = &H8000000
Public Const WS_DLGFRAME As Long = &H400000
Public Const WS_GROUP As Long = &H20000
Public Const WS_HSCROLL As Long = &H100000
Public Const WS_ICONIC As Long = &H20000000
Public Const WS_MAXIMIZE As Long = &H1000000
Public Const WS_MAXIMIZEBOX As Long = &H10000
Public Const WS_MINIMIZE As Long = &H20000000
Public Const WS_MINIMIZEBOX As Long = &H20000
Public Const WS_OVERLAPPED As Long = &H0
Public Const WS_POPUP As Long = &H80000000
Public Const WS_SIZEBOX As Long = &H40000
Public Const WS_SYSMENU As Long = &H80000
Public Const WS_TABSTOP As Long = &H10000
Public Const WS_THICKFRAME As Long = &H40000
Public Const WS_TILED As Long = &H0
Public Const WS_VISIBLE As Long = &H10000000
Public Const WS_VSCROLL As Long = &H200000
Public Const WS_OVERLAPPEDWINDOW As Long = (WS_OVERLAPPED + WS_CAPTION + WS_SYSMENU + WS_THICKFRAME + WS_MINIMIZEBOX + WS_MAXIMIZEBOX)
Public Const WS_TILEDWINDOW As Long = (WS_OVERLAPPED + WS_CAPTION + WS_SYSMENU + WS_THICKFRAME + WS_MINIMIZEBOX + WS_MAXIMIZEBOX)
Public Const WS_POPUPWINDOW As Long = (WS_POPUP + WS_BORDER + WS_SYSMENU)

'Declare custom type for lpDialogFunc argument
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms645469(v=vs.85).aspx
Public Type DIALOGPROC
    hwndDlg As Long
    uMsg As LongPtr
    wparam As Long
    lparam As Long
End Type


'MAKEINTRESOURCE Macro emulation
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms648029(v=vs.85).aspx
'Bitwise function example found here: http://support.microsoft.com/kb/112651
'VB conversion found here: https://groups.google.com/forum/#!topic/microsoft.public.vb.winapi/UaK3S-bJaiQ _
 modified with strong typing and to use string pointers for VB7
Private Function MAKEINTRESOURCE(ByVal lID As Long) As LongPtr
     MAKEINTRESOURCE = StrPtr("#" & CStr(MAKELONG(lID, 0)))
End Function

Private Function MAKELONG(ByRef wLow As Long, ByRef wHi As Long)
    'Declare variables
        Dim LoLO            As Long
        Dim HiLO            As Long
        Dim LoHI            As Long
        Dim HiHI            As Long

    'Get the HIGH and LOW order words from the long integer value
        GetHiLoWord wLow, LoLO, HiLO
        GetHiLoWord wHi, LoHI, HiHI

            If (wHi And &H8000&) Then
                MAKELONG = (((wHi And &H7FFF&) * 65536) Or (wLow And &HFFFF&)) Or &H80000000
            Else
                MAKELONG = LoLO Or (&H10000 * LoHI)
                'MAKELONG = ((wHi * 65535) + wLow)
            End If
End Function

Private Function GetHiLoWord(lparam As Long, LOWORD As Long, HIWORD As Long)
    'This is the LOWORD of the lParam:
        LOWORD = lparam And &HFFFF&
    'LOWORD now equals 65,535 or &HFFFF
    'This is the HIWORD of the lParam:
        HIWORD = lparam \ &H10000 And &HFFFF&
    'HIWORD now equals 30,583 or &H7777
        GetHiLoWord = 1
End Function

Public Function TstDialog()
    Dim dpDialog                As DIALOGPROC

    dpDialog.hwndDlg = 0
    dpDialog.uMsg = StrPtr("TEST")
    dpDialog.lparam = 0
    dpDialog.wparam = 0

    CreateDialog hInstance:=0, lpTemplateName:=MAKEINTRESOURCE(WS_POPUPWINDOW + WS_VISIBLE), lpDialogFunc:=dpDialog, dwInitParam:=&H110
End Function
participant
  • 2,923
  • 2
  • 23
  • 40
CBRF23
  • 1,340
  • 1
  • 16
  • 44
  • 2
    `CreateDialog` indicates in its [documentation](http://msdn.microsoft.com/en-us/library/windows/desktop/ms645434%28v=vs.85%29.aspx) that it's a macro that actually uses `CreateDialogParam`. It also indicates that it returns a value, and that if that return value is NULL you should use `GetLastError` to find out why it failed. You're not doing that - why not? (Not sure why you're jumping through all these hoops anyway; any Office product that supports VBA has far easier to use ways of creating forms (dialogs) built in.) – Ken White Nov 17 '14 at 21:44
  • 1
    Ken, I'm using VBA 7.0 with Solidworks. There is not (to my knowledge) any native function to create a modeless dialog in VBA other than creating a generic form, and calling an instance of it set to VbModeless. I was exploring the Windows API option because I don't like to have unnecessary forms in my projects, and also as a learning/challenging experience. – CBRF23 Nov 17 '14 at 22:08
  • Where would I use the `GetLastError` function? The documentation indicates I should use `err.LastDllError` in VBA. Currently nothing happens when the function is called, so I don't believe I have a return to check. – CBRF23 Nov 17 '14 at 22:09
  • You would call `CreateDialog` as a function, and store the return value. If the return value is 0 (NULL), you then call `GetLastError` and examine its result for a specific error code. There's a link to `GetLastError` right on the documentation page I linked you to previously. So the logic would be 'Err = CreateDialog() if Err = 0 then Err = GetLastError()`. `LastDllError` (as its name indicates) applies to DLLs. Creating a generic form and using an instance would be considerably easier than what you're attempting now (from VBA, anyway). You're creating a generic dlg resource-about the same. – Ken White Nov 17 '14 at 22:45
  • 1
    Oh, wait. I just looked more closely at your code. DIALOGPROC is all wrong. A DIALOGPROC is a pointer to a function (DIALOG procedure) that accepts the parameters `hwnd, umsg, wParam, lParam` - it's not a data structure. You need to be passing a pointer to a method, which I'm not even sure is possible to do from VBA. You also need a predefined DIALOG resource (a resource script compiled using the MS resource compiler and linked into the app) in order to use `CreateDialog`; one of the parameters it receives is the name of that resource. You're not even remotely close here, I'm afraid. – Ken White Nov 17 '14 at 22:50
  • @KenWhite, I agree a generic form would be easier, but not the most satisfying or experientially valuable. Now that I've gotten this far, I'd really like to make it work. I've learned a *TON* of stuff already (about Windows, API functions, bit-typing, etc) that I would never have learned about had I just went with a user form. I'm heading home for the night, but I'll play with the `GetLastError` function in the morning. – CBRF23 Nov 17 '14 at 22:52
  • @KenWhite, thank you for the help. I'm really at a loss as to how to emulate the `DialogProc` callback function in VBA as I can't find any explanation of the underlying function. It looks like I could use the `DIALOGEX` system resource, which again looks to me like a data type, but probably is not. I guess I will look at using a generic userform. It is frustrating not being able to do this the way I would like. – CBRF23 Nov 18 '14 at 13:40
  • `DialogProc` is [documented]. It's a callback function (a function whose pointer you pass to the WinAPI and that API uses it to pass information back to your application via Windows messages). It's a basic fundamental of WinAPI programming. – Ken White Nov 18 '14 at 13:47
  • 1
    Oops - got distracted before I could put the link in. The docs for [DialogProc](http://msdn.microsoft.com/en-us/library/windows/desktop/ms645469%28v=vs.85%29.aspx) I mentioned. – Ken White Nov 18 '14 at 14:09
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/65154/discussion-between-cbrf23-and-ken-white). – CBRF23 Nov 18 '14 at 14:25

4 Answers4

8

This can be made to work, although if you should try to make it work is another question. I have a working version that shows an empty dialog. I don't have any more time tonight to finish with actual controls on the dialog, but I'm posting in the hope it'll get you started.

First you need to forget about CreateDialog because they require the dialog template to be in the resource section. You can use CreateDialogIndirectParam to create a dialog from an in-memory dialog template. You will need this:

Private Type DLGTEMPLATE
    style As Long
    dwExtendedStyle As Long
    cdit As Integer
    x As Integer
    y As Integer
    cx As Integer
    cy As Integer
End Type

Private Type DLGITEMTEMPLATE
    style As Long
    dwExtendedStyle As Long
    x As Integer
    y As Integer
    cx As Integer
    cy As Integer
    id As Integer
End Type

Private Type DLG
    dlgtemp As dlgtemplate
    menu As Long
    classname As String
    title As String
End Type

Private Declare PtrSafe Function CreateDialogIndirectParam Lib "User32.dll" Alias "CreateDialogIndirectParamW" _
  (ByVal hInstance As Long, _
  ByRef lpTemplate As DLGTEMPLATE, _
  ByVal hWndParent As Long, _
  ByVal lpDialogFunc As LongPtr, _
  ByVal lParamInit As Long) _
  As LongPtr

Const WM_INITDIALOG As Long = &H110
Const DS_CENTER As Long = &H800&
Const DS_SETFONT As Long = &H40
Const DS_MODALFRAME As Long = &H80
Const WS_EX_APPWINDOW As Long = &H40000

Then call it like this:

Dim d As DLG
d.dlgtemp.style = DS_MODALFRAME + WS_POPUP + WS_VISIBLE + WS_CAPTION + WS_SYSMENU
d.dlgtemp.dwExtendedStyle = WS_EX_APPWINDOW
d.dlgtemp.cdit = 0
d.dlgtemp.x = 100
d.dlgtemp.y = 100
d.dlgtemp.cx = 200
d.dlgtemp.cy = 200
d.menu = 0
d.title = "Test"
d.classname = "Test"

CreateDialogIndirectParam 0, d.dlgtemp, 0, AddressOf DlgFunc, 0

with DlgFunc looking something like this:

Public Function DlgFunc(ByVal hwndDlg As LongPtr, ByVal uMsg As LongPtr, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
    If uMsg = h110 Then  ' = WM_INITDIALOG - you should make a const for the various window messages you'll need...
        DlgFunc = True
    Else
        DlgFunc = False
    End If
End Function

It's been over a decade since I last did any of this stuff. But if you're determined to go this route, I think this approach is the most promising - the next step is to adapt the DLG struct to add some DLGITEMTEMPLATE members, set d.dlgtemp.cdit to the number of controls on your dialog, and start handling control messages in your DlgFunc.

Roel
  • 19,338
  • 6
  • 61
  • 90
  • 1
    Also, you should probably read this series: http://blogs.msdn.com/b/oldnewthing/archive/2005/04/29/412577.aspx if you're really going to do this. – Roel Nov 18 '14 at 22:11
  • 2
    I can verify that I have blank modeless dialog windows but I cannot close them now. They do leave on closing excel. – cheezsteak Nov 18 '14 at 22:22
  • @Roel That looks promising! I'll have to try this when I get back to work tomorrow (should have SW up and running tomorrow - repair didn't work, so I had to wait to the end of the day to start a clean uninstall/reinstall process). – CBRF23 Nov 19 '14 at 00:48
  • @cheezsteak Looking at Roel's answer, he did say I will need to set d.dlgtemp.cdit to the number of controls on the dialog, and start handling control messages - which would agree with Hans' comment below about needing to write procedures to handle command events. – CBRF23 Nov 19 '14 at 04:24
  • I've marked this as the answer as it has addressed everything in my original question - it has gotten me to the point where I can create a modeless dialog using an API call, and that was exactly what I was trying to do. Now I have to decide whether or not this is the best path, or to follow an alternative. To that end, many have been supplied here, and there are many other options it seems than I originally knew of - so I received a lot of great information here. Thanks to everyone!! – CBRF23 Nov 20 '14 at 12:15
  • If you do decide to go down this route, I think the easiest thing to do would be re-implement CDlgTempl (from http://support.microsoft.com/kb/155257/en-us/) in VBA, including using calls to HeapAlloc/HeapFree to substitute for the lack of malloc() in VBA. Memory alignment will still be an issue though, but I guess not unsolvable. The implementation of a class/module in VBA that handles all of this correctly will be hairy, but not much more hairy than what is done in MFC to support dialogs. And it will still leave you with a very cumbersome (no UI designer) way to implement dialogs. – Roel Nov 20 '14 at 13:24
  • @Roel My original plan was to create a class module that I could use in multiple projects (I'm still working on a good distribution method for source code in VBA - I found a decent looking solution a while back, but I can't find the link right now - I have it bookmarked at the office). I have created quite a few API call "wrappers" as class objects for stuff like BrowseForFolder, GetOpenFileName, etc. which there is no native support for in the SW VSTA suite, but which I use in a lot of projects. I never expected this attempt to be as complicated as it was. Still a GREAT learning experience! – CBRF23 Nov 21 '14 at 03:22
8

I don't want to detract from the in depth and well researched but there are possible work-arounds to dynamically creating modeless dialog boxes in VBA. That was the original problem before the asker bravely dived down the rabbit hole with CreateDialog. So this answer is for the original problem of dynamically creating Modeless dialog boxes in VBA not how to use CreateDialog. I can't help there.

As previously stated, modeless dialog boxes can be created using a UserForm, but we don't want useless forms littering the project. The work-around I have achieved uses the Microsoft VBA Extensibility Library. In short, we create a class that adds a generic userform to the project on construction and removes the userform on termination.

Also note, this is tested using Excel VBA. I don't have SolidWorks so I can't test it there.

Crudely done as a class module.

Option Explicit

Private pUserForm As VBIDE.VBComponent

Private Sub Class_Initialize()
    ' Add the userform when created '
    Set pUserForm = ThisWorkbook.VBProject.VBComponents.Add(VBIDE.vbext_ct_MSForm)
End Sub
Private Sub Class_Terminate()
    ' remove the userform when instance is deleted '
    ThisWorkbook.VBProject.VBComponenets.Remove pUserForm
End Sub
Public Property Get UserForm() As VBIDE.VBComponent
    ' allow crude access to modify the userform '
    ' ideally this will be replaced with more useful methods '
    Set UserForm = pUserForm
End Property
Public Sub Show(ByVal mode As Integer)
    VBA.UserForms.Add(pUserForm.Name).Show mode
End Sub

Ideally, this class would be better developed and would allow easier access to modifying the form, but for now it's a solution.

Tests

Private Sub TestModelessLocal()

    Dim localDialog As New Dialog
    localDialog.UserForm.Properties("Caption") = "Hello World"
    localDialog.Show vbModeless

End Sub

You should see a window appear and disappear as localDialog leaves scope. A UserForm1 was created in your VBProject and deleted.

This test will create a persistent dialog box. Unfortunately, UserForm1 will remain in your VBProject as globalDialog is still defined. Resetting the project will not remove the userform.

Dim globalDialog As Dialog
Private Sub TestModeless()

    Set globalDialog = New Dialog
    globalDialog.UserForm.Properties("Caption") = "Hello World"
    globalDialog.Show vbModeless
    'Set globalDialog = Nothing  closes window and removes the userform '
    'Set gloablDialog = new Dialog should delete userform1 after added userform2'
End Sub

So never use this at module scope.

In conclusion, its an ugly solution but it's far less ugly than what the Asker was trying to do.

cheezsteak
  • 2,731
  • 4
  • 26
  • 41
  • Hi cheezsteak, yes dynamically adding the form is an option. The only thing I don't like about dynamically modifying VBA projects is it makes debugging a PITA because as soon as the project is modified programmatically it is no longer possible to pause execution and the code must run to completion. This means to me I have to do a lot of error checking in my code and printing to the debug window, or storing results and printing to a messagebox at the end, which in VBA is kind of a pain as there's no try/catch like .net :/ Part of the reason I was looking at other methods, but is an option! – CBRF23 Nov 18 '14 at 23:14
  • As far as the global solution, you could probably move the terminate procedure to a public procedure and then just have the terminate even call that procedure. That way you could destroy global/module level variables. i.e. `Public Sub Destroy() ' remove the userform when instance is deleted ThisWorkbook.VBProject.VBComponents.Remove pUserForm End Sub Private Sub Class_Terminate() Destroy End Sub` – CBRF23 Nov 18 '14 at 23:18
  • @CBRF23 You cannot pause the execution? I can F8 through the above code just fine. If you exit early the userform won't be removed but that is the only fault I know of. – cheezsteak Nov 19 '14 at 14:57
  • I am sorry maybe I don't fully understand the question but isn't this answer somehow a lot overcomplicated? The user can design the form in the VBE and run a simple `UserForm1.Show vbModeless` to show the form and keep the code execution. Why would do meta-programming to build it dynamically? Sorry, if I am being ignorant but I just don't see why this is so complex... –  Nov 19 '14 at 15:48
  • @vba4all In the comments of the question the asker stated "I don't like to have unnecessary forms in my projects". As I understand he wants to dynamically create dialog boxes like you can with `Msgbox` but modeless. All dialog producing builtin functions are modal only. – cheezsteak Nov 19 '14 at 15:58
  • 1
    would [this](https://gallery.technet.microsoft.com/scriptcenter/An-HTA-based-VBScript-36122f57) work? –  Nov 19 '14 at 16:15
  • @vba4all That does work and is certainly a cleaner solution. I would replace `wsh.sleep` with `DoEvents` though. – cheezsteak Nov 19 '14 at 16:49
  • ok that I think the sleep is there so the popup exits after sometime though –  Nov 19 '14 at 16:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/65225/discussion-between-cheezsteak-and-vba4all). – cheezsteak Nov 19 '14 at 17:37
  • @CBRF23: does this solution work? While I see that you accepted a different answer, because I'm unable to ascertain 'good'/'functional' code in [tag:c++] and [tag:vba], I'm inclined to follow the community's recommendation, choosing the most-upvoted answer, but obviously it's not a valid answer if it doesn't work for you. – David Thomas Nov 24 '14 at 09:17
  • @DavidThomas Yes and no. You can check out the chat discussion for more info, but to summarize: YES- it works in programmatically adding a `UserForm` to the project and showing it modeless. And NO - in that it doesn't really work for my application because it creates too many problems for me in debugging the rest of the project. For whatever reason, changing a project programmatically in VSTA 7.0 for Solidworks disables break mode - so the entire project then must run to completion and I have to hope I put enough error checking/reporting in to catch any problems. – CBRF23 Nov 24 '14 at 14:39
  • @DavidThomas My comment above was directed to this answer, using extensibility to add a userform programatically. Vba4All did provide a pretty good solution as well, which is a viable alternative to using windows API. I definitely am looking at this solution in my project - it seems simple and can easily be accommodated in a wrapper function. I marked the answer I did because it addressed everything in my original question, but as far as solutions go - I think Vba4All's may be the easiest to implement. – CBRF23 Nov 24 '14 at 14:51
4

You got a very poor start on this project. You completely scrambled the order of the arguments for CreateDialogParam, note how the hInstance argument is first, the dwInitParam argument is last.

You completely fumbled the DIALOGPROC declaration, it is a function pointer. That requires LongPtr in the declaration and the AddressOf operator when you make the call.

This was just the first 1% of making it work. Next problem is that you'll have to write a functional dialog procedure (the target of AddressOf) that handles the notifications that the dialog generates. Basic stuff, like recognizing that the user clicked the OK button. Very hard to write when you don't know enough about WinAPI programming, little mistakes are big undiagnosable problems at runtime.

That's just the small stuff, there are much bigger problems. The lpTemplateName argument is a very serious obstacle. That needs to be a resource identifier, the kind that's generated by "rc.exe" and added to the executable file by the linker. You cannot relink SolidWorks. A modeless dialog requires help from the message loop, it must call IsDialogMessage(). You cannot convince SolidWorks to make this call for you. Without it, the dialog misbehaves in hard to diagnose ways, like tabbing will not work.

You have to know when you stand absolutely no chance to make it work. You can't make it work.

BartoszKP
  • 34,786
  • 15
  • 102
  • 130
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • I appreciate the very knowledable feedback. I did originally have the `CreateDialogParam` arguments in the order per the MSDN documentation, but VBA unforunately requires optional arguments to be last, which is why they are now ordered the way they are. I did not know if this was something that would affect the API call. Sounds like it is. As Ken above pointed out, I did completely misinterpret how to use the `DialogProc` callback function - but I did find some info on using `AddressOf`. It really is sounding like this is not doable within VBA, or at least not worth the time/effort. – CBRF23 Nov 18 '14 at 17:09
  • [I can't decide if my comment goes here or in your Tumbleweed question. - randy] It's clear that you have thought about this problem and *question* quite a bit. My only suggestion (if it's possible) would be to distill the code down to the smallest it can be. This will make it easier for people to see the code and what might be broken. And maybe verify independently if the utility functions like MAKELONG are correct. Good Luck! – Randy Stegbauer Nov 18 '14 at 19:53
4

This answer, like Cheezsteak's doesn't directly deal with the issues you're having with CreateDialog. It addresses the end goal of creating a modeless dialog box.

My suggestion is to use a UserForm to accomplish this. It's Show Method takes an optional parameter that determines whether or not the userform is displayed as a modal or modeless form.

From the MSDN documentation:

modal Optional. Boolean value that determines if the UserForm is modal or modeless.

  1. Create a UserForm and design it to your needs.
  2. In the code that creates the instance of the UserForm, simply pass it the vbModeless constant.

    Option Explicit
    
    Private frm As UserForm1
    
    Sub test2()
        Set frm = New UserForm1
        frm.Show vbModeless
    End Sub
    

If you're worried about cluttering up your project with forms, don't be. Just create the form on the fly.

Community
  • 1
  • 1
RubberDuck
  • 11,933
  • 4
  • 50
  • 95