WPF でも、ブログカードを使いたい(一部未実装あり)

はてなさんのブログカード良いですよね!WPF で使う場面があるか分かりませんが、作ってみました!

目次

出来上がりイメージ

見た目的には、こんな感じのレイアウトでそれっぽく見えます。

<Window x:Class="WpfApp3.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:WpfApp3"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    
    <StackPanel>
        
        <!-- BlogCard.Wpf -->
        <Border 
            BorderThickness="1" 
            BorderBrush="LightGray"
            Margin="10"
            Padding="10">

            <Grid>

                <!-- Title, Contents, Footer(link) -->
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                    <RowDefinition Height="Auto"  />
                </Grid.RowDefinitions>

                <!-- Contents, Site Image -->
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>

                <TextBlock 
                    Grid.Row="0" 
                    Grid.Column="0"
                    Text="ホームページ"
                    FontSize="20"
                    FontWeight="Bold"
                    TextWrapping="WrapWithOverflow" />

                <TextBlock 
                    Grid.Row="1" 
                    Grid.Column="0"
                    Text="あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、"
                    TextWrapping="WrapWithOverflow" />

                <Image
                    Grid.Row="0"
                    Grid.Column="1"
                    Grid.RowSpan="2"
                    Source="Images/first_image.png" Width="100" Height="100"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center" />

                <StackPanel
                    Grid.Row="2" 
                    Grid.Column="0"
                    Orientation="Horizontal">
                    <Image Source="Images/favicon.png" Width="20" Height="20" />
                    <TextBlock Text="www.sample.com" Margin="5,0,0,0" />
                </StackPanel>

            </Grid>

        </Border>
        
    </StackPanel>
    
</Window>

画像は、UWP の Assets フォルダにあるものを使っていますが、見た目のイメージが確認できればいいだけですので favicon サイズの画像と、ブログ本文に貼るようなサイズの画像をペイントとかで作成する、でも大丈夫です。

仕様確認

はてなさんのブログカードは、タイトル、本文の開始数行分、本文に貼った最初の画像(あれば)、ドメイン?ホームページのリンク、という構成みたいで、本文以外にマウスオーバーすると、リンクされていることが分かります。クリックすると該当リンク先を表示します。

  • 文章か画像をクリックすると、リンク先が表示される

後は、内部処理として、リンクをもらったら、そのサイトのソースコードを取得して、各項目を取得して表示させる、ですね。

  • 各項目を表示

作りこむ

まずは、1つのコントロールとして扱えるように(=プログラマーは、タグ書いてリンク書くだけで使えるように)します。新しい項目の追加 → カスタムコントロールWPF)を選択して、“BlogCard”と名付けます。

Generic.xaml と BlogCard クラスを以下のように修正します。“各項目の取得”は、具体的には OGP(Open Graph Protocol) を利用します。逆に言うと、OGP 対応していないサイトには未対応です。

OGP 対応したサイトの場合、head タグ 内に以下のような meta タグが記載されています。これから値を取得していきます。

<meta property="og:title" content="もしも、WPF が xaml 形式ではなく yaml 形式で書く仕組みだったら - sutefu7.com"/>
<meta property="og:type" content="article"/>
<meta property="og:url" content="https://sutefu7.hatenablog.com/entry/2019/03/06/184628"/>
<meta property="og:image" content="https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png"/>
<meta property="og:description" content="WPF とか UWP とか xaml 系について、頭の中で、ピコーン!ってなったので書いておきます。xaml って機能で見るとそんなに多くなくても、記述量で見ると長いんですよね。んで、意味はそのまま保持しながら、情報量だけ削減できればいいのにと思ったときに、それ、yaml で書き直せるヤムルー!ってなったので書いてみました。" />
<meta property="og:site_name" content="sutefu7.com"/>

それと、Nuget で AngleSharp をインストールして使っています。

Generic.xaml

