热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:从React的视角谈谈Rust和GTK

篇首语:本文由编程笔记#小编为大家整理,主要介绍了从React的视角谈谈Rust和GTK相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了从React的视角谈谈Rust和GTK相关的知识,希望对你有一定的参考价值。








作者 | Savanni D'Gerinel


译者 | 王强


策划 | 李俊辰


最近我尝试了多种框架,想要制作出既易用又容易安装的应用程序,但是都以失败告终;最后我决定转向 Rust 和 GTK,开始拥抱原生软件开发。

虽说以前我也短暂尝试过 GTK,但它对我来说还是很陌生的。在此之前,我在用户界面上的大部分经验都来自于 React 应用程序的构建。从 React 到 GTK 的过渡带来了一些挑战,其中多数是小部件原理上的差异造成的。用 Rust 写 GTK 是尤其困难的事情,因为 Rust 强制执行一些额外的规则来防止内存管理错误,并避免在线程上下文中执行不安全的操作。



在本文中,我将主要讨论如何将 React 的理念应用到 GTK 中,并重点介绍一些使 GTK 符合 Rust 规则所必需的技巧。Rust 制订了一些不好对付的强制规则,这些规则对于大多数开发人员来说都是陌生的;规则主要涉及值的共享方式,但在可变性方面也有严格的限制。我将在本文中遇到这些场景时指出它们。


本文中的所有示例均来自 FitnessTrax,这是一款遵循隐私优先原则的健身追踪应用程序。用户可以在他们的 PC 上的一处存储空间内收集健身和生物识别数据,而不必依赖那些可能无法持续保护用户数据的公司。


关于这款应用程序的外观我要说句抱歉,因为 0.4 版发布的时候,我还没去花时间了解 GTK 是如何处理样式的。我保证会尽快改进用户界面。



框架哲学上的一些差异

Conrod 是针对 Rust 的一个图形工具包,它试着将函数式响应编程技术应用到了图形编程上;它的开发者它描述了两种有着明显区别的图形组件管理模式。在大多数原生图形编程采用的通用模式,亦即“保留模式(retained mode)”下,开发人员将创建一个个屏幕组件,然后在它们的整个生命周期内一次次更新。在“立即模式(immediate mode)”下,组件将具有一个绘制(draw)方法,其中组件会立即实例化自身的所有子级。然后,框架将对比这棵树与上一棵树,来判断该如何更新屏幕。


React 完全运行在即时模式下,而 GTK 完全运行在保留模式下。在 Web 开发行业中流行的数据可视化库 D3 也可以运行在保留模式下。2018 年,我写了一篇关于 React 和 D3 之间对接的文章:


https://www.cloudcity.io/blog/2018/08/07/breaking-d3s-deathgrip-on-the-dom-bringing-old-code-back-to-life-in-a-react-era/


与 Redux 或 Apollo-GraphQL 搭配的 React 实现了函数式响应编程(FRP)的一些理念,让它可以自动处理传播到组件的数据更改。我入门 FRP 时看的是 Elise Huard 写的一本书“Haskell 中的游戏编程”。时至今日这本书可能已经过时了,但在 Haskell 中特定的某个 FRP 库的背景下,它确实很好地介绍了这种理念。不幸的是,FRP 尚未在 React 之外得到广泛采用。虽说至少有一个可用于 Rust 的 FRP 库,但在撰写本文时,对于我来说采用它还为时过早。因此,凭借一些创造力和我在 React 领域的经验,我设计了一些类似于 FRP 范式的机制。


一些术语的注释:



  • 小部件(widget) 是一个 GTK 对象,代表屏幕上的某些内容。它可以是一个窗口、按钮、标签或一个布局容器。GTK 小部件只能将其他 GTK 小部件作为自身的子级。


  • 组件 是屏幕上一个部分的任意逻辑抽象。在简单的情况下,它会是一个从某个函数返回的 GTK 小部件。在更复杂的情况下,它可能是包含一个或多个小部件的结构。组件不一定必须传递给 GTK 函数。结构组件始终提供一个'widget'字段,其代表这个组件的根小部件。




不可变值的显示

所有组件中最简单的,就像 React 组件一样是一小组小部件,这些小部件创建后就永远不会更新。这可以简单地实现为返回一个 GTK 小部件的函数。



