这两天给我的窗边岛项目实现了 X 岛揭示板网页中的防剧透功能。这个功能本质上就是,当鼠标悬浮在文字上时显示原本的内容,当鼠标移出文字时则用黑块代替。即

X 岛揭示板 窗边岛

虽然说起来很简单,但是好像网上并没有针对这个需求有什么相关的内容,所以在这里记录下我的实现,权当抛砖引玉。

背景

X岛揭示板中,防剧透是通过 [h][/h] 这个自定义标签实现的,所以上面 GIF 图中的文字其实是正常文字--[h]防剧透文字[/h]--正常文字--[h]防剧透文字[/h]--。那么这里要做的就有两件事:解析这个自定义标签,以及在 TextBlock 控件中实现黑块和正常文字的互相替换。

为 TextBlock 对象填充内容

TextBlock 对象有两种填充内容的方式:

  • 直接将内容放入 TextBlock.Text 属性中。这种方式适合不包含防剧透标签的内容。

    1
    2
    3
    4
    5
    6
    7
    8
    new TextBlock
    {
    VerticalAlignment = VerticalAlignment.Top,
    HorizontalAlignment = HorizontalAlignment.Stretch,
    Text = content,
    TextWrapping = TextWrapping.Wrap,
    IsTextSelectionEnabled = textSelectionEnabled,
    };
  • 将内容分散到各个 Run 对象中,并将这些 Run 对象放在 TextBlock.Inlines 属性中。我就是搭配这种方式实现的防剧透功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var run1 = new Run { Text = "Run 1" };
    var run2 = new Run { Text = "Run 2" };

    var textBlock = new TextBlock
    {
    VerticalAlignment = VerticalAlignment.Top,
    HorizontalAlignment = HorizontalAlignment.Stretch,
    TextWrapping = TextWrapping.Wrap,
    IsTextSelectionEnabled = textSelectionEnabled,
    };

    textBlock.Inlines.Add(run1);
    textBlock.Inlines.Add(run2);

保存黑块下的原本内容

因为防剧透本质就是,平时用黑块替换掉要遮挡的内容,仅在鼠标悬浮时再用真正的内容替换掉黑块,所以我们需要一个地方来保存原本的内容。本来我想直接在 Run 对象上下功夫,但是可惜 Run 不像 TextBlock 有一个 DataContext 属性可以放东西,所以最后我还是把目光放在了 TextBlock 上。

TextBlock.DataContext 是一个 object 类型的属性,所以我们可以随意放任何我们想放的东西。

当然为了扩展性考虑,我们最好还是给它创建一个类。

1
2
3
4
5
6
class TextBlockDataContext
{
// key用来放应该被防剧透的Run在Inline里的下标
// value是这个Run实际的内容
public Dictionary<int, string> IndexAndOriginalTextOfHiddenContent = new Dictionary<int, string>();
}

然后我在给一个段落创建 TextBlock 时,就可以把这个 TextBlockDataContext 对象放在 DataContext 属性中备用。

1
2
3
4
5
6
7
8
textBlock = new TextBlock
{
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Stretch,
TextWrapping = TextWrapping.Wrap,
IsTextSelectionEnabled = textSelectionEnabled,
DataContext = new TextBlockDataContext { },
};

解析标签并生成 Run 对象

这部分的思路就是,整行文字会被 [h][/h] 标签切割成各自的 Run,因为 TextBlock.Inlines 是一个有序的列表,所以在切割和生成 Run 对象时,我可以在 TextBlockDataContext.IndexAndOriginalTextOfHiddenContent 中记录下要防剧透的 Run 的下标和它实际的内容。同时,针对要防剧透的 Run,我先用黑块字符填充它的 Text 属性。

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
int indexOfRun = 0;
var totalLength = content.Length;
var enteredHiddenBlock = false;
Run run;
while (content.Length > 0)
{
var indexOfBeginHideMark = content.IndexOf("[h]");
if (!enteredHiddenBlock && indexOfBeginHideMark >= 0)
{
run = new Run
{
Text = content.Substring(0, indexOfBeginHideMark),
};

textBlock.Inlines.Add(run);
content = content.Substring(indexOfBeginHideMark + 3);

enteredHiddenBlock = true;
indexOfRun++;
continue;
}

var indexOfEndHideMark = content.IndexOf("[/h]");
if (indexOfEndHideMark > 0)
{
var text = content.Substring(0, indexOfEndHideMark);
run = new Run
{
Text = new string('█', text.Length),
};

textBlock.Inlines.Add(run);
content = content.Substring(indexOfEndHideMark + 4);
(textBlock.DataContext as TextBlockDataContext).IndexAndOriginalTextOfHiddenContent.Add(indexOfRun, text);

enteredHiddenBlock = false;
indexOfRun++;
continue;
}

run = new Run
{
Text = content,
};
textBlock.Inlines.Add(run);
indexOfRun++;
break;
}

实现鼠标悬浮时显示真实内容

TextBlock 提供了两个事件 PointerEnteredPointerExited,分别对应鼠标指针进入和离开 TextBlock 范围。所以我们就可以给这两个事件分别绑定 UnhidingContent 方法和 HidingContent 方法来实现鼠标悬浮时显示真正内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void HidingContent(object sender, PointerRoutedEventArgs pointerRoutedEventArgs)
{
var textBlock = sender as TextBlock;
if (textBlock.DataContext is TextBlockDataContext dataContext)
{
foreach (var indexAndOriginalText in dataContext.IndexAndOriginalTextOfHiddenContent)
{
var textLength = indexAndOriginalText.Value.Length;
(textBlock.Inlines.ElementAt(indexAndOriginalText.Key) as Run).Text = new string('█', textLength);
}
}
}

private static void UnhidingContent(object sender, PointerRoutedEventArgs pointerRoutedEventArgs)
{
var textBlock = sender as TextBlock;
if (textBlock.DataContext is TextBlockDataContext dataContext)
{
foreach (var indexAndOriginalText in dataContext.IndexAndOriginalTextOfHiddenContent)
{
(textBlock.Inlines.ElementAt(indexAndOriginalText.Key) as Run).Text = indexAndOriginalText.Value;
}
}
}

至此,与 X 岛揭示板网页端类似的防剧透功能就完成实现了。完整的代码可以参考对应的 GitHub commit