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 程度の気になりポイントしか無かったので、これでヨシ!(指差し確認する現場猫の顔)
あかんやんけ
リンク機能を忘れていました。申し訳ございません。まぁどうせ誰も使わんからこのままでもいいでしょ・・・。