从React的视角谈谈Rust和GTK




pub fn date_c(date: &chrono::Date<chrono_tz::Tz>) -> gtk::Label {
    gtk::Label::new(Some(&format!("{}", date.format("%B %e, %Y"))))
}


当组件实际上是一个很少或甚至从不更新的可视组件时,这种模式就是可行的。在我的应用程序中,日期标签是更大一块显示内容的子组件,因此是永远不变的东西。



具有内部小部件状态的组件

具有内部小部件状态的组件肯定要复杂得多,但仍然可以实现为一个返回 GTK 小部件的函数。调用方可以直接从返回的 GTK 小部件中读取数据;在调用方提供一个回调,并且组件代码写明了何时调用回调时,这种模式可以说是最有效的。


我有一个会验证文本的输入字段。这是一个常规的 gtk::Entry),但是接口抽象了'render'、'parse'和'on_update'函数背后的文本处理过程。



从React的视角谈谈Rust和GTK


pub fn validated_text_entry_c<A: 'static + Clone>(
    value: A,
    render: Box<dyn Fn(&A) -> String>,
    parse: Box<dyn Fn(&str) -> Result<A, Error>>,
    on_update: Box<dyn Fn(A)>,
) -> gtk::Entry {
    let widget = gtk::Entry::new();
    widget.set_text(&render(&value));

    let w = widget.clone();
    widget.connect_changed(move |v| match v.get_text() {
        Some(ref s) => match parse(s.as_str()) {
            ...
        },
        NOne=> (),
    });

    widget
}


调用者必须提供一个初始值、一个'render'函数,一个'parse'函数和一个'on_update'函数。在我的实现中,验证文本的输入框将在每次更改后尝试解析框中的字符串,并且仅在解析成功时才调用'on_update'函数。这样以来,调用方负责保存数据,而不必去管解析或验证数据是否有效的机制。我发现,将一个表单的所有值都存储在一个位置的模式特别有用。将所有数据存储在一起可以让我立即将错误通知给用户,还可以检测出由于无效数据组合而发生的错误,并能在出现错误时轻松禁用“提交”按钮。



具有内部状态的组件


2020-01-31:事实证明,我在本节的代码中犯了一些大错。我需要对其进行相当大的修改,以更有效地处理组件更新,并在一个 GTK 回调中更改组件状态。




当我使用上面提到的这种简单组件构建应用程序时,我将它们组合到一些更复杂的组件中,这些组件具有多份逻辑上互相归属,但机制上可以在各个子组件中编辑的数据。为此,我在设置内部状态时会独立于子组件的状态。


所幸我一般来说还是可以将其实现为一个函数。


拿骑自行车来举例,我把这个活动抽象为一个“时间 / 距离”记录。一个时间 / 距离事件具有一个开始时间、一个活动类型(骑自行车、步行、奔跑、皮划艇旅行...)、一段距离和一段持续时间。我的用户界面将所有这些都绑定到一个组件中,可以一次性更新全部记录。



从React的视角谈谈Rust和GTK


pub time_distance_record_edit_c(
    record: TimeDistanceRecord,
    on_update: Box,
    ) -> gtk::Box {
}


到了这里,我们就开始遇到 Rust 用来确保安全内存管理的强制规则了。每个值都只有一个所有者。虽然你可以借用该值的引用,但是只有这些引用超出范围后,该值的所有者才超出范围。此外,如果没有其他任何类型的引用,则你只能获得一个可变的引用。Rust Book 详细讨论了这些规则,并提供了大量示例和场景。


还好所有内容已经齐备了。我需要一种在多个回调函数之间共享记录的方法,并且需要一种方法来确保对记录的安全多线程访问。我们用一个 Arc 来解决共享问题。这是一个线程安全的引用计数容器。传递给 Arc 的初始化器的所有值都归 Arc 所有。克隆一个 Arc 会增加引用计数,并创建另一个指向该共享值的引用。



Arc 不允许对其包含的值进行可变访问,因此我们还需要包含一个 RwLock。像我们期望的那样,RwLock 允许多重读取者,但仅允许一个写入者,并且当存在写入者时不允许有读取者。于是我们像这样来安全更改记录:




