Kylix 2 uses a version of the Mozilla
web browser to provide HTML preview within the IDE. Although Mozilla
provides an easy way of embedding a browser within a GTK application,
there is not an easy way to do this from within Kylix. We
investigated a few different ways of embedding Mozilla and ultimately
settled on a solution that is fairly simple and can provide a
mechanism for embedding most any application within another one.
The truth is, Mozilla isn't really
embedded within the Kylix IDE. It's all smoke and mirrors. A
customized, GTK-based version of Mozilla is running as a completely
independent application. But, by using a combination of the X window
system API and a small amount of interprocess communication, we are
able to give the illusion of an embedded web browser. To make this
illusion work, there were two fundamental problems we had to resolve:
(1) Finding a modern, Javascript-enabled web browser that we could
control from the IDE and (2) making it act as if it were a part of
the HTML Preview frame of the Kylix code editor window.
Controlling Mozilla from the Kylix IDE
For good or bad, Microsoft has tightly
bound Internet Explorer into Windows. For Delphi 6, this provided us
with an easily embeddable web browser. On Linux, there is no
standard, built-in HTML viewer. Instead, there are many web browsers
available. The one that seemed most appropriate for us to use was
Mozilla. It provides the Javascript and modern HTML support that we
wanted, and since Mozilla is an open source project, it could be
adapted to allow the IDE to control it.
Mozilla is a large, complex C++
application that is built on a component object model, called XPCOM.
The same Mozilla codebase works on various flavors of Unix, Windows
and Macintosh. To accomplish this, Mozilla isolates the
platform-specific code into several objects which provide
platform-specific implementations of platform-neutral interfaces. One
mechanism that we considered was to produce an Object Pascal
definition of several hundred of the Mozilla and XPCOM interfaces and
to make Object Pascal/CLX implementations of the platform-specific
objects that we needed. In essence, this would make CLX another
platform on which Mozilla could be run. Although there are a number
of advantages to this approach, the time required to build and test
it was more than what we had available. So, we looked for other
alternatives.
We found that Mozilla produces an
embedding widget that is intended for applications much like ours.
The only problem with the Mozilla widget is that it is written to
work with the GTK widget set, while the Kylix IDE uses a combination
of CLX (based on the Qt widget set) and VCL (using the
Windows/Winelib widget set). Attempting to integrate yet another
widget set into the IDE proved impractical. So for this first step,
we decided to make the browser a stand-alone application. The trick
was getting the IDE to control it.
We found that the Mozilla source tree
contains a bare-bones browser application (TestGtkEmbed) which is
used to test the GTK embedding widget. This test application is what
we used as our starting point.
To control the browser, we needed a way
to have the browser render the contents of a CLX/VCL TStream object
and have the browser refresh its display whenever the IDE was ready
to send a new stream. To do this, we created a class in Kylix that
serves as the interface between Kylix and Mozilla. It does several
things:
Launches the Mozilla TestGtkEmbed
application (which we customized and renamed to BorMozBrowser).
Writes the TMemoryStream that we
get from the WebSnap page producer objects to a fixed filename,
$HOME/.borland/preview.
Whenever the IDE sends a new
stream, rewrite the file and send a signal to the BorMozBrowser
application to refresh its display.
The class looks something like this:
TMozillaBrowser = class (TObject)
private
ViewerPID: Integer;
outfilename: AnsiString;
homedir: PChar;
instream: TStream;
ViewerCmd: AnsiString;
procedure StartMozilla;
public
constructor Create;
procedure Navigate(const input: TStream);
procedure Reload;
procedure Quit;
destructor Destroy; override;
end;
resourcestring
ENOBROWSER = 'Cannot find browser application.';
constructor TMozillaBrowser.Create;
begin
inherited Create;
homedir := getenv('HOME');
ViewerCmd := './mozilla/BorMozBrowser';
if not FileExists(ViewerCmd) then
raise EAbort.Create(ENOBROWSER);
outfilename := homedir + '/.borland/preview';
ViewerPID := 0;
end;
procedure TMozillaBrowser.StartMozilla;
var
Argv: array of PChar;
begin
ViewerPID := 0;
SetLength(Argv, 1);
Argv[0] := PChar(ViewerCmd);
ViewerPID := fork;
if ViewerPID = 0 then
begin
execv(PChar(Argv[0]), @Argv[0]);
Exit;
end;
end;
procedure TMozillaBrowser.Navigate(const input: TStream);
begin
instream := input;
Reload;
end;
procedure TMozillaBrowser.Reload;
var
fHandle : Integer;
outfile : TFileStream;
begin
if FileExists(outfilename) then
DeleteFile(outfilename);
fHandle := FileCreate(outfilename);
FileClose(fHandle);
outfile := TFileStream.Create(outfilename, fmOpenReadWrite or fmShareExclusive);
outfile.CopyFrom(instream, 0);
outfile.Destroy;
if (ViewerPID = 0) or (kill(ViewerPID, SIGUSR1) < 0) then
StartMozilla;
end;
The interesting work here is done by the Reload method. It takes the stream which is passed into the Navigate method and writes it to the $HOME/.borland/preview file.
Next, it either launches the browser or refreshes the one that
already exists.
Launching the browser is done by
calling the StartMozilla method. This procedure constructs a
command-line argument array. In this case, the array has a single
element: the full path to the browser executable. We will add more
command-line arguments later. After constructing the array, the
method uses the Linux fork-exec mechanism to launch the browser as a
child process. The process ID of the BorMozBrowser application is
saved in the variable ViewerPID.
Refreshing the browser display is done
by using the standard Linux signal mechanism. Linux has a very
limited number of signals and most of them have a particular meaning.
For example, SIGTERM terminates a process. There are two
user-defined signals that can be used for very simple interprocess
communication, SIGUSR1 and SIGUSR2. Since the communication that we
need to do is very simple (a simple refresh trigger), the signal
mechanism seemed to be a good choice. We use SIGUSR1 to tell the
browser to refresh its display. The command kill(ViewerPID,
SIGUSR1)sends the signal SIGUSR1 to the process ID defined by
ViewerPID.
To make the browser recognize this
signal, we had to modify the TestGtkEmbed application that comes with
Mozilla. We made these modifications:
Stripped off the extra controls
that would normally be used to enter an URL.
Hard coded the application to load
only a single URL and to load it on startup. The URL it loads is
file://$HOME/.borland/preview.
Added a signal handler so that
after the browser loads the URL, it waits to receive a signal,
SIGUSR1. Upon receipt of the signal, it simply reloads the URL and
waits for the signal again.
The full source code to our modified
version of the browser can be downloaded with this article. Note that
this application is covered by the terms of the Mozilla Public
License. If you review this code, you will see that there are some
other modifications as well. These have to do with sizing and
positioning the browser window. We will cover those changes in the
next section.
Now we have an Object Pascal class
which can launch an HTML viewer (BorMozBrowser), change the HTML
content (by re-writing the $HOME/.borland/preview file) and refresh
the display (by sending SIGUSR1 to the browser process). This gives
us a web browser that can be controlled by the IDE. That's one
problem down, one to go.
Embedding the browser application in the Kylix IDE
The problem with getting the Mozilla
browser application to appear to work inside the IDE has to do with
widgets. Each widget set has its own event loop that needs to receive
events so that the browser can appear to be embedded. We took
advantage of the fact that all of these widget sets run on top of the
X window system. Qt, GTK and Winelib all receive X events and
translate them into Qt, GTK or Windows signals or messages. By having
the Kylix IDE send X events to the browser window, it forces the
window to respond the way we want.
There are several things that need to
happen for the illusion of embedding to be successful:
The BorMozBrowser window needs to
be stripped of its window elements (title bar, frame) and be owned
by and IDE window.
The browser window must be
correctly sized and positioned so that it exactly covers the space
for the HTML Preview frame of the IDE code editor.
The browser needs to resize when
the code editor window is resized.
The browser must hide and show
itself appropriately as the user selects the various tabs in the
IDE.
The browser application must
terminate when the corresponding code editor window is closed.
When the browser application starts, it
is a top level X window which is owned by the root X window. The IDE
code editor is also a top level X window. There is an Xlib API
function, XReparentWindow, which allows one top level window to take
ownership of another top level window. In the case of the browser
application, this function also conveniently strips off the window
elements. This is what we use for the first two steps.
The Kylix unit Xlib has Object Pascal
definitions of the relevant portions of the X API. Since we already
created a Kylix object which launches and refreshes the browser
application, it made sense to have it control the windowing as well.
The only problem is that the Kylix object needs to know the X window
ID (XID) of the browser window. We use a form of interprocess
communication, called an X event, to have the browser tell the IDE
what its XID is.
As mentioned in the previous section,
the TMozillaBrowser.StartMozilla method actually passes a number of
command line arguments to the BorMozBrowser application. One of these
parameters is the XID of the IDE's code editor window. This is to
allow the browser application to send a message to the code editor
window.
In the case of the IDE, the top-level
code editor window is a VCL TForm. We use a Winelib function to
obtain the XID of this TForm. In a CLX application, you can use the
QWidget_WinID function to get the XID of a CLX QForm.
Once the BorMozBrowser application
loads, we use a GTK function call to obtain the XID of the browser
window. BorMozBrowser then sends an X event back to the parent
window. An X event is similar to a Windows message. Data can be
embedded within the XEvent structure. In this case, the browser
application embeds its own XID into the XEvent. The Kylix IDE has a
message handler which processes this X event. To receive an XEvent
from within a CLX application, it is necessary to monitor the Qt X11
event loop. Mark Duncan posted an article earlier (http://codecentral.borland.com/codecentral/ccweb.exe/listing?id=16133) which
provides a mechanism for doing this. Once the Kylix object receives
the XEvent, it calls XReparentWindow and takes ownership of the
browser.
Step two is a simple matter of getting
the geometry of the HTML Preview frame in the IDE. We pass the height
and width as command line parameters to the BorMozBrowser
application. TMozillaBrowser uses the Top and Left values of the HTML
Preview frame to determine the proper offset when it calls
XReparentWindow.
The remaining steps involve hooking
additional events and creating event handlers in TMozillaBrowser. For
example, when the object receives a resize window event, it
recalculates the window geometry and sends an XResizeWindow event to
the XID of BorMozBrowser. This X event resizes the browser window and
causes GTK to fire the gtk_resize_request signal which makes the
browser application resize its content.
Similarly, when the browser needs to be
hidden, an XUnmapWindow event is sent. When the browser needs to be
shown again, an XMapWindow event is sent. These X events cause GTK to
fire its signals for hiding and repainting a window.
Now we have an HTML browser that can be
controlled by the Kylix IDE and it can appear to be embedded within
the HTML Preview frame of the code editor.
Implementing an embedded browser in CLX
Of the two steps: (1) controlling
Mozilla from a Kylix application; and (2) reparenting Mozilla inside a
Kylix form); the first can easily be done within a CLX application in
the same way that has been shown in this article. The BorMozBrowser
application can be easily modified to load your own pre-defined URL
(or accept a command-line argument with the URL to load). Note that a
built Mozilla source tree is required to build BorMozBrowser. This
method can be used to provide an HTML viewer window that can be
launched and closed by a CLX application.
Step 2 (embedding the Mozilla window
within a CLX window) may not be possible at this time. The Kylix code
editor is VCL based and consequently, uses the Winelib widget set.
Winelib and Qt respond quite differently to the sleight of hand that
we have used here.
One promising avenue for investigation
is the Qt version of Mozilla. On Linux, Mozilla's default widget set
is GTK. However, it can be configured and built to use Qt widgets.
This configuration has suffered from some code rot, but it has
recently been resurrected and is showing some activity. The Mozilla
newsgroup netscape.public.mozilla.qt on news.mozilla.org has more
information (and a lot of spam) on this port.
You can find the source code for this BorMozBrowser on CodeCentral at http://codecentral.borland.com/codecentral/ccweb.exe/listing?id=17248.
Connect with Us