当前位置:   article > 正文

基于C#语言的GUI开发,主要介绍WPF框架_wpf ui框架

wpf ui框架

框架

简介

下面是几种常见基于.Net的界面开发框架,都是Microsoft提供,下面简介由GPT生成

WinForms (Windows Forms

WPF (Windows Presentation Foundation)

UWP (Universal Windows Platform)

MAUI (Multi-platform App UI)

  1. WinForms(Windows Forms):
    • 介绍: WinForms是由Microsoft提供的用于开发Windows桌面应用程序的UI框架。它使用简单的拖放界面进行用户界面的构建,并基于.NET框架。
    • 主要特点:
      • 使用可视化设计器进行快速应用程序开发(RAD)。
      • 事件驱动的编程模型。
      • 与Windows API直接集成。
  2. WPF(Windows Presentation Foundation):
    • 介绍: WPF是用于构建具有丰富用户界面的Windows桌面应用程序的框架。与WinForms相比,它提供了更现代和灵活的UI开发方法。
    • 主要特点:
      • 强大的数据绑定和模板系统。
      • 分辨率独立的矢量图形。
      • 可扩展和可定制的控件。
      • UI和业务逻辑的分离。
  3. UWP(Universal Windows Platform):
    • 介绍: UWP是由Microsoft引入的平台,用于创建可以在各种Windows设备上运行的通用应用程序,包括PC、平板、手机、Xbox、HoloLens等。它是Windows 10生态系统的一部分。
    • 主要特点:
      • 自适应用户界面,适应不同设备形状。
      • 安全性和性能改进。
      • 与Windows 10功能的集成,如Cortana和Live Tiles。
  4. MAUI(Multi-platform App UI):
    • 介绍: MAUI是由Microsoft推出的现代跨平台框架,用于从单一代码库构建适用于iOS、Android、Windows和macOS的本机应用程序。它是Xamarin.Forms的演进,是.NET MAUI堆栈的一部分。
    • 主要特点:
      • 单一代码库适用于多个平台。
      • 支持每个平台的本机UI控件。
      • 与.NET 6及更高版本集成。
      • 面向各种设备,包括桌面和移动设备。

选择

winform过于古早

uwp的话,我们就用windows电脑,没必要

maui因为跨平台,导致了其臃肿

所以我们选择WPF,下面文章也是主要讲WPF的

UI

设计思路

先根据功能,设计布局

然后就是如果项目比较大,要考虑设计模式的选取,让前后端分离

控件

因为UI的开发都是基于界面和控件的,而我们常用的控件都大同小异,需要的时候自行查阅文档就好了

常用

信息类 label

编辑类 文本编辑 textbox 状态编辑 checkbox

按钮 button

添加方式

拖拽添加

代码添加

布局

定位

anchor,锚定某个边缘

dock,停靠,按弹出的选项进行设置

重写Onlayout函数(布局改变时触发)

分组

panel,多层次布局,将控件绑定到panel上,便于组合整体控制

事件

即界面获取操作,传到后端进行相应

winform

项目结构

VS选择基于C#的和.Net的窗体应用创建

直接运行,可以看到一个空的界面

然后可以看解决方案管理,可以看到有

  • Form1.cs这个是界面设计文件,直接打开展示UI,可以右键查看代码
  • Form1.Designer.cs,由我们的设计自动生成的底层代码,由设计文件调用
  • 上面两个是同一个类分在不同的文件,前者处理事件,后者处理布局
  • Form.resx
  • program.cs 程序入口,new Form1()来示例化我们的窗口

通过拖拽添加控件,通过属性添加事件.界面的主要代码都在designer中,我们最好不要动它,需要的改动都在Form1.cs中

因为应用场景少,所以不深入学习

WPF

WPF入门基础教程系列

项目结构

  • App.xaml StartupUri设置启动的窗口,Resources样式模板
  • MainWindow.xaml 拖拽布局,可实时渲染

然后我们直接运行项目,可以看到一个空白窗口

xaml

xaml是一种基于xml的标记语言,用于定义UI和属性,这里面都会根据xaml生成对应的cs文件

就下面这个语法

<CMD ..... />等价于<CMD>.....</CMD>

我们看``MainWindow.xaml`

<Window x:Class="WPF.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:WPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>

    </Grid>
</Window>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

解释如下

  1. <Window> 元素:
    • x:Class="WPF.MainWindow":这个属性指定了窗口的类名。在这里,窗口的类名是 MainWindow,它与代码中的类名相对应。这个属性通常用于将XAML文件与后端代码(例如C#)中的类关联起来。
  2. 命名空间声明:
    • xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation":指定默认的XML命名空间,该命名空间用于定义WPF的基本呈现元素,如窗口、面板、按钮等。
    • xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml":指定XAML专用命名空间,其中包含一些用于XAML文件的特殊属性。
  3. 附加的命名空间声明:
    • xmlns:d="http://schemas.microsoft.com/expression/blend/2008":用于Blend工具的命名空间,该工具通常用于设计和交互式开发。
    • xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006":定义了一些用于在不同XAML编译器版本之间保持兼容性的属性。
  4. 本地命名空间声明:
    • xmlns:local="clr-namespace:WPF":指定了一个本地的命名空间,它与命名空间 WPF 相关联。这使得在XAML文件中可以引用 WPF 命名空间中定义的类和资源。
  5. mc:Ignorable="d"
    • 这个属性告诉编译器在处理未知的 d 前缀时忽略错误。在这里,d 是Blend工具的命名空间,而 Ignorable 属性允许在不影响编译的情况下忽略这个未知的前缀。
  6. 窗口的属性设置:
    • Title="MainWindow":设置窗口的标题为 “MainWindow”。
    • Height="450"Width="800":设置窗口的高度和宽度分别为450和800。

布局

所有东西都可以通过属性来改变

  • StackPanel

    StackPanel 主要用于垂直或水平排列元素、在容器的可用尺寸内放置有限个元素,元素的 尺寸总和(长/高)不允许超过 StackPanel 的尺寸, 否则超出的部分不可见。

  • WrapPanel

    WrapPanel 默认排列方向与 StackPanel 相反、WrapPanel 的 Orientation 默认为 Horizontal。 WrapPanel 具备 StackPanel 的功能基础上具备在尺寸变更后自动适应容器的宽高进行换行换列处理。

  • DockPanel

    停靠式布局

    内部控件通过DockPanel.Dock 属性来布局,默认添加 Left。

    DockPanel 有一个 LastChildFill 属性, 该属性默认为 true, 该属性作用为, 当容器中的最后一个元素时, 默认该元素填充 DockPanel 所有空间。

  • Grid

    Grid 具备分割空间的能力。

    RowDefinitions / ColumnDefinitions 用于给 Grid 分配行与列。

    Row / Column 来确定控件位置

    ColumnSpan / RowSpan来设置控件长宽

    如下来进行分割与分配

    尺寸中auto表示跟网格内的控件一样,*表示比例划分auto剩下的

      <Grid.RowDefinitions>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="37*" />
          <RowDefinition Height="138*" />
          <RowDefinition Height="175*" />
          <RowDefinition Height="175*" />
          <RowDefinition Height="175*" />
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
          <ColumnDefinition Width="58*" />
          <ColumnDefinition Width="58*" />
          <ColumnDefinition Width="Auto" />
          <ColumnDefinition Width="362*" />
          <ColumnDefinition Width="71*" />
      </Grid.ColumnDefinitions>
      <Label Grid.Column="1" Content="Label" Grid.ColumnSpan="1" Grid.RowSpan="3" HorizontalAlignment="Center" Grid.Row="2" VerticalAlignment="Center"/>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

控件

WPF.jpg

  • ContentControl 类

    内容属性为 Content,只能设置一次,但可以嵌套

  • ItemsControl 类

    此类控件大多数属于显示列表类的数据、设置数据源的方式一般通过 ItemSource 设置。

  • HeaderedContentControl 类

    相对于 ContentControl 来说、这类控件即可设置 Content, 还有带标题的 Header。 像比较常见的分组控件 GroupBox、TabControl 子元素 TabItem、它们都是具备标题和内容的控件。

  • 常用控件

    • TextBlock: 用于显示文本, 不允许编辑的静态文本。
    • TextBox: 用于输入/编辑内容的控件、作用与 winform 中 TextBox 类似, Text 设置输入显示的内容。
    • Button: 简单按钮、Content 显示文本、Click 可设置点击事件、Command 可设置后台的绑定命令。
    • ComboBox: 下拉框控件, ItemSource 设置下拉列表的数据源, 也可以显示设置。

事件

在MainWindow.xaml里面设置,用于触发函数

namespace WPF
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();//界面显示
      		
        }

        //添加的事件函数
        private void Button_Click(object sender, RoutedEventArgs e)
        {

        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

样式

可设置样式

  • 字体(FontFamily)
  • 字体大小(FontSize)
  • 背景颜色(Background)
  • 字体颜色(Foreground)
  • 边距(Margin)
  • 水平位置(HorizontalAlignment)
  • 垂直位置(VerticalAlignment)

全局的在app中的<Application.Resources>中添加

局部的在当前xaml里面添加<Window.Resources>(与grid同级)

<Style x:Key="TextBlockStyle"  TargetType="TextBlock">
    <Setter Property="Text" Value="TextBlock" />
    <Setter Property="Height" Value="30" />
    <Setter Property="Width" Value="60" />
    <Setter Property="Background" Value="Red" />
</Style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用

<TextBlock TextWrapping="Wrap" Style="{StaticResource TextBlockStyle}"/>
<!--这里也可以再修改样式,优先级高于Style-->
  • 1
  • 2

触发器

一样是style,在对应事件触发时执行

<Style x:Key="TextBlockStyle"  TargetType="TextBlock">
    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Background" Value="Blue" />
        </Trigger>
    </Style.Triggers>
</Style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

模板

控件模板

我们在样式中所能做的修改有限,

于是可以右键控件,编辑副本来创建新的控件

如给按钮添加圆角

在编辑副本后,在生成的副本中,给Border标签加上CornerRadius="10"

然后设置按钮Style="{StaticResource ButtonStyle1}"

数据模板

CellTemplate

表格实现,利用grid

模板直接放在里,这个表格设置了三列,并在第四列设置了两个按钮

 <DataGrid Name="gd" AutoGenerateColumns="False" CanUserSortColumns="True"  CanUserAddRows="False">
     <DataGrid.Columns>
         <DataGridTextColumn Binding="{Binding UserName}" Width="100" Header="学生姓名"/>
         <DataGridTextColumn Binding="{Binding ClassName}" Width="100" Header="班级名称"/>
         <DataGridTextColumn Binding="{Binding Address}" Width="200" Header="地址"/>
         <DataGridTemplateColumn Header="操作" Width="100" >
             <DataGridTemplateColumn.CellTemplate>
                 <DataTemplate>
                     <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
                         <Button Content="编辑"/>
                         <Button Margin="8 0 0 0" Content="删除" />
                     </StackPanel>
                 </DataTemplate>
             </DataGridTemplateColumn.CellTemplate>
         </DataGridTemplateColumn>
     </DataGrid.Columns>
 </DataGrid>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

绑定数据,在InitializeComponent();后面

public class Student
{
    public string UserName { get; set; }
    public string ClassName { get; set; }
    public string Address { get; set; }
}

List<Student> students = new List<Student>();
students.Add(new Student() { UserName = "小王", ClassName = "高二三班", Address = "广州市" });
students.Add(new Student() { UserName = "小李", ClassName = "高三六班", Address = "清远市" });
students.Add(new Student() { UserName = "小张", ClassName = "高一一班", Address = "深圳市" });
students.Add(new Student() { UserName = "小黑", ClassName = "高一三班", Address = "赣州市" });
gd.ItemsSource = students;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
ItemTemplate

用来绑定控件内容,如下

    <Window.Resources>
        <DataTemplate x:Key="comTemplate">
            <StackPanel Orientation="Horizontal" Margin="5,0">
                <Border Width="10" Height="10" Background="{Binding Code}"/>
                <TextBlock Text="{Binding Code}" Margin="5,0"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
            <ComboBox Name="cob" Width="120" Height="30" ItemTemplate="{StaticResource comTemplate}"/>
            <ListBox Name="lib" Width="120" Height="100" Margin="5,0"  ItemTemplate="{StaticResource comTemplate}"/>
        </StackPanel>
    </Grid>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

绑定数据

public class Color
{
    public string? Code { get; set; }
}

List<Color> ColorList = new List<Color>();
ColorList.Add(new Color() { Code = "#FF8C00" });
ColorList.Add(new Color() { Code = "#FF7F50" });
ColorList.Add(new Color() { Code = "#FF6EB4" });
ColorList.Add(new Color() { Code = "#FF4500" });
ColorList.Add(new Color() { Code = "#FF3030" });
ColorList.Add(new Color() { Code = "#CD5B45" });

cob.ItemsSource = ColorList;
lib.ItemsSource = ColorList;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
ContentTemplate

用得很少,不提了

绑定

前端显示后后端数据的关联

绑定控件

通过给控件取名,然后根据名字将text的值绑定为上面slider控件的value

<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
    <Slider Name="slider" Width="200" />
    <TextBlock Text="{Binding ElementName=slider, Path=Value}" HorizontalAlignment="Center" />
</StackPanel>
  • 1
  • 2
  • 3
  • 4

绑定模式

如下进行设置

<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
    <Slider Name="slider" Width="200" />
    <TextBox Width="200" Text="{Binding ElementName=slider, Path=Value, Mode=OneWay}" HorizontalAlignment="Center" />
</StackPanel>
  • 1
  • 2
  • 3
  • 4

常用模式

OneWay : 单向绑定,绑定对象会决定自己
TwoWay : 双向绑定,下面输入后tab即可显示
OneTime : 只绑定第一次的值,这里直接绑定启动的值了
OneWayToSource : 与OneTime一样,对象相反
Default : 默认单项或双向
  • 1
  • 2
  • 3
  • 4
  • 5

绑定数据

Source

<Window.Resources>
    <TextBox x:Key="txt1">ABC</TextBox>
</Window.Resources>
<Grid>
    <TextBox Text="{Binding Source={StaticResource txt1}, Path=Text}" />
</Grid>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

RelativeSource

查看相关标签的数据

<StackPanel Width="20">
    <StackPanel Width="60">
        <TextBlock Text="{Binding Path=Width,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type StackPanel}}}" />
    </StackPanel>
</StackPanel>
  • 1
  • 2
  • 3
  • 4
  • 5

DataContext

与代码数据绑定

<Grid>
    <DataGrid Grid.Row="1" ItemsSource="{Binding Students}" AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding Name}" Header="名称" />
            <DataGridTextColumn Binding="{Binding Age}" Header="年龄" />
            <DataGridTextColumn Binding="{Binding Sex}" Header="性别" />
        </DataGrid.Columns>
    </DataGrid>
</Grid>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

绑定

 public partial class MainWindow : Window
 {
     public MainWindow()
     {
         InitializeComponent();
         PageModel page = new PageModel();
         page.Students = new List<Student>();
         page.Students.Add(new Student() { Name = "张三", Age = "18", Sex = "男" });
         page.Students.Add(new Student() { Name = "李四", Age = "19", Sex = "男" });
         page.Students.Add(new Student() { Name = "王二", Age = "20", Sex = "女" });
         this.DataContext = page;
     }
 }

 public class PageModel
 {
     public List<Student> ?Students { get; set; }
 }

 public class Student
 {
     public string? Name { get; set; }
     public string? Age { get; set; }
     public string? Sex { get; set; }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

MVVM

在开发大型项目时收益高

我们的页面设计xaml和我们的数据分开,利用框架,可以二者完全分离

在MainWindow.xaml中设置界面和绑定(数据,函数(函数参数))

在MainViewModel.cs中来实现数据的处理和提供函数接口,(替换原生在MainWindow.xaml.cs里来设置响应)

让前后端的工作分开,专心处理一种逻辑

有很多MVVM框架,这里使用prism

实战

把页面划分为区域,用区域便于设置

安装框架 : 项目->管理NuGet程序包->Prism.DryIoc(Prism Dry Inversion of Control)

NuGet(发音为"New Get")是一个用于.NET平台的包管理系统

使用Prism

对App.xaml进行操作

cs

修改继承并重写方法

public partial class App : PrismApplication
{
    protected override Window CreateShell()
    {//初始页
        return Container.Resolve<MainWindow>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {//用于注册要实现的页面

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

xaml

添加命名空间prism并删除StartupUri

<prism:PrismApplication x:Class="WPF.App" 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WPF"
             xmlns:prism ="http://prismlibrary.com/">
    <Application.Resources>

    </Application.Resources>
</prism:PrismApplication>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

项目模板

扩展->管理扩展->Prism Template Pack->然后新建该模板的项目,就会自动配置好上面使用框架的内容

框架自动生成两个文件夹 :

Views用来存放我们的界面

ViewModels来存放我们的指令

只要放在这两个文件夹下就会自动关联,不需要麻烦的指定

然后就是了解region,在框架中,界面通过region来控制.

MainWindow.xaml代码中定义region

<ContentControl prism:RegionManager.RegionName="ContentRegion" Grid.ColumnSpan="2" />
  • 1

如果需要显示,在MainWindowViewModel.cs中注册区域,

下面将显示ViewA这个界面

public class MainWindowViewModel : BindableBase
{

    private IRegionManager regionManager;
    public MainWindowViewModel(IRegionManager regionManager)
    {
        this.regionManager = regionManager;
        regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

模块

创建 -> 模块

模块的ModuleAModule.cs文件

public class ModuleAModule : IModule
{
    public void OnInitialized(IContainerProvider containerProvider)
    {
        var regionManager = containerProvider.Resolve<IRegionManager>();
        regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

然后主程序的App

 public partial class App : PrismApplication
 {
     protected override Window CreateShell()
     {
         return Container.Resolve<MainWindow>();
     }

     protected override void RegisterTypes(IContainerRegistry containerRegistry)
     {

     }

     protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
     {
         moduleCatalog.AddModule<ModuleA.ModuleAModule>();
     }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

MAUI

需要下载.NET Multi_platform App UI开发

新建->.NET MAUI->.net6.0->运行

因为跨平台,没办法像WPF那样实时渲染,得自己运行来看效果,不过确实方便

因为跨平台,开发成本低,运行成本高

示例程序计算器源码

项目结构

  1. App是MAUI应用的入口点,它们定义了一个App类,继承自Application类;App.xaml是用
    XAML语言声明App类的属性和资源,如主题颜色,字体大小等;App.xaml.cs是用C#语言
    编写App类的逻辑,如初始化应用,设置主窗口,处理生命周期事件等。
  2. AppShell是MAUI应用的导航框架,它们定义了一个AppShell类,继承自Shell类;
    AppShell.xaml是用XAML语言声明AppShell类的内容和结构,如导航菜单,标签页,飞出
    页等;AppShell.xaml.cs是用C#语言编写AppShell类的逻辑,如注册路由,处理导航事件
    等。
  3. MainPage是MAUI应用的主页面,它们定义了一个MainPage类,继承自ContentPage类;
    MainPage.xaml是用XAML语言声明MainPage类的用户界面,如控件,布局,样式等;
    MainPage.xaml.cs是用C#语言编写MainPage类的逻辑,如事件处理,数据绑定,页面生命
    周期等。
  4. MauiProgram.cs是应用的跨平台入口点,它用来配置和启动应用。模板启动代码指向了
    App.xaml文件中定义的App类。

安卓环境

用虚拟机或真机

  1. 模拟器是一种虚拟设备,它可以在你的开发机上模拟不同平台的设备,如Android手机或iOS手机。配置方式请参考:管理虚拟设备
  2. 真机是一种实体设备,它可以通过USB或无线连接到你的开发机上,如Android手机或iOS手机。配置方式请参考:设置设备进行开发

安全

对所有输入一定要谨慎,(正则匹配),防止胡乱的输入让程序崩溃,即提高鲁棒性

如在计算器中输入1***************1*

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/700081
推荐阅读
相关标签
  

闽ICP备14008679号