爬取B站弹幕

爬取B站弹幕和发送者

获取弹幕文件


首先随便打开一个视频页面,我这里选取了av810872,想必大多数人对这个视频也都很熟悉,毕竟也是镇站之宝之一,分析源码
网页源码
很明显弹幕文件是不可能在这里面的,猜测是向服务器请求数据得到的json或者xml文件,F12->Network选择过滤XHR请求,一条一条的查看,可以发现url为https://api.bilibili.com/x/v1/dm/list.so?oid=1176840&type=1的请求获取的是弹幕的xml文件
2
接下来查看请求头,发现请求头里面包含了cookie,但是获取cookie还要登陆,仅仅为了一个弹幕文件这么大动干戈未免有点事倍功半,于是尝试去掉cookie发现依然能正常的请求到文件,接下来分析请求参数

https://api.bilibili.com/x/v1/dm/list.so?oid=1176840&type=1

发现参数type实际上不影响结果,所以可以排除掉,还剩下一个参数oid,根据以往的经验猜测是由aid(也就是810872)或者cid构成的参数,复制1176840在源码里面查找,发现对应的是该视频的cid
3
由于这里的代码都是静态加载的,因此可以直接爬取源码后正则解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static string GetDanmakuXml(string aid)
{
var header = new Dictionary<string, string>
{
{
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
}
};
var cid = SendRequest($"https://www.bilibili.com/video/av{aid}",header)
.DecompressGzipBytes()
.GetString(Encoding.UTF8)
.PatternFirst("\"pages\":\\[\\{\"cid\":(\\d+)", 1);
return SendRequest(Trend(cid),header).DecompressDeflateBytes().GetString(Encoding.UTF8);

}


> 注: 使用C#的 WebClient Api下载下来的数据是经过压缩的,视频页面的压缩算法是Gzip,弹幕xml页面的压缩算法是deflate,所以获取页面之后要进行解压,具体的解压方法不再赘述,有需要的可以去看我的工具类库中的EncodingUtils

破解用户哈希


获取完cid之后就是发送获取弹幕xml的请求了,请求本身只有一个参数,加上没有任何加密,所以获取起来非常简单。这是获取到弹幕xml的样例,接下来我们要分析xml中包含的弹幕->发送者的映射信息,这一步才是最难的,首先我们看xml文件中的一个节点



1
<d p="105.70600,1,25,16707842,1545495823,0,79559ff8,9622597929009152">君指先跃动の光は、私の一生不変の信仰に、唯私の超电磁炮永世生き</d>


xml的文本内容自然是弹幕内容不用说,而节点的属性p里面则让人有点摸不着头脑,但是有一点是可以肯定的,弹幕发送者的信息一定在其中,把视角放大到整个文件,可以发现这样的情况


1
2
3
4
5
6
7
8
9
<d p="92.67200,5,25,8700107,1545493502,0,583b3e2,9621380772397056">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="91.96300,5,25,8700107,1545493532,0,583b3e2,9621396751122432">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="91.96300,5,25,15772458,1545493536,0,583b3e2,9621398781689856">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="91.96300,5,25,15138834,1545493540,0,583b3e2,9621400690622464">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="91.96300,5,25,15138834,1545493544,0,583b3e2,9621402883719168">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="91.96300,5,25,9599289,1545493548,0,583b3e2,9621404980346882">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="91.96300,5,25,38979,1545493554,0,583b3e2,9621407805210624">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="95.60900,1,25,8700107,1545493589,0,583b3e2,9621426469339136">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>
<d p="95.60900,1,25,9599289,1545493595,0,583b3e2,9621429415313408">君指先跃动の光は、私の一生不变の信仰に、唯私の超电永世生き</d>


从弹幕内容可以很明显的看出来这些弹幕是一人所发,再看前面的属性内容,583b3e2这串字符从始至终都没有变化,因此大概可以确定这就是代表了用户id的字符串,从形式上看很明显是经过加密的。然而加密算法我并不知道,问大佬之后告诉我是CRC32,CRC32具体是什么请去这里,接下来就是实现了,因为CRC32不可逆,因此通过密码逆向得到明文是不可能的,然而B站的id是顺序的,也就是每注册一个新用户他的id就是上一个用户的id+1,导致我们可以穷举1-n的所有数字并且计算它CRC之后的值,接下来与加密的用户id进行配对,不过首先穷举花费的空间非常大,基本家用机的内存都是吃不消的,我的建议是把这些数据存放到数据库里面随用随查,方便快捷,当然,网上也有大佬们现成的api接口,作为懒癌患者我最终选择了使用api接口,api接口是//biliquery.typcn.com/api/user/hash来源是BiliBili工具箱,获得了暴力破解的表之后,就可以下手写代码了

