Introduction
Have you seen a menu with no menu items in it, or just a single item popping
up? For example the standard Help menu created in VC has only the "About" item.
Some applications start with very basic menu panels. Why not show the dialogs
behind the single menu item instead of the menu item itself. There I come with
this new idea!
How it works
Making animated dialog menus is truly simple on Windows 98, NT5 and later,
the solution to the problem on older systems is far more complex. This is the
first version of my code, it surely has lots of "bugs". I hope it is useful to
someone.
The key is the AnimateWindow
function deeply hidden in the
Windows API. A good idea when using "preliminary" windows API functions is to
add these two lines before any headers get included in the application code:
#undef WINVER
#define WINVER 1000000
Heh, not really a good idea, but the only choice for Visual C++ 6 with
some service packs and Windows XP Pro SP1 (and yeah, it works!). You should
first check if the call to AnimateWindow
works without it.
WINVER is what VC defines in order to identify the version of windows you
are building for. It is predefined and the API declarations refer to it when
some compatibility info is required. While compiling you will be informed what
to do if you want a stable build.
I created a dialog based application which itself contains a child dialog
that serves as a menu bar (namely CDummy
). The menu bar dialog
handles user requests by popping up the dialogs. It also does draw the effects
and most of the cleanup/repaint operations. Simple buttons control the popup
operations. A little trick I used is that the IDs of the buttons are consecutive
1026, 1027, 1028, so I can address them by an offset value if necessary. It is
not implemented in the demo project.
The code that draws the menus is very straightforward (and slow), the best
explanation is just to read along the lines:
void CDummy::ShowDialogMenu(int number, int initial_control)
{
HideAllDialogMenus();
if(!statMenu[number])
{
CRect rc;
GetDlgItem(initial_control)->GetClientRect(&rc);
GetDlgItem(initial_control)->ClientToScreen(&rc);
this->ScreenToClient(&rc);
listMenu[number]->SetWindowPos(0,rc.left,rc.bottom,
0,0,SWP_NOSIZE|SWP_HIDEWINDOW);
AnimateWindow(listMenu[number]->GetSafeHwnd(),300,
AW_ACTIVATE|AW_SLIDE|AW_VER_POSITIVE|AW_HOR_POSITIVE);
CWindowDC dc(0);
CDC tdc;
statMenu[number]=1;
listMenu[number]->GetClientRect(&rc);
listMenu[number]->ClientToScreen(&rc);
dc.MoveTo(rc.left,rc.top);
dc.LineTo(rc.right,rc.top);
dc.LineTo(rc.right,rc.bottom);
dc.LineTo(rc.left,rc.bottom);
dc.LineTo(rc.left,rc.top);
tdc.CreateCompatibleDC(&dc);
CBitmap map1;
map1.CreateCompatibleBitmap(&dc,1600,1200);
tdc.SelectObject(&map1);
tdc.BitBlt(rc.left,rc.top,rc.right+S_SHADE,
rc.bottom+S_SHADE,&dc,rc.left,rc.top,SRCCOPY);
for(int w=rc.right;w<=rc.right+S_SHADE;w++)
{
float f=((float)w-rc.right)/S_SHADE;
for(int e=rc.top+S_SHADE;e< rc.bottom;e++)
{
tdc.SetPixel(w,e,RGB(
GetRValue(tdc.GetPixel(w,e))/(2-f),
GetGValue(tdc.GetPixel(w,e))/(2-f),
GetBValue(tdc.GetPixel(w,e))/(2-f)));
}
}
for(int e=rc.bottom;e<=rc.bottom+S_SHADE;e++)
{
float f=((float)e-rc.bottom)/S_SHADE;
for(w=rc.left+S_SHADE;w<=rc.right+1;w++)
{
tdc.SetPixel(w,e,RGB(
GetRValue(tdc.GetPixel(w,e))/(2-f),
GetGValue(tdc.GetPixel(w,e))/(2-f),
GetBValue(tdc.GetPixel(w,e))/(2-f)));
}
}
for(e=rc.bottom;e<=rc.bottom+S_SHADE-2;e++)
{
float f=((float)e-rc.bottom)/S_SHADE;
for(w=rc.right+2;w<=rc.right+S_SHADE-2;w++)
{
float g=((float)w-rc.right+2)/(S_SHADE);
tdc.SetPixel(w,e,RGB(
GetRValue(tdc.GetPixel(w,e))/(2-(f+g)/2),
GetGValue(tdc.GetPixel(w,e))/(2-(f+g)/2),
GetBValue(tdc.GetPixel(w,e))/(2-(f+g)/2)));
}
}
dc.BitBlt(rc.left,rc.top,rc.right+S_SHADE,rc.bottom+S_SHADE,
&tdc,rc.left,rc.top,SRCCOPY);
}
}
The shadow is completely owner-drawn, it paints directly on the desktop
window. This causes some problems with the cleanup paint job but it is fine for
shadows. I will soon update to popup windows and then it will make some sense.
However, if you are concerned about efficiency or paint safety you can simply do
a CClientDC
paint. The hide menu routine is simply a repaint.
Something important to say is that you can change the animation type by
modifying the flags in the AnimateWindow
. For example change
AW_ACTIVATE | AW_SLIDE | AW_VER_POSITIVE | AW_HOR_POSITIVE
to
AW_ACTIVATE | AW_SLIDE | AW_HOR_POSITIVE
or AW_ACTIVATE |
AW_SLIDE | AW_VER_POSITIVE
or something else as defined in MSDN under
AnimateWindow
.
Using the code in your apps
I didn't try to make a class to implement the functionality because I wanted
to keep the way user controls the dialogs custom. I am open to suggestions here.
Anyways I managed to separate the following steps, that should make using the
code in your applications easier:
- Create all dialogs you want to appear as menus. It is recommended to use
CDialog
inherited dialogs. In properties select child, check
control, visible and set foreground. If you don�t specify any of these styles
the menu may not paint correctly. They all have to do with the z-order and the
parent management, not sure how exactly. You know where to dig for details, do
you not. Hey, don't forget to include the proper headers in your CDummy
source file, so you can create instances.
- Create a menu bar dialog, like
CDummy
. This one is inherited
from CDialog
, but it could be any CWnd
based thing.
For the SDK fans - you can use direct handles with slight modifications to
eliminate the MFC device contexts. In your main window class declare an instance
of CDummy
and initialize it (in OnInitDialog()
for
example) as follows:
dummy=new CDummy(this);
dummy->Create(IDD_DIALOG2,this);
dummy->SetWindowPos(0,0,0,0,0,SWP_NOSIZE|SWP_SHOWWINDOW);
- Declare arrays to store the pointers to your dialogs and the current status
(in the demo project: listMenu and statMenu). Create instances of
your menu dialogs and put initialization code in the
WM_INITDIALOG
handler, like that: BOOL CDummy::OnInitDialog()
{
CDialog::OnInitDialog();
memset(listMenu,0,sizeof(listMenu));
menu1.Create(IDD_DIALOG1,this->GetParent());
menu1.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW);
menu2.Create(IDD_DIALOG3,this->GetParent());
menu2.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW);
menu3.Create(IDD_DIALOG4,this->GetParent());
menu3.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW);
menu4.Create(IDD_DIALOG5,this->GetParent());
menu4.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW);
listMenu[0]=&menu1;
listMenu[1]=&menu2;
listMenu[2]=&menu3;
listMenu[3]=&menu4;
return TRUE;
}
- The events causing the menus to show up or hide must occur in
CDummy
. Put some buttons or whatever and process the events. In the
demo project I used simple buttons. This is how I handle one of my buttons�
BN_CLICKED
messages: void CDummy::OnButton1()
{
ShowDialogMenu(0,IDC_BUTTON1);
}
ShowDialogMenu(int arg1,int arg2)
shows the dialog on the
screen. arg1
tells which dialog should be shown, for example
0 means the dialog referenced by listMenu[0]
will be show.
arg2
is the ID of the control (static, button, edit box, check box,
whatever) next to which the dialog will be drawn. The function gets the client
rectangle of the control and draws the dialog just below the
rectangle.
- Handle
WM_KILLFOCUS
in the main window, one of the reasons all
menu dialogs must be child:void CDmenuDlg::OnKillFocus(CWnd* pNewWnd)
{
CDialog::OnKillFocus(pNewWnd);
dummy->HideAllDialogMenus();
}
- Handle
WM_ACTIVATE
in the main window, this kind of fixes the
"repaint after lost focus", but not always (see below):void CDmenuDlg::OnActivate(UINT nState, CWnd* pWndOther,
BOOL bMinimized)
{
CDialog::OnActivate(nState, pWndOther, bMinimized);
dummy->HideAllDialogMenus();
Invalidate();
}
Known issues
There are few bugs in Visual C++ or in my code.
- Sometimes
AnimateWindow
doesn�t do anything and returns
ERROR_SUCCESS
. The demo project application will never do that, but
your applications may not be so lucky. I didn�t manage to find out what causes
the problem, but it applies to all preliminary functions.
- Clicking on a control in the main window doesn�t make the menu to disappear
and paints the control over it. To fix this you may use message reflection or
manual message forwarding to the main window, but you have to do that for all
controls on the main window. I am working on another solution.
- If a menu is active and the application loses focus, incorrect repaint will
occur. The last one is a mystery to me!
- On Windows 98 and ME the parallel tasks do not work together very well, I
mean they go slow because of the weak OS threading. Do not overload the app with
many active threads at a time.
I was hired by the pentagon to do this project. They needed some software to
attach to the control interface. I have not included the drivers you see, but I
may have cut/left some other code by an accident. Keep you eyes
open!
Heh, kidding, but keep your eyes open, I may have forgotten to
mention about some little trick you need to make it work. This is a good place
to add �If this code works it�s written by Vladimir Ralev else I don�t know who
wrote it!�, like someone writing for the MSDN.
Thanks for all comments
and fixes.
Credits
Written by Vladimir Ralev <v_ralev@yahoo.com>, 20 October 2003
Special thanks to PJ Naughter for his great work on
CTreeFileCtrl
& CSortedArray
v1.06. I used (a
little cut) CTreeFileCtrl
to illustrate how easily one can select a
file without having to popup a modal dialog.