The XAML file for the spirograph parameters is below. It includes three text boxes for the spirograph parameters and a group of three radio buttons for color. When we give radio buttons the same group name - as we have here - WPF handles the on/off switching when one is selected.
<!-- This first part is boilerplate, except for the title, height and width.
Note that some fussing with alignment and margins may be required to get
the box looking the way you want it. -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Parameters" Height="200" Width="250">
<!-- Here we define a layout of 3 rows and 2 columns below the title bar -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- Define a label and a text box for the first three rows. Top row is
the integer radius of the outer circle -->
<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="0"
Grid.ColumnSpan="2">
<Label VerticalAlignment="Top" Margin="5,6,0,1" Content="R: Outer"
Height="24" Width='65'/>
<TextBox x:Name="radiusR" Margin="0,0,0,0.5" Width="120"
VerticalAlignment="Bottom" Height="20">Integer</TextBox>
</StackPanel>
<!-- This defines a label and text box for the integer radius of the
inner circle -->
<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="1"
Grid.ColumnSpan="2">
<Label VerticalAlignment="Top" Margin="5,6,0,1" Content="r: Inner"
Height="24" Width='65'/>
<TextBox x:Name="radiusr" Margin="0,0,0,0.5" Width="120"
VerticalAlignment="Bottom" Height="20" Text="Integer"/>
</StackPanel>
<!-- This defines a label and text box for the float ratio of the inner
circle radius at which the pen is positioned -->
<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="2"
Grid.ColumnSpan="2">
<Label VerticalAlignment="Top" Margin="5,6,0,1" Content="l: Ratio"
Height="24" Width='65'/>
<TextBox x:Name="ratiol" Margin="0,0,0,1" Width="120"
VerticalAlignment="Bottom" Height="20" Text="Float"/>
</StackPanel>
<!-- This defines a radio button group to select color -->
<StackPanel Orientation="Horizontal" Grid.Column="0" Grid.Row="3"
Grid.ColumnSpan="2">
<Label VerticalAlignment="Top" Margin="5,6,4,5.333" Content="Color"
Height="24"/>
<RadioButton x:Name="buttonBlue" Content="Blue" GroupName="Color"
HorizontalAlignment="Left" VerticalAlignment="Top"
Click="buttonBlueClick"
Margin="5,13,11,3.5" Height="17"/>
<RadioButton x:Name="buttonRed" Content="Red" GroupName="Color"
HorizontalAlignment="Left" VerticalAlignment="Top"
Click="buttonRedClick"
Margin="5,13,5,3.5" Height="17" />
<RadioButton x:Name="buttonRandom" Content="Random"
GroupName="Color" Click="buttonRandomClick"
HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="5,13,5,3.5" Height="17" />
</StackPanel>
<!-- These are the standard OK/Cancel buttons -->
<Button Grid.Row="4" Grid.Column="0" Name="okButton"
Click="okButton_Click" IsDefault="True">OK</Button>
<Button Grid.Row="4" Grid.Column="1" Name="cancelButton"
IsCancel="True">Cancel</Button>
</Grid>
</Window>
Now we add the code behind for the Dialog.Box. By convention, the code used to handle the interface of the dialog box with the rest of the program is named XXX.xaml.fs, where the associated XAML file is named XXX.xaml.
namespace Spirograph
open System.Windows.Controls
type DialogBox(app: App, model: Model, win: MainWindowXaml) as this =
inherit DialogBoxXaml()
let myApp = app
let myModel = model
let myWin = win
// These are the default parameters for the spirograph, changed by this dialog
// box
let mutable myR = 220 // outer circle radius
let mutable myr = 65 // inner circle radius
let mutable myl = 0.8 // pen position relative to inner circle
let mutable myColor = MBlue // pen color
// These are the dialog box controls. They are initialized when the dialog box
// is loaded in the whenLoaded function below.
let mutable RBox: TextBox = null
let mutable rBox: TextBox = null
let mutable lBox: TextBox = null
let mutable blueButton: RadioButton = null
let mutable redButton: RadioButton = null
let mutable randomButton: RadioButton = null
// Call this functions to enable or disable parameter input depending on the
// state of the randomButton. This is a () -> () function to keep it from
// being executed before we have loaded the dialog box below and found the
// values of TextBoxes and RadioButtons.
let enableParameterFields(b: bool) =
RBox.IsEnabled <- b
rBox.IsEnabled <- b
lBox.IsEnabled <- b
let whenLoaded _ =
// Load and initialize text boxes and radio buttons to the current values in
// the model. These are changed only if the OK button is clicked, which is
// handled below. Also, if the color is Random, we disable the parameter
// fields.
RBox <- this.FindName("radiusR") :?> TextBox
rBox <- this.FindName("radiusr") :?> TextBox
lBox <- this.FindName("ratiol") :?> TextBox
blueButton <- this.FindName("buttonBlue") :?> RadioButton
redButton <- this.FindName("buttonRed") :?> RadioButton
randomButton <- this.FindName("buttonRandom") :?> RadioButton
RBox.Text <- myModel.MyR.ToString()
rBox.Text <- myModel.Myr.ToString()
lBox.Text <- myModel.Myl.ToString()
myR <- myModel.MyR
myr <- myModel.Myr
myl <- myModel.Myl
blueButton.IsChecked <- new System.Nullable<bool>(myModel.MyColor = MBlue)
redButton.IsChecked <- new System.Nullable<bool>(myModel.MyColor = MRed)
randomButton.IsChecked <- new System.Nullable<bool>(myModel.MyColor = MRandom)
myColor <- myModel.MyColor
enableParameterFields(not (myColor = MRandom))
let whenClosing _ =
// Show the actual spirograph parameters in a message box at close. Note the
// \n in the sprintf gives us a linebreak in the MessageBox. This is mainly
// for debugging, and it can be deleted.
let s = sprintf "R = %A\nr = %A\nl = %A\nColor = %A"
myModel.MyR myModel.Myr myModel.Myl myModel.MyColor
System.Windows.MessageBox.Show(s, "Spirograph") |> ignore
()
let whenClosed _ =
()
do
this.Loaded.Add whenLoaded
this.Closing.Add whenClosing
this.Closed.Add whenClosed
override this.buttonBlueClick(sender: obj,
eArgs: System.Windows.RoutedEventArgs) =
myColor <- MBlue
enableParameterFields(true)
()
override this.buttonRedClick(sender: obj,
eArgs: System.Windows.RoutedEventArgs) =
myColor <- MRed
enableParameterFields(true)
()
override this.buttonRandomClick(sender: obj,
eArgs: System.Windows.RoutedEventArgs) =
myColor <- MRandom
enableParameterFields(false)
()
override this.okButton_Click(sender: obj,
eArgs: System.Windows.RoutedEventArgs) =
// Only change the spirograph parameters in the model if we hit OK in the
// dialog box.
if myColor = MRandom
then myModel.Randomize
else myR <- RBox.Text |> int
myr <- rBox.Text |> int
myl <- lBox.Text |> float
myModel.MyR <- myR
myModel.Myr <- myr
myModel.Myl <- myl
model.MyColor <- myColor
// Note that setting the DialogResult to nullable true is essential to get
// the OK button to work.
this.DialogResult <- new System.Nullable<bool> true
()
Much of the code here is devoted to ensuring that the spirograph parameters in Spirograph.fs match those shown in this dialog box. Note that there is no error checking: If you enter a floating point for the integers expected in the top two parameter fields, the program will crash. So, please add error checking in your own effort.
Note also that the parameter input fields are disabled with Random color is picked in the radio buttons. It's here just to show how it can be done.
In order to move data back and forth between the dialog box and the program I use the System.Windows.Element.FindName() to find the appropriate control, cast it to the control it should be, and then get the relevant settings from the Control. Most other example programs use data binding. I did not for two reasons: First, I couldn't figure out how to make it work, and second, when it didn't work I got no error message of any kind. Maybe someone who visits this on StackOverflow can tell me how to use data binding without including a whole new set of NuGet packages.