65.9K
CodeProject 正在变化。 阅读更多。
Home

开始学习 F# - 一个 Windows 窗体应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (25投票s)

2008年10月24日

CPOL

10分钟阅读

viewsIcon

104768

downloadIcon

844

使用 F# 创建 Windows 窗体项目。

引言

在拖延之后,我终于下定决心坐下来学习 F# 编程,因为函数式编程一直以来都让我着迷。尽管很久以前我曾有一些 Prolog 的经验,但我大部分编程都是使用命令式语言,而不是函数式或声明式语言,最近主要使用 C#。当我学习新语言时,总需要花费一番功夫来“跟上节奏”,但对于 F#,我发现初始步骤比我预期的要困难,这不仅是因为学习一门新语言和适应不同的编程范例,还因为缺乏成熟的 IDE。我仍然不是 F# 专家,但也许我早期的一些磕绊可能对他人有所帮助,这样他们就不必经历我所经历的痛苦了。

本文描述了我让 F# 用于桌面应用程序的步骤。首先,我将介绍如何在 VS 2008 中安装 F#,并指出我遇到的一些通用程序和架构相关问题。接下来,我将展示一个简单的 Windows 窗体应用程序,该应用程序可以用作 F# 编写的桌面应用程序的模板。最后,我将总结我对 F# 作为一门语言的看法,提及当前实现的优点和缺点,并解释我认为 F# 在我个人的 .NET 编程工具箱中的位置。

作为一个示例应用程序,我组合了一个简单的解析器测试,用于基于模糊逻辑的专家系统。该解析器仅接受文本输入,并定义变量和模糊集。该应用程序允许将文本保存到文本文件并从文本文件中读取,它仅用于我的测试,因此在模糊逻辑方面没什么用处。对于任何感兴趣的人,实际的解析器和模糊集代码包含在示例项目中,但此处不进行讨论,因为它超出了本文的范围。也许在未来的文章中,我会描述系统的其余部分并展示一个实际的应用。

作为背景,该解析器用于解析模糊集和变量定义。变量使用以下任一形式声明

Variable Name of Context [=] Value (i.e. Variable Depth of Water = 250)
Variable Context Name [=] Value (i.e. Variable Water Depth 250)

模糊集通过以下形式声明

