{
 Modified 20-Mar-2019 by Rickard Johansson (www.rj-texted.se).
 Purpose: Add per-monitor DPI awareness
 Usage: Add the unit to the interface uses statement of the main form and add code below:

 TMyForm = class(TForm)
 private
   FStyleDPIAwareness: TStyleDPIAwareness;

 procedure TFrmMain.FormCreate(Sender: TObject);
 begin
   FStyleDPIAwareness := TStyleDPIAwareness.Create(Self);
   FStyleDPIAwareness.Parent := Self;

 procedure TFrmMain.FormDestroy(Sender: TObject);
 begin
   FStyleDPIAwareness.Free;

 procedure TFrmMain.FormAfterMonitorDpiChanged(Sender: TObject; OldDPI, NewDPI: Integer);
 begin
   FStyleDPIAwareness.AfterDPIChange(OldDPI, NewDPI);
 end;

 procedure TFrmMain.FormBeforeMonitorDpiChanged(Sender: TObject; OldDPI, NewDPI: Integer);
 begin
   FStyleDPIAwareness.BeforeDPIChange(OldDPI, NewDPI);
 end;
}

{-----------------------------------------------------------------------------
 Unit Name: VCL.Styles.DPIAware
 Author:    PyScripter  (https://github.com/pyscripter)
 Date:      13-Nov-2017
 Purpose:   Use VCL Styles in DPI Aware applications by scaling styles
 History:
-----------------------------------------------------------------------------}
{
  To use the unit just add it to the implementation uses statement of the main form and add
  the following code to the FormCreate handler.

  procedure TFrmMain.FormCreate(Sender: TObject);
  Var
    StyleDPIAwareness: TStyleDPIAwareness;
  begin
    StyleDPIAwareness := TStyleDPIAwareness.Create(Self);
    StyleDPIAwareness.Parent := Self;

  By default the component scales the styles at multiples of 100%. You can change that,
  by adding the line:

  StyleDPIAwareness.RoundScalingFactor := False;

  With this statement styles are scaled to whatever scaling factor results for Screen.PixelsPerInch.
  Most of the styles would work fine, but a few may show some visual defects.

  Limitations:
    Does not support perMonitor DPI Awareness.
    You need to set DPI Awareness to System.
}

unit VCL.Styles.DPIAware;

interface
uses
  Winapi.Windows, WinAPI.Messages, System.SysUtils, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Themes, Vcl.Styles;

Type
  TStyleDPI = class(TObject)
  private
    FCurrentDPI: Integer;
  public
    property CurrentDPI: Integer read FCurrentDPI write FCurrentDPI;
  end;

  TStyleDPIAwareness = class(TControl)
  private
    FScaledStyles: TStringList;
    FRoundScalingFactor: Boolean;
    FUseCustomScalingFactor: Boolean;
    FCustomPPI: integer;
    FOldDPI: Integer;
   protected
    procedure CMStyleChanged(var Message: TMessage); message CM_STYLECHANGED;
    procedure RecreateForms;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure AfterDPIChange(OldDPI, NewDPI: Integer);
    procedure BeforeDPIChange(OldDPI, NewDPI: Integer);
    procedure ScaleStyle(Style: TCustomStyleServices);
    property OldDPI: Integer read FOldDPI write FOldDPI;
  published
    property  RoundScalingFactor: Boolean read FRoundScalingFactor
      write FRoundScalingFactor default True;
    property  UseCustomScalingFactor: Boolean read FUseCustomScalingFactor
      write FUseCustomScalingFactor default False;
    property CustomPPI: integer read FCustomPPI write FCustomPPI default 96;
  end;

implementation

Uses
  System.Rtti, DDetours, System.Math;

{ TStyleDPIAwareness }

procedure ResizeBitmap(Bitmap: TBitmap; const NewWidth, NewHeight: integer);
var
  buffer: TBitmap;
begin
  buffer := TBitmap.Create;
  try
    buffer.SetSize(NewWidth, NewHeight);
    buffer.Canvas.StretchDraw(Rect(0, 0, NewWidth, NewHeight), Bitmap);
    Bitmap.SetSize(NewWidth, NewHeight);
    Bitmap.Canvas.Draw(0, 0, buffer);
  finally
    buffer.Free;
  end;
end;

procedure TStyleDPIAwareness.CMStyleChanged(var Message: TMessage);
begin
  ScaleStyle(TStyleManager.ActiveStyle);
end;

constructor TStyleDPIAwareness.Create(AOwner: TComponent);
begin
  inherited;
  FRoundScalingFactor := True;
  FUseCustomScalingFactor := False;
  FCustomPPI := 96;
  FOldDPI := 96;

  FScaledStyles := TStringList.Create;
  FScaledStyles.Sorted := False;

  ScaleStyle(TStyleManager.ActiveStyle);
end;

destructor TStyleDPIAwareness.Destroy;
var
  i: Integer;
begin
  for i := 0 to FScaledStyles.Count - 1 do
    TStyleDPI(FScaledStyles.Objects[i]).Free;
  FScaledStyles.Free;
  inherited;
end;

procedure TStyleDPIAwareness.AfterDPIChange(OldDPI, NewDPI: Integer);
begin
  ScaleStyle(TStyleManager.ActiveStyle);
end;

procedure TStyleDPIAwareness.BeforeDPIChange(OldDPI, NewDPI: Integer);
begin
  FOldDPI := OldDPI;
end;

procedure TStyleDPIAwareness.RecreateForms;
Var
  i: Integer;
begin
  for i := 0 to Screen.FormCount - 1 do
  begin
    if Screen.Forms[i] <> TForm(Owner) then
      Screen.Forms[i].Perform(CM_RECREATEWND, 0, 0);
  end;
end;

procedure TStyleDPIAwareness.ScaleStyle(Style: TCustomStyleServices);
Var
  NewDPI: integer;
  SeStyle: TObject;
  SeStyleSource: TObject;
  BitmapList: TList;
  BitMap: TBitmap;
  StyleObjectList: Tlist;
  i,n: integer;
  StyleObject: TComponent;
  obj: TStyleDPI;

  procedure ProcessBitmapLink(BL: TObject);
  Var
    BLType: TRTTIType;
  begin
    BLType := TRttiContext.Create.GetType(BL.ClassType);
    BLType.GetProperty('Bottom').SetValue(BL,  Round((BLType.GetProperty('Bottom').GetValue(BL).AsInteger * NewDPI - 1) / OldDPI));
    BLType.GetProperty('Right').SetValue(BL,  Round(BLType.GetProperty('Right').GetValue(BL).AsInteger * NewDPI / OldDPI));
    BLType.GetProperty('Left').SetValue(BL,  Round(BLType.GetProperty('Left').GetValue(BL).AsInteger * NewDPI / OldDPI));
    BLType.GetProperty('Top').SetValue(BL,  Round(BLType.GetProperty('Top').GetValue(BL).AsInteger * NewDPI / OldDPI));
  end;

  procedure ProcessSO(aSO: TComponent;  aSOType: TRTTIType);
  begin
    aSOType.GetProperty('Top').SetValue(aSO,  Round(aSOType.GetProperty('Top').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('Left').SetValue(aSO,  Round(aSOType.GetProperty('Left').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('Width').SetValue(aSO,  Round(aSOType.GetProperty('Width').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('Height').SetValue(aSO,  Round(aSOType.GetProperty('Height').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('MarginTop').SetValue(aSO,  Round(aSOType.GetProperty('MarginTop').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('MarginLeft').SetValue(aSO,  Round(aSOType.GetProperty('MarginLeft').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('MarginBottom').SetValue(aSO,  Round(aSOType.GetProperty('MarginBottom').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('MarginRight').SetValue(aSO,  Round(aSOType.GetProperty('MarginRight').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('TextMarginTop').SetValue(aSO,  Round(aSOType.GetProperty('TextMarginTop').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('TextMarginLeft').SetValue(aSO,  Round(aSOType.GetProperty('TextMarginLeft').GetValue(aSO).AsInteger * NewDPI / OldDPI));
    aSOType.GetProperty('TextMarginRight').SetValue(aSO,  Round(aSOType.GetProperty('TextMarginRight').GetValue(aSO).AsInteger * NewDPI / OldDPI));
  end;

  procedure ProcessStyleObject(SO: TComponent);
  var
    i: integer;
    ChildSo: TComponent;
    SOType: TRTTIType;
    BitmapLink: TObject;
  begin
      SOType := TRttiContext.Create.GetType(SO.ClassType);
      ProcessSO(SO, SOType);

    if So.ClassName = 'TSeBitmapObject' then begin
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmap').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
    end;

    if So.ClassName = 'TSeActiveBitmap' then begin
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmap').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FActiveBitmap').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
    end;

    if So.ClassName = 'TSeSystemButton' then begin
      // Shift the form title to the right
      if SO.Name = 'btnSysMenu' then
        SOType.GetProperty('Width').SetValue(SO,  MulDiv(28, NewDPI, OldDPI));
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmap').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FActiveBitmap').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmapPressed').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmapHot').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
    end;

   if So.ClassName = 'TSeButtonObject' then begin
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmap').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmapFocused').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmapHot').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmapPressed').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
      BitmapLink := TRttiContext.Create.GetType(SO.ClassType).GetField('FBitmapDisabled').GetValue(SO).AsObject;;
      ProcessBitmapLink(BitmapLink);
    end;

     for i := 0 to SO.ComponentCount - 1 do begin
       ChildSo := SO.Components[i];
       ProcessStyleObject(ChildSo);
     end;
  end;

begin
  n := FScaledStyles.IndexOf(TStyleManager.ActiveStyle.Name);
  if n >= 0 then
  begin
    obj := TStyleDPI(FScaledStyles.Objects[n]);
    OldDPI := obj.FCurrentDPI;
  end;

  if UseCustomScalingFactor then
    NewDPI := CustomPPI
  else
    NewDPI := TForm(Owner).Monitor.PixelsPerInch;

  if (Style = TStyleManager.SystemStyle) then
    Exit;

  SeStyle := TRttiContext.Create.GetType(Style.ClassType).GetField('FSource').GetValue(Style).AsObject;
  SeStyleSource := TRttiContext.Create.GetType(SeStyle.ClassType).GetField('FCleanCopy').GetValue(SeStyle).AsObject;
  BitMapList := TRttiContext.Create.GetType(SeStyleSource.ClassType).GetField('FBitmaps').GetValue(SeStyleSource).AsObject as TList;

  if BitMapList.Count = 1 then
  begin
    Bitmap := TObject(BitmapList[0]) as TBitmap;
    ResizeBitmap(Bitmap, Round(Bitmap.Width * NewDPI / OldDPI), Round(Bitmap.Height * NewDPI / OldDPI));

    StyleObjectList := TRttiContext.Create.GetType(SeStyleSource.ClassType).GetField('FObjects').GetValue(SeStyleSource).AsObject as TList;
    for i := 0 to StyleObjectList.Count -1 do begin
      StyleObject := TObject(StyleObjectList[i]) as TComponent;
      ProcessStyleObject(StyleObject);
    end;
   TRttiContext.Create.GetType(SeStyle.ClassType).GetMethod('ResetStyle').Invoke(SeStyle, []);

  end;

  n := FScaledStyles.IndexOf(Style.Name);
  if n >= 0 then
  begin
    obj := TStyleDPI(FScaledStyles.Objects[n]);
    obj.FCurrentDPI := NewDPI;
  end
  else
  begin
    obj := TStyleDPI.Create;
    obj.FCurrentDPI := NewDPI;
    FScaledStyles.AddObject(Style.Name, obj);
  end;

  if Style = TStyleManager.ActiveStyle then
    RecreateForms;
end;
{$IFDEF VER330} // RAD Studio 10.3
  type
   TGetBorderSize = function: TRect of object;

   TFormStyleHookFix = class helper for TFormStyleHook
     procedure SetStretchedCaptionInc(Value: Integer);
     function GetBorderSizeAddr: Pointer;
     function Detour_GetBorderSize: TRect;
   end;

var
  Trampoline_TFormStyleHook_GetBorderSize: TGetBorderSize;
  Detour_TFormStyleHook_GetBorderSize: TGetBorderSize;


{ TFormStyleHookFix }

function TFormStyleHookFix.GetBorderSizeAddr: Pointer;
var
  MethodPtr: TGetBorderSize;
begin
  with Self do MethodPtr := GetBorderSize;
  Result := TMethod(MethodPtr).Code;
end;

procedure TFormStyleHookFix.SetStretchedCaptionInc(Value: Integer);
begin
  with Self do FStretchedCaptionInc := Value;
end;

function TFormStyleHookFix.Detour_GetBorderSize: TRect;
var
  MethodPtr: TGetBorderSize;
begin
  TMethod(MethodPtr).Code := TMethod(Trampoline_TFormStyleHook_GetBorderSize).Code;
  TMethod(MethodPtr).Data := Pointer(Self);
  Result := MethodPtr;
  Self.SetStretchedCaptionInc(1);
  if (Form.Monitor.PixelsPerInch > 96) then
    Result.Top := MulDiv(Result.Top, 96, Form.Monitor.PixelsPerInch);
end;

initialization
 Detour_TFormStyleHook_GetBorderSize := TFormStyleHook(nil).Detour_GetBorderSize;
 TMethod(Trampoline_TFormStyleHook_GetBorderSize).Code :=
   InterceptCreate(TFormStyleHook(nil).GetBorderSizeAddr,
   TMethod(Detour_TFormStyleHook_GetBorderSize).Code)
finalization
 InterceptRemove(TMethod(Trampoline_TFormStyleHook_GetBorderSize).Code);
{$ENDIF VER330}
end.
