lazarus/components/fpvectorial/examples/svgviewer/svmain.pas

595 lines
16 KiB
ObjectPascal

unit svMain;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
StdCtrls, ComCtrls, Spin, ShellCtrls, Buttons, fpVectorial, SynEdit,
SynHighlighterXML, types;
type
{ TMainForm }
TMainForm = class(TForm)
BtnExternalViewer: TButton;
CbAdjustPenColorToBackground: TCheckBox;
CbHistory: TComboBox;
EdMargin: TSpinEdit;
Image: TImage;
ImageList1: TImageList;
Label3: TLabel;
LeftPanel: TPanel;
PageControl: TPageControl;
ImagePanel: TPanel;
MouseWheelTimer: TTimer;
ControlPanel: TPanel;
Panel2: TPanel;
ShellListView: TShellListView;
ShellTreeView: TShellTreeView;
ShellSplitter: TSplitter;
MainSplitter: TSplitter;
StatusBar: TStatusBar;
SynEdit: TSynEdit;
SynXMLSyn1: TSynXMLSyn;
PgImage: TTabSheet;
PgSource: TTabSheet;
procedure BtnExternalViewerClick(Sender: TObject);
procedure CbAdjustPenColorToBackgroundChange(Sender: TObject);
procedure CbHistoryCloseUp(Sender: TObject);
procedure CbHistoryDrawItem(Control: TWinControl; Index: Integer;
ARect: TRect; State: TOwnerDrawState);
procedure CbHistoryEditingDone(Sender: TObject);
procedure EdOffsetXChange(Sender: TObject);
procedure FormActivate(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure ImageMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure ImageMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
procedure ImageMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
procedure ImagePanelMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
procedure MouseWheelTimerTimer(Sender: TObject);
procedure PageControlChange(Sender: TObject);
procedure ShellListViewCompare(Sender: TObject; Item1, Item2: TListItem;
Data: Integer; var Compare: Integer);
procedure ShellListViewSelectItem(Sender: TObject; Item: TListItem;
Selected: Boolean);
procedure ShellTreeViewGetImageIndex(Sender: TObject; Node: TTreeNode);
procedure ShellTreeViewGetSelectedIndex(Sender: TObject; Node: TTreeNode);
function ShellTreeViewSortCompare(Item1, Item2: TFileItem): integer;
private
{ private declarations }
FVec: TvVectorialDocument;
FScale: Double;
FScaleEff: Double;
FImgSize: TSize;
FZoomPos: TPoint;
FMousePos: TPoint;
FImgPos: TPoint;
FFormActivated: boolean;
FFileName: string;
procedure DrawSVG(ACanvas: TCanvas; ACanvasSize: TSize);
procedure LoadSVG(AFilename: String);
procedure PrepareCanvas(ACanvas: TCanvas);
procedure RedrawSVG;
procedure UpdateHistory(AFileName: String);
procedure UpdateStatusInfo;
procedure ReadIni;
procedure WriteIni;
public
{ public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.lfm}
uses
LCLIntf, LCLType, LCLProc, IniFiles, LazFileUtils, FileCtrl,
svgVectorialReader;
const
PROGRAM_NAME = 'svgViewer';
ZOOM_FACTOR = 1.05;
DO_NOT_DRAW = false;
INCH = 25.4;
function mm2px(mm: Double): Integer;
begin
Result := round(mm/INCH * Screen.PixelsPerInch);
end;
{ TMainForm }
procedure TMainForm.CbAdjustPenColorToBackgroundChange(Sender: TObject);
begin
RedrawSVG;
end;
procedure TMainForm.BtnExternalViewerClick(Sender: TObject);
begin
OpenDocument(FFileName);
end;
procedure TMainForm.CbHistoryCloseUp(Sender: TObject);
var
dir: String;
begin
if CbHistory.ItemIndex = -1 then
exit;
dir := CbHistory.Items[CbHistory.ItemIndex];
ShellTreeView.Path := dir;
end;
procedure TMainForm.CbHistoryDrawItem(Control: TWinControl; Index: Integer;
ARect: TRect; State: TOwnerDrawState);
var
s: String;
combobox: TComboBox;
begin
combobox := Control as TComboBox;
s := MinimizeName(combobox.Items[Index], combobox.Canvas, combobox.ClientWidth);
combobox.Canvas.TextOut(ARect.Left, ARect.Top, s);
end;
procedure TMainForm.CbHistoryEditingDone(Sender: TObject);
begin
UpdateHistory(AppendPathDelim(CbHistory.Text)+'.');
ShellTreeView.Path := CbHistory.Text;
ShellTreeView.MakeSelectionVisible;
end;
{ Paint the svg image into the provided canvas. It is the canvas of the
Image's bitmap }
procedure TMainForm.DrawSVG(ACanvas: TCanvas; ACanvasSize: TSize);
var
page: TvPage;
dx, dy: Integer;
begin
// Erase background of the image
ACanvas.Brush.Color := clWhite;
ACanvas.FillRect(0, 0, ACanvasSize.cx, ACanvasSize.cy);
if FVec = nil then
exit;
// Get page of vectorial file
page := FVec.GetPage(0);
page.AdjustPenColorToBackground := CbAdjustPenColorToBackground.Checked;
// Measure size of drawing
// page.Render(ACanvas, 0, 0, FScaleEff, FScaleEff, DO_NOT_DRAW);
// Draw background as defined in the file
page.DrawBackground(ACanvas);
// Draw the page
dx := EdMargin.Value - page.RenderInfo.EntityCanvasMinXY.X;
dy := EdMargin.Value - page.RenderInfo.EntityCanvasMinXY.Y;
page.Render(ACanvas, dx, dy, FScaleEff, FScaleEff);
end;
procedure TMainForm.EdOffsetXChange(Sender: TObject);
begin
RedrawSVG;
end;
procedure TMainForm.FormActivate(Sender: TObject);
var
fn: string;
begin
if FFormActivated then
exit;
FFormActivated := true;
if (ParamCount > 0) then
fn := ExpandfileName(ParamStr(1))
else
fn := FFileName;
if (fn <> '') and FileExists(fn) then
begin
ShellTreeView.Path := ExtractFilePath(fn);
ShellListView.Selected := ShellListView.Items.FindCaption(0, ExtractFileName(fn), false, true, false);
LoadSVG(fn);
end;
end;
procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if CanClose then
try
WriteIni;
finally
end;
end;
{ Paint the svg file if specified as a command line parameter }
procedure TMainForm.FormCreate(Sender: TObject);
begin
Caption := PROGRAM_NAME;
ShellListview.Mask := '*.svg';
{$IFNDEF MSWINDOWS}
ShellTreeView.Images := ImageList1;
ShellTreeView.OnGetImageIndex := @ShellTreeViewGetImageIndex;
ShellTreeView.OnGetSelectedIndex := @ShellTreeViewGetSelectedIndex;
{$ENDIF}
ReadIni;
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
FreeAndNil(FVec);
end;
{ Remember the mouse-down position for panning }
procedure TMainForm.ImageMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Shift = [ssLeft] then
FMousePos := Point(X, Y);
end;
{ Pan the svg bitmap image while dragging the mouse }
procedure TMainForm.ImageMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
if Shift = [ssLeft] then
begin
Image.Left := Image.Left + X - FMousePos.X;
Image.Top := Image.Top + Y - FMousePos.Y;
FImgPos.X := Image.Left;
FImgPos.Y := Image.Top;
end;
end;
{ Mouse wheel is for zooming into the svg bitmap image }
procedure TMainForm.ImageMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
f: Double;
begin
// Do not paint the svg here, this will take too long in case of large
// zoom factors. Instead, trigger a timer. Only the last mouse wheel event
// will cause a repaint. But after each mouse wheel event, we must
// calculate the new image position, because catching the zoom point is
// cumulative.
if WheelDelta > 0 then
f := ZOOM_FACTOR
else
f := 1.0 / ZOOM_FACTOR;
FScale := f * FScale;
FScaleEff := FScaleEff * f;
FZoomPos.X := MousePos.X;
FZoomPos.Y := MousePos.Y;
if Sender = Image then
begin
inc(FZoomPos.X, Image.Left);
inc(FZoomPos.Y, Image.Top);
end;
FImgPos.X := round(FZoomPos.X - f * (FZoomPos.X - FImgPos.X));
FImgPos.Y := round(FZoomPos.Y - f * (FZoomPos.Y - FImgPos.Y));
MouseWheelTimer.Enabled := true;
end;
procedure TMainForm.ImagePanelMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
begin
ImageMouseWheel(Sender, Shift, WheelDelta, MousePos, Handled);
end;
{ Loads an svg image. Zooms and centers it into the image. }
procedure TMainForm.LoadSVG(AFileName: String);
var
page: TvPage;
bmp: TBitmap;
rimg, rsvg, factor: Double;
m2: Integer;
begin
if AFilename = '' then
exit;
if not FileExists(AFileName) then
begin
MessageDlg(Format('File "%s" not found.', [AFileName]), mtError, [mbOK], 0);
exit;
end;
FFileName := AFileName;
// Load the file into the memo
SynEdit.Lines.LoadFromFile(AFileName);
// Load the svg file into a TvVectorialDocument
FreeAndNil(FVec);
FVec := TvVectorialDocument.Create;
FVec.ReadFromFile(AFilename);
if FVec.GetPageCount = 0 then
begin
MessageDlg('Document has no pages.', mtError, [mbOK], 0);
FreeAndNil(FVec);
exit;
end;
// Get used page size of svg file
page := FVec.GetPage(0);
// svg units are millimeters.
factor := mm2px(1.0);
FScale := 1.0;
// Calculate image size by using a temporary bitmap since we need a canvas
bmp := TBitmap.Create;
try
bmp.SetSize(1, 1);
page.Render(bmp.Canvas, 0, 0, factor, factor, DO_NOT_DRAW);
FImgSize.cx := page.RenderInfo.EntityCanvasMaxXY.X - page.RenderInfo.EntityCanvasMinXY.X;
FImgSize.cy := page.RenderInfo.EntityCanvasMaxXY.Y - page.RenderInfo.EntityCanvasMinXY.Y;
finally
bmp.Free;
end;
// Calculate initial zoom factor and offset coordinates such that drawing
// fits into the image area
m2 := EdMargin.Value * 2;
if (FImgSize.cx + m2 <= Image.Parent.Width) and
(FImgSize.cy + m2 <= Image.Parent.Height) then
begin
FImgPos.X := (Image.Parent.Width - m2 - FImgSize.cx) div 2;
FImgPos.Y := (Image.Parent.Height - m2 - FImgSize.cy) div 2;
FScale := 1.0;
end else
begin
rimg := Image.Parent.Height / Image.Parent.Width;
rsvg := (FImgSize.cy + m2) / (FImgSize.cx + m2);
if rimg > rsvg then
begin
FScale := Image.Parent.Width / (FImgSize.cx + m2);
FImgPos.X := -m2 div 2;
FImgPos.Y := round((Image.Parent.Height - FScale * FImgSize.cy - m2) / 2);
// FImgPos.Y := 0;
end else
begin
FScale := Image.Parent.Height / (FImgSize.cy + m2);
FImgPos.X := round((Image.Parent.Width - FScale * FImgSize.cx - m2) / 2);
FImgPos.Y := -m2 div 2;
end;
end;
// Overall magnification factor
FScaleEff := FScale * factor;
// Paint the initial version of the file
PrepareCanvas(Image.Picture.Bitmap.Canvas);
DrawSVG(Image.Picture.Bitmap.Canvas, FImgSize);
// Update information in form
Caption := Format('%s - %s', [PROGRAM_NAME, ExpandFilename(AFilename)]);
UpdateStatusInfo;
end;
procedure TMainForm.MouseWheelTimerTimer(Sender: TObject);
begin
// Stop the timer
MouseWheelTimer.Enabled := false;
// Repaint the svg image.
RedrawSVG;
end;
procedure TMainForm.PageControlChange(Sender: TObject);
begin
if PageControl.ActivePage = PgImage then
UpdateStatusInfo
else
Statusbar.Panels[0].Text := '';
end;
procedure TMainForm.ShellListViewCompare(Sender: TObject; Item1,
Item2: TListItem; Data: Integer; var Compare: Integer);
var
fn1, fn2: String;
begin
fn1 := Item1.Caption;
fn2 := Item1.Caption;
if (fn1[1] = '_') or (fn2[1] = '_') then
Compare := AnsiCompareText(fn1, fn2)
else
Compare := CompareText(fn1, fn2);
end;
procedure TMainForm.ShellListViewSelectItem(Sender: TObject; Item: TListItem;
Selected: Boolean);
var
fn: String;
begin
if Selected then
begin
fn := ShellListview.GetPathFromItem(Item);
ShellTreeview.MakeSelectionVisible;
LoadSVG(fn);
UpdateHistory(fn);
end;
end;
procedure TMainForm.ShellTreeViewGetImageIndex(Sender: TObject;
Node: TTreeNode);
begin
Node.ImageIndex := 0;
end;
procedure TMainForm.ShellTreeViewGetSelectedIndex(Sender: TObject;
Node: TTreeNode);
begin
Node.SelectedIndex := 1;
end;
// https://forum.lazarus.freepascal.org/index.php/topic,61347.msg462091.html#msg462091
function TMainForm.ShellTreeViewSortCompare(Item1, Item2: TFileItem): integer;
var
fn1, fn2: String;
begin
// Make sure that folders are moved to the top
Result := ord(Item2.isFolder) - ord(Item1.isFolder);
if Result = 0 then
begin
fn1 := Item1.FileInfo.Name;
fn2 := Item2.FileInfo.Name;
// Move names with leading underscors to the top
if (fn1[1] = '_') or (fn2[1] = '_') then
Result := AnsiCompareText(fn1, fn2)
else
Result := CompareText(fn1, fn2);
end;
end;
procedure TMainForm.PrepareCanvas(ACanvas: TCanvas);
var
page: TvPage;
begin
page := FVec.GetPage(0);
page.Render(ACanvas, 0, 0, FScaleEff, FScaleEff, DO_NOT_DRAW);
FImgSize.cx := page.RenderInfo.EntityCanvasMaxXY.X - page.RenderInfo.EntityCanvasMinXY.X + 2*EdMargin.Value;
FImgSize.cy := page.RenderInfo.EntityCanvasMaxXY.Y - page.RenderInfo.EntityCanvasMinXY.Y + 2*EdMargin.Value;
Image.SetBounds(FImgPos.X, FImgPos.Y, FImgSize.cx, FImgSize.cy);
Image.Picture.Bitmap.SetSize(FImgSize.cx, FImgSize.cy);
end;
procedure TMainForm.ReadIni;
var
ini: TIniFile;
L, T, W, H, p: Integer;
R: TRect;
List: TStrings;
i: Integer;
s: String;
begin
ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
T := Ini.ReadInteger('Position', 'Top', Top);
L := Ini.ReadInteger('Position', 'Left', Left);
W := Ini.ReadInteger('Position', 'Width', Width);
H := Ini.ReadInteger('Position', 'Height', Height);
R := Screen.WorkAreaRect;
if W > R.Width then W := R.Width;
if H > R.Height then H := R.Height;
if L < R.Left then L := R.Left;
if T < R.Top then T := R.Top;
if L + W > R.Right then L := R.Right - W - GetSystemMetrics(SM_CXSIZEFRAME);
if T + H > R.Bottom then T := R.Bottom - H - GetSystemMetrics(SM_CYCAPTION) - GetSystemMetrics(SM_CYSIZEFRAME);
SetBounds(L, T, W, H);
WindowState := wsNormal;
Application.ProcessMessages;
WindowState := TWindowState(ini.ReadInteger('Position', 'WindowState', 0));
p := ini.ReadInteger('Settings', 'LeftSplitter', LeftPanel.Width);
if p > ClientWidth then p := ClientWidth div 4;
LeftPanel.Width := p;
p := ini.ReadInteger('Settings', 'ShellSplitter', ShellTreeView.Height);
if p > ClientHeight then p := ClientHeight div 2;
ShellTreeView.Height := p;
FFileName := ini.ReadString('Settings', 'Filename', '');
CbHistory.Items.BeginUpdate;
List := TStringList.Create;
try
ini.ReadSection('History', List);
CbHistory.Items.Clear;
for i := 0 to List.Count-1 do
begin
s := ini.ReadString('History', Format('Item%d', [i+1]), '');
if (s <> '') and DirectoryExists(s) then
CbHistory.Items.Add(s);
end;
if CbHistory.Items.Count > 0 then
CbHistory.ItemIndex := 0;
finally
List.Free;
CbHistory.Items.EndUpdate;
end;
finally
ini.Free;
end;
end;
procedure TMainForm.RedrawSVG;
begin
// Repaint the svg image.
PrepareCanvas(Image.Picture.Bitmap.Canvas);
DrawSVG(Image.Picture.Bitmap.Canvas, FImgSize);
// Update some status information
UpdateStatusInfo;
end;
procedure TMainForm.UpdateHistory(AFileName: String);
var
dir: String;
idx: Integer;
begin
if AFileName = '' then
exit;
dir := ExtractFilePath(AFileName);
idx := CbHistory.Items.IndexOf(dir);
if idx = -1 then
CbHistory.Items.Insert(0, dir)
else
CbHistory.Items.Move(idx, 0);
while CbHistory.Items.Count > 10 do
CbHistory.Items.Delete(CbHistory.Items.Count-1);
CbHistory.ItemIndex := 0;
end;
procedure TMainForm.UpdateStatusInfo;
var
page: TvPage;
begin
page := FVec.GetPage(0);
Statusbar.Panels[0].Text := Format('page.width=%.1f page.height=%.1f w=%d h=%d scale=%.2f', [
page.Width, page.Height, Image.Width, Image.Height, FScale
]);
end;
procedure TMainForm.WriteIni;
var
ini: TIniFile;
i: Integer;
begin
ini := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
try
ini.WriteInteger('Position', 'Top', RestoredTop);
ini.WriteInteger('Position', 'Left', RestoredLeft);
ini.WriteInteger('Position', 'Width', RestoredWidth);
ini.WriteInteger('Position', 'Height', RestoredHeight);
ini.WriteInteger('Position', 'WindowState', Integer(WindowState));
ini.WriteInteger('Settings', 'LeftSplitter', LeftPanel.Width);
ini.WriteInteger('Settings', 'ShellSplitter', ShellTreeView.Height);
ini.WriteString('Settings', 'FileName', FFileName);
ini.EraseSection('History');
for i := 0 to CbHistory.Items.Count-1 do
ini.WriteString('History', Format('Item%d', [i+1]), CbHistory.Items[i]);
finally
ini.Free;
end;
end;
end.