11.03.2013 Create a Web Browser in Lazarus with Gecko (Part 2)

Web browsers are our daily thing. We browse the internet through them. But what about making our own browser? That we are going to do.

Before you continue please read part 1 of this tutorial to setup your Lazarus to add support for GeckoPort components in your Toolbar.

Ever thought of having a browser of your own that you wanted to add a stopwatch to? A stopwatch that measures how much time each website takes to load? Or may be counts how much time you surf in a day, month or year? That's just a weird idea. But the head of programers are filled with such ideas. Let's not forget, some of the best creative ideas can come from these weird ideas.

Anyway, I think more or less 1 in 10 developers among us would think making a browser would be cool, considering customizability. Making a browser and completely controling its behavior yourself, is very cool.

So today we are creating a simple web browser in Lazarus IDE. So let's start...

Before we begin...


I am quite sure that you know there are many browsers. For example, Mozilla Firefox, Microsoft Internet Explorer, Google Chrome (Chromium in Linux), Opera etc. Lazarus also has many libraries of creating a web browser. Such as LazActiveX, LazWebkit, Gecko. Check out this wiki page for details on the options you have. But I have chosen GeckoPort for our tutorial. GeckoPort is a port of Mozilla's Gecko engine. Gecko is the library which Firefox uses to render web pages. So if we use GeckoPort then we could show pages like Firefox.

Although there is a "but". The Lazarus Wiki has two versions of GeckoPort. Version 1 and version 2. Version 2, according to the wiki, has better support for later versions of XULRunner:

Version 2 is based on automatically generated include-files from the idl files from the Gecko SDK. For this purpose the idltopas utility is used. This way it's earier to follow the fast Gecko-release cycle now gecko-developers do not maintain backwards compatibility any more.

Now the big BUT is that nobody ever (I think) could make it to work. It has problems that interrupts the compiling. So we would have to use version 1 instead.

Tutorial


In the part 1 of the tutorial we have successfully setup Gecko in Lazarus and built & ran one of the sample projects. We have extracted XULRunner 1.9.2.x on the exe path and everything ran okey.

In this instalment, we will see how to create a browser project ourselves and create a basic browser to browse with.

Project Preparation


Start Lazarus.

Create a new Application Project (Project->New Project->Application->OK).

Now save the project through Save All (File->Save All). Name the project something like proj_gecko_test.lpi and the unit to something like frm1.pas.

Now click Open from the toolbar (or File->Open). Now open the proj_gecko_test.lpr file (not the .lpi file). Now we have to add Math unit to the uses section. I had some compiler directives before so I kept them. I have brought the Interfaces unit down in a compiler directive with Math unit. But I think you are safe without it, just by adding Math unit without the compiler directives. Here is the uses part of my project:

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Forms, frm1
  { you can add units after this }
  {$IFDEF LCL}
    ,Interfaces
    {$IFDEF MSWINDOWS}
      ,Math
    {$ENDIF}
  {$ENDIF}
  ;

Now, after the following code between the begin and end:
  {$IFDEF FPC}
   {$IFDEF MSWINDOWS}
    //For now - disable all floating point exceptions or XULRUNNER will crash.
    SetExceptionMask([exInvalidOp,exDenormalized,exZeroDivide,exOverflow,exUnderflow,exPrecision]);
   {$ENDIF}
  {$ENDIF}

Click Save All to save all your project files (including the .lpr file). Now close the .lpr file (you can simply middle click on the tab that has the file name). Now you should only have the frm1 code tab.

And also, extract the XULRunner 1.9.x to your project directory.

Okey, now we are ready for some serious action.

Components on Form


Go to form view (F12). Now go to Gecko tab from the toolbar and drop TGeckoBrowser on your form. You should see a component with a Lizard in it. That's Gecko! Our trusty browser component! Name your trusty component as "Browser".

TGeckoBrowser component in the Gecko toolbar tab -- Lazarus IDE
 
Gecko component drawn in the form


Now Drop a TEdit (Standard tab) and a TBitBtn (Additional Tab). Set the caption of BitButton as GO. Set it's name to btnGo. Name the TEdit to edtAddress.

Drop 3 TSpeedButtons. They will be our Back, Forward and Refresh/Reload button. Name them btnBack, btnForward and btnReload. Set their Glyphs to icons. I have used FamFamFam and Silk Companion icon packs.

Creating buttons and address bar in form view


Now drop a TProgressBar (Common Controls). That will be our loading progress bar. Drop a TLabel as well and name that lblStatus.

Progress bar and status bar

Now to coding...


Double click btnGo and enter:
procedure TForm1.btnGoClick(Sender: TObject);
begin
  Browser.LoadURI(edtAddress.Text);
end;

Now that you are in code view, go to the first var clause of the unit and add the homepage string, like this:

var
  Form1: TForm1;
  
  // Contains the homepage URL. Change it as you wish!
  HomePage: string = 'http://lazplanet.blogspot.com';

( The Form1: TForm1; line will vary if you have named the form to something else other than Form1. )

Now switch to form view (F12) and double click the form. And enter:
procedure TForm1.FormCreate(Sender: TObject);
begin
  edtAddress.Text := HomePage;
  btnGoClick(Sender);
end;

Basically, we are setting the address bar text to our homepage url, then virtually clicking our go button. That should open the homepage for us when we run the program.

Now some other buttons. Double click btnBack and enter:
procedure TForm1.btnBackClick(Sender: TObject);
begin
  Browser.GoBack;
end;

Double click btnForward and enter:
procedure TForm1.btnForwardClick(Sender: TObject);
begin
  Browser.GoForward;
end;

Only Browser.GoBack and Browser.GoForward is enough for managing our browsing history. In the background, Gecko will manage everything. Like our trusty sidekick, right?! :-)

Reload is also easy. Browser.Reload is the code. Double click btnReload and enter:
procedure TForm1.btnReloadClick(Sender: TObject);
begin
  Browser.Reload;
end;

Now, let's see how our browser looks and works. Press F9 to compile and run the project exe

Okey, now that you have the project exe running, you should see the LazPlanet website (or the URL that you have set in the HomePage string variable). Now if you click any link it loads the page, but the URL of the new page does not show in the Address bar. Also, we want the progress bar to be working and the status too. So, we would now consult the Events of Gecko component.

Now switch to Form view if you are not already (F12). Now select the Browser component. Now go to Object Inspector-> Events. Now click the [...] button in front of OnLocationChange. Now enter:

procedure TForm1.BrowserLocationChange(Sender: TObject; const uri: AnsiString);
begin
  edtAddress.Text:=uri;
end;

Now Run (F9). Click any URL and the address bar will be updated.

Now we head off to the progress bar and status bar text.
Add the following code to Browser's OnProgressChange event procedure:
procedure TForm1.BrowserProgressChange(Sender: TObject; Progress: Integer;
  ProgressMax: Integer);
begin
  ProgressBar1.Max:=ProgressMax;
  ProgressBar1.Position:=Progress;
end;

Add the following code to Browser's OnStatusChange event procedure:
procedure TForm1.BrowserStatusChange(Sender: TObject; aMessage: WideString);
begin
  lblStatus.Caption:=aMessage;
end;

Also, we love to type address and then press enter to access the website. So select edtAddress and add the following code on OnKeyPress event procedure:
procedure TForm1.edtAddressKeyPress(Sender: TObject; var Key: char);
begin
  // if the user presses enter...
  if (Key = chr(10)) or (Key = chr(13)) then begin
    btnGoClick(Sender);
  end;
end;

Now Run (F9). When you browse, it would feel more responsive because you will know what the browser is doing with status message and the progress bar. Also you can type an address and press enter to visit it. But what about the resize scenario?

Resize Scenario

We can use Anchor property to update the components' size on form resize, without writing a single line of code. Every Anchor property (akBottom, akLeft etc.) is available under Anchors property.
Anchors in the Properties tab helps to manage form resize scenario

Click on the plus sign or the arrow beside the Anchors title to expand and see all the items.

Select edtAddress and set akRight to True. Select btnGo and set akLeft to False, akRight to True. Now select Browser component and set akRight and akBottom to True.

Press F9 and see the results. Resize the form and the components will be resized automatically. But the progressbar and status bar does not stay in the right position.

Progressbar overlay screenshot


Select ProgressBar1 and set akTop to False, akBottom to True. Select lblStatus and set akTop to False, akBottom to True.

Now press F9. It should now work nicely.

Web browser made with Lazarus IDE using Gecko engine

Download Sample Code ZIP

You can download the above example tutorial project source code from here: https://db.tt/Vv8h9mk8
Size: 577 KB
The package contains compiled executable EXE. Don't forget to extract xulrunner in the same directory as the exe to make it work.

41 comments:

Unknown said...

hi,

When i compile the component, i have an error :

windres" not found, switching to external mode
[my path]/GeckoBrowser.pas(2350) Fatal: There were 1 errors compiling module, stopping

the component was found here : https://db.tt/dugAn4No

Lazarus : 1.0.12 31.10.2013
FPC : 2.6.2
OS Linux DEBIAN 7.2

Regards,

Willy

Adnan Shameem said...

Hello Willy,

May be you don't have windres installed. Check out the wiki page:

"On Linux/OSX, you might need to make sure you have the windres resource compiler, which is provided by the mingw32 tools. E.g. on Debian you could run:..."

Regards
Adnan

Unknown said...

Hello Adnan,

Is ok for the compilation but the GBrowser.lpi project not running. I copied the directory 'xulrunner ' into my app directory and i still get the error message 'Failed to initialize TGeckoBrowser'. Have you an idea of the origin of this problem ?

Tanks,
Willy

Adnan Shameem said...

Willy,

The instruction given above is for Windows. For linux you need to set the value of an environment variable named LD_LIBRARY_PATH showing the path to your "xulrunner" directory. I guess I should've mentioned it in the article.

So, for example, if you have copied your xulrunner to /usr/lib/xulrunner-1.9.1.4, then you will have to run the command below:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/xulrunner-1.9.1.4

Source: Lazarus wiki

Good luck!
-Adnan

Unknown said...

Hello Adnan,

Yes i saw that statement. I did a test by running the application from a Linux terminal and the application works fine but when i run the application from nautilus, it does not work. So i created a script with the environment variable and it works.

Again thank you for your help.

Willy

Adnan Shameem said...

Willy,

It seems that the export command only sets the env variable temporarily. To make the effect permanent, try:
1. gedit ~/.bash_proflle
2. Append the export command line, such as:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/xulrunner-1.9.1.4
3. Save and restart.

Try it and let me know if you can use the browser when opening with nautilus.

Ref: http://www.cyberciti.biz/faq/set-environment-variable-linux/

-Adnan

Unknown said...

Hi,


Excuse me for this late reply.
The problem persists despite the creation of the file .bash_profile.

again thank you.
cordially

Willy

Adnan Shameem said...

Willy,

Are you sure the env variable is set after editing .bash_profile? Did you try:

echo $LD_LIBRARY_PATH

in the terminal? Does it return the xulrunner path?

Marc said...

A collegue of mine presented me a C# application, in which he embedded the gecko engine, too.

What was really suprising was the fact, that he was able to call C# functions from JavaScript (where a webdeveloper would do an ajax request).

So he could design the user interfaces of his application with existing HTML / JavaSCript / CSS components and implement the business logic in C#.

Is something like that possible with the Gecko-Lazarus port? This would be really awesome!

Adnan Shameem said...

Hello Marc,

May be it is possible to do with Custom URL Protocol. I haven't tried it with Lazarus. But I discovered this amazing possiblity when I coded with Multimedia Builder (MMB). It had a browser frame component. It was possible to create such a webpage/html file that could communicate with the software to run certain procedures. MMB is written in Delphi, so I guess it is also possible in Lazarus (although the component was based on IE). But I never tried to reproduce that in Lazarus.