## 动手写代码

代码本身很简单,只有两个主要的类,一个负责获取弹幕-用户的映射,另一个则是UI类

但是要注意一点,写的时候记得不要事先就把3001条弹幕的发送者全部获取下来再显示在界面上,而是要先显示弹幕,然后监听鼠标的点击事件,点击到之后获取相应弹幕的发送者,这样可以大大的加快查询速度,否则3000条弹幕下来少说也要2分钟

爬虫类

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
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Xml;

// ReSharper disable IdentifierTypo
namespace BiliBiliDanmakuCrawler
{
/// <summary>
/// 后台处理逻辑
/// </summary>
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "StringLiteralTypo")]
public class BiliBiliDanmakuCrawler
{
public static Func<string, string> Api = s => $"https://biliquery.typcn.com/api/user/hash/{s}",
Trend = s => $"https://api.bilibili.com/x/v1/dm/list.so?oid={s}";

public static int Size, Current;

public static string GetDanmakuXml(string aid)
{
var header = new Dictionary<string, string>
{
{
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
}
};
var cid = SendRequest($"https://www.bilibili.com/video/av{aid}", header).DecompressGzipBytes()
.GetString(Encoding.UTF8)
.PatternFirst("\"pages\":\\[\\{\"cid\":(\\d+)", 1);
return SendRequest(Trend(cid), header).DecompressDeflateBytes().GetString(Encoding.UTF8);

}

public static string DecryptHash(string hash)
{
var header = new Dictionary<string, string>
{
{
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
}
};
return SendRequest(Api(hash), header).GetString(Encoding.UTF8).PatternFirst("\"id\":(\\d+)", 1);
}

private static void SetProgressBarMaxValue(RangeBase progressBar, int value) =>
progressBar.Dispatcher.Invoke(() => progressBar.Maximum = value);

private static void UpdateProgressBar(RangeBase progressBar) =>
progressBar.Dispatcher.Invoke(() => progressBar.Value = Current);

private static void UpdateTextBlock(TextBlock textBlock) =>
textBlock.Dispatcher.Invoke(() => { textBlock.Text = $"{Current}/{Size}"; });

public static byte[] SendRequest(string url, Dictionary<string, string> header = null)
{
var clients = new WebClient();
// ReSharper disable once InvertIf
if (header != null)
{
foreach (var property in header)
{
clients.Headers[property.Key] = property.Value;
}
}
return clients.DownloadData(url);
}

public static Dictionary<string, string> AnalyzeDanmaku(string text)
{
var document = new XmlDocument();
var xmlMap = new Dictionary<string, string>();
ThreadPool.SetMinThreads(32, 32);
ThreadPool.SetMinThreads(32, 32);
document.LoadXml(text);
var nodes = document.ChildNodes;
SetProgressBarMaxValue(MainWindow._SearchProgressBar, nodes.Count);
foreach (XmlNode documentNode in nodes)
{
new Task(() =>
{
UpdateProgressBar(MainWindow._SearchProgressBar);
UpdateTextBlock(MainWindow.ProgressTextBlock);
var attr = documentNode.Attributes?["p"].Value;
var hash = attr?.Split(',')[6];
try
{
xmlMap[documentNode.InnerText] = DecryptHash(hash);
}
catch (Exception)
{
//ignored
}
}).Start();
}
return xmlMap;
}

public static Dictionary<string, string> GetPair(string aid)
{
var xml = GetDanmakuXml(aid);
var danmaku = AnalyzeDanmaku(xml);
Console.WriteLine(@"finish");
return danmaku;
}
}
}

UI类

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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Xml;

