0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

MVVM是什么?

汽车电子技术 来源:工控羊 作者: zyuanlbj 2023-02-07 14:32 次阅读

前言

最近在学习WPF,买了一本刘铁锰老师的《深入浅出WPF》书籍,受益颇深。刘老师是微软社区精英,他的C#视频课程也是很受欢迎,感兴趣的小伙伴可以去b站观看。

这里,以一个在线订餐系统为例,和大家一起分享MVVM的魅力。

MVVM

首先,我们来看看MVVM到底是什么?

MVVM是Model-View-ViewModel的简写,它是一种极度优秀的设计模式,也是MVC的增强版。

图片

View:用户界面,也叫视图

由控件构成的、与用户进行交互的界面,用于把数据展示给用户并响应用户的输入。

ViewModel:MVVM的核心

ViewModel通过双向数据绑定将View和Model连接了起来,而View和Model之间的同步工作都是完全自动的,无需人为操作。

Model:数据模型

现实世界中事物和逻辑的抽象。

在WPF中,数据占据主导地位,数据与界面之间的桥梁是数据关联,通过这个桥梁,数据可以流向界面,也可以从界面流回数据源。

ViewModel的存在,使得界面交互业务逻辑处理导致的属性变更会通知到View前端,让View前端实时更新;View的变动,也会自动反应到ViewModel上。

MVVM的出现促进了前端开发与后端的分离,极大提高了前端的开发效率;几乎完全解耦了视图和业务逻辑的关系。

案例剖析

基于以上理解,我们来剖析下面这个在线订餐系统。

图片

将主界面划分为三个区域:第一个区域是餐馆的信息(名字、地址以及电话),中间区域是菜单列表,每个菜品都有名字、种类、点评、评分以及价格,还有选中框;第三个区域有菜品的选中总数和订餐按钮。

这里,我们忽视菜品数据的来源是来自数据库还是其他什么存储方式,也忽视点击订餐按钮后订餐数据的处理,主要是想将更多精力集中在MVVM实现上。

我们来找找有多少个数据属性和命令属性。

显而易见能看出有一个 餐馆Model ,它有名字、地址和电话三个属性,因此,有一个餐馆类数据属性。

右下角有个Order按钮,明显是一个命令属性。

当我们选中某个菜品时,这是一个命令属性;同时共计框那里会有数值变化,所以,也会有一个菜品选中总数数据属性。

中间区域菜单列表中,有很多不同的菜品,所以会有一个 菜品Model ,它有名字、种类、点评、评分以及价格属性。

以上,我们都能轻易分析出来,但是还有一个选中框,理解起来有一点点难度。

当我们打开软件时,所有的菜品以列表形式展示出来,它的属性是固定不变的,只有后面的选中框是用户点击的,它的值是动态变化的。

因此,我们把不变的菜品和变化的选中框当做是一个ViewModel,它有两个数据属性,菜品类和是否被选中。

我们的主界面,也会有一个与之对应的ViewModel 它有三个数据属性,餐馆类、选中菜品总数和Dish列表;有两个命令属性,订餐和菜品是否选中命令。

到这里,基于在线订餐系统的MVVM各个模型,都已经清楚明了了。接下来,我们编程实现它。

案例实现

要实现MVVM,我们需借助Prism。

Prism是一个框架,用于在WPF和Xamarin Forms中构建松散耦合,它提供了一组设计模式的实现,这些设计模式有助于编写结构良好且可维护的XAML应用程序,包括MVVM、依赖注入、命令、EventAggregator等。

这里用到的是Prism的NotificationObject基类和DelegateCommand,旨在帮助我们借助ViewModel实现View与Model的数据自动更新。

打开VS2019,新建一个解决方案,再新建几个文件夹,分别是Data、Services、Views、Models和ViewModels。这样,我们的项目整体架构就搭建好了。

图片

右击引用,通过管理NuGet程序包,在弹出的窗口浏览中输入“Prism.MVVM”,在线安装Prism。

Data文件夹中,存放的是Data.xml,里面是菜品信息。