I found this C# article which uses custom URL protocol to communicate with the desktop app, but it requires a server. Are you aware that if your colleague used a server or not?
http://www.codeproject.com/Articles/545083/Custom-HyperLinks-Using-a-Generic-Protocol-Handler
Also check out this MSDN Article

Regards
Adnan

Marc said...

Hello Adnan,

sorry for the late reply. No, my colleague did not used a server. He used for demonstration purposes the GUI of one of our web applications, changed a little bit the event procedures in JavaScript so that they talked to an API instead of creating Ajax-requests.

With the API it was possible to call the C# objects and methods.

I really really really want this to do with Free Pascal :-)

Help please! ;-)

Adnan Shameem said...

Marc,

Okey, if you are that desperate, then here's what you can do...

We have a Status Change event in gecko. And we can change the status from the html (through javascript). So we can trigger an action when the status changes to something we have planned. This way we can interact with browser from our html page.

You create a link or button or whatever that changes the status text in your html file:

<a href="" onmousedown="window.status='__MySecretAction'">CLICK ME!!!</a>

Then add some code in the GeckoBrowser's OnStatusChange event:

if aMessage = '__MySecretAction' then
ShowMessage('internal message');


Now load the html file on your browser, click on the link, and it will trigger the message!

It is a crazy solution to that problem, but it works.

Marc said...

I today found the following project, which gives you access to JavaScript from Pascal and vice versa: https://code.google.com/p/fpcjs/ In combination with the gecko engine designing UIs with HTML / JavaScript would be awesome. But I have too less experience with Free Pascal to find out if both work together... :-/

Adnan Shameem said...

Marc,
You mean from the Program to Browser communication (instead of Browser to Program). There is a simple trick for running javascript through entering them to Location bar. You may try that to communicate to the browser's page.

See this: http://support.mozilla.org/en-US/questions/876916

I'm at another computer, so I can't try it. You may be lucky if it works.

FPCJS may also be a solution.

zbyna said...
This comment has been removed by the author.
zbyna said...

Hi,
I have a small question.
Is it possible to use DOM with gecko port at this stage of project ? ( HTML elements as objects, properties of all HTML elements,methods to access all HTML elements, events for all HTML elements)
I have not found any decent solution for it in Lazarus sofar.

Thanks
Zbyna

Adnan Shameem said...

@zbyna,

Did you try something like:
Browser.ContentDocument.Getxxxx...;

zbyna said...

Thanks for advice ! It helped me very much.
I have tried this today and I found
these links which describe most of all I wanted to achieve:
http://forum.lazarus.freepascal.org/index.php?topic=15352.5;
http://forum.lazarus.freepascal.org/index.php/topic,15980.msg86405.html#msg86405

I am beginner and I was confused by interfaces in nsXPCOM.pas . It seems
to be something like protocols from ObjectiveC.

Newertheless if you decide to write some short tutorial on this topic I will appreciate it.

I am able to find elements, change them and click buttons from pascal code sofar.

Adnan Shameem said...

@zbyna

What are you trying to do? List all HTML elements and their attributes?

zbyna said...

I am trying to catch focused element:
procedure TForm1.BrowserDOMKeyPress(Sender: TObject; aEvent: TGeckoDOMEvent);
var
mujFocus: nsIWebBrowserFocus;
aktivniElement: nsIDOMElement;
s,s2:IInterfacedString;
begin
mujFocus:=Browser as nsIWebBrowserFocus;
aktivniElement:=mujFocus.GetFocusedElement();
s:=newstring(widestring('id'));
s2:=newstring(widestring(' '));
aktivniElement.GetAttribute(s.AString,s2.AString);
Memo1.Text:=string(sender.ClassName)+ ' ' +aEvent.name + s.ToString;
end;

but line: mujFocus:=Browser as nsIWebBrowserFocus; seems to be problem
runtime error: 219 and then Invalid Cast message
It seems I have problems with Gecko SDK object model :-)

zbyna said...

I found solution but a little bit dirty:
I change source code of project -
unit GeckoBrowser.pas:
TCustomGeckoBrowser = class(TCustomControl,IGeckoCreateWindowTarget)
private
// Zbyňa FWebBrowser: nsIWebBrowser;
public
FWebBrowser: nsIWebBrowser; // Zbyňa

then this example works:

procedure TForm1.BrowserDOMClick(Sender: TObject; aEvent: TGeckoDOMEvent);
var
ir:nsIInterfaceRequestor;
mujFocus: nsIWebBrowserFocus;
mujBrowser:nsIWebBrowser;
aktivniElement: nsIDOMElement;
s,s2:IInterfacedString;
begin

//Browser.QueryInterface(nsIInterfaceRequestor,ir);
//Browser.ContentWindow.QueryInterface(nsIInterfaceRequestor,ir);
//ir.GetInterface(nsIWebBrowser,mujBrowser);
//ir.GetInterface(NS_IWEBBROWSERFOCUS_IID,mujFocus);
//mujBrowser.QueryInterface(nsIInterfaceRequestor,ir);
//ir.GetInterface(nsIWebBrowserFocus,mujFocus);
mujFocus:=Browser.FWebBrowser as nsIWebBrowserFocus;
if assigned(mujFocus) then
begin
mujFocus.Activate();
aktivniElement:=mujFocus.GetFocusedElement();
s:=newstring(widestring('id'));
s2:=newstring(widestring(' '));
if Assigned(aktivniElement)then
begin
aktivniElement.GetAttribute(s.AString,s2.AString);
Memo1.Text:=string(sender.ClassName)+ LineEnding +aEvent.name +
LineEnding + s2.ToString;
end;
end;
end;

In comments you can see my desprite :-) attempts to get this work.

Adnan Shameem said...

@zbyna

Yes, I can see that you tried so hard. Good to see that you have solved the problem!

rubendaj said...

Hello Adnan

Very Interesting Job. But I Have a Question: TGeckoBroser Support HTML5 ? Because I need to run pdf.js

thanks in advance

Adnan Shameem said...

@rubendaj

pdf.js is one of my favorite projects.

Well, according to the XULRunner 1.9.2 release notes - "XULRunner 1.9.2.x is built from the same source code snapshot as Firefox 3.6.x."

So basically what Firefox version 3.6 can run, our GeckoPort can run it too. I am a little busy right now, so go ahead and test it out yourself.

Edit: See these pages for compatibility information:
- Required features: https://github.com/mozilla/pdf.js/wiki/Required-Browser-Features
- Test if features available: http://mozilla.github.io/pdf.js/features/
- FAQ: https://github.com/mozilla/pdf.js/wiki/Frequently-Asked-Questions#faq-support

ipkwena said...

Hi there,

Your guide is working flawlessly in Lazarus ver 1.0.8 running on Win 7.

I have managed to embed the gecko browser and was wondering how to export or save the browser content as an HTML file. any ideas from anyone?

Regards.


Unknown said...

Personally, I can get some information that is loaded into the Gecko page. Precise type get a value only the page returns me when I visit a site. Thanks and great article. Site very useful for Brazilians

Unknown said...

Salam Adnan. I have done everything as you have said. I have used the same versions which you have mentioned, the same icons, the same names etc. The sample browser mentioned in Part 1 of tutorial works like charm but the self made browser in tutorial part 2 shows an error when I compile it.
The error is "frm1.pas(8,32) Fatal: Can not find unit GeckoBrowser used by frm1. Check if package GeckoComponents is in the dependencies."
So what should I do next!! Please Help.
I am using windows and lazarus v1.2.6.

Unknown said...

Another Silly Question! Can we make this browser use plugins like flash, unity, etc. for viewing videos(as it is not playing dailymotion or youtube right now) and more add-ons like adblock plus etc. I just want to know about the possibility. If it is possible I gonna search and seek by my own and make it do so. You just say that is it possible or not.

Adnan Shameem said...

@Rehan Ullah
Nice to have you here.
Well, I have tested the part 2 instructions in my Lazarus 1.4.0. I had no problems running it.

From your error message, it seems that the GeckoComponents has not been installed properly. Please check that you have a "Gecko" tab in your components toolbar. I believe that if you re-install GeckoComponents (described in part 1) then the problem will be solved.

If I remember correctly, yes you can run flash inside your program. (I am not sure about unity though.) I roughly remember I followed this solution:
https://bitbucket.org/geckofx/geckofx-10.0/issue/20/how-to-use-flash-plugin-in-xulrunner

Unknown said...

@Adnan Shameem
Thank You, Yeah. Re-installation solved the problem. As far flash is concerned, I checked the page you have mentioned and I am going to check that soon.
Another question Please Answer. How to connect through this browser using a proxy server. I searched online but found nothing good. At one site I found code like:
Gecko.GeckoPreferences.User("network.proxy.http") = "89.248.189.83"
Gecko.GeckoPreferences.User("network.proxy.http_port") = CInt("8080")
Gecko.GeckoPreferences.User("network.proxy.type") = 1
but it is not working in lazarus saying that it can not identify geckopreferences.
Thank You Very Much;
Adnan Shameem.

Unknown said...

Hi, i'm a beginner with lazarus and i have an error when i compile the code.

Compile Project, Target: proj_gecko_test.exe: Exit code 1, Errors: 4
frm1.pas(49,3) Error: Identifier not found "Browser"
frm1.pas(54,3) Error: Identifier not found "Browser"
frm1.pas(59,3) Error: Identifier not found "Browser"
frm1.pas(70,3) Error: Identifier not found "Browser"

is there something i miss?
i tried downloading your source code and compile it with same result.

regards,
Ari

Adnan Shameem said...

@Ariotomo Satyo
Hello and welcome.
It seems that the Gecko component didn't get installed on your Lazarus. Please try to follow part 1 instructions to get it installed.

Regards.

Unknown said...

Program work perfect but I have problem. I can not download files (pdf) through this program. Help.

Luis said...

How to download files on browser,which code of I make? I must use TSaveDialog?

Adnan Shameem said...

@Luis Henrique Ruthes,
I think you can use Synapse to download files from the internet.
http://wiki.freepascal.org/Synapse

And yes, you can also use TSaveDialog to show where to download that file.

p. n. said...

Is possible to set a proxy in gecko? if yes... how?

Unknown said...

Hello,
Thanks for such an important tip. I followed the instructions and successfully created a browser. However, some pages do not open, the following message appears in the program:

Browser project raised exception class 'EGeckoHint' with the message: Chrome flags: FFE


In the 'CallbackInterfaces.pas' file at line 153:
Raise EGeckoHint.CreateFmt ('Attempt to create a new Chrome window but handler does not create a new one. Chrome flags:% 8X', [chromeFlags]);


I know it may be some component of Chorme, some plugin or add on, but I do not know what it is. Can you help me?

Adnan Shameem said...

@Leonardo Santos

This maybe an issue when the page tries to open a new window. I don't remember where but I saw some code for handling new window with the Gecko component.

Can you post the url in which this message appears?

Regards

Unknown said...

I search some term on this page (as in the example):

http://search.scielo.org/?q=algoritm&where=ORG

And the moment I try to access any link in the result, the error message appears.

Just like any other page functionality (like export, share, save etc ...)

Thanks for listening.

Unknown said...

Another thing ... I'm trying to create a button that can copy the text that I selected with the mouse in the browser. I thought about the copy and past from clipboard function, but it is not working because it does not seem to identify what I selected in the browser.

Unknown said...

Did you find anything about that problem in opening new windows?

 
Copyright 2013 LazPlanet
Carbon 12 Blogger template by Blogger Bits. Supported by Bloggermint