namespace BiliBiliDanmakuCrawler
{
/// <inheritdoc cref="MainWindow" />
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "InvertIf")]
public partial class MainWindow : Window
{
public static ProgressBar _SearchProgressBar;
public static TextBlock ProgressTextBlock;
private readonly List<User> _danmakuList = new List<User>();

private readonly Dictionary<string, string> _danmakuXml = new Dictionary<string, string>();
private BackgroundWorker _danmakuWorker;
private bool _flag;

public MainWindow()
{
_flag = false;
InitializeComponent();
_SearchProgressBar = SearchProgressBar;
ProgressTextBlock = ProgressBlock;
}

private void Worker_RunWorkerComplete(object sender, RunWorkerCompletedEventArgs e)
{
var userList = new List<User>();
_flag = true;
if (e.Error != null)
{
MessageBox.Show("该aid对应的视频不存在,请确认视频是否被删除及aid是否正确");
GetVideoButton.IsEnabled = true;
return;
}

DanmakuList.Items.Clear();
foreach (var pair in _danmakuXml)
{
var user = new User
{
DanmakuContent = pair.Key.Trim(),
Id = pair.Value
};
userList.Add(user);
_danmakuList.Add(user);
}

DanmakuList.ItemsSource = userList;
GetVideoButton.IsEnabled = true;
}

private void Worker_OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
SearchProgressBar.Value = e.ProgressPercentage;
ProgressBlock.Text = e.UserState.ToString();
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
var xml = BiliBiliDanmakuCrawler.GetDanmakuXml(e.Argument.ToString());

var i = 0;
var document = new XmlDocument();
ThreadPool.SetMinThreads(32, 32);
ThreadPool.SetMinThreads(32, 32);
document.LoadXml(xml);
var nodes = document.GetElementsByTagName("d");
var size = nodes.Count;

foreach (XmlNode documentNode in nodes)
{
var attr = documentNode.Attributes?["p"].Value;
var hash = attr?.Split(',')[6];
try
{
_danmakuXml[documentNode.InnerText] = hash;
}
catch (Exception)
{
//ignored
}

_danmakuWorker.ReportProgress((int) Math.Round((double) ++i / size * 100), $"{i}/{size}");
}
}

public BackgroundWorker MakeInstance()
{
var danmakuWorker = new BackgroundWorker {WorkerReportsProgress = true, WorkerSupportsCancellation = true};
danmakuWorker.DoWork += Worker_DoWork;
danmakuWorker.ProgressChanged += Worker_OnProgressChanged;
danmakuWorker.RunWorkerCompleted += Worker_RunWorkerComplete;
return danmakuWorker;
}

private void GetVideoButton_Click(object sender, RoutedEventArgs e)
{
SearchProgressBar.Value = 0;
SearchProgressBar.Visibility = Visibility.Visible;
ProgressBlock.Visibility = Visibility.Visible;
GetVideoButton.IsEnabled = false;
_danmakuWorker = MakeInstance();
_danmakuWorker.RunWorkerAsync(AidBox.Text);

DanmakuList.Visibility = Visibility.Visible;
DanmakuContentBox.Visibility = Visibility.Visible;
SearchDanmakuButton.Visibility = Visibility.Visible;
}

private async void DanmakuList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!UserNameBox.FontFamily.Equals(new FontFamily("Corbel Light")) || !DanmakuContentBox.FontFamily.Equals(new FontFamily("Corbel Light")))
{
OnGotFocusFontChange(UserNameBox);
OnGotFocusFontChange(DanmakuContentBox);
}
// ReSharper disable once AssignNullToNotNullAttribute
if (DanmakuList.SelectedItem != null)
{
var user = DanmakuList.SelectedItem as User;
var id = "";
await Task.Run(() => id = BiliBiliDanmakuCrawler.DecryptHash(user.Id));
var content = $"https://space.bilibili.com/{id}";
UserNameBox.Text = content;
DanmakuContentBox.Text = user.DanmakuContent;
}
}

private void ViewInExplorerButton_Click(object sender, RoutedEventArgs e)
{
if (!UserNameBox.Text.StartsWith("http") && !UserNameBox.Text.IsNumber())
{
MessageBox.Show("请输入正确的用户空间链接或用户id");
return;
}

Process.Start(UserNameBox.Text.StartsWith("http")
? UserNameBox.Text
: $"http://space.bilibili.com/{UserNameBox.Text}");
}

