4

My problem is with a custom control I am trying to develop and I cannot seem to figure out how to implement the scroll bars correctly. I will highlight in key points what I am trying to do to make the question easier to understand.

  • The control will be a simple image viewer, the image will be drawn in the center of the control.
  • The control derives from TScrollingWinControl.
  • I have a published property called FImage which is a TPicture class, this allows loading a image into the control.
  • There will be no child controls added as I will be painting the FImage onto the control.
  • In the constructor I have written AutoScroll := False;
  • I have intercepted the WM_SIZE message and here I determine offsets for centering FImage to the middle of the control and also try to recalculate the scroll ranges.
  • Finally I override the Paint method to draw the centered FImage onto the control.

So far so good, an image can be loaded at design or runtime and is displayed in the center of the control. Now I cannot understand how to get the scrolling set up properly.

Here is the relevant code so far:

unit uImageViewer;

interface

uses
  Winapi.Windows,
  Winapi.Messages,
  System.Classes,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Graphics;

type
  TMyImageViewer = class(TScrollingWinControl)
  private
    FCanvas: TCanvas;
    FImage: TPicture;
    FOffsetX: Integer; // center position in control for FImage
    FOffsetY: Integer; // center position in control for FImage
    procedure SetImage(const Value: TPicture);
  private
    procedure CalculateOffsets; //recalculates the center for FImage
    procedure CalculateScrollRanges;
  protected
    procedure Loaded; override;
    procedure PaintControl;
    procedure PaintWindow(DC: HDC); override;
    procedure WMEraseBkGnd(var Message: TMessage); message WM_ERASEBKGND;
    procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
    procedure WMSize(var Message: TMessage); message WM_SIZE;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    property Canvas: TCanvas read FCanvas;
  published
    property Align;

    property Color;
    property Image: TPicture read FImage write SetImage;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Standard', [TMyImageViewer]);
end;

constructor TMyImageViewer.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  FCanvas := TControlCanvas.Create;
  TControlCanvas(FCanvas).Control:=Self;

  FImage := TPicture.Create;
  Self.AutoSize := False; //?
  AutoScroll := False;

  ControlStyle := ControlStyle + [csOpaque];
end;

destructor TMyImageViewer.Destroy;
begin
  FCanvas.Free;
  FImage.Free;
  inherited Destroy;
end;

procedure TMyImageViewer.Loaded;
begin
  inherited Loaded;
  CalculateOffsets;
  CalculateScrollRanges;
end;

procedure TMyImageViewer.PaintControl;

  procedure DrawClientBackground; // paints the control color
  begin
    Canvas.Brush.Color  := Color;
    Canvas.Brush.Style  := bsSolid;
    Canvas.FillRect(ClientRect);
  end;

begin
 // if not (csDesigning in ComponentState) then
 // begin
  DrawClientBackground;

  // draw the FImage
  if (FImage <> nil) and (FImage.Graphic <> nil) then
  begin
    Canvas.Draw(FOffsetX, FOffsetY, FImage.Graphic);
  end;
//  end;

end;

procedure TMyImageViewer.PaintWindow(DC: HDC);
begin
  FCanvas.Handle := DC;
  try
    PaintControl;
  finally
    FCanvas.Handle := 0;
  end;
end;

procedure TMyImageViewer.SetImage(const Value: TPicture);
begin
  if Value <> FImage then
  begin
    FImage.Assign(Value);
    CalculateOffsets;
    CalculateScrollRanges;
    Invalidate;
  end;
end;

procedure TMyImageViewer.CalculateOffsets;
begin
  // for centering FImage in the middle of the control
  if FImage.Graphic <> nil then
  begin
    FOffsetX := (Width - FImage.Width) div 2;
    FOffsetY := (Height - FImage.Height) div 2;
  end;
end;

procedure TMyImageViewer.CalculateScrollRanges;
begin
  HorzScrollBar.Range:= FOffsetX + FImage.Width + FOffsetX;
  VertScrollBar.Range:=  FOffsetY + FImage.Height + FOffsetY;
end;

procedure TMyImageViewer.WMEraseBkGnd(var Message: TMessage);
begin
  Message.Result := 1;
end;

procedure TMyImageViewer.WMPaint(var Message: TWMPaint);
begin
  PaintHandler(Message);
end;

procedure TMyImageViewer.WMSize(var Message: TMessage);
begin
  inherited;

  CalculateOffsets;
  CalculateScrollRanges;
  Invalidate;
end;

end.

I originally started writing this in Lazarus but would also like to use it in Delphi hence both tags have been added.

How exactly should the scrollbars be calculated? Bearing in mind there is no children or auto scrolling enabled so it must be manual calculations, I am simply drawing a image in the center of the control and need to know how to calculate the scrollbar ranges etc.

I have tried a few different things with no success and it just seems like I am now putting anything in and hoping for the best, so I really could do with some guidance here please.

EDIT

So having tried running the original code in Delphi has now made me realise how much more different Lazarus is, lots of things had to be changed to run under Delphi and even right now the scrollbars are disappearing.

Craig
  • 1,874
  • 13
  • 41

2 Answers2

1

As Garth already answered, you should set the scroll bar's range to the size of the picture. But that is not enough. You must realize that you need two distinct kinds of placement behaviour of your image: When the scroll bar is visible (1), you are able to pan the image to an uncentered position, but when the scroll bar is not visible (2), the image should automatically center. This requires a similar distinction in your code.

Also, you are making it yourself a little hard by wanting to paint on a TScrollingWinControl. To acquire a canvas, the most easy way is by mimicking the implementation of TCustomControl, which I kind of did in the example shown below, and then your code could look like:

unit AwImageViewer;

interface

uses
  Winapi.Windows, Winapi.Messages, System.Classes, Vcl.Controls, Vcl.Forms,
  Vcl.Graphics;

type
  TAwImageViewer = class(TScrollingWinControl)
  private
    FPicture: TPicture;
    procedure PictureChanged(Sender: TObject);
    procedure SetPicture(Value: TPicture);
    procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
  protected
    procedure PaintWindow(DC: HDC); override;
    procedure Resize; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Color;
    property Picture: TPicture read FPicture write SetPicture;
  end;

implementation

{ TAwImageViewer }

constructor TAwImageViewer.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FPicture := TPicture.Create;
  FPicture.OnChange := PictureChanged;
end;

destructor TAwImageViewer.Destroy;
begin
  FPicture.Free;
  inherited Destroy;
end;

procedure TAwImageViewer.PaintWindow(DC: HDC);
var
  Canvas: TCanvas;
  R: TRect;
begin
  if FPicture.Graphic = nil then
    inherited PaintWindow(DC)
  else
  begin
    Canvas := TCanvas.Create;
    try
      Canvas.Lock;
      try
        Canvas.Handle := DC;
        try
          if ClientWidth > FPicture.Width then
            R.Left := (ClientWidth - FPicture.Width) div 2
          else
            R.Left := -HorzScrollBar.Position;
          if ClientHeight > FPicture.Height then
            R.Top := (ClientHeight - FPicture.Height) div 2
          else
            R.Top := -VertScrollBar.Position;
          R.Width := FPicture.Width;
          R.Height := FPicture.Height;
          Canvas.Draw(R.Left, R.Top, FPicture.Graphic);
          ExcludeClipRect(DC, R.Left, R.Top, R.Right, R.Bottom);
          FillRect(DC, ClientRect, Brush.Handle);
        finally
          Canvas.Handle := 0;
        end;
      finally
        Canvas.Unlock;
      end;
    finally
      Canvas.Free;
    end;
  end;
end;

procedure TAwImageViewer.PictureChanged(Sender: TObject);
begin
  HorzScrollBar.Range := FPicture.Width;
  VertScrollBar.Range := FPicture.Height;
  Invalidate;
end;

procedure TAwImageViewer.Resize;
begin
  HorzScrollBar.Position := (FPicture.Width - ClientWidth) div 2;
  VertScrollBar.Position := (FPicture.Height - ClientHeight) div 2;
  if HorzScrollBar.Position * VertScrollBar.Position = 0 then
    Invalidate;
  inherited Resize;
end;

procedure TAwImageViewer.SetPicture(Value: TPicture);
begin
  FPicture.Assign(Value);
end;

procedure TAwImageViewer.WMPaint(var Message: TWMPaint);
begin
  ControlState := ControlState + [csCustomPaint];
  inherited;
  ControlState := ControlState - [csCustomPaint];
end;

end.

And if you prepare your painting on a temporary bitmap, then you do not need a canvas:

procedure TAwImageViewer.PaintWindow(DC: HDC);
var
  Bmp: TBitmap;
  R: TRect;
