Link to home
Start Free TrialLog in
Avatar of hush021299
hush021299

asked on

Bitmap Editing with paint box

I try to ad a basic bmp (or Icon) editing feature into my app. So I am testing around with a paintbox.
I plan to load an image, convert to bmp, edit it in the paint box, convert back and save.

I guess, this will be a way, but it takes a while. Especially since I want to draw "blockwise" (zooming into the bmp and simulating big pixels). Maybe I should use a DrawGrid instead?

Beside this, there are many issues need to be resolved like drawing this frames while moving the mouse, undo (should I save old bmps etc, or drawing vectors like described in mastering delphi?);

Hence I look forward to acomplish this. I want to know whether this is the right approach before dwelling deep into this stuff. Maybe sombody has finished code for such a thing.

I would give more points for a finalized solution. The 125 points are meant for input which really helps me further.


I have created my first simple example on this topic:

(* Unit ================================*)

unit BMPEditU;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    PaintBox1: TPaintBox;
    Button1: TButton;
    Panel1: TPanel;
    CD: TColorDialog;
    rgZoom: TRadioGroup;
    RGTool: TRadioGroup;
    procedure Button1Click(Sender: TObject);
    procedure PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Panel1Click(Sender: TObject);
    procedure rgZoomClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure PaintBox1MouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
  private
    { Private declarations }
    Zoom:integer;
    XD,YD,XU,YU:Integer;
    BM:TBitmap;
    procedure DrawPixes(x, y: integer; col: TColor);
    procedure DrawLine(xS, YS, xE, yE, size: integer; Col: TColor);
    procedure DrawCircle(xS,YS,xE,yE,size:integer; Col:TColor);
    procedure DrawBox(xS, YS, xE, yE, size: integer; Col: TColor);
    procedure Flood(xS, YS: integer; Col: TColor);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
 

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
begin
BM.LoadFromFile('D:\My Documents\My Programs\Glyphs\ACCOUNT.BMP');
PaintBox1.canvas.Draw(0,0,bm);

end;

procedure TForm1.PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
XD:=X;YD:=Y;

   
end;


procedure TForm1.DrawPixes(x,y:integer;col:TColor);
  var i,j,x1,y1:integer;
begin
if (zoom > 0) AND (BM<>nil) then
begin
x1:=x div zoom * zoom;
y1:=y div zoom * zoom;

for i:= 1 to zoom do
for j:= 1 to zoom do
paintbox1.Canvas.Pixels[x1+i,y1+j]:=col;
end;
end;



procedure TForm1.Panel1Click(Sender: TObject);
begin
cd.execute;
panel1.Color:=cd.Color;
end;

procedure TForm1.rgZoomClick(Sender: TObject);
var w,h:integer;
begin
w:=bm.Width;
h:=BM.Height;
Paintbox1.Width:=w*16;
Paintbox1.Height:=h*16;

case RGZoom.ItemIndex of
0: begin Zoom:=1; w:=bm.Width; h:=BM.Height; end;
1: begin Zoom:=4; w:=bm.Width*4; h:=BM.Height*4; end;
2: begin Zoom:=16; w:=bm.Width*16; h:=BM.Height*16; end;
end;

Paintbox1.Refresh;
PaintBox1.canvas.stretchDraw(Rect(0,0,w,h),bm);

end;

procedure TForm1.FormCreate(Sender: TObject);
begin
BM:=TBitmap.create;

end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
bm.free;

end;

procedure TForm1.PaintBox1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
if (ssLeft in shift) AND (RGTool.itemindex=0)then
   DrawPixes(x,y,panel1.color);
end;

procedure TForm1.PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
XU:=X;YU:=Y;


  case RGTool.itemindex of
  0: if (XU = XD) and (YU=YD) then DrawPixes(x,y,panel1.color);
  1: DrawLine(xd,yd,xu,yu,zoom, Panel1.color);
  2: DrawCircle(xd,yd,xu,yu,zoom, Panel1.color);
  3: DrawBox(xd,yd,xu,yu,zoom, Panel1.color);
  4: Flood(xd,yd, Panel1.color);

  end;

end;

procedure TForm1.DrawLine(xS,YS,xE,yE,size:integer; Col:TColor);
begin
     paintbox1.Canvas.MoveTo(xS,yd);
     paintbox1.Canvas.pen.Color :=col;
     paintbox1.Canvas.pen.Width:=size;
     paintbox1.Canvas.LineTo(xE,yE);
end;

procedure TForm1.DrawCircle(xS,YS,xE,yE,size:integer; Col:TColor);
begin
     paintbox1.Canvas.MoveTo(xS,yd);
     paintbox1.Canvas.pen.Color :=col;
     paintbox1.Canvas.pen.Width:=size;
     paintbox1.Canvas.brush.Style :=bsClear;
     paintbox1.Canvas.Ellipse(xs,ys,xe,ye);
