Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zooming in/out of a TImage inside a TScrollBox to a particular focus?

I'm making a simple control based on a TScrollingWinControl (and code copied from a TScrollBox) with a TImage control. I somewhat got the zooming to work, but it doesn't necessarily zoom to a focused point - the scrollbars don't change accordingly to keep the center point in focus.

I would like to be able to tell this control ZoomTo(const X, Y, ZoomBy: Integer); to tell it where to zoom the focus to. So when it zooms, the coordinates I passed will stay 'centered'. At the same time, I also need to have a ZoomBy(const ZoomBy: Integer); which tells it to keep it centered in the current view.

For example, there will be one scenario where the mouse is pointed at a particular point of the image, and when holding control and scrolling the mouse up, it should zoom in focused on the mouse pointer. On the other hand, another scenario would be sliding a control to adjust the zoom level, in which case it just needs to keep the center of the current view (not necessarily center of the image) focused.

The problem is my math gets lost at this point, and I can't figure out the right formula to adjust these scroll bars. I've tried a few different ways of calculating, nothing seems to work right.

Here's a stripped version of my control. I removed most to only the relevant stuff, original unit is over 600 lines of code. The most important procedure below is SetZoom(const Value: Integer);

unit JD.Imaging;

interface

uses
  Windows, Classes, SysUtils, Graphics, Jpeg, PngImage, Controls, Forms,
  ExtCtrls, Messages;

type
  TJDImageBox = class;

  TJDImageZoomEvent = procedure(Sender: TObject; const Zoom: Integer) of object;

  TJDImageBox = class(TScrollingWinControl)
  private
    FZoom: Integer; //level of zoom by percentage
    FPicture: TImage; //displays image within scroll box
    FOnZoom: TJDImageZoomEvent; //called when zoom occurs
    FZoomBy: Integer; //amount to zoom by (in pixels)
    procedure MouseWheel(Sender: TObject; Shift: TShiftState;
      WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
    procedure SetZoom(const Value: Integer);
    procedure SetZoomBy(const Value: Integer);
  public
    constructor Create(AOwner: TComponent); override;
  published
    property Zoom: Integer read FZoom write SetZoom;
    property ZoomBy: Integer read FZoomBy write SetZoomBy;
    property OnZoom: TJDImageZoomEvent read FOnZoom write FOnZoom;
  end;

implementation

{ TJDImageBox }

constructor TJDImageBox.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  OnMouseWheel:= MouseWheel;
  ControlStyle := [csAcceptsControls, csCaptureMouse, csClickEvents,
    csSetCaption, csDoubleClicks, csPannable, csGestures];
  AutoScroll := True;
  TabStop:= True;
  VertScrollBar.Tracking:= True;
  HorzScrollBar.Tracking:= True;
  Width:= 100;
  Height:= 100;
  FPicture:= TImage.Create(nil);
  FPicture.Parent:= Self;
  FPicture.AutoSize:= False;
  FPicture.Stretch:= True;
  FPicture.Proportional:= True;
  FPicture.Left:= 0;
  FPicture.Top:= 0;
  FPicture.Width:= 1;
  FPicture.Height:= 1;
  FPicture.Visible:= False;
  FZoom:= 100;
  FZoomBy:= 10;
end;

destructor TJDImageBox.Destroy;
begin
  FImage.Free;
  FPicture.Free;
  inherited;
end;

procedure TJDImageBox.MouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  NewScrollPos: Integer;
begin
  if ssCtrl in Shift then begin
    if WheelDelta > 0 then
      NewScrollPos := Zoom + 5
    else
      NewScrollPos:= Zoom - 5;
    if NewScrollPos >= 5 then
      Zoom:= NewScrollPos;
  end else
  if ssShift in Shift then begin
    NewScrollPos := HorzScrollBar.Position - WheelDelta;
    HorzScrollBar.Position := NewScrollPos;
  end else begin
    NewScrollPos := VertScrollBar.Position - WheelDelta;
    VertScrollBar.Position := NewScrollPos;
  end;
  Handled := True;
end;

procedure TJDImageBox.SetZoom(const Value: Integer);
var
  Perc: Single;
begin
  FZoom := Value;
  if FZoom < FZoomBy then
    FZoom:= FZoomBy;
  Perc:= FZoom / 100;
  //Resize picture to new zoom level
  FPicture.Width:= Trunc(FImage.Width * Perc);
  FPicture.Height:= Trunc(FImage.Height * Perc);
  //Move scroll bars to properly position the center of the view
  //This is where I don't know how to calculate the 'center'
  //or by how much I need to move the scroll bars.
  HorzScrollBar.Position:= HorzScrollBar.Position - (FZoomBy div 2);
  VertScrollBar.Position:= VertScrollBar.Position - (FZoomBy div 2);
  if assigned(FOnZoom) then
    FOnZoom(Self, FZoom);
end;

procedure TJDImageBox.SetZoomBy(const Value: Integer);
begin
  if FZoomBy <> Value then begin
    FZoomBy := EnsureRange(Value, 1, 100);
    Paint;
  end;
end;

end.
like image 597
Jerry Dodge Avatar asked May 02 '12 01:05

Jerry Dodge


1 Answers

It's not clear what would you like to refer for X, Y when passing to 'ZoomBy()'. I'll assume you've put an 'OnMouseDown' handler for the image and the coordinates refer to where you click on the image, i.e. they're not relative to scrollbox coordinates. If this is not so, you can tweak it yourself.

Let's forget about zooming for a minute, let our task be centering the point that we click on the image in the scrollbox. Easy, we know that the center of the scrollbox is at (ScrollBox.ClientWidth/2, ScrollBox.ClientHeight/2). Think horizontal, we want to scroll up to a point so that, if we add ClientWidth/2 to it, it will be our click point:

procedure ScrollTo(CenterX, CenterY: Integer);
begin
  ScrollBox.HorzScrollBar.Position := CenterX - Round(ScrollBox.ClientWidth / 2);
  ScrollBox.VertScrollBar.Position := CenterY - Round(ScrollBox.ClientHeight / 2);
end;


Now consider zooming. All we have to do is to calculate X, Y positions accordingly, the size of the scrollbox won't change. CenterX := Center.X * ZoomFactor. But be careful, 'ZoomFactor' here is not the effective zoom, it is the zoom that will be applied when we click on the image. I'll use the image's before and after dimensions to determine that:

procedure ZoomTo(CenterX, CenterY, ZoomBy: Integer);
var
  OldWidth, OldHeight: Integer;
begin
  OldWidth := FImage.Width;
  OldHeight := FImage.Height;

  // zoom the image, we have new image size and scroll range

  CenterX := Round(CenterX * FImage.Width / OldWidth);
  ScrollBox.HorzScrollBar.Position := CenterX - Round(ScrollBox.ClientWidth / 2);

  CenterY := Round(CenterY * FImage.Height / OldHeight);
  ScrollBox.VertScrollBar.Position := CenterY - Round(ScrollBox.ClientHeight / 2);
end; 

Of course, you'd refactor them into one line so that you call Round() only once to reduce rounding error.

I'm sure you can workout from here yourself.

like image 183
Sertac Akyuz Avatar answered Sep 21 '22 12:09

Sertac Akyuz