<?xml version="1.0" encoding="Shift_JIS"?>
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApp3">


    <Style TargetType="{x:Type local:BlogCard}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:BlogCard}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="LightGray"
                            BorderThickness="1"
                            Margin="10" 
                            Padding="10">

                        <Grid>

                            <!-- Title, Contents, Footer(link) -->
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition />
                                <RowDefinition Height="Auto"  />
                            </Grid.RowDefinitions>

                            <!-- Contents, Site Image -->
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>

                            <TextBlock 
                                Grid.Row="0" 
                                Grid.Column="0"
                                Text="{Binding Title, RelativeSource={RelativeSource AncestorType={x:Type local:BlogCard}}}"
                                FontSize="20"
                                FontWeight="Bold"
                                TextWrapping="WrapWithOverflow" />

                            <TextBlock 
                                Grid.Row="1" 
                                Grid.Column="0"
                                Text="{Binding Contents, RelativeSource={RelativeSource AncestorType={x:Type local:BlogCard}}}"
                                TextWrapping="WrapWithOverflow" />

                            <Image
                                Grid.Row="0"
                                Grid.Column="1"
                                Grid.RowSpan="2"
                                Source="{Binding ContentsImage, RelativeSource={RelativeSource AncestorType={x:Type local:BlogCard}}}" Stretch="Uniform" Height="100"
                                VerticalAlignment="Center"
                                HorizontalAlignment="Center" />

                            <StackPanel
                                Grid.Row="2" 
                                Grid.Column="0"
                                Orientation="Horizontal">
                                <Image Source="{Binding Favicon, RelativeSource={RelativeSource AncestorType={x:Type local:BlogCard}}}" Width="20" Height="20" />
                                <TextBlock Text="{Binding Homepage, RelativeSource={RelativeSource AncestorType={x:Type local:BlogCard}}}" Margin="5,0,0,0" />
                            </StackPanel>

                        </Grid>

                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

BlogCard.cs

using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
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.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp3
{
    /// <summary>
    /// ~長いので省略~
    /// </summary>
    public class BlogCard : Control
    {
        // Href, 外から URL をセットしてもらう用途
        public static readonly DependencyProperty HrefProperty =
            DependencyProperty.Register(nameof(Href), typeof(string), typeof(BlogCard), new PropertyMetadata(string.Empty, HrefChanged));

        public string Href
        {
            get => GetValue(HrefProperty) as string;
            set => SetValue(HrefProperty, value);
        }
        
        // Title, 以降、内部処理用、private でもよかったか?
        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register(nameof(Title), typeof(string), typeof(BlogCard), new PropertyMetadata(string.Empty));

        public string Title
        {
            get => GetValue(TitleProperty) as string;
            set => SetValue(TitleProperty, value);
        }

        // Contents
        public static readonly DependencyProperty ContentsProperty =
            DependencyProperty.Register(nameof(Contents), typeof(string), typeof(BlogCard), new PropertyMetadata(string.Empty));

        public string Contents
        {
            get => GetValue(ContentsProperty) as string;
            set => SetValue(ContentsProperty, value);
        }

        // ContentsImage
        public static readonly DependencyProperty ContentsImageProperty =
            DependencyProperty.Register(nameof(ContentsImage), typeof(ImageSource), typeof(BlogCard), new PropertyMetadata(null));

        public ImageSource ContentsImage
        {
            get => GetValue(ContentsImageProperty) as ImageSource;
            set => SetValue(ContentsImageProperty, value);
        }

        // Favicon
        public static readonly DependencyProperty FaviconProperty =
            DependencyProperty.Register(nameof(Favicon), typeof(ImageSource), typeof(BlogCard), new PropertyMetadata(null));

        public ImageSource Favicon
        {
            get => GetValue(FaviconProperty) as ImageSource;
            set => SetValue(FaviconProperty, value);
        }

        // Homepage
        public static readonly DependencyProperty HomepageProperty =
            DependencyProperty.Register(nameof(Homepage), typeof(string), typeof(BlogCard), new PropertyMetadata(string.Empty));

        public string Homepage
        {
            get => GetValue(HomepageProperty) as string;
            set => SetValue(HomepageProperty, value);
        }
        
        // static constructor
        static BlogCard()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(BlogCard), new FrameworkPropertyMetadata(typeof(BlogCard)));
        }

        private static async void HrefChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var self = d as BlogCard;
            var url = e.NewValue as string;
            var doc = default(IHtmlDocument);
            using (var client = new HttpClient())
            using (var stream = await client.GetStreamAsync(new Uri(url)))
            {
                var parser = new HtmlParser();
                doc = await parser.ParseDocumentAsync(stream);
            }
            
            // 各項目は、OGP(Open Graph Protocol) を利用して取得します。
            
            // タイトル
            var element = doc.QuerySelectorAll(@"meta[property=""og:title""]").FirstOrDefault();
            if (element != null)
            {
                self.Title = element.GetAttribute("content");
            }

            // 説明文
            element = doc.QuerySelectorAll(@"meta[property=""og:description""]").FirstOrDefault();
            if (element != null)
            {
                self.Contents = element.GetAttribute("content");
            }

            // サムネイル画像
            element = doc.QuerySelectorAll(@"meta[property=""og:image""]").FirstOrDefault();
            if (element != null)
            {
                var src = new BitmapImage();
                src.BeginInit();
                src.UriSource = new Uri(element.GetAttribute("content"));
                src.EndInit();
                self.ContentsImage = src;
            }

            // ファビコン
            element = doc.QuerySelectorAll(@"link[rel*=""icon""]").FirstOrDefault();
            if (element != null)
            {
                var src = new BitmapImage();
                src.BeginInit();
                src.UriSource = new Uri(element.GetAttribute("href"));
                src.EndInit();
                self.Favicon = src;
            }

            // ホームページのリンク
            // 判定は適当・・・
            var link = url.Replace("http://", string.Empty).Replace("https://", string.Empty);
            self.Homepage = link.Substring(0, link.IndexOf('/'));
            
        }

    }
}

