Chapter 4 was actually quite fun! This chapter goes through a simple application that reads data from a csv file and produces a pie chart. The chapter starts off using the F# interactive console all the way to creating a F# application that creates a simple GUI.
Some notes while building the application
- By the use of pattern matching you can create generic functions. In C# you need to specify the function as generic but by the use of pattern matching F# will create functions that are automatically generalized. see calculateSum
- F# does not insert automatic conversions between numeric types as in C#, you need to directly use int(), float(), etc.
- You can use pattern matching the in for in construct like
for (title, value) in data do
module Module1
open System
open System.Drawing
open System.Windows.Forms
open System.IO
let convertDataRow(csvLine:string) =
let cells = List.ofSeq(csvLine.Split(',')) //creates a list where the first first
//element is the name and the last is a number
match cells with //do a pattern match on the list
| title :: number :: _ -> //match the tuple that has a first element a next/last element and any other elements are ignored
let parsedNumber = Int32.Parse(number) //parse the first element value
(title, parsedNumber) //create a new tuple that is of type string and int
| _ -> failwith "Incorrect data format!"
let rec processLines(lines) =
match lines with //do a pattern match on the lines
| [] -> [] //if the line is empty do nothing
| currentLine :: remaining -> // if the lines are not empty,
//grab the first element into currentLine and the remaining lines into that variable
let parsedLine = convertDataRow(currentLine) //parse the line into a tuple (string, int)
let parsedRest = processLines(remaining) //rec call the processLines function with the rest of the lines remaining
parsedLine :: parsedRest //build a list of all the parsed lines.
let rec calculateSum(rows) =
match rows with
| [] -> 0
| (_, value) :: tail ->
let remainingSum = calculateSum(tail)
value + remainingSum
let mainForm = new Form(Width = 620, Height = 450, Text = "Pie Chart") //same way of doing
//var mainForm = new Form {Width = 620};
let menu = new ToolStrip()
let openButton = new ToolStripButton("Open")
let saveButton = new ToolStripButton("Save", Enabled = false) //same way of
//doing var saveButton = new ToolStripButton("Save") { Enabled = false};
ignore(menu.Items.Add(openButton)) //calling menu.Items.Add() returns the index of the added element, to make sure the result
//is ignored by F# use ignore()
ignore(menu.Items.Add(saveButton)) //this takes any return value and makes it return unit (void)
let boxChart =
new PictureBox
(BackColor = Color.White, Dock = DockStyle.Fill,
SizeMode = PictureBoxSizeMode.CenterImage)
mainForm.Controls.Add(menu)
mainForm.Controls.Add(boxChart)
let random = new Random()
let randomBrush() =
let r,g,b = random.Next(256), random.Next(256), random.Next(256)
new SolidBrush(Color.FromArgb(r,g,b))
let drawPieSegment(gr:Graphics, title, startAngle, occupiedAngle) =
let brush = randomBrush()
gr.FillPie(brush, 170, 70, 260, 260, startAngle, occupiedAngle)
brush.Dispose()
let font = new Font("Times New Roman", 11.0f)
let centerX, centerY = 300.0, 200.0
let labelDistance = 150.0
let drawLabel (graphics:Graphics, title, startAngle, angle) =
let labelAngle = float(startAngle + angle/2)
let ra = Math.PI * 2.0 * labelAngle / 360.0
let x = centerX + labelDistance * cos(ra)
let y = centerY + labelDistance * sin(ra)
let size = graphics.MeasureString(title, font)
let xPoint = float32(x) - size.Width / 2.0f
let yPoint = float32(y) - size.Height / 2.0f
let rc = new PointF(xPoint, yPoint)
let boundingBox = new RectangleF(rc, size)
graphics.DrawString(title, font, Brushes.Black, boundingBox)
let drawStep(drawingFunction, graphics:Graphics, sum, data) =
let rec drawStepUntil(data, angleSoFar) =
match data with
| [] -> () //returns unit ie void
| [title, value] -> //only matches a tuple of tile, value with no tail element
let angle = 360 - angleSoFar
drawingFunction(graphics, title, angleSoFar, angle)
| (title, value) :: tail -> //matches a tuple head of title/value WITH a tail element(s)
let angle = int(float(value) / sum * 360.0)
drawingFunction(graphics, title, angleSoFar, angle)
drawStepUntil(tail, angleSoFar + angle)
drawStepUntil(data, 0)
let drawChart(file) =
let lines = List.ofSeq(File.ReadAllLines(file))
let data = processLines(lines)
let sum = float(calculateSum(data))
let pieChart = new Bitmap(600, 400)
let graphics = Graphics.FromImage(pieChart)
graphics.Clear(Color.White)
drawStep(drawPieSegment, graphics, sum, data)
drawStep(drawLabel, graphics, sum, data)
graphics.Dispose()
pieChart
let openAndDrawChart(e) =
let dialog = new OpenFileDialog(Filter="CSV Files|*.csv")
if(dialog.ShowDialog() = DialogResult.OK) then
let pieChart = drawChart(dialog.FileName)
boxChart.Image <- pieChart
saveButton.Enabled <- true
let saveDrawing(e) =
let dialog = new SaveFileDialog(Filter="PNG Files|*.png")
if(dialog.ShowDialog() = DialogResult.OK) then
boxChart.Image.Save(dialog.FileName)
//TODO: Drawing ofthe chart and UI
[<STAThread>]
do
openButton.Click.Add(openAndDrawChart)
saveButton.Click.Add(saveDrawing)
Application.Run(mainForm)
I will say as exciting as this application was to produce I am still struggling with the way you create applications. I am very curious what is the best practice when you start to create separate modules for your applications. Right now everything is a function, which makes sense but looking at the code you have one file with just a bunch of functions! (I guess i am still thinking in the O-O style mindset.
-
nickthejam likes this
-
markcoleman posted this