I have a little project I'm working on in Delphi 7, and I got to a point where I'm stumped how to continue. The full source to just this project is below, but this project is using a set of components for parsing HTML: however, with how I have this, you can ignore and remove these components, and in the procedure where I do the parsing, you can replace it with adding a few fake values (ignoring the HTML all together). That procedure is towards the end (called cmdParseClick).
The project's concept is fairly simple, it's an editor for HTML image maps. It consists of two main parts: HTML Editor, and Image Editor. The HTML Editor is only holding the relevant block of HTML code for the map and image, whereas the image editor is showing the image with the ability to draw, move, and resize each 'hotspot' area on the image. The image and the back-end array of hotspot records should be directly synchronized with each other.
My problem is figuring out how to recognize when the user is trying to resize one of the boxes on the image. I have it to the point where I can recognize where the user is pointing the mouse on the image, and changing the cursor accordingly. So here's the sequence of what is already working:
1) Load image into back-end image (fImgMain) where original image is always maintained.
2) Copy image to back-end temp image (fImgTemp) where temporary drawing is done to image before display
3) Copy image to TImage component from the temp image to display the image with the drawn areas
4) Paste or write HTML code for an image map (<map>, <area>, and <img> tags involved)
5) Parse HTML and build a virtual list of these 'hotspots' - or "array of THotspot" records
6) Copy main image to temp image
7) Go through the virtual list and draw each box on the temp image
8) Copy temp image with drawn boxes to the image display
9) Recognize when user points mouse over certain areas of image
10) Change cursor of image if mouse is over hotspot (to move), the edge (to resize), or no hotspot (to draw new hotspot)
And at this point, how to actually accept user's mouse actions is way over my head.
Now when an area is being moved or resized (while mouse is down), the changes on the image need to be updated in the corresponding record in the array. Meaning, as long as user is moving mouse while mouse button is down, any change is recorded in the virtual list of records. After each movement of the mouse in this case, it needs to a) update the record in the array, b) copy main image over to temp image, c) draw new boxes on temp image, and d) copy temp image to image display.
At no point shall the main image (fImgMain) ever be changed or drawn to (unless a new image is loaded).
Note the type "TMouseDownState" which is a record to recognize what action is to be done while the mouse is down (moving hotspot, resizing left edge, resizing bottom-right edge, etc.). Two important functions are "HotspotAtCursor" and "MouseStateAtCursor" which recognize the position of the mouse over the display image (Img: TImage).
The project is simple, just 1 main form only. I tried to put as many comments as possible. Here's the code:
And here's the DFM (form) code:
JD Solutions
The project's concept is fairly simple, it's an editor for HTML image maps. It consists of two main parts: HTML Editor, and Image Editor. The HTML Editor is only holding the relevant block of HTML code for the map and image, whereas the image editor is showing the image with the ability to draw, move, and resize each 'hotspot' area on the image. The image and the back-end array of hotspot records should be directly synchronized with each other.
My problem is figuring out how to recognize when the user is trying to resize one of the boxes on the image. I have it to the point where I can recognize where the user is pointing the mouse on the image, and changing the cursor accordingly. So here's the sequence of what is already working:
1) Load image into back-end image (fImgMain) where original image is always maintained.
2) Copy image to back-end temp image (fImgTemp) where temporary drawing is done to image before display
3) Copy image to TImage component from the temp image to display the image with the drawn areas
4) Paste or write HTML code for an image map (<map>, <area>, and <img> tags involved)
5) Parse HTML and build a virtual list of these 'hotspots' - or "array of THotspot" records
6) Copy main image to temp image
7) Go through the virtual list and draw each box on the temp image
8) Copy temp image with drawn boxes to the image display
9) Recognize when user points mouse over certain areas of image
10) Change cursor of image if mouse is over hotspot (to move), the edge (to resize), or no hotspot (to draw new hotspot)
And at this point, how to actually accept user's mouse actions is way over my head.
Now when an area is being moved or resized (while mouse is down), the changes on the image need to be updated in the corresponding record in the array. Meaning, as long as user is moving mouse while mouse button is down, any change is recorded in the virtual list of records. After each movement of the mouse in this case, it needs to a) update the record in the array, b) copy main image over to temp image, c) draw new boxes on temp image, and d) copy temp image to image display.
At no point shall the main image (fImgMain) ever be changed or drawn to (unless a new image is loaded).
Note the type "TMouseDownState" which is a record to recognize what action is to be done while the mouse is down (moving hotspot, resizing left edge, resizing bottom-right edge, etc.). Two important functions are "HotspotAtCursor" and "MouseStateAtCursor" which recognize the position of the mouse over the display image (Img: TImage).
The project is simple, just 1 main form only. I tried to put as many comments as possible. Here's the code:
Code:
unit uMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, XPMan, ExtCtrls, Jpeg, ExtDlgs, Menus, Buttons,
DIUnicode, DIHtmlParser;
const
//GripSize = size of resizable border around each hotspot
GripSize = 3;
type
//Represents one single 'hotspot' or 'area' on the 'map'
THotspot = record
Area: TRect;
URL: String;
Shape: String;
end;
//Represents current mouse status of a hotspot
TMouseHotspot = record
Hotspot: THotspot;
Active: Bool;
DownPos: TPoint;
UpPos: TPoint;
CurrPos: TPoint;
end;
//Primary storage of all hotspot records
THotspots = array of THotspot;
//Represents what mouse is doing if mouse button is down
TMouseDownState = (msNone, msMove, msSizeW, msSizeN, msSizeE, msSizeS,
msSizeNW, msSizeNE, msSizeSW, msSizeSE);
TForm1 = class(TForm)
HParser: TDIHtmlParser;
dlgOpen: TOpenPictureDialog;
pRight: TPanel;
Tags: TMemo;
Splitter1: TSplitter;
pMain: TPanel;
pHTML: TPanel;
HTML: TMemo;
pImage: TPanel;
Box: TScrollBox;
Img: TImage;
Splitter2: TSplitter;
MainMenu1: TMainMenu;
File1: TMenuItem;
New1: TMenuItem;
Open1: TMenuItem;
Save1: TMenuItem;
SaveAs1: TMenuItem;
Close1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
Edit1: TMenuItem;
Cut1: TMenuItem;
Copy1: TMenuItem;
Paste1: TMenuItem;
View1: TMenuItem;
Image1: TMenuItem;
HTML1: TMenuItem;
HotspotImage1: TMenuItem;
cmdParse: TBitBtn;
procedure cmdParseClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure ImgMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure ImgMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure ImgMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure Exit1Click(Sender: TObject);
procedure Close1Click(Sender: TObject);
private
fImgMain: TBitmap;
fImgTemp: TBitmap;
fHotspots: THotspots;
fMouseDownState: TMouseDownState;
fSelHotspot: Integer;
fPosDown: TPoint;
function GetHotspot(Index: Integer): THotspot;
procedure SetHotspot(Index: Integer; const Value: THotspot);
procedure DrawHotspot(const H: THotspot);
procedure CodeToImage;
procedure ImageToCode;
public
function LoadFile(const Filename: TFilename): Bool;
procedure CloseFile;
function HotspotAtCursor: TMouseHotspot;
function MouseStateAtCursor: TMouseDownState;
property MouseDownState: TMouseDownState read fMouseDownState;
property SelHotspot: Integer read fSelHotspot;
property Hotspots[Index: Integer]: THotspot read GetHotspot write SetHotspot;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
//Draws a single hotspot to the temporary bitmap
procedure TForm1.DrawHotspot(const H: THotspot);
var
C: TCanvas;
begin
C:= fImgTemp.Canvas;
C.Brush.Style:= bsClear;
C.Pen.Style:= psSolid;
C.Pen.Color:= clGray;
C.Pen.Width:= 2;
C.Rectangle(H.Area);
end;
//Acquires a single hotspot from global list by index
function TForm1.GetHotspot(Index: Integer): THotspot;
begin
Result:= fHotspots[Index];
end;
//Assigns a single hotspot in global list by index
procedure TForm1.SetHotspot(Index: Integer; const Value: THotspot);
begin
fHotspots[Index]:= Value;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
//Create objects
fImgMain:= TBitmap.Create;
fImgTemp:= TBitmap.Create;
//Set default values
SetLength(fHotspots, 0);
fSelHotspot:= -1;
fMouseDownState:= msNone;
//Prepare controls
pMain.Align:= alClient;
pImage.Align:= alClient;
HTML.Align:= alClient;
Box.Align:= alClient;
//Forcefully show main form
Show;
BringToFront;
Application.ProcessMessages;
//Automatically prompt to open an image (not necessary)
if dlgOpen.Execute then begin
LoadFile(dlgOpen.FileName);
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
//Free objects
fImgMain.Free;
fImgTemp.Free;
end;
//Loads a single image file to be primary image to work with
function TForm1.LoadFile(const Filename: TFilename): Bool;
var
Ext: String;
J: TJpegImage;
begin
Result:= False;
if FileExists(Filename) then begin
Ext:= LowerCase(ExtractFileExt(Filename));
if (Ext = '.jpg') or (Ext = '.jpeg') then begin
J:= TJpegImage.Create;
try
J.LoadFromFile(Filename);
fImgMain.Assign(J);
Result:= True;
finally
J.Free;
end;
end else
if (Ext = '.bmp') then begin
fImgMain.LoadFromFile(Filename);
Result:= True;
end else begin
raise Exception.Create('Invalid file format "'+Ext+'"');
end;
end;
if Result then begin
fImgTemp.Assign(fImgMain);
Img.Picture.Assign(fImgMain);
end;
end;
//Closes the currently opened file
procedure TForm1.CloseFile;
begin
end;
//Detect mouse movement on image
procedure TForm1.ImgMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var
H: TMouseHotspot;
begin
//Identify current mouse state
case Self.fMouseDownState of
msNone: begin
//Mouse button is NOT down - change cursor
H:= HotspotAtCursor;
if H.Active then begin
//Mouse is over a hotspot
case MouseStateAtCursor of
msMove: Img.Cursor:= crSizeAll;
msSizeN, msSizeS: Img.Cursor:= crSizeNS;
msSizeW, msSizeE: Img.Cursor:= crSizeWE;
msSizeNW, msSizeSE: Img.Cursor:= crSizeNWSE;
msSizeSW, msSizeNE: Img.Cursor:= crSizeNESW;
end;
end else begin
//Mouse is NOT over a hotspot
Img.Cursor:= crCross;
end;
end;
//The following events update Rect values within fHotspots
// With these, the mouse button IS down
msMove: begin
//Mouse is moving selected hotspot
end;
msSizeW: begin
//Mouse is sizing left side of hotspot
end;
msSizeN: begin
//Mouse is sizing top side of hotspot
end;
msSizeE: begin
//Mouse is sizing right side of hotspot
end;
msSizeS: begin
//Mouse is sizing bottom side of hotspot
end;
msSizeNW: begin
//Mouse is sizing top-left corner of hotspot
end;
msSizeNE: begin
//Mouse is sizing top-right corner of hotspot
end;
msSizeSW: begin
//Mouse is sizing bottom-left corner of hotspot
end;
msSizeSE: begin
//Mouse is sizing bottom-right corner of hotspot
end;
end;
end;
procedure TForm1.ImgMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
H: TMouseHotspot;
begin
fPosDown:= Point(X, Y); //Record current mouse position for calculations
H:= HotspotAtCursor;
if H.Active then begin
//Mouse clicked on a hotspot
fMouseDownState:= MouseStateAtCursor; //Record what mouse is doing
//Record currently active hotspot
end else begin
//Mouse clicked where there is no hotspot - draw new?
end;
end;
procedure TForm1.ImgMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
I: Integer;
H: THotspot;
XM, YM: Integer;
L: TStringList;
begin
L:= TStringList.Create;
try
//Update code with new value(s)
for I:= 0 to Length(fHotspots) - 1 do begin
H:= fHotspots[I];
L.Append(' <area shape="'+H.Shape+'" coords="'+'" href="'+'" alt="'+'" />');
fHotspots[I]:= H;
end;
finally
L.Free;
end;
//Re-parse HTML with new values
//Re-draw hotspots on image
fMouseDownState:= msNone;
end;
//Acquires hotspot info at current mouse position
// Returns with 'Active = False' if no hotspot is present
function TForm1.HotspotAtCursor: TMouseHotspot;
var
I: Integer;
H: THotspot;
X, Y: Integer;
begin
X:= Img.ScreenToClient(Mouse.CursorPos).X;
Y:= Img.ScreenToClient(Mouse.CursorPos).Y;
Result.Active:= False;
for I:= 0 to Length(fHotspots)-1 do begin
H:= fHotspots[I];
//Detect if mouse is at an edge of hotspot (to resize)
if (X >= H.Area.Left) and (X <= H.Area.Right) and
(Y >= H.Area.Top) and (Y <= H.Area.Bottom) then
begin
Result.Hotspot:= H;
Result.Active:= True;
Result.DownPos:= fPosDown;
Result.CurrPos:= Point(X, Y);
Break;
end;
end;
end;
//Acquires mouse down state info at current mouse position
// In other words, with mouse where it is now, what would be done if clicked?
function TForm1.MouseStateAtCursor: TMouseDownState;
var
X, Y: Integer;
H: TMouseHotspot;
begin
Result:= msNone;
X:= Img.ScreenToClient(Mouse.CursorPos).X;
Y:= Img.ScreenToClient(Mouse.CursorPos).Y;
H:= Self.HotspotAtCursor;
if H.Active then begin
//Mouse is over a hotspot
if (X >= H.Hotspot.Area.Right - GripSize) and (Y >= H.Hotspot.Area.Bottom - GripSize) then begin
Result:= msSizeSE;
end else
if (X >= H.Hotspot.Area.Right - GripSize) and (Y <= H.Hotspot.Area.Top + GripSize) then begin
Result:= msSizeNE;
end else
if (X <= H.Hotspot.Area.Left + GripSize) and (Y >= H.Hotspot.Area.Bottom - GripSize) then begin
Result:= msSizeSW;
end else
if (X <= H.Hotspot.Area.Left + GripSize) and (Y <= H.Hotspot.Area.Top + GripSize) then begin
Result:= msSizeNW;
end else
if (X <= H.Hotspot.Area.Left + GripSize) then begin
Result:= msSizeW;
end else
if (Y <= H.Hotspot.Area.Top + GripSize) then begin
Result:= msSizeN;
end else
if (X >= H.Hotspot.Area.Right - GripSize) then begin
Result:= msSizeE;
end else
if (Y >= H.Hotspot.Area.Bottom - GripSize) then begin
Result:= msSizeS;
end else begin
//Mouse is directly over hotspot, move, not resize
Result:= msMove;
end;
end;
end;
//Exit the application
procedure TForm1.Exit1Click(Sender: TObject);
begin
Close;
end;
//Close currently opened image
procedure TForm1.Close1Click(Sender: TObject);
begin
Self.CloseFile;
end;
//Main parsing procedure - need to migrate to 'CodeToImage' procedure
procedure TForm1.cmdParseClick(Sender: TObject);
var
X: Integer;
H: THotspot;
T: String;
ID: String;
procedure Add(const Hotspot: THotspot);
begin
SetLength(fHotspots, Length(fHotspots)+1);
fHotspots[Length(fHotspots)-1]:= Hotspot;
end;
begin
Tags.Lines.Clear;
HParser.SourceBufferAsStr:= HTML.Text;
ID:= '';
SetLength(fHotspots, 0);
while HParser.ParseNextPiece do begin
case HParser.PieceType of
ptHTMLTag: begin
T:= LowerCase(HParser.HtmlTag.TagName);
if T = 'map' then begin
ID:= HParser.HtmlTag.ValueOfNameCS['id'];
if ID = '' then
ID:= HParser.HtmlTag.ValueOfNameCI['name'];
if ID <> '' then begin
Tags.Lines.Append('Map: "'+ID+'"');
end;
end else
if T = 'area' then begin
H.URL:= HParser.HtmlTag.ValueOfNameCI['href'];
H.Shape:= HParser.HtmlTag.ValueOfNameCI['shape'];
T:= HParser.HtmlTag.ValueOfNameCI['coords']+',';
X:= 0;
while Length(T) > 0 do begin
if Pos(',', T) > 1 then begin
case X of
0: H.Area.Left:= StrToIntDef(Copy(T, 1, Pos(',', T)-1), 0);
1: H.Area.Top:= StrToIntDef(Copy(T, 1, Pos(',', T)-1), 0);
2: H.Area.Right:= StrToIntDef(Copy(T, 1, Pos(',', T)-1), 0);
3: H.Area.Bottom:= StrToIntDef(Copy(T, 1, Pos(',', T)-1), 0);
else T:= '';
end;
Delete(T, 1, Pos(',', T));
end else begin
T:= '';
end;
Inc(X);
end;
Add(H);
Tags.Lines.Append(' Area: '+
IntToStr(H.Area.Left)+'x'+IntToStr(H.Area.Top));
end else
if T = 'img' then begin
Tags.Lines.Append('Image: '+HParser.HtmlTag.ValueOfNameCS['src']);
end;
end;
end;
end;
fImgTemp.Assign(fImgMain);
for X:= 0 to Length(fHotspots)-1 do begin
DrawHotspot(fHotspots[X]);
end;
Img.Picture.Assign(fImgTemp);
end;
//Converts HTML code to list of hotspots, then draws image
procedure TForm1.CodeToImage;
begin
end;
//Converts list of hotspots (updated by image) to HTML code
procedure TForm1.ImageToCode;
begin
end;
end.
And here's the DFM (form) code:
Code:
object Form1: TForm1
Left = 407
Top = 245
Width = 810
Height = 779
Caption = 'Image Map Editor'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
Menu = MainMenu1
OldCreateOrder = False
Position = poScreenCenter
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 13
object Splitter1: TSplitter
Left = 611
Top = 0
Width = 5
Height = 721
Align = alRight
end
object pRight: TPanel
Left = 616
Top = 0
Width = 178
Height = 721
Align = alRight
TabOrder = 0
DesignSize = (
178
721)
object Tags: TMemo
Left = 6
Top = 16
Width = 166
Height = 666
Anchors = [akLeft, akTop, akRight, akBottom]
TabOrder = 0
end
object cmdParse: TBitBtn
Left = 8
Top = 688
Width = 163
Height = 25
Anchors = [akLeft, akRight, akBottom]
Caption = 'Parse'
TabOrder = 1
OnClick = cmdParseClick
end
end
object pMain: TPanel
Left = 0
Top = 0
Width = 569
Height = 721
Align = alLeft
Anchors = [akLeft, akTop, akRight, akBottom]
TabOrder = 1
object Splitter2: TSplitter
Left = 1
Top = 225
Width = 567
Height = 5
Cursor = crVSplit
Align = alTop
end
object pHTML: TPanel
Left = 1
Top = 1
Width = 567
Height = 224
Align = alTop
TabOrder = 0
object HTML: TMemo
Left = 1
Top = 1
Width = 510
Height = 222
Align = alLeft
Anchors = [akLeft, akTop, akRight, akBottom]
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'Consolas'
Font.Style = []
Lines.Strings = (
'<MAP NAME="bm10">'
' <AREA SHAPE="RECT" COORDS="8,29,55,48" HREF="the_list_tab.htm"' +
'>'
' <AREA SHAPE="RECT" COORDS="56,29,111,48" HREF="the_general_tab' +
'.htm">'
' <AREA SHAPE="RECT" COORDS="112,29,198,47" HREF="the_general_de' +
'fault_tab.htm">'
' <AREA SHAPE="RECT" COORDS="200,29,257,47" HREF="the_price_setu' +
'p_tab.htm">'
' <AREA SHAPE="RECT" COORDS="258,30,345,47" HREF="the_customer_p' +
'rice_setup_tab.htm">'
' <AREA SHAPE="RECT" COORDS="346,28,421,48" HREF="the_measuremen' +
't_tab.htm">'
' <AREA SHAPE="RECT" COORDS="422,29,471,49" HREF="reports_tab.ht' +
'm">'
' <AREA SHAPE="RECT" COORDS="12,52,134,72" HREF="hs204.htm">'
' <AREA SHAPE="RECT" COORDS="134,52,251,71" HREF="hs205.htm">'
' <AREA SHAPE="RECT" COORDS="324,89,534,232" HREF="purchase_orde' +
'r_view_summary_tab.htm">'
' <AREA SHAPE="RECT" COORDS="82,112,159,124" HREF="purchase_orde' +
'rs_vendor_invoice.htm">'
' <AREA SHAPE="RECT" COORDS="69,128,110,144" HREF="inventory_bat' +
'ch_transfer.htm">'
' <AREA SHAPE="RECT" COORDS="471,31,536,50" HREF="hs219.htm">'
'</MAP>'
'<IMG border=0 src="../images/bm10.png" isMap width=571 height =4' +
'51 useMap=#bm10 >')
ParentFont = False
ScrollBars = ssBoth
TabOrder = 0
WordWrap = False
end
end
object pImage: TPanel
Left = 1
Top = 272
Width = 567
Height = 448
Align = alBottom
Anchors = [akLeft, akTop, akRight, akBottom]
TabOrder = 1
object Box: TScrollBox
Left = 1
Top = 1
Width = 510
Height = 446
HorzScrollBar.Smooth = True
HorzScrollBar.Tracking = True
VertScrollBar.Smooth = True
VertScrollBar.Tracking = True
Align = alLeft
Anchors = [akLeft, akTop, akRight, akBottom]
Color = clWhite
ParentColor = False
TabOrder = 0
object Img: TImage
Left = 0
Top = 0
Width = 200
Height = 500
Cursor = crSizeNWSE
AutoSize = True
IncrementalDisplay = True
OnMouseDown = ImgMouseDown
OnMouseMove = ImgMouseMove
OnMouseUp = ImgMouseUp
end
end
end
end
object HParser: TDIHtmlParser
FilterHtmlTags.StartTags = fiShow
Left = 184
Top = 56
end
object dlgOpen: TOpenPictureDialog
Left = 216
Top = 88
end
object MainMenu1: TMainMenu
Left = 185
Top = 89
object File1: TMenuItem
Caption = 'File'
object New1: TMenuItem
Caption = 'New...'
end
object Open1: TMenuItem
Caption = 'Open'
object Image1: TMenuItem
Caption = 'Image...'
end
object HTML1: TMenuItem
Caption = 'HTML...'
end
object HotspotImage1: TMenuItem
Caption = 'Mapped Image...'
end
end
object Save1: TMenuItem
Caption = 'Save'
end
object SaveAs1: TMenuItem
Caption = 'Save As...'
end
object Close1: TMenuItem
Caption = 'Close'
OnClick = Close1Click
end
object N1: TMenuItem
Caption = '-'
end
object Exit1: TMenuItem
Caption = 'Exit'
OnClick = Exit1Click
end
end
object Edit1: TMenuItem
Caption = 'Edit'
object Cut1: TMenuItem
Caption = 'Cut'
end
object Copy1: TMenuItem
Caption = 'Copy'
end
object Paste1: TMenuItem
Caption = 'Paste'
end
end
object View1: TMenuItem
Caption = 'View'
end
end
end
JD Solutions