前言 最近突然想写一个一直以来想要的 App , 用于记录每日做的事情,做一个统计,最好能一眼看到一共学了什么多少个小时(可能是之前的 10000 小时情结)之前在安卓端有看到一个类似的 App , 但是主打功能是番茄钟,另外有 VIP。于是打算自力更生写一款。但是这回想试试 Windows PC 端,而且希望 App 能有比较便于使用的图形化界面,所以这回不打算用之前的语言,用 Windows 家的 C# 试试,毕竟自家原生支持肯定会非常好。
但是这就面临了学习一个新语言的问题。回想自己的各个语言都是怎么学习的:
C : 啊哈C语言 ,洛谷,程序设计专业课
Python : 菜鸟教程,Codecademy
Java : Head First Java , Head First Android .
Javascript : MDN , 廖雪峰的教程
最后想了想还是用 Head First C# 来学习 C# .
正文 阅读体验 Head First 系列阅读起来都是比较爽快的,几乎全程手把手实验,还有很多有趣的配图和文字,效果非常好。但是这一本 Head First C# 读起来却遇到了很多问题。
计算机学科的书籍基本都面临数年内更新换代,API 变动的问题。找了很久,发现 Head First C# 的最新版是第三版。但是也已经是 2013 年的书籍了,书中使用的操作系统还是 Windows 8, 使用的平台也不是现在的 VS 2019,问题重重。所幸 VS 的界面变化几乎不大,让大部分的阅读过程还是不算太艰难。
但是很多 API 都发生了改变,包括现在使用 .NET 时甚至还区分 Framework 和 Core , 这样刚刚上手 C# 的程序员很容易束手无策。下面介绍一些我在阅读过程中发现的问题(很多问题百度、谷歌无果)
问题 新建项目 首先在新建项目时就遇到了问题。在配置 VS 2019 时就面临了选择 .NET 平台开发 还是 Windows 通用平台开发的问题。稍作百度后发现,后者是用于 Windows 的跨平台应用的,如用于平板等,由于存储空间吃紧便只安装了 .NET 平台开发 。 注意这里也许安装了 Windows 通用平台开发 可能就不会遇到后面我叙述的一些问题,所以仅供参考。
新建项目时,要选择什么模板呢?
比对后发现已经找不到书上的模板,我们只好推测。因为是一个图形化界面窗口而非控制台界面,也并非服务、库、测试项目等,排除后就剩下了 WPF 应用 和 Windows 窗体应用。我先试了 Windows 窗体应用,但是发现这个模板不会生成 XAML 文件,而按照书中说明会有 XAML 文件。于是又试了 WPF App,我目前的使用来看, Core 和 Framework 之间没有找到显著区别。(百度后的结果说明 Core 应该是一个较新的版本,具体区别还是待 C# 略微入门后在做研究)
最终使用的是 WPF 应用(.NET Framework)
Page 与 Window 新建项目后最明显的与书中不同便是自动生成的是 MainWindow 而非书中所述的 MainPage。百度后得知 page 有点像我们浏览器里的那个 page ,有前进后退等操作,而 Window 是一个更大的对象,可能内部的内部会包含 page。所以书中所有的 Page 换做 Window 应该是没有问题的。
替换 MainPage? 书中会要求删去自动生成的 Page , 导入模板。我在尝试后发现并没有相关的模板。但是这一步我改为了修改 Window 大小。因为书中的 Page 的大小时自动的,但是我自动生成的窗口大小显得很古怪,尤其项目中将grid 的两行都确定了高度,使得中间的游戏区域变得很窄,最后我调整了窗口的大小为 1366 * 768. 相关代码如下。
1 2 3 4 5 6 7 8 <Window x:Class="SaveTheHumans.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:SaveTheHumans" mc:Ignorable="d" Title="MainWindow" Height="768" Width="1366">
失踪的文件大纲图标
书中提到点击这个图标可以打开文件大纲的窗口,但是找了很久没有找到。事实上并不需要找他,默认情况下他就在左侧。如果还是找不到的话可以在视图中寻找。
这个库应该改名了,其实在代码中如果直接使用 Storyboard 会自动提示补全新的库的名称。
改为 using System.Windows.Media.Animation;
就可以了。
找不到 PointerPressed 新版的 VS 和 C# 中似乎没有这个了。在我的代码中全部替换为了 Mouse 相关的事件,并且测试在笔记本平台可以使用。
最终代码 我对代码做了一些修改,原版代码打开后就显示 Game Over 实在太不友好,我改为了中文的介绍以便于扔给亲朋好友测试。同时在屏幕上方加了分数机制。
下面两份代码分别是 MainWindow.xaml 与 MainWindow.xaml.cs。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 <Window x:Class="SaveTheHumans.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:SaveTheHumans" mc:Ignorable="d" Title="MainWindow" Width="1280" Height="720" > <Window.Resources> <ControlTemplate x:Key="EnemyTemplate" TargetType="{x:Type ContentControl}"> <Grid> <Ellipse Height="100" Stroke="Black" Width="75" Fill="Gray"/> <Ellipse Fill="Black" Height="35" Width="25" Margin="40,20,70,0" Stroke="Black" VerticalAlignment="Top" HorizontalAlignment="Center" RenderTransformOrigin="0.5,0.5"> <Ellipse.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform AngleX="10"/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </Ellipse.RenderTransform> </Ellipse> <Ellipse Fill="Black" Height="35" Width="25" Margin="70,20,40,0" Stroke="Black" VerticalAlignment="Top" HorizontalAlignment="Center" RenderTransformOrigin="0.5,0.5"> <Ellipse.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform AngleX="-10"/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </Ellipse.RenderTransform> </Ellipse> </Grid> </ControlTemplate> </Window.Resources> <Grid x:Name="grid" > <Grid.ColumnDefinitions> <ColumnDefinition Width="160"/> <ColumnDefinition/> <ColumnDefinition Width="160"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition /> <RowDefinition Height="160"/> </Grid.RowDefinitions> <Button x:Name="startButton" Content="Start!" HorizontalAlignment="Center" Grid.Row="2" VerticalAlignment="Center" Click="startButton_Click" FontSize="25"/> <StackPanel Grid.Column="2" Orientation="Vertical" Grid.Row="2"> <TextBlock Text="Avoid These" TextWrapping="Wrap" HorizontalAlignment="Center" VerticalAlignment="Center"/> <ContentControl Template="{DynamicResource EnemyTemplate}" Content="ContentControl" HorizontalAlignment="Center" VerticalAlignment="Center"/> </StackPanel> <ProgressBar x:Name="progressBar" Grid.Column="1" Grid.Row="2" Height="20"/> <Canvas x:Name="playArea" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="3" MouseMove="playArea_MouseMove" MouseLeave="playArea_MouseLeave"> <Canvas.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FF4AB4FF"/> <GradientStop Color="#FFE0E0E0" Offset="1"/> </LinearGradientBrush> </Canvas.Background> <StackPanel x:Name="human" Orientation="Vertical" MouseDown="human_MouseDown" TouchDown="human_TouchDown"> <Ellipse Fill="White" Height="10" Width="10"/> <Rectangle Fill="White" Height="25" Width="10"/> </StackPanel> <TextBlock x:Name="gameOverText" Text="Game Over" TextWrapping="Wrap" FontFamily="Arial" FontSize="100" FontWeight="Bold" FontStyle="Italic" Canvas.Left="416" Canvas.Top="177" HorizontalAlignment="Center" VerticalAlignment="Center"/> <Rectangle x:Name="target" Height="50" Width="50" RenderTransformOrigin="0.5,0.5" MouseEnter="target_MouseEnter"> <Rectangle.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform Angle="45"/> <TranslateTransform/> </TransformGroup> </Rectangle.RenderTransform> <Rectangle.Fill> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="Black"/> <GradientStop Color="White" Offset="1"/> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> </Canvas> <StackPanel Grid.Column="1"> <TextBlock Grid.Column="1" HorizontalAlignment="Center" Margin="0" TextWrapping="Wrap" Text="Your score " VerticalAlignment="Center" FontSize="40"/> <TextBlock x:Name="scoreNumber" Grid.Column="1" HorizontalAlignment="Center" Margin="0" TextWrapping="Wrap" Text="100" VerticalAlignment="Center" FontSize="50"/> </StackPanel> </Grid> </Window>
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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.Animation;using System.Windows.Media.Imaging;using System.Windows.Navigation;using System.Windows.Shapes;using System.Windows.Threading;namespace SaveTheHumans { public partial class MainWindow : Window { Random random = new Random(); DispatcherTimer enemyTimer = new DispatcherTimer(); DispatcherTimer targetTimer = new DispatcherTimer(); bool humanCaptured = false ; int score = 0 ; public MainWindow () { InitializeComponent(); enemyTimer.Tick += enemyTimer_Tick; enemyTimer.Interval = TimeSpan.FromSeconds(2 ); targetTimer.Tick += targetTimer_Tick; targetTimer.Interval = TimeSpan.FromSeconds(.1 ); gameOverText.Text = "欢迎! 点击左下角的 start 按钮开始游戏,单击白色小人指引其移动。\n当其成功移动至菱形传送门后即可获得一分。注意当你不控制小人时,\n外星人就无法抓住小人。另外,注意不要让下方的时间条耗尽。" ; gameOverText.FontSize = 20 ; scoreNumber.Text = "0" ; } private void enemyTimer_Tick (object sender, EventArgs e ) { AddEnemy(); } private void targetTimer_Tick (object sender, EventArgs e ) { progressBar.Value += 1 ; if (progressBar.Value >= progressBar.Maximum) EndTheGame(); scoreNumber.Text = score.ToString(); } private void EndTheGame () { if (!playArea.Children.Contains(gameOverText)) { gameOverText.Text = "Game over" ; gameOverText.FontSize = 100 ; enemyTimer.Stop(); targetTimer.Stop(); humanCaptured = false ; startButton.Visibility = Visibility.Visible; playArea.Children.Add(gameOverText); } } private void startButton_Click (object sender, RoutedEventArgs e ) { StartGame(); } private void StartGame () { human.IsHitTestVisible = true ; humanCaptured = false ; progressBar.Value = 0 ; startButton.Visibility = Visibility.Collapsed; playArea.Children.Clear(); playArea.Children.Add(target); playArea.Children.Add(human); enemyTimer.Start(); targetTimer.Start(); score = 0 ; Canvas.SetLeft(target, random.Next(100 , (int )playArea.ActualWidth - 100 )); Canvas.SetTop(target, random.Next(100 , (int )playArea.ActualHeight - 100 )); Canvas.SetLeft(human, random.Next(100 , (int )playArea.ActualWidth - 100 )); Canvas.SetTop(human, random.Next(100 , (int )playArea.ActualHeight - 100 )); } private void AddEnemy () { ContentControl enemy = new ContentControl(); enemy.Template = Resources["EnemyTemplate" ] as ControlTemplate; AnimateEnemy(enemy, 0 , playArea.ActualWidth - 100 , "(Canvas.Left)" ); AnimateEnemy(enemy, random.Next((int )playArea.ActualHeight - 100 ), random.Next((int )playArea.ActualHeight - 100 ), "(Canvas.Top)" ); playArea.Children.Add(enemy); enemy.MouseEnter += Enemy_MouseEnter; } private void Enemy_MouseEnter (object sender, MouseEventArgs e ) { if (humanCaptured) { EndTheGame(); } } private void AnimateEnemy (ContentControl enemy, double from , double to, string propertyToAnimate ) { Storyboard storyboard = new Storyboard() { AutoReverse = true , RepeatBehavior = RepeatBehavior.Forever }; DoubleAnimation animation = new DoubleAnimation() { From = from , To = to, Duration = new Duration(TimeSpan.FromSeconds(random.Next(4 , 6 ))) }; Storyboard.SetTarget(animation, enemy); Storyboard.SetTargetProperty(animation, new PropertyPath(propertyToAnimate)); storyboard.Children.Add(animation); storyboard.Begin(); } private void human_MouseDown (object sender, MouseButtonEventArgs e ) { if (enemyTimer.IsEnabled) { humanCaptured = true ; human.IsHitTestVisible = false ; } } private void human_TouchDown (object sender, TouchEventArgs e ) { if (enemyTimer.IsEnabled) { humanCaptured = true ; human.IsHitTestVisible = false ; } } private void target_MouseEnter (object sender, MouseEventArgs e ) { if (targetTimer.IsEnabled && humanCaptured) { progressBar.Value = 0 ; Canvas.SetLeft(target, random.Next(100 , (int )playArea.ActualWidth - 100 )); Canvas.SetTop(target, random.Next(100 , (int )playArea.ActualHeight - 100 )); Canvas.SetLeft(human, random.Next(100 , (int )playArea.ActualWidth - 100 )); Canvas.SetTop(human, random.Next(100 , (int )playArea.ActualHeight - 100 )); humanCaptured = false ; human.IsHitTestVisible = true ; score++; } } private void playArea_MouseMove (object sender, MouseEventArgs e ) { if (humanCaptured) { Point mousePosition = e.GetPosition(null ); Point relativePosition = grid.TransformToVisual(playArea).Transform(mousePosition); if ((Math.Abs(relativePosition.X - Canvas.GetLeft(human))>human.ActualWidth * 3 ) || (Math.Abs(relativePosition.Y - Canvas.GetTop(human)) > human.ActualHeight * 3 )) { humanCaptured = false ; human.IsHitTestVisible = true ; } else { Canvas.SetLeft(human, relativePosition.X - human.ActualWidth / 2 ); Canvas.SetTop(human, relativePosition.Y - human.ActualHeight / 2 ); } } } private void playArea_MouseLeave (object sender, MouseEventArgs e ) { if (humanCaptured) { EndTheGame(); } } } }