How To ... Develop a Bitmap Painting Tool - Load and Save

In this step of the Bitmap Painting tool development, we will add a main menu bar with New, Open, and Save options.

NATURAL LANGUAGE

We will define a new right-click menu with some menu items and corresponding event handlers.

We will assign the right-click menu as menu bar to the dialog.

SCRIPT:

macroScript MicroPaint category: "HowTo"
(
global MicroPaint_CanvasRollout
try(destroyDialog MicroPaint_CanvasRollout) catch()
local isDrawing = false
local bitmapX = bitmapY = 512
local theCanvasBitmap = bitmap bitmapX bitmapY color:white
local theBackgroundBitmap = bitmap bitmapX bitmapY color:white
local currentPos = lastPos = [0,0]

--NEW MENU CODE STARTS HERE
rcMenu CanvasMenu
(
  subMenu"File"
  (
    menuItem new_menu "New"
    menuItem open_menu "Open..."
    menuItem save_as "Save As..."
    separator file_menu_1
    menuItem quit_tool "Quit"  
  )
  subMenu "Edit"( )
  subMenu "Help"
  (
    menuItem about_tool "About MicroPaint..." 
  )

  on new_menu picked do
  (
    theBackgroundBitmap = theCanvasBitmap = bitmap bitmapX bitmapY color:MicroPaint_CanvasRollout.paperColor.color
    MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
  ) 

  on open_menu picked do
  (
    theOpenBitmap= selectBitmap()
    if theOpenBitmap != undefined do
    (
      copy theOpenBitmap theCanvasBitmap
      copy theOpenBitmap theBackgroundBitmap
      close theOpenBitmap
      MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
    )
  )

  on save_as picked do
  (
    theSaveName = getSaveFileName types:"BMP (*.bmp)|*.bmp|Targa (rotation*.tga)|*.tga|JPEG (*.jpg)|*.jpg"
    if theSaveName != undefined do
    (
      theCanvasBitmap.filename = theSaveName
      save theCanvasBitmap
    )
  )
  on about_tool picked do messagebox "MicroPaint\nMAXScript Tutorial" title:"About..."
  on quit_tool picked do destroyDialog MicroPaint_CanvasRollout
)
--NEW MENU CODEENDHERE

rollout MicroPaint_CanvasRollout "MicroPaint"
(
  bitmap theCanvas pos:[0,0] width:bitmapX height:bitmapY bitmap:theCanvasBitmap
  colorpicker inkColor height:16 modal:false color:black across:5
  colorpicker paperColor height:16 modal:false color:white
  checkbutton airBrush "AirBrush" width:50
  spinner AirBrushSpeed "Speed" range:[0.1,50,10] fieldwidth:30
  spinner BrushSize "Size"range:[1,50,10] type:#integer fieldwidth:40
  listbox BrushShape items:#("Circle","Box","Circle Smooth") pos:[bitmapX+5,0] width:90

  fn paintBrush pos =
  (
    case BrushShape.selection of
    (
      1: (
        if distance pos currentPos <= BrushSize.value/2 do
          setPixels theCanvasBitmap pos #(inkColor.color)
      )
      2: setPixels theCanvasBitmap pos #(inkColor.color)
      3: (
        theFactor = (distance pos currentPos) / (BrushSize.value/2.0)
        if theFactor <= 1.0 do
        (
          theFactor = sin ( 90.0 * theFactor)
          thePixels = getPixels theCanvasBitmap pos 1
          if thePixels[1] != undefined do
          (
            thePixels[1] = (thePixels[1] * theFactor) + (inkColor.color * (1.0 - theFactor))
            setPixels theCanvasBitmap pos thePixels
          )
        )
      )--end case 3
    )--end case
  )--end fn

  fn drawStroke lastPos pos =
  (
    currentPos = lastPos
    deltaX = pos.x - lastPos.x
    deltaY = pos.y - lastPos.y
    maxSteps = amax #(abs(deltaX),abs(deltaY))
    deltaStepX = deltaX / maxSteps
    deltaStepY = deltaY / maxSteps
    for i = 0 to maxSteps do
    (
      if airBrush.checked then
      (
        for b = 1 to (BrushSize.value / AirBrushSpeed.value) do
          paintBrush (currentPos + (random [-BrushSize.value/2,-BrushSize.value/2] [BrushSize.value/2,BrushSize.value/2] ))
      )
      else
        for b = -BrushSize.value/2 to BrushSize.value/2 do
          for c = -BrushSize.value/2 to BrushSize.value/2 do
            paintBrush (currentPos + [c,b])
      currentPos += [deltaStepX, deltaStepY]
    )
    theCanvas.bitmap = theCanvasBitmap
  )

  on MicroPaint_CanvasRollout lbuttondown pos do
  (
    lastPos = pos
    isDrawing = true
    drawStroke lastPos pos
  )
  on MicroPaint_CanvasRollout lbuttonup pos do isDrawing = false
  on MicroPaint_CanvasRollout mousemove pos do
  (
    if isDrawing do drawStroke lastPos pos
    lastPos = pos
  )
)
createDialog MicroPaint_CanvasRollout (bitmapx+100) (bitmapy+30) menu:CanvasMenu
MicroPaint_CanvasRollout.theCanvas.bitmap = theBackgroundBitmap
)

