2

I am trying to create a save/load function for 2d objects that has been drawn into a form.

type circle = { X : int; Y : int; Diameter : int; Brush : Brush}
type Square = { X : int; Y : int; Length : int; Height: int; Brush : Brush}

When i create the object i put them into 2 lists 1 for each type. My initial thought was to read and write these objects to a textfile, see below:

saveFile.Click.Add(fun _ ->
 for c in listOfCircles do 
   myfile.WriteLine("Circle," + c.X.ToString() + "," + c.Y.ToString() + "," + c.Diameter.ToString() + "," + c.Brush.ToString())
 for s in listOfSquares do
   myfile.WriteLine("Square," + s.X.ToString() + "," + s.Y.ToString() + "," + s.Height.ToString() + "," + s.Length.ToString() + "," + s.Brush.ToString())
 myfile.Close() // close the file

And in the textfile it looks like this

Circle,200,200,50,System.Drawing.SolidBrush
Square,50,55,45,55,System.Drawing.SolidBrush

From here i want to read these values and then be able to parse them and recreate the objects by adding the objects the lists and redraw them.

let readCircle =
  System.IO.File.ReadAllLines path
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|x; y ; z ; b ; _|] when x = "Circle" -> Some (y, z, b)
    | _ -> None )

let readSquare =  
  System.IO.File.ReadAllLines path
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|x; y ; z ; b ; a ; _|] when x = "Square" -> Some (y, z, b, a)
    | _ -> None )

These functions gives me

val readCircle : (string * string * string) [] = [|("200", "200", "50")|]
val readSquare : (string * string * string * string) [] = [|("50", "55", "45", "55")|]

The problem i have now is im not sure how to obtain the values from the array. Beneath is example with multiple circles.

val readCircle : (string * string * string) [] =  [|("200", "200", "50"); ("200", "200","50")|]

Any ideas or comments about how to go from here/how to resolve this issue is very appreciated! Question summary: how could i get the values from the array and put them in for example my already created add functions, see below:

 listOfCircles.Add({ X = 200; Y = 200; Diameter = 50; Brush = Brushes.Black})
2
  • I'm not sure I understand the question. But assuming you need to convert your array of values to a list of shapes, you could use values |> Seq.map createCircle |> Seq.toList. Commented Jan 23, 2014 at 15:21
  • I want to get the values from the array and put them into the listOfCircles.Add function, see the edited bottom. Commented Jan 23, 2014 at 15:31

3 Answers 3

3

You could convert the arrays of string tuples you have using Array.map, e.g.

[|("200", "200", "50"); ("200", "200","50")|]
|> Array.map (fun (x,y,d) -> {X = int32 x; Y = int32 y; Diameter = int32 d; Brush = Brushes.Black})

It might be a bit clearer if you converted to circle or square as you parsed the file, then you would have an array of circle or an array of square that you can add directly to your lists.

let readCircle =
  System.IO.File.ReadAllLines path
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|t; x; y; d; _|] when t = "Circle" -> 
        Some {X = int32 x; Y = int32 y; Diameter = int32 d; Brush = Brushes.Red}
    | _ -> None )

But... if you wanted to make larger changes, you could use discriminated unions to represent your shapes, they would then share a common type of Shape and you could parse circles and squares in the same function.

type Shape = 
| Circle of X : int * Y : int * Diameter : int * Brush : Brush
| Square of X : int * Y : int * Length : int * Height: int * Brush : Brush 

let readShapes (data: string array) =
  data
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|t; x; y; d; _|] when t = "Circle" -> 
        Some (Circle(X = int32 x, Y = int32 y, Diameter = int32 d, Brush = Brushes.Red))
    | [|t; x; y; l; h; _|] when t = "Square" -> 
        Some (Square(X = int32 x, Y = int32 y, Length = int32 l, Height = int32 h, Brush = Brushes.Red))
    | _ -> None )

let listOfShapes = ResizeArray<_>()

let testInput = """
Circle,200,200,50,System.Drawing.SolidBrush
Square,50,55,45,55,System.Drawing.SolidBrush"""

testInput.Split('\n') // System.IO.File.ReadAllLines path
|> readShapes
|> Array.iter (listOfShapes.Add)

Which would result in

val it : System.Collections.Generic.List<Shape> =
  seq
    [Circle (200,200,50,System.Drawing.SolidBrush {Color = Color [Red];});
     Square (50,55,45,55,System.Drawing.SolidBrush {Color = Color [Red];})]

You could then use pattern matching to draw each type of shape

