F#奇妙游(15):优雅的WPF程序

news2024/11/26 22:20:17

WPF but F#

上一篇,写了一个F#的WPF,所有的东西都是随手写出来的,没有经过详细考虑和设计。就是吧,一点也不优雅……咋们虽然头发不多,但是优雅永不过时!

实际上能用的开源UI库(F#,WPF)有两个:

  • Avalonia.FuncUI

在这里插入图片描述

  • Elmish.WPF

在这里插入图片描述

本文旨在学习F#,因此不使用任何第三方库,只用F#和WPF,来实现一个优雅的WPF程序。

优雅的F#

前面写了一大堆F#的帖子,记录了我慢慢学习的过程。到底F#哪里优雅?目前我学到的地方有以下几个,排序完全是个人爱好,不涉及任何学术和技术因素。

  1. 模式识别,简直是太优雅了,配合类型系统、可分解联合类型、匿名函数、管道操作符,简直是太优雅了!
  2. 管道操作,就是那一串集合类型的操作,大概跟LINQ类似的,但是F#提供的方式更加顺眼。
  3. 面向对象的一堆东西,搞来搞去都是type,包括扩散、重载、属性,语法和语义都挺好的,真是excited!

其他我还没感觉到。

那么按照F#的技术特征和上一篇帖子中的大量重复代码,我也来优雅一下。

目标

这一次,我们要把上一篇的计算玩具升级一下。增加两个部分:

  1. 右下角增加一个Slider,同步计算器的数值;
  2. 左下角增加一个TextBox,用来输入前缀,同步窗口标题。

当然,核心的数据还是计算的值。

在这里插入图片描述

优雅一下

从main函数开始

接下来,我要开始装……优雅啦!到底多优雅呢?我希望入口函数只有如下的代码。初始化Application,用属性初始化ViewModel,用属性初始化Window,用属性初始化Window的Title。

[<STAThread>]
[<EntryPoint>]
let main _ =
    let app = Application()

    let FakeViewModel = MyDataViewModel(Count = 20, Prefix = "你好")

    let window =
        Window(
            Width = 480,
            Height = 300,
            WindowStartupLocation = WindowStartupLocation.CenterScreen,
            Content = mainContent FakeViewModel,
            TitleBinding = (FakeViewModel, "Title")
        )

    app.Run(window) |> ignore
    0

这里面有一个需要注意的就是View直接放在Window的Content属性里面。然后ViewModel作为参数传递给mainContent函数。

这里一个特别优雅的就是那行代码:

TitleBinding = (FakeViewModel, "Title")

这里面有几个技术支持我们这样做:

  1. F#(或者说.NET)支持在创建对象是直接设置对象的属性,只能是属性,所以看起来没有重载那么复杂的构造函数,但是用出了一种非常优雅的构造方式。
  2. 对这个属性,TtitleBinding不是WPF提供的,是利用F#的特殊语法进行的类扩展属性。
  3. 属性的设定采用了元组来实现,也特别的优雅。

那我们就先来搞一下这个TitleBinding属性。

优雅的属性扩展

直接先看扩展属性的代码:

    type Window with
        member this.TitleBinding
            with set (value: obj * string) =
                match value with
                | obj, prop ->
                    this.SetBinding(
                        Window.TitleProperty,
                        Binding(Source = obj, Path = PropertyPath(prop), Mode = BindingMode.OneWay)
                    )
                    |> ignore

首先是语法,F#中type ... with 就是扩展类。代码中定义了一个属性TitleBinding,这个属性是只写的,其类型是obj * string,也就是一个元组,第一个元素是一个对象,第二个元素是一个字符串。

只写属性采用with set定义,这里set就是方法名,从set开始就是一个普通函数的样子,参数列表,=,函数体。

这个函数中,通过match xxx with把元组的元素提取出来,其实可以采用let obj, prop = value,不过我match上瘾了……客观地说,这里用let更好,不过我就是喜欢match

最后就是把这个元组的第一个元素作为对象,第二个元素作为属性名,然后调用SetBinding方法,把这个属性绑定到Window.TitleProperty上。

ViewModel

这里不再讲细节,这个部分上一个帖子作为核心内容讲了。

module viewmodel =


    type MyDataViewModel() =
        inherit ViewModelBase()

        let mutable prefix = "Hello"
        let mutable count = 0
        member this.Text = $"计数: %4d{this.Count}"
        member this.Title = $"{this.Prefix} - {this.Text}"

        member this.Prefix
            with get () = prefix
            and set value =
                prefix <- value
                this.OnPropertyChanged(<@ this.Prefix @>)
                this.OnPropertyChanged(<@ this.Title @>)

        member this.Count
            with get () = count
            and set value =
                count <- value
                this.OnPropertyChanged(<@ this.Text @>)
                this.OnPropertyChanged(<@ this.Title @>)
                this.OnPropertyChanged(<@ this.Count @>)

        member this.Increment() = this.Count <- this.Count + 1

只是优雅地定义几个可以用来绑的东西,在优雅地定义了属性之间的依赖关系。

优雅地布局

主函数就这么点内容,下面就是View。这里通过一个函数来定义View,这个函数的参数就是ViewModel,这样就可以在View中使用ViewModel的属性做各种绑定(单向/双向)和操作。

我们先从最优雅的地方来说起,我们希望用Grid来布局。但是Grid布局的调用方式是这样的:

// define grid layout
Grid()
|> Grid.rows [ 1.0; 0.0; 0.0 ]
|> Grid.columns [ 1.0; 1.0 ]
|> Grid.place 0 0 1 2 vb
|> Grid.place 1 0 1 1 button
|> Grid.place 1 1 1 1 button2
|> Grid.place 2 0 1 1 input
|> Grid.place 2 1 1 1 count

这算是优雅了吧。借鉴了集合类的接口设计方法,把Grid对象作为各个函数的最后一个参数,把设置行、列、添加控件三个功能都定义为Grid的扩展静态函数。

调用过程可以用管道操作符来连接,这样就可以把Grid的各个功能调用串联起来,看起来就像是在定义Grid的布局。

同样的,如果需要设置Grid的其他属性,可以Grid构造函数中增加属性设置。

Grid(
    ShowGridLines = true,
)
|> Grid.rows 2
|> Grid.columns 2

这个几个扩展函数的实现也很直观,依然是type Grid with语法。

type Grid with
    /// <summary>
    /// Extension methods for the <see cref="Gird"/> class.
    /// To place a child on row,col with rowspan and colspan, use:
    ///
    ///     <code>Grid.place row col rowspan colspan child g</code>
    ///
    /// Also pipes are supported for add multiple children:
    ///
    ///     <code>
    ///         g
    ///         |> Grid.place row col rowspan colspan child1
    ///         |> Grid.place row col rowspan colspan child2
    ///     </code>
    ///
    /// No exception is handled in this function.
    /// </summary>
    static member place row col rowspan colspan (child: UIElement) (g: Grid) =
        Grid.SetRow(child, row)
        Grid.SetColumn(child, col)
        Grid.SetRowSpan(child, rowspan)
        Grid.SetColumnSpan(child, colspan)
        g.Children.Add(child) |> ignore
        g

    static member rows (rowDefinitions: obj) (g: Grid) =
        match rowDefinitions with
        | :? int as n ->
            [ 1..n ]
            |> List.iter (fun _ -> g.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto)))
        | :? (float list) as heights ->
            heights
            |> List.map (fun h ->
                match h with
                | x when x > 0.0 -> GridLength(x, GridUnitType.Star)
                | _ -> GridLength.Auto)
            |> List.iter (fun h -> g.RowDefinitions.Add(RowDefinition(Height = h)))
        | :? (GridLength list) as heights ->
            heights
            |> List.iter (fun h -> g.RowDefinitions.Add(RowDefinition(Height = h)))
        | _ -> ()

        g

    static member columns (colDefinitions: obj) (g: Grid) =
        match colDefinitions with
        | :? int as n ->
            [ 1..n ]
            |> List.iter (fun _ -> g.ColumnDefinitions.Add(ColumnDefinition(Width = GridLength.Auto)))
        | :? (float list) as widths ->
            widths
            |> List.map (fun w ->
                match w with
                | x when x > 0.0 -> GridLength(w, GridUnitType.Star)
                | _ -> GridLength.Auto)
            |> List.iter (fun w -> g.ColumnDefinitions.Add(ColumnDefinition(Width = w)))
        | :? (GridLength list) as widths ->
            widths
            |> List.iter (fun h -> g.ColumnDefinitions.Add(ColumnDefinition(Width = h)))
        | _ -> ()

        g