end;

procedure TForm1.DrawBox(xS,YS,xE,yE,size:integer; Col:TColor);
begin
     paintbox1.Canvas.MoveTo(xS,yd);
     paintbox1.Canvas.pen.Color :=col;
     paintbox1.Canvas.pen.Width:=size;
     paintbox1.Canvas.brush.Style :=bsClear;
     paintbox1.Canvas.Rectangle(xs,ys,xe,ye);

end;

procedure TForm1.Flood(xS,YS :integer; Col:TColor);
var c:TColor;
begin
     paintbox1.Canvas.brush.Color:=Col;
     c:=paintbox1.Canvas.Pixels[xs,ys];
     paintbox1.Canvas.FloodFill(xs,ys,c,fsSurface);
end;

end.
(*
===========================================
The form:
===========================================
*)
object Form1: TForm1
  Left = 233
  Top = 107
  Width = 696
  Height = 480
  Caption = 'Form1'
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Shell Dlg 2'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  OnDestroy = FormDestroy
  PixelsPerInch = 96
  TextHeight = 13
  object PaintBox1: TPaintBox
    Left = 32
    Top = 24
    Width = 321
    Height = 329
    OnMouseDown = PaintBox1MouseDown
    OnMouseMove = PaintBox1MouseMove
    OnMouseUp = PaintBox1MouseUp
  end
  object Button1: TButton
    Left = 32
    Top = 368
    Width = 75
    Height = 25
    Caption = 'Load'
    TabOrder = 0
    OnClick = Button1Click
  end
  object Panel1: TPanel
    Left = 32
    Top = 400
    Width = 73
    Height = 25
    Caption = 'Color'
    TabOrder = 1
    OnClick = Panel1Click
  end
  object rgZoom: TRadioGroup
    Left = 128
    Top = 360
    Width = 89
    Height = 65
    Caption = 'Zoom'
    ItemIndex = 0
    Items.Strings = (
      '1'
      '4'
      '16')
    TabOrder = 2
    OnClick = rgZoomClick
  end
  object RGTool: TRadioGroup
    Left = 248
    Top = 328
    Width = 89
    Height = 113
    Caption = 'RGTool'
    ItemIndex = 0
    Items.Strings = (
      'Free line'
      'Line'
      'Circle'
      'Rectangle'
      'Flodfill')
    TabOrder = 3
  end
  object CD: TColorDialog
    Ctl3D = True
    Left = 16
    Top = 400
  end
end
Avatar of Member_2_248744
Member_2_248744
Flag of United States of America image

hello hush, I am not sure what to try and say here for your question, doing a bitmap editor (drawing on and changing) is not a simple thing to do, and as with all coding there will be many many ways to do it. Of course what you want to do will be a factor in your development, as to how to do it. The TPaintBox is as good as any to use for a "Basic" mouse draw-on editor, but you have said something about "Zooming" for editing, which will complicate all od your drawing operations (which you already see), you may want to start out with a NON-ZOOMING drawing operations, to get your line, rectangle and fill drawing code to work, and after you have some success with that, you can try and add the Zoom to it. A TDrawGrid may not be very helpful to you, since you seem to want to do actual size (non-zoom) drawing, I do not think that the TDrawGrid can do single pixel cells.  For zooming you will have to keep to the even number zoom factors (use 4, but not 3 x zoom).
I wonder what I am suppose to look at your code above and tell you about? as I said this graphic editing can get very complex, but I wonder why why you draw on the Paintbox, as in your DrawBox

paintbox1.Canvas.Rectangle(xs,ys,xe,ye);

shouldn't you be drawing on the bitmap?, any draw on the paintbox is lost, not remembered.
Avatar of hush021299
hush021299

ASKER

You are right, I should paint onto the bmp.
But anyway. The question is on the one hand whether ther is a start up code example around, which I could study and improve
or
to find the optimal way to start, since I do not want to turn back after half work.

What I tried:
The normal drawing on the canvas is so far ok. I can do the basic shapes and a selection rectangle.
But all image editors show a gutter when increasing the zoom. And every pixel is Pixel*zoom big.
 I wonder how this gutter work.
 I have to translate every move of the mouse in bigpixels (circle: in which bigpixel it is in?).

3rd party tools I ve tried:
There was a bmpeditor component from colin wilson, giving me the right start, but this thing was created for an old delphi version and it doesnt work as far I tested.
IconEdit32 from Neil Rubenkrieg. This could be perfect, but very enhanced to get through the basics.
ASKER CERTIFIED SOLUTION
Avatar of Member_2_248744
Member_2_248744
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
With gutter I mean the horizontal and vertical lines, usually drawn on the "canvas" showing boxes like in a math paper.