let drawShape shape =
    match shape with
    | Circle(x,y,d,b) -> 
        printfn "Pretend I just drew a circle at %d,%d with diameter %d." x y d
    | Square(x,y,l,h,b) -> 
        printfn "Pretend I just drew a rectangle at %d,%d that was %d long and %d high." x y l h

listOfShapes |> Seq.iter drawShape

Giving

Pretend I just drew a circle at 200,200 with diameter 50.
Pretend I just drew a rectangle at 50,55 that was 45 long and 55 high.
Sign up to request clarification or add additional context in comments.

Comments

2

If I understand your goal, this is how I would go about it. I've only implemented Circle; you'll need to modify it to handle Square.

open System
open System.Collections.Generic
open System.Drawing
open System.IO

let memoize f =
  let cache = Dictionary()
  fun key ->
    match cache.TryGetValue(key) with
    | true, value -> value
    | _ ->
      let value = f key
      cache.Add(key, value)
      value

let getBrush =
  memoize (fun name -> typeof<Brushes>.GetProperty(name).GetValue(null) :?> SolidBrush)

type Circle = 
  { X : int
    Y : int
    Diameter : int
    Brush : SolidBrush } with
  override this.ToString() = 
    sprintf "Circle,%d,%d,%d,%s" this.X this.Y this.Diameter this.Brush.Color.Name
  static member Parse(s: string) =
    match s.Split(',') with
    | [|"Circle";x;y;diameter;brushName|] -> {X=int x; Y=int y; Diameter=int diameter; Brush=getBrush brushName}
    | _ -> invalidArg "s" "Cannot parse string"

let writeShapesToFile fileName shapes =
  File.WriteAllLines(fileName, Seq.map (sprintf "%O") shapes)

let readShapesFromFile fileName =
  File.ReadAllLines(fileName) |> Array.map Circle.Parse

Also, you might consider using a class hierarchy instead of records since much of the structure and behavior of Circle and Square are shared.

Comments

1

This is fun - I approached it in a totally different way than Daniel (but I agree with him that you classes might be a better approach for your shapes). Instead, I took advantage of discriminated unions (and there are better ways to do this - more later):

First, I define a type for a list of parameters for making a shape:

type Parameter =
    | Label of string
    | Number of int

Now let's convert a string to a parameter:

let toParameter s =
    match Int32.TryParse(s) with
    | (true, i) -> Number(i)
    | (_, _) -> Label(s)

Now to convert a list of strings to a list of Parameter:

let stringListToParameterList stringlist = stringlist |> List.map(function s -> toParameter s)

Now to convert a comma-separated string to a list of string:

let commastringToList (s:string) = s.Split(',') |> Array.toList

OK - great - let's define your records and a master Shape:

type circlerec = { X : int; Y : int; Diameter : int; Brush : Brush}
type squarerec = { X : int; Y : int; Length : int; Height: int; Brush : Brush}
type Shape =
    | Circle of circlerec
    | Square of squarerec

With this we need a way to make a Shape from a parameter list. This is brute force, but it reads well enough:

let toShape list =
    match list with
    | Label("Circle") :: Number(x) :: Number(y) :: Number(diam) :: Label(colorName) :: [] ->
        Circle({X = x; Y = y; Diameter = diam; Brush = new SolidBrush(Color.FromName(colorName)); })
    | Label("Circle") :: rest -> raise <| new ArgumentException("parse error:expected Circle num num num color but got " + list.ToString())
    | Label("Square") :: Number(x) :: Number(y) :: Number(length) :: Number(height) :: Label(colorName) :: [] ->
        Square({X = x; Y = y; Length = length; Height = height; Brush = new SolidBrush(Color.FromName(colorName)); })
    | Label("Square") :: rest -> raise <| new ArgumentException("parse error:expected Square num num num num color but got " + list.ToString())
    | _ -> raise <| new ArgumentException("parse error: unknown shape: " + list.ToString())

It's dense, but I'm using F#'s pattern matching to spot the various parameters for each shape. Note that you could now do things like have Square,x,y,size,colorName in your file and make a Square where Length and Height are equal to size by just adding in the pattern.

Finally comes the piece de resistance, converting your file into shapes:

let toShapes path =
    System.IO.File.ReadAllLines path |> Array.toList |>
        List.map(function s -> s |> commastringToList |>
        stringListToParameterList |> toShape)

which maps every line in the file to a list of string which then maps each line to a shape, but piping the comma string to the list converter and then through the parameter list and then to a shape.

Now where this is bad is that the error checking is pretty horrid and that the Parameter type should really include Pigment of Color, which would allow you to look at the string that comes in and if it's valid Color name, map it to a Pigment else a Label.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.