Introduction
What prompted me to write this (rather monstrously large) project was the article that Jens Nilsson wrote about snappable windows. As a matter of fact, the demo project that's included with this article is basically a modified demo project from Jens' article. (Hope I'm not infringing on any copyright there... I was just too lazy to write my own demo.)
So what's wrong with Jens' article then?
There is one feature of the UI Jens created that really bugged me. That is, that the snapping windows actually obscure sections of the client window, even if they're in the autohidden state. To me, that's unacceptable -- if I have a control there I want the user to see, having it obscured by a window that's supposed to be "hidden" in the first place rather defeats the point.
VC.NET, I thought, has a terrific solution to this problem -- snap bars that live just outside the client area. So I decided to simulate what VC.NET does, with one major difference. The part of VC.NET implementation I don't like is that you have to pin a window to be able to drag it to a different snapbar, or to float it. That's rather annoying, and pointless. So I changed the dragging behavior of the windows to remove that limitation.
Note that this UI was really created for a project I'm building ( I figured putting it here will in a small way pay back all the code I've used from codeproject so far :-) ) What this means is that, while some design/UI decisions made may be absolutely applicable to what I'm doing, they may not be quite as applicable to you guys. I've done my best to make this framework as generic as possible, but if any artifacts still remain, please notify me and I'll correct them.
Finally, this is my first article. So forgive any inconsistencies/problems -- I'll do my best to correct them as they come up.
What this can do for you
- A complete solution for managing more than 3 windows at a time in one application. If you notice, the Window menu isn't really much of a solution to switch between windows, neither is docking (can you dock 16 windows at one time?). Snapping really seems to be the only way to go at the moment.
- A complete, easy to use solution. You hand the window handle off to the framework, and forget about it. The framework will take care of destruction, sizing, activation, etc.
- Support for snapped, docked, and floating windows. I.E. the user can switch between the three.
- Support for window state loading/saving using the registry.
- Standard window docking behavior (as suggested by James R. Twine)
- Optional window animation. (Instead of plain old Hide/Show). Intelligently works only on 2000/XP (ME? not sure), and defaults to Show/Hide on others, courtesy of Bjarke Viksoe. Intelligently slides from the appropriate side. (Thanks to Simon Steele for the suggestion.)
- Handles multithreaded UI -- i.e. does
AttachThreadInput
when it's supposed to, and detaches when it's supposed to.
Warnings
This code is over 3000 lines, all in one .h file. That means a couple things. One, that you'll get really annoyed at having a 120+k .h file, and wonder why I didn't distribute the code among many different files. Answer: convenience. This way you only include one file. I don't know, although I hate having implementation in .h files, I love seeing that in 3-rd party projects I'm thinking of using. It means I don't have to mess with my project file, and that the code is nicely self-contained.
You will also notice that all of my classes are nested within the main one. This is done for the same reason -- I didn't want to clutter up the ClassView window with internal classes you won't care about. This is what class nesting is for, I figure. I did not, however, nest third party classes -- it's enough I've put them into my .h file, I didn't feel like appropriating them to such an extent. :)
Finally, as the code is so huge, I can pretty much guarantee that there'll be bugs in it. I'm not Bill Gates (snicker), and can't write that much code without giving birth to a few bugs. Although I've play-tested the code on WinXP and fixed all the bugs I've found, I have not done any testing on any other platform. (Mostly because I don't have easy access to another platform at the moment.)
Update: I did test on Win98, and it does not work there -- well, vertical text doesn't work, which basically ruins the whole look; everything else seems to work ok though. I also haven't gone through the kind of QA process industry code goes through -- I figure that's what you guys are for. I think that's the nice feature of code sharing sites -- the users of the site get some ready-made code, and developers get free QA.
Anyway, consider yourselves warned.
Usage
Using this framework is frightfully simple. All you do is derive from the CSnapFramework
class, like this:
class CUitest3View : public CWindowImpl< CUitest3View >, public CSnapFramework< CUitest3View >
If you want to use the window saving/loading features, all you have to do is initialize the snap framework class with a registry key, like so:
CUitest3View() : CSnapFramework<CUitest3View>("SOFTWARE\\<your registry key here>")
The snapping framework will create a subkey under the key you specify, called WindowPositions in HKCU.
After you do that, make sure the chain the message map correctly, like so:
BEGIN_MSG_MAP(CUitest3View)
CHAIN_MSG_MAP(CSnapFramework< CUitest3View >)
... your messages go here....
END_MSG_MAP()
Notice that if you want to take care of some destruction stuff, you CAN put the OnDestroy
handler above the CHAIN_MSG_MAP
, just make sure to set the bHandled to FALSE
to give the framework a chance to handle the OnDestroy
too. (The framework sets bHandled to false as well, incidentally, just in case your OnDestroy
is below the CHAIN_MSG_MAP
.)
To add a window to the snapping framework, you do this:
void AttachWindow(IN HWND hWnd, IN eWinState state, IN CSize szSize,
IN bool bHasCloseCheckBox,
IN CPoint ptFloatingWinPosition = CPoint(0,0),
IN bool bLoad = true);
Finally, if you want window animation (you know, windows sliding out of nowhere, that sort of thing), call this:
void SetWindowAnimation(DWORD dwFlags = AW_CENTER, DWORD dwTime = 200)
That's it. I'm not providing a RemoveWindow
function -- it didn't seem to me (in my possibly delusional state) that anyone would want it. In my project, I certainly don't.
Here's one thing to remember though. Do NOT derive a top-level window from the CSnapFramework
. Huh? Ok, here's what's going on. Suppose you've got an SDI app. You do NOT want to derive the MainFrame window from this framework. You want to derive the View from it. Why? Two reasons:
- It's more convenient for me, the developer: The client automatically takes into account any toolbars/menus/whatever the mainframe might have, so by deriving the view from the framework I'm guaranteed not to mess up anyone's non-client areas.
- It's more convenient for you, the user. Deriving the view means you can derive ANY CWindow-derived window from this framework. I.E. it can be as arbitrarily nested as you feel like. This gives you more control over where to place the framework.
Implementation
There three main classes in this framework. I'll start explaining them bottom-up. Forgive the rather sketchy nature of the implementation details, look in source for more info. I've made some effort to comment it pretty thoroughly.
CSnapWindow
. This is a State-based class, which means the class nests three state classes all derived from a base state class. The state classes are CSnapWindowStateAuto
, CSnapWindowStateFloating
, and CSnapWindowStatePinned
. The window passes off all messages received to the current state, and the state does the necessary work. Ah, isn't it nice to have design patterns?
CSnapBarWindow
. This class is an implementation of the snapbar. It contains a std::list
of CSnapWindow
instances that belong to that snapbar. It knows how to draw itself, with some help from the particular CSnapWindow
to do its window rect calculation and painting. Handles a part of window tracking.
CSnapFramework
. Nests all the classes previously mentioned. Handles WM_NCSIZE
to control the client area, as well as CSnapBarWindow
placement calculations. Also handles dragging, animation, and the main part of window tracking.
Notice that CMouseHover
lies outside this whole hierarchy of classes. This is because it's not my class (it's Bjarke's -- see credits), and I figured someone else may want to use it in their code. I suppose I could've nested it with public permissions, but I didn't feel like mixing up my code and Bjarke's.
Requirements
You need to install WTL7 and Microsoft Platform SDK to get the demo and the framework to compile. I have now managed to compile the code with both VC6 and VC7. The code does not work correctly in Win98, because for some reason Win98 doesn't want to draw vertical text. Maybe there's something wrong in the code... I'm not sure, because I can't seem to find documentation claiming vertical text isn't supported on Win98... oh well. It should work fine on WinME, and, of course, on Win2000 and XP.
To Do
- Window size outlines when snapping to a bar -- kind of like in VC.NET, I think -- when you snap a window to one of the snapbars, it shows how much screen real estate that window will take up. It does that using dragrects. I think that's worth doing, just have to get time to do it.
- [possibly not?] Snapbar icons, also kind of like in VC.NET. I actually don't like the fact that text of the windows gets hidden, so all you see is an icon -- I think that's kind of confusing, but having the icons there certainly makes it look better.
- Get this code working under Win98/95. Basically I've tested with Win98, and the only thing that doesn't work is the vertical text. I see two solutions to this problem. One is to use a DC rotation code kind of like this. The other is to use GdiPlus, which I think works with any OS. (Am I wrong?) I'd like to use GdiPlus, actually -- I've used it already, and it's just beautiful, but the prospect of converting ALL this code to GdiPlus just somehow doesn't appeal to me...yet. Anyone else is welcome to do it though. :)
Version History
- 4/8/2002: Original article submission
- 4/9/2002: Article modified to add Requirements, ToDo, and correct Bjarke's name. (Kind of defeats the purpose of giving credit to someone if you misspell their name...:).
- 4/11/2002 Apologizing in advance for this rather.. er... frenetic frequency of releases. I tend to forget small details...
- Code modified to make it compatible/compilable with VC6.
- A rather large modification to get away from using percentages in sizing windows. After playing around with the UI for awhile, I decided that percentages are largely counterintuitive.
- <bug fix> Corrected usage of ::IsWindow -- I thought it returns TRUE or FALSE, turns out it returns either wildly non-zero values, or false. No big deal.
- <bug fix> Corrected size calculations for transferring a snapped window to a pinned state.
- <bug fix> Corrected numerous problems with windows starting out in Floating state.
- <bug fix> Corrected other minor crap.
- Added size gating for snapped and pinned windows. Now you can no longer resize the window so it occupies the entire client area. Ah, the subtle pleasures of
WM_GETMINMAXINFO
:).
- 4/15/2002 Took care of some of the ToDo's, and incorporated some requested features:
- Holding down Ctl key will disable snapping-docking behavior when dragging a window.
- <bug fix> Corrected problems that occur if Framework-derived class has a WM_DESTROY handler.
- <bug fix> Corrected tracking error that occurs if two pinned windows are unpinned in rapid succession. (the second one didn't track properly)
- <bug fix> Corrected a (user-created?) problem where several snapped windows could occupy the entire client area. Now pinned windows will resize automatically to make sure some client area always remains. Note that snapped windows can still obscure the entire client area, but I think it's up to the user to take care of that situation.
- 4/23/2002 A few functional changes:
- Added window animation
- <bug fix> Corrected a minor problem with pinned window sizing on startup
- Added multithreaded UI behavior -- intelligent AttachThreadInput calling.
- 4/23/2002 Incorporated a couple suggestions, and fixed another bug
- Added intelligent window animation -- i.e. if you specify the
AW_INTELLIGENT_SIDE
flag as the animation flag (combined with AW_SLIDE
), the window will slide out from the side it's docked at, and slide in at the right side too. (Thanks to Simon Steele for the suggestion.)
- <bug fix> Corrected bad snapping behavior: if you snapped out a window, and then dragged another one, the first window would stay snapped out forever. Argggh.
- Added behavior: clicking on snapped window buttons on the snapbar will cause them to appear. (Thanks to Simon Steele for the suggestion.)
- 5/6/2002 Bug fixes:
- <bug fix> Fixed focus handling when a snapped window gets hidden -- if that window's child has the focus we reset the focus to the main client window
- <bug fix> Changed
SendMessage(WM_PAINT)
s to RedrawWindow
calls, thanks to to Christian Andritzky for the suggestion.
- <bug fix> Fixed a typo in multithreaded UI handling that would prevent AttachThreadInput from detaching on close.
- 5/24/2002 Bug fixes:
- <bug fix> Fixed handling of floating windows. They can now no longer be hidden by the mainframe.
- <bug fix> Fixed handling of snapping windows. There was a small bug where they'd get hidden incorrectly.
- UI Enhancement: improved the way snapped window title bar looks
- UI Enhancement: improved the way pinned window's gripper looks.
Credits
As mentioned before, big thanks to Jens Nilsson for his original article. There were a couple pieces of code I used from there:
- Used the Demo Project he supplied in its entirety, except of course for disconnecting his snapping framework, and connecting mine.
- Used a function from his snapping framework to draw a pushpin. It's documented in the code.
Used Bjarke Viksoe's terrific CMouseHover class, with a few slight modifications. I believe its use there is consistent with Bjarke's copyright.
Also used Bjarke's AnimateWindow implementation (4 lines of code, really, but giving credit where credit's due :)
Speaking of Copyright
(you knew it was coming...)
The code is completely, utterly, and absolutely free. Feel free to use it in private or commercial applications, modify it, sit on it, or print it out and use it for toilet paper. God knows I've ignored enough copyrights from CodeProject to have the nerve to make my own. :) One thing though -- if this code messes up your computer or puts your project seven months behind schedule, it's not my fault. None of it is my fault. Not even that "format c:" somewhere in there. Er, just kidding. :) Oh yeah, needless to say, you can't claim this code as your own. (I.E. I'd hate for someone to reprint this code, and copyright the hell out of it. Not sure if that's possible anyhow, but it would suck. Greatly.)
P.S. Probably a pointless thing to say, but if you're going to rate the article badly, at least take the time to write a comment saying how I could improve it, or what you find inadequate about it...