private async void SearchDanmakuButton_Click(object sender, RoutedEventArgs e)
{
var flag = _danmakuList.Select(t => t.DanmakuContent).Contains(DanmakuContentBox.Text);

if (flag)
{
foreach (var t in _danmakuList)
{
if (t.DanmakuContent == DanmakuContentBox.Text)
{
var id = "";
await Task.Run(() => id = BiliBiliDanmakuCrawler.DecryptHash(t.Id));
UserNameBox.Text = $"http://space.bilibili.com/{id}";
}
}

return;
}

MessageBox.Show("该弹幕不存在");
}

private void MainGrid_OnLoaded(object sender, RoutedEventArgs e)
{
var rand = new Random();
var files = new DirectoryInfo("../../backgrounds").GetFiles();
var bg = files[rand.Next(files.Length)].FullName;
BgImage.Source = new BitmapImage(new Uri(bg));
}

private void ContactButton_Click(object sender, RoutedEventArgs e)
{
Process.Start("mailto: decem0730@gmail.com");
}

private void ViewGithubButton_Click(object sender, RoutedEventArgs e)
{
Process.Start("https://github.com/Rinacm");
}

private static void OnGotFocusFontChange(TextBox box)
{
box.Text = "";
box.Foreground = Brushes.Black;
box.FontFamily = new FontFamily("Corbel Light");
}

private void AidBox_OnGotFocus(object sender, RoutedEventArgs e)
{
OnGotFocusFontChange(AidBox);
}

private void UserNameBox_OnGotFocus(object sender, RoutedEventArgs e)
{
OnGotFocusFontChange(UserNameBox);
}
private void DanmakuContentBox_OnGotFocus(object sender, RoutedEventArgs e)
{
OnGotFocusFontChange(DanmakuContentBox);
}
}
}

UI XAML布局文件

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
<!-- ReSharper disable MarkupAttributeTypo -->
<!-- ReSharper disable IdentifierTypo -->
<Window
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
x:Class="BiliBiliDanmakuCrawler.MainWindow"
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:BiliBiliDanmakuCrawler"
xmlns:av="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=PresentationFramework"
av:TextElement.Foreground="{DynamicResource MaterialDesignBody}"
TextElement.FontWeight="Regular"
TextElement.FontSize="13"
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="Auto"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}" Height="800" Width="1140"
MaxHeight="800" MinHeight="800"
MaxWidth="1140" MinWidth="1140"
Title="BiliBili弹幕获取"
Icon="C:\Users\26532\source\repos\BiliBiliDanmakuCrawler\BiliBiliDanmakuCrawler\69870463_p0.png"
>

<Grid Loaded="MainGrid_OnLoaded">
<av:Grid.ColumnDefinitions>
<av:ColumnDefinition Width="209*"/>
<av:ColumnDefinition Width="923*"/>
</av:Grid.ColumnDefinitions>
<Image x:Name="BgImage" Stretch="UniformToFill" Opacity="0.5" av:Grid.ColumnSpan="2"/>
<materialDesign:Card Opacity="0.7" Margin="42,220,42,0" Height="320" Width="1049" av:Grid.ColumnSpan="2"
Grid.Column="0">
<av:ListView Visibility="Visible" x:Name="DanmakuList" HorizontalAlignment="Left" Height="320" VerticalAlignment="Top" Width="1049" SelectionChanged="DanmakuList_SelectionChanged" Margin="0,0,0,0"
DisplayMemberPath="DanmakuContent" SelectedValuePath="Id">
</av:ListView>
</materialDesign:Card>

<Button Grid.Column="0" x:Name="GetVideoButton" Content="Submit" Height="43" Background="BlueViolet" FontFamily="Comic Sans MS" Click="GetVideoButton_Click" VerticalAlignment="Top" RenderTransformOrigin="0.458,0.8" Margin="42,78,69,0"/>
<av:TextBox x:Name="AidBox" Foreground="Crimson" HorizontalAlignment="Left" Height="39" Margin="41,30,0,0" FontSize="18" TextWrapping="Wrap" Text="video aid here↓" VerticalAlignment="Top" Width="207" FontFamily="Kristen ITC" av:Grid.ColumnSpan="2"
Grid.Column="0" GotFocus="AidBox_OnGotFocus" VerticalContentAlignment="Center">
</av:TextBox>
<av:TextBox x:Name="UserNameBox" Foreground="Crimson" Text="Enter Space Link or User Id" Visibility="Visible" HorizontalAlignment="Left" FontSize="18" Height="40" Margin="42,579,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="315" av:Grid.ColumnSpan="2"
Grid.Column="0" FontFamily="Kristen ITC" GotFocus="UserNameBox_OnGotFocus" VerticalContentAlignment="Center"/>
<av:Button x:Name="SearchDanmakuButton" Background="BlueViolet" Visibility="Visible" Content="Search" HorizontalAlignment="Right" Margin="0,579,41,0" VerticalAlignment="Top" Width="80" Height="40" RenderTransformOrigin="0.225,-0.728" FontFamily="Comic Sans MS" Click="SearchDanmakuButton_Click" av:Grid.Column="1"/>
<av:ProgressBar x:Name="SearchProgressBar" Visibility="Hidden" HorizontalAlignment="Left" Height="10" Margin="42,165,0,0" VerticalAlignment="Top" Width="1048" av:Grid.ColumnSpan="2"
Grid.Column="0" />
<av:TextBlock x:Name="ProgressBlock" TextAlignment="Center" Visibility="Hidden" HorizontalAlignment="Right" TextWrapping="Wrap" VerticalAlignment="Top" FontFamily="DengXian Light" av:Grid.Column="1" Width="70" Margin="0,190,531,0" Height="16"/>
<av:TextBox x:Name="DanmakuContentBox" VerticalContentAlignment="Center" Foreground="Crimson" Text="Enter Danmaku To Search" FontSize="18" Visibility="Visible" HorizontalAlignment="Left" Height="40" Margin="357,579,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="439" av:Grid.Column="1" GotFocus="DanmakuContentBox_OnGotFocus" FontFamily="Kristen ITC"/>
<av:Button x:Name="ViewInExplorerButton" Background="BlueViolet" FontSize="15" Visibility="Visible" Content="View in Explorer" HorizontalAlignment="Left" Margin="153,579,0,0" VerticalAlignment="Top" Width="199" Height="40" RenderTransformOrigin="0.225,-0.728" FontFamily="Comic Sans MS" Click="ViewInExplorerButton_Click" av:Grid.Column="1"/>
<av:TextBlock FontSize="17" HorizontalAlignment="Right" TextWrapping="Wrap" Text="https://github.com/Rinacm" VerticalAlignment="Top" Height="23" FontFamily="Kristen ITC" Margin="0,739,0,0" av:Grid.Column="1"/>
<av:Separator HorizontalAlignment="Left" Height="72" Margin="0,103,0,0" VerticalAlignment="Top" Width="1132" av:Grid.ColumnSpan="2"
Grid.Column="0" />
<av:Button x:Name="ContactButton" Content="Contact Me" Foreground="Black" FontSize="20" HorizontalAlignment="Left" BorderBrush="Black" VerticalAlignment="Top" Width="163" Margin="81,670,0,0" Height="51" FontFamily="Kristen ITC" Click="ContactButton_Click" av:Grid.Column="1">
<av:Button.Background>
<SolidColorBrush Color="White" Opacity="0.3"/>
</av:Button.Background>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.BorderBrush).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Black" To="Crimson"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Background).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="White" To="White"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Foreground).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Black" To="Crimson"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.BorderBrush).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Crimson" To="Black"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Background).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="White" To="White"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Foreground).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Crimson" To="Black"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</av:Button>
<av:Button x:Name="ViewGithubButton" Content="View My Github" Foreground="Black" FontSize="16.5" HorizontalAlignment="Right" BorderBrush="Black" VerticalAlignment="Top" Width="164" Margin="0,670,290,0"
av:Grid.Column="1" Height="51" FontFamily="Kristen ITC" Click="ViewGithubButton_Click">
<av:Button.Background>
<SolidColorBrush Color="White" Opacity="0.3"/>
</av:Button.Background>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.BorderBrush).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Black" To="Aqua"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Background).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="White" To="White"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Foreground).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Black" To="Aqua"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.BorderBrush).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Aqua" To="Black"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Background).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="White" To="White"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Control.Foreground).(SolidColorBrush.Color)"
BeginTime="00:00:00" From="Aqua" To="Black"
Duration="00:00:00.3"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</av:Button>
</Grid>
</Window>

实际效果:

4

全部源码可以在我的Github上面查看
0%