読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

UWPで色付きTextBlockを作ろう

Textプロパティに所定の書式に従った文字列突っ込んだらいい感じに色つけて表示してくれるTextBlockみたいなのが欲しくなることってないでしょうか。

そんな時は、TextBlockを1枚ラップしたカスタムコントロールかユーザーコントロールを作ると捗ります。今回は、実装が簡単なユーザーコントロールを例に作ってみたいと思います。

ColoredTextBlockという名前でユーザーコントロールを作って以下のようにTextBlockを置きます。

<UserControl x:Class="ColoredTextBlock.ColoredTextBlock"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:ColoredTextBlock"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             d:DesignHeight="300"
             d:DesignWidth="400">

    <Grid>
        <TextBlock x:Name="TextBlock" />
    </Grid>
</UserControl>

そして、このユーザーコントロールにText依存関係プロパティを作ります。Textが変更されたときのコールバックでTextをパースしてTextBlockのInlinesを組み立てます。

using System.Reflection;
using System.Xml.Linq;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents;
using Windows.UI.Xaml.Media;

namespace ColoredTextBlock
{
    public sealed partial class ColoredTextBlock : UserControl
    {
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register(
                nameof(Text),
                typeof(string),
                typeof(ColoredTextBlock),
                new PropertyMetadata(null, (s, e) =>
                {
                    ((ColoredTextBlock)s).Parse();
                }));

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        private void Parse()
        {
            // ここで文字列をパースしてInlinesを組み立てる
        }

        public ColoredTextBlock()
        {
            this.InitializeComponent();
        }
    }
}

今回はXML形式でFontタグにColor属性で色を指定できるみたいなのをイメージして以下のようなパース処理を実装しました。

this.TextBlock.Inlines.Clear();
try
{
    var doc = XDocument.Parse(this.Text);
    foreach (var node in doc.Root.Nodes())
    {
        if (node.NodeType == System.Xml.XmlNodeType.Text)
        {
            var textNode = (XText)node;
            this.TextBlock.Inlines.Add(new Run
            {
                Text = textNode.Value
            });
        }
        else if (node.NodeType == System.Xml.XmlNodeType.Element)
        {
            var elm = (XElement)node;
            if (elm.Name != "Font")
            {
                continue;
            }

            var color = elm.Attribute("Color")?.Value ?? "Black";
            var text = elm.Value;

            var property = (Color)(typeof(Colors).GetTypeInfo().GetDeclaredProperty(color)?.GetValue(null) ?? Colors.Black);
            this.TextBlock.Inlines.Add(new Run
            {
                Text = text,
                Foreground = new SolidColorBrush(property)
            });
        }
    }
}
catch { }

以下のようにTextBoxの入力とバインドして使ってみます。

<Page x:Class="ColoredTextBlock.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:ColoredTextBlock"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox x:Name="TextBox"
                 Text="&lt;Doc&gt;&lt;Font Color='Blue'&gt;Hello&lt;/Font&gt; &lt;Font Color='Red'&gt;World&lt;/Font&gt;&lt;/Doc&gt;" />
        <local:ColoredTextBlock Text="{x:Bind TextBox.Text, Mode=OneWay}" />
    </StackPanel>
</Page>

実行すると以下のようになります。

f:id:okazuki:20160227221833p:plain

ソースコード

ソースコードの全体はGitHubに上げています。

github.com