수색…


소개

목표는 전통적인 메뉴와 대화 상자가있는 WPF (Windows Presentation Foundation)를 사용하여 F #에서 간단한 응용 프로그램을 작성하는 것입니다. 그것은 F # 및 WPF를 다루는 문서, 기사 및 게시물의 수백 섹션을 통해 웨이드하려는 내 좌절에서 유래합니다. WPF로 무엇이든하기 위해서는 그것에 대해 모든 것을 알아야 할 것 같습니다. 여기 내 목적은 앱의 템플릿으로 사용할 수있는 간단한 데스크톱 프로젝트에서 가능한 방법을 제공하는 것입니다.

프로젝트 설정

Visual Studio 2015 (VS 2015 Community, 제 경우)에서이 작업을 수행한다고 가정합니다. VS에서 빈 콘솔 프로젝트를 만듭니다. 프로젝트 | 속성은 출력 유형을 Windows 응용 프로그램으로 변경합니다.

그런 다음, NuGet을 사용하여 FsXaml.Wpf를 프로젝트에 추가하십시오. 이 패키지는 추정 가능한 Reed Copsey, Jr.에 의해 만들어졌으며 F #에서 WPF를 사용하는 것을 크게 단순화했습니다. 설치시 다른 WPF 어셈블리가 추가되어 사용자가하지 않아도됩니다. FsXaml과 비슷한 패키지가 있지만 내 목표 중 하나는 전체 프로젝트를 가능한 간단하고 maintaiable하게 만들기 위해 가능한 한 작게 도구의 수를 유지하는 것이 었습니다.

또한 UIAutomationTypes를 참조로 추가하십시오. .NET의 일부로 제공됩니다.

"비즈니스 로직"추가

아마, 당신의 프로그램은 뭔가를 할 것입니다. Program.fs 대신 프로젝트에 작업 코드를 추가하십시오. 이 경우, 우리의 임무는 창 캔버스에 spirograph 커브를 그리는 것입니다. 이것은 아래의 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에 spirograph를 그립니다. 스피로 그래프를 그리는 방법에 대한 많은 참고 자료가 있으므로 여기서는 다루지 않을 것입니다.

XAML에서 기본 창 만들기

메뉴 및 그리기 공간이 포함 된 주 창을 정의하는 XAML 파일을 만들어야합니다. MainWindow.xaml의 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 #에서 대화 상자 만들기

spirograph 매개 변수에 대한 XAML 파일은 아래와 같습니다. 그것은 spirograph 매개 변수에 대한 세 개의 텍스트 상자와 색상에 대한 세 개의 라디오 버튼 그룹을 포함합니다. 라디오 버튼에 동일한 그룹 이름을 지정하면 (여기에있는 것처럼) 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

여기에는 많은 일이 없습니다. 필요한 경우 매개 변수 대화 상자를 열고 현재 매개 변수가 무엇이든간에 spirograph를 다시 그리는 옵션이 있습니다.

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는 Model과 MainWindow를 함께 가져 와서 MainWindow를 사용 가능한 화면 크기의 3 분의 2로 크기를 매기고 실행합니다.

빌드 할 때 각 xaml 파일의 Build 속성이 Resource로 설정되어 있는지 확인하십시오. 그런 다음 디버거를 실행하거나 exe 파일로 컴파일 할 수 있습니다. F # 인터프리터를 사용하여 실행할 수 없습니다. FsXaml 패키지와 인터프리터는 호환되지 않습니다.

당신은 그것을 가지고 있습니다. 이것을 자신의 응용 프로그램을위한 출발점으로 삼을 수 있기를 바랍니다. 그렇게하면 여기에 표시된 것 이상으로 지식을 확장 할 수 있습니다. 모든 의견 및 제안을 주시면 감사하겠습니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow