WPF学习笔记01-一个简单的计算器

<0x00> 我为何选择WPF

(@ 23-08-25)
慢慢学了一个月,总算知道为啥现在用WPF的个人项目不多了
写简单的UI确实非常方便,但如果要用些现代些的设计就很烦了
非常容易出现些神秘问题

这个我也说不明白,大概就是因为之前看到有些项目的窗口就是WPF写的吧

一定要找个理由就是WPF的支持丰富
作为一个老UI框架,教程满天飞,更重要的是,用WPF仍可以设计出现代的UI
(而且相比那些electron框架来说性能更好,只要不搞跨平台)

目前在Windows下,微软这常见的有四套UI框架
分别是:WinFromWPFWinUI3XAUI
WinFrom实在是老,做一些效果动画会比较费力
WPF虽然也挺老的,但支持的特性足够做些不错的UI了
WinUI3新是新,但教程不多,文档支持也比较神秘
XAUI能跨平台,但微软特色,永不推广,而且也比较新,文档比较神秘

WPF开始,微软的UI框架都是要写.xaml
所以学会WPF之后,迁移到微软更新的UI框架的学习成本是相对低的

目前我体验下来WPF算是很好入门的了,只需要会一门.net框架下的语言就可以入门
xaml语法也不难,可以说看多了也会了
而且微软的看家本领,UI的可视化开发(Visual Studio),这对效率提升真的很有帮助

这篇文章的代码来自我自己随便搞的简易计算器
(整个程序就花了一个小时写完,足见WPF是多方便)

<0x01> WPF的典型结构

我这里创建的是基于C#WPF项目

创建完我们可以看到如图的项目结构
差不多就是一个.xaml文件下套.cs文件

其中在App.xaml文件中,定义了这个应用的基本信息(主要就是启动窗口是哪一个)
MainWindows.xaml定义了窗口的样式(这个的编辑界面就是一半实时预览窗口,一半是代码编辑器)
下面的.cs文件可以说描述程序的行为

简单来说就是.xaml文件管前端(界面),.cs文件管后端(行为)
具体的东西后面再讲

<0x02> XAML的简单语法

xaml这玩意就是参考xml语法的
如果能看懂xml甚至是html也能看懂xaml

<!--MainWindow.xaml-->
<Window x:Class="WPFTest.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:WPFTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="800" Width="800">
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="5"/>
            <Setter Property="FontSize" Value="30"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <TextBlock Name="Answer" Text="0" FontSize="30" HorizontalAlignment="Right" Margin="20"/>
        </Grid>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Button Name="Button_MinusSign" Click="Button_MinusSign_Click"
                    Content="±" Grid.Column="0" Grid.Row="0"/>
            <Button Name="Button_Clear" Click="Button_Clear_Click"
                    Content="CE" Grid.Column="1" Grid.Row="0"/>
            <Button Name="Button_Backspace" Click="Button_Backspace_Click"
                    Content="←" Grid.Column="2" Grid.Row="0"/>
            <Button Name="Button_Num9" Click="Button_Num9_Click"
                    Content="9" Grid.Column="2" Grid.Row="1"/>
            <Button Name="Button_Num8" Click="Button_Num8_Click"
                    Content="8" Grid.Column="1" Grid.Row="1"/>
            <Button Name="Button_Num7" Click="Button_Num7_Click"
                    Content="7" Grid.Column="0" Grid.Row="1"/>
            <Button Name="Button_Num6" Click="Button_Num6_Click"
                    Content="6" Grid.Column="2" Grid.Row="2"/>
            <Button Name="Button_Num5" Click="Button_Num5_Click"
                    Content="5" Grid.Column="1" Grid.Row="2"/>
            <Button Name="Button_Num4" Click="Button_Num4_Click"
                    Content="4" Grid.Column="0" Grid.Row="2"/>
            <Button Name="Button_Num3" Click="Button_Num3_Click"
                    Content="3" Grid.Column="2" Grid.Row="3"/>
            <Button Name="Button_Num2" Click="Button_Num2_Click"
                    Content="2" Grid.Column="1" Grid.Row="3"/>
            <Button Name="Button_Num1" Click="Button_Num1_Click"
                    Content="1" Grid.Column="0" Grid.Row="3"/>
            <Button Name="Button_Num0" Click="Button_Num0_Click"
                    Content="0" Grid.ColumnSpan="2" Grid.Column="0" Grid.Row="4"/>
            <Button Name="Button_Dot" Click="Button_Dot_Click"
                    Content="." Grid.Column="2" Grid.Row="4"/>
            <Button Name="Button_Div" Click="Button_Div_Click"
                    Content="/" Grid.Column="3" Grid.Row="0"/>
            <Button Name="Button_Mul" Click="Button_Mul_Click"
                    Content="*" Grid.Column="3" Grid.Row="1"/>
            <Button Name="Button_Sub" Click="Button_Sub_Click"
                    Content="-" Grid.Column="3" Grid.Row="2"/>
            <Button Name="Button_Add" Click="Button_Add_Click"
                    Content="+" Grid.Column="3" Grid.Row="3"/>
            <Button Name="Button_Equal" Click="Button_Equal_Click"
                    Content="=" Grid.Column="3" Grid.Row="4"/>
        </Grid>
    </Grid>
</Window>

(为了代码高亮正常工作,这里的代码设置为xml的高亮格式)
一块块掰碎讲

声明部分

就是最上面一坨不知道什么东西的部分,随便写写的话根本不需要知道这些是啥,别动就好
(因为我目前也不是很懂这些)

<Window.Resources>

这里面我就定义了个Button类型的Style
其实就是规定了Button的一些默认参数
在下面写Button的时候就不用一个个都写这些东西了

Style里面定义了按钮到界面边界的距离(Margin)有5单位
字体大小(FontSize)为30个单位
(在WPF里,并不是以像素为单位,而是按DPI来确定实际像素单位大小)

<Style>里面要指定设置的类型(TargetType)
<Style>块中,用<Setter>标签来设置类型下面的属性
<Setter>标签里面Property选择属性,Value选择值

<Grid>

WPF中,有两种常用的界面布局
分别是:网格布局(<Grid>)和栈布局(<StackPanel>)
<Grid>可以把一块区域分割成一个网格,网格里面可以塞控件
<StackPanel>就是个栈,所有控件从上到下(从左到右)排列
基本上用这两种布局的互相嵌套能解决绝大多数的布局
我这里的布局选择了<Grid>里面再套一个<Grid>

具体实现

<Grid.RowDefinitions>块里面塞两个<RowDefinition/>把Grid分成两行
(塞几个<RowDefinition/>就是有几行,列同理)
在一个<RowDefinition/>里加入Height="auto"表示第一行的高度随内容改变
(就是跟着第一行的控件高度走)

然后正式往Grid网格里面塞控件,先第一行
首先是再塞了个<Grid>,然后在里面塞了个<TextBlock>
其中,Grid.Row可以指定控件所在的网格行号,列同理
并定义了一系列的属性参数
(Name属性就是给.cs调用的对象名,HorizontalAlignment是水平对正方式,设置为右对齐)
(不是很懂我当时写的逻辑,其实不用多写个<Grid>,直接塞<TextBlock>就好)

第二行,塞了个<Grid>,并定义出5*4的网格
下面就是一大堆的按键定义,相对比较无趣
值得说道的是Click属性,这个会指向对应.cs的一个具体的方法名,实现前后端的融合
(具体看后端实现就可以了)
还有按钮0多了个Grid.ColumnSpan="2",这个就是让这个按钮可以跨两列显示
由于有之前的<Style>的定义,所有的按钮都自带字体大小30单位,间距5单位的属性

这样,我们成功整出了一个简单的计算器界面
长这样的

<0x03> 与C#后端的结合

(@ 23-08-14)
实际上这样并不是搞后端,这些都是再定义前端的行为
具体搞后端可以看我之后写的讲MVVM设计模型的博客
但如果项目很小的话这样搞也没问题的

与C#后端的结合可以说非常简单,就是调用和更改控件对象的成员值就可以
还记得我们已经在.xaml中为一些控件添加了Name属性吗
这些Name属性是实现前后端融合的关键

//等于号按键点击事件的实现逻辑
private void Button_Equal_Click(object sender, RoutedEventArgs e)
{
    if (flag && ans != null)
    {
        Calculate();
        flag = false;
        Answer.Text = ans.ToString();
        calculate = null;
    }
}

别的都不重要,还记得我们有一个<TextBlock>Name属性设置为Answer
通过Answer.Text可以直接访问并修改<TextBlock>的内容,非常方便
(Answer.Text就是String类型)
其他的控件也同理

接下来要处理下按钮的点击事件了
这个也非常简单,我们之前已经设置了每个按钮的Click属性
接下来就是在对应的.cs中实现逻辑就好

//大概的结构
namespace WPFTest
{
    public partial class MainWindow : Window
    {
        //...
        private void Button_MinusSign_Click(object sender, RoutedEventArgs e);
        //加负号按钮的点击事件
        private void Button_Clear_Click(object sender, RoutedEventArgs e);
        //清屏键的点击事件
        private void Button_Backspace_Click(object sender, RoutedEventArgs e);
        //退格键的点击事件
        private void Button_Num9_Click(object sender, RoutedEventArgs e);
        private void Button_Num8_Click(object sender, RoutedEventArgs e);
        private void Button_Num7_Click(object sender, RoutedEventArgs e);
        private void Button_Num6_Click(object sender, RoutedEventArgs e);
        private void Button_Num5_Click(object sender, RoutedEventArgs e);
        private void Button_Num4_Click(object sender, RoutedEventArgs e);
        private void Button_Num3_Click(object sender, RoutedEventArgs e);
        private void Button_Num2_Click(object sender, RoutedEventArgs e);
        private void Button_Num1_Click(object sender, RoutedEventArgs e);
        private void Button_Num0_Click(object sender, RoutedEventArgs e);
        //各个数字键的点击事件
        private void Button_Dot_Click(object sender, RoutedEventArgs e);
        //小数点按键的点击事件
        private void Button_Div_Click(object sender, RoutedEventArgs e);
        private void Button_Mul_Click(object sender, RoutedEventArgs e);
        private void Button_Sub_Click(object sender, RoutedEventArgs e);
        private void Button_Add_Click(object sender, RoutedEventArgs e);
        //+-*/键的点击事件
        private void Button_Equal_Click(object sender, RoutedEventArgs e);
        //等于号的点击事件
    }
}

每个按钮的点击事件都相当于在C#中的一个方法
点击一次就触发一次

附录

完整的MainWindow.xaml已经贴在上文了,这里就不再放了

//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;

namespace WPFTest
{
    public partial class MainWindow : Window
    {
        bool flag = false;
        int? calculate = null;
        double? ans = null;
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Calculate()
        {
            switch (calculate)
            {
                case 1:
                    ans += Double.Parse(Answer.Text);
                    break;
                case 2:
                    ans -= Double.Parse(Answer.Text);
                    break;
                case 3:
                    ans *= Double.Parse(Answer.Text);
                    break;
                case 4:
                    ans /= Double.Parse(Answer.Text);
                    break;
            }
        }

        private void Button_MinusSign_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                if (Answer.Text[0]=='-')
                {
                    Answer.Text = Answer.Text.Substring(1, Answer.Text.Length - 1);
                }
                else
                {
                    Answer.Text = "-" + Answer.Text;
                }
            }
        }

        private void Button_Clear_Click(object sender, RoutedEventArgs e)
        {
            flag = false;
            calculate = null;
            ans = null;
            Answer.Text = "0";
        }

        private void Button_Backspace_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text = Answer.Text.Substring(0, Answer.Text.Length - 1);
                if (Answer.Text.Length == 0)
                {
                    flag = false;
                    Answer.Text = "0";
                }
            }
        }

        private void Button_Num9_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "9";
            }
            else
            {
                flag = true;
                Answer.Text = "9";
            }
        }

        private void Button_Num8_Click(object sender, RoutedEventArgs e)
        {

            if (flag)
            {
                Answer.Text += "8";
            }
            else
            {
                flag = true;
                Answer.Text = "8";
            }
        }

        private void Button_Num7_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "7";
            }
            else
            {
                flag = true;
                Answer.Text = "7";
            }
        }

        private void Button_Num6_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "6";
            }
            else
            {
                flag = true;
                Answer.Text = "6";
            }
        }

        private void Button_Num5_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "5";
            }
            else
            {
                flag = true;
                Answer.Text = "5";
            }
        }

        private void Button_Num4_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "4";
            }
            else
            {
                flag = true;
                Answer.Text = "4";
            }
        }

        private void Button_Num3_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "3";
            }
            else
            {
                flag = true;
                Answer.Text = "3";
            }
        }

        private void Button_Num2_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "2";
            }
            else
            {
                flag = true;
                Answer.Text = "2";
            }
        }

        private void Button_Num1_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "1";
            }
            else
            {
                flag = true;
                Answer.Text = "1";
            }
        }

        private void Button_Num0_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += "0";
            }
            else
            {
                flag = true;
                Answer.Text = "0";
            }
        }

        private void Button_Dot_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                Answer.Text += ".";
            }
        }

        private void Button_Div_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                if (ans == null)
                {
                    ans = Double.Parse(Answer.Text);
                    calculate = 4;
                    flag = false;
                }
                else
                {
                    Calculate();
                    flag = false;
                    Answer.Text = ans.ToString();
                }
            }
            else
            {
                calculate = 4;
            }
        }

        private void Button_Mul_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                if (ans == null)
                {
                    ans = Double.Parse(Answer.Text);
                    calculate = 3;
                    flag = false;
                }
                else
                {
                    Calculate();
                    flag = false;
                    Answer.Text = ans.ToString();
                }
            }
            else
            {
                calculate = 3;
            }
        }

        private void Button_Sub_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                if (ans == null)
                {
                    ans = Double.Parse(Answer.Text);
                    calculate = 2;
                    flag = false;
                }
                else
                {
                    Calculate();
                    flag = false;
                    Answer.Text = ans.ToString();
                }
            }
            else
            {
                calculate = 2;
            }
        }

        private void Button_Add_Click(object sender, RoutedEventArgs e)
        {
            if (flag)
            {
                if (ans == null)
                {
                    ans = Double.Parse(Answer.Text);
                    calculate = 1;
                    flag = false;
                }
                else
                {
                    Calculate();
                    flag = false;
                    Answer.Text = ans.ToString();
                }
            }
            else
            {
                calculate = 1;
            }
        }

        private void Button_Equal_Click(object sender, RoutedEventArgs e)
        {
            if (flag && ans != null)
            {
                Calculate();
                flag = false;
                Answer.Text = ans.ToString();
                calculate = null;
            }
        }
    }
}

逻辑就是随性写的,不保证完全没Bug,应该是没严重Bug的
(所以也没写高精度的运算处理)
(写这篇博客比我写代码的时间都长😂)

Licensed under CC BY-NC-SA 4.0