用 C# WPF 做了个抽号小程序


这个小项目,真的说来话长…
随机数生成依靠 Random类和 Next(min, max)函数,这样最简单…
本来只打算做两个文本框,一个供用户输入 最大值,另一个是 最小值。可是,项目越做功能越来越多,代码量也从十几行变成上千行…
先放上Github仓库: Lottery
软件开源许可证为Apache 2.0,使用代码时请注意。
下面,就让我们开始吧!

界面

主界面 (MainWindow)

主界面

自定义弹窗 (MyMessageBox)

自定义弹窗
界面分为两个部分,主界面MainWindow和自定义弹窗MyMessageBox
说明一下,为了Button控件的动画,我在App.xaml中重写了Button的模板(如果不重写,在背景色改变时无法正常显示设定的颜色,而显示Button默认的对应状态时的背景色),所以每个引用都加上了Style="{DynamicResource ButtonStyle}"
废话不多说,上代码!

App.xaml

<Application x:Class="Lottery.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
        <Style x:Key="FocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="ButtonStyle" TargetType="{x:Type Button}">
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Border x:Name="border" CornerRadius="8" Background="{TemplateBinding Background}">
                            <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>
</Application>

Border中添加CornerRadius属性可以为控件添加圆角。(Line 23

MainWindow.xaml

使用Grid做了布局,在行高方面使用按比例分配,同时设定窗体最小尺寸,防止用户拖拽缩放窗口时窗口布局出现意外情况。

<Window x:Class="Lottery.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"
        mc:Ignorable="d"
        Title="Lottery" Height="297" Width="360" MinWidth="230" MinHeight="297">
    <Window.Resources>
        <Style TargetType="TextBox">
            <Setter Property="FontSize" Value="17"/>
            <Setter Property="Height" Value="24"/>
            <Setter Property="Margin" Value="3"/>
            <Setter Property="Grid.Column" Value="1"/>
        </Style>
        <Style TargetType="Label">
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="Height" Value="30"/>
            <Setter Property="Margin" Value="3"/>
            <Setter Property="Grid.Column" Value="0"/>
        </Style>
    </Window.Resources>
    <Grid Margin="3">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="91"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Label Content="Minimum:" Grid.Row="0"/>
        <TextBox Name="mint" Grid.Row="0"/>
        <Label Content="Maximum:" Grid.Row="1"/>
        <TextBox Name="maxt" Grid.Row="1"/>
        <Label Content="Ignore:" Grid.Row="2" ToolTip="Enter the numbers you want to ignore here.&#x0a;If you want to ignore more than one number, use ' ' or '~' to split.&#x0a;For example:&#x0a;'1~3 5' has the same meaning as '1 2 3 5'.&#x0a;'-1~3 5 7' has the same meaning as '-1 0 1 2 3 5 7'."/>
        <TextBox Name="ignt" Grid.Row="2" ToolTip="Enter the numbers you want to ignore here.&#x0a;If you want to ignore more than one number, use ' ' or '~' to split.&#x0a;For example:&#x0a;'1~3 5' has the same meaning as '1 2 3 5'.&#x0a;'-1~3 5 7' has the same meaning as '-1 0 1 2 3 5 7'."/>
        <Label Content="Quatity:" Grid.Row="3"/>
        <TextBox Name="quat" Grid.Row="3"/>
        <CheckBox Name="c" Content="No Duplication" Height="30" FontSize="15" Margin="3" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="4" Grid.ColumnSpan="2"/>
        <Button Style="{DynamicResource ButtonStyle}" Content="Generate" Height="30" Name="genb" Grid.Row="5" Grid.ColumnSpan="2" Margin="3"/>
        <Button Style="{DynamicResource ButtonStyle}" Content="Source Code Repository" Height="30" Name="scrb" Grid.Row="6" Grid.ColumnSpan="2" Margin="3" ToolTip="Don't forget to give us a star if you like it."/>
    </Grid>
</Window>

MyMessageBox.xaml

<Window x:Class="Lottery.MyMessageBox"
        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"
        mc:Ignorable="d"
        Height="260" Width="300" MinHeight="200" MinWidth="250" WindowStartupLocation="CenterOwner">
    <Grid Margin="3" Name="g">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="27"/>
            <RowDefinition Height="36"/>
            <RowDefinition Height="36"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="0" Margin="3" Grid.ColumnSpan="2">
            <TextBlock Name="t" FontSize="17" TextWrapping="Wrap"/>
        </ScrollViewer>
        <ComboBox Name="cb" IsReadOnly="True" Grid.Row="1" Margin="3" Grid.Column="1" ToolTip="Change the font size of the text above."/>
        <Label Content="Font Size:" FontSize="15" Grid.Row="1" Grid.Column="0" ToolTip="Change the font size of the text above."/>
        <Button Style="{DynamicResource ButtonStyle}" Content="Copy" Name="c" Grid.Row="2" Grid.Column="0" Margin="3"/>
        <Button Style="{DynamicResource ButtonStyle}" Content="Close" Name="b" Grid.Row="2" Grid.Column="1" Margin="3"/>
        <Button Style="{DynamicResource ButtonStyle}" Content="Save" Name="sb" Grid.Row="3" Margin="3" Grid.ColumnSpan="2"/>
    </Grid>
</Window>

这里使用ScrollViewer来显示TextBlock的用意方便是在输出的数据量较大时展示所有的内容。此外,使用Style节省了一些代码…

后台

动画效果展示

动画效果展示

Ani.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace Lottery
{
    internal class Ani
    {
        /// <summary>
        /// Apply a scaling animation to the specified <see cref="UIElement"/>, animating its size from one value to another.
        /// </summary>
        /// <param name="element">The UI element to which the scaling animation will be applied.</param>
        /// <param name="sizeFrom">The starting size of the element.</param>
        /// <param name="sizeTo">The target size to which the element will be scaled.</param>
        /// <param name="renderX">The X-coordinate of the center of the scaling transformation (default is 0.5).</param>
        /// <param name="renderY">The Y-coordinate of the center of the scaling transformation (default is 0.5).</param>
        /// <param name="power">The strength of the transition animation (default is 5).</param>
        public static void ScaleAniShow(UIElement element, double sizeFrom, double sizeTo, double renderX = 0.5, double renderY = 0.5, int power = 5)
        {
            ScaleTransform scale = new ScaleTransform();
            element.RenderTransform = scale;  // Define the central position of the circle.
            element.RenderTransformOrigin = new Point(renderX, renderY);  // Define the transition animation, 'power' is the strength of the transition.
            DoubleAnimation scaleAnimation = new DoubleAnimation()
            {
                From = sizeFrom,  // Start value
                To = sizeTo,  // End value
                FillBehavior = FillBehavior.HoldEnd,
                Duration = TimeSpan.FromMilliseconds(250),  // Animation playback time
                EasingFunction = new PowerEase()  // Ease function
                {
                    EasingMode = EasingMode.EaseInOut,
                    Power = power
                }
            };
            scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation);
            scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation);
        }

        public static void ButtonBind(Button b, Brush start, Brush mid, Brush end)
        {
            b.Background = mid;
            b.Foreground = Brushes.White;
            b.MouseEnter += (s, e) => { ScaleAniShow(b, 1, 1.05); b.Background = start; };
            b.MouseLeave += (s, e) => { ScaleAniShow(b, 1.05, 1); b.Background = mid; };
            b.PreviewMouseDown += (s, e) => { ScaleAniShow(b, 1.05, 0.95); b.Background = end; };
            b.PreviewMouseUp += (s, e) => { ScaleAniShow(b, 0.95, 1.05); b.Background = start; };
        }

        public static void TextBoxBind(TextBox t)
        {
            t.PreviewMouseDown += (s, e) => { ScaleAniShow(t, 1, 0.95); };
            t.PreviewMouseUp += (s, e) => { ScaleAniShow(t, 0.95, 1); };
        }
    }
}

