Visual Basic Shell Programming
only for RuBoard - do not distribute or recompile |
14.3 The Project
The project for this chapter is fairly straightforward. We are going to start with the browser helper object that we built in Chapter 12 and give it a user interface. Not too creative, but that's not really the point. The point is to show you how you can take a simple component like a BHO and give it a user interface. Coming up with a creative way to use this knowledge will be your job. Now, how's that for passing the buck? You might be saying to yourself, "The component in Chapter 12 already has a user interface." True, but browser extensions (as opposed to BHOs) are for IE 5.0 and up. A BHO with a docking window is backwards compatible to IE 4.0.
14.3.1 Primary Component
Let's start with the IObjectWithSite::SetSite method of the component we built in Example 12.2. We'll add the code necessary for the docking window and then discuss the details. We only need to add a few new lines of code, which appear in boldface in Example 14.1.
Example 14.1. SetSite in Primary Object
Private Const IID_IShellBrowser = _ "{000214e2-0000-0000-c000-000000000046}" Private Const IID_IDockingWindowFrame = _ "{47d2657a-7b27-11d0-8ca9-00a0c92dbfe8}" Private szToolbar As String Private Sub Class_Initialize( ) szToolbar = "IEDockingWindow" End Sub Private Sub IObjectWithSite_SetSite(ByVal pSite As IUnknownVB) If ObjPtr(pSite) = 0 Then CopyMemory m_ie, 0&, 4 Exit Sub End If Set m_pUnkSite = pSite 'Save the site pointer for GetSite Dim pServiceProvider As IServiceProvider Set pServiceProvider = m_pUnkSite Dim clsidWebApp As GUID Dim clsidWebBrowser2 As GUID Dim clsidShellBrowser As GUID Dim clsidDockWndFrame As GUID 'Query service provider to get IWebBrowser2 (InternetExplorer) CLSIDFromString StrPtr(IID_IWebBrowserApp), clsidWebApp CLSIDFromString StrPtr(IID_IWebBrowser2), clsidWebBrowser2 CLSIDFromString StrPtr(IID_IShellBrowser), _ clsidShellBrowser CLSIDFromString StrPtr(IID_IDockingWindowFrame), _ clsidDockWndFrame Set m_ie = pServiceProvider.QueryService(_ VarPtr(clsidWebApp), VarPtr(clsidWebBrowser2)) Set m_pDockingWindowFrame = _ pServiceProvider.QueryService _ (VarPtr(clsidShellBrowser), _ VarPtr(clsidDockWndFrame)) Dim pDockWnd As clsDockWindow Set pDockWnd = New clsDockWindow pDockWnd.Initialize DW_BOTTOM Set pDockWnd.InternetExplorer = m_ie Set pDockWnd.InetSpeak = Me m_pDockingWindowFrame.AddToolbar pDockWnd, _ ByVal StrPtr(szToolbar), _ 0 Set pServiceProvider = Nothing End Sub
The first thing that is added are two new constants that contain the string representations of the CLSIDs for IShellBrowser and IDockingWindowFrame . We will use them in conjunction with the CLSIDFromString API to obtain their actual numerical equivalents. After we have the CLSIDs, we can then query the "Shell Browser Service" to obtain a pointer to IDockingWindowFrame .
|
Now that we have access to IDockingWindowFrame , we can create an instance of clsDockWindow. This is our class that will implement all the specifics of the docking window; it implements IObjectWithSite and IDockingWindow . But before we pass the object to AddToolbar , we'll need to do some initialization work first. This includes telling the docking window where it will be displayed and providing it with references to Internet Explorer and our InetSpeak object.
The first thing we do is call the Initialize method of clsDockWindow (not to be confused with Class_Initialize, which is, of course, called automatically). This is a method of the class itself, created for our own convenience. It takes a location parameter; either DW_TOP , DW_BOTTOM , DW_LEFT , or DW_RIGHT . The docking window will use this information in order to position itself in the proper location within Explorer's frame window. Basically, this function sets a Private member variable in clsDockWindow. The IDockingWindow::ResizeBorderDW will make use of this parameter when it requests border space for the window. Ignore the implementation details of Initialize for now; we'll look at the actual method in the next section.
Next, we pass the docking window a reference to Internet Explorer and to clsInetSpeak, our BHO. This gives the docking window access to all those Internet goodies as well as access back to the primary component, which it will need to execute commands.
Finally, we call IDockingWindowFrame::AddToolbar to display the docking window. At this point the shell calls IObjectWithSite::SetSite on the docking window, transferring control away from the primary component.
14.3.2 Docking Window Component
The first thing we need to do in order to create our docking window component is to add a new class module to the project called clsDockWindow and make sure that it is implementing the appropriate interfaces:
Implements IDockingWindow Implements IObjectWithSite
Before we start implementing any interfaces, let's get our class methods out of the way. If you remember from Example 14.1, we have three such methods : Initialize , InternetExplorer , and InetSpeak .
The first method, Initialize , which we've already discussed in the previous section, is just used to pass position and size information to the docking window. This information will be used when the docking window negotiates its border space within Explorer's client area. This function is simply:
Public Enum DW_LOCATION DW_TOP = 1 DW_LEFT = 2 DW_BOTTOM = 3 DW_RIGHT = 4 End Enum Private m_location As DW_LOCATION Private m_nSize As Integer Public Sub Initialize(ByVal Location As DW_LOCATION) m_location = Location m_nSize = size End Sub
The class is also going to need a subroutine that the docking window form can call in order to specify its dimensions. That function looks like so:
Private m_nWidth As Integer Private m_nHeight As Integer Friend Sub SetSize(ByVal x As Integer, ByVal y As Integer) m_nWidth = x m_nHeight = y End Sub
The next two functions are used to pass object references to the docking window. These two functions are self-explanatory and are defined like so:
Public Property Set InternetExplorer(ie As InternetExplorer) Set m_ie = ie Set frmDock.InternetExplorer = m_ie End Property Public Property Set InetSpeak(ins As clsInetSpeak) Set m_iis = ins Set frmDock.InetSpeak = m_iis End Property
As you can see, these properties are also mirrored within the docking window form. This will give the form access to these objects as well.
|
14.3.2.1 SetSite
Now, the easiest thing to do is get the IObjectWithSite implementation out of the way. The docking window's version of SetSite is much simpler than our other implementations . All we need to do is query the site pointer for the IDockingWindowSite interface. We'll cache it away in a private member variable. Later, the docking window will use it to negotiate its border space.
Another thing we need to be aware of is that SetSite will be called again when Explorer shuts down. This is no different than any other time we have implemented IObjectWithSite , so just consider this a reminder. It's easy enough to determine whether Explorer has been shut down. All we need to do is examine the cached site pointer. If its address is not zero, then we know SetSite has already been called. If that's the case, we'll need to exit. Example 14.2 contains the details.
Example 14.2. The Docking Window's SetSite Method
Private m_pDockSite As IDockingWindowSite Private Sub IObjectWithSite_SetSite( _ ByVal pSite As VBShellLib.IUnknownVB) If (ObjPtr(m_pDockSite)) Then Set m_pDockSite = Nothing Exit Sub End If If ObjPtr(pSite) Then Set m_pDockSite = pSite End If End Sub
14.3.2.2 GetSite
There is nothing new to GetSite . It is implemented like we have always done before:
Private Sub IObjectWithSite_GetSite( _ ByVal priid As VBShellLib.REFIID, _ ppvObj As VBShellLib.VOID) Dim pUnknown As IUnknownVB Set pUnknown = m_pDockSite pUnknown.QueryInterface priid, ppvObj End Sub
14.3.2.3 The docking window form
Now before we continue, let's actually design the docking window. We need to add a new form to the project called frmDock . As with all docking windows you write, it needs to be borderless. Also, you want to size the form approximately to the size you want it be when it is displayed. The band has a height of 600. 600/15 = 40, which is the height in pixels that we want for display.
After adding the form, you can add a command button called cmdInetSpeak to the form. This button will carry out our command. The band is shown in Figure 14.2.
Figure 14.2. Docking window design
We need to add a few properties to the docking window that will allow the component to pass references into the form for IWebBrowser2 , clsInetSpeak , and IDockingWindow . These properties look like this:
Private m_dw As clsDockWindow Private WithEvents m_ie As InternetExplorer Private m_iis As clsInetSpeak Friend Property Set InternetExplorer(ie As InternetExplorer) Set m_ie = ie End Property Friend Property Set InetSpeak(iis As clsInetSpeak) Set m_iis = iis End Property Friend Property Set DockingWindow(dw As clsDockWindow) Set m_dw = dw End Property
Now that the form has a valid reference to clsInetSpeak, we can add code to the command button that actually fires the command. This is simply:
Private Sub cmdInetSpeak_Click( ) m_iis.Translate End Sub
Also, this is an Internet-specific component. It doesn't really apply to file objects. So we need a way to disable the browser when we are not browsing the Web. We can capture the BeforeNavigate2 event from our IE reference to accomplish the task. The code looks like this:
Private Sub m_ie_BeforeNavigate2(ByVal pDisp As Object, _ URL As Variant, _ Flags As Variant, _ TargetFrameName As Variant, _ PostData As Variant, _ Headers As Variant, _ Cancel As Boolean) 'Need to make sure command does not get 'executed against non-HTML If InStr(URL, "http") Then cmdInetSpeak.Enabled = True Else cmdInetSpeak.Enabled = False End If End Sub
Last but not least, we need to add some code to the Form_Load and Form_Unload events. When the form is loaded, its size information becomes valid. We'll use this opportunity to inform the docking window class about the form's size:
Private Sub Form_Load( ) m_dw.SetSize Me.Width / Screen.TwipsPerPixelX, _ Me.Height / Screen.TwipsPerPixelY End Sub
The Form_Unload event will merely be used to clean up after ourselves ; we'll free up our object references here:
Private Sub Form_Unload(Cancel As Integer) Set m_dw = Nothing Set m_ie = Nothing Set m_iis = Nothing End Sub
Okay, we have the docking window designed, so let's get back to clsDockWindow and our IDockingWindow implementation. The big action happens in IDockingWindow::ResizeBorderDW and IDockingWindow::ShowDW , so we'll save those methods for last. For now, we'll get the easier methods out of the way.
14.3.2.4 CloseDW
Some things never change. Just as was the case with band objects, this method's only responsibility is to unload the docking window:
Private Sub IDockingWindow_CloseDW(ByVal dwReserved As Long) Unload frmDock End Sub
14.3.2.5 GetWindow
This method also performs the same duty that it did when we were discussing band objects. All it needs to do is return with a window handle for our docking window:
Private Function IDockingWindow_GetWindow( ) As Long IDockingWindow_GetWindow = frmDock.hwnd End Function
14.3.2.6 ResizeBorderDW
Okay, now we're ready to get down to business. We need to implement ResizeBorderDW , which we have not done before. All we had to do for bands was capture the Resize event and perform any resizing that might be necessary. The container was able to handle itself as far as size and position. This is not the case here. Whenever Explorer is resized, the docking window has to renegotiate its border space and resize itself accordingly . Let's take a peek at the implementation for this method (see Example 14.3).
Example 14.3. ResizeBorderDW Implementation
Private Sub IDockingWindow_ResizeBorderDW( _ ByVal prcBorder As Long, _ ByVal punkToolbarSite As VBShellLib.IUnknownVB, _ ByVal fReserved As Boolean) If NegotiateBorderSpace Then IDockingWindow_ShowDW True End If End Sub
Well, that looks innocuous , doesn't it? That's because all the work is being done by a private helper function called NegotiateBorderSpace . If the border space is obtained successfully, the function then calls ShowDW to display the docking window. We'll talk about ShowDW soon, but now, let's get into NegotiateBorderSpace , which is shown in Example 14.4.
Example 14.4. NegotiateBorderSpace
Private m_rcDisplay As RECT Private Function NegotiateBorderSpace( ) As Boolean NegotiateBorderSpace = False Dim pDockingWindow As IDockingWindow Dim bw As RECT 'BORDERWIDTHS Dim rcBorder As RECT Dim rcTemp As RECT Dim nSize As Integer Select Case m_location Case DW_TOP nSize = m_nHeight bw.Top = nSize Case DW_LEFT nSize = m_nWidth bw.Left = nSize Case DW_BOTTOM nSize = m_nHeight bw.bottom = nSize Case DW_RIGHT nSize = m_nWidth bw.Right = nSize End Select Set pDockingWindow = Me m_pDockSite.RequestBorderSpaceDW pDockingWindow, _ ByVal VarPtr(bw) m_pDockSite.SetBorderSpaceDW pDockingWindow, _ ByVal VarPtr(bw) m_pDockSite.GetBorderDW pDockingWindow, ByVal VarPtr(rcBorder) Select Case m_location Case DW_RIGHT m_rcDisplay.Left = rcBorder.Right - m_nSize m_rcDisplay.Top = rcBorder.Top m_rcDisplay.Right = rcBorder.Right 'Accomodate Status Bar m_rcDisplay.bottom = rcBorder.bottom - 82 Case DW_LEFT m_rcDisplay.Left = rcBorder.Left m_rcDisplay.Top = rcBorder.Top m_rcDisplay.Right = rcBorder.Left + m_nSize 'Accomodate Status Bar m_rcDisplay.bottom = rcBorder.bottom - 82 Case DW_TOP m_rcDisplay.Left = rcBorder.Left m_rcDisplay.Top = rcBorder.Top m_rcDisplay.Right = rcBorder.Right - rcBorder.Left m_rcDisplay.bottom = m_nSize Case DW_BOTTOM m_rcDisplay.Left = rcBorder.Left m_rcDisplay.Top = rcBorder.bottom - m_nSize m_rcDisplay.Right = rcBorder.Right - rcBorder.Left ' -1 leaves beveled edge on Status Bar m_rcDisplay.bottom = m_nSize - 1 End Select NegotiateBorderSpace = True End Function
Let's step through this nice and easy; it's actually much easier than it looks. The first thing that happens is that a RECT structure, bw , is filled out based on the parameters we passed to the Initialize function of the class. In terms of the chapter example, we asked for a docking window on the bottom of Explorer's frame window. Earlier, when our window was loaded, its size (converted from twips to pixels) was passed back to clsDockWindow . Since the form's height is 600, and 600 divided by Screen.TwipsPerPixelY (15) equals 40, we will end up with a RECT structure whose bottom member is set to 40. All the other values of the RECT will contain 0.
Next, we get a reference to our own IDockingWindow interface pointer. This is passed to IDockingWindowSite::RequestBorderSpaceDW along with a pointer to bw . Translation: allocate border space that is 40 pixels tall at the bottom of your client area. This object is going to need it.
Next, we finalize the request by calling SetBorderSpaceDW . This actually allocates and reserves the space for our docking window.
Now, things get a little confusing. We call GetBorderDW , which returns to us another RECT structure, rcBorder , that supposedly contains the dimensions of the space that has been allocated for our docking window. In practice, the method seems to return the dimensions of the entire client area. So the rest of this function is dedicated to determining the actual location of the docking window based on the size of the client area and the location and size that we have asked for. When all is said and done, we have a private member variable, m_rcDisplay , that contains the actual coordinates of the window.
14.3.2.7 ShowDW
The last thing on our list for clsDockWindow is ShowDW . As you might remember from Chapter 13, Thi method is called when the window is about to be either displayed or destroyed . Example 14.5 contains the code listing.
Example 14.5. ShowDW
Private m_hwnd As hwnd Private Sub IDockingWindow_ShowDW(ByVal fShow As Boolean) Dim pDockingWindow As IDockingWindow Dim rcBorderWidths As RECT Dim dwStyle As DWORD If Not m_hwnd Then m_hwnd = frmDock.hwnd dwStyle = GetWindowLong(m_hwnd, GWL_STYLE) dwStyle = dwStyle Or WS_CHILD Or WS_CLIPSIBLINGS SetWindowLong m_hwnd, GWL_STYLE, dwStyle SetParent frmDock.hwnd, m_pDockSite.GetWindow End If If (fShow) Then ShowWindow frmDock.hwnd, SW_SHOW MoveWindow frmDock.hwnd, m_rcDisplay.Left, _ m_rcDisplay.Top, m_rcDisplay.Right, _ m_rcDisplay.bottom, True Else ShowWindow frmDock.hwnd, SW_HIDE Set pDockingWindow = Me 'Release border space - rcBorderWidths is empty m_pDockSite.SetBorderSpaceDW pDockingWindow, _ ByVal VarPtr(rcBorderWidths) End If End Sub
The docking window, like the band object, also needs to have some style bits changed and have its parent window set to the container window. Remember, we do this so that when the docking window gets the focus, Explorer will not lose the focus. The idea here is that the docking window is fully integrated, right? But since ShowDW will be called many times throughout the docking window's life, we need to make sure this does not happen more than once.
If fShow is True , we simply show the window and move its position to coordinates specified by the RECT m_rcDisplay (the coordinates were determined in NegotiateBorderSpace ).
If fShow is False , we hide the window and release the border space. To release the border space, IDockingWindowSite::SetBorderSpaceDW is called with the address of an empty RECT .
That's it; we have done everything we need in order to implement a docking window.
only for RuBoard - do not distribute or recompile |