Change the background of an area containing already written text

Viewed 203

(Delphi DX 10.3)
I have a big blank (white) canvas (on a Tpanel's descendant) where I draw some text (using Textout(), let's suppose text is always black) and graphics (lines, rectangles, nothing of so complex).
After drawing, I need to change the color of the white background of some specific areas, from white to another colour.
The effect I want to reach is much like an excel sheet with coloured cells. In the attached example, all the columns are created blank (white) like "value" and "difference", then the yellow (price) and the red (result) columns have been coloured.

If I could fill in the areas before writing the text, I would use SetBkMode(TRANSPARENT) and get an optimal result. Unfortunately I need to fill in the areas after writing the text and graphics. The first solution I thought of was pixel-by-pixel replacement (using the Pixels[] function), but it's dramatically slow and graphically unsatisfactory.

So my question is: how can I color the background of an area containing already written text?

Here a Minimal Reproducibile Example.
Button 1 execute pixels substitution, very slow and with unsatisfactory graphical result.
Button 2 fills areas after writing text, using SetBkMode(TRANSPARENT). Perfect result, but I can't do it.

program BK_mode;

uses
  Vcl.Forms,
  main in 'main.pas' {Form1};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

////////////////////////////////

unit main;

interface

uses Windows, Forms, SysUtils, Vcl.StdCtrls, UiTypes, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Graphics, Dialogs;

type
  TForm1 = class(TForm)
        btn_01: TButton;
        btn_02: TButton;
        procedure btn_02Click(Sender: TObject);
        procedure btn_01Click(Sender: TObject);
  private
        procedure write_text(canvas: TCanvas;x, y : integer;i_fontsize : smallint;const str_text: String;style : TFontStyles;bo_transparent : boolean);
        procedure switch_color(canvas : TCanvas;color_source, color_target : TColor);
        procedure prepare_example(bo_transparent : boolean);
  end;

var
    Form1: TForm1;

implementation

{$R *.dfm}

const
    BASE_COLOR = clWhite;
    ALTERNATIVE_COLOR = clRed;

procedure TForm1.write_text(canvas: TCanvas;x, y : integer;i_fontsize : smallint;const str_text: String;style : TFontStyles;bo_transparent : boolean);
begin
    var lo_old_BK_color : TColor := canvas.Brush.Color;
    var lo_old_BK_mode := GetBKMode(canvas.Handle);
    if bo_transparent then SetBKMode(canvas.Handle, TRANSPARENT)
    else begin
        SetBKMode(canvas.Handle, OPAQUE);
        canvas.Brush.Color := BASE_COLOR
    end;
    canvas.Font.Color := clBlack;canvas.Font.Size := i_fontsize;canvas.Font.Style := style;
    TextOut(canvas.Handle, x, y, PChar(str_text), Length(str_text));
    if NOT bo_transparent then canvas.Brush.Color := lo_old_BK_color;
    SetBKMode(canvas.Handle, lo_old_BK_mode)
end;

procedure TForm1.switch_color(canvas : TCanvas;color_source, color_target : TColor);
begin
    for var x := 0 to clientWidth-1 do
        for var y := 0 to clientHeight - 1 do
            if (canvas.Pixels[x, y] = color_source) then canvas.Pixels[x, y] := color_target
end;

procedure TForm1.prepare_example(bo_transparent : boolean);
begin
    if bo_transparent then Color := ALTERNATIVE_COLOR else Color := BASE_COLOR;
    invalidate;
    application.MessageBox('Click me', 'Test');
    canvas.Rectangle(10, 10, 200, 100);
    canvas.MoveTo(10, 110);canvas.LineTo(200, 140);
    canvas.MoveTo(10, 140);canvas.LineTo(200, 110);
    write_text(canvas, 30, 30, 14, 'This is a text!', [], bo_transparent);
    write_text(canvas, 30, 60, 11, 'This is another text!', [fsBold, fsItalic], bo_transparent)
end;

procedure TForm1.btn_01Click(Sender: TObject);
begin
    prepare_example(FALSE);
    switch_color(canvas, BASE_COLOR, ALTERNATIVE_COLOR)
end;

procedure TForm1.btn_02Click(Sender: TObject);
begin
    prepare_example(TRUE)
end;

end.