FuzzySet Name Context [=] Values (i.e. FuzzySet Deep Water (0,0) (900,1)

在这两个定义中,都指定了 NameContext,以便区分名称相似的想法,例如,热的一天 vs. 热的火山。两者都可以用温度单位衡量,并且名称相同,但“热”概念的范围和含义却大相径庭。

启动并运行 F#

要开始使用 F#,我从 Microsoft F# 网站[^] 下载了 Microsoft F# CTP (版本 1.9.6.2),保存了 .msi 文件然后运行它来安装 F#。我接受了所有安装默认设置,并且它在没有明显问题的情况下安装好了。

安装完成后,我打开了 VS 2008 Professional,并尝试按照 Microsoft 的说明激活 F# 插件,但 F# 插件不在 工具 | 插件管理器 列表下。在卸载并重新安装但结果相同后,我终于发现,只需键入 Alt+Ctrl+F,F# 交互窗口就会打开,显然第一次安装在大约一小时前就已经成功了。F# 插件仍然没有出现在插件管理器中,但它似乎工作得很好。

一旦安装并运行,使用带有 F# 交互工具的 VS 编辑器就很容易了。只需在编辑器中键入代码,选中它,然后按 Alt+Enter,选中的代码就会被复制到交互式 F# 窗口中,进行编译并运行。这对于尝试、测试和交互式调试代码片段非常方便。此外,在编辑器中,Intellisense 对于我需要的大部分 F# 代码似乎都有效,但并非全部。

但是,在尝试编写比一些微不足道的测试更复杂的内容时,我遇到了两个问题,这些问题在网上其他地方几乎没有提到过。

首先,即使我能够弹出 Windows 窗体,但黑色的 DOS/命令窗口也总是会显示出来。由于 F# 系统不允许您添加 Windows 窗体项目,因此您必须手动转到 项目 | 属性 并将应用程序类型设置为 Windows 应用程序。似乎默认的 F# 项目总是 控制台应用程序

其次,即使我添加了对标准 .NET 命名空间的引用(open System.Windows.Forms 等),F# 编译器也总是抱怨找不到 System.Windows.FormsSystem.Windows.Drawing 等。显然,VS 不会自动添加常用引用,而这些引用必须在 解决方案资源管理器 | 引用 下手动添加。解决了这两项问题后,使用 F# 就相当容易了,其余的麻烦都源于学习一门新语言和编程范例。

一旦我的项目发展到需要多个源文件来保持组织性的程度,我又遇到了一些其他问题。首先,F# 程序中没有明显的入口点、主函数或其他显而易见的起始位置。那么,编译器如何知道从哪里开始呢?

显然,F# 系统会运行 最后一个 编译文件的所有可执行语句。这有两个直接的影响

  • 解决方案资源管理器中文件的顺序很重要。
  • 请确保程序的起点位于项目列表的最后一个文件中。

为了解决这些问题,我采用了将每个类型放在一个单独的文件中的方法,几乎就像在 C# 中一样,并创建一个简单的文件,该文件始终位于文件列表的最后,称为 project_name.fs,其中 project_name 由特定应用程序的实际项目名称替换。这个文件非常简单,只包含以下几行,其中 MyNamespaceMyMainForm() 是特定应用程序使用的命名空间和主窗体名称。请注意,如果您需要在实际显示主窗体之前执行其他操作,则可以在 do Application.Run 语句之前插入这些操作的代码,并且可能需要额外的 open 语句来引用任何需要的命名空间或模块。

#light

open System
open System.Windows.Forms
open MyNamespace

[<STAThread>]
do Application.Run(new MyMainForm())

请注意,[<STAThread>] 行是一个 .NET 属性,它定义应用程序在单个线程单元中运行。在使用某些 .NET 对话框(如 FileOpenDialogFileSaveDialog)时,这是必需的,因为它们似乎在后台使用了 COM Interop。如果您不使用其中任何一个,则不需要该属性。

构建 Windows 窗体应用程序

在处理 F# 时,我真正开始欣赏 VS 2008 和其他 IDE 为 C#、VB 及其他语言提供的设计器。由于 F# 没有窗体设计器,我不得不手动编写所有窗口。为了保持简单(对我而言),我采用了下面所示的编码风格来处理代表应用程序的 MainForm 的一部分。

#light

namespace MyNamespace

open System
open System.Windows.Forms
open System.Drawing

type MainForm() as form = 
    inherit Form()
    // Define private variables
    let mutable fuzzySets = []
    let mutable fuzzyRules = []
    let mutable fuzzyVariables = []
    let mutable fileName = ""
    // Define the controls for this form
    let mainMenu = new MainMenu()
    let mnuFile = new MenuItem()
    let mnuFileOpen = new MenuItem()
    let mnuFileSave = new MenuItem()
    let mnuFileSaveAs = new MenuItem()
    let mnuFileExit = new MenuItem()
    let mnuHelp = new MenuItem()
    let mnuHelpAbout = new MenuItem()
    let label1 = new Label()
    let label2 = new Label()
    let label3 = new Label()
    let lstFuzzySets = new ListBox()
    let lstVariables = new ListBox()
    let txtInput = new RichTextBox()
    let btnCalculate = new Button()
    let dlgFileOpen = new OpenFileDialog()
    let dlgFileSave = new SaveFileDialog()
    let HomeDir = Application.ExecutablePath
    // Private functions
    let rec getVariable ((lst:(Variable list)), vName:string, vContext:string) =
        match lst with
        | [] -> failwith (sprintf "Variable %s.%s not found" vName vContext)
        | x::_ when (x.Name = vName) && (x.Context = vContext) -> x
        | _::t -> getVariable(t, vName, vContext)
    let rec getFuzzySet ((lst:(FuzzySet list)), vName:string, vContext:string) =
        match lst with
        | [] -> failwith (sprintf "FuzzySet %s.%s not found" vName vContext)
        | x::_ when (x.Name = vName) && (x.Context = vContext) -> x
        | _::t -> getFuzzySet(t, vName, vContext)
    let rec prtVars (s:(Variable list)) =
        match s with
        | [] -> ""
        | x::y -> (sprintf "[%s %s = %f]" x.Name x.Context x.Value)^(prtVars y)
    let rec prtFSets (s:(FuzzySet list)) =
        match s with
        | [] -> ""
        | x::y -> (sprintf "[%s %s = %A]" x.Name x.Context x.Def)^(prtFSets y)
    // The constructor simply initializes the form
    do form.InitializeForm

    // member definitions
    member this.InitializeForm =
        // Set Form attributes
        this.FormBorderStyle <- FormBorderStyle.Sizable
        this.Text <- "Fuzzy Logic Parser F# Test"
        this.Width <- 300
        this.Height <- 300
        // Declare Form events
        this.Load.AddHandler(new System.EventHandler 
            (fun s e -> this.Form_Loading(s, e)))
        this.Closed.AddHandler(new System.EventHandler 
            (fun s e -> this.Form_Closing(s, e)))
        // MainMenu
        mnuFile.Text <- "&File"
        mnuFileOpen.Text <- "&Open"
        mnuFileOpen.Click.AddHandler(new System.EventHandler 
            (fun s e -> this.mnuFileOpen_Click(s, e)))
        mnuFileSave.Text <- "&Save"
        mnuFileSave.Click.AddHandler(new System.EventHandler 
            (fun s e -> this.mnuFileSave_Click(s, e)))
        mnuFileSaveAs.Text <- "Save &As"
        mnuFileSaveAs.Click.AddHandler(new System.EventHandler 
            (fun s e -> this.mnuFileSaveAs_Click(s, e)))
        mnuFileExit.Text <- "E&xit"
        mnuFileExit.Click.AddHandler(new System.EventHandler 
            (fun s e -> this.mnuFileExit_Click(s, e)))
        mnuFile.MenuItems.AddRange([| mnuFileOpen; mnuFileSave; 
                mnuFileSaveAs; mnuFileExit |])
        mnuHelp.Text <- "&Help"
        mnuHelpAbout.Text <- "&About"
        mnuHelpAbout.Click.AddHandler(new System.EventHandler 
            (fun s e -> this.mnuHelpAbout_Click(s, e)))
        mnuHelp.MenuItems.AddRange([| mnuHelpAbout |])
        mainMenu.MenuItems.AddRange([| mnuFile; mnuHelp |])
        this.Menu <- mainMenu
        // label1
        label1.Text <- "Fuzzy Sets"
        label1.Location <- new Point(5,2)
        label1.Dock <- DockStyle.None
        label1.AutoSize <- true
        // lstFuzzySets
        lstFuzzySets.Location <- new Point(5,19) 
        lstFuzzySets.Width <- 137
        lstFuzzySets.Height <- 120
        lstFuzzySets.MouseDoubleClick.AddHandler(new MouseEventHandler 
            (fun s e -> this.lstFuzzySets_Click(s, e)))

        ...

        // Add controls to form
        this.Controls.AddRange([| 
                                (label1:> Control);
                                (lstFuzzySets:> Control);
                                (label2:> Control);
                                (lstVariables:> Control);
                                (label3:> Control);
                                (txtInput:> Control);
                                (btnCalculate:> Control)
                               |])
    
    member this.Form_Loading(sender : System.Object, e : EventArgs) =
        lstFuzzySets.Items.Clear()
        lstVariables.Items.Clear()

    member this.Form_Closing(sender : System.Object, e : EventArgs) =
         null

    member this.mnuFileOpen_Click(sender : System.Object, e : EventArgs) = 
        dlgFileOpen.DefaultExt <- "txt"
        if dlgFileOpen.ShowDialog() = DialogResult.OK then
            fileName <- dlgFileOpen.FileName
            txtInput.Clear()
            txtInput.LoadFile(fileName)
        else null

    ...
        
    member this.mnuHelpAbout_Click(sender : System.Object, e : EventArgs) = 
        (new AboutForm()).ShowDialog() |> ignore
        
    member this.lstFuzzySets_Click(sender : System.Object, e : MouseEventArgs) =
        let s = String.split ['.'] (lstFuzzySets.SelectedItem.ToString())
        let text (x:FuzzySet) = sprintf "%s = %A" (x.Name^"."^x.Context) (x.Def)
        MessageBox.Show(text (getFuzzySet(fuzzySets, s.Head, s.Tail.Head)))  |> ignore
        
    ...

请注意,窗体通过继承标准的 .NET Form 来定义。然后,我定义了任何需要的私有“变量”,然后定义了放置在窗体上的所有控件。在控件之后,我放置了要执行的构造函数代码;在这种情况下,它只是调用 form.InitializeForm 函数来初始化窗体。最后是所有需要的成员函数,包括所有事件处理程序。

InitializeForm 成员内部,每个控件都按需设置,最后所有控件都添加到窗体中。此外,所有事件都定义为成员,以便对它们进行组织、单独编码并从窗体外部调用。这与 C# 和 VB 设计器自动组织事物的方式类似。据推测,当 F# 的设计器可用时,它们也会以类似的方式来处理事物的组织。我必须承认,在手动完成控件布局后,我确实很欣赏使用其他语言可用的设计器的能力!

上述代码有几点需要注意。

首先,事件处理程序使用标准的 .NET AddHandler 添加到窗体和每个控件中,并使用 F# 中的匿名函数进行定义。大多数事件处理程序接受两个参数,分别对应 senderEventArgs,在匿名函数中用 se 表示。

其次,所有事件处理程序都期望返回一个 unit(在 C# 中为 void)。在 F# 函数结果为非 unit 类型的情况下,有必要使用 null 关键字显式返回一个 unit 值,或者使用 |> ignore 丢弃一个值。

正如你所见,上述代码中并没有真正的函数式编程;它几乎都是纯粹的命令式代码,类似于 C# 中使用的代码,只是语法不同。这主要是因为依赖于本质上是命令式的 .NET 框架。当然,F# 同时处理命令式和声明式代码的能力是其优势之一,并且能够编程 Windows 窗体展示了 F# 的命令式一面。

对于任何感兴趣的人,实际的解析器和模糊集代码包含在示例项目中,但此处不进行讨论,因为它超出了本文的范围。也许在未来的文章中,我会描述系统的这些部分。

结论

在对 F# 进行一段时间的实验并基本掌握之后,我无疑形成了一些看法。首先,我必须说 F# 看起来是一门非常好的、声明式的函数式语言,并且将在科学和数学计算的许多领域得到广泛应用。由于它也允许命令式编程,因此可以相对容易地访问底层 .NET 框架中的对象和功能,这是一个很大的优点。

不幸的是,F# 在其当前的实现中,在“开发工具”方面存在很大不足。具体来说,在 Visual Studio 中缺乏窗体设计器和完整的 Intellisense 支持使得使用起来有些乏味。此外,一些默认的项目设置不太直观,并且文档不够完善。希望这两个问题在不久的将来都能得到解决,F# 才能与其他 .NET 语言一起,成为一门严肃的开发语言。

除了缺乏开发工具之外,F# 编程还需要一些时间来适应,特别是如果您像我一样习惯使用命令式语言。从命令式到声明式的范式转变可能很困难,而且 F# 似乎有很多细微的怪癖,确实需要付出一些集中的努力才能掌握。我当然还没有完全掌握它,但我希望下次回家时能买一两本关于这门语言的书,并在未来更多地深入研究 F#,因为我确实看到了它将成为我编程工具箱中的一个有用工具。

我建议任何有一点点兴趣的人都去了解 F#。扩展自己的简历,或者拓展思维从不同的角度思考问题,总是有益的。

此外,我真诚地希望 Microsoft 能继续改进 F#,并在不久的将来提供额外的项目模板、窗体设计器和完整的 Intellisense 支持。在我的愿望清单中,我还要加上,能够用 C#(甚至 VB?)设计和实现窗体,但调用用 F# 编写的函数将是极好的。到目前为止,我唯一能做到这一点的方法是,将 F# 代码编译成一个库(*.dll),然后从 C# 调用它。能够轻松地将 C# 和 F# 文件包含在同一个项目中,并分别用相应的编译器编译,然后链接成一个应用程序,这将允许人们享受到两种语言的最佳特性。

历史

  • 2008 年 10 月 24 日 - 文章提交;果然,发布后不久,我就不得不纠正一个拼写错误。
© . All rights reserved.