But what I do not understand here is the mouse movement.
Ok, it is much easier to draw on the original size bitmap then on the zoomed, and then translate the bitmap into the zoomed paintbox.

But then the mouse will be moved only over the zoomed picture. The user draw where the mouse is, and he want to draw on the large bitmap. I guess this is even harder to tackle?

(Btw. I probably will not be online for a week from later today on)
???
if you look at the code in the  IconEdit32 from Neil Rubenkrieg you can see that he uses an array for each Icon size that he supports (16x16, 32x32, 48x48), each array has the color of the pixel and other info for that pixel, and when he draws his Icon for editing he DOES NOT stretch Draw the icon bitmap to his display, he for loops through his Array and draws a single rectangle (with a gutter as a black pen of the rectangle draw) for every pixel in the icon, , , .
However, you should NOT use this method for general Bitmap editing, because for an Icon you have fixed small sizes (32x32), and for a bitmap you do not have any fixed sizes, so it would be difficult to have any array with your pixel definitions in it

Maybe if you ask some more I can help you some? ?
but this is not so easy
but one last question in that.

I want to paste images as well.

I guess i need an new image which freely can be moved with the mouse over the paintbox. Meanwhile my paintboxthing is an object. SO i have to create the image there.
What do you suggest?
..so pasting is fine, but how to get something visible for the user??
I dont see the image!
thanks.
sorry, but I do not really follow your comments (the description of what you want and what you have). . . You say
  "I dont see the image!"

???
if you want to see something (I guess it's a Bitmap that you have pasted an image into? ? ?) then you will need to draw it somewhere. . . . try Stretch Draw that  new pasted bitmap onto your Paintbox, OR  replace the current bitmap that is drawn on the paintbox with the new pasted one. . . . .


and you say

"I guess i need an new image which freely can be moved with the mouse over the paintbox."

I really do NOT see this a a good thing at all? Moving an Image with mouse? ?
but I can not follow your needs from what you say
The idea is to store the pasted image in a timage which I can freely move until I merge it into the bitmap.
However meanwhile I realize that I will draw the pasteimage on the canvas of the paintbox while moving the mouse. On duble click I will merge it into the bitmap.
This is going to work, but also here the paintbox flickers. Do you have an idea how to get rid of this?

Thanks
and
cheers
hh
???
it flickers, OK,
as to your question - - - no I do not,
since I do not have any Idea, what so ever, ,  about what you are doing or the methods you are using to do it,
 I am not a mind reader, or a psychic trans-receptor able to read thoughts or computer binary data from a distance ! !
could you answer your question with the limited information that you have given?

Here is a VAST GENERALITY   about  fickering on screen

if you call several DC drawing operations in sequence (in the same function, line after line) and these DC draws overlap  (draw on the same area of the DC or in delhli a Canvas) then you might get "Filcker". flicker happens when your over draw the same area on screen and a screen "Refresh" occures while your lines of core are executed, You get a screen paint of One color, and then a screen refresh and then your code continues and the screen is painted with another color, and the color change will look like a "Flicker", . . . . .
this is very aparent if you do a mouse move paint (drag an image) and do a Two paint (one paint to erase and one paint to draw new image) and your Two paints overlap (almost ALWAYS overlap), I have solved this problem, by creating a Bitmap that will cover the ENTIRE area of the old erase area and the new draw image area, you copy the underlying "Base" bitmap to the new bitmap and then draw your sprite (the moving image) to this new bitmap (in the corrct position) and then do a  SINGLE  draw onscreen of this new bitmap,

 if you do NOT draw on the same areas of the DC twice, you will never get flicker



 = = = = = = = = = = = = = = =  ==  = = = = = = = =

I hope that you have succes, and I am glad to help you, but I am not able to follow your last few questions, due to lack of information,
So thanks for your help.
I really have appreciated your comments and help.
It might not be the best way to draw on a bitmap while showing everything on a paintbox but so far it works. I hope I will have no bad awakening in that.

When it comes to the flickering you are right again.
I always do a
showimage (display the bitmap stuff on the canvas of the paintbox) and a drawraster(drawing the lines crosswise) after performing an action.

Of course when drawing a single rectangle this approach is fine.

Now when drawing free hand, meaning every pixel i touch will be colored, the paintbox will permanently be repainted. This is where the flickering starts.

The only idea "I" have about this is, that I need to find a way to control whether the mouse has moved to another pixel when asking for the drawing function. Then i will not ask for repainting so often.

cheers
hh
I will just take a guess here . . . .

DO NOT CALL for a total repaint for a mouse move sort of draw, you should ONLY PAINT the Rectangle (I guess that it represents a Pixel) or rectangles that the mouse has moved over, , not all the rectangles,

and yes you should NOT paint a rect that has already been painted

I beleive that  IconEdit32 from Neil Rubenkrieg  , he implements this, doesn't he? Can you not review his code and get some methods from him?

I looked up his code in the imFrameU  or imFrame  Unit, he uses a TPaintBox  named   "pbDraw"

here is the paintBox mouse move -


procedure TImageFrame.pbDrawMouseMove(Sender: TObject;
  Shift: TShiftState; X, Y: Integer);
//Translate this mouse-move event into a 'cell-move' event, as
// long as the cell location IS actually changed
VAR cX, cY, Stat : Integer;
begin
  CellFmPixel(X, Y, cX, cY);
  //Stats is 1 if left button down, 2 if right, 0 if neither
  Stat := fGotDown;
  //Set 8-bit in Stat if Ctrl key is down
  IF ssCtrl IN Shift THEN Stat := Stat OR 8;
  //If cell-location has changed...
  IF (cX <> fMouseX) OR (cY <> fMouseY) THEN
    begin
      fMouseX := cX;
      fMouseY := cY;
      IF Assigned(fECellMove) THEN
        fECellMove(Self, fMouseX, fMouseY, Stat);
    end;
end;


function TImageFrame.CellFmPixel(X, Y: Integer; VAR cX, cY: Integer):
  Boolean;
// Take an X,Y pixel coordinate and put the corresponding
// cell within the drawing area into cX,cY. Return false if
// outside the drawing grid.
begin
  IF (X < 0) OR (Y < 0) THEN
    begin cX := -1; cY := -1; end
  ELSE IF (X >= fCellAll) OR (Y >= fCellAll) THEN
    begin cX := -1; cY := -1; end
  ELSE
    begin
      cX := X DIV fCellOne;
      cY := Y DIV fCellOne;
    end;
  Result := ValidCell(cX, cY);
  IF NOT Result THEN
    begin cX := -1; cY := -1; end;
end;
I'm not sure that the code above is very clear to understand, here is a prtion of code I have used to do this

the TStart is a Position Record That set on a Mouse Down event

type
  TStart = Record
    PaintOp, // has the Paint operation, like Draw Line, Draw Rectangle
    X, Y, // has center points of cell in pixels on the PaintBox
    PosX, PosY: Integer; // has the grid array index positions for the cell, should have called them IndexX. IndexY
    end;


// GetGridPos function is called on a mouse event to get the cellGrid index numbers and Position
function GetGridPos(X, Y: Integer): TStart;
  begin
  if cellRec.Size = 16 then
        begin // cell size for 16x16 is 24 pixels
        Result.PosX := X div 24;
  // PosX and PosY are the array indexes for the cells
  // for a 16x16 the array would be Array[0..15, 0..15] of cellColorRecord
        Result.PosY := Y div 24;
        Result.X := (Result.PosX * 24)+ 12;
  // Result.X and Result.Y are the center points if that cell in pixels
  // these center points are used for mouse move drawing operations
        Result.Y := (Result.PosY * 24)+ 12;
        end else
        if cellRec.Size = 32 then
          begin  // cell size for 32x32 is 12 pixels
          Result.PosX := X div 12;
          Result.PosY := Y div 12;
          Result.X := (Result.PosX * 12)+ 6;
          Result.Y := (Result.PosY * 12)+ 6;
          end else
          begin  //size 48
          Result.PosX := X div 8;
          Result.PosY := Y div 8;
          Result.X := (Result.PosX * 8)+ 4;
          Result.Y := (Result.PosY * 8)+ 4;
          end;
  end;
...
I guess this is comparable to my approach

FUNCTION TIconPaintBox.RasterMousePoint(x, y: integer): TPoint;
//get mouse to snap to the PIXEL" raster on the Paintbox canvas
BEGIN

  IF (x > 0) AND (FZoom > 0) THEN
    begin
    if (X > (width - FZoom div 2)) then x:= width else
    IF (X < (FZoom div 2)) then x:= 0 else
   // if (x MOD FZoom) > (FZoom div 2) then
   //    x := x + x MOD FZoom else
       x := x - x MOD FZoom;
    END ELSE x := 0;

 

  IF (y > 0) AND (FZoom > 0) THEN
    BEGIN
     IF (Y > (height - FZoom div 2)) then Y:= height else
     IF (Y < (FZoom div 2)) then Y:= 0 else
   // if (Y MOD FZoom) > (FZoom div 2) then
  //  y := y + y MOD FZoom else
    y := y - y MOD FZoom;
    END ELSE y := 0;
 

  Result := Point(x, y);

END;

But I cant say for sure