使う

カスタムコントロールができたので使ってみます!比較用に、構想段階の時のサンプルレイアウトを下に表示させています。

<Window x:Class="WpfApp3.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:WpfApp3"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    
    <StackPanel>

        <local:BlogCard Href="https://sutefu7.hatenablog.com/entry/2019/03/06/184628" />

        <!-- BlogCard.Wpf -->
        <Border 
            BorderThickness="1" 
            BorderBrush="LightGray"
            Margin="10"
            Padding="10">

            <Grid>

                <!-- Title, Contents, Footer(link) -->
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                    <RowDefinition Height="Auto"  />
                </Grid.RowDefinitions>

                <!-- Contents, Site Image -->
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>

                <TextBlock 
                    Grid.Row="0" 
                    Grid.Column="0"
                    Text="ホームページ"
                    FontSize="20"
                    FontWeight="Bold"
                    TextWrapping="WrapWithOverflow" />

                <TextBlock 
                    Grid.Row="1" 
                    Grid.Column="0"
                    Text="あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、あああ、"
                    TextWrapping="WrapWithOverflow" />

                <Image
                    Grid.Row="0"
                    Grid.Column="1"
                    Grid.RowSpan="2"
                    Source="Images/first_image.png" Width="100" Height="100"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center" />

                <StackPanel
                    Grid.Row="2" 
                    Grid.Column="0"
                    Orientation="Horizontal">
                    <Image Source="Images/favicon.png" Width="20" Height="20" />
                    <TextBlock Text="www.sample.com" Margin="5,0,0,0" />
                </StackPanel>

            </Grid>

        </Border>

    </StackPanel>
    
</Window>

画像が潰れているのが気になりますが、どうしたらいいのかよく分かりませんでした;つД`)。まぁそれくらいだし、Margin や Padding 程度の気になりポイントしか無かったので、これでヨシ!(指差し確認する現場猫の顔)

あかんやんけ

リンク機能を忘れていました。申し訳ございません。まぁどうせ誰も使わんからこのままでもいいでしょ・・・。