文章目录
这个小项目,真的说来话长…
随机数生成依靠
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.
If you want to ignore more than one number, use ' ' or '~' to split.
For example:
'1~3 5' has the same meaning as '1 2 3 5'.
'-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.
If you want to ignore more than one number, use ' ' or '~' to split.
For example:
'1~3 5' has the same meaning as '1 2 3 5'.
'-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
类来实现线形插值
的动画,应用在控件的ScaleX
和ScaleY
属性,放在Ani.cs
中。
缩放动画使用了缓动函数
(PowerEase
-EasingMode.EaseInOut
,图像如下图,纵轴为f(t)
,横轴为t
),使动画过渡更自然,不显得突兀。
使用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)={(t−m)p+n−(t−a)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−(t−a)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
!