Zooming in/out of a TImage inside a TScrollBox to a particular focus?
controls, delphi, delphi-xe2, mousewheel, zooming
Solution
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.
Problem
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. ```