<Dishes>
  <Dish>
    <Name>水煮肉片Name>
    <Category>徽菜Category>
    <Comment>招牌菜Comment>
    <Score>9.7Score>
    <Price>45元Price>
  Dish>
  <Dish>
    <Name>椒盐龙虾Name>
    <Category>川菜Category>
    <Comment>招牌菜Comment>
    <Score>9.2Score>
    <Price>43元Price>
  Dish>
  <Dish>
    <Name>京酱猪蹄Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.8Score>
    <Price>51元Price>
  Dish>
  <Dish>
    <Name>爆炒鱿鱼Name>
    <Category>徽菜Category>
    <Comment>招牌菜Comment>
    <Score>9.3Score>
    <Price>54元Price>
  Dish>
  <Dish>
    <Name>可乐鸡翅Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>44元Price>
  Dish>
  <Dish>
    <Name>凉拌龙须Name>
    <Category>湘菜Category>
    <Comment>凉拌Comment>
    <Score>8.6Score>
    <Price>18元Price>
  Dish>
  <Dish>
    <Name>麻辣花生Name>
    <Category>湘菜Category>
    <Comment>凉拌Comment>
    <Score>8.7Score>
    <Price>19元Price>
  Dish>
  <Dish>
    <Name>韭菜炒肉Name>
    <Category>湘菜Category>
    <Comment>炒菜Comment>
    <Score>9.4Score>
    <Price>25元Price>
  Dish>
  <Dish>
    <Name>青椒肉丝Name>
    <Category>湘菜Category>
    <Comment>炒菜Comment>
    <Score>9.1Score>
    <Price>26元Price>
  Dish>
  <Dish>
    <Name>红烧茄子Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>24元Price>
  Dish>
  <Dish>
    <Name>红烧排骨Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>42元Price>
  Dish>
  <Dish>
    <Name>番茄蛋汤Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>21元Price>
  Dish>
  <Dish>
    <Name>山药炒肉Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>27元Price>
  Dish>
  <Dish>
    <Name>极品肥牛Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>58元Price>
  Dish>
  <Dish>
    <Name>香拌牛肉Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>48元Price>
  Dish>
  <Dish>
    <Name>手撕包菜Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>16元Price>
  Dish>
  <Dish>
    <Name>香辣花甲Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>36元Price>
  Dish>
  <Dish>
    <Name>酸菜鱼Name>
    <Category>湘菜Category>
    <Comment>招牌菜Comment>
    <Score>9.4Score>
    <Price>56元Price>
  Dish>
Dishes>

