Reputation: 233
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})
Upvotes: 2
Views: 681
Reputation: 49179
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.
Upvotes: 1
Reputation: 3687
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.
Upvotes: 3
Reputation: 47904
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.
Upvotes: 2