/////////////////////////////////////////////////////////

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 283
  ClientWidth = 208
  Color = clWhite
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  DesignSize = (
    208
    283)
  PixelsPerInch = 96
  TextHeight = 13
  object btn_01: TButton
    Left = 17
    Top = 161
    Width = 178
    Height = 51
    Anchors = [akLeft, akRight, akBottom]
    Caption = '1. write text and fill area'
    Font.Charset = ANSI_CHARSET
    Font.Color = clWindowText
    Font.Height = -16
    Font.Name = 'Arial'
    Font.Style = [fsBold]
    ParentFont = False
    TabOrder = 0
    WordWrap = True
    OnClick = btn_01Click
  end
  object btn_02: TButton
    Left = 17
    Top = 220
    Width = 178
    Height = 51
    Anchors = [akLeft, akRight, akBottom]
    Caption = '2. fill area then write text'
    Font.Charset = ANSI_CHARSET
    Font.Color = clWindowText
    Font.Height = -16
    Font.Name = 'Arial'
    Font.Style = [fsBold]
    ParentFont = False
    TabOrder = 1
    WordWrap = True
    OnClick = btn_02Click
  end
end
1 Answers

What you are asking for is simply not doable this way. You need to redraw the entire Canvas, drawing your backgrounds BEFORE drawing lines+text on top. Once you have drawn, you need a repaint to do everything over. DO NOT draw on a Canvas from outside of a Paint event.

Your example draws on a TForm.Canvas, so use the TForm.OnPaint event. For a TPanel descendant, override the virtual Paint() method instead. Either way, keep some variables with the desired settings, use those variable while drawing, and call Invalidate() after updating the variables and you want to trigger a repaint.

For example:

unit main;

interface

uses Windows, Forms, SysUtils, Vcl.StdCtrls, UiTypes, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Graphics, Dialogs;

type
  TForm1 = class(TForm)
    btn_01: TButton;
    btn_02: TButton;
    procedure btn_02Click(Sender: TObject);
    procedure btn_01Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormPaint(Sender: TObject);
  private
    FDrawTransparent : Boolean;
    FDrawColor : TColor;
    procedure write_text(ACanvas: TCanvas; x, y : integer; i_fontsize : smallint; const str_text: String; style : TFontStyles; bo_transparent : boolean);
    procedure prepare_example(bo_transparent : boolean; color_target : TColor);
  end;
    
var
  Form1: TForm1;
    
implementation
    
{$R *.dfm}
    
const
  BASE_COLOR = clWhite;
  ALTERNATIVE_COLOR = clRed;
    
procedure TForm1.FormCreate(Sender: TObject);
begin
  FDrawTransparent := False;
  FDrawColor := BASE_COLOR;
end;

procedure TForm1.FormPaint(Sender: TObject);
begin
  Canvas.Brush.Color := Self.Color;
  Canvas.Rectangle(10, 10, 200, 100);
  Canvas.MoveTo(10, 110);
  Canvas.LineTo(200, 140);
  Canvas.MoveTo(10, 140);
  Canvas.LineTo(200, 110);
  write_text(Canvas, 30, 30, 14, 'This is a text!', [], FDrawTransparent);
  write_text(Canvas, 30, 60, 11, 'This is another text!', [fsBold, fsItalic], FDrawTransparent);
end;

procedure TForm1.write_text(ACanvas: TCanvas; x, y : integer; i_fontsize : smallint; const str_text: String;style : TFontStyles; bo_transparent : boolean);
begin
  var lo_old_BK_color := ACanvas.Brush.Color;
  var lo_old_BK_mode := GetBKMode(ACanvas.Handle);
  if bo_transparent then
    SetBKMode(ACanvas.Handle, TRANSPARENT)
  else begin
    SetBKMode(ACanvas.Handle, OPAQUE);
    ACanvas.Brush.Color := FDrawColor;
  end;
  ACanvas.Font.Color := clBlack;
  ACanvas.Font.Size := i_fontsize;
  ACanvas.Font.Style := style;
  TextOut(ACanvas.Handle, x, y, PChar(str_text), Length(str_text));
  if NOT bo_transparent then ACanvas.Brush.Color := lo_old_BK_color;
  SetBKMode(ACanvas.Handle, lo_old_BK_mode);
end;    
    
procedure TForm1.prepare_example(bo_transparent : boolean; color_target: TColor);
begin
  FDrawTransparent := bo_transparent;
  FDrawColor := color_target;
  if bo_transparent then Color := ALTERNATIVE_COLOR else Color := BASE_COLOR;
  Invalidate;
  Application.MessageBox('Click me', 'Test');
end;
    
procedure TForm1.btn_01Click(Sender: TObject);
begin
  prepare_example(False, ALTERNATIVE_COLOR);
end;
    
procedure TForm1.btn_02Click(Sender: TObject);
begin
  prepare_example(True, BASE_COLOR);
end;    

end.
Related