Models文件夹中,存放的是Dish类和Restaurant类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace zy.CrazyElephant.Client.Models
{
    public class Dish
    {
        public string Name { get; set; }
        public string Category { get; set; }
        public string Comment { get; set; }
        public double Score { get; set; }
        public string Price { get; set; }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace zy.CrazyElephant.Client.Models
{
   public class Restaurant
    {
        public string Name { get; set; }
        public string Address { get; set; }
        public string PhoneNumber { get; set; }
    }
}

ViewModels中,存放的是两个ViewModel。

using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using zy.CrazyElephant.Client.Models;

namespace zy.CrazyElephant.Client.ViewModels
{
   public class DishMenuItemViewModel:NotificationObject
    {
        public Dish Dish{ get; set; }

        private bool isSelected;

        public bool IsSelected
        {
            get { return isSelected; }
            set
            {
                isSelected = value;
                this.RaisePropertyChanged("IsSelected");
            }
        }

    }
}
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using zy.CrazyElephant.Client.Models;
using zy.CrazyElephant.Client.Services;

namespace zy.CrazyElephant.Client.ViewModels
{
    public class MainWindowViewModel:NotificationObject
    {
        public MainWindowViewModel()
        {
            this.LoadRestaurant();
            this.LoadMenu();

            this.PlaceOrderCommand = new DelegateCommand(PlaceOrderCommandExecute);
            this.SelectMenuItemCommand = new DelegateCommand(SelectMenuItemExecute);
        }

        public DelegateCommand PlaceOrderCommand { get; set; }
        public DelegateCommand SelectMenuItemCommand { get; set; }

        private int count;

        public int Count
        {
            get { return count; }
            set
            {
                count = value;
                this.RaisePropertyChanged("Count");
            }
        }

        private Restaurant restaurant;

        public Restaurant Restaurant
        {
            get { return restaurant; }
            set
            {
                restaurant = value;
                this.RaisePropertyChanged("Restaurant");
            }
        }

        private List dishMenu;

        public List DishMenu
        {
            get { return dishMenu; }
            set
            {
                dishMenu = value;
                this.RaisePropertyChanged("DishMenu");
            }
        }

        private void LoadRestaurant()
        {
            this.Restaurant = new Restaurant();
            this.Restaurant.Name = "聚贤庄";
            this.Restaurant.Address = "xx省xx市xx区xx街道xx楼xx层xx号";
            this.Restaurant.PhoneNumber = "18888888888 or 6666-6666666";
        }

        private void LoadMenu()
        {
            XmlDataService ds = new XmlDataService();
            var dishes = ds.GetAllDishes();
            this.DishMenu = new List();
            foreach (var dish in dishes)
            {
                DishMenuItemViewModel item = new DishMenuItemViewModel();
                item.Dish = dish;
                this.DishMenu.Add(item);
            }
        }

        private void PlaceOrderCommandExecute()
        {
            var selectedDishes = this.DishMenu.Where(i => i.IsSelected == true).Select(i => i.Dish.Name).ToList();
            IOrderService os = new MockOrderService();
            os.PlaceOrder(selectedDishes);
            MessageBox.Show("订餐成功!");
        }

        private void SelectMenuItemExecute()
        {
            this.Count = this.DishMenu.Count(i => i.IsSelected == true);
        }
    }
}

Services中,IDataService和IOrderService,是两个基接口,前一个用于获取菜品数据;后一个用于订餐处理。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using zy.CrazyElephant.Client.Models;

namespace zy.CrazyElephant.Client.Services
{
    public interface IDataService
    {
        List GetAllDishes();
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace zy.CrazyElephant.Client.Services
{
    public interface IOrderService
    {
        void PlaceOrder(List dishes);
    }
}

XmlDataService类,继承自IDataService,用于读取xml文件,获取菜品数据。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using zy.CrazyElephant.Client.Models;

namespace zy.CrazyElephant.Client.Services
{
    public class XmlDataService : IDataService
    {
        public List GetAllDishes()
        {
            List dishList = new List();
            string xmlFileName = System.IO.Path.Combine(Environment.CurrentDirectory, @"Data\\Data.xml");
            XDocument doc = XDocument.Load(xmlFileName);
            var dishes = doc.Descendants("Dish");
            foreach (var d in dishes)
            {
                Dish dish = new Dish();
                dish.Name = d.Element("Name").Value;
                dish.Category = d.Element("Category").Value;
                dish.Comment = d.Element("Comment").Value;
                dish.Score = Convert.ToDouble(d.Element("Score").Value);
                dish.Price = d.Element("Price").Value;
                dishList.Add(dish);
            }
            return dishList;
        }
    }
}

MockOrderService类,继承自IOrderService,用于处理订餐逻辑,这里是把选中的菜品名字以txt文件保存到硬盘中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace zy.CrazyElephant.Client.Services
{
    public class MockOrderService : IOrderService
    {
        public void PlaceOrder(List dishes)
        {
            System.IO.File.WriteAllLines(Environment.CurrentDirectory + "\\\\order.txt", dishes.ToArray());
        }
    }
}

只有一个界面,即MainWindow.xaml,代码如下。

<Window x:Class="zy.CrazyElephant.Client.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:zy.CrazyElephant.Client"
        mc:Ignorable="d"
        Title="{Binding Restaurant.Name,StringFormat=\\{0\\}-在线订餐}" Height="600" Width="1000" WindowStartupLocation="CenterScreen">
    <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="6" Background="AliceBlue">
        <Grid x:Name="Root" Margin="4">
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="auto"/>
            Grid.RowDefinitions>
            <Border BorderBrush="Orange" BorderThickness="1" CornerRadius="6" Padding="4">
                <StackPanel>
                    <StackPanel Orientation="Horizontal">
                        <StackPanel.Effect>
                            <DropShadowEffect Color="LightGray"/>
                        StackPanel.Effect>
                        <TextBlock Text="欢迎光临-" FontSize="60" FontFamily="LiShu"/>
                        <TextBlock Text="{Binding Restaurant.Name}" FontSize="60" FontFamily="LiShu"/>
                    StackPanel>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="小店地址:" FontSize="24" FontFamily="LiShu"/>
                        <TextBlock Text="{Binding Restaurant.Address}" FontSize="24" FontFamily="LiShu"/>
                    StackPanel>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="订餐电话:" FontSize="24" FontFamily="LiShu"/>
                        <TextBlock Text="{Binding Restaurant.PhoneNumber}" FontSize="24" FontFamily="LiShu"/>
                    StackPanel>
                StackPanel>
            Border>
            <DataGrid AutoGenerateColumns="False" GridLinesVisibility="None" CanUserAddRows="False" CanUserDeleteRows="False" Margin="0.4" Grid.Row="1" FontSize="16" ItemsSource="{Binding DishMenu}">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="菜品" Binding="{Binding Dish.Name}" Width="120"/>
                    <DataGridTextColumn Header="种类" Binding="{Binding Dish.Category}" Width="120"/>
                    <DataGridTextColumn Header="点评" Binding="{Binding Dish.Comment}" Width="120"/>
                    <DataGridTextColumn Header="推荐分数" Binding="{Binding Dish.Score}" Width="120"/>
                    <DataGridTextColumn Header="价格" Binding="{Binding Dish.Price}" Width="120"/>
                    <DataGridTemplateColumn Header="选中" SortMemberPath="IsSelected" Width="120">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <CheckBox IsChecked="{Binding Path=IsSelected,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" HorizontalAlignment="Center" Command="{Binding Path=DataContext.SelectMenuItemCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGrid}}"/>
                            DataTemplate>
                        DataGridTemplateColumn.CellTemplate>
                    DataGridTemplateColumn>
                DataGrid.Columns>
            DataGrid>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="2">
                <TextBlock Text="共计" VerticalAlignment="Center"/>
                <TextBox IsReadOnly="True" TextAlignment="Center" Width="120" Text="{Binding Count}" Margin="4,0"/>
                <Button Content="Order" Height="24" Width="120" Command="{Binding PlaceOrderCommand}"/>
            StackPanel>
        Grid>
    Border>
Window>

MainWindow.xaml.cs中,代码很简单,只需要添加一行即可。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using zy.CrazyElephant.Client.ViewModels;

namespace zy.CrazyElephant.Client
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel();
        }
    }
}

无论界面如何变化,只要符合这种逻辑形式的软件通过前端界面Binding的方式,我们的业务逻辑代码就不会变动,通用性很强。实现了前端界面与后端逻辑分离,开闭原则应用的很到位。

写在最后

基本上,绝大多数软件所做的工作无非就是从数据存储中读出数据,展现到用户界面上,然后从用户界面接收输入,写入到数据存储里面去。所以,对于数据存储(Model)和界面(View)这两层,大家基本没什么异议。但是,如何把Model展现到View上,以及如何把数据从View写入到Model里,不同的人有不同的意见。

MVC派的看法是,界面上的每个变化都是一个事件,我只需要针对每个事件写一堆代码,来把用户的输入转换成Model里的对象就行了,这堆代码可以叫Controller。

而MVVM派的看法是,我给View里面的各种控件也定义一个对应的数据对象,这样,只要修改这个数据对象,View里面显示的内容就自动跟着刷新;而在View里做了任何操作,这个数据对象也跟着自动更新,这样多美。

所以,ViewModel就是与View对应的Model。因为,数据库结构往往是不能直接跟界面控件一一对应上的,因此,需要再定义一个数据对象专门对应View上的控件。而ViewModel的职责就是把Model对象封装成可以显示和接受输入的界面数据对象。

至于ViewModel的数据随着View自动刷新,并且同步到Model里去,这部分代码可以写成公用的框架,不用程序员自己操心了。

简单的说,ViewModel就是View与Model的连接器,View与Model通过ViewModel实现数据双向绑定。

END

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Model
    +关注

    关注

    0

    文章

    338

    浏览量

    25031
  • 设计模式
    +关注

    关注

    0

    文章

    53

    浏览量

    8622
  • MVC
    MVC
    +关注

    关注

    0

    文章

    73

    浏览量

    13840
收藏 人收藏

    评论

    相关推荐

    #硬声创作季 Java项目实战(金融项目)-MVVM

    JAVA编程语言
    Mr_haohao
    发布于 :2022年09月07日 10:55:17

    Vue2.0+3.0-Day3-03.简介 - mvvm

    vue
    电子学习
    发布于 :2023年01月08日 21:10:12

    89.089 尚硅谷 尚融宝 MVVM

    项目开发
    充八万
    发布于 :2023年07月18日 18:21:32

    AWTK-MVVM在STM32H743上是怎样去移植的

    AWTK-MVVM 在 STM32H743 上的移植笔记本项目除了实现基本功能的移植外,还提供了如下功能:集成实时操作系统 (RTOS)(腾讯的 TinyOS)集成 FATFS 文件系统,访问 SD
    发表于 08-24 06:45

    AWTK-MVVM是什么?其功能有哪些

    AWTK-MVVM是一套为AWTK用C语言开发,并支持各种脚本语言的MVVM框架,实现了数据绑定、命令绑定和窗口导航等基本功能,使用AWTK-MVVM开发应用程序,无需学习AWTK本身的API,只需
    发表于 12-15 06:07

    iOS中怎样用代码实现mvvm的记录

    卷首 最近新工作中用到的RAC+MVVM的开发模式,由于之前都是用MVC,从自己的菜鸡水平感觉这两种设计模式在思想上还是有些微区别的,然后自己也是看了挺多关于这两个模式异同与使用利弊的文章,但是说
    发表于 09-25 11:19 0次下载
    iOS中怎样用代码实现<b class='flag-5'>mvvm</b>的记录

    前端渲染引擎的优势分析

    React、Vue、Angular等均属于MVVM模式,在一些只需完成数据和模板简单渲染的场合,显得笨重且学习成本较高,而解决该问题非常优秀框架之一是doT.js,本文将对它进行详解。 背景 前端
    发表于 09-30 13:14 0次下载
    前端渲染引擎的优势分析

    如何使用协议的实现 MVVM 架构

    类型,而转为使用继承来实现。 通过 Natasha 在 do{iOS} 2015上对 MVVM 的介绍,您可以学习到如何使用协议来实现这个功能,而不再采用继承的方式!Natasha The Robot
    发表于 10-11 15:13 0次下载
    如何使用协议的实现 <b class='flag-5'>MVVM</b> 架构

    MVC、MVP与MVVM的异同介绍

    View和ViewModel内部通过一个Binder进行事件交互,该Binder通过双向绑定将View与ViewModel中与对于数据操作的部分链接,当对应数据由更新时同样会自动地反馈到View层上。
    的头像 发表于 06-22 15:34 4689次阅读

    基于MVVM模式的气动数据可视化分析系统

    基于MVVM模式的气动数据可视化分析系统
    发表于 06-23 16:05 27次下载

    AWTK-MVVM C语言MVVM框架

    ./oschina_soft/gitee-awtk-mvvm.zip
    发表于 06-21 11:33 2次下载
    AWTK-<b class='flag-5'>MVVM</b> C语言<b class='flag-5'>MVVM</b>框架

    工业上位机开发实战WPF+MVVM框架

    上一篇博客介绍了上位机实现MVVM 框架的步骤 MVVMtoolkit 学习_叮当说的博客-CSDN博客 下面我们继续来讲解下实现上位机中会遇到的一些小问题:之前的程序中我们已经知道了 ,当数据改变
    发表于 05-09 11:30 0次下载
    工业上位机开发实战WPF+<b class='flag-5'>MVVM</b>框架

    MVVM+RAC的基本概念和使用方式

    在iOS开发中,采用合适的架构模式能够提高代码的可维护性和可测试性。
    的头像 发表于 06-06 14:55 1172次阅读

    使用MVVM框架实现一个简单加法器

    使用MVVM框架来实现一个简单加法器。最终效果如下,点击按钮可以对上面两个文本框中的数字进行相加得出结果显示在第三个文本框中。重点在于看mvvm框架下程序该怎么写。使用CommunityToolkit.Mvvm框架,通过nuge
    的头像 发表于 10-24 14:23 766次阅读
    使用<b class='flag-5'>MVVM</b>框架实现一个简单加法器

    AWTK 开源智能串口屏方案

    AWTK开源智能串口屏方案发布,旨在解决传统串口屏诸多痛点,为用户提供更开放、更易用、更强大的开源串口屏方案。基于AWTK和AWTK-MVVM实现的串口屏方案。界面修改数据,自动通知MCU。MCU
    的头像 发表于 12-02 08:24 882次阅读
    AWTK 开源智能串口屏方案