Finally! If you've been following along with all of the theoretical posts thus far, I bet you're wondering if I was ever going to get to the point. ;-)
Now we're going to put together everything we've learned about Ptrs and Structures and whatnot to get some real work done. To refresh your memories, what we're trying to accomplish is putting a simple dialog box on the screen using only the Win32 APIs to do it. This involves making some dialog templates in memory, and calling DialogBoxIndirect.
For starters, we're going to define the two structures that this API relies on: DLGTEMPLATE and DLGITEMTEMPLATE.
Thankfully, both of these structures are quite easy to recreate in REALbasic. DWORD stands for UInt32, WORD is UInt16 and short is Int16. Armed with this, you should be all set. Make a new module named Structs, and add two new structures to it:
Structure DLGTEMPLATE style as UInt32 extendedStyle as UInt32 cdit as UInt16 x as Int16 y as Int16 cx as Int16 cy as Int16 End Structure
Structure DLGITEMTEMPLATE style as UInt32 extendedStyle as UInt32 x as Int16 y as Int16 cx as Int16 cy as Int16 id as UInt16 End Structure
There, that takes care of our structure needs. Now we need to see how this relates to the API we're going to call. The DialogBoxIndirect function requires a chunk of global memory to store these structures, and it also has some very strict wording about alignment. We need both DWORD alignment (for the main structures) and WORD alignment (for the internals of the structures). So we're going to need to design a function which aligns things to the nearest boundary. The following code is a fairly dense way to enforce the alignment1.
Private Function Align(bs as Ptr, bytes as Integer) As Ptr bytes = bytes \ 2 dim ul as UInt32 = UInt32( bs ) ul = ul + 1 ul = ShiftRight( ul, bytes ) ul = ShiftLeft( ul, bytes ) return Ptr( ul ) End Function
So let's cover what this code is doing since I left out comments. The first line is turning the bytes into a shift count. If we want to align to two bytes, we want to shift by one. If we want to align by four bytes, we shift by two. The second line is typecasting the Ptr passed in to an integer so that we can do some arithmetic on it. The third line adds one to the address, while the fourth and fifth lines do some bit shifting. Finally, we return the resulting address as a Ptr. It's the adding one and shifting around that's interesting. Let's look at an example of how this works using binary numbers. If the address is &b0010 and we want to align to a DWORD boundary (nearest, which means we want to pack, not explode), then we add one (&b0011), shift right by two (&b0000) and then shift left by two (&b0000). That's on a DWORD boundary, so we're good. But if the address was &b0011 initially, then we add one (&b0100), shift right by two (&b0001), and then shift left by two (&b0100). Again, we're aligned on a DWORD boundary. One more try with the initial value of &b0110 will show the same results. Add one (&b0111), shift right (&b0001), shift left (&b0100) and back on a DWORD boundary. You can try other combinations if you'd like, but this is a quick and dense way to pack-align.
Now that we've got the initial pieces and parts done, let's start putting the ingredients together to bake this cake, eh?
Our first order of business is allocating the global memory we'll need to store things into. This requires calling GlobalAlloc to allocate the memory and GlobalLock to actually lock that memory into place so we can use it (and GlobalUnlock so that the API can use it, and GlobalFree so that we don't leak).
Declare Function GlobalAlloc Lib "Kernel32" ( flags as Integer, size as UInt32 ) as UInt32 Declare Function GlobalLock Lib "Kernel32" ( data as UInt32 ) as Ptr Declare Sub GlobalUnlock Lib "Kernel32" ( data as UInt32 ) Declare Sub GlobalFree Lib "Kernel32" ( data as UInt32 )
Const GMEM_ZEROINIT = &h0040 dim dlgMemPtr as UInt32 = GlobalAlloc( GMEM_ZEROINIT, 1024 ) dim dlgTemplatePtr as Ptr = GlobalLock( dlgMemPtr ) dim oldStartValue as UInt32 = UInt32( dlgTemplatePtr )
We're cheating and allocating more space than we'll need (1024 bytes is plenty of room), but in production code, we'd want to calculate the space requirements a bit better. Also notice the distinct lack of error checking -- another thing that production code requires. At least we've got our global memory pointer, and the locked memory pointer. We're going to be doing all of our work with the locked memory pointer, so we're going to keep its initial value around for later. Note that we don't have to worry about alignment of the pointer because it's guaranteed to be aligned on an 8-byte boundary (according to MSDN).
Now we want to create and fill out a DLGTEMPLATE structure. I'm going to skip a number of the constant values, but the example project will have them.
dim template as DLGTEMPLATE template.style = WS_BORDER + WS_SYSMENU + WS_CAPTION + DS_MODALFRAME template.cdit = 2 template.x = 10 template.y = 10 template.cx = 100 template.cy = 100
This code is fairly self-explanatory (which is why we like using structures in the first place, right?) -- it defines the size of the dialog, its style and how many controls it will have. We're going to have two controls: a static text, and a push button.
The next chunk of code is where things start to get funky. We're going to move the structure's data into our pointer, and then start modifying the dynamic parts of the data such as the menu, the window class and the dialog's title. Because these are dynamic in length, we couldn't have them as part of the structure (which is fixed-length by definition). One of the stipulations of the API is that we need to use MultiByteToWideChar to ensure that our strings are properly encoded for any version of Windows. So that declare looks like this:
Declare Function MultiByteToWideChar Lib "Kernel32" ( cp as UInt32, flags as UInt32, _ str as CString, numChars as Integer, retStr as Ptr, retStrChars as Integer ) as Integer
Now we're all set to fill the entire structure (dynamic parts and all) into memory:
// Move the structure itself into memory dlgTemplatePtr.DLGTEMPLATE = template
// Advance our pointer to just past the end of the template. We're // adding four bytes on as well because we have no menu resource and // we're using the default window class (each of which takes up a WORD) dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + DLGTEMPLATE.Size + 4 )
// Convert our dialog's caption into wide char string, and write it directly into // the memory at the current location. The return value is the number of // characters (not bytes!), and we're adding one on because we want to advance // past the string. dim numChars as Integer = MultiByteToWideChar( CP_ACP, 0, "My Dialog!", -1, dlgTemplatePtr, 50 ) + 1
// Advance to the end of the string we just wrote in. Note that we multiply // by two because a wide string is UCS 2, which is always 2 bytes long. dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + (numChars * 2) )
That takes care of the dialog header itself. Now we need to ensure that the child items are aligned on a DWORD boundary. This is where we're going to make use of our alignment function.
dlgTemplatePtr = Align( dlgTemplatePtr, 4 )
Now we're ready to make our items. This dialog is going to have a StaticText that displays a message, and an OK button which closes the dialog. The layout for a template item is very similar to the layout for the dialog's template. We'll make use of the DLGITEMTEMPLATE structure, and the stuff some information after it. Specifically, we need to append information such as the class (what type of control is it), title (what's the control's caption) and any extra data associated with the control. Let's take a look at the code used to make a default PushButton named OK.
dim item as DLGITEMTEMPLATE item.x = 10 item.y = 70 item.cx = 80 item.cy = 20 item.id = 500 item.style = WS_CHILD + WS_VISIBLE + BS_DEFPUSHBUTTON dlgTemplatePtr.DLGITEMTEMPLATE = item
It looks an awful lot like the code for the dialog itself, eh? We are making a child control that is visible and is default. Then we're sticking the data into the Ptr we allocated. Now let's look at the funky stuff:
// Advance our pointer to the end of the item structure dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + DLGITEMTEMPLATE.Size )
// We are going to be making a button, and that is done by ordinal dlgTemplatePtr.Int16( 0 ) = &hFFFF dlgTemplatePtr.Int16( 2 ) = &h0080 // PushButton
// Advance the pointer again so that we can write the caption into it dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + 4 ) numChars = MultiByteToWideChar( CP_ACP, 0, "OK", -1, dlgTemplatePtr, 50 ) + 1
// Advance the pointer past the end of the button's caption dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + (numChars * 2) )
// Specify that there's no creation data by skipping over its location in // memory (remember, the memory was zero-initialized). dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + 2 )
By now the code should not look too scary since you've already seen it with the dialog's template. But you may have noticed that we did not bother aligning anything to a WORD boundary. That's because we don't have to (sort of). You see, all of those fields are naturally WORD aligned -- even the text. The text is WORD aligned because we must write out wide characters which always occupy two bytes. So at no point in time can we get off a WORD alignment.
Let's look at the final piece to the puzzle -- the message to be displayed.
// Align it to a DWORD boundary dlgTemplatePtr = Align( dlgTemplatePtr, 4 )
// Set the position for the text item.x = 10 item.y = 10 item.cx = 40 item.cy = 25 item.id = 502 item.style = WS_CHILD + WS_VISIBLE
// Assign it to the memory pointer dlgTemplatePtr.DLGITEMTEMPLATE = item
// Advance the pointer to the end dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + DLGITEMTEMPLATE.Size )
// We want a static text, which is also done by ordinal dlgTemplatePtr.Int16( 0 ) = &hFFFF dlgTemplatePtr.Int16( 2 ) = &h0082 // StaticText
// Advance the pointer dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + 4 )
// Write out our caption numChars = MultiByteToWideChar( CP_ACP, 0, "This is my message to the world", -1, dlgTemplatePtr, 50 ) + 1
// Advance the pointer again dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + (numChars * 2) )
// No creation data dlgTemplatePtr = Ptr( Int32( dlgTemplatePtr ) + 2 )
And that's the end of that. We've now created our entire structure, wholly in memory. We've followed all of the rules, done our little dance, and now it's time to put something on the screen. We're going to be using the DialogBoxIndirect function. However, if you thought we were on to the easy part now, you were wrong. If you try to declare against DialogBoxIndirect, you'll notice that the function doesn't exist. After a whole lot of header diving, you'll notice that DialogBoxIndirect is a C macro for DialogBoxIndirectA or DialogBoxIndirectW. Trying again, you'll see that the function still isn't found. Header diving again shows that DialogBoxIndirectX is in turn a C macro for DialogBoxIndirectParamX. So picking one of those, you'll finally come up with this (which does work):
Soft Declare Function DialogBoxIndirectParamA Lib "User32" ( hInstance as Integer, template as Ptr, parent as Integer, func as Ptr, param as Integer ) as Integer Soft Declare Function DialogBoxIndirectParamW Lib "User32" ( hInstance as Integer, template as Ptr, parent as Integer, func as Ptr, param as Integer ) as Integer Declare Function GetModuleHandleA Lib "Kernel32" ( name as Integer ) as Integer
The call to GetModuleHandleA is needed because the DialogBoxIndrectParam functions need an HINSTANCE as their first parameter, and this is the API to get it for you. The second parameter to the function is our template pointer, which we've already filled out. The third parameter is a callback function, which we are going to ignore for today (but come back to tomorrow). Finally, the last parameter is a cookie that we can pass in, but we're going to ignore it and pass 0.
Armed with this knowledge, the final piece to the puzzle is:
// Unlock our memory pointer so that it can be worked with. Note // that unlock is not the same as free. GlobalUnlock( dlgMemPtr )
// Restore the original pointer value so that we're passing in // the start of our structure. dlgTemplatePtr = Ptr( oldStartValue )
// Get the module handle (HINSTANCE) dim moduleHandle as Integer = GetModuleHandleA( 0 ) dim ret as Integer
// Figure out which version to call, and call it. The Self.Handle // is because we need an owner window handle, and I'm calling // this from the Action event of a PushButton. if System.IsFunctionAvailable( "DialogBoxIndirectParamW", "User32" ) then ret = DialogBoxIndirectParamW( moduleHandle, dlgTemplatePtr, self.Handle, nil, 0 ) else ret = DialogBoxIndirectParamA( moduleHandle, dlgTemplatePtr, self.Handle, nil, 0 ) end if
// Free up our memory because we no longer need it GlobalFree( dlgMemPtr )
Holy moly, that's a lot of code just to get an ugly dialog on the screen! For an undocumented, but functional version of what we just described here, download this project. If you run it, you should see a dialog display on the screen once you click the PushButton. But you'll also notice that you can't actually interact with the dialog once it's been displayed. That's our topic for next time: the dialog box procedure.
1 The code you see may look somewhat similar to the code you find in the DialogBoxIndirect example. However, it's worth noting that the example from MSDN is essentially worthless because it is riddled with hard to find bugs. I've reported these bugs to Microsoft and hopefully they will fix their documentation. But my recommendation is to ignore the sample code on MSDN for this topic.
I apologize for not having this up on Sunday, but it was a busy weekend and took forever to format the code properly. Apparently, MT does not have a single functional syntax coloring plugin. I tried several, but all of them had major problems. So I'm stuck with pre tags for now, but MT messes those up as well if there's any double-newlines within the pre tags. Go figure.
It also appears as though the code snippets are cut off if they're too long. :: sighs :: If anyone here happens to be an MT expert, I would love some help getting a syntax highlighting plugin to work (I can take care of making sure the RB language is supported). Anyone feel like helping a poor desktop programmer out?
Holy Moly! That is a lot of stuff just for a little dialog box! Is this stuff applicable for normal C++ GUI stuff (not visual studio), or should I be very very glad that I use RB?
Generally speaking, most C++ programmers would never touch this stuff. The reason is because most C++ programmers making dialogs work with resource templates. So, for instance, they can do this:
DIALOG 36, 44, 230, 94
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Foobar"
BEGIN
CONTROL "OK", -1, "PushButton", WS_GROUP | BS_DEFAULTBUTTON, 4, 9, 48, 8
CONTROL "Whatever", -1, "static", WS_GROUP | SS_LEFT, 14, 19, 148, 18
END
And, this in turn allows them to use layout editors which write the code for them. However, if you're writing a resource compiler, then you need to know how to compile the above down into what we're working with.
But you should still be thankful you use REALbasic. :-)