Did you know that the MDIWindow class exists? (I find that most people who've used MDI in REALbasic don't know it's there). Check out App.MDIWindow for details. Even better trivia question: did you know that MDIWindow had the ability to maximize the MDI frame -- something you couldn't do with a regular Window class until RB 2005?
Ok, enough about lame trivia, time for some useful trivia. In MDI-land, there are three concepts of a window. There's the frame window, which is the part that has the global menu bar, the UI state widgets, etc. Basically, it's the big, all-encompassing window. Then there are the document windows -- those are the small "inner" windows that are contained within the larger area. The third part of an MDI application is called the MDIClient window, which is the grey-ish background window. You would think that's part of the MDI frame window and nothing too special, but you'd be thinking wrong. The MDIClient window is the only child of the MDI frame window. The document windows are all children of the MDIClient window.
So why is this interesting, you might ask? Simple! Most MDI applications have three standard menu items under the Window menu: Tile Horizontally, Tile Vertically and Cascade. These all refer to different ways that you can automatically position all the document windows with one menu command. Let's say you wanted to provide this functionality for your own application.
You'd get some code that looks like this:
[rbcode]
Declare Sub TileWindows Lib "User32" ( parent as Integer, _
how as Integer, r as Integer, num as Integer, kids as Integer )
// The last three are always 0 because you're going to use the entire
// frame and all children
TileWindows( App.MDIWindow.Handle, 0, 0, 0, 0 )
[/rbcode]
The trouble is -- this code won't work! That's because MDIWindow.Handle returns the MDI Frame handle (which is useful for when you need to set the state of the UI state buttons on the fly, for example). Since the wrong parent is passed into the function, all that happens is you end up tiling the MDIClient window, which is already taking up the entire content area of the MDI Frame window!
That leaves you in quite a pickle since you don't have a handle to the MDIClient window itself. But you don't have to worry, it's easy! You need to get the only child window from the frame, and there are APIs to do this trick.
So how do you get the MDIClient window handle? With enumeration code, of course!
[rbcode]
Module SomeHandyModule
Dim mClientHandle as Integer
Function MDIClientHandle( extends w as MDIWindow ) as Integer
Declare Sub EnumChildWindows Lib "User32" ( parent as Integer, _
proc as Ptr, cookie as Integer )
// The first order of business is to nil out
// our global handle for the client HWND
mClientHandle = 0
// Now enumerate
EnumChildWindows( w.Handle, AddressOf enumChildProc, 0 )
// And return the handle
return mClientHandle
End Function
Private Function enumChildProc( hwnd as Integer, cookie as Integer )
// We want to be doubly-certain that we find the right handle, so
// we will check the class name of the window passed to us
Declare Sub GetClassNameA Lib "User32" ( hwnd as Integer, _
name as Ptr, size as Integer )
Dim name as new MemoryBlock( 256 )
GetClassNameA( hwnd, name, name.Size )
// Check to see whether this is an MDICLIENT window
if name.CString( 0 ) = "MDICLIENT" then
// This is the right window, so store its handle and return
// false to say that we're done enumerating
mClientHandle = hwnd
return false
end if
// Return true so we can keep enumerating
return true
End Function
End Module
[/rbcode]
So now that you have the extension function in the module to get the MDIClient window's handle, you can change our first code snippet quite easily to be able to tile the windows. Now it will look like this:
[rbcode]
Declare Sub TileWindows Lib "User32" ( parent as Integer, _
how as Integer, r as Integer, num as Integer, kids as Integer )
// The last three are always 0 because you're going to use the entire
// frame and all children
TileWindows( App.MDIWindow.MDIClientHandle, 0, 0, 0, 0 )
[/rbcode]
Note the simple change from MDIWindow.Handle to MDIWindow.MDIClientHandle. Now you're able to tile and cascade document windows in your MDI application! You should also note that this function (as well as the ability to tile and cascade document windows) will be in the Windows Functionality Suite 2.0 release so that you don't have to worry quite so much about this.
Disclaimer: I don't condone the use of MDI windows except under extreme circumstances. If you are thinking of using an MDI window, please rethink your design a little bit. If you absolutely can come up with no other way, then go ahead and use MDI -- and hopefully this lesson will help you out!
That's pretty cool. I'm forced to use MDI for now, so this will be a nice addition.
Something I could really use is the ability to fix a floating window to a side of the MDI. Just like how the RB 5.x IDE's Controls palate and properties list are under Windows. Is this possible with a nifty declare or two?
It's certainly possible to do (they're called rebar controls btw), but it's not just a few declares to get them to work well, which is why I've never added them to the Suite. I'd like to see them built into the product personally -- they're powerful, and that's the only decent way to implement them.
Wow, I've posted comments to your blog twice in a row now. Sorry.
I have one question. This is the reason I have never attempted to use declares before. When using a declare, how do you know the name of the method you want to call? This little piece of information always seems to be missing from Declare tutorials.
Lol, no need to apologize Steve. :-)
The reason it's left out is because it's a tough one to answer. The name comes from whatever the system API you want to call. So in order to pick the name, you have to already know what you want to be calling. So here's a mini-primer on how I typically do declares when I know what I want to solve, but don't know what API to call.
Let's say I want to know the double click time (the max amount of time allowed to count two clicks "together" as a double click) on Windows.
The first thing I do is go to google and type something sensible like "Win32 API double click time". Then I search through the results until I find a snippet of code (usually from MSDN) that talks about what I am trying to do. In this case, I ran into Raymond Chen's blog, and I notice a user comment say: "For double click time there is also SetDoubleClickTime and GetDoubleClickTime."
Then I go to "http://msdn.microsoft.com/" and search for GetDoubleClickTime. This brings me to a results page where I can find a page that describes the function (in this case, we're lucky -- it's the first page returned, but sometimes you have to do some digging).
This page shows all the information I need for the declare. It shows the name of the function (GetDoubleClickTime), the Lib string (User32), the parameter list along with information about what goes there (in this case, void, meaning it's empty), and the return type along with what's returned and when (a UINT is an Integer in RB).
So I just convert the C declaration on that page into an RB declare and I'm on my way.
Make sense?
Yes, that make sense. I just always figured that there had to be a resource somewhere that documented this better.
So instead of MDI Windows, what do you suggest to use when you have an application that has multiple documents open? Having a taskbar item for every document AND the application is just plain ridiculous if you ask me. That is the one thing I hate the most about Office 2003. It's the first "feature" I turn off when I install it. And a tabbed window isn't always a good idea either. For example, when you want to compare to documents side-by-side. I know it's Real Software's standard line to not use MDI, but really, it seems to really still be the best way to do things, IMHO.
It's not just *our* party-line, it's the party line from Microsoft as well. MSDN -- check out the very last section about design tradeoffs.
MDI is generally a considered a crutch and it's hell for anyone with multiple monitors unless most of the windows are floating palettes (in which case, the MDI is useless anyways). Also, it's generally a much more confusing interfaces for most users to understand and get comfortable with.
If you don't like tabbed interfaces, then go with the list interface (like TextPad or Windows Explorer). If you want to compare docs side by side, you can use a single window compare feature (like CodeWarrior). It's pretty rare that there's a quality application design which absolutely necessitates use of MDI.
Well... I'm gonna have to disagree with ya (and Microsoft, whose stds leave much to be desired anyways - don't get me started about how poorly designed Windows XP's UI is).
In MANY specialty applications, you will run into a situation where you need MDI windows. The average text-style document such as word processing docs, spreadsheets and the like, of course, work with a split-window style.
But, for example, in a large class registration/scheduling system, like we use you need MDI. Here's just a few examples of why:
- Comparing Classes
- Comparing a Customer's previous receipts
- Comparing Customer records (for families for example)
I call each of those a document, but they are radically different looking windows with very different information. If we had done single windows for each document, the taskbar would get flooded with windows and we become difficult to navigate.
We used to employ a SDI system, using tabs. Our users found it needlessly complex to do simple tasks.
I guess, what I'm really getting at is good UI design is not what Microsoft or any other software firm says is good design. Good design is a design that the user can live with.
... And sometimes that means using MDI :P
-Scott
PS-I'm not a huge fan of MDI, but I do know that it has its place.
We're definitely both agreed on the fact that MDI has its place (as you aptly pointed out). I just put very dire warnings out there because a lot of people tend to use it as a crutch because it's easy to do so, which is wrong. So if I make it seem like the devil incarnate, then people will hopefully put more thought into the design of their application. ;-)