I wrote a nice Mandelbrot fractal explorer application. It is written using Delphi XE5 and use extended precision floating point calculation to be able to produce ultra high zoom images.
You can create bitmaps much larger than the screen size. This way you can have a high resolution printout.
The program use multithreading to take advantage for multicore CPU. Computation is quite fast.
You can download the utility from http://www.overbyte.be/arch/dump/MandelbrotExplorer.zip The zip file contains the executable as well as a number of color palette files (You can edit it with notepad if you like).
If there is some interest, I could write another blog article about the design of the application. Just add a comment to this blog post if you are interested.
The program make use of multithreading to compute several parts of the image simultaneously. This makes huge speed improvement for multicore CPU.
Also, GDI plus is used for graphic rendering which make the display very fast, even for ultra high bitmap resolution.
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
This article is available from http://francois-piette.blogspot.be
Showing posts with label codegear. Show all posts
Showing posts with label codegear. Show all posts
December 2, 2013
October 15, 2013
Where is that MenuItem displayed ?
I had the need to display a form when the user clicks on an item in a menu. Easy! But the form, which
is small had to be displayed where the MenuItem was displayed. After Googleling a little bit, I did not
find what I needed. So I wrote it. Here is the result.
In Delphi, a TMenuItem has no property to tell where is has been displayed. I had to find how to get
that information. Among TMenuItem events is OnDrawItem. This event is called when the menu has
the OwnDraw property set to true. The corresponding event handler has a TCanvas property and a
TRect property. That was enough. A TCanvas has a handle which can be used to get hand on the
window where it is drawn. And the rectangle gives the position and size of the menu item in that
window. I had everything I needed.
But wait! When the OwnerDraw property of a menu is set to true, all menu items having an
OnDrawItem event assigned are no more drawn. That was annoying because I want it to be drawn as
usual.
Looking at TMenuItem source code, I found that drawing an item is done by AdvancedDrawItem
method. So I had to call it. There was a problem: this is a protected method and I cannot call it from
my TForm1 class. I can’t? Not really.
A protected class member can only be accessed by the class itself or any derived class, or any class in
the same source code. This feature has been considered as a bug by OOP purist but it is “as
designed”. In recent Delphi version, there is now a new “strict” keyword which restrict the
accessibility like OOP purist want it. This isn’t used for AdvancedDrawItem. To access a protected
method, you just have to declare a class deriving from the target class without changing anything. If
that declaration is located in the same source file as you code that need to call it, it can be called.
That empty class is declared like this:
The final code is the following:
ServiceMnuClick is where the secondary form is displayed by calling ShowModal. His position is first
set from the top-left corner of FServiceMnuRect (A member variable of TForm1).
ServiceMnuDrawItem has the responsibility of initializing FServiceMnuRect. Basically, it make use of
the canvas handle (which is a “HDC” or Handle to Device Context in Windows terminology) to get the
rectangle of the window behind the canvas. I used two Windows API calls: WindowFromDc and
GetWindowRect. WindowFromDc fetch the window handle given an HDC and GetWindowRect fetch
the bounding rectangle of a given window.
The bounding rectangle has to be fixed by the drawing rectangle passed to the OnDrawItem event
handler. This drawing rectangle is where the menu item is really drawn.
To call AdvancedDrawItem which drawn the menu item, I used the trick explained above. But there
was another issue: Normally AdvancedDrawItem will call OnDrawItem event handler, so we end up
with a recursive call and a nice stack overflow. So before calling AdvancedDrawItem, I save and clear
the event handler and restore it after.
Complete source file is as follow:
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
is small had to be displayed where the MenuItem was displayed. After Googleling a little bit, I did not
find what I needed. So I wrote it. Here is the result.
In Delphi, a TMenuItem has no property to tell where is has been displayed. I had to find how to get
that information. Among TMenuItem events is OnDrawItem. This event is called when the menu has
the OwnDraw property set to true. The corresponding event handler has a TCanvas property and a
TRect property. That was enough. A TCanvas has a handle which can be used to get hand on the
window where it is drawn. And the rectangle gives the position and size of the menu item in that
window. I had everything I needed.
But wait! When the OwnerDraw property of a menu is set to true, all menu items having an
OnDrawItem event assigned are no more drawn. That was annoying because I want it to be drawn as
usual.
Looking at TMenuItem source code, I found that drawing an item is done by AdvancedDrawItem
method. So I had to call it. There was a problem: this is a protected method and I cannot call it from
my TForm1 class. I can’t? Not really.
A protected class member can only be accessed by the class itself or any derived class, or any class in
the same source code. This feature has been considered as a bug by OOP purist but it is “as
designed”. In recent Delphi version, there is now a new “strict” keyword which restrict the
accessibility like OOP purist want it. This isn’t used for AdvancedDrawItem. To access a protected
method, you just have to declare a class deriving from the target class without changing anything. If
that declaration is located in the same source file as you code that need to call it, it can be called.
That empty class is declared like this:
THackMenuItem = class(TMenuItem)
// Intentionally left empty
end;
The final code is the following:
procedure TForm1.ServiceMnuClick(Sender: TObject);
begin
Form2.Top := FServiceMnuRect.Top;
Form2.Left := FServiceMnuRect.Left;
Form2.ShowModal;
end;
procedure TForm1.ServiceMnuDrawItem(
Sender : TObject;
ACanvas : TCanvas;
ARect : TRect;
Selected : Boolean);
var
State : TOwnerDrawState;
MenuItem : TMenuItem;
Handler : TMenuDrawItemEvent;
begin
MenuItem := Sender as TMenuItem;
GetWindowRect(WindowFromDc(ACanvas.Handle), FServiceMnuRect);
FServiceMnuRect.Top := FServiceMnuRect.Top + ARect.Top;
Handler := MenuItem.OnDrawItem;
try
MenuItem.OnDrawItem := nil;
if Selected then
State := [odSelected]
else
State := [];
THackMenuItem(MenuItem).AdvancedDrawItem(ACanvas, ARect, State, FALSE);
finally
MenuItem.OnDrawItem := Handler;
end;
end;
ServiceMnuClick is where the secondary form is displayed by calling ShowModal. His position is first
set from the top-left corner of FServiceMnuRect (A member variable of TForm1).
ServiceMnuDrawItem has the responsibility of initializing FServiceMnuRect. Basically, it make use of
the canvas handle (which is a “HDC” or Handle to Device Context in Windows terminology) to get the
rectangle of the window behind the canvas. I used two Windows API calls: WindowFromDc and
GetWindowRect. WindowFromDc fetch the window handle given an HDC and GetWindowRect fetch
the bounding rectangle of a given window.
The bounding rectangle has to be fixed by the drawing rectangle passed to the OnDrawItem event
handler. This drawing rectangle is where the menu item is really drawn.
To call AdvancedDrawItem which drawn the menu item, I used the trick explained above. But there
was another issue: Normally AdvancedDrawItem will call OnDrawItem event handler, so we end up
with a recursive call and a nice stack overflow. So before calling AdvancedDrawItem, I save and clear
the event handler and restore it after.
Complete source file is as follow:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, Menus, Unit2;
type
THackMenuItem = class(TMenuItem)
// Intentionally left empty
end;
TForm1 = class(TForm)
MainMenu1: TMainMenu;
FileMnu: TMenuItem;
OptionsMnu: TMenuItem;
BrolMnu: TMenuItem;
N1: TMenuItem;
ExitMnu: TMenuItem;
ServiceMnu: TMenuItem;
TestMnu: TMenuItem;
procedure ServiceMnuClick(Sender: TObject);
procedure ServiceMnuDrawItem(Sender: TObject; ACanvas: TCanvas; ARect: TRect;
Selected: Boolean);
private
FServiceMnuRect : TRect;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.ServiceMnuClick(Sender: TObject);
begin
Form2.Top := FServiceMnuRect.Top;
Form2.Left := FServiceMnuRect.Left;
Form2.ShowModal;
end;
procedure TForm1.ServiceMnuDrawItem(
Sender : TObject;
ACanvas : TCanvas;
ARect : TRect;
Selected : Boolean);
var
State : TOwnerDrawState;
MenuItem : TMenuItem;
Handler : TMenuDrawItemEvent;
begin
MenuItem := Sender as TMenuItem;
GetWindowRect(WindowFromDc(ACanvas.Handle), FServiceMnuRect);
FServiceMnuRect.Top := FServiceMnuRect.Top + ARect.Top;
Handler := MenuItem.OnDrawItem;
try
MenuItem.OnDrawItem := nil;
if Selected then
State := [odSelected]
else
State := [];
THackMenuItem(MenuItem).AdvancedDrawItem(ACanvas, ARect, State, FALSE);
finally
MenuItem.OnDrawItem := Handler;
end;
end;
end.
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
August 8, 2013
RAD Studio Mobile Roadmap updated
Embarcadero updated his RAD Studio Mobile Roadmap. This concern Delphi and C++ Builder for Android and iOS.
- NDK based Android Platform support (Gingerbread, ICS and Jelly Bean)
- FireMonkey mobile Framework for Android
- Mobile designer for Android
- iOS 7 support
- Mobile sensor support for Android (GPS, Camera, Gyro, Accelerometer)
- Advertizing and payment for Android and iOS
- Native Android UX w/Native UI styles
- iOS 7 UX w/Native UI style
- High performance iOS and Android mobiel DACs for popular databases
- Interbase Lite and IBtoGo for Android support
- Datasnap middleware mobile enterprise enhancements
- Cross Platform desktop/tablet/phone
- Automatic retina/Hi-Res display handling
- Integrated Android Design, development, simulation, debugging and deployment
- Rest client Framework and mobile BaaS support
- Ad Hoc and AppStore deployment
- C++ Builder for Android: estimated winter 2013
Source: http://edn.embarcadero.com/article/42544
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
What is in development:
- Integrated C++ and Delphi Native ARM toolchains for iOS and Android (compiler, linker, debugger)- NDK based Android Platform support (Gingerbread, ICS and Jelly Bean)
- FireMonkey mobile Framework for Android
- Mobile designer for Android
- iOS 7 support
- Mobile sensor support for Android (GPS, Camera, Gyro, Accelerometer)
- Advertizing and payment for Android and iOS
- Native Android UX w/Native UI styles
- iOS 7 UX w/Native UI style
- High performance iOS and Android mobiel DACs for popular databases
- Interbase Lite and IBtoGo for Android support
- Datasnap middleware mobile enterprise enhancements
- Cross Platform desktop/tablet/phone
- Automatic retina/Hi-Res display handling
- Integrated Android Design, development, simulation, debugging and deployment
- Rest client Framework and mobile BaaS support
- Ad Hoc and AppStore deployment
Roadmap:
- Delphi for Android: estimated fall 2013- C++ Builder for Android: estimated winter 2013
Source: http://edn.embarcadero.com/article/42544
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
July 29, 2013
Breakpoint not honored while debugging a DLL
Using Delphi, it is very easy to debug a DLL having it called from a host application. This host application can be written using Delphi or any other language.
To debug your DLL in the context for a host application, just add the host application into the corresponding field in Delphi / Menu / Run / Parameters. Then run your project as usual. Delphi will not load the DLL but the host application and intercept the DLL when it is loaded by the host application. It will then honor any breakpoint you've set.
Easy? Yes, it is easy but sometimes, there is a glitch. For some reasons, Delphi won't honor your breakpoints. Among the reasons is that you forgot to use the debug build, or you forgot to enable debugging in the DLL project options.
Sometimes, there is another reason. It made me lose a lot of time playing with all options. Sometimes, I thought that I had found the right option. But later the breakpoints were still not honored.
Finally I found the correct solution. Why it works, I don't know. But so far it worked for me. The solution is: Set option "Include remote debug symbols" to true (Delphi compiler / Linking).
This was not obvious since I didn't used the remote debugger. Anyway, it works!
When option "Include remote debug symbols" is used, Delphi creates one more file with rsm extension in the same directory as the executable. It looks like the debugger always find the symbols there.
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
To debug your DLL in the context for a host application, just add the host application into the corresponding field in Delphi / Menu / Run / Parameters. Then run your project as usual. Delphi will not load the DLL but the host application and intercept the DLL when it is loaded by the host application. It will then honor any breakpoint you've set.
Easy? Yes, it is easy but sometimes, there is a glitch. For some reasons, Delphi won't honor your breakpoints. Among the reasons is that you forgot to use the debug build, or you forgot to enable debugging in the DLL project options.
Sometimes, there is another reason. It made me lose a lot of time playing with all options. Sometimes, I thought that I had found the right option. But later the breakpoints were still not honored.
Finally I found the correct solution. Why it works, I don't know. But so far it worked for me. The solution is: Set option "Include remote debug symbols" to true (Delphi compiler / Linking).
This was not obvious since I didn't used the remote debugger. Anyway, it works!
When option "Include remote debug symbols" is used, Delphi creates one more file with rsm extension in the same directory as the executable. It looks like the debugger always find the symbols there.
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
July 27, 2013
Using RTTI to convert record to/from string
Delphi RTTI can be easily used to convert a record (Or a class by the way) to a string representation without taking care of how the record is changed during software maintenance.
RTTI has a set of methods to handle metadata collected by the compiler at compile time. For example, you can iterate thru all fields of a record to find out his name, data type and get or set his value. I used those methods as a simple way to marshal record values between two different processes communicating using sockets.
To simplify my original problem without removing features, I designed a very simple sample program demonstrating the RTTI usage. This simple program simply adds two methods “ToString” and “FromString” to a record. Instead of hardcoding each field in the conversion, I used RTTI to iterate all fields.
Basically, to use RTTI with records, you need the following data types: TRttiContext, TRttiRecordType and TRttiField.
Assuming we handle a record type name “THdr”, the simplest code looks like this:
AContext := TRttiContext.Create;
try
ARecord := AContext.GetType(TypeInfo(THdr)).AsRecord;
for AField in ARecord.GetFields do begin
AFldName := AField.Name;
end;
finally
AContext.Free;
end;
As you can see, we have to create a TRttiContext instance. We then use his method “GetType” to get an instance of TRttiRecord related to the record we are handling. Finally we can iterate thru all fields using a for..in construct. Each field has a corresponding TRttiField instance which can be used to get field name, set or get his value and so on.
My sample code use a loop similar to the above twice: one for converting all fields to a string and one for extracting values from a string and assigning the values to the corresponding fields.
To achieve the goal, I used two support functions: “ParamByNameAsString” and “EscapeQuotes”. They are required to be able to store any fieldname=”fieldvalue” pair into a simple string. There is a simple escape mechanism so that the value can contain embedded double quote.
Here is the full source code:
unit RttiTestForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, Rtti, TypInfo, Vcl.StdCtrls;
type
THdr = record
Name : String;
Phrase : String;
ActionDate : TDateTime;
procedure FromString(const FromValue: String);
function ToString : String;
end;
TForm1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
// This will take an inut string having the format:
// name1=value1;name2=value2;...
// Value may also be placed between double quotes if it contain spaces.
// Double quotes inside the value is escaped with a backslash
// Backslashes are double. See EscapeQuotes below.
function ParamByNameAsString(
const Params : String;
const ParamName : String;
var Status : Boolean;
const DefValue : String) : String;
var
I, J : Integer;
Ch : Char;
begin
Status := FALSE;
I := 1;
while I <= Length(Params) do begin
J := I;
while (I <= Length(Params)) and (Params[I] <> '=') do
Inc(I);
if I > Length(Params) then begin
Result := DefValue;
Exit; // Not found
end;
if SameText(ParamName, Trim(Copy(Params, J, I - J))) then begin
// Found parameter name, extract value
Inc(I); // Skip '='
if (I <= Length(Params)) and (Params[I] = '"') then begin
// Value is between double quotes
// Embedded double quotes and backslashes are prefixed
// by backslash
Status := TRUE;
Result := '';
Inc(I); // Skip starting delimiter
while I <= Length(Params) do begin
Ch := Params[I];
if Ch = '\' then begin
Inc(I); // Skip escape character
if I > Length(Params) then
break;
Ch := Params[I];
end
else if Ch = '"' then
break;
Result := Result + Ch;
Inc(I);
end;
end
else begin
// Value is up to semicolon or end of string
J := I;
while (I <= Length(Params)) and (Params[I] <> ';') do
Inc(I);
Result := Copy(Params, J, I - J);
Status := TRUE;
end;
Exit;
end;
// Not good parameter name, skip to next
Inc(I); // Skip '='
if (I <= Length(Params)) and (Params[I] = '"') then begin
Inc(I); // Skip starting delimiter
while I <= Length(Params) do begin
Ch := Params[I];
if Ch = '\' then begin
Inc(I); // Skip escape character
if I > Length(Params) then
break;
end
else if Ch = '"' then
break;
Inc(I);
end;
Inc(I); // Skip ending delimiter
end;
// Param ends with ';'
while (I <= Length(Params)) and (Params[I] <> ';') do
Inc(I);
Inc(I); // Skip semicolon
end;
Result := DefValue;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function EscapeQuotes(const S: String) : String;
begin
// Easy but not best performance
Result := StringReplace(S, '\', '\\', [rfReplaceAll]);
Result := StringReplace(Result, '"', '\"', [rfReplaceAll]);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure THdr.FromString(const FromValue: String);
var
Status : Boolean;
Value : String;
AValue : TValue;
AContext : TRttiContext;
ARecord : TRttiRecordType;
AField : TRttiField;
AFldName : String;
begin
if FromValue = '' then
Exit;
// We use RTTI to iterate thru all fields of THdr and use each field name
// to get field value from metadata string and then set value into Hdr.
AContext := TRttiContext.Create;
try
ARecord := AContext.GetType(TypeInfo(THdr)).AsRecord;
for AField in ARecord.GetFields do begin
AFldName := AField.Name;
Value := ParamByNameAsString(FromValue, AFldName, Status, '0');
if Status then begin
try
case AField.FieldType.TypeKind of
tkFloat: // Also for TDateTime !
begin
if Pos('/', Value) >= 1 then
AValue := StrToDateTime(Value)
else
AValue := StrToFloat(Value);
AField.SetValue(@Self, AValue);
end;
tkInteger:
begin
AValue := StrToInt(Value);
AField.SetValue(@Self, AValue);
end;
tkUString:
begin
AValue := Value;
AField.SetValue(@Self, AValue);
end;
// You should add other types as well
end;
except
// Ignore any exception here. Likely to be caused by
// invalid value format
end;
end
else begin
// Do whatever you need if the string lacks a field
// For example clear the field, or just do nothing
end;
end;
finally
AContext.Free;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function THdr.ToString: String;
var
AContext : TRttiContext;
AField : TRttiField;
ARecord : TRttiRecordType;
AFldName : String;
AValue : TValue;
begin
Result := '';
AContext := TRttiContext.Create;
try
ARecord := AContext.GetType(TypeInfo(THDR)).AsRecord;
for AField in ARecord.GetFields do begin
AFldName := AField.Name;
AValue := AField.GetValue(@Self);
Result := Result + AFldName + '="' +
EscapeQuotes(AValue.ToString) + '";';
end;
finally
AContext.Free;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TForm1.Button1Click(Sender: TObject);
var
Hdr1 : THdr;
Hdr2 : THdr;
Buf : String;
begin
Hdr1.Name := 'Francois Piette';
Hdr1.Phrase := 'I said "\Hello\"';
Hdr1.ActionDate := Now;
Memo1.Lines.Add(Hdr1.Phrase);
Buf := Hdr1.ToString;
Memo1.Lines.Add(Buf);
Hdr2.FromString(Buf);
Memo1.Lines.Add(Hdr2.Phrase);
Memo1.Lines.Add(Hdr2.ToString);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
end.
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
July 20, 2013
Creating an AVI file from bitmaps using Delphi
It is quite easy to create an AVI file from a number of individual bitmaps. The AVI file maybe compressed on the fly using any installed codec. An example application would be creating a video from computer generated images such as 3D view or discrete bitmaps such as those generated from a Mandelbrot fractal zoom in. Each zoom step produces a new bitmap. Taking all bitmap into an AVI makes a nice movie showing the zoom into the fractal deep space!
Windows has two API to handle AVI files: an old API named “Video For Windows” (VfW) and a new API named “DirectShow”. The old API is the easiest to use for the task.
A guy named Gopalakrishna Palemat wrote a Visual Studio C++ application to demonstrate that use of VfW API. I have ported his code to Delphi. I have slightly updated his code to fix shortcomings and make it “the Delphi way”.
The original article can be found at http://gopalakrishna.palem.in/createmovie.html
My Delphi source code can be found at http://www.overbyte.be/eng/blog_source_Code.html
I will speak only about my source code from now.
All the AVI creation code has been moved into a component named “TAviFromBitmaps”. This class handle everything required with VfW API to create an AVI movie, compressed or not. You just has to create an instance of the class and call his AppendNewFrame method to add a bitmap at the end of the already added bitmaps. The result is an AVI file.
As a simple sample, I wrote a procedure which generate an AVI file made of 25 bitmaps generated on the fly. Each bitmap contain the text “Delphi rocks!” and the number of the frame. This is for simple demo. As I said above, you could as well produce bitmap with a 3D scene viewed from a moving point of view, or a zoom into a Mandelbrot fractal.
Compression of the AVI is very important. Without compression, your AVI file will be quickly very large. VfW API is able to compress with about any algorithm. It is enough to install a “codec” into Windows. I like the XVID codec because it produces MPEG-4 video with high quality and high compression. Xvid is an open-source research project focusing on video compression and is a collaborative development effort (http://www.xvid.org). All code is released under the terms of GNU GPL license.
To use XVID compressor, download the setup from http://www.xvid.org and install it. Microsoft Media Player is able to decode XVID compressed movie without installing xvid software because the result is a standard MPEG-4 file.
Each codec is identified by a four character code name “FourCC”. VfW API has a function to transform those four characters codes into an integer value suitable for the rest of the API. This is why you see “MKFOURCC(‘x’, ‘v’, ‘I’, ‘d’)” in the above code.
Of course compression comes with a price: image quality is not the same as the original. So in some applications, you may want to avoid compression. This is done by selecting a compressor of type “MKFOURCC(‘D’, ‘I’, ‘B’, ‘ ’)” (Fourth character is a space).
Download source code from: http://www.overbyte.be/eng/blog_source_code.html
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
Windows has two API to handle AVI files: an old API named “Video For Windows” (VfW) and a new API named “DirectShow”. The old API is the easiest to use for the task.
A guy named Gopalakrishna Palemat wrote a Visual Studio C++ application to demonstrate that use of VfW API. I have ported his code to Delphi. I have slightly updated his code to fix shortcomings and make it “the Delphi way”.
The original article can be found at http://gopalakrishna.palem.in/createmovie.html
My Delphi source code can be found at http://www.overbyte.be/eng/blog_source_Code.html
I will speak only about my source code from now.
All the AVI creation code has been moved into a component named “TAviFromBitmaps”. This class handle everything required with VfW API to create an AVI movie, compressed or not. You just has to create an instance of the class and call his AppendNewFrame method to add a bitmap at the end of the already added bitmaps. The result is an AVI file.
As a simple sample, I wrote a procedure which generate an AVI file made of 25 bitmaps generated on the fly. Each bitmap contain the text “Delphi rocks!” and the number of the frame. This is for simple demo. As I said above, you could as well produce bitmap with a 3D scene viewed from a moving point of view, or a zoom into a Mandelbrot fractal.
procedure TCAviFileForm.GenerateAvi
var
I : Integer;
Y : Integer;
H : Integer;
BackBitmap : Graphics.TBitMap;
Avi : TAviFromBitmaps;
begin
BackBitmap := Graphics.TBitMap.Create;
BackBitmap.Width := 320;
BackBitmap.Height := 240;
BackBitmap.PixelFormat := TPixelFormat.pf32bit;
Avi := TAviFromBitmaps.CreateAviFile(
nil, 'output.avi',
//MKFOURCC('x', 'v', 'i', 'd'),// XVID (MPEG-4) compression
MKFOURCC('D', 'I', 'B', ' '), // No compression
2, 1); // 2 frames per second
// First, add a blank frame
Avi.AppendNewFrame(BackBitmap.Handle);
// Then add frames with text
BackBitmap.Canvas.Font.Size := 20;
H := (BackBitmap.Canvas.TextHeight('I') * 15) div 10;
Y := (BackBitmap.Height div 2) - H;
BackBitmap.Canvas.TextOut(10, Y, 'Delphi rocks!');
Y := (BackBitmap.Height div 2);
for I := 1 to 25 do begin
BackBitmap.Canvas.TextOut(10, Y, IntToStr(I));
Avi.AppendNewFrame(BackBitmap.Handle);
end;
// Finally, add two blank frame
// (MediaPlayer doesn't show the last two frames)
BackBitmap.Canvas.FillRect(Rect(0, 0,
BackBitmap.Width, BackBitmap.Height));
Avi.AppendNewFrame(BackBitmap.Handle);
Avi.AppendNewFrame(BackBitmap.Handle);
FreeAndNil(Avi);
FreeAndNil(BackBitmap);
end;
Compression of the AVI is very important. Without compression, your AVI file will be quickly very large. VfW API is able to compress with about any algorithm. It is enough to install a “codec” into Windows. I like the XVID codec because it produces MPEG-4 video with high quality and high compression. Xvid is an open-source research project focusing on video compression and is a collaborative development effort (http://www.xvid.org). All code is released under the terms of GNU GPL license.
To use XVID compressor, download the setup from http://www.xvid.org and install it. Microsoft Media Player is able to decode XVID compressed movie without installing xvid software because the result is a standard MPEG-4 file.
Each codec is identified by a four character code name “FourCC”. VfW API has a function to transform those four characters codes into an integer value suitable for the rest of the API. This is why you see “MKFOURCC(‘x’, ‘v’, ‘I’, ‘d’)” in the above code.
Of course compression comes with a price: image quality is not the same as the original. So in some applications, you may want to avoid compression. This is done by selecting a compressor of type “MKFOURCC(‘D’, ‘I’, ‘B’, ‘ ’)” (Fourth character is a space).
Download source code from: http://www.overbyte.be/eng/blog_source_code.html
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
Labels:
API,
bitmap,
borland,
codegear,
delphi,
DirectShow,
DIVX,
embarcadero,
Movie,
Mutlimedia,
VFW,
XVID
June 29, 2013
Delayed loading of a DLL
The delayed directive can be used when declaring a function exported from a DLL so that the DLL is not loaded before the function is called.
This is very useful because you may first check if the conditions are met to call the DLL before actually loading the DLL, for example check if the DLL is located where it should.
If you statically load a DLL (the default), then your application won’t start if the DLL is not present. You could as well dynamically load the DLL (LoadLibrary) to avoid this issue, but it is a little bit more work for you to declare function pointer and the get each function address (GetProcAddress) by code. Using delayed directive is much easier. Just add delayed keyword in each function declaration and suddenly a static DLL is loaded the first time one of his functions is called. If you never call a DLL’s function, then the DLL is not loaded at all.
A declaration looks like this:
This directive exists since Delphi 2010. There is valuable documentation on Embarcadero DocWiki at this URL: http://docwiki.embarcadero.com/RADStudio/XE4/en/Libraries_and_Packages#Delayed_Loading And you can find some code sample here: http://docwiki.embarcadero.com/CodeExamples/XE4/en/DelayedLoading_(Delphi)
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
This is very useful because you may first check if the conditions are met to call the DLL before actually loading the DLL, for example check if the DLL is located where it should.
If you statically load a DLL (the default), then your application won’t start if the DLL is not present. You could as well dynamically load the DLL (LoadLibrary) to avoid this issue, but it is a little bit more work for you to declare function pointer and the get each function address (GetProcAddress) by code. Using delayed directive is much easier. Just add delayed keyword in each function declaration and suddenly a static DLL is loaded the first time one of his functions is called. If you never call a DLL’s function, then the DLL is not loaded at all.
A declaration looks like this:
function GetSomething: Integer; external 'somelibrary.dll' delayed;
This directive exists since Delphi 2010. There is valuable documentation on Embarcadero DocWiki at this URL: http://docwiki.embarcadero.com/RADStudio/XE4/en/Libraries_and_Packages#Delayed_Loading And you can find some code sample here: http://docwiki.embarcadero.com/CodeExamples/XE4/en/DelayedLoading_(Delphi)
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
June 17, 2013
Drag And Drop from Windows Explorer
This article presents the required code to handle drag& drop of images from Windows Explorer to your Delphi application. The demo code shows how to drop images on a TListView and to drag & drop from TListView to a TImage.
The code is fairly basic and made so that it can be clearly understood and applied to other types of controls.
Drag & Drop from windows Explorer is handled by an application by registering a window handle along with an instance of an IDropTarget interface.
To make to code easy to reuse, I encapsulated the IDropTarget implementation into a class named TDropTarget and expose the features the Delphi way: using event.
To allow Drag & Drop from Windows Explorer to one of your form, you have to create an instance of TDropTarget and call his Register method passing the form’s handle. Of course you have to assign the events to handler in your form. The events handle all drag and drop operation:
DropAllowed event is called once when the dragged files are entering the area of the registered window. The event handler must set the “Allowed” var argument to TRUE if dropping the file(s) is allowed at the given point.
DragOver event is called as mouse move above the registered window. The event handler must set the “Allowed” var argument to TRUE if dropping the file(s) is allowed at the given point.
Drop event is called when the user drops the files.
DragLeave event is called when the dragged files leave the registered window area.
I could have made a component of TDropTarget but I didn’t. It is a simple object deriving from Object. As it implements an interface IDroptarget, beside the methods of this interface, the object also has to handle _AddRef, _Release and QueryInterface methods which exist in all interfaces. Here we use TObject life cycle, so those methods are simply do-nothing methods.
The code required in your form include creating the object in the form’s contructor (or FormCreate event), assign the event handlers. And of course free the object instance in the destructor:
Register and Revoke should be called when the window handle is created or destroyed. For that, we have to override CreateWnd and DestroyWnd.
The demo application uses a TListView in vsList view mode and a TImage. The ListView accept the dropped images from Windows Explorer while TImage accepte images dropped from TListView. It is a good exercise for you to make TImage accept image also from Windows Explorer.
The demo application doesn’t show image in real size in TListView. Rather, it creates a thumbnail which is displayed in the list view. The thumbnails are stored on disk in the same folder as the original image and are only created if it doesn’t exist yet, or if the original image has been modified. Storing the thumbnail on disk could be a problem in some application because it requires write permission. In my application (Well the application I extracted this code from), it is an advantage because the images are very large and it takes time to create the thumbnails. Keeping the thumbnails on disk improve performance.
Thumbnails are created using GDI+ (See my other blog article about it: http://francois-piette.blogspot.be/2013/05/opensource-gdi-library.html). The code is really easy:
A last note about the demo application: I used custom draw of the ListView items so that it looks exactly how I require it. All list view items are represented by a class named TImageListViewItem. I have selected this representation because in the real application this demo is extracted from, there is a lot of information about each image. The class is really handy to hold the information and the processing related to it.
Here after is the complete source code. There are mainly two files: DropHanlder.pas and DragDropMain.pas. You can also download a zip file with the complete project. See my website at: http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
Download source code from: http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
The code is fairly basic and made so that it can be clearly understood and applied to other types of controls.
Drag & Drop from windows Explorer is handled by an application by registering a window handle along with an instance of an IDropTarget interface.
To make to code easy to reuse, I encapsulated the IDropTarget implementation into a class named TDropTarget and expose the features the Delphi way: using event.
To allow Drag & Drop from Windows Explorer to one of your form, you have to create an instance of TDropTarget and call his Register method passing the form’s handle. Of course you have to assign the events to handler in your form. The events handle all drag and drop operation:
DropAllowed event is called once when the dragged files are entering the area of the registered window. The event handler must set the “Allowed” var argument to TRUE if dropping the file(s) is allowed at the given point.
DragOver event is called as mouse move above the registered window. The event handler must set the “Allowed” var argument to TRUE if dropping the file(s) is allowed at the given point.
Drop event is called when the user drops the files.
DragLeave event is called when the dragged files leave the registered window area.
I could have made a component of TDropTarget but I didn’t. It is a simple object deriving from Object. As it implements an interface IDroptarget, beside the methods of this interface, the object also has to handle _AddRef, _Release and QueryInterface methods which exist in all interfaces. Here we use TObject life cycle, so those methods are simply do-nothing methods.
The code required in your form include creating the object in the form’s contructor (or FormCreate event), assign the event handlers. And of course free the object instance in the destructor:
constructor TDragDropMainForm.Create(AOwner: TComponent);
begin
FDropTarget := TDropTarget.Create;
FDropTarget.OnDropAllowed := ImageDropAllowedHandler;
FDropTarget.OnDrop := ImageDropHandler;
FDropTarget.OnDragOver := ImageDragOverHandler;
inherited Create(AOwner);
end;
destructor TDragDropMainForm.Destroy;
begin
FreeandNil(FDropTarget);
inherited;
end;
Register and Revoke should be called when the window handle is created or destroyed. For that, we have to override CreateWnd and DestroyWnd.
procedure TDragDropMainForm.CreateWnd;
begin
inherited CreateWnd;
if Assigned(FDropTarget) then
FDropTarget.Register(Handle);
end;
procedure TDragDropMainForm.DestroyWnd;
begin
inherited DestroyWnd;
if Assigned(FDropTarget) then
FDropTarget.Revoke;
end;
The demo application uses a TListView in vsList view mode and a TImage. The ListView accept the dropped images from Windows Explorer while TImage accepte images dropped from TListView. It is a good exercise for you to make TImage accept image also from Windows Explorer.
The demo application doesn’t show image in real size in TListView. Rather, it creates a thumbnail which is displayed in the list view. The thumbnails are stored on disk in the same folder as the original image and are only created if it doesn’t exist yet, or if the original image has been modified. Storing the thumbnail on disk could be a problem in some application because it requires write permission. In my application (Well the application I extracted this code from), it is an advantage because the images are very large and it takes time to create the thumbnails. Keeping the thumbnails on disk improve performance.
Thumbnails are created using GDI+ (See my other blog article about it: http://francois-piette.blogspot.be/2013/05/opensource-gdi-library.html). The code is really easy:
Image := TGPImage.Create(AFileName);
Thumbnail := Image.GetThumbnailImage(ThWidth, ThHeight, nil, nil);
Quality := 50;
Params := TGPEncoderParameters.Create;
Params.Add(EncoderQuality, Quality);
Thumbnail.Save(AThumbFileName, TGPImageFormat.Jpeg, Params);
A last note about the demo application: I used custom draw of the ListView items so that it looks exactly how I require it. All list view items are represented by a class named TImageListViewItem. I have selected this representation because in the real application this demo is extracted from, there is a lot of information about each image. The class is really handy to hold the information and the processing related to it.
Here after is the complete source code. There are mainly two files: DropHanlder.pas and DragDropMain.pas. You can also download a zip file with the complete project. See my website at: http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
DropHandler.pas
unit DropHandler;
interface
uses
Windows, Types, Classes, SysUtils, ShellAPI, ActiveX;
type
TStringArray = array of String;
TDropAllowedEvent = procedure (Sender : TObject;
const FileNames : array of String;
const grfKeyState : Longint;
const pt : TPoint;
var Allowed : Boolean)
of object;
TDragOverEvent = procedure (Sender : TObject;
const grfKeyState : Longint;
const pt : TPoint;
var Allowed : Boolean)
of object;
TDropEvent = procedure (Sender : TObject;
const DropPoint : TPoint;
const FileNames : array of String)
of object;
TDropTarget = class(TObject, IDropTarget)
private
FRegisteredHandle : HWND;
FDropAllowed : Boolean;
FOnDropAllowed : TDropAllowedEvent;
FOnDrop : TDropEvent;
FOnDragOver : TDragOverEvent;
FOnDragLeave : TNotifyEvent;
procedure GetFileNames(const dataObj : IDataObject;
var FileNames : TStringArray);
function DragEnter(const dataObj : IDataObject;
grfKeyState : Integer;
pt : TPoint;
var dwEffect : Integer): HResult; stdcall;
function DragOver(grfKeyState : Longint;
pt : TPoint;
var dwEffect : Longint): HResult; stdcall;
function DragLeave: HResult; stdcall;
function Drop(const dataObj : IDataObject;
grfKeyState : Longint;
pt : TPoint;
var dwEffect : Longint): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
public
destructor Destroy; override;
// Call Register() with a window handle so that that window starts
// accepting dropped files. Events will then be generated.
function Register(AHandle : HWnd) : HResult;
// Stop accepting files dropped on the registered window.
procedure Revoke;
// DropAllowed event is called once when the dragged files are
// entering the area of the registered window.
// The event handler must set the Allowed var argument to TRUE if
// dropping the file(s) is allowed at the given point
property OnDropAllowed : TDropAllowedEvent read FOnDropAllowed
write FOnDropAllowed;
// DragOver event is called as mouse move above the registered window
// The event handler must set the Allowed var argument to TRUE if
// dropping the file(s) is allowed at the given point
property OnDragOver : TDragOverEvent read FOnDragOver
write FOnDragOver;
// Drop event is called when the user drops the files.
property OnDrop : TDropEvent read FOnDrop
write FOnDrop;
// DragLeave event is called when the dragged files leave the
// registered window area.
property OnDragLeave : TNotifyEvent read FOnDragLeave
write FOnDragLeave;
end;
implementation
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget.Register(AHandle: HWnd): HResult;
begin
if FRegisteredHandle = AHandle then begin
Result := S_OK;
Exit;
end;
if FRegisteredHandle <> 0 then
Revoke;
FRegisteredHandle := AHandle;
Result := ActiveX.RegisterDragDrop(FRegisteredHandle, Self);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDropTarget.Revoke;
begin
if FRegisteredHandle <> 0 then begin
ActiveX.RevokeDragDrop(FRegisteredHandle);
FRegisteredHandle := 0;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
destructor TDropTarget.Destroy;
begin
Revoke;
inherited Destroy;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDropTarget.GetFileNames(
const dataObj : IDataObject;
var FileNames : TStringArray);
var
I : Integer;
FormatetcIn : TFormatEtc;
Medium : TStgMedium;
DropHandle : HDROP;
begin
FileNames := nil;
FormatetcIn.cfFormat := CF_HDROP;
FormatetcIn.ptd := nil;
FormatetcIn.dwAspect := DVASPECT_CONTENT;
FormatetcIn.lindex := -1;
FormatetcIn.tymed := TYMED_HGLOBAL;
if dataObj.GetData(FormatetcIn, Medium) = S_OK then begin
DropHandle := HDROP(Medium.hGlobal);
SetLength(FileNames, DragQueryFile(DropHandle, $FFFFFFFF, nil, 0));
for I := 0 to high(FileNames) do begin
SetLength(FileNames[I], DragQueryFile(DropHandle, I, nil, 0));
DragQueryFile(DropHandle, I, @FileNames[I][1],
Length(FileNames[I]) + 1);
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget.DragEnter(
const dataObj : IDataObject;
grfKeyState : Integer;
pt : TPoint;
var dwEffect : Integer): HResult;
var
FileNames: TStringArray;
begin
Result := S_OK;
try
GetFileNames(dataObj, FileNames);
if (Length(FileNames) > 0) and Assigned(FOnDropAllowed) then begin
FDropAllowed := FALSE;
FOnDropAllowed(Self, FileNames, grfKeyState, pt, FDropAllowed);
end;
if FDropAllowed then
dwEffect := DROPEFFECT_COPY
else
dwEffect := DROPEFFECT_NONE;
except
Result := E_UNEXPECTED;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget.DragLeave: HResult;
begin
if Assigned(FOnDragLeave) then
FOnDragLeave(Self);
Result := S_OK;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget.DragOver(
grfKeyState : Integer;
pt : TPoint;
var dwEffect : Integer): HResult;
begin
Result := S_OK;
try
if Assigned(FOnDragOver) then
FOnDragOver(Self, grfKeyState, pt, FDropAllowed);
if FDropAllowed then
dwEffect := DROPEFFECT_COPY
else
dwEffect := DROPEFFECT_NONE;
except
Result := E_UNEXPECTED;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget.Drop(
const dataObj : IDataObject;
grfKeyState : Integer;
pt : TPoint;
var dwEffect : Integer): HResult;
var
FileNames: TStringArray;
begin
Result := S_OK;
try
GetFileNames(dataObj, FileNames);
if (Length(FileNames) > 0) and Assigned(FOnDrop) then
FOnDrop(Self, Pt, FileNames);
except
// Silently ignore any exception bacsue if required, they should
// be handled in OnDrop event handler.
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget._AddRef: Integer;
begin
// We don't use reference counting in this object
// We need _AddRef because RegisterDragDrop API call it
Result := 1;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDropTarget._Release: Integer;
begin
// We don't use reference counting in this object
// We need _Release because RevokeDragDrop API call it
Result := 1;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
end.
DragDropMain.pas
unit DragDropMain;
interface
uses
Windows, Messages, Types, SysUtils, Variants, Classes, Graphics,
StdCtrls, ExtCtrls, Controls, ComCtrls, CommCtrl, Forms, Dialogs,
Jpeg, ImgList,
GdiPlus,
DropHandler;
const
AtEndOfPipe = -1;
AtTopOfPipe = -2;
THUMBNAIL_SIZE = 64;
THUMBNAIL_MARGIN = 8;
// List of accepted image file extensions
Exts : array [0..3] of String = ('.jpg', '.png', '.bmp', '.tif');
type
TImageListViewItem = class
public
FileName : String;
Bitmap : TBitmap;
Data : TObject;
ThumbnailFileName : String;
constructor Create(const AFileName : String;
const AThumbnailFileName : String;
const AItem : TListItem;
const AWidth : Integer;
const AHeight : Integer);
destructor Destroy; override;
end;
TDragDropMainForm = class(TForm)
ListView1: TListView;
Splitter1: TSplitter;
Image1: TImage;
procedure ListView1CustomDrawItem(Sender : TCustomListView;
Item : TListItem;
State : TCustomDrawState;
var DefaultDraw : Boolean);
procedure ListView1Deletion(Sender : TObject;
Item : TListItem);
procedure ListView1MouseDown(Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X, Y : Integer);
procedure ListView1MouseMove(Sender: TObject;
Shift : TShiftState;
X, Y : Integer);
procedure ListView1MouseUp(Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X, Y : Integer);
private
FDropTarget : TDropTarget;
FMouseDownPt : TPoint;
FMouseMovePt : TPoint;
FMouseDownFlag : Boolean;
FDraggingImage : Boolean;
procedure ImageDragOverHandler(Sender : TObject;
const grfKeyState : Longint;
const pt : TPoint;
var Allowed : Boolean);
procedure ImageDropAllowedHandler(Sender : TObject;
const FileNames : array of string;
const GrfKeyState : Integer;
const Pt : TPoint;
var Allowed : Boolean);
procedure ImageDropHandler(Sender : TObject;
const DropPoint : TPoint;
const FileNames : array of string);
function DropImage(const AFileName : String;
XScreen : Integer;
YScreen : Integer): Boolean;
procedure CreateThumbnail(const AFileName : String;
var AThumbFileName : String);
function KnownExtension(const FileName : String): Boolean; overload;
function KnownExtension(const FileNames: array of string): Boolean; overload;
protected
procedure CreateWnd; override;
procedure DestroyWnd; override;
public
constructor Create(AOwner : TComponent); override;
destructor Destroy; override;
procedure AddImage(const FileName : String;
BeforeIndex : Integer);
procedure MoveImage(IFrom, ITo: Integer);
procedure RemoveImage(Index: Integer); overload;
function FindImage(const FileName: String): Integer;
function AppendImage(const FileName: String): Integer;
end;
function ReplaceThumb(const FileName : String) : String;
function ListViewMouseToItem(
Pt : TPoint;
LV : TListView;
var ColIndex : Integer): TListItem;
var
DragDropMainForm: TDragDropMainForm;
implementation
{$R *.dfm}
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ TDragDropMainForm }
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
constructor TDragDropMainForm.Create(AOwner: TComponent);
begin
FDropTarget := TDropTarget.Create;
FDropTarget.OnDropAllowed := ImageDropAllowedHandler;
FDropTarget.OnDrop := ImageDropHandler;
FDropTarget.OnDragOver := ImageDragOverHandler;
inherited Create(AOwner);
// To have TListView work correctly in vsList view mode, we must have
// at least one group, one column and a SmallImages image list.
ListView1.Groups.Clear;
ListView1.Groups.Add;
ListView1.Columns.Clear;
ListView1.Columns.Add;
// Height of displayed image is set by height of SmallImages
ListView1.SmallImages := TImageList.Create(Self);
ListView1.SmallImages.Height := THUMBNAIL_SIZE + 2 * THUMBNAIL_MARGIN;
// Width of displayed image is set by ListView_SetColumnWidth macro with
// column index set to zero.
ListView_SetColumnWidth(ListView1.Handle, 0, THUMBNAIL_SIZE + 2 * THUMBNAIL_MARGIN);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.CreateWnd;
begin
inherited CreateWnd;
if Assigned(FDropTarget) then
FDropTarget.Register(Handle);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
destructor TDragDropMainForm.Destroy;
begin
FreeandNil(FDropTarget);
inherited;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.DestroyWnd;
begin
inherited DestroyWnd;
if Assigned(FDropTarget) then
FDropTarget.Revoke;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ImageDragOverHandler(
Sender : TObject;
const grfKeyState : Longint;
const pt : TPoint;
var Allowed : Boolean);
begin
Allowed := TRUE;
if not PtInRect(ListView1.BoundsRect, ListView1.ScreenToClient(Pt)) then begin
Allowed := FALSE;
Exit;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ImageDropAllowedHandler(
Sender : TObject;
const FileNames : array of string;
const GrfKeyState : Integer;
const Pt : TPoint;
var Allowed : Boolean);
begin
Allowed := KnownExtension(FileNames);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ImageDropHandler(
Sender : TObject;
const DropPoint : TPoint;
const FileNames : array of string);
var
I : Integer;
begin
for I := 0 to High(FileNames) do
DropImage(ReplaceThumb(FileNames[I]), DropPoint.X, DropPoint.Y);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDragDropMainForm.DropImage(
const AFileName : String;
XScreen : Integer;
YScreen : Integer) : Boolean;
var
Pt : TPoint;
Item : TListItem;
ColIndex : Integer;
begin
Result := FALSE;
// First check if the extension is allowed
if not KnownExtension(AFileName) then begin
ShowMessage('Unacceptable file type (' +
ExtractFileExt(AFileName) + ')');
Exit;
end;
// Check if we already got the image
if FindImage(AFileName) >= 0 then begin
ShowMessage(AFileName + #10 + 'Already in the ListView, ignoring');
Exit;
end;
// Check if the drop point is inside the ListView
Pt := ListView1.ScreenToClient(Point(XScreen, YScreen));
if not PtInRect(ListView1.BoundsRect, Pt) then
Exit;
// Check if dropped on an existing item
Item := ListViewMouseToItem(Pt, ListView1, ColIndex);
if not Assigned(Item) then
AppendImage(AFileName) // Not on an item, add at the end
else
AddImage(AFileName, Item.Index); // Insert before the item
Result := TRUE;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ListView1CustomDrawItem(
Sender : TCustomListView;
Item : TListItem;
State : TCustomDrawState;
var DefaultDraw : Boolean);
var
Bitmap : TBitMap;
Rc1 : TRect;
Rc2 : TRect;
Rc3 : TRect;
ACanvas : TCanvas;
YOff : Integer;
XOff : Integer;
begin
ACanvas := Sender.Canvas;
Rc1 := Item.DisplayRect(drBounds);
if Assigned(Item.Data) then begin
Bitmap := TImageListViewItem(Item.Data).Bitmap;
// Center the bitmap
YOff := ((THUMBNAIL_SIZE - BitMap.Height) div 2) + THUMBNAIL_MARGIN;
XOff := ((THUMBNAIL_SIZE - Bitmap.Width) div 2) + THUMBNAIL_MARGIN;
ACanvas.Draw(Rc1.Left + 2 + XOff, Rc1.Top + 2 + YOff, Bitmap);
// Draw a double FrameRect around the image with a color depending
// on the status of the image
Rc2.Left := Rc1.Left + XOff;
Rc2.Top := Rc1.Top + YOff;
Rc2.Right := Rc1.Left + Bitmap.Width + 4 + XOff;
Rc2.Bottom := Rc1.Top + Bitmap.Height + 4 + YOff;
Rc3.Left := Rc1.Left + 1 + XOff;
Rc3.Top := Rc1.Top + 1 + YOff;
Rc3.Right := Rc1.Left + Bitmap.Width + 3 + XOff;
Rc3.Bottom := Rc1.Top + Bitmap.Height + 3 + YOff;
if cdsSelected in State then
ACanvas.Brush.Color := clBlue
else if cdsHot in State then
ACanvas.Brush.Color := clRed
else
ACanvas.Brush.Color := ListView1.Color;
ACanvas.FrameRect(Rc2);
ACanvas.FrameRect(Rc3);
DefaultDraw := FALSE;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ListView1Deletion(
Sender : TObject;
Item : TListItem);
begin
if Assigned(Item.Data) then
TObject(Item.Data).Free;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ListView1MouseDown(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X, Y : Integer);
begin
if ssLeft in Shift then begin
FMouseDownPt := Point(X, Y);
FMouseDownFlag := TRUE;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ListView1MouseMove(
Sender : TObject;
Shift : TShiftState;
X, Y : Integer);
var
Item : TListItem;
ColIndex : Integer;
begin
FMouseMovePt := Point(X, Y);
if not FMouseDownFlag then
Exit;
if not FDraggingImage then begin
Item := ListViewMouseToItem(FMouseDownPt, ListView1, ColIndex);
if Assigned(Item) then begin
FDraggingImage := TRUE;
Screen.Cursor := crDrag;
SetCaptureControl(ListView1);
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.ListView1MouseUp(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X, Y : Integer);
var
Pt : TPoint;
ItemFrom : TListItem;
ItemTo : TListItem;
ColIndex : Integer;
LV : TListView;
IFrom : Integer;
ITo : Integer;
FileName : String;
begin
FMouseDownFlag := FALSE;
if FDraggingImage then begin
FDraggingImage := FALSE;
Screen.Cursor := crDefault;
SetCaptureControl(nil);
LV := Sender as TListView;
ItemFrom := ListViewMouseToItem(FMouseDownPt, LV, ColIndex);
ItemTo := ListViewMouseToItem(Point(X, Y), LV, ColIndex);
IFrom := ItemFrom.Index;
FileName := TImageListViewItem(ItemFrom.Data).FileName;
if not FileExists(FileName) then begin
if Application.MessageBox(
PChar('File "' + FileName + '" doesn''t exist anymore' +
#10 + 'Remove from ListView ?'), 'WARNING',
MB_YESNO + MB_DEFBUTTON2) = IDYES then begin
RemoveImage(IFrom);
Exit;
end;
end;
if Assigned(ItemTo) then begin
// Drop inside of the pipe, move items around
if ItemTo <> ItemFrom then begin
ITo := ItemTo.Index;
MoveImage(IFrom, ITo);
end;
end
else begin
if PtInRect(LV.BoundsRect, Point(X, Y)) then begin
// Drop on the listview but not on an item, just move at the end
ITo := LV.Items.Count - 1;
MoveImage(IFrom, ITo);
end
else begin
// Drop outside of the ListView
// Check if within Image1
Pt := ListView1.ClientToScreen(Point(X, Y));
Pt := Image1.ScreenToClient(Pt);
if (Pt.X >= 0) and (Pt.X < Image1.Width) and
(Pt.Y >= 0) and (Pt.Y < Image1.Height) then begin
Image1.Picture.LoadFromFile(TImageListViewItem(ItemFrom.Data).FileName);
end;
end;
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.AddImage(
const FileName : String;
BeforeIndex : Integer); // Index of the item where to insert (before)
var
IFrom : Integer;
begin
IFrom := AppendImage(FileName);
if IFrom < 0 then
Exit; // Not found or already exist, not added
if BeforeIndex = AtTopOfPipe then
MoveImage(IFrom, 0)
else if (BeforeIndex >= 0) and (BeforeIndex < ListView1.Items.Count) then
MoveImage(IFrom, BeforeIndex);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
// Search if a give file already exists in list.
// Return -1 if not found
// Return item index if already in the list
function TDragDropMainForm.FindImage(const FileName: String): Integer;
begin
for Result := 0 to ListView1.Items.Count - 1 do begin
if SameText(FileName,
TImageListViewItem(ListView1.Items[Result].Data).FileName) then
Exit;
end;
Result := -1;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.MoveImage(
IFrom : Integer;
ITo : Integer);
var
Data : Pointer;
Capt : String;
I : Integer;
begin
if IFrom < ITo then begin
Data := ListView1.Items[IFrom].Data;
Capt := ListView1.Items[IFrom].Caption;
for I := IFrom to ITo - 1 do begin
ListView1.Items[I].Data := ListView1.Items[I + 1].Data;
ListView1.Items[I].Caption := ListView1.Items[I + 1].Caption;
TImageListViewItem(ListView1.Items[I].Data).Data := ListView1.Items[I];
end;
ListView1.Items[ITo].Data := Data;
ListView1.Items[ITo].Caption := Capt;
TImageListViewItem(ListView1.Items[ITo].Data).Data := ListView1.Items[ITo];
end
else begin
Data := ListView1.Items[IFrom].Data;
Capt := ListView1.Items[IFrom].Caption;
for I := IFrom downto ITo + 1 do begin
ListView1.Items[I].Data := ListView1.Items[I - 1].Data;
ListView1.Items[I].Caption := ListView1.Items[I - 1].Caption;
TImageListViewItem(ListView1.Items[I].Data).Data := ListView1.Items[I];
end;
ListView1.Items[ITo].Data := Data;
ListView1.Items[ITo].Caption := Caption;
TImageListViewItem(ListView1.Items[ITo].Data).Data := ListView1.Items[ITo];
end;
Windows.InvalidateRect(ListView1.Handle, nil, FALSE);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDragDropMainForm.KnownExtension(
const FileName : String) : Boolean;
var
Ext : String;
I : Integer;
begin
Result := FALSE;
Ext := ExtractFileExt(FileName);
for I := Low(Exts) to High(Exts) do begin
if SameText(Ext, Exts[I]) then begin
Result := TRUE;
Exit;
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDragDropMainForm.KnownExtension(
const FileNames : array of string) : Boolean;
var
I : Integer;
begin
Result := FALSE;
for I := Low(FileNames) to High(FileNames) do begin
if KnownExtension(FileNames[I]) then begin
Result := TRUE;
Exit;
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
// Given a filename which could be a thumbnail filename, return either the
// filename unchanged or the image which is represented by thumbnail
function ReplaceThumb(const FileName : String) : String;
const
ThSuffix = '.thumb.jpg';
var
S : String;
I : Integer;
begin
if not SameText(Copy(FileName, Length(FileName) - Length(ThSuffix) + 1, 200),
ThSuffix) then begin
Result := FileName;
Exit;
end;
S := Copy(FileName, 1, Length(FileName) - Length(ThSuffix));
for I := Low(Exts) to High(Exts) do begin
Result := S + Exts[I];
if FileExists(Result) then
Exit;
end;
Result := FileName;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
// ColIndex returns the column index, not the SubItem index.
function ListViewMouseToItem(
Pt : TPoint;
LV : TListView;
var ColIndex : Integer): TListItem;
var
Info : TLVHitTestInfo;
begin
// Pt := LV.ScreenToClient(Mouse.Cursorpos);
Result := LV.GetItemAt(Pt.X, Pt.Y);
if Assigned(Result) then
ColIndex := 0
else begin
FillChar(Info, SizeOf(Info), 0);
Info.Pt := Pt;
if LV.Perform(LVM_SUBITEMHITTEST, 0, LParam(@Info)) <> -1 then begin
Result := LV.Items[Info.iItem];
ColIndex := Info.iSubItem;
end;
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function TDragDropMainForm.AppendImage(
const FileName : String) : Integer;
var
Item : TListItem;
ThumbnailFileName : String;
begin
Result := -1;
if not FileExists(FileName) then
Exit;
if FindImage(FileName) >= 0 then
Exit; // Already exist, do not add
ThumbnailFileName := '';
CreateThumbnail(FileName, ThumbnailFileName);
Item := ListView1.Items.Add;
// Item.Caption is used as the hint
Item.Caption := FileName;
Item.Data := TImageListViewItem.Create(FileName,
ThumbnailFileName,
Item,
THUMBNAIL_SIZE,
THUMBNAIL_SIZE);
Result := Item.Index;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.RemoveImage(Index : Integer);
var
I : Integer;
begin
ListView1.Items.Delete(Index);
for I := Index to ListView1.Items.Count - 1 do
TImageListViewItem(ListView1.Items[I].Data).Data := ListView1.Items[I];
Windows.InvalidateRect(ListView1.Handle, nil, FALSE);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TDragDropMainForm.CreateThumbnail(
const AFileName : String;
var AThumbFileName : String);
var
ThWidth : Integer;
ThHeight : Integer;
FTFile : TDateTime;
FTThumb : TDateTime;
Image : IGPImage;
Thumbnail : IGPImage;
Params : IGPEncoderParameters;
Quality : Int32;
begin
AThumbFileName := ChangeFileExt(AFileName, '.thumb.jpg');
if FileExists(AThumbFileName) then begin
// Thumbnail file must be dated AFTER original file so that it
// is recreated when the original file is changed.
FileAge(AFileName, FTFile);
FileAge(AThumbFileName, FTThumb);
if FTThumb >= FTFile then
Exit;
end;
Image := TGPImage.Create(AFileName);
// Thumbnail preserve original width/height ratio
if Image.Width > Image.Height then begin
ThWidth := THUMBNAIL_SIZE;
ThHeight := THUMBNAIL_SIZE * Image.Height div Image.Width;
end
else if Image.Width < Image.Height then begin
ThHeight := THUMBNAIL_SIZE;
ThWidth := THUMBNAIL_SIZE * Image.Width div Image.Height;
end
else begin
ThWidth := THUMBNAIL_SIZE;
ThHeight := THUMBNAIL_SIZE;
end;
Thumbnail := Image.GetThumbnailImage(ThWidth, ThHeight, nil, nil);
Quality := 50;
Params := TGPEncoderParameters.Create;
Params.Add(EncoderQuality, Quality);
Thumbnail.Save(AThumbFileName, TGPImageFormat.Jpeg, Params);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ TImagePipeItem }
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
constructor TImageListViewItem.Create(
const AFileName : String;
const AThumbnailFileName : String;
const AItem : TListItem;
const AWidth : Integer;
const AHeight : Integer);
var
JpegImg : TJPEGImage;
Ext : String;
begin
inherited Create;
Data := AItem;
FileName := AFileName;
ThumbnailFileName := AThumbnailFileName;
Bitmap := TBitMap.Create;
if (AThumbnailFileName <> '') and (FileExists(AThumbnailFileName)) then begin
Ext := ExtractFileExt(AThumbnailFileName);
if SameText(Ext, '.jpg') then begin
JpegImg := TJPEGImage.Create;
try
JpegImg.LoadFromFile(AThumbnailFileName);
BitMap.Width := JpegImg.Width;
BitMap.Height := JpegImg.Height;
BitMap.Canvas.Draw(0, 0, JpegImg);
finally
JpegImg.Destroy;
end;
end
else if SameText(Ext, '.bmp') then
Bitmap.LoadFromFile(AThumbnailFileName)
end
else begin
Bitmap.Width := AWidth - 4;
Bitmap.Height := AHeight - 4;
Bitmap.PixelFormat := pf24bit;
Bitmap.Canvas.MoveTo(0, 0);
BitMap.Canvas.LineTo(Bitmap.Width, Bitmap.Height);
Bitmap.Canvas.MoveTo(Bitmap.Width, 0);
BitMap.Canvas.LineTo(0, Bitmap.Height);
BitMap.Canvas.LineTo(0, 0);
BitMap.Canvas.LineTo(Bitmap.Width - 1, 0);
BitMap.Canvas.LineTo(Bitmap.Width - 1, Bitmap.Height - 1);
BitMap.Canvas.LineTo(0, Bitmap.Height - 1);
Bitmap.Canvas.TextOut(4, 4, AFileName);
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
destructor TImageListViewItem.Destroy;
begin
FreeAndNil(Bitmap);
inherited Destroy;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
end.
Download source code from: http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
June 9, 2013
Dynamic web page using Delphi, ICS and DWScript
ICS has a web application server component which allows you to build dynamic web page very easily. Delphi code for each web page is encapsulated in a TUrlHandler class and compiled into your application, making a standalone webserver application.
In the article, we will create a TUrlHandler class which will read a DWScript script from disc and execute it. The script is responsible for build a valid answer which ICS will send back to the client.
A simple “Hello World” script is made of a single line like this:
Response.Write('Server time is ' + DateTimeToStr(Now) + ' ');
To invoke it, assuming the script is located in “Hello.pas”, the user must enter this URL into his browser:
http://localhost:20105/DWScripts/Hello.pas
Remember it is a script. You can change the file on disc and the changes will be immediately reflected for the next request, without recompiling your application. This looks much like PHP but use Delphi syntax instead of PHP. To be honest, PHP may use embedded HTML code within the script which is not supported here.
A more complex script may make use of request parameters. For example, the script “Add.pas” could looks like:
var Value1 : String;
var Value2 : String;
Response.ContentType := 'text/html';
Response.Status := '200 OK';
Response.Write('');
Response.Write('ICS and DWScript demo
');
Response.Write('Server time is ' + DateTimeToStr(Now) + '
');
if not Request.CheckParamByName('Value1', Value1) then
Response.Write('Missing Value1 parameter
')
else if not Request.CheckParamByName('Value2', Value2) then
Response.Write('Missing Value2 parameter
')
else Response.Write(Value1 + ' + ' + Value2 + ' = ' +
IntToStr(StrToIntDef(Value1, 0) + StrToIntDef(Value2, 0)));
Response.Write('');
The URL to use in the browser looks like:
http://localhost:20105/DWScripts/Add.pas?Value1=123&Value2=456
Getting ICS and DWScript
ICS: http://wiki.overbyte.be/wiki/index.php/ICS_DownloadDWScript: http://code.google.com/p/dwscript/
Implementation
Less than 200 Delphi code is required to implement this behavior in an ICS based web application server. The code I will show you below can be plugged in OverbyteIcsWebAppServer demo application you can find in ICS V8 distribution. Two lines must be added to the main file in order to add the feature: add the unit in the uses clause and add a line to map “/DWScript/*” to the TUrlHandler taking care of the script.
Using the above wild card mapping, the TUrlHandler will be invoked for any URL beginning with “/DWScript/”. It will then access the full path to get the script filename. In the above examples, I used a “.pas” file extension for ease, but this is not mandatory at all. You can use any extensions and even no extension if you don’t like to have the user know you are using a DWScript.
In the implementation, I managed to have the DWScript not directly accessible thru an URL. This protects your source code. The user can’t access it. He can just execute it (Note that ICS THttpServer component has an option which allows access to any file, so what I just said maybe wrong).
The script has two objects instances readily available: “Request” and “Response”. They maps to the corresponding Delphi object instances and classes:
THttpResponse = class(TPersistent)
private
FStatus : String;
FContentType : String;
public
DocStream : TStream;
published
property Status : String read FStatus write FStatus;
property ContentType : String read FContentType write FContentType;
procedure Write(const S : String);
end;
THttpRequest = class(TPersistent)
public
Params : String;
class function ReadTextFile(const FileName : String) : String;
published
function GetParamByName(const ParamName: String): String;
function CheckParamByName(const ParamName : String;
var ParamValue : String): Boolean;
end;
When using DWScript ExposeRTTI function, you are exposing a Delphi class. With the options I selected, only the published methods and properties will be exposed to the script.
THttpResponse exposes Status and ContentType properties as well as Write method. The two properties allows the script to select the HTTP status return and the HTTP content type. They default to “200 OK” and “text/html” which are the most common values. You can use anything you need for your application.
The Write method will be used by the script to produce the document part of the HTTP response sent back to the client. Obviously, the document format must match the content type.
THttpRequest exposes the incoming request. Using it you may access the parameters passed thru the URL. One method gets a parameter value when you provides his name while the other return the value as well as a Boolean value telling if the parameter exists or not.
The most important part of the code is a TUrlHandler derived class. I named it TUrlHandlerDWScript. His declaration looks like this:
TUrlHandlerDWScript = class(TUrlHandler)
protected
FScript : IdwsProgram;
FCompileMsgs : String;
FDelphiWebScript : TDelphiWebScript;
FUnit : TdwsUnit;
FExec : IdwsProgramExecution;
FHttpRequest : THttpRequest;
FHttpResponse : THttpResponse;
procedure ExposeInstancesAfterInitTable(Sender: TObject);
public
procedure Execute; override;
end;
The five first member variables are required for DWScript operation. You should look at DWScript documentation for help about their use.
The last two member variables are the object instances we talk above. Their published parts are exposed to the script.
There is also an event handler ExposeInstancesAfterInitTable which is required by DWScript engine to expose the Delphi object instances to the script.
Finally, there is a single method which is called by ICS web application server framework to handle the mapped URL. This is where almost everything happens.
procedure TUrlHandlerDWScript.Execute;
var
SrcFileName : String;
Source : String;
begin
FDelphiWebScript := TDelphiWebScript.Create(nil);
FUnit := TdwsUnit.Create(nil);
FHttpResponse := THttpResponse.Create;
FHttpRequest := THttpRequest.Create;
try
DocStream.Free;
DocStream := TMemoryStream.Create;
FHttpResponse.DocStream := DocStream;
FHttpResponse.Status := '200 OK';
FHttpResponse.ContentType := 'text/html';
FHttpRequest.Params := Params;
FUnit.OnAfterInitUnitTable := ExposeInstancesAfterInitTable;
FUnit.UnitName := 'WebPage';
FUnit.Script := FDelphiWebScript;
FUnit.ExposeRTTI(TypeInfo(THttpResponse), [eoNoFreeOnCleanup]);
FUnit.ExposeRTTI(TypeInfo(THttpRequest), [eoNoFreeOnCleanup]);
// In this application, we have placed DWScripts source code in
// a directory at the same level as the "Template" folder.
SrcFileName := ExcludeTrailingPathDelimiter(
ExtractFilePath(Client.TemplateDir)) +
StringReplace(Client.Path, '/', '\', [rfReplaceAll]);
if not FileExists(SrcFileName) then
FHttpResponse.Write('Script not found')
else begin
Source := FHttpRequest.ReadTextFile(SrcFileName);
FScript := FDelphiWebScript.Compile(Source);
FCompileMsgs := FScript.Msgs.AsInfo;
if FScript.Msgs.HasErrors then begin
FHttpResponse.Write('' + FCompileMsgs + '');
end
else begin
FExec := FScript.Execute;
end;
end;
AnswerStream(FHttpResponse.Status, FHttpResponse.ContentType, NO_CACHE);
finally
FreeAndNil(FUnit);
FreeAndNil(FDelphiWebScript);
FreeAndNil(FHttpResponse);
FreeAndNil(FHttpRequest);
end;
Finish;
end;
Basically the code creates all the required object instances, initializes default values, expose Delphi classes, read the script source code from file, compile the script and either create an error message should any compilation fails, or execute the script so that it can produce the document. Finally, the document is sent back and all object instances are destroyed.
Full source code:
The code is available from my website, see
http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
unit OverbyteIcsWebAppServerDWScriptUrlHandler;
interface
uses
Classes, SysUtils, OverbyteIcsHttpAppServer, OverbyteIcsHttpSrv,
dwsVCLGUIFunctions,
dwsMagicExprs,
dwsRTTIExposer,
dwsFunctions,
dwsSymbols,
dwsExprs,
dwsComp;
type
THttpResponse = class(TPersistent)
private
FStatus : String;
FContentType : String;
public
DocStream : TStream;
published
property Status : String read FStatus write FStatus;
property ContentType : String read FContentType write FContentType;
procedure Write(const S : String);
end;
THttpRequest = class(TPersistent)
public
Params : String;
class function ReadTextFile(const FileName : String) : String;
published
function GetParamByName(const ParamName: String): String;
function CheckParamByName(const ParamName : String;
var ParamValue : String): Boolean;
end;
TUrlHandlerDWScript = class(TUrlHandler)
protected
FScript : IdwsProgram;
FCompileMsgs : String;
FDelphiWebScript : TDelphiWebScript;
FUnit : TdwsUnit;
FExec : IdwsProgramExecution;
FHttpRequest : THttpRequest;
FHttpResponse : THttpResponse;
procedure ExposeInstancesAfterInitTable(Sender: TObject);
public
procedure Execute; override;
end;
implementation
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ TUrlHandlerDWScript }
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TUrlHandlerDWScript.Execute;
var
SrcFileName : String;
Source : String;
begin
FDelphiWebScript := TDelphiWebScript.Create(nil);
FUnit := TdwsUnit.Create(nil);
FHttpResponse := THttpResponse.Create;
FHttpRequest := THttpRequest.Create;
try
DocStream.Free;
DocStream := TMemoryStream.Create;
FHttpResponse.DocStream := DocStream;
FHttpResponse.Status := '200 OK';
FHttpResponse.ContentType := 'text/html';
FHttpRequest.Params := Params;
FUnit.OnAfterInitUnitTable := ExposeInstancesAfterInitTable;
FUnit.UnitName := 'WebPage';
FUnit.Script := FDelphiWebScript;
FUnit.ExposeRTTI(TypeInfo(THttpResponse), [eoNoFreeOnCleanup]);
FUnit.ExposeRTTI(TypeInfo(THttpRequest), [eoNoFreeOnCleanup]);
// In this application, we have placed DWScripts source code in
// a directory at the same level as the "Template" folder.
SrcFileName := ExcludeTrailingPathDelimiter(
ExtractFilePath(Client.TemplateDir)) +
StringReplace(Client.Path, '/', '\', [rfReplaceAll]);
if not FileExists(SrcFileName) then
FHttpResponse.Write('Script not found')
else begin
Source := FHttpRequest.ReadTextFile(SrcFileName);
FScript := FDelphiWebScript.Compile(Source);
FCompileMsgs := FScript.Msgs.AsInfo;
if FScript.Msgs.HasErrors then begin
FHttpResponse.Write('' + FCompileMsgs + '');
end
else begin
FExec := FScript.Execute;
end;
end;
AnswerStream(FHttpResponse.Status, FHttpResponse.ContentType, NO_CACHE);
finally
FreeAndNil(FUnit);
FreeAndNil(FDelphiWebScript);
FreeAndNil(FHttpResponse);
FreeAndNil(FHttpRequest);
end;
Finish;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TUrlHandlerDWScript.ExposeInstancesAfterInitTable(Sender: TObject);
begin
FUnit.ExposeInstanceToUnit('Response', 'THttpResponse', FHttpResponse);
FUnit.ExposeInstanceToUnit('Request', 'THttpRequest', FHttpRequest);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ THttpResponse }
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure THttpResponse.Write(const S: String);
var
Ch : Char;
B : Byte;
begin
for Ch in S do begin
// We should convert the Unicode string to whatever the document
// is supposed to be. Here we just convert it, brute force, to ASCII.
// This won't work eastern character sets.
B := Ord(AnsiChar(Ch));
DocStream.Write(B, 1);
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
{ THttpRequest }
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function THttpRequest.CheckParamByName(
const ParamName : String;
var ParamValue : String): Boolean;
begin
Result := ExtractURLEncodedValue(Params, ParamName, ParamValue);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
function THttpRequest.GetParamByName(const ParamName: String): String;
begin
ExtractURLEncodedValue(Params, ParamName, Result);
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
class function THttpRequest.ReadTextFile(const FileName : String) : String;
var
Stream : TFileStream;
AnsiBuf : AnsiString;
begin
Stream := TFileStream.Create(FileName, fmOpenRead);
try
SetLength(AnsiBuf, Stream.Size);
Stream.Read(AnsiBuf[1], Stream.Size);
Result := String(AnsiBuf);
finally
FreeAndNil(Stream);
end;
end;
{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
end.
Download source code from: http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
Labels:
Application Server,
borland,
codegear,
delphi,
DWScript,
embarcadero,
HTTP,
ICS,
InternetExplorer,
PHP,
programming,
RAD,
script,
web
May 23, 2013
Programmer picks: 7 must-try mobile dev tools
InfoWorld has selected Embarcadero RAD Studio XE4 in his slideshow about mobile dev tools. Established companies like Intel and Embarcadero Technologies, as well as lesser-known players are continually rolling out technologies to help meet mobile developers' needs.
Read the full article at: http://www.infoworld.com/slideshow/101586/programmer-picks-7-must-try-mobile-dev-tools-218867#slide6
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
Read the full article at: http://www.infoworld.com/slideshow/101586/programmer-picks-7-must-try-mobile-dev-tools-218867#slide6
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
Labels:
borland,
codegear,
delphi,
embarcadero,
IOS,
Mobile Development,
pascal,
programming,
RAD
May 21, 2013
Internet Explorer Automation Part 4
I this article, I will explain how to extract statistics from Blogger stats page. This follows the previous article in which you learned how to automate the login process and get the stats page.
The stats page is organized in a number of HTML elements. The one which is interesting for us is a table. Since there are many tables in the page, I had to find out a way to detect the correct one, even if the page layout changes.
The idea is to enumerate all the HTML elements in the page, check for the table tag and check the labels against string constants. This is easy since the table is organized in two columns, one with the label and one with the number.
To iterate all the HTML elements is easy. As we saw in previous article, there is a property of the HTML document which is a collection (a kind of array) name “all”. It is enough to enumerate it and for each item in the collection query the interface IID_IHTMLElement to get hand on the HTML element.
Having the HTML element, we can check the tagName property which is actually the tag type. We are looking for ‘Table’. If it is a table, we get the innertext property which as its name implies is the raw text inside the tag. Raw text means it is the text without any embedded tags. In the case of an HTML table, we get the content of all table cells. We will search that text for the labels such as “Pageviews today” and then extract the number just after. For that purpose I wrote a little utility function I will show you in a moment.
Here is the code to do what I’ve just described:
Coll := FHtmlDoc.all;
for I := 0 to Coll.Length - 1 do begin
pDisp := Coll.item(I, var2);
if pDisp.QueryInterface(IID_IHTMLElement, HtmlElem) = S_OK then begin
if SameText(HtmlElem.tagName, 'TABLE') then begin
Txt := String(HtmlElem.innertext);
if not ExtractNumberAfterText(Txt, TxtToday,
CountToday) then
continue;
if not ExtractNumberAfterText(Txt, TxtYesterday,
CountYesterday) then
continue;
if not ExtractNumberAfterText(Txt, TxtLastMonth,
CountLastMonth) then
continue;
if not ExtractNumberAfterText(Txt, TxtAllTime,
CountAllTime) then
continue;
Buf := AnsiString(
FormatDateTime('YYYY/MM/DD;HH:NN:SS;', Now) +
'"_' + FBlogId + '";' +
IntToStr(CountToday) + ';' +
IntToStr(CountYesterday) + ';' +
IntToStr(CountLastMonth) + ';' +
IntToStr(CountAllTime));
Result := TRUE;
break;
end;
end;
end;
The utility function ExtractNumberAfterText is rather simple. It is just simple Delphi code to parse the string. We just have to pay attention to skip all spaces and line breaks because they are not significant in HTML.
function ExtractNumberAfterText(
const Source : String;
const Text : String;
out Number : Integer) : Boolean;
var
J : Integer;
begin
Result := FALSE;
Number := 0;
J := Pos(Text, Source);
if J <= 0 then
Exit;
// Search for first digit right after searched text,
// ignore anything not a digit
J := J + Length(Text);
while (J <= Length(Source)) and
(not CharInSet(Source[J], ['0'..'9'])) do
Inc(J);
// After first digit, scan all digit and ',' or '.' (which
// are used as thousand separator (Depends on language, any will do)
repeat
// If we have a digit, use it to build the final number
if CharInSet(Source[J], ['0'..'9']) then
Number := Number * 10 + Ord(Source[J]) - Ord('0');
Inc(J);
until (J > Length(Source)) or
(not CharInSet(Source[J], ['0'..'9', ',', '.']));
Result := TRUE;
end;
About the design of the application
I explained how to automate Internet Explorer. I showed the actual code used. But I didn’t gave any explanation about how I have designed the whole application.
I always like to separate the user interface from data processing. For that purpose, I created two source files: one with the user interface and one with a class having the automation code.
My user interface is very basic: a simple form with a memo showing messages about what is going on. I could as well write a console mode application or a service application. This doesn’t really matters.
My data processing code is encapsulated in a class I named TQueryBloggerStatistics. It explains what it does. The class is a kind of container. It exposes a few methods and properties to permit what has to be done with that kind of automation.
The class declaration is as follow:
TQueryBloggerStatistics = class
private
FWebBrowser : IWebBrowser2;
FBlogID : String;
FUserEMail : String;
FUserPassword : String;
FLogFileName : String;
FVisible : Boolean;
FOnDisplay : TDisplayEvent;
function WaitComplete(const URL : String = ''): IHTMLDocument2;
function FindTag(const Coll : IHTMLElementCollection;
const TagName, TagID: String): IHTMLElement;
procedure Display(const Msg : String);
public
constructor Create;
function Execute : Boolean;
procedure Quit;
procedure LoadConfig(const IniFileName : String); overload;
procedure LoadConfig; overload;
function SaveConfig(const IniFileName: String) : Boolean; overload;
function SaveConfig : Boolean; overload;
property BlogID : String read FBlogID
write FBlogID;
property UserEMail : String read FUserEMail
write FUserEMail;
property UserPassword : String read FUserPassword
write FUserPassword;
property LogFileName : String read FLogFileName
write FLogFileName;
property Visible : Boolean read FVisible
write FVisible;
property OnDisplay : TDisplayEvent read FOnDisplay
write FOnDisplay;
end;
I won’t reproduce the implementation here because I already showed most interesting part. You can download the full source code for the class and the complete demo application from my website at:http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
Previous article: http://francois-piette.blogspot.be/2013/05/internet-explorer-automation-part-3.html
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
Labels:
Automation,
borland,
codegear,
COM,
delphi,
DOM,
embarcadero,
InternetExplorer,
IWebBrowser,
pascal,
programming
May 20, 2013
Pascal still an advantage for some iOS, Android developers
Just read this article. It could also interest you so I share it:
http://www.zdnet.com/pascal-still-an-advantage-for-some-ios-android-developers-7000014743/
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
http://www.zdnet.com/pascal-still-an-advantage-for-some-ios-android-developers-7000014743/
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
May 12, 2013
Internet Explorer Automation Part 3
Today I will present an Internet Explorer automation which will query Blogger stats page automatically. IE automation is required because Blogger website makes heavy use of JavaScript to dynamically construct the stats page. Downloading the webpage with a HTTP component won’t work because the numbers we are looking for are not in clear! JavaScript must be executed to get hand of it.
The code I will show you will also take care of authentication. Asking for the stats page without being first authenticated and you get the authentication page instead. The code I’ll present will detect the login page, fill the form automatically, submit it and then request the stats page again and finally extract the data.
For those not accustomed with Blogger author interface and his stats page, the screen dump shows an actual view of the page. It shows the stats for this week (At the time of writing this article). What we are interested in is to get the column on the right showing “Pageviews today 149”, “Pageviews yesterday 434” and the two other lines. This is an HTML table that we have to extract from the document.
As I said above before getting this stats page, you must be authenticated. This means that if you are not authenticated, Blogger will show you the login page whatever you asked in the first place. For your reference, here is a screen dump of the authentication page:
On that page, we see a form with two fields for Email and Password and a button “Sign in” to click. The program will locate those fields, assign a value and then click on the button.
Document Object Model (DOM)
The World Wide Web Consortium (W3C) Document Object Model (DOM) is a platform- and language-neutral interface that permits programs or scripts to access and update the content, structure, and style of a document. The W3C DOM includes a model for how a standard set of objects representing HTML and XML documents are combined, and an interface for accessing and manipulating them.
Internet Explorer exposes DOM thru a set of COM interfaces available to external programs such as our Delphi application. This is documented on MSDN website at:
http://msdn.microsoft.com/en-us/library/ie/hh772384(v=vs.85).aspx
I will only scratch the surface of DOM. Just enough to get you started and to accomplish the task for the sample application.
We saw in previous article that we can connect to IE by calling this line:
FWebBrowser := CreateComObject(CLASS_InternetExplorer) as IWebBrowser2;
And that we can navigate to an URL with this line of code:
FWebBrowser.Navigate(Url, EmptyParam, EmptyParam, EmptyParam, EmptyParam);
To get hand on the interface which is the entry point for the DOM, we must get the document (whatever it is) and the get the interface to the HTML document (if it exists):
Doc := FWebBrowser.Document;
Doc.QueryInterface(IID_IHTMLDocument2, HtmlDoc);
Those code lines are easy but wait! There can be some glitches. Internet Explorer takes some time to fetch URL and build document. A document can be quite complex and could requires a lot of downloads for HTML, images, CSS, scripts and more. And once everything is downloaded, scripts have to be executed. There are various status available to be sure everything is OK. The method WaitComplete here after takes an URL, navigate to it and wait until the HTML document interface is available and the document is ready:
function TQueryBloggerStatistics.WaitComplete(
const URL : String = ''): IHTMLDocument2;
var
Doc : IDispatch;
begin
Result := nil;
if URL <> '' then
FWebBrowser.Navigate(Url, EmptyParam, EmptyParam, EmptyParam, EmptyParam);
while FWebBrowser.Busy do
Sleep(250);
while FWebBrowser.Document = nil do
Sleep(250);
Doc := FWebBrowser.Document;
if Doc.QueryInterface(IID_IHTMLDocument2, Result) <> S_OK then
Exit;
while not SameText(Result.readyState, 'complete') do
Sleep(250);
end;
WaitComplete takes and optional URL and returns the IHTMLDocument2 interface required for handling the document. Tests are made to be sure everything is ready or complete. The code is quite straightforward but this must be done like that.
Once we’ve got an IHTMLDocument2 interface, we can use it to traverse the document object model (DOM) to find the HTML elements we need and to get or set their properties.
The HTML document has a number of collections like images, links, scripts and the likes. And there is a special collection returning absolutely everything. It is named “all”. We will use it to find what we need. For example, in the login form, we need to get hand on the HTML INPUT tag for each field and submit buttons. Each HTML tag has a TagName such as “input” and a tagID. TagName is an HTML standard while TagID is chosen by the web developer, in this case by Blogger. Fortunately at Blogger, they used very clear and meaningful TagId sucha as “Email” (for the Email input field), “Passwd” (for the password input field) and “Signin” for the submit button.
Since we have to get hand on several HTML elements, I wrote a little function FindTag:
function TQueryBloggerStatistics.FindTag(
const Coll : IHTMLElementCollection;
const TagName : String;
const TagID : String) : IHTMLElement;
var
PDisp : IDispatch;
Var2 : OleVariant;
I : Integer;
begin
for I := 0 to Coll.Length - 1 do begin
pDisp := Coll.item(I, var2);
if pDisp.QueryInterface(IID_IHTMLElement, Result) = S_OK then begin
if SameText(Result.tagName, TagName) and
SameText(Result.Id, TagID) then
Exit;
end;
end;
Result := nil;
end;
FindTag has to be called like this: HtmlElem := FindTag(FHtmlDoc.All, 'INPUT', 'EMail');
if Assigned(HtmlElem) then
HtmlElem.setAttribute('Value', FUserEMail, 0);
This excerpt find tag name “input” tag having an ID “Email”. The result, if found, is the interface to handle that HTML element. Here I use the interface to set the attribute “value” to the user email (variable FUserEMail hold the Email address).
FindTag code is relatively simple although accessing the collection items is a little bit tricky and must pass thru the use of another interface. Sorry but this is how Microsoft designed IE to handle the DOM.
Detecting and handling the login page
The code I’ll show you below will query a webpage by his URL. Nere this URL is supposed to be the stats page of a given Blogger’s blog. We’ll come back to that URL later. It makes use of WaitComplete to fetch the URL, wait until it is ready and complete and then use FindTag to see it the page conatins an “input” tag with and ID “Email”. If this is the case, then it is assumed we have received the login page. The conde then fetch in cascade all other required tags in that page, fill it with user data and then claa the “Click” method of the HTML element which is the submit button. And guess what… IE will send the form to Blogger and authentication take place. FHtmlDoc := WaitComplete(URL);
if not Assigned(FHtmlDoc) then
Exit;
// Check for login page
// If found, fill in the form and subit it before continuing
HtmlElem := FindTag(FHtmlDoc.All, 'INPUT', 'EMail');
if Assigned(HtmlElem) then begin
HtmlElem.setAttribute('Value', FUserEMail, 0);
HtmlElem := FindTag(FHtmlDoc.All, 'INPUT', 'Passwd');
if Assigned(HtmlElem) then begin
HtmlElem.setAttribute('Value', FUserPassword, 0);
HtmlElem := FindTag(FHtmlDoc.All, 'INPUT', 'PersistentCookie');
if Assigned(HtmlElem) then
HtmlElem.setAttribute('Checked', '', 0);
HtmlElem := FindTag(FHtmlDoc.All, 'INPUT', 'Signin');
if Assigned(HtmlElem) then begin
HtmlElem.click;
Display('Login...');
// We have found login form and must wait for login to occur
FHtmlDoc := WaitComplete;
if not Assigned(FHtmlDoc) then
Exit;
// Login is finished, we must navigate again to the target URL
FHtmlDoc := WaitComplete(URL);
if not Assigned(FHtmlDoc) then
Exit;
HtmlElem := FindTag(FHtmlDoc.All, 'INPUT', 'EMail');
if Assigned(HtmlElem) then begin
Display('Login failed');
Exit;
end;
end;
end;
end;
The next step is to extract the statistics from the stat page.
We will do that in the next article. Stay tuned!
Read also part 1 and part 2.
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
Labels:
API,
Automation,
Blogger,
borland,
codegear,
COM,
delphi,
DOM,
embarcadero,
InternetExplorer,
IWebBrowser,
microsoft,
programming,
site web,
XE4
Subscribe to:
Posts (Atom)