Step-By-Step

--Code in italic has no changes since the previous version.

macroScript MicroPaint category:"HowTo"
(
global MicroPaint_CanvasRollout
try(destroyDialog MicroPaint_CanvasRollout)catch()
local isDrawing = false
local bitmapX = bitmapY = 512
local theCanvasBitmap = bitmap bitmapX bitmapY color:white

local theBackgroundBitmap = bitmap bitmapX bitmapY color:white 

We will define a second bitmap called background. It will not be used now, but will be needed in the next tutorial to allow for an eraser function.

local currentPos = lastPos = [0,0]

rcMenu CanvasMenu (

This is a right-click menu definition.

RCMenu User-Interface Items

subMenu "File" (

The first item in the menu will be called File. A typical program implements File, Edit, and Help items in its main menu, so we will define these too.

menuItem new_menu "New"
menuItem open_menu "Open..."
menuItem save_as "Save As..."

Inside the File menu, we will add some menu items to start a new drawing, open an existing one, and save the results to a file on disk.

separator file_menu_1

This is a separator that draws a horizontal line between the menu items.

menuItem quit_tool "Quit"  

This menu item will be used to quit the tool.

)
subMenu "Edit" ( )

We will leave the Edit menu empty for now.

subMenu "Help"
(
menuItem about_tool "About MicroPaint..." 
)

The Help menu will contain only the about item.

on new_menu picked do
(

This is the event handler of the File > New menu item. If the user selected it from the menu,

RCMenu Clauses

theBackgroundBitmap = theCanvasBitmap = bitmap bitmapX bitmapY color:MicroPaint_CanvasRollout.paperColor.color

we will define a new bitmap using the new paper color (background color) added to the User Interface, and will assign to the painting canvas and the new background bitmap.

MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap

Then, we assign the result to the User Interface bitmap.

)
on open_menu picked do
(

This is the event handler of the File > Open menu item. If the user selected it from the menu,

theOpenBitmap= selectBitmap()

we open the standard Bitmap picker dialog of 3dsmax. It provides options to preview all supported file formats.

if theOpenBitmap != undefined do
(

If the user picked a valid bitmap and did not cancel out,

copy theOpenBitmap theCanvasBitmap

we copy the bitmap that was opened into the painting canvas,

copy theOpenBitmap theBackgroundBitmap

and into the background bitmap. The copy method resizes the original to fit the size of the canvas.

close theOpenBitmap

Finally, we close the opened bitmap,

MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap

and assign the painting canvas to the User Interface bitmap.

)
)

on save_as picked do
(

This is the event handler of the File > Save As menu item. If the user selected it from the menu,

theSaveName = getSaveFileName types:"BMP (*.bmp)|*.bmp|Targa (*.tga)|*.tga|JPEG (*.jpg)|*.jpg"

we open the standard file saving dialog of 3ds Max and provide a list of some file extensions. You can add any supported file formats such as, RLA, RPF, and so on to this list.

if theSaveName != undefined do
(

If the user specified a valid name and did not cancel out,

theCanvasBitmap.filename = theSaveName

we set the file name of the painting canvas to the selected name,

save theCanvasBitmap

and save the bitmap to disk.

)
)
on about_tool picked do messagebox "MicroPaint\nMAXScript Tutorial" title:"About..."

If the user picked the Help > About menu item, we open a message box with some text. You can add your own text to this dialog with version number and others.

on quit_tool picked do destroyDialog MicroPaint_CanvasRollout

If the user picked the File > Quit menu item, we destroy the dialog. Later, we can add a prompt whether the current painting can be saved to disk.

)


rollout MicroPaint_CanvasRollout "MicroPaint"
(
bitmap theCanvas pos:[0,0] width:bitmapX height:bitmapY bitmap:theCanvasBitmap
 colorpicker inkColor height:16modal:false color:black across:5 

To add the paper color (background color of new images), we have to increase the across: parameter to 5.

colorpicker paperColor height:16 modal:false color:white

This colorpicker adds the paper color (background color of new images). Note that the color pickers are modeless and can be kept open all the time. You can also drag and drop colors to copy colors between the Ink and Paper color pickers, or drag and drop from other areas of 3ds Max.

checkbutton airBrush "AirBrush" width:50
spinner AirBrushSpeed "Speed" range:[0.1,50,10] fieldwidth:30
spinner BrushSize "Size" range:[1,50,10] type:#integer fieldwidth:40
listbox BrushShape items:#("Circle", "Box", "Circle Smooth") pos:[bitmapX+5,0] width:90

fn paintBrush pos =
(
case BrushShape.selection of
(
1: (
if distance pos currentPos <= BrushSize.value/2 do
setPixels theCanvasBitmap pos #(inkColor.color)
)
2: setPixels theCanvasBitmap pos #(inkColor.color)
3: (
theFactor = (distance pos currentPos) / (BrushSize.value/2.0)
if theFactor <= 1.0 do
(
theFactor = sin ( 90.0 * theFactor)
thePixels = getPixels theCanvasBitmap pos 1
if thePixels[1] != undefined do
(
thePixels[1] = (thePixels[1] * theFactor) + (inkColor.color * (1.0 - theFactor))
setPixels theCanvasBitmap pos thePixels
)
)
)--end case 3
)--end case
)--end fn

fn drawStroke lastPos pos =
(
currentPos = lastPos
deltaX = pos.x - lastPos.x
deltaY = pos.y - lastPos.y
maxSteps = amax #(abs(deltaX),abs(deltaY))
deltaStepX = deltaX / maxSteps
deltaStepY = deltaY / maxSteps
for i = 0 to maxSteps do
(
if airBrush.checked then
(
for b = 1 to (BrushSize.value / AirBrushSpeed.value) do
paintBrush (currentPos + (random [-BrushSize.value/2,-BrushSize.value/2] [BrushSize.value/2,BrushSize.value/2] ))
)
else
for b = -BrushSize.value/2 to BrushSize.value/2 do
for c = -BrushSize.value/2 to BrushSize.value/2 do
paintBrush (currentPos + [c,b])
currentPos += [deltaStepX, deltaStepY]
)
theCanvas.bitmap = theCanvasBitmap
)

on MicroPaint_CanvasRollout lbuttondown pos do
(
lastPos = pos
isDrawing = true
drawStroke lastPos pos
)
on MicroPaint_CanvasRollout lbuttonup pos do isDrawing = false
on MicroPaint_CanvasRollout mousemove pos do
(
if isDrawing do drawStroke lastPos pos
lastPos = pos
)
) createDialog MicroPaint_CanvasRollout (bitmapx+100) (bitmapy+30) menu:CanvasMenu 

We add the RightClick menu defined in the beginning of the script to the dialog.

MicroPaint_CanvasRollout.theCanvas.bitmap = theBackgroundBitmap

After starting the tool, it is a good idea to clear the display, as the bitmap might be still displaying an older version from a previous painting session.

)

RESULT:

Previous Tutorial:

How To ... Develop a Bitmap Painting Tool - Smooth Brushes

Next Tutorial:

How To ... Develop a Bitmap Painting Tool - Erase Changes