F#
Использование F #, WPF, FsXaml, меню и диалогового окна
Поиск…
Вступление
Целью здесь является создание простого приложения в F # с использованием Windows Presentation Foundation (WPF) с традиционными меню и диалоговыми окнами. Это связано с моим разочарованием в попытке пробраться через сотни разделов документации, статей и сообщений, посвященных F # и WPF. Чтобы что-либо делать с WPF, вам, похоже, все нужно знать об этом. Моя цель заключается в том, чтобы предоставить возможный путь, простой настольный проект, который может служить шаблоном для ваших приложений.
Настроить проект
Предположим, вы делаете это в Visual Studio 2015 (сообщество VS 2015, в моем случае). Создайте пустой проект консоли в VS. В проекте | Свойства изменяют тип вывода на приложение Windows.
Затем используйте NuGet для добавления FsXaml.Wpf в проект; этот пакет был создан оценочным Ридом Копси, младший, и он значительно упрощает использование WPF из F #. При установке он добавит ряд других сборок WPF, поэтому вам не придется. В FsXaml есть другие подобные пакеты, но одна из моих целей заключалась в том, чтобы максимально сократить количество инструментов, чтобы сделать общий проект максимально простым и доступным.
Кроме того, добавьте UIAutomationTypes в качестве ссылки; он входит в состав .NET.
Добавьте «Бизнес-логику»
Предположительно, ваша программа что-то сделает. Добавьте свой рабочий код в проект вместо Program.fs. В этом случае наша задача - нарисовать кривые спирографа на холсте окна. Это выполняется с использованием Spirograph.fs, ниже.
namespace Spirograph
// open System.Windows does not automatically open all its sub-modules, so we
// have to open them explicitly as below, to get the resources noted for each.
open System // for Math.PI
open System.Windows // for Point
open System.Windows.Controls // for Canvas
open System.Windows.Shapes // for Ellipse
open System.Windows.Media // for Brushes
// ------------------------------------------------------------------------------
// This file is first in the build sequence, so types should be defined here
type DialogBoxXaml = FsXaml.XAML<"DialogBox.xaml">
type MainWindowXaml = FsXaml.XAML<"MainWindow.xaml">
type App = FsXaml.XAML<"App.xaml">
// ------------------------------------------------------------------------------
// Model: This draws the Spirograph
type MColor = | MBlue | MRed | MRandom
type Model() =
let mutable myCanvas: Canvas = null
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
let rng = new Random()
let mutable myRandomColor = Color.FromRgb(rng.Next(0, 255) |> byte,
rng.Next(0, 255) |> byte,
rng.Next(0, 255) |> byte)
member this.MyCanvas
with get() = myCanvas
and set(newCanvas) = myCanvas <- newCanvas
member this.MyR
with get() = myR
and set(newR) = myR <- newR
member this.Myr
with get() = myr
and set(newr) = myr <- newr
member this.Myl
with get() = myl
and set(newl) = myl <- newl
member this.MyColor
with get() = myColor
and set(newColor) = myColor <- newColor
member this.Randomize =
// Here we randomize the parameters. You can play with the possible ranges of
// the parameters to find randomized spirographs that are pleasing to you.
this.MyR <- rng.Next(100, 500)
this.Myr <- rng.Next(this.MyR / 10, (9 * this.MyR) / 10)
this.Myl <- 0.1 + 0.8 * rng.NextDouble()
this.MyColor <- MRandom
myRandomColor <- Color.FromRgb(rng.Next(0, 255) |> byte,
rng.Next(0, 255) |> byte,
rng.Next(0, 255) |> byte)
member this.DrawSpirograph =
// Draw a spirograph. Note there is some fussing with ints and floats; this
// is required because the outer and inner circle radii are integers. This is
// necessary in order for the spirograph to return to its starting point
// after a certain number of revolutions of the outer circle.
// Start with usual recursive gcd function and determine the gcd of the inner
// and outer circle radii. Everything here should be in integers.
let rec gcd x y =
if y = 0 then x
else gcd y (x % y)
let g = gcd this.MyR this.Myr // find greatest common divisor
let maxRev = this.Myr / g // maximum revs to repeat
// Determine width and height of window, location of center point, scaling
// factor so that spirograph fits within the window, ratio of inner and outer
// radii.
// Everything from this point down should be float.
let width, height = myCanvas.ActualWidth, myCanvas.ActualHeight
let cx, cy = width / 2.0, height / 2.0 // coordinates of center point
let maxR = min cx cy // maximum radius of outer circle
let scale = maxR / float(this.MyR) // scaling factor
let rRatio = float(this.Myr) / float(this.MyR) // ratio of the radii
// Build the collection of spirograph points, scaled to the window.
let points = new PointCollection()
for degrees in [0 .. 5 .. 360 * maxRev] do
let angle = float(degrees) * Math.PI / 180.0
let x, y = cx + scale * float(this.MyR) *
((1.0-rRatio)*Math.Cos(angle) +
this.Myl*rRatio*Math.Cos((1.0-rRatio)*angle/rRatio)),
cy + scale * float(this.MyR) *
((1.0-rRatio)*Math.Sin(angle) -
this.Myl*rRatio*Math.Sin((1.0-rRatio)*angle/rRatio))
points.Add(new Point(x, y))
// Create the Polyline with the above PointCollection, erase the Canvas, and
// add the Polyline to the Canvas Children
let brush = match this.MyColor with
| MBlue -> Brushes.Blue
| MRed -> Brushes.Red
| MRandom -> new SolidColorBrush(myRandomColor)
let mySpirograph = new Polyline()
mySpirograph.Points <- points
mySpirograph.Stroke <- brush
myCanvas.Children.Clear()
this.MyCanvas.Children.Add(mySpirograph) |> ignore
Spirograph.fs - это первый файл F # в порядке компиляции, поэтому он содержит определения типов, которые нам понадобятся. Его задача - нарисовать спирограф в главном окне Canvas на основе параметров, введенных в диалоговом окне. Поскольку есть много ссылок на то, как нарисовать спирограф, мы не будем здесь заниматься этим.
Создайте главное окно в XAML
Вам необходимо создать файл XAML, который определяет главное окно, содержащее наше меню и пространство рисования. Вот код XAML в MainWindow.xaml:
<!-- This defines the main window, with a menu and a canvas. Note that the Height
and Width are overridden in code to be 2/3 the dimensions of the screen -->
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Spirograph" Height="200" Width="300">
<!-- Define a grid with 3 rows: Title bar, menu bar, and canvas. By default
there is only one column -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Define the menu entries -->
<Menu Grid.Row="0">
<MenuItem Header="File">
<MenuItem Header="Exit"
Name="menuExit"/>
</MenuItem>
<MenuItem Header="Spirograph">
<MenuItem Header="Parameters..."
Name="menuParameters"/>
<MenuItem Header="Draw"
Name="menuDraw"/>
</MenuItem>
<MenuItem Header="Help">
<MenuItem Header="About"
Name="menuAbout"/>
</MenuItem>
</Menu>
<!-- This is a canvas for drawing on. If you don't specify the coordinates
for Left and Top you will get NaN for those values -->
<Canvas Grid.Row="1" Name="myCanvas" Left="0" Top="0">
</Canvas>
</Grid>
</Window>
Комментарии, как правило, не включены в файлы XAML, что, я думаю, является ошибкой. Я добавил некоторые комментарии ко всем файлам XAML в этом проекте. Я не утверждаю, что они лучшие комментарии, когда-либо написанные, но они хотя бы показывают, как комментарий должен быть отформатирован. Обратите внимание, что вложенные комментарии не допускаются в XAML.
Создайте диалоговое окно в XAML и F #
Файл XAML для параметров спирографа приведен ниже. Он включает в себя три текстовых поля для параметров спирографа и группу из трех переключателей для цвета. Когда мы даем радиокнопкам одно и то же имя группы - как здесь, WPF обрабатывает включение / выключение при выборе одного из них.
<!-- 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>
Теперь мы добавим код для Dialog.Box. По соглашению, код, используемый для обработки интерфейса диалогового окна с остальной частью программы, называется XXX.xaml.fs, где связанный файл XAML называется 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
()
Большая часть кода здесь посвящена обеспечению того, чтобы параметры спирографа в Spirograph.fs соответствовали параметрам, показанным в этом диалоговом окне. Обратите внимание, что проверка ошибок отсутствует. Если вы введете плавающую точку для целых чисел, ожидаемых в двух верхних полях параметров, программа выйдет из строя. Поэтому, пожалуйста, добавьте проверку ошибок в свои собственные усилия.
Также обратите внимание, что поля ввода параметров отключены, и в переключателях выбран случайный цвет. Здесь просто показать, как это можно сделать.
Чтобы переместить данные назад и вперед между диалоговым окном и программой, я использую System.Windows.Element.FindName (), чтобы найти соответствующий элемент управления, применить его к элементу управления, которым он должен быть, а затем получить соответствующие настройки из Контроль. Большинство других примеров программ используют привязку данных. Я не по двум причинам: во-первых, я не мог понять, как заставить его работать, а во-вторых, когда это не сработало, я не получил никаких сообщений об ошибках. Возможно, кто-то, кто посещает это в StackOverflow, может сказать мне, как использовать привязку данных, не включая совершенно новый набор пакетов NuGet.
Добавьте код для MainWindow.xaml
namespace Spirograph
type MainWindow(app: App, model: Model) as this =
inherit MainWindowXaml()
let myApp = app
let myModel = model
let whenLoaded _ =
()
let whenClosing _ =
()
let whenClosed _ =
()
let menuExitHandler _ =
System.Windows.MessageBox.Show("Good-bye", "Spirograph") |> ignore
myApp.Shutdown()
()
let menuParametersHandler _ =
let myParametersDialog = new DialogBox(myApp, myModel, this)
myParametersDialog.Topmost <- true
let bResult = myParametersDialog.ShowDialog()
myModel.DrawSpirograph
()
let menuDrawHandler _ =
if myModel.MyColor = MRandom then myModel.Randomize
myModel.DrawSpirograph
()
let menuAboutHandler _ =
System.Windows.MessageBox.Show("F#/WPF Menus & Dialogs", "Spirograph")
|> ignore
()
do
this.Loaded.Add whenLoaded
this.Closing.Add whenClosing
this.Closed.Add whenClosed
this.menuExit.Click.Add menuExitHandler
this.menuParameters.Click.Add menuParametersHandler
this.menuDraw.Click.Add menuDrawHandler
this.menuAbout.Click.Add menuAboutHandler
Здесь не так много: при необходимости мы открываем диалоговое окно «Параметры», и у нас есть возможность перерисовать спирограф с любыми текущими параметрами.
Добавьте App.xaml и App.xaml.fs, чтобы связать все вместе
<!-- All boilerplate for now -->
<Application
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>
Вот код:
namespace Spirograph
open System
open System.Windows
open System.Windows.Controls
module Main =
[<STAThread; EntryPoint>]
let main _ =
// Create the app and the model with the "business logic", then create the
// main window and link its Canvas to the model so the model can access it.
// The main window is linked to the app in the Run() command in the last line.
let app = App()
let model = new Model()
let mainWindow = new MainWindow(app, model)
model.MyCanvas <- (mainWindow.FindName("myCanvas") :?> Canvas)
// Make sure the window is on top, and set its size to 2/3 of the dimensions
// of the screen.
mainWindow.Topmost <- true
mainWindow.Height <-
(System.Windows.SystemParameters.PrimaryScreenHeight * 0.67)
mainWindow.Width <-
(System.Windows.SystemParameters.PrimaryScreenWidth * 0.67)
app.Run(mainWindow) // Returns application's exit code.
App.xaml - это все здесь, в основном, чтобы показать, где могут быть объявлены ресурсы приложений, такие как значки, графика или внешние файлы. Сопутствующий App.xaml.fs объединяет модель и MainWindow, размеры MainWindow до двух третей доступного размера экрана и запускает его.
Когда вы создадите это, не забудьте убедиться, что для свойства Build для каждого xaml-файла установлено значение Resource. Затем вы можете либо запустить отладчик, либо выполнить компиляцию в exe-файл. Обратите внимание, что вы не можете запустить это с помощью интерпретатора F #: пакет FsXaml и интерпретатор несовместимы.
Там у вас это есть. Надеюсь, вы сможете использовать это как отправную точку для своих приложений, и при этом вы можете расширить свои знания за пределы того, что показано здесь. Будут оценены любые комментарии и предложения.