pub time_distance_record_edit_c(
    record: TimeDistanceRecord,
    ...) -> gtk::Box {

    let record_ref = Arc::new(RwLock::new(record));

    {
        let mut rec = record_ref.write().unwrap();
        ref.activity = Cycling
    }



在代码子块内,'rec'成为对记录数据的一个可变引用。'RwLock'控制对数据的读 / 写访问,而'Arc'允许跨函数甚至线程共享数据。综上所述,我们的代码如下所示:




pub time_distance_record_edit_c(
    record: TimeDistanceRecord,
    ...
    on_update: Box,
) -> gtk::Box {

    let on_update = Arc::new(on_update);
    let record = Arc::new(RwLock::new(record));

    let duration_entry = {
        let record = record.clone();
        let on_update = on_update.clone();
        let duration = record.read().unwrap().duration.clone();
        duration_edit_c(
            &duration,
            Box::new(move |res| match res {
                Some(val) => {
                    let mut r = record.write().unwrap();
                    r.duration = Some(val);
                    on_update(r.clone());
                }
                NOne=> (),
            }),
        )
    };
}


注意:函数始终是只读的,因此仅需要'Arc'即可共享


回顾一下,在上面的函数中,我们有一段代码来克隆包含记录的 Arc。该克隆将移至'duration_edit_c'的回调函数中(这意味着该回调函数现在拥有这个克隆)。在这个回调函数中将可变地借用记录、更新记录、克隆数据并将其传递给'on_update',然后将在该块末尾自动删除写锁定。


一下子要学的东西真不少。如果你不熟悉 Rust,我强烈建议你阅读有关所有权和借用系统的知识,这是让内存管理无需开发人员操心,而又不会带来垃圾收集器负担的魔法。



从系统状态更改来更新

最后,第四个模式涵盖了需要响应系统更改的所有组件。用 React 术语来说,这意味着属性可能从 Redux 更改。


在较高的层级上,我们需要一个'struct'来跟踪在给定新数据时可能会更新的所有可视组件,以及一个将处理这些更新并返回根级小部件的'render'函数。


在这里我用自己的 History 组件举例。





struct HistoryComponent {
    widget: gtk::Box,
    history_box: gtk::Box,
}

pub struct History {
    component: Option,
    ctx: Arc>>,
}

impl History {
    pub fn new(ctx: Arc>>) -> History { ... }

    pub fn render(
        &mut self,
        range: DateRange,
        records: Vec>>,
    ) -> >k::Box { ... }



实际上,这里的构造函数非常简单,除了创建抽象的'History'组件外什么都不做。由于它没有数据可填充到小部件中,因此它这里甚至还没有创建小部件。这样非常方便,因为在构造时组件可能会需求尚不可用的数据。大部分工作是在'render'中完成的:




pub fn render(
        &mut self,
        range: DateRange,
        records: Vec>,
    ) -> >k::Box {
        match self.component {
            NOne=> {
                let widget = gtk::Box::new(gtk::Orientation::Horizontal, 5);

                /* create and show all of the widgets */

                self.compOnent= Some(HistoryComponent {
                    widget,
                    history_box,
                });

                self.render(prefs, range, records)
            }
            Some(HistoryComponent {...}) => {
                ..
            }
        }
    }



如果这是第一个'render'调用,则可视组件尚不存在。'Render'将创建所有组件,然后再次调用自身以使用数据填充它们。




pub fn render(
        &mut self,
        range: DateRange,
        records: Vec>,
    ) -> >k::Box {
        match self.component {
            NOne=> {
                ...
            }
            Some(HistoryComponent {
                ref widget,
                ref history_box,
                ...
            }) => {
                history_box.foreach(|child| child.destroy());
                records.iter().for_each(|record| {
                    let ctx = self.ctx.clone();
                    let day = Day::new(
                        record.clone(),
                        ctx,
                    );
                    day.show();
                    history_box.pack_start(&day.widget, true, true, 25);
                });
                &widget
            }
        }
    }


在随后的调用中,render 将处理小部件的更新。如何填充新数据的细节因组件而异。在本例中我将销毁所有现有子组件,并根据我拥有的数据创建新的子组件。这是一个非常幼稚的策略,但有时它挺好用的。



    总结    

就这些了。经过数周的学习,在理解如何编写 GTK 的过程中我发现了四种高级模式。我觉得这些模式已经很完备了。


在撰写本文的整个过程中,我也对我的组件做了大量修改、重构和简化。我想这四种模式将帮助我进一步改进我的应用程序,同时我也希望在继续学习的过程中能学到更多内容。



延伸阅读

https://savanni.luminescent-dreams.com/2020/01/15/rust-react-gtk/




推荐阅读
  • 本文详细解析了Java中流的概念,特别是OutputStream和InputStream的区别,并通过实际案例介绍了如何实现Java对象的序列化。文章不仅解释了流的基本概念,还探讨了序列化的重要性和具体实现步骤。 ... [详细]
  • 本文探讨了Flutter和Angular这两个流行框架的主要区别,包括它们的设计理念、适用场景及技术实现。 ... [详细]
  • 本文详细解析了MySQL中常见的几种错误,并提供了具体的解决方法,帮助开发者快速定位和解决问题。 ... [详细]
  • 函子(Functor)是函数式编程中的一个重要概念,它不仅是一个特殊的容器,还提供了一种优雅的方式来处理值和函数。本文将详细介绍函子的基本概念及其在函数式编程中的应用,包括如何通过函子控制副作用、处理异常以及进行异步操作。 ... [详细]
  • 一个转子曲线面积问题及其反问题的解答
    曾经解答过这样一个问题,从该ID的最后一次登录时间、该ID显示的专业信息,误以为是新闻里某个想不开的同学,不安了一阵子。经确认是我多虑了,不过把问题答案还是写出来。之后就收到一堆要求帮忙算 ... [详细]
  • 本文详细介绍了Python的multiprocessing模块,该模块不仅支持本地并发操作,还支持远程操作。通过使用multiprocessing模块,开发者可以利用多核处理器的优势,提高程序的执行效率。 ... [详细]
  • Java 中静态和非静态嵌套类的区别 ... [详细]
  • PHP网站部署指南:从零开始搭建PHP网站
    本文提供了详细的步骤指导,帮助开发者在不同环境下成功部署PHP网站,包括在IIS和Apache服务器上的具体操作。 ... [详细]
  • 使用EF Core在.Net Core控制台应用中操作SQLite数据库
    本文介绍如何利用Visual Studio 2019和Windows 10环境,通过Entity Framework Core(EF Core)实现对SQLite数据库的读写操作。项目源代码可从百度网盘下载。 ... [详细]
  • 本文详细介绍了Java中的泛型概念及其在类、接口和方法中的应用。泛型是自JDK 1.5以来引入的一项重要特性,旨在增强代码的灵活性和安全性。 ... [详细]
  • 本文详细探讨了在使用 Python 的 pip 工具安装包时遇到的 ReadTimeoutError 错误,并提供了有效的解决方案。 ... [详细]
  • 本文将深入探讨 Unreal Engine 4 (UE4) 中的距离场技术,包括其原理、实现细节以及在渲染中的应用。距离场技术在现代游戏引擎中用于提高光照和阴影的效果,尤其是在处理复杂几何形状时。文章将结合具体代码示例,帮助读者更好地理解和应用这一技术。 ... [详细]
  • EHF频段(5G毫米波频段)的应用与发展
    国际电信联盟(ITU)负责管理卫星业务的频段分配,其中Ka频段因其较高的雨衰减和对器件工艺的高要求,发展相对缓慢。然而,随着C波段和Ku波段卫星轨道资源的枯竭及频率带宽的紧张,Ka频段近年来得到了快速发展。 ... [详细]
  • Ubuntu 14.04 系统安装后网卡名称修改方法
    本文介绍了在安装 Ubuntu 14.04 Server 版本后,如何将默认的网卡名称从非 eth 格式修改为传统的 eth 格式,并提供了详细的步骤和示例。 ... [详细]
  • 传统上,Java 的 String 类一直使用 char 数组来存储字符数据。然而,在 Java 9 及更高版本中,String 类的内部实现改为使用 byte 数组。本文将探讨这一变化的原因及其带来的好处。 ... [详细]
author-avatar
手机用户2502920591_700
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有