封装了些控件的缩放动画函数,使用了DoubleAnimaion类来实现线形插值的动画,应用在控件的ScaleXScaleY属性,放在Ani.cs中。
缩放动画使用了缓动函数PowerEase-EasingMode.EaseInOut,图像如下图,纵轴为f(t),横轴为t),使动画过渡更自然,不显得突兀。
PowerEase-EasingMode.EaseInOut
使用PowerEase,也就是以幂函数为原型的缓动函数,即
f ( t ) = { ( t − m ) p + n − ( t − a ) p + b f(t)= \begin{cases} (t-m)^p+n \\ -(t-a)^p+b \\ \end{cases} f(t)={(tm)p+n(ta)p+b
(函数不同会导致含幂项系数的正负出现差异,也会有图像的平移,甚至像上图曲线一样将两者合并,参数p为指定的power,在本程序中为5),也就是说本程序中缓动函数为
f ( t ) = { t 5 − ( t − a ) 5 + b f(t)= \begin{cases} t^5 \\ -(t-a)^5+b \\ \end{cases} f(t)={t5(ta)5+b
具体实现见Ani.cs中的ScaleAniShow()函数。

因为控件绑定的事件实在太多,所以只能降低代码可维护性上了匿名函数,本着能跑就行的选择,大家就忍着看看吧…

MainWindow.xaml.cs

using System.Windows;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;

namespace Lottery
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Closed += (s, e) => { Environment.Exit(0); };
            Ani.ButtonBind(genb, Brushes.DeepSkyBlue, Brushes.DodgerBlue, Brushes.CornflowerBlue);
            Ani.ButtonBind(scrb, Brushes.DeepSkyBlue, Brushes.DodgerBlue, Brushes.CornflowerBlue);
            c.MouseEnter += (s, e) => { Ani.ScaleAniShow(c, 1, 1.05); };
            c.MouseLeave += (s, e) => { Ani.ScaleAniShow(c, 1.05, 1); };
            c.PreviewMouseDown += (s, e) => { Ani.ScaleAniShow(c, 1.05, 0.95); };
            c.PreviewMouseUp += (s, e) => { Ani.ScaleAniShow(c, 0.95, 1.05); };
            scrb.Click += (s, e) => { System.Diagnostics.Process.Start("https://github.com/Unqualified-Developers/Lottery"); };
            genb.Click += (s, e) => { Gen(); };
            Ani.TextBoxBind(mint);
            Ani.TextBoxBind(maxt);
            Ani.TextBoxBind(ignt);
            Ani.TextBoxBind(quat);
        }

        /// <summary>
        /// Generates a random <see langword="int"/> value within the specified range, excluding the numbers in the given HashSet.
        /// </summary>
        /// <remarks>
        /// This method generates a random <see langword="int"/> value within the range specified by min and max, while ensuring that the generated number is not present in the provided HashSet. <br/>
        /// If a suitable number cannot be found within 10,000,000 iterations, a <see cref="NotImplementedException"/> is thrown.
        /// </remarks>
        /// <param name="min">The minimum value of the range.</param>
        /// <param name="max">The maximum value of the range.</param>
        /// <param name="iset">The HashSet containing the numbers to be excluded.</param>
        /// <param name="r">The Random object used for generating random numbers.</param>
        /// <returns>A random <see langword="int"/> value within the specified range, excluding the numbers in the HashSet.</returns>
        /// <exception cref="NotImplementedException">Thrown when the maximum number of iterations is reached without finding a suitable number.</exception>
        private int Generate(int min, int max, HashSet<int> iset, Random r)
        {
            int i = 0;
            int re;
            do
            {
                re = r.Next(min, max + 1);
                i++;
            }
            while (iset.Contains(re) && i <= 10000000);
            if (i == 10000001) throw new NotImplementedException();
            else return re;
        }

        private void Gen()
        {
            MyMessageBox m = new MyMessageBox();
            Random random = new Random();
            HashSet<int> iset = new HashSet<int>();
            foreach (string str in ignt.Text.Split(' '))
            {
                if (str.Contains('~'))
                {
                    string[] range = str.Split('~');
                    if (int.TryParse(range[0], out int min) && int.TryParse(range[1], out int max))
                    {
                        if (min > max) (min, max) = (max, min);
                        for (int i = min; i <= max; i++) { iset.Add(i); }
                    }
                }
                else if (int.TryParse(str, out int num)) { iset.Add(num); }
            }
            try
            {
                int mini = int.Parse(mint.Text);
                int maxi = int.Parse(maxt.Text);
                if (mini > maxi) (mini, maxi) = (maxi, mini);
                int quai = int.TryParse(quat.Text, out int _quai) ? _quai : 1;
                if (quai < 1 || quai > 99999) m.Display("Range", "The value of 'Quality' you entered is not in the valid range. Valid range: 1~99999.", this, MyMessageBoxStyles.Error);
                else if (quai != 1)
                {
                    int r;
                    int[] rl = new int[quai];
                    bool cc = (bool)c.IsChecked;
                    for (int i = 0; i < quai; i++)
                    {
                        r = Generate(mini, maxi, iset, random);
                        rl[i] = r;
                        if (cc) iset.Add(r);
                    }
                    m.Display("Generate", $"Numbers: {string.Join(", ", rl)}.", this, Gen);
                }
                else m.Display("Generate", $"Number {Generate(mini, maxi, iset, random)}.", this, Gen);
            }
            catch (FormatException) { m.Display("Check", "Please enter correct numbers.", this, MyMessageBoxStyles.Warning); }
            catch (NotImplementedException) { m.Display("Joke", "This is not a joke.", this, MyMessageBoxStyles.Warning); }
            catch (Exception ex) when (ex is OverflowException || ex is ArgumentException) { m.Display("Range", "The value of 'Minimum' or 'Maximum' entered is not in the valid range. Valid range: -2147483648~2147483646. You have better not enter the range '-2147483648~2147483647' because it may go wrong.", this, MyMessageBoxStyles.Error); }
        }
    }
}

使用了三目运算符(Line 55)和哈希集,简化代码并提高执行效率。
三目运算符的使用方法:

(逻辑值或表达式)? (条件为真则执行的语句): (条件为假则执行的语句)

这其实等价于if-else的嵌入语句:

if(逻辑值或表达式)(条件为真则执行的语句)
else(条件为假则执行的语句)

所以,我们经常可以将if-else的嵌入语句优化为三目运算式,这样可以提高代码的简洁性可读性。(毕竟两行可以变成一行…)

可能有细心的读者看到了Generate函数中的10 000 000次尝试机会,这是我曾经遇到的一个问题。为了避免出现一些意外情况,例如输入最小值为1,最大值为1,还在Ignore中输入了1…这样真的无法生成一个满足条件的数,如果不这么做可能在这样的条件下使程序出现未响应的情况。(我承认如果用户运气异常好,是程序每次都生成了不符合要求的数,这种方法有极小的概率误判)
为什么呢?
逻辑运算来实现错误的判断可以减少判错时“不必要”的10 000 000次运算,提高判错效率,不经过微小的卡顿就能判断输入错误。
因为Ignore的输入逻辑太复杂了,用户既可以用 (空格)来分割多个数,又可以输入一个范围(例如1~6),甚至还能将两者组合起来,例如1 2~6 10 9,使判断算法的制定极为困难,所以这算是一个妥协…

MyMessageBox.xaml.cs

这是一个自定义弹窗,用于显示抽号结果。
Continue按钮的作用是使用户在不关闭弹窗时就能生成下一批随机数(相当于可以少点一次鼠标)。

using Microsoft.Win32;
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Lottery
{
    /// <summary>
    /// Interaction logic for MyMessageBox.xaml
    /// </summary>
    public enum MyMessageBoxStyles
    {
        Information,
        Warning,
        Error
    }
    
    public partial class MyMessageBox : Window
    {
        private readonly Button conb = new Button
        {
            Margin = new Thickness(3),
            Content = "Continue",
            Style = (Style)Application.Current.FindResource("ButtonStyle")
        };

        private void Set(Brush start, Brush mid, Brush end)
        {
            Ani.ButtonBind(b, start, mid, end);
            Ani.ButtonBind(c, start, mid, end);
            Ani.ButtonBind(sb, start, mid, end);
        }

        private void SetMore(Brush start, Brush mid, Brush end)
        {
            Set(start, mid, end);
            Ani.ButtonBind(conb, start, mid, end);
        }

        private void Register(string title, string content, Window owner, bool c, MyMessageBoxStyles style)
        {
            switch (style)
            {
                case MyMessageBoxStyles.Information:
                    if (c) SetMore(Brushes.DeepSkyBlue, Brushes.DodgerBlue, Brushes.CornflowerBlue);
                    else Set(Brushes.DeepSkyBlue, Brushes.DodgerBlue, Brushes.CornflowerBlue);
                    break;
                case MyMessageBoxStyles.Warning:
                    if (c) SetMore(Brushes.Orange, Brushes.DarkOrange, Brushes.Coral);
                    else Set(Brushes.Orange, Brushes.DarkOrange, Brushes.Coral);
                    break;
                case MyMessageBoxStyles.Error:
                    if (c) SetMore(new SolidColorBrush(Color.FromRgb(255, 75, 75)), Brushes.Red, Brushes.Crimson);
                    else Set(new SolidColorBrush(Color.FromRgb(255, 75, 75)), Brushes.Red, Brushes.Crimson);
                    break;
            }
            Title = title;
            Owner = owner;
            t.Text = content;
            ShowDialog();
        }

        public MyMessageBox()
        {
            InitializeComponent();
            cb.ItemsSource = new int[] { 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29 };
            cb.SelectedIndex = 4;
            cb.SelectionChanged += (s, e) => { t.FontSize = int.Parse(cb.SelectedItem.ToString()); };
            b.Click += (s, e) => { Close(); };
            c.Click += (s, e) =>
            {
                try { Clipboard.SetText(t.Text); }
                catch
                {
                    MyMessageBox m = new MyMessageBox { Width = 250, Height = 200 };
                    m.c.Visibility = Visibility.Collapsed;
                    m.Display("Copy", "Stop clicking the button 'Copy'!", this, MyMessageBoxStyles.Warning);
                }
            };
            sb.Click += (s, e) =>
            {
                SaveFileDialog dialog = new SaveFileDialog { Title = "Save File", Filter = "Text Files (*.txt)|*.txt" };
                if ((bool)dialog.ShowDialog()) File.WriteAllText(dialog.FileName, t.Text);
            };
        }

        /// <summary>
        /// Display a message box with a "Continue" button that executes the specified action when clicked.
        /// </summary>
        /// <param name="title">The title of the message box.</param>
        /// <param name="content">The content of the message box.</param>
        /// <param name="owner">The owner window of the message box.</param>
        /// <param name="action">The action to be executed when the "Continue" button is clicked.</param>
        /// <param name="style">The style of the message box (Information, Warning, Error).</param>
        public void Display(string title, string content, Window owner, Action action, MyMessageBoxStyles style = MyMessageBoxStyles.Information)
        {
            conb.Click += (s, e) =>
            {
                Close();
                action(); 
            };
            RowDefinition newRow = new RowDefinition { Height = new GridLength(36) };
            g.RowDefinitions.Add(newRow);
            Grid.SetRow(conb, 4);
            Grid.SetColumnSpan(conb, 2);
            g.Children.Add(conb);
            MinHeight = 236;
            Register(title, content, owner, true, style);
        }

        /// <summary>
        /// Display a message box without a "Continue" button.
        /// </summary>
        /// <param name="title">The title of the message box.</param>
        /// <param name="content">The content of the message box.</param>
        /// <param name="owner">The owner window of the message box.</param>
        /// <param name="style">The style of the message box (Information, Warning, Error).</param>
        public void Display(string title, string content, Window owner, MyMessageBoxStyles style = MyMessageBoxStyles.Information) { Register(title, content, owner, false, style); }
    }
}

Display()的两个重载,一个是带Continue按钮的(显示抽号结果),另一个是不带的(显示错误或警告)。

代码可能有些冗余,但还是希望能帮助到大家,也望大家去仓库提Issue

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值