唯一值得注意的是rows和columns的定义可以用多种不同的方式,直接用整数表明数量,用浮点数表明比例,用GridLength表明具体的长度。实现的方式是在match obj with中用:?进行类型判断,然后分别处理。

我用match我骄傲!

优雅的控件

那些放在Grid中的控件呢?在F#是不是也有一些很优雅的方式来定义呢?答案是肯定的。这个程序中,仅仅中了TextBlock、Viewbox、Button、TextBox、Slider这几个控件,我们来看看这几个控件的定义。

let textBlock =
    TextBlock(
        Foreground = Brushes.Lime,
        Background = Brushes.Bisque,
        TextAlignment = TextAlignment.Center,
        HorizontalAlignment = HorizontalAlignment.Center,
        VerticalAlignment = VerticalAlignment.Center,
        TextBinding = (datasource, "Text")
    )

let vb =
    Viewbox(
        Margin = Thickness(5.0),
        Stretch = Stretch.Fill,
        StretchDirection = StretchDirection.Both,
        Child = textBlock
    )

// add a button to grid
let button =
    Button(
        Content = "加一",
        FontSize = 32.0,
        FontWeight = FontWeights.Bold,
        Margin = Thickness(5.0),
        OnClick = (fun _ -> datasource.Increment())
    )