begin
  if FPicture.Graphic = nil then
    inherited PaintWindow(DC)
  else
  begin
    Bmp := TBitmap.Create;
    try
      Bmp.Canvas.Brush.Assign(Brush);
      Bmp.SetSize(ClientWidth, ClientHeight);
      if ClientRect.Width > FPicture.Width then
        R.Left := (ClientWidth - FPicture.Width) div 2
      else
        R.Left := -HorzScrollBar.Position;
      if ClientHeight > FPicture.Height then
        R.Top := (ClientHeight - FPicture.Height) div 2
      else
        R.Top := -VertScrollBar.Position;
      R.Width := FPicture.Width;
      R.Height := FPicture.Height;
      Bmp.Canvas.Draw(R.Left, R.Top, FPicture.Graphic);
      BitBlt(DC, 0, 0, ClientWidth, ClientHeight, Bmp.Canvas.Handle, 0, 0,
        SRCCOPY);
    finally
      Bmp.Free;
    end;
  end;
end;

But if you place a TImage component on your control, then this all becomes much more simple:

unit AwImageViewer2;

interface

uses
  System.Classes, Vcl.Forms, Vcl.Graphics, Vcl.ExtCtrls;

type
  TAwImageViewer = class(TScrollingWinControl)
  private
    FImage: TImage;
    function GetPicture: TPicture;
    procedure SetPicture(Value: TPicture);
  protected
    procedure Resize; override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property Color;
    property Picture: TPicture read GetPicture write SetPicture;
  end;

implementation

{ TAwImageViewer }

constructor TAwImageViewer.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  AutoScroll := True;
  FImage := TImage.Create(Self);
  FImage.AutoSize := True;
  FImage.Parent := Self;
end;

function TAwImageViewer.GetPicture: TPicture;
begin
  Result := FImage.Picture;
end;

procedure TAwImageViewer.Resize;
begin
  if ClientWidth > FImage.Width then
    FImage.Left := (ClientWidth - FImage.Width) div 2
  else
    HorzScrollBar.Position := (FImage.Width - ClientWidth) div 2;
  if ClientHeight > FImage.Height then
    FImage.Top := (ClientHeight - FImage.Height) div 2
  else
    VertScrollBar.Position := (FImage.Height - ClientHeight) div 2;
  inherited Resize;
end;

procedure TAwImageViewer.SetPicture(Value: TPicture);
begin
  FImage.Picture := Value;
end;

end.
Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200
  • Thanks, I knew about using the TImage component but I don't like how it shows at designtime as I wanted it to look like my own control. I will look over your answer closely thanks :) – Craig Jan 29 '16 at 14:52
  • Is there a way to get the scrollbars to not jump? For example I loaded a bitmap and scrolled the very left and then when I resized the window the scrollbars jumped (the control was aligned to client). – Craig Jan 30 '16 at 22:35
  • I don't know why `TScrollingWinControl` does not feature a canvas anyway I think it should do, it kind of makes you have to choose between `TCustomControl` and `TScrollingWinControl`. – Craig Jan 31 '16 at 13:49
  • On designtime `TImage presentation`: if you do not load the picture designtime, then you set its size 1 px bigger and the designer will not show it. Set `Autoscroll` to `False` in that case. But without TImage, it works just fine, as I showed in two examples. – NGLN Feb 01 '16 at 16:16
  • On jumpy scroll bars: when the component resizes, the range of a scroll bar does too, and the thumb of the scroll bar will resize accordingly. You cannot prevent that. If you mean that the position of the scroll bar changes: well you can prevent that by not altering the position in the `Resize` method, but what I understood from your question is that you want it to center automatically. If not, then correct it yourself, you have the code. – NGLN Feb 01 '16 at 16:18
  • On `TCustomControl` vs `TScrollingWinControl`: the former is designed for custom drawing/painting, the latter is designed for custom placement of child controls. A control which has both these features doesn't exist in the VCL. But you can easily make one yourself, as I showed with these examples. Just look at the code of `TCustomControl`, and you are set. – NGLN Feb 01 '16 at 16:20
  • Ok, this gives me enough to work with and think about thank you. – Craig Feb 01 '16 at 19:57
0

Just set the scrollbar ranges to the width and height of the image, and the offsets to the scrollbar positions. You may need to use height-Foffsety instead for drawing, depending on your bitmap format.

Garth Thornton
  • 94
  • 1
  • 1
  • 7
  • I know this part, I calculate where the X and Y of the centered image will be drawn and when intercepting the `WM_Size` message I work out the scrollbar ranges from the image dimensions and X,Y offsets. As I was using Lazarus primarily I think there are some quirks and differences to Delphi, Lazarus may be the problem here with its implementation of the TScrollingWinControl. Guess I need to try the other way around now and try and set it up in Delphi first and then try again with Lazarus. As much as I like Lazarus and how far it has come there are always some niggly things with using it. – Craig Jan 25 '16 at 12:33