6
votes

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.
1
I can't even begin to imagine what "zoom to" point would do. I would "zoom to" a rectangle, not a point. I can't guess what the implementation of your class looks like so I can't guess what math you need, nor could anybody else.Warren P
@WarrenP Suppose a photo of multiple people is displayed, mouse is pointed in the center of a person's face. When the user holds the control key and scrolls the mouse wheel up, it will zoom into that person's face, with the mouse pointer still in the same position of the picture. This is why I'm zooming to a Point and not a Rect. I'm pretty sure I included all relevant code above to demonstrate how I handle the mouse events.Jerry Dodge

1 Answers

4
votes

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.