// add a button to grid
let button2 =
    Button(
        Content = "清零",
        FontSize = 32.0,
        FontWeight = FontWeights.Bold,
        Margin = Thickness(5.0),
        OnClick = (fun _ -> datasource.Count <- 0)
    )

let input =
    TextBox(FontSize = 32.0, TextBinding = (datasource, "Prefix"), Margin = Thickness(5.0))

let count =
    Slider(
        Minimum = 0,
        Maximum = 100,
        Value = 0,
        ValueBinding = (datasource, "Count"),
        Margin = Thickness(5.0),
        TickPlacement = TickPlacement.BottomRight,
        TickFrequency = 10
    )

这些控件的属性定义全部写在一起,看起来就像是调用了一个很复杂的构造函数。其实实现的方式跟上面一样,都是属性。其中有两个属性是特别优雅的:

  1. 绑定的属性,比如TextBinding,这个属性的类型是一个元组,第一个元素是一个对象,第二个元素是一个字符串,这个字符串是对象的属性名。这样就可以在构造控件的时候,直接把数据源和属性名传递进去,然后在构造函数中进行绑定。
  2. Button的Action,这里定义了OnClick属性。

这个也很简单,OnClick需要注册一个函数,非常简单的实现。

type Button with
    member this.OnClick
        with set func = this.Click.Add(fun _ -> func ())

另外一个需要注意的就是TextBox和Slider这两个可以作为输入的控件,那么这里的绑定就需要实现双向数据传递。

type TextBox with
    member this.TextBinding
        with set (value: obj * string) =
            match value with
            | obj, prop ->
                this.SetBinding(
                    TextBox.TextProperty,
                    Binding(
                        Source = obj,
                        Path = PropertyPath(prop),
                        Mode = BindingMode.TwoWay,
                        UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
                    )
                )
                |> ignore

type Slider with
    member this.ValueBinding
        with set (value: obj * string) =
            match value with
            | obj, number ->
                this.SetBinding(
                    Slider.ValueProperty,
                    Binding(
                        Source = obj,
                        Mode = BindingMode.TwoWay,
                        UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
                        Path = PropertyPath(number)
                    )
                )
                |> ignore

唯一需要注意的就是必须定义UpdateSourceTigger属性,否则默认是在控件失去焦点的时候才会更新数据源,这样就会导致数据源的值和控件的值不一致。

完整代码

整个程序的源代码比较长,ico文件也不好传,就做了一个git的仓库,有兴趣的可以看看。

fsharp.wpf代码仓库

用命令就可以下载。

git clone https://gitcode.net/withstand/fsharp.wpf.git

然后用VS打开,或者 dotnet run

总结

  1. F#可以用来做很多事情,但是UI总是一个让初学者激动的主题,实际上目前F#也有几个能用的UI库,比如Avalonia.FuncUI, Elmish.WPF,但是这里为了更好地学习F#和WPF,用编码的方式则更加有价值。
  2. F#提供的语言工具,类扩展、match、管道操作符、元组、属性等等,都可以用来实现一些非常优雅的代码,这些代码可以让我们的程序更加简洁、易读、易维护。
  3. 我自己也是现学现卖,感觉还挺开心的!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/764474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【NLP】多头注意力概念(02)

接上文: 【NLP】多头注意力概念(01) 五、计算注意力 将 Q、K 和 V 拆分为它们的头部后,现在可以计算 Q 和 K 的缩放点积。上面的等式表明,第一步是执行张量乘法。但是,必须先转置 K。 展望未来,每个张量的seq_length形状将通过其各自的张量来识别,以确保清晰…

2023年知识库软件爆火的有哪些?

2023年知识库软件爆火的可能性有很多&#xff0c;以下是一些可能的候选者&#xff1a; 一、Baklib&#xff1a;Baklib是一款新兴的知识库软件&#xff0c;它提供了强大的知识管理和组织功能。它能够帮助用户收集、整理和共享知识&#xff0c;并提供智能搜索和推荐引擎&#xf…

DP1042 国产代替TJA1042 CAN总线收发器接口芯片

5V 供电&#xff0c;IO 口兼容 3.3V&#xff0c;70V 总线耐压&#xff0c;待机模式 CAN FD 总线收发器DP1042是一款应用于 CAN 协议控制器和物理总线之间的接口芯片&#xff0c;可应用于卡车、公交、小汽车、工业控制等领域&#xff0c;支持 5Mbps CAN FD 灵活数据速率&#xf…

使用stata做meta系列三|Meta命令安装

上期小统带大家学习了Meta分析纳排标准确定&#xff0c;这期小统和大家一起学习一下Meta命令安装。 Meta命令安装 Stata安装meta命令包&#xff1a; http://3g.dxy.cn/bbs/topic/34967648 命令&#xff1a; help pr0012 点击 pr0012…… 点击 click here to install 将pro…

Gateway网关简介及使用

一、 什么是 API 网关&#xff08;API Gateway&#xff09;&#x1f349; 1.分布式服务架构、微服务架构与 API 网关&#x1f95d; 在微服务架构里&#xff0c;服务的粒度被进一步细分&#xff0c;各个业务服务可以被独立的设计、开发、测试、部署和管理。这时&#xff0c;各个…

MySQL数据库,创建表及其插入数据和多表查询数据

首先&#xff0c;建表 创建student表&#xff0c;SQL代码如下 create table student (id int(10) not null unique primary key, name varchar(20) not null, sex varchar (4), brith year, department varchar(20), address varchar(50) ); 创建score表。SQL代码如下&…

自动化测试selenium(1)

自动化测试&#x1f4ea;selenium 自动化测试&#x1f4ea;selenium自动化测试&#x1f4d1;selenium定位元素&#x1f4cd; 实战测试百度搜索&#x1f50d;安装测试环境Idea中进行自动化脚本编写打开网页&#xff1a;实现搜索功能&#xff1a;浏览器清空效果clear()&#xff1…

Vue通过微软官方链接预览pptx docx xlsx

目录&#xff1a; 一、实现步骤 二、实现效果 代码真实可用&#xff01; 一、实现步骤&#xff1a; 1、使用的是vue和elementUI&#xff0c; 假设有这些变量&#xff1a;attachment是附件的意思 data() {return {previewDialog: false,attachmentSrc: ,attachmentList: [{name:…

第六次CCF计算机软件能力认证

第一题&#xff1a;数位之和 给定一个十进制整数 n&#xff0c;输出 n 的各位数字之和。 输入格式 输入一个整数 n。 输出格式 输出一个整数&#xff0c;表示答案。 数据范围 1≤n≤1e9 输入样例&#xff1a; 20151220输出样例&#xff1a; 13样例解释 20151220 的各位数字之和…

PhaseNet论文阅读总结

PhaseNet论文阅读总结 PhaseNet: a deep-neural-network-based seismic arrival-time pickingmethod 背景 地震监测和定位是地震学的基础 地震目录的质量主要取决于到达时间测量的数量和精度相位拾取一般都是网络分析员来执行但是地震仪越来越多&#xff0c;数据流增加&#…

记录自己的程序移植经历(裸机软PLC C语言程序移植到Linux)

先说一下本人的基础&#xff0c;本人是一个小公司初来乍到的实习生&#xff0c;拿到这个任务的时候&#xff0c;不懂PLC&#xff0c;而对于linux只懂一点点皮毛的操作。结果硬是把程序移植完毕且能顺利运行。 该程序是嵌入式软PLC&#xff0c;主要代码是对四元式指令的解析。说…

解决Hadoop集群hive库建表中文和表数据乱码问题

最近在测试环境,发现DDL建表后,发现中文注释和表数据乱码的问题,如下 查询元数据 原因是hive 的 metastore 支持的字符集是 latin1,所以中文写入的时候会有编码问题。 解决方案如下: 对MySQL的编码设置 [client]下面增加 default-character-set=utf8 在[mysqld]下面增…

Pytorch如何打印与Keras的model.summary()类似的输出

1 Keras的model.summary() 2 Pytorch实现 2.1 安装torchsummary包 pip install torchsummary2.2 代码 import torch import torch.nn as nn import torch.nn.functional as F from torchsummary import summaryclass Net(nn.Module):def __init__(self):super(Net, self).__…

微服务Gateway网关(自动定位/自定义过滤器/解决跨域)+nginx反向代理gateway集群

目录 Gateway网关 1.0.为什么需要网关&#xff1f; 1.1.如何使用gateway网关 1.2.网关从注册中心拉取服务 1.3.gateway自动定位 1.4.gateway常见的断言 1.5.gateway内置的过滤器 1.6.自定义过滤器-全局过滤器 1.7.解决跨域问题 2.nginx反向代理gateway集群 2.1.配置…

Matplotlib坐标轴格式

在一个函数图像中&#xff0c;有时自变量 x 与因变量 y 是指数对应关系&#xff0c;这时需要将坐标轴刻度设置为对数刻度。Matplotlib 通过 axes 对象的xscale或yscale属性来实现对坐标轴的格式设置。 示例&#xff1a;右侧的子图显示对数刻度&#xff0c;左侧子图则显示标量刻…

61、Mysql中MVCC是什么

什么是MVCC 多版本并发控制&#xff1a;读取数据时通过一种类似快照的方式将数据保存下来&#xff0c;这样读锁就和写锁不冲突了&#xff0c;不同的事务session会看到自己特定版本的数据&#xff0c;版本链 MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。…

apache ozone详细介绍

Ozone是哪路神 Apache Ozone https://github.com/apache/ozone Ozone是Apache软件基金会下的一个项目&#xff0c;其定位是&#xff1a;一个用户大数据分析和云原生应用、具有高扩展性、强一致性的分布式Key-Value对象存储。 HDFS是业界默认的大数据存储系统&#xff0c;在业…

ThunderScope开源示波器

简介 4CH&#xff0c;1GSa/S 开源示波器。前端很简洁&#xff0c;BUF802LMH6518&#xff0c;ADC是HMCAD1511&#xff0c;用Xilinx A7 FPGA进行控制&#xff0c;数据通过PCIE总线传输到上位机处理。目前这个项目已经被挂到了Xilinx官网&#xff0c;强。 设计日志&#xff1a;h…

设计自己的脚手架

如何设计自己的脚手架 前言前置知识如何搭建一个脚手架搭建自己的脚手架初始化项目安装依赖packagejson 配置lint 和typescript配置 加入bin字段调试npm link调试核心代码实现获取所有命令create实现 美化项目添加logo 发包源码仓库 前言 ​ 在工程中&#xff0c;不仅是软件工…

车载测试:CANoe中环境变量和系统变量的区别

目录 环境变量和系统变量相同点&#xff1a; 环境变量和系统变量不同点&#xff1a; 环境变量和系统变量相同点&#xff1a; 都可以作为ECU、面板和CAPL程序相连接的媒介。例如&#xff0c;在CAPL程序中&#xff0c;通过改变或监控某一环境变量的值可以触发特定的动作&#x…