|
|
|
|
|
|
About this page Noticed
the new menus available on OfficeXP and .NET?
On this page you will find a free OfficeXP-style menu for VB6, plus an article that describes how to build
such a control.
Downloads
|
|
It is happening again! a new
release of Office is a synonym of a new user interface.
Now Microsoft has changed the look-and-feel of the menus in both OfficeXP
and VS.NET and all VB developers (including me) are wondering how
they can add this new menu to their programs. It all began with the flat
buttons, remember? We were all searching for free flat buttons so we could
mimic the Office97 toolbars (there are things that will never change!).
And this is what this article is all about: how to create an OfficeXP menu
for VB6. First things
first: in order to create a menu that has a look different to the standard
we need to use what is called owner-drawn menus. Windows provides a set of
APIs to create menus and by specifying the owner-drawn flag (MF_OWNERDRAW)
we can completely control the appearance of the menu items. Let's
see an example. Suppose we want to popup an XP menu when we right-click on
our VB6 form. The menu should look something like: 
Private Sub Form_MouseDown( _
Button As Integer, _
Shift As Integer, _
X As Single, Y As Single)
Dim pt As POINTAPI
If Button <> vbRightButton Then Exit Sub
pt.X = Me.ScaleX(X, vbTwips, vbPixels)
pt.Y = Me.ScaleY(Y, vbTwips, vbPixels)
ClientToScreen Me.hWnd, pt
pShowMenu pt.X, pt.Y
End Sub
Private Sub
pShowMenu(ByVal X As Long, ByVal Y As Long)
m_MenuHandle = CreatePopupMenu()
AppendMenu m_MenuHandle, MF_STRING Or MF_OWNERDRAW, 1, 1
AppendMenu m_MenuHandle, MF_STRING Or MF_OWNERDRAW, 2, 2
AppendMenu m_MenuHandle, MF_SEPARATOR Or MF_OWNERDRAW, 3, 3
AppendMenu m_MenuHandle, MF_STRING Or MF_OWNERDRAW, 4, 4
TrackPopupMenuEx _
m_MenuHandle, _
TPM_LEFTALIGN Or TPM_TOPALIGN Or TPM_LEFTBUTTON, _
X, _
Y, _
Me.hWnd, _
0
End Sub
The above code will popup a menu every time we right-click on our
VB form, but because the menu has been created with the MF_OWNERDRAW flag,
we will have to respond to the WM_MEASUREITEM and WM_DRAWITEM messages in
order to set the size of every menu item and draw its contents. But how
can we trap these two messages? Well, we can use the SmartSubClass
in order to subclass the form and listen to every message posted to its window. We will also have to listen for the WM_EXITMENULOOP message in
order to destroy the menu when it closes. We will need to add the
following code:
Dim WithEvents m_Sniff As SmartSubClass
Private Sub Form_Load()
Set m_Sniff = New SmartSubClass
m_Sniff.SubClassHwnd Me.hWnd, True
End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
If Not m_Sniff Is Nothing Then
m_Sniff.SubClassHwnd Me.hWnd, False
End If
End Sub
Private Sub m_Sniff_NewMessage(ByVal hWnd As Long, uMsg As Long, wParam As Long, lParam As Long, Cancel As Boolean)
Select Case uMsg
Case WM_EXITMENULOOP
If m_MenuHandle <> 0 Then
DestroyMenu m_MenuHandle
m_MenuHandle = 0
End If
Case WM_MEASUREITEM
Call pMenuItemMeasure(lParam)
Case WM_DRAWITEM
Call pDrawMenuItem(lParam)
End Select
End Sub
The Subroutine pMenuItemMesure() takes care of returning the size of every
menu item, and the subroutine pDrawMenuItem() takes care of drawing its
contents. If you want more information about these two messages you can
find it on the MSDN.
Well, seems easy, doesn't it? I first thought that by
creating owner-drawn menus I would take FULL control of the painting
process and that I would be able to build XP menus very easily but... I was
wrong! Why? Because owner-drawn menus give you control on how
menu-items are painted but there's no way you can change the menu border.
The menu border always remains 3D. But menus are basically windows,
right? So my second thought was - "if a menu is using a window to
display the menu items, I should be able to change the window border by subclassing
it" - and that's when I started looking for a function that would
return a window handle from a menu handle but... it doesn't exist.
Menus
are a system global class and Windows takes care of creating its window
and handling all its messages. When you create menus using Win32 APIs you
don't get any information at all about its window handle. So what's
the solution then? Well, I would like to thank a very good friend of mine,
Garth
Oatley, who gave me the answer. He sent me a VB6 project where he was
using hooks to trap all windows messages that belonged to the same thread
and he showed me how to detect when a window belonging to the menu class
was created. So thank you Garth!
For those of you who don't have experience using hooks, Microsoft
describes hooks as "A point in the system
message-handling mechanism where an application can install a subroutine
to monitor the message traffic in the system and process certain types of
messages before they reach the target window procedure". In other
words, a hook is a way of subclassing a whole thread. There are different
hooks you can create, depending on the type of message you want to
trap. In order to detect when a menu-window is about to be created, we
will use the WH_CALLWNDPROC hook.
You can find below an example that shows how we can set a hook that
detects when a menu-window is being created in order to subclass it and
modify its border style.
1. First we will need to modify both the Load() and QueryUnload()
events in order to install our own function in the hook-chain.
Private Sub Form_Load()
' - Get the Process thread...
m_ThreadID = GetWindowThreadProcessId(hwnd, 0)
' - Install our own hook...
m_HookID = SetWindowsHookEx( _
WH_CALLWNDPROC, _
AddressOf pHookCallWndProc, _
0, _
m_ThreadID)
' -
Subclass the window...
Set m_Sniff = New SmartSubClass
m_Sniff.SubClassHwnd Me.hWnd, True
End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
If Not m_Sniff Is Nothing Then
m_Sniff.SubClassHwnd
Me.hWnd, False
End If
If m_HookID <> 0 Then
UnhookWindowsHookEx m_HookID
End If
End Sub
2. Add a new module with the following code:
Public Function pHookCallWndProc( _
ByVal ncode As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
Dim CWP As CWPSTRUCT
Dim lRet As Long
If ncode = HC_ACTION Then
CopyMemory CWP, ByVal lParam, Len(CWP)
Select Case CWP.message
Case WM_CREATE
' - Make sure that the window
' belongs to the menu class
If pGetClassName(CWP.hwnd) = "#32768" Then
' - Subclass the window...
lRet = SetWindowLong( _
CWP.hwnd, _
GWL_WNDPROC, _
AddressOf pSubclassWndProc)
' - Store the old windowproc...
SetProp CWP.hwnd, "OldWndProc", lRet
End If
End Select
End If
' - Call the next hook...
pHookCallWndProc = CallNextHookEx( _
WH_CALLWNDPROC, _
ncode, _
wParam, _
lParam)
End Function
Public Function pSubclassWndProc( _
ByVal hwnd As Long, _
ByVal uMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
Dim lRet As Long
Dim lTmp As Long
lRet = GetProp(hwnd, "OldWndProc")
Select Case uMsg
Case WM_CREATE
' - Change the window border
' and make it flat...
lTmp = GetWindowLong(hwnd, GWL_STYLE)
lTmp = lTmp And Not WS_BORDER
SetWindowLong hwnd, GWL_STYLE, lTmp
lTmp = GetWindowLong(hwnd, GWL_EXSTYLE)
lTmp = lTmp And Not WS_EX_WINDOWEDGE
lTmp = lTmp And Not WS_EX_DLGMODALFRAME
SetWindowLong hwnd, GWL_EXSTYLE, lTmp
Case WM_DESTROY
' - UnSubclass the window...
RemoveProp hwnd, "OldWndProc"
SetWindowLong hwnd, GWL_WNDPROC, lRet
End Select
' - Call the next WindowProc...
pSubclassWndProc = CallWindowProc( _
lRet, _
hwnd, _
uMsg, _
wParam, _
lParam)
End Function
Public Function pGetClassName(ByVal hwnd As Long) As String
Dim sClass As String
Dim nLen As Long
sClass = String$(128, Chr$(0))
nLen = GetClassName(hwnd, sClass, 128)
If nLen = 0 Then
sClass = ""
Else
sClass = Left$(sClass, nLen)
End If
pGetClassName = sClass
End Function
Ok, let's review the above code with more detail. As you can
see, we use the API SetWindowsHookEx() in the Form_Load() event, to add
our own function to the thread's hook-chain. We are creating a
WH_CALLWNDPROC hook, which means that we'll have access to all messages
before they get their window procedure. In the
Form_QueryUnload() event, we need to unhook in order to prevent a system crash.
Next we have the function pHookCallWndProc(). This function is the
actual hook. Because all the messages of the thread will pass through this
function, we need to make sure that the function is not acting as a
bottle-neck. What that means is to add as little code as we can.
What we do here is to check for the WM_CREATE message and, every time we
trap this message, check that the window belongs to the menu class. A menu
creates a window with its class equal to "#32768" (don't ask me
how I know that!). As soon as we detect that a menu window is
about to be created, we must subclass that window.
Finally, after a menu window has been subclassed, function
pSubclassWndProc() takes care of making its border flat. It does that when
it detects a WM_CREATE message. The function also unsubclasses the window
when message WM_DESTROY is posted.
That's it! by just using two procedures and a couple of APIs we have
created a hook, detected when a menu is about to be created and, by
subclassing that window, convert its 3D border into a flat border.
What's next? We've created menus using the owner-drawn flag, we've
used a hook to modify the window border of the menu and make it flat and... what
about the shadow? As you all probably know, OfficeXP menus have a very
nice feature: there's a shadow on the right-bottom border of their
window. How can we implement this effect?
Well, now that we have created the hook, to add the shadow is quite
simple. We just need to add more code to the function pSubclassWndProc() in
order to draw the shadow every time the message WM_ERASEBKGND is posted.
There's only one trick: the shadow has to be drawn within the menu window
hDC. You can find below an example. You can call this function from the WM_ERASEBKGND
message and you just need to provide the window handle, its hDC and the
position of the window using screen coordinates.
Public Sub DrawMenuShadow( _
ByVal hWnd As Long, _
ByVal hDC As Long, _
ByVal xOrg As Long, _
ByVal yOrg As Long)
Dim hDcDsk As Long
Dim Rec As RECT
Dim winW As Long, winH As Long
Dim X As Long, Y As Long, c As Long
'- Get the size of the menu...
GetWindowRect hWnd, Rec
winW = Rec.Right - Rec.Left
winH = Rec.Bottom - Rec.Top
' - Get the desktop hDC...
hDcDsk = GetWindowDC(GetDesktopWindow)
' - Simulate a shadow on right edge...
For X = 1 To 4
For Y = 0 To 3
c = GetPixel(hDcDsk, xOrg + winW - X, yOrg + Y)
SetPixel hDC, winW - X, Y, c
Next Y
For Y = 4 To 7
c = GetPixel(hDcDsk, xOrg + winW - X, yOrg + Y)
SetPixel hDC, winW - X, Y, pMask(3 * X * (Y - 3), c)
Next Y
For Y = 8 To winH - 5
c = GetPixel(hDcDsk, xOrg + winW - X, yOrg + Y)
SetPixel hDC, winW - X, Y, pMask(15 * X, c)
Next Y
For Y = winH - 4 To winH - 1
c = GetPixel(hDcDsk, xOrg + winW - X, yOrg + Y)
SetPixel hDC, winW - X, Y, pMask(3 * X * -(Y - winH), c)
Next Y
Next X
' - Simulate a shadow on the bottom edge...
For Y = 1 To 4
For X = 0 To 3
c = GetPixel(hDcDsk, xOrg + X, yOrg + winH - Y)
SetPixel hDC, X, winH - Y, c
Next X
For X = 4 To 7
c = GetPixel(hDcDsk, xOrg + X, yOrg + winH - Y)
SetPixel hDC, X, winH - Y, pMask(3 * (X - 3) * Y, c)
Next X
For X = 8 To winW - 5
c = GetPixel(hDcDsk, xOrg + X, yOrg + winH - Y)
SetPixel hDC, X, winH - Y, pMask(15 * Y, c)
Next X
Next Y
' - Release the desktop hDC...
ReleaseDC GetDesktopWindow, hDcDsk
End Sub
' - Function pMask splits a color
' into its RGB components and
' transforms the color using
' a scale 0..255
Private Function pMask( _
ByVal lScale As Long, _
ByVal lColor As Long) As Long
Dim R As Long
Dim G As Long
Dim B As Long
pConvertToRGB lColor, R, G, B
R = pTransform(lScale, R)
G = pTransform(lScale, G)
B = pTransform(lScale, B)
pMask = RGB(R, G, B)
End Function
' - Function pTransform converts
' a RGB subcolor using a scale
' where 0 = 0 and 255 = lScale
Private Function pTransform( _
ByVal lScale As Long, _
ByVal lColor As Long) As Long
pTransform = lColor - Int(lColor * lScale / 255)
End Function
SmartMenuXP is a free control that provides VB6 with OfficeXP
look-and-feel menus. You can freely use this control in your applications.
 |
|
 |
This is how SmartMenuXP
appears on the toolbox |
|
This is how
SmartMenuXP shows in design mode |
After you have dropped the SmartMenuXP control on your form you can easily
build your menu by using its property MenuItems. Let's see an example:
With SmartMenuXP1.MenuItems
.Add 0, "keyFile", , "&File"
.Add "keyFile", , , "&Open...",
GetPic(1), vbCtrlMask, vbKeyO
.Add "keyFile", , , "&Save...",
GetPic(2), vbCtrlMask, vbKeyS
.Add "keyFile", , smiSeparator
.Add "keyFile", , , "E&xit", , vbAltMask, vbKeyQ
End With
You will always use the function MenuList.Add() to add menuitems to the
menu. All parameters in this function are optional except one: the
"Parent" parameter. You can specify the parent by using either
its numeric ID or its key. Menu items that appear on the menu bar
always have Parent=0. The function returns the ID for the new menu item.
The syntax of MenuList.Add() is as follows:

After adding a menu item you can read/modify all its properties by
using the MenuItem class.
Example:
SmartMenuXP1.MenuItems.Caption(1) = "Hello World"
SmartMenuXP1.MenuItems.Enabled(2) = False
Next step after creating the menu items is to decide where you want
the menu to appear. You can place the menu at any point on your form by
setting property Align = vbAlignNone, or you can stick the menu to the
top, bottom, left or right sides of your form.
Another interesting thing is that you have access to all the different
areas of the menu. You can change the color of these areas by using
properties ArrowColor, BackColor,
CheckBackColor, CheckBoxColor,
CheckMarkColor, FontBackColor,
FontForeColor, SelBackColor,
SelForeColor,
SelBoxColor
and SeparatorColor. You can also change the font by using property
Font.
SmartMenuXP comes with default values for all these properties so that you
always get the new OfficeXP look-and-feel. However, by changing any of
the properties you can get interesting effects.
|
 |
|
Change the style of your menus
by using the color properties |
Also, every time a menu item is selected by either using the mouse, the
keyboard, typing its access key (ALT+key) or its shortcut key, the event Click() is fired. This event has a parameter that returns the menu item
ID.
Private Sub SmartMenuXP1_Click(ByVal ID As Long)
With SmartMenuXP1.MenuItems
Select Case .Key(ID)
Case "keyOpen"
' - Open a file...
Case "keySave"
' - Save a file...
Case "keyExit"
' - Exit the application...
End Select
End With
End Sub
Finally, you can find below a table containing all SmartMenuXP
properties, methods and events.
Class SmartMenuList
29.Oct.2001 - Build 1.8.0.2
On Windows XP the control was displaying two
shadows.
This bug is now fixed. The problem was due to a new Windows XP system
parameter that indicates whether a drop shadow effect will be active.
The parameter is called SPI_GETDROPSHADOW and can be retrieved by
using the API SystemParametersInfo(). The control now checks for the
OS platform and if its equal to "WinXP" the shadow is
hidden. In Windows XP the menu will rely on the OS for creating the shadow. Also, a new property is now available. Property
Shadow sets whether or not the menu will drop a shadow.
Thanks to Alan Osman for finding and reporting this bug.
01.Nov.2001 - Build 1.8.0.3
It was sometimes impossible to access the
drop-down menu when the menu bar was wrapped.
This bug is now fixed.
Thanks to Duplex
for finding and reporting this bug.
The drop-down menu window wasn't joining
the menu bar correctly when the window was opening bottom-up.
This bug is now fixed. The shadow effect is now complete and the
drop-down windows are always correctly joined to the menu bar.
Thanks to George for finding and reporting this bug.
Menu buttons weren't showing a shadow on
Windows XP.
This bug is now fixed. Now there's no difference at all between
Windows XP and all other Windows platforms when it comes to creating a
shadow. Also, the button now shows a shadow underneath when
the menu opens bottom-up.
Thanks to Thomas Molitor for finding and reporting this bug.
On Windows XP, the menu was creating a 2
pixel border rather than a 1 pixel border.
This bug is now fixed and there's no difference at all between Windows
XP and all other Windows platforms when it comes to drawing the menu.
Thanks again to Alan Osman for finding and reporting this bug.
Three more methods have been created.
You can use method PopupMenu() to popup a menu at any point of the
screen. There's only one requirement: in order to use this method the
SmartMenuXP control has to be invisible. Two other functions have also
been created. ScreenToClientX() and
ScreenToClientY() allows you to
convert from client points expressed in twips to screen points
expressed in pixels.
Private Sub Form_MouseDown( _
Button As Integer, _
Shift As Integer, _
X As Single, _
Y As Single)
If Button <> vbRightButton Then Exit Sub
With SmartMenuXP1
X = .ClientToScreenX(Me.hWnd, X)
Y = .ClientToScreenY(Me.hWnd, Y)
.PopupMenu
.MenuItems.Key2ID("kMenu"), X, Y, 0
End With
End Sub
Thanks to Duplex and Phil
Hirst for suggesting this improvement.
06.Nov.2001 - Build 1.8.0.4
A new property has been created.
There's a new property available. SelForeColor
returns or sets the foreground color used to display menu items when
these are highlighted.
Thanks to Phil Hirst for suggesting this improvement.
All properties of a menu item can be now
referenced by using its key.
The SmartMenuItems class has been modified in order to allow all its
properties to be referenced by either using the menu item ID or the
menu item key. The PopupMenu method has also been modified in order to
allow its parameter to be a key string.
It is now possible to use the following code:
With SmartMenuXP1.MenuItems
.Visible("keyFile") = False
.Caption("KeyView") = "&View"
End With
Thanks to Tom for suggesting this improvement.
The Visible property wasn't working on menu
items.
This bug is now fixed. Now menu items can be hidden by setting its Visible
property to False.
Thanks to Morpheus and Tim Mccurdy for finding and reporting this bug.
SmartMenuList class has a new method.
Now you can remove all menu items by using the new method Clear.
Thanks to Morpheus for suggesting this improvement.
06.Nov.2001 (II) - Build 1.8.0.5
A new property has been created.
There's a new property available. This property, hWnd, returns
the window handle of the usercontrol.
Thanks to Daniel Moreira for suggesting this improvement.
The method MenuItems.Clear wasn't
refreshing the menu bar.
This bug is now fixed. After calling the Clear
method the menu bar is refreshed. You should notice that the menu bar
becomes invisible if it contains no items.
There seems to be a bug on Windows 95 when
control keys are pressed.
There are still problems with Windows XP and
themes. Build 1.8.0.4 works fine under Windows XP with the 'Windows
Classic' theme, but it doesn't work with the 'Windows XP' theme.
|
|