|
Author | Ted Faison |
Title | Component-Based Development with Visual C# |
Publisher | Wiley |
Published | April 2002 |
ISBN | 0-7645-4914-6 |
Price | US 49.99 |
Pages | 1008 |
|
In This Chapter
- Creating a simple Web browser
- Using ActiveX components in managed code
- Exposing COM interfaces from managed code
- Customizing the WebBrowser component
Chapter 8: Creating Front Ends with the WebBrowser Component
A billion here, a billion there — sooner or later it adds up to real money.
— Former U.S. Senator Everett Dirksen on public finance
Senator Dirksen’s concept of real money may have been slightly different from yours or mine, but one thing is certain: If you fail to take advantage of existing components when building the front end of your application, you’ll find your development costs adding up to real money.
The Microsoft WebBrowser Component
One of the most powerful reusable UI components I know of is the WebBrowser
ActiveX component used by Internet Explorer. Microsoft designed it to be extremely flexible in terms of the kinds of content it can display. Obviously, it can display HTML documents, but it may come as a surprise to some of you that it can also display other common file types such as Word, Excel, PowerPoint, TXT, RTF, GIF, JPEG, XML, PDF, and others. WebBrowser
achieves this incredible flexibility by using an embedded ActiveX component to render its data. In the case of HTML documents, the component is MsHtml
. For more complex documents like Word or Excel, WebBrowser
acts as an Active Document host and embeds Word or Excel in the client area to handle the rendering.
Since WebBrowser
was designed specifically to be hosted by a container application, it has a lot of features you can control. Microsoft uses the component in several different applications, and customizes its look and feel to blend in with the rest of the host application. Examples of hosts include Windows Explorer, Outlook, and even the VS .NET Start Page. In this chapter, I’ll show you how to create a Windows application that hosts the WebBrowser
component. Later, since customization is very important to many of you, I’ll create a second Windows application showing how to access the various customization features that WebBrowser
makes available.
I’ll call the first browser application MyWebBrowser and give it a tool bar, to show how to control some of the common functions of WebBrowser
. As I said earlier, WebBrowser
can handle more than just HTML files. Figures 8-1 through 8-6 show MyWebBrowser displaying different types of files.
Figure 8-1: Using MyWebBrowser to display an HTML page
Figure 8-2: Using MyWebBrowser to display a Word document
Figure 8-3: Using MyWebBrowser to display an Excel spreadsheet
Figure 8-4: Using MyWebBrowser to display a Powerpoint document
Figure 8-5: Using MyWebBrowser to display an XML document
Figure 8-6: Using MyWebBrowser to display a PDF document
These screen shots hopefully give you some sense of how flexible WebBrowser
really is. WebBrowser
and its Active Document subcomponents do all the work behind the scenes. If you can find a more powerful reusable UI component than WebBrowser
, let me know.
Not only does WebBrowser
display a zillion file types, it also lets you control the way documents are presented. In the case of HTML documents, there is a whole slew of behaviors that can be changed, such as:
I’ll show you how to make these and other kinds of customizations to MsHtml
, but first, I’ll get you familiar with WebBrowser
by showing you a simple application called MyWebBrowser that can be used as a simple mini-browser without any customizations. After describing
MyWebBrowser, I’ll show you a customized browser in an application called MyCustomWebBrowser. This application will use COM interfaces and callbacks to control the way MsHtml
works.
Designing MyWebBrowser
Since almost all the functionality you need is already built into WebBrowser, the design of an application based on Windows Forms that embeds the component is fairly trivial. Figure 8-7 shows the salient parts of the class diagram for MyWebBrowser.
Figure 8-7: The class diagram for MyWebBrowser
Like I said, in this first example, I won’t add any special customizations to MsHtml
or WebBrowser
, because doing so requires utilizing COM interoperability features. After showing a simple example, I’ll show you how to use COM interfaces to customize many MsHtml
features.
Developing MyWebBrowser
Enough said about the glories of WebBrowser
and MsHtml
. They’re built-in, they’re good, they’re wonderful. Great. Let’s put them to work in a real program. MyWebBrowser is a simple Windows application that has just a single form, called MainForm, which hosts the WebBrowser component and demonstrates not only how to use it, but more generally how to access ActiveX components from managed code.
I created MyWebBrowser with the New Project Wizard, choosing Windows Application as the project type. I renamed the main form class from Form1 to MainForm, and set the project name to MyWebBrowser. The next step was to add the WebBrowser
ActiveX component to MainForm, a task that deserves a separate explanation.
Importing the WebBrowser ActiveX Component
There is no WebBrowser
component in the Toolbox when you install VS .NET, so you have to import a .NET version of the WebBrowser
into your project. As with just about everything in life, the import process can be done the easy way or the hard way.
The easy way
The easy way is with the Customize Toolbox dialog box. First select the Windows Forms page on the Toolbox and then right-click on the Toolbox and choose the Customize Toolbox command on the pop-up menu. Select the COM Components tab, scroll down to the Microsoft Web Browser item, and check its checkbox, as shown in Figure 8-8.
Figure 8-8: Importing the ShDocVw.WebBrowser control into the VS .NET Toolbox
The WebBrowser
will show up on the Windows Forms tab of the Toolbox with the name Explorer, as shown in Figure 8-9.
Figure 8-9: The WebBrowser component after installing it on the Toolbox
Now drop an instance of WebBrowser
on MainForm. When the component is dropped, VS .NET performs the following tasks.
It launches an import process that creates the two files AxInterop.SHDocVw.dll and Interop.SHDocVw.dll.
It puts the two files in your project’s bin\Debug
or bin\Release
directory (depending on whether your project configuration is set to Debug or Release).
It adds the two files to the References node of your project in the Solution Explorer.
It adds an instance of AxSHDocVw.AxWebBrowser to your form.
A nice piece of work, saving you precious time and money — perhaps not billions, but hey, no one said life is fair.
In case you’re wondering, much of the process was performed under the covers by a command-line utility called aximp, described later. At this point the Solution Explorer looks like Figure 8-10.
Figure 8-10: The Solution Explorer, showing the newly imported files
That was the easy way to import the WebBrowser
component. The hard way is for those of you that like typing (and I know there are a lot of you out there).
The hard way
I always get a kick out of watching people open DOS boxes and furiously type in long commands that could be replaced with a couple of mouse clicks. Force of habit is a powerful thing. But using command-line utilities for the import process is not always a bad thing, because they create the necessary import files without adding anything to the Toolbox. Although you may want to have a useful component like WebBrowser
in the Toolbox, you don’t necessarily want every ActiveX component you’ll ever use cluttering up the precious Toolbox space.
In any case, there are two command-line utilities available for converting COM types into .NET-compatible types that can be referenced in a VS .NET project. Which one to use depends on how you want to use the imported components.
Using TlbImp
The lowest level command-line utility for ActiveX importing is TlbImp
. You’ll want to use this utility when importing ActiveX components that won’t be used in a Windows Form. TlbImp
reads a file containing COM Type Library information — which can be a
.tlb, .dll, .odl, or other file type — and produces a DLL containing .NET-compatible metadata. The DLL must then be added to the References node of your project.
For example, to use shdocvw.dll (containing the WebBrowser
component, for those of you that jumped into this section without reading the previous ones) in the MyWebBrowser project, you would open a Command-Prompt box, go to the folder C:\Program Files\Microsoft.NET\FrameworkSDK\Bin
, and then type the command:
tlbimp c:\winnt\system32\shdocvw.dll /out:C:\MyWebBrowser\bin\Debug\Interop.shdocvw.dll
|
You can inspect the .NET metadata in the DLL generated by TlbImp
with a standard .NET tool like ildasm
. Open a Command-Line box and go to the folder containing the newly generated Shdocvw.dll file and then type the command:
"C:\Program Files\Microsoft.NET\FrameworkSDK\Bin\ildasm" shdocvw.dll
|
Figure 8-11 shows some of the contents of the metadata file displayed by ildasm
.
Figure 8-11: Inspecting the metadata in the DLL produced by running TlbImp on c:\winnt\system32\shdocvw.dll
The metadata makes it possible for your code to interact with the unmanaged code of COM components using exactly the same syntax you would use with native C# components.
Using AxImp
If you plan to use an ActiveX component in a Windows Form, as I did with WebBrowser
, you’ll need to use the AxImp
command-line utility instead of TlbImp
. The reason is this: To use an ActiveX component in a Windows Form, a wrapper class must also be generated. All components dropped in Windows Forms are required to be derived from the common base class System.Windows.Forms.Control
. The utility AxImp
creates the wrapper for you.
To create the wrapper for WebBrowser
, open a Command-Line box, go to the folder where the executable code of your project will go. For the project MyWebBrowser, the folder will be MyWebBrowser\bin\debug
or MyWebBrowser\bin\release
. Type the following command:
"C:\Program Files\Microsoft.NET\FrameworkSDK\Bin\aximp" c:\winnt\system32\shdocvw.dll
|
The utility will generate two files, called SHDocVw.dll and AxSHDocVw.dll. The first file contains the .NET metadata that describes the COM types contained in c:\winnt\system32\shdocvw.dll
. The file is identical to the one produced by TlbImp
in the previous section (because AxImp
internally calls TlbImp to generate it). The second file is a .NET assembly containing the wrapper classes that allow you to use the ActiveX component in standard Windows Forms. Figure 8-12 shows the contents of AxSHDocVw.dll, as viewed with ildasm
:
Figure 8-12: The wrapper class created for SHDocVw
Basically, AxImp
creates a new class derived from System.Windows.Forms.AxHost
, which is derived from System.Windows.Forms.Control
. This new class acts as a .NET wrapper for the ActiveX component.
Runtime Callable Wrappers (RCW)
When you instantiate a COM type in your C# code, using code like this:
AxSHDocVw.AxWebBrowser axWebBrowser1 = new AxSHDocVw.AxWebBrowser();
|
there is more going on than meets the eye. What actually happens is this: The compiler looks at the metadata describing the class AxWebBrowser
, in the AxSHDocVw
assembly generated by TlbImp
or AxImp
. Using this metadata, it creates something called a Runtime Callable Wrapper (RCW), which is a proxy component that on one side is callable from your managed code and on the other side deals with the unmanaged COM code of the WebBrowser
ActiveX component. The RCW acts as an invisible bridge between your code and the ActiveX code, as shown in Figure 8-13.
Figure 8-13: The Runtime Callable Wrapper as a proxy for COM components
The RCW manages all those pesky COM details that you don’t want to deal with, such as reference counting, deleting the component when it is no longer used, marshalling parameters in method calls, and so on. If you create multiple instances of the same COM component in your code, all the instances will share a single
RCW. Keep in mind that all this RCW business is generally completely transparent to you. It’s there to help you, making it as easy to access an ActiveX component as any other managed component.
Adding a toolbar
Before I completely lose track of where I am, let me finish describing the code for
MyWebBrowser. First I’ll discuss the toolbar buttons. Most UIs containing WebBrowser
support some means for navigating, whether it’s with toolbar buttons, menu commands, or some other element.
I added a toolbar to MyWebBrowser by dropping a Toolbox component of type ToolBar
on the main form. In the ToolBar’s properties, I clicked on the Collections
field and added seven buttons with the following functions:
Back
Forward
Stop
Refresh
Home
Search
Print
To make the buttons look like those used by Internet Explorer, I used a screen capture utility to get the IE button images, and then saved them as bitmap files in the folder containing the source code for
MyWebBrowser. I set them all to have a green background. I added an ImageList
component to the form and added all the bitmap images to it. I set the ImageList TransparentColor
property to green, so the green areas of the bitmaps would be transparent and assume the color of the button faces. Then I set the ToolBar
’s ImageList
property to reference the ImageList
component. For each toolbar button, I set the following properties: ToolTipText
, Text
, and ImageIndex
. To the right of the toolbar buttons, I dropped a TextBox
control. To make the TextBox
always stretch to the right side of the form, I set its Anchor
property to (Top, Left, Right). Figure 8-14 shows the finished toolbar.
Figure 8-14: The finished toolbar
Notice the flat look of the toolbar, with the buttons showing no borders. This look was achieved by setting the toolbar’s Appearance
property to Flat
. Next, I added some event handling code for the toolbar buttons, as shown in Listing 8-1.
Listing 8-1:: The Toolbar Event Handler
protected void toolBar1_ButtonClick(object sender,
System.WinForms.ToolBarButtonClickEventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
try
{
if (e.button == toolBarButtonBack)
axWebBrowser1.GoBack();
else if (e.button == toolBarButtonForward)
axWebBrowser1.GoForward();
else if (e.button == toolBarButtonStop)
{
axWebBrowser1.Stop();
toolBarButtonStop.Enabled = false;
}
else if (e.button == toolBarButtonSearch)
axWebBrowser1.GoSearch();
else if (e.button == toolBarButtonPrint)
PrintPage();
else if (e.button == toolBarButtonRefresh)
{
object REFRESH_COMPLETELY = 3;
axWebBrowser1.Refresh2(ref REFRESH_COMPLETELY);
}
else if (e.button == toolBarButtonHome)
axWebBrowser1.GoHome();
}
finally
{
Cursor.Current = Cursors.Default;
}
}
The method WebBrowser.GoSearch()
navigates to the Search site, which can be set up in Internet Explorer by clicking the Search toolbar button and selecting Customize in the Search pane. By default, WebBrowser
uses the search engine at http:
.
The method WebBrowser.GoHome()
navigates to the Home site set up in Internet Explorer with the Tools @@> Internet Options dialog box, on the General tab.
Adding support for printing
All of the buttons except Print are handled by calling a method in the embedded WebBrowser
component. As noted earlier, there is no Print()
method, so printing is handled differently, with the code shown in Listing 8-2.
Listing 8-2:: Printing the HTML Page Displayed in the WebBrowser
private bool IsPrinterEnabled()
{
int response =
(int) axWebBrowser1.QueryStatusWB(SHDocVw.OLECMDID.OLECMDID_PRINT);
return (response & (int) SHDocVw.OLECMDF.OLECMDF_ENABLED) != 0 ?
true : false;
}
private void PrintPage()
{
object o = "";
SHDocVw.OLECMDID Print = SHDocVw.OLECMDID.OLECMDID_PRINT;
SHDocVw.OLECMDEXECOPT DontPromptUser =
SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
if (!IsPrinterEnabled() ) return;
axWebBrowser1.ExecWB(Print, DontPromptUser, ref o, ref o);
}
The last two lines in the listing show two ways to print: The first way prints silently, the second way prints after prompting the user for printer settings. I created two constants called DontPromptUser
and PromptUser
, which are derived from ShDocVw
enumerated values. When you call WebBrowser.ExecWB()
, you simply pass the constant indicating whether you want the method to display a Print dialog box or not.
Adding navigation support
To navigate to a Web site, you call the Navigate()
or Navigate2()
method of the WebBrowser
. The first method handles navigating to ordinary URLs, including local files. The second method extends the first by supporting navigation to items in the Windows Desktop and My Computer folders. To protect the rest of the application from errors caused by invalid URLs or network problems, I wrapped the call to WebBrowser.Navigate()
in a try
block in a method called GotoURL()
, shown in Listing 8-3.
Listing 8-3:: Setting the URL of the Document to Load
public void GotoURL(String theURL)
{
try
{
Cursor.Current = Cursors.WaitCursor;
Object o = null;
axWebBrowser1.Navigate(theURL, ref o, ref o, ref o, ref o);
}
finally {
Cursor.Current = Cursors.Default;
}
}
To allow the user to type a URL in the TextBox
, I added an event handler for the TextBox
that calls GotoURL()
as shown in Listing 8-4.
Listing 8-4:: The TextBox Event Handler
protected void textBoxAddress_KeyDown (object sender,
System.WinForms.KeyEventArgs e)
{
if (e.KeyCode == Keys.Return)
GotoURL(textBoxAddress.Text);
}
To display an hourglass cursor while a page is being loaded, I set the cursor in the WebBrowser’s BeforeNavigate2()
handler as shown in Listing 8-5.
Listing 8-5:: Setting the Cursor to an Hourglass during Navigation
protected void axWebBrowser1_BeforeNavigate2 (object sender,
AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2Event e)
{
toolBarButtonStop.Enabled = true;
Cursor.Current = Cursors.WaitCursor;
}
Once a page is loaded, I need make sure the cursor is eventually restored to an arrow pointer. Navigation commands can end in one of two ways: If the page can’t be loaded, the NavigateError
handler is called. If the page was loaded, the NavigateComplete
handler is called. I created a simple NavigateError
handler, as shown in Listing 8-6.
Listing 8-6:: The NavigateError Handler that Restores the Mouse Cursor
private void axWebBrowser1_NavigateError(object sender,
AxSHDocVw.DWebBrowserEvents2_NavigateErrorEvent e)
{
Cursor.Current = Cursors.Default;
toolBarButtonStop.Enabled = false;
toolBarButtonHome.Enabled = true;
toolBarButtonSearch.Enabled = true;
toolBarButtonRefresh.Enabled = true;
}
If navigation to a site is successful, the NavigateComplete2
handler is called, if available. I created a NavigateComplete2
handler, as shown in Listing 8-7.
Listing 8-7:: The NavigateComplete2 Handler
private void axWebBrowser1_NavigateComplete2(object sender,
AxSHDocVw.DWebBrowserEvents2_NavigateComplete2Event e)
{
Cursor.Current = Cursors.Default;
toolBarButtonStop.Enabled = false;
toolBarButtonHome.Enabled = true;
toolBarButtonSearch.Enabled = true;
toolBarButtonRefresh.Enabled = true;
String s = e.uRL.ToString();
textBoxAddress.Text = s;
int i = urlsVisited.IndexOf(s);
if (i >= 0)
currentUrlIndex = i;
else
currentUrlIndex = urlsVisited.Add(s);
toolBarButtonBack.Enabled = (currentUrlIndex == 0) ? false : true;
toolBarButtonForward.Enabled =
(currentUrlIndex >= urlsVisited.Count-1) ? false : true;
toolBarButtonPrint.Enabled = IsPrinterEnabled();
}
In the NavigateComplete2
handler, I displayed the current URL in the TextBox and updated the list of visited URLs. Each time the user goes to a new URL, I store it in an ArrayList. I used the list to support enabling and disabling the Back and Forward buttons. When the user clicks the Back button, I retrieve the previous URL in the list and navigate to it. When the user navigates all the way back to the first URL in the list, I disable the Back button. Similarly, when the user navigates forward to the end of the list, I disable the Forward button.
Disabling the Back and Forward buttons is more than just a cosmetic exercise: If you call the WebBrowser’s
GoBack()
or GoForward()
method when there is no previous or next URL to navigate to, the component will raise an exception. If you don’t catch the exception, an error message will be displayed.
The last feature to add is one that will make MyWebBrowser go to the Home page when it is run. All you need to do is call the method axWebBrowser1.GoHome()
in the constructor for MainForm
.
The complete code
The next section will deal with ways to customize the WebBrowser
component. Before I move to this new topic, I’ve included the full code for MyWebBrowser in Listing 8-8.
Listing 8-8:: The Code for MyWebBrowser
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
namespace MyWebBrowser
{
public class MainForm : System.Windows.Forms.Form
{
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.ToolBar toolBar1;
private System.Windows.Forms.ImageList imageList1;
private System.Windows.Forms.ToolBarButton toolBarButtonBack;
private System.Windows.Forms.ToolBarButton toolBarButtonForward;
private System.Windows.Forms.ToolBarButton toolBarButtonStop;
private System.Windows.Forms.ToolBarButton toolBarButtonRefresh;
private System.Windows.Forms.ToolBarButton toolBarButtonHome;
private System.Windows.Forms.ToolBarButton toolBarButtonSearch;
private System.Windows.Forms.ToolBarButton toolBarButtonPrint;
private AxSHDocVw.AxWebBrowser axWebBrowser1;
private System.Windows.Forms.TextBox textBoxAddress;
private System.ComponentModel.IContainer components;
ArrayList urlsVisited = new ArrayList();
int currentUrlIndex = -1;
public MainForm()
{
InitializeComponent();
toolBarButtonBack.Enabled = false;
toolBarButtonForward.Enabled = false;
toolBarButtonStop.Enabled = false;
toolBarButtonRefresh.Enabled = false;
toolBarButtonHome.Enabled = false;
toolBarButtonSearch.Enabled = false;
toolBarButtonPrint.Enabled = false;
axWebBrowser1.GoHome();
}
protected override void Dispose( bool disposing )
{
}
private void InitializeComponent()
{
}
[STAThread]
static void Main()
{
Application.Run(new MainForm());
}
private void toolBar1_ButtonClick(object sender,
System.Windows.Forms.ToolBarButtonClickEventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
try
{
if (e.Button == toolBarButtonBack)
axWebBrowser1.GoBack();
else if (e.Button == toolBarButtonForward)
axWebBrowser1.GoForward();
else if (e.Button == toolBarButtonStop)
{
axWebBrowser1.Stop();
toolBarButtonStop.Enabled = false;
}
else if (e.Button == toolBarButtonSearch)
axWebBrowser1.GoSearch();
else if (e.Button == toolBarButtonPrint)
PrintPage();
else if (e.Button == toolBarButtonRefresh)
{
object REFRESH_COMPLETELY = 3;
axWebBrowser1.Refresh2(ref REFRESH_COMPLETELY);
}
else if (e.Button == toolBarButtonHome)
axWebBrowser1.GoHome();
}
finally
{
Cursor.Current = Cursors.Default;
}
}
private bool IsPrinterEnabled()
{
int response =
(int) axWebBrowser1.QueryStatusWB(SHDocVw.OLECMDID.OLECMDID_PRINT);
return (response & (int) SHDocVw.OLECMDF.OLECMDF_ENABLED) != 0 ?
true : false;
}
private void PrintPage()
{
object o = "";
SHDocVw.OLECMDID Print = SHDocVw.OLECMDID.OLECMDID_PRINT;
SHDocVw.OLECMDEXECOPT DontPromptUser =
SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
if (!IsPrinterEnabled() ) return;
axWebBrowser1.ExecWB(Print, DontPromptUser, ref o, ref o);
}
public void GotoURL(String theURL)
{
try
{
Cursor.Current = Cursors.WaitCursor;
Object o = null;
axWebBrowser1.Navigate(theURL, ref o, ref o, ref o, ref o);
}
finally
{
Cursor.Current = Cursors.Default;
}
}
private void textBoxAddress_KeyDown(object sender,
System.Windows.Forms.KeyEventArgs e)
{
if (e.KeyCode == Keys.Return)
GotoURL(textBoxAddress.Text);
}
private void axWebBrowser1_BeforeNavigate2(object sender,
AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2Event e)
{
toolBarButtonStop.Enabled = true;
Cursor.Current = Cursors.WaitCursor;
}
private void axWebBrowser1_NavigateComplete2(object sender,
AxSHDocVw.DWebBrowserEvents2_NavigateComplete2Event e)
{
Cursor.Current = Cursors.Default;
toolBarButtonStop.Enabled = false;
toolBarButtonHome.Enabled = true;
toolBarButtonSearch.Enabled = true;
toolBarButtonRefresh.Enabled = true;
String s = e.uRL.ToString();
textBoxAddress.Text = s;
int i = urlsVisited.IndexOf(s);
if (i >= 0)
currentUrlIndex = i;
else
currentUrlIndex = urlsVisited.Add(s);
toolBarButtonBack.Enabled = (currentUrlIndex == 0) ? false : true;
toolBarButtonForward.Enabled =
(currentUrlIndex >= urlsVisited.Count-1) ? false : true;
toolBarButtonPrint.Enabled = IsPrinterEnabled();
}
private void axWebBrowser1_NavigateError(object sender,
AxSHDocVw.DWebBrowserEvents2_NavigateErrorEvent e)
{
Cursor.Current = Cursors.Default;
toolBarButtonStop.Enabled = false;
toolBarButtonHome.Enabled = true;
toolBarButtonSearch.Enabled = true;
toolBarButtonRefresh.Enabled = true;
}
}
}
In the next section, I’ll switch gears and get into advanced topics with COM Interop programming. If you’re not experienced in COM, you may want to skip the rest of the chapter entirely.
Creating a Customized Web Browser
MyWebBrowser has all the basic browser features, such as navigation, printing, and so on. It also has features you may want to change, such as how accelerator keys are handled or what commands are available on the context menu. To make these kinds of changes, things get a bit more complicated with WebBrowser
, and you are forced to get your hands dirty with some COM interoperability programming. In this and the following sections, I’ll create another Windows application called MyCustomWebBrowser that demonstrates how to develop a fully customized WebBrowser
. To start the project, I just copied the entire MyWebBrowser solution into a new folder and renamed it to MyCustomWebBrowser.
Customizing the WebBrowser
component is more complicated than one might think. The problem is this: Most of the customizable features of MsHtml
depend on COM callbacks that must be handled by the host window — MainForm
in this case. Callbacks are methods exposed through COM interfaces, so you have to make the necessary interfaces available to MsHtml
. It would have been nice if MsHtml
had exposed a list of properties for all its customizable features, so you could change a feature with a simple line of code like this:
AxWebBrowser.Use3Dborders = false;
|
That would have been way too simple! Besides, each time the parent WebBrowser
loads a new HTML page, it creates a new instance of MsHtml
, so even if you could set the properties using the previous code, the new MsHtml
component wouldn’t be affected. You might be thinking, “Why didn’t Microsoft store the customizable properties of MsHtml in the parent WebBrowser component? This way you could set them once, and have WebBrowser automatically reapply them each time it created a new MsHtml component”. For better or for worse, WebBrowser
doesn’t want anything to do with presentation of content. It was designed to act as a host for rendering components. It handles other aspects of Web browsing, like navigation. WebBrowser
is an Active Document host, and it delegates all details of presentation to the hosted Active Document rendering component (MsHtml
in this case).
The moral of the story is this: MainForm needs to implement a number of COM interfaces to support MsHtml
customizing. Figure 8-15 shows the class diagram for MainForm.
Figure 8-15: The class diagram for a fully customized WebBrowser host Windows Form
Let’s take a look at what the COM interfaces are used for. The first step in any type of WebBrowser
customization is to establish a component as the controlling host. By default, the WebBrowser
has no controlling host. To set up MainForm
as the new controlling host, you need to call WebBrowser
through its OleClient
interface as shown in Listing 8-9.
Listing 8-9:: Setting Up MainForm as the Controlling Host of WebBrowser
object obj = axWebBrowser1.GetOcx();
IOleObject oc = obj as IOleObject;
oc.SetClientSite(this);
These three deceptively simple lines of code are critical. The method GetOcx()
retrieves the IUnknown
interface of the native COM object wrapped by axWebBrowser1
. The second line implicitly issues a QueryInterface
, seeking an IOleObject
interface from the WebBrowser
COM object. You can use the as
operator like this to query for any type of interface. If the interface is not available, a null
will be returned. Once the IOleObject
interface is obtained from WebBrowser
, the method SetClientSite()
is called to set MainForm
as the controlling host. Objects passed as a parameter to SetClientSite()
must implement the IOleClientSite
, ergo the presence of IOleClientSit
e in the Class Diagram previously shown in Figure 8-15.
Importing and wrapping COM interfaces
MyCustomWebBrowser implements the two COM interfaces IOleClientSite
and IDocHostUIHandler
, and calls methods of the interface IOleObject
. Unfortunately there are no type libraries published by Microsoft that contain these interfaces, meaning you have to get your hands dirty with a bit of COM programming to work around the problem. As it turns out, most of the COM interfaces that Microsoft uses in its components are not published as type libraries. You’ll either find them as
.idl files, or in some cases as C++ header files.
IOleObject and IOleClientSite
You sometimes have to do a bit of digging to locate the file that declares an interface. In the case of IOleClientSite
and IOleObject
, I found them both declared in the file oleidl.h, which is in the following folder:
C:\Program Files\Microsoft Visual Studio.NET\Vc7\PlatformSDK\include
|
I created C# interfaces to wrap the two COM interfaces, as shown in Listings 8-10 and 8-11.
Listing 8-10:: The Interface that Wraps the COM Interface IOleObject
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace MyCustomWebBrowser
{
[ComImport,
Guid("00000112-0000-0000-C000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
public interface IOleObject
{
void SetClientSite(IOleClientSite pClientSite);
void GetClientSite(IOleClientSite ppClientSite);
void SetHostNames(object szContainerApp, object szContainerObj);
void Close(uint dwSaveOption);
void SetMoniker(uint dwWhichMoniker, object pmk);
void GetMoniker(uint dwAssign, uint dwWhichMoniker, object ppmk);
void InitFromData(IDataObject pDataObject, bool
fCreation, uint dwReserved);
void GetClipboardData(uint dwReserved, IDataObject ppDataObject);
void DoVerb(uint iVerb, uint lpmsg, object pActiveSite,
uint lindex, uint hwndParent, uint lprcPosRect);
void EnumVerbs(object ppEnumOleVerb);
void Update();
void IsUpToDate();
void GetUserClassID(uint pClsid);
void GetUserType(uint dwFormOfType, uint pszUserType);
void SetExtent(uint dwDrawAspect, uint psizel);
void GetExtent(uint dwDrawAspect, uint psizel);
void Advise(object pAdvSink, uint pdwConnection);
void Unadvise(uint dwConnection);
void EnumAdvise(object ppenumAdvise);
void GetMiscStatus(uint dwAspect,uint pdwStatus);
void SetColorScheme(object pLogpal);
};
}
Listing 8-11:: The Interface that Wraps the COM Interface IOleClientSite
using System;
using System.Runtime.InteropServices;
namespace MyCustomWebBrowser
{
[ComImport,
Guid("00000118-0000-0000-C000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
public interface IOleClientSite
{
void SaveObject();
void GetMoniker(uint dwAssign, uint dwWhichMoniker, object ppmk);
void GetContainer(object ppContainer);
void ShowObject();
void OnShowWindow(bool fShow);
void RequestNewObjectLayout();
}
}
A few words of explanation are due regarding the attributes used on the interface declarations. The ComImport
attribute is used to tag an interface as an existing COM interface that is being implemented in managed code. A requirement for a ComImported interface is that there must also be a Guid
attribute providing the interface’s Globally Unique ID. The InterfaceType
attribute indicates the basic type of the interface. The choices are:
InterfaceIsDual
. This interface supports both early and late binding.
InterfaceIsIDispatch
. This interface supports the IDispatch
interface.
InterfaceIsIUnknown
. This interface just derives directly or indirectly from IUnknown
.
The value ComInterfaceType.InterfaceIsIUnknown
is by far the most common type of interface.
I don’t have enough space here to describe all the methods exposed by the interfaces in the last two listings. For the purpose of this discussion, the only method that matters is IOleObject.SetClientSite()
.
When creating wrapper interfaces that will be called from native COM code, it is obviously critical that the GUID you specify matches the original one used by COM. It is also essential that the wrapper interface implements all the original COM methods, and that all methods have the same signature as the original method.
Using ICustomDoc
As stated earlier, the first step in customizing WebBrowser
is to establish MainForm
as the controlling host. Calling IOleObject.SetClientSite()
is the way to do this. If you’re not interested in working with WebBrowser
, but only with MsHtml
, there is another way to support customizations, without the need to use IOleObject
or IOleClientSite
.
Since customizing MsHtml
is a fairly common operation, the component was provided with an interface called ICustomDoc
that you can call to specify an IDocHostUIHandler
object, which will act as a
customizer. When MsHtml
is about to interact with the user is some way, it checks to see if an IDocHostUIHandler
object is available. If so, it calls its methods to find out how to behave. I’ll talk about these methods a bit later. For the moment, what counts is how MainForm
would use MsHtml
’s ICustomDoc
interface. Listing 8-12 shows the details.
Listing 8-12:: Setting a Component as the Customizer for MsHtml
object obj = mshtml.GetOcx();
ICustomDoc doc = obj as ICustomDoc;
doc.SetUIHandler(this);
|
Keep in mind that the interface ICustomDoc
is only available on MsHtml
— not on WebBrowser
— so the code shown in the listing requires you to have a reference to an instance of MsHtml
. There are many applications that use MsHtml
by itself to render HTML pages. For example, it is increasingly common to see forms and dialog boxes containing HTML controls instead of Windows controls. By embedding MsHtml
on a form, you have easy access to the powerful HTML renderer contained in MsHtml
.
IDocHostUIHandler
This is the most important interface MainForm
needs, in terms of customizing MsHtml
. In the following sections, I’ll give you a detailed description about what methods the interface has, what parameters the methods take, when MsHtml
calls the methods, and what effect they have on the user interface.
Before I get ahead of myself (and I’m not talking about an out-of-body experience), let me show you how to import the interface into your managed code. As it turns out, IDocHostUIHandler
references a fairly large number of other OLE interfaces and enumerations, so it isn’t as easy to import as other interfaces.
Since I didn’t have days or weeks to create the necessary wrapper classes and interfaces for all the items referenced in IDocHostUIHandler
, I cheated and used a tool to help me. Borland’s Delphi product has a very powerful and easy-to-use Type Library editor. The types used by IDocHostUIHandler
are mostly defined in the file
mshtmhst.idl, available on the Web and in the following folder:
C:\Program Files\Microsoft Visual Studio.NET\Vc7\PlatformSDK\include
|
But cutting and pasting code from the .idl file into the Delphi Type Library editor, I was able create a new type library file that I could use with TypImp
. I’ll spare you the gory details of my cutting and pasting adventures. Suffice it to say that I created a file called
MsHtmlCustomization.tlb, which I saved in the folder “MyCustomWebBrowser\Type Libraries”
. Using TlbImp
, I easily converted the file into a
.dll that I could use with MyCustomWebBrowser. All I had to do is use the Add Reference Wizard to import the
.dll and then add the following statement to class MainForm
:
using MsHtmlCustomization;
|
In case you’re curious, Figure 8-16 shows Delphi’s Type Library editor with the interfaces and other items the library contains.
Figure 8-16: The Delphi Type Library editor, which is a convenient tool for rapidly creating type libraries
Figure 8-17 shows what the imported assembly MsHtmlCustomization.dll looks like with ildasm
.
Figure 8-17: Exploring the MsHtmlCustomization contents with ILDASM
As you can see from the figure, MsHtmlCustomization contains a number of interfaces that are useful when customizing MsHtml
, including ICustomDoc
and IDocHostShowUI
. Listing 8-13 shows how IDocHostUIHandler
would look if it were declared in C#.
Listing 8-13:: The C# Implementation of IDocHostUIHandler
namespace MsHtmlCustomization
{
[ComImport,
Guid("BD3F23C0-D43E-11CF-893B-00AA00BDCE1A"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
public interface IDocHostUIHandler : IUnknown
{
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement);
public void GetHostInfo(
ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo);
public void ShowUI(int dwID, object pActiveObject,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object pFrame, object pDoc);
public void HideUI();
public void UpdateUI();
public void EnableModeless(int fEnable);
public void OnDocWindowActivate(int fActivate);
public void OnFrameWindowActivate(int fActivate);
public void ResizeBorder(ref MsHtmlCustomization.RECT prcBorder,
int pUIWindow, int fFrameWindow) {}
public void TranslateAccelerator(ref MsHtmlCustomization.MSG lpMsg,
ref MsHtmlCustomization.UUID pguidCmdGroup, int nCmdID);
public void GetOptionKeyPath(ref int pchKey, int dw);
public MsHtmlCustomization.IDropTarget
GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget);
public object GetExternal();
public int TranslateUrl(int dwTranslate, int pchURLIn);
public MsHtmlCustomization.IdataObject
FilterDataObject(MsHtmlCustomization.IDataObject pDO);
}
}
Besides IDocHostUIHandler
, there is another COM interface that is useful in customizing MsHtml
: IDocHostShowUI
. Although the limited space available prevents me from describing this interface in detail, I did include it in MsHtmlCustomization. Listing 8-14 shows how the class would look if declared in C#.
Listing 8-14:: The C# Implementation of IDocHostShowUI
namespace MsHtmlCustomization
{
[ComImport,
Guid("C4D244B0-D43E-11CF-893B-00AA00BDCE1A"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown) ]
public interface IDocHostShowUI : IUnknown
{
public void ShowMessage(int hwnd, ref int lpstrText,
ref int lpstrCaption, uint dwType,
ref int lpstrHelpFile, uint dwHelpContext,
out int lpResult);
public void ShowHelp(uint hwnd, ref int pszHelpFile,
uint uCommand, uint dwData,
MsHtmlCustomization.POINT ptMouse,
out object pDispatchObjectHit);
}
}
This interface is used to control how message boxes and help windows are handled by MsHtml
. As you can see, the interface only supports two methods. In order for IDocHostShowUI
methods to be called back from MsHtml
, the host component (MainForm
in this case) needs to implement the interfaces IOleClientSite
and IOleDocumentSite
. To keep my discussion as short as possible, my example MyCustomWebBrowser doesn’t support IOleDocumentSite
or IDocHostShowUI
. Using the guidelines shown for supporting IOleObject
, IOleClientSite
, and IDocHostUIHandler
, you should be able to add support for IDocHostShowUI
if you need it.
Returning values from methods invoked through a COM interface
When you import a COM method into managed code, the imported method’s signature is different from the original one. For example, Listing 8-15 shows a native COM method and its equivalent C# method.
Listing 8-15:: A COM Method and Its Equivalent C# Method
HRESULT ShowContextMenu([in] ContextMenuTarget dwContext,
[in, out] POINT* pPOINT,
[in] IOleCommandTarget* pCommandTarget,
[in] IDispatch* HTMLTagElement);
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement);
The C# method uses a more friendly notation that is much easier to deal with. A potential problem with the new C# method is the absence of an HRESULT
return value. While COM methods are expected to return an HRESULT to indicate success or failure, the translated C# methods don’t appear to give you access to the HRESULT
. As you can see, the C# version of the method returns void
, while the IDL version returns an HRESULT
.
When you implement COM interfaces using managed code, there are times when you need to set the HRESULT
value returned. For example, IDocHostUIHandler.ShowContextMenu
uses the HRESULT
to return an important value to the caller. Normally the Runtime Callable Wrapper class takes care of the HRESULT
automatically like this: If a managed method throws an exception, the value S_FALSE
is returned to COM; otherwise, the value S_OK
is returned.
If you want to directly control the value returned back to COM, using HRESULT
to return a parameter, you need to throw a special exception called COMException
. When you throw this type of exception, the RCW intercepts it and uses its parameter as the HRESULT
returned to COM. Listing 8-16 shows you how you use COMException
.
Listing 8-16:: Using COMException to Return HRESULT Values to COM
const int Ok = 0;
const int Error = 1;
throw new COMException("", Ok);
throw new COMException("", Error);
|
Most OLE and COM methods use the HRESULT
to signal success or failure of execution. In those few cases in which the HRESULT
is used to return a parameters, just throw a COMException
.
Common customizations
Although you can customize most user interface features of MsHtml
, some customizations are in high demand, so I’ll discuss them in this separate section to make them easier to find in the book. For the other types of customization, you’ll need to look through the later sections of the chapter that discuss the individual callback methods that MsHtml
invokes.
Removing the vertical scroll bar
This is probably the number one customization that developers ask for. When MsHtml
displays a page, a vertical scroll bar is always shown, regardless of whether the page is completely visible or not. If the page fits on the screen, the vertical scroll bar appears with no thumb, as shown in Figure 8-18.
Figure 8-18: The vertical scroll bar that MsHtml shows by default on all HTML pages
Internet Explorer also exhibits this behavior, which is to be expected, since it also uses MsHtml
internally to display HTML content. No matter how much you resize the window, the vertical scroll bar always appears. One way to remove the scroll bar, if you have control over the content displayed, involves setting an attribute of the <body>
element of the page. Using this approach, you don’t need to write any programming code at all. Just add the scroll
attribute to the page’s <body>
tag like this:
That’s it. The page will then appear with no scroll bars, as shown in Figure 8-19.
Figure 8-19: Displaying HTML pages with no vertical scroll bar
The problem with this approach is that it requires you to change the HTML code of the page displayed — something that is not always feasible. A better way to remove the vertical scrollbar is by using one of the IDocHostUIHandler
callbacks. When MsHtml
interacts with the user interface, it frequently makes calls to these callback methods to see how to proceed. By implementing the method GetHostInfo()
, you can disable the vertical scroll bar. The nice thing about this programmatic customization is that it doesn’t require you to make any changes to the content of the HTML page being displayed. Listing 8-17 shows how the GetHostInfo()
method needs to look to turn off the vertical scroll bar.
Listing 8-17:: Programmatically Hiding the Vertical Scroll Bar
public void GetHostInfo(ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
{
theHostUIInfo.dwFlags |= DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO;
}
|
The code does hide the vertical scroll bar, but is not quite sufficient to prevent users from scrolling. How could they scroll without scroll bars? All they have to do is click the mouse somewhere in the HTML page and drag it off the end of the window. MsHtml
thinks the user wants to select text and scrolls to the right or downwards. To disable this behavior, you need to add the DOCHOSTUIFLAG_DIALOG
flag to the value returned from GetHostInfo()
, as shown in Listing 8-18.
Listing 8-18:: Preventing Users from Scrolling an HTML Document
public void GetHostInfo(ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
{
theHostUIInfo.dwFlags |= (DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO |
DOCHOSTUIFLAG.DOCHOSTUIFLAG_NO3DBORDER |
DOCHOSTUIFLAG.DOCHOSTUIFLAG_DIALOG);
}
In case you’re wondering, the name DOCHOSTUIFLAG_DIALOG
comes from the fact that the flag is generally used to display an HTML page inside an ordinary dialog box. In such a case, you normally don’t want users to be able to scroll around the dialog box contents.
The GetHostInfo()
method enables you to control a wide variety of other features. For details, see the section titled “GETHOSTINFO”, later in this chapter. Keep in mind that if you disable the vertical scroll bar, users won’t be able to scroll downwards on HTML pages even if the documents extend beyond the bottom of the window.
Customizing the context menu
Another common request from developers is a way to hide or customize the context menu that appears when users right-click on an HTML page. Figure 8-20 shows the default context menu shown by MsHtml
.
Figure 8-20: The default MsHtml context menu
The items listed in the menu depend on what type of element was right-clicked in the HTML page. The most popular reason for hiding the context menu is to prevent users from choosing the View Source command to gain access to the HTML code of the page. The proper way to control MsHtml
’s context menu is through the ShowContextMenu()
callback. This callback gives you complete control over the context menu, enabling you to do the following:
Let’s take a look at each of these options. To hide the context menu altogether, just return S_OK
as the HRESULT
from ShowContextMenu()
, as shown in Listing 8-19.
Listing 8-19:: Preventing MsHtml’s Context Menu from Appearing
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement)
{
const int Ok = 0;
throw new COMException("", Ok);
}
That was easy. Returning S_OK
from the callback is your way of telling MsHtml
that your host code handled the context menu and MsHtml
is to take no further action. Whether your callback code displayed a context menu of its own or not is important to MsHtml
.
To show your own context menu, you can use the pPoint
parameter to get the position at which the user right-clicked the mouse. Using a standard .NET Framework ContextMenu
component, you can show your custom menu at the same location where MsHtml
would have displayed the default menu. Listing 8-20 shows an example.
Listing 8-20:: Replacing the MsHtml Context Menu with a Custom Menu
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement)
{
Point p = new Point(pPoint.x, pPoint.y);
p = PointToClient(p);
myCustomContextMenu.Show(this, p);
const int Ok = 0;
throw new COMException("", Ok);
}
The parameter pPoint
passed to ShowContextMenu()
is the location right-clicked by the user. The point is in screen coordinates, so you need to call PointToClient
to convert it to client coordinates. I created a simple menu with one command labeled Print. Figure 8-21 shows how the context menu appears on the browser.
Figure 8-21: Displaying a custom context menu
In the ShowContextMenu()
callback, you may want to add logic that displays a custom menu under certain conditions, and the standard MsHtml
menu in others. To make MsHtml
show its own default context menu, throw a COMException
as shown in Listing 8-21.
Listing 8-21:: Telling MsHtml to Use Its Own Default Context Menu
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement)
{
const int Error = 1;
throw new COMException("", Error);
}
Preventing new windows from being opened
Another default behavior of MsHtml
that you may want to disable is allowing a new browser window to be opened with an accelerator key. The built-in behavior of MsHtml
is to open the current document in a new window when the Ctrl-N accelerator key is pressed. To disable this behavior, all you need to do is add some code to a callback method. Any time an accelerator key is pressed (while MsHtml
has the input focus), it will call the callback method TranslateAccelerator()
, passing a message containing the key code for the accelerator key. The value returned in the HRESULT
from the callback tells MsHtml
what to do. The value S_OK
tells MsHtml
to handle the accelerator. If you don’t want MsHtml
to process the accelerator, return the value S_FALSE
. Listing 8-22 shows how to prevent all accelerators from being processed.
Listing 8-22:: Disabling All Accelerator Keys
public void TranslateAccelerator(
ref MsHtmlCustomization.MSG lpMsg,
ref MsHtmlCustomization.UUID pguidCmdGroup,
int nCmdID)
{
const int Error = 1;
throw new COMException("", Error);
}
If you only want to disable certain accelerators, you need to use the lpMsg
parameter to see which keys were pressed. Listing 8-23 shows how to disable only the Ctrl-N accelerator.
Listing 8-23:: Disabling Only the Ctrl-N Accelerator
public void TranslateAccelerator(
ref MsHtmlCustomization.MSG lpMsg,
ref MsHtmlCustomization.UUID pguidCmdGroup,
int nCmdID)
{
const int WM_KEYDOWN = 0x0100;
const int VK_CONTROL = 0x11;
if (lpMsg.message != WM_KEYDOWN)
throw new COMException("", 1);
lpMsg.wParam &= 0xFF;
if (lpMsg.wParam == 'N')
if (GetAsyncKeyState(VK_CONTROL) < 0)
throw new COMException("", 0);
throw new COMException("", 1);
}
The code makes use of the native Windows API method GetAsyncKeyState()
to get the status of the Ctrl key. The method is imported using the code shown in Listing 8-24.
Listing 8-24:: Importing the Windows API Method GetAsyncKeyState()
[DllImport("User32.dll")]
public static extern short GetAsyncKeyState(int vKey);
I’ve taken you through some of the most common types of customization you can apply to MsHtml
, but there are many more. In the following sections, I’ll describe them in terms of the callback methods that are exposed to MsHtml
through the IDocHostUIHandler
interface.
IDocHostUIHandler methods in detail
In the following sections, I’ll describe the various methods of the IDocHostUIHander
interface. The methods appear in alphabetical order.
The IDocHostUIHandler
methods are called when the WebBrowser
component is showing an HTML document using MsHtml
. If a different document type is loaded, such as a Word or PDF file, IDocHostUIHandler
methods won’t necessarily be called.
EnableModeless
This method has the signature:
public void EnableModeless(int fEnable);
|
EnableModeless()
is called at various times by MsHtml
to tell you to disable any modeless dialogs you might have in your host component. For example, when MsHtml
is about to display an error message, it calls EnableModeless
with fEnable
set to 0
. After the user closes the error message, MsHtml
calls EnableModeless()
again with fEnable
set to 1
, telling you that it’s OK to enable any modeless dialog boxes you may have. Listing 8-25 is a simple example that just logs calls to EnableModeless()
to the Trace output window.
Listing 8-25:: A Simple Example with EnableModeless()
using System.Diagnostics;
public void EnableModeless(int fEnable) {
int i = fEnable;
Trace.WriteLine("EnableModeless: fEnable= " + i);
}
FilterDataObject
This method has the signature:
public MsHtmlCustomization.IDataObject
FilterDataObject(MsHtmlCustomization.IDataObject pDO);
|
The DataObjects in question are those used typically with Clipboard operations. At various times, MsHtml
may call FilterDataObject
to let the host component see what type of data is about to be handled. To prevent handling, return the value null
. To allow handling, return the pDO
object, as show in Listing 8-26.
Listing 8-26:: Allowing All Data Types to Be Handled
public MsHtmlCustomization.IDataObject
FilterDataObject(MsHtmlCustomization.IDataObject pDO)
{
return pDO;
}
GetDropTarget
This method has the signature:
public MsHtmlCustomization.IdropTarget
GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget)
|
During Drag and Drop operations, when the user drops an object on a target object, MsHtml
calls GetDropTarget()
. Using this method, you can supply an alternative target. It is unusual to change the drop target. Listing 8-27 shows how to just accept the default target.
Listing 8-27:: Accepting the Default Drop Target
public MsHtmlCustomization.IdropTarget
GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget)
{
return pDropTarget;
}
GetExternal
This method has the signature:
public object GetExternal();
|
MsHtml
calls this method to obtain the IDispatch
interface of the host component (MainForm, in my case). If the host doesn’t implement IDispatch
, it must return null
. Listing 8-28 shows how you would return the IDispatch if your host did implement it.
Listing 8-28:: Returning the IDispatch Interface
public object GetExternal()
{
return this as IDispatch;
}
If you need to implement the IDispatch
interface in a class, the managed code wrapper for IDispatch
is available in the file StdOle.dll in the folder:
C:\Program Files\Microsoft.NET\Primary Interop Assemblies
|
GetHostInfo
This is an important customization callback. The method has the signature:
public void GetHostInfo(
ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo);
|
The parameter passed in is a struct
that looks like the code in Listing 8-29.
Listing 8-29:: The Equivalent C# Code for the DOCHOSTUIINFO struct
public struct DOCHOSTUIINFO
{
public uint cbSize;
public uint dwFlags;
public uint dwDoubleClick;
public uint pchHostCss;
public uint pchHostNS;
};
cbSize
is the length of the struct
in bytes. dwFlags
is the most important field, and is described later in detail. dwDoubleClick
is where you specify what to show in MsHtml
when the user double-clicks the mouse. Possible values are shown in Listing 8-30.
Listing 8-30: The Values that Can Be Assigned to dwDoubleClick
public enum DOCHOSTUIDBLCLK: uint
{
DOCHOSTUIDBLCLK_DEFAULT = 0,
DOCHOSTUIDBLCLK_SHOWPROPERTIES = 1,
DOCHOSTUIDBLCLK_SHOWCODE = 2
};
Table 8-1 describes the values.
Table 8-1
Allowed Values for DOCHOSTUIINFO.dwDoubleClick
Value
|
Meaning
|
DOCHOSTUIDBLCLK_DEFAULT
|
Perform the default double-click action.
|
DOCHOSTUIDBLCLK_SHOWPROPERTIES
|
Show the properties of the double-clicked item.
|
DOCHOSTUIDBLCLK_SHOWCODE
|
Show the code for the double-clicked item.
|
Before you get too excited about controlling the double-click action with GetHostInfo()
, let me inform you that this feature appears to have been disabled in MsHtml
.
Getting back to my description of DOCHOSTUIINFO
, the field pchHostCss
references the Cascading Style Sheet (CSS) used to layout the current HTML page.
The field pchHostNS
references a semicolon-delimited list of namespaces used on the page.
By far the most useful field of DOCHOSTUIINFO
is dwFlags
, which defines a fairly long list of flags you can control to change MsHtml
interface elements. Table 8-2 describes each flag.
Table 8-2
Flags Available in DOCHOSTUIINFO.dwFlags
Flag Name
|
Effect When Property Is Set
|
DOCHOSTUIFLAG_DIALOG
|
Prevents the user from selecting text. If you don’t want users to be able to scroll content by dragging the mouse, use this flag.
|
DOCHOSTUIFLAG_DISABLE_HELP_MENU
|
Disables the right mouse button pop-up menu.
|
DOCHOSTUIFLAG_NO3DBORDER
|
Disables the 3D border around the HTML document displayed.
|
DOCHOSTUIFLAG_SCROLL_NO
|
Turns off both vertical and horizontal scroll bars. Users will then only be able to see the part of the HTML document that fits in the window. They will still be able to scroll the window by dragging the mouse off the document. To prevent this last behavior, include the flag DOCHOSTUIFLAG_DIALOG .
|
DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE
|
Disables all scripts from being run while a page is being loaded.
|
DOCHOSTUIFLAG_OPENNEWWIN
|
Forces WebBrowser to open a new Internet Explorer window if a link is clicked.
|
DOCHOSTUIFLAG_FLAT_SCROLLBAR
|
Disables the 3D look on all scroll bars. If no scroll bars are visible, the property has no effect.
|
DOCHOSTUIFLAG_DIV_BLOCKDEFAULT
|
When the user edits the HTML text on the screen and presses the Enter key, this property makes MsHtml insert a <DIV> tag in the HTML code, rather than the default <P> tag.
|
DOCHOSTUIFLAG_ACTIVATE_CLIENTHIT_ONLY
|
Tells MsHtml to take the input focus only when the user clicks the mouse in the client area. By default, the component will become focused even if the user clicks a non-client area, such as a scroll bar.
|
DOCHOSTUIFLAG_OVERRIDEBEHAVIORFACTORY
|
Disables DHTML behaviors IE 5 and later. (For a discussion of behaviors, see http: .)
|
DOCHOSTUIFLAG_CODEPAGELINKEDFONTS
|
This flag was added only to provide a common look and feel across the two Microsoft products, Outlook Express and Internet Explorer. It applies to Outlook Express 4 and Internet Explorer 5 (or later versions). You’ll probably never use this flag.
|
DOCHOSTUIFLAG_URL_ENCODING_DISABLE_UTF8
|
Disables the use of UTF8 character coding for URLs that have characters that are not in the UTF8 set. By default, MsHtml will always try to use UTF8. Applies to IE 5 and later.
|
DOCHOSTUIFLAG_URL_ENCODING_ENABLE_UTF8
|
Forces the use of UTF8 character coding with URLs that have characters that are not in the UTF8 set. By default, MsHtml will always try to use UTF8. Applies to IE 5 and later.
|
DOCHOSTUIFLAG_ENABLE_FORMS_AUTOCOMPLETE
|
Enables the AutoComplete feature for Forms, which by default is enabled. The value set for this property will be ignored if the user has disabled AutoComplete for Forms in Internet Explorer. To disable AutoComplete for Forms in IE, select the menu command Tools @@> Internet Options, switch to the Content tab, click the AutoComplete button and uncheck the Forms checkbox.
|
You can combine multiple flags, as shown in Listing 8-31.
Listing 8-31:: Setting Multiple Flags in GetHostInfo()
public void GetHostInfo(
ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
{
theHostUIInfo.dwFlags |=
(DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO |
DOCHOSTUIFLAG.DOCHOSTUIFLAG_NO3DBORDER |
DOCHOSTUIFLAG.DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE);
}
You can set any flags you need, by combining them together with a Boolean OR
operation.
GetOptionKeyPath
This method is called to retrieve the Registry path to use for storing user preferences. It is rarely used.
HideUI
If the host component draws UI elements, such as toolbars or menus, related to the MsHtml
state, you need to make sure your UI elements are shown only at the proper time. When the HideUI()
callback occurs, you need to hide those elements. At some later time, MsHtml
will call the ShowUI()
method to let you restore the hidden elements back on the screen.
OnDocWindowActivate
This method has the signature:
public void OnDocWindowActivate(int fActivate);
|
The method is called at various times by MsHtml
to tell you when the HTML document being displayed is activated or deactivated. A document is basically considered activated when it has the input focus. Windows shows the currently active window with a different caption bar color from inactive windows. The reason MsHtml
calls to inform you when the Active status changes is to allow your host component to make any necessary changes to its UI.
OnFrameWindowActivate
This method has the signature:
public void OnFrameWindowActivate(int fActivate);
|
The method is called at various times by MsHtml
to tell you when the top-level frame window containing MsHtml
is activated or deactivated. Use this callback method to change any UI elements in the host component that appear differently when the window is activated versus deactivated.
ResizeBorder
This method has the signature:
public void ResizeBorder(ref MsHtmlCustomization.RECT prcBorder,
int pUIWindow, int fFrameWindow);
|
This method is useful if your host component allows MsHtml
to become inplace-activated. When an embedded component becomes inplace-activated, the host normally draws a hatched border around the embedded component. Inplace-activation used to be considered a really cool technology that used menu-merging and OLE-nested documents. Outside of products like Microsoft Office, inplace-activation just isn’t used that much, so the requirement to support ResizeBorder()
is somewhat unusual.
ShowContextMenu
This method has the signature:
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement);
|
This is commonly used callback method, because it allows you to customize how pop-up menus behave with MsHtml
. You basically have three options:
Allow MsHtml
to display its context menu.
Disable context menus altogether.
Hide MsHtml
’s menu and display your own.
To allow MsHtml
to show its normal context menu, return the value S_FALSE
by throwing a COMException
as shown in Listing 8-32.
Listing 8-32:: Allowing MsHtml to Show Its Default Context Menu
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement)
{
const int Error = 1;
throw new COMException("", Error);
}
To prevent MsHtml
from showing its context menu, return the value S_OK
by throwing a COMException
as shown in Listing 8-33.
Listing 8-33:: Disabling Context Menus Altogether
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement)
{
const int Ok = 0;
throw new COMException("", Ok);
}
To show your own custom menu, use a ContextMenu
component in the callback and return the value S_OK by throwing a COMException
as shown in Listing 8-34.
Listing 8-34: Disabling Context Menus Altogether
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement)
{
Point p = new Point(pPoint.x, pPoint.y);
p = PointToClient(p);
myCustomContextMenu.Show(this, p);
const int Ok = 0;
throw new COMException("", Ok);
}
Notice the call to PointToClient()
. This call is necessary, because the pPoint
is in screen coordinates, while ContextMenu.Show()
requires client coordinates. By setting up your own ContextMenu
, you can customize the pop-up menu in any way you want.
ShowUI
If the host component (MainForm in my case) needs to draw UI elements such as toolbars or menus related to MsHtml
, this callback tells you it’s okay to show them. At some later time, MsHtml
will call the HideUI()
and UpdateUI()
methods where you should be prepared to hide or refresh your UI elements. You don’t need to handle ShowUI()
unless your UI elements are related to MsHtml
’s state.
TranslateAccelerator
When the user presses an accelerator key, such as Ctrl-O or Ctrl-P, MsHtml
calls TranslateAccelerator()
to see what you want to do. This method has the signature:
public void TranslateAccelerator(
ref MsHtmlCustomization.MSG lpMsg,
ref MsHtmlCustomization.UUID pguidCmdGroup,
int nCmdID);
|
Accelerator keys are keys that activate menu commands. Table 8-3 shows the important accelerator keys supported by MsHtml
.
Table 8-3
Main Accelerator Keys Supported by MsHtml
Accelerator
|
Description
|
Ctrl-N
|
Opens the current HTML document in a new WebBrowser window.
|
Crtl-P
|
Displays a Print dialog box for printing the HTML document.
|
Ctrl-A
|
Selects the entire contents of the HTML document.
|
Crtl-F
|
Displays a Find dialog box for searching the HTML document.
|
F5, Ctrl-F5
|
Refreshes the currently loaded HTML document.
|
The accelerators most often disabled are Ctrl-N and Ctrl-P. Listing 8-35 shows how to disable them.
Listing 8-35:: Disabling the Ctrl-N and Ctrl-P Accelerators
public void TranslateAccelerator(
ref MsHtmlCustomization.MSG lpMsg,
ref MsHtmlCustomization.UUID pguidCmdGroup,
int nCmdID)
{
const int Ok = 0;
const int Error = 1;
const int WM_KEYDOWN = 0x0100;
const int VK_CONTROL = 0x11;
if (lpMsg.message != WM_KEYDOWN)
throw new COMException("", Error);
if (GetAsyncKeyState(VK_CONTROL) >= 0)
throw new COMException("", Error);
lpMsg.wParam &= 0xFF;
if ( (lpMsg.wParam == 'N') || ((lpMsg.wParam == 'P')) )
throw new COMException("", Ok);
throw new COMException("", Error);
}
TranslateUrl
This method is called when the user clicks a hyperlink in an HTML document. Before passing the link’s URL to the WebBrowser
, MsHtml
calls TranslateUrl()
to allow the host component to modify the URL.
UpdateUI
If you draw UI elements that are based on the state of MsHtml
, this callback tells you when to refresh those elements. This callback will occur anytime a significant state occurs in MsHtml
.
The Complete Code
I’ve shown you lots of bits and pieces of code. Listing 8-36 shows the complete code for MainForm
of MyCustomWebBrowser.
Listing 8-36:: The Complete Code for MainForm of MyCustomWebBrowser
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;
using System.Diagnostics;
using MsHtmlCustomization;
namespace MyCustomWebBrowser
{
public class MainForm : System.Windows.Forms.Form,
IOleClientSite,
IDocHostUIHandler
{
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.ToolBar toolBar1;
private System.Windows.Forms.ImageList imageList1;
private System.Windows.Forms.ToolBarButton toolBarButtonBack;
private System.Windows.Forms.ToolBarButton toolBarButtonForward;
private System.Windows.Forms.ToolBarButton toolBarButtonStop;
private System.Windows.Forms.ToolBarButton toolBarButtonRefresh;
private System.Windows.Forms.ToolBarButton toolBarButtonHome;
private System.Windows.Forms.ToolBarButton toolBarButtonSearch;
private System.Windows.Forms.ToolBarButton toolBarButtonPrint;
private AxSHDocVw.AxWebBrowser axWebBrowser1;
private System.Windows.Forms.TextBox textBoxAddress;
private System.ComponentModel.IContainer components;
ArrayList urlsVisited = new ArrayList();
private System.Windows.Forms.ContextMenu myCustomContextMenu;
private System.Windows.Forms.MenuItem menuItemPrint;
int currentUrlIndex = -1;
public MainForm()
{
InitializeComponent();
object obj = axWebBrowser1.GetOcx();
IOleObject oc = obj as IOleObject;
oc.SetClientSite(this);
toolBarButtonBack.Enabled = false;
toolBarButtonForward.Enabled = false;
toolBarButtonStop.Enabled = false;
toolBarButtonRefresh.Enabled = false;
toolBarButtonHome.Enabled = false;
toolBarButtonSearch.Enabled = false;
toolBarButtonPrint.Enabled = false;
axWebBrowser1.GoHome();
}
protected override void Dispose( bool disposing )
{
}
private void InitializeComponent()
{
this.axWebBrowser1 = new AxSHDocVw.AxWebBrowser();
this.panel1 = new System.Windows.Forms.Panel();
this.textBoxAddress = new System.Windows.Forms.TextBox();
this.myCustomContextMenu = new System.Windows.Forms.ContextMenu();
this.menuItemPrint = new System.Windows.Forms.MenuItem();
this.axWebBrowser1.Dock = System.Windows.Forms.DockStyle.Fill;
this.axWebBrowser1.Enabled = true;
this.axWebBrowser1.Location = new System.Drawing.Point(0, 28);
this.axWebBrowser1.OcxState =
((System.Windows.Forms.AxHost.State)
(resources.GetObject("axWebBrowser1.OcxState")));
this.axWebBrowser1.Size = new System.Drawing.Size(394, 245);
this.axWebBrowser1.TabIndex = 1;
this.axWebBrowser1.NavigateError += new
AxSHDocVw.DWebBrowserEvents2_NavigateErrorEventHandler(
this.axWebBrowser1_NavigateError);
this.axWebBrowser1.NavigateComplete2 += new
AxSHDocVw.DWebBrowserEvents2_NavigateComplete2EventHandler(
this.axWebBrowser1_NavigateComplete2);
this.axWebBrowser1.BeforeNavigate2 += new
AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2EventHandler(
this.axWebBrowser1_BeforeNavigate2);
}
[STAThread]
static void Main()
{
Application.Run(new MainForm());
}
private void toolBar1_ButtonClick(object sender,
System.Windows.Forms.ToolBarButtonClickEventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
try
{
if (e.Button == toolBarButtonBack)
axWebBrowser1.GoBack();
else if (e.Button == toolBarButtonForward)
axWebBrowser1.GoForward();
else if (e.Button == toolBarButtonStop)
{
axWebBrowser1.Stop();
toolBarButtonStop.Enabled = false;
}
else if (e.Button == toolBarButtonSearch)
axWebBrowser1.GoSearch();
else if (e.Button == toolBarButtonPrint)
PrintPage();
else if (e.Button == toolBarButtonRefresh)
{
object REFRESH_COMPLETELY = 3;
axWebBrowser1.Refresh2(ref REFRESH_COMPLETELY);
}
else if (e.Button == toolBarButtonHome)
axWebBrowser1.GoHome();
}
finally
{
Cursor.Current = Cursors.Default;
}
}
private bool IsPrinterEnabled()
{
int response =
(int) axWebBrowser1.QueryStatusWB(SHDocVw.OLECMDID.OLECMDID_PRINT);
return (response & (int) SHDocVw.OLECMDF.OLECMDF_ENABLED) != 0 ?
true : false;
}
private void PrintPage()
{
object o = "";
SHDocVw.OLECMDID Print = SHDocVw.OLECMDID.OLECMDID_PRINT;
SHDocVw.OLECMDEXECOPT DontPromptUser =
SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
if (!IsPrinterEnabled() ) return;
axWebBrowser1.ExecWB(Print, DontPromptUser, ref o, ref o);
}
public void GotoURL(String theURL)
{
try
{
Cursor.Current = Cursors.WaitCursor;
Object o = null;
axWebBrowser1.Navigate(theURL, ref o, ref o, ref o, ref o);
}
finally
{
Cursor.Current = Cursors.Default;
}
}
private void textBoxAddress_KeyDown(object sender,
System.Windows.Forms.KeyEventArgs e)
{
if (e.KeyCode == Keys.Return)
GotoURL(textBoxAddress.Text);
}
private void axWebBrowser1_BeforeNavigate2(object sender,
AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2Event e)
{
toolBarButtonStop.Enabled = true;
Cursor.Current = Cursors.WaitCursor;
}
private void axWebBrowser1_NavigateComplete2(object sender,
AxSHDocVw.DWebBrowserEvents2_NavigateComplete2Event e)
{
Cursor.Current = Cursors.Default;
toolBarButtonStop.Enabled = false;
toolBarButtonHome.Enabled = true;
toolBarButtonSearch.Enabled = true;
toolBarButtonRefresh.Enabled = true;
String s = e.uRL.ToString();
textBoxAddress.Text = s;
int i = urlsVisited.IndexOf(s);
if (i >= 0)
currentUrlIndex = i;
else
currentUrlIndex = urlsVisited.Add(s);
toolBarButtonBack.Enabled = (currentUrlIndex == 0) ? false : true;
toolBarButtonForward.Enabled =
(currentUrlIndex >= urlsVisited.Count-1) ? false : true;
toolBarButtonPrint.Enabled = IsPrinterEnabled();
}
private void axWebBrowser1_NavigateError(object sender,
AxSHDocVw.DWebBrowserEvents2_NavigateErrorEvent e)
{
Cursor.Current = Cursors.Default;
toolBarButtonStop.Enabled = false;
toolBarButtonHome.Enabled = true;
toolBarButtonSearch.Enabled = true;
toolBarButtonRefresh.Enabled = true;
}
void IOleClientSite.SaveObject() {}
void IOleClientSite.GetMoniker(uint dwAssign,
uint dwWhichMoniker,
object ppmk) {}
void IOleClientSite.GetContainer(object ppContainer)
{
ppContainer = this;
}
void IOleClientSite.ShowObject() {}
void IOleClientSite.OnShowWindow(bool fShow) {}
void IOleClientSite.RequestNewObjectLayout() {}
public void ShowContextMenu(
MsHtmlCustomization.ContextMenuTarget dwContext,
ref MsHtmlCustomization.POINT pPoint,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object HTMLTagElement)
{
const int Ok = 0;
Point p = new Point(pPoint.x, pPoint.y);
p = PointToClient(p);
myCustomContextMenu.Show(this, p);
throw new COMException("", Ok);
}
public void GetHostInfo(
ref MsHtmlCustomization.DOCHOSTUIINFO theHostUIInfo)
{
theHostUIInfo.dwFlags |= (DOCHOSTUIFLAG.DOCHOSTUIFLAG_SCROLL_NO |
DOCHOSTUIFLAG.DOCHOSTUIFLAG_NO3DBORDER);
}
public void ShowUI(int dwID, object pActiveObject,
MsHtmlCustomization.IOleCommandTarget pCommandTarget,
object pFrame, object pDoc) {}
public void HideUI() {}
public void UpdateUI() {}
public void EnableModeless(int fEnable) {
int i = fEnable;
Trace.WriteLine("EnableModeless: fEnable= " + i);
}
public void OnDocWindowActivate(int fActivate) {}
public void OnFrameWindowActivate(int fActivate) {}
public void ResizeBorder(ref MsHtmlCustomization.RECT prcBorder,
int pUIWindow, int fFrameWindow) {}
public void TranslateAccelerator(
ref MsHtmlCustomization.MSG lpMsg,
ref MsHtmlCustomization.UUID pguidCmdGroup,
int nCmdID)
{
const int Ok = 0;
const int Error = 1;
const int WM_KEYDOWN = 0x0100;
const int VK_CONTROL = 0x11;
if (lpMsg.message != WM_KEYDOWN)
throw new COMException("", Error);
if (GetAsyncKeyState(VK_CONTROL) >= 0)
throw new COMException("", Error);
lpMsg.wParam &= 0xFF;
if ( (lpMsg.wParam == 'N') || ((lpMsg.wParam == 'P')) )
throw new COMException("", Ok);
throw new COMException("", Error);
}
public void GetOptionKeyPath(ref int pchKey, int dw) {}
public MsHtmlCustomization.IdropTarget
GetDropTarget(MsHtmlCustomization.IDropTarget pDropTarget)
{
return pDropTarget;
}
public object GetExternal()
{
return null;
}
public int TranslateUrl(int dwTranslate, int pchURLIn) {return 0;}
public MsHtmlCustomization.IDataObject
FilterDataObject(MsHtmlCustomization.IDataObject pDO)
{
return pDO;
}
[DllImport("User32.dll")]
public static extern short GetAsyncKeyState(int vKey);
}
}
Summary
By the sheer number of ways WebBrowser
can be customized, you must realize by now how important this component is, not only
for Microsoft, but also for your own applications. Once you import the proper COM interfaces into your managed code, WebBrowser
is your friend. Moreover, it’s already installed on practically every Windows machine on the planet, is flexible beyond